mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-28 18:24:17 -05:00
When several files have conflicts, resolving one of them makes it vanish
from the files panel as soon as it is auto-staged, and it only comes back
once the last conflict is resolved and the filter turns off again. By
then it sits among all the other changed files of the merge, so it is
hard to find the ones whose resulting diff you still wanted to check.
So remember which files had conflicts while the conflicted-files filter
is on, and keep showing them once they are resolved. This is the general
solution that 39513d244d called for; that commit only helped for the
case of a single conflicted file.
The consequence is that the selection no longer moves on to the next
conflicted file when one is resolved: it stays on the file you just
resolved, which shows you its diff right away.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
261 lines
7.1 KiB
Go
261 lines
7.1 KiB
Go
package filetree
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/jesseduffield/generics/set"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/common"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type FileTreeDisplayFilter int
|
|
|
|
const (
|
|
DisplayAll FileTreeDisplayFilter = iota
|
|
DisplayStaged
|
|
DisplayUnstaged
|
|
DisplayTracked
|
|
DisplayUntracked
|
|
// this shows files with merge conflicts
|
|
DisplayConflicted
|
|
)
|
|
|
|
type ITree[T any] interface {
|
|
InTreeMode() bool
|
|
ExpandToPath(path string)
|
|
ToggleShowTree()
|
|
GetIndexForPath(path string) (int, bool)
|
|
Len() int
|
|
GetItem(index int) types.HasUrn
|
|
SetTree()
|
|
IsCollapsed(path string) bool
|
|
ToggleCollapsed(path string)
|
|
CollapsedPaths() *CollapsedPaths
|
|
CollapseAll()
|
|
ExpandAll()
|
|
GetVisualDepth(index int) int
|
|
}
|
|
|
|
type IFileTree interface {
|
|
ITree[models.File]
|
|
|
|
FilterFiles(test func(*models.File) bool) []*models.File
|
|
SetStatusFilter(filter FileTreeDisplayFilter)
|
|
RememberConflictedPaths(paths []string)
|
|
ForceShowUntracked() bool
|
|
Get(index int) *FileNode
|
|
GetFile(path string) *models.File
|
|
GetAllItems() []*FileNode
|
|
GetAllFiles() []*models.File
|
|
GetStatusFilter() FileTreeDisplayFilter
|
|
GetRoot() *FileNode
|
|
SetTextFilter(filter string, useFuzzySearch bool)
|
|
GetTextFilter() string
|
|
}
|
|
|
|
type FileTree struct {
|
|
getFiles func() []*models.File
|
|
tree *Node[models.File]
|
|
showTree bool
|
|
common *common.Common
|
|
filter FileTreeDisplayFilter
|
|
// Paths of the files that had conflicts while the current filter has been
|
|
// active. The DisplayConflicted filter keeps showing them after their
|
|
// conflicts have been resolved, so that their diffs can be reviewed while
|
|
// the remaining files are still being worked on.
|
|
conflictedPaths *set.Set[string]
|
|
collapsedPaths *CollapsedPaths
|
|
textFilter string
|
|
useFuzzySearch bool
|
|
}
|
|
|
|
var _ IFileTree = &FileTree{}
|
|
|
|
func NewFileTree(getFiles func() []*models.File, common *common.Common, showTree bool) *FileTree {
|
|
return &FileTree{
|
|
getFiles: getFiles,
|
|
common: common,
|
|
showTree: showTree,
|
|
filter: DisplayAll,
|
|
conflictedPaths: set.New[string](),
|
|
collapsedPaths: NewCollapsedPaths(),
|
|
}
|
|
}
|
|
|
|
func (self *FileTree) InTreeMode() bool {
|
|
return self.showTree
|
|
}
|
|
|
|
func (self *FileTree) ExpandToPath(path string) {
|
|
self.collapsedPaths.ExpandToPath(path)
|
|
}
|
|
|
|
func (self *FileTree) getFilesForDisplay() []*models.File {
|
|
var files []*models.File
|
|
switch self.filter {
|
|
case DisplayAll:
|
|
files = self.getFiles()
|
|
case DisplayStaged:
|
|
files = self.FilterFiles(func(file *models.File) bool { return file.HasStagedChanges })
|
|
case DisplayUnstaged:
|
|
files = self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges })
|
|
case DisplayTracked:
|
|
// untracked but staged files are technically not tracked by git
|
|
// but including such files in the filtered mode helps see what files are getting committed
|
|
files = self.FilterFiles(func(file *models.File) bool { return file.Tracked || file.HasStagedChanges })
|
|
case DisplayUntracked:
|
|
files = self.FilterFiles(func(file *models.File) bool { return !(file.Tracked || file.HasStagedChanges) })
|
|
case DisplayConflicted:
|
|
files = self.FilterFiles(func(file *models.File) bool {
|
|
return file.HasMergeConflicts || self.conflictedPaths.Includes(file.Path)
|
|
})
|
|
default:
|
|
panic(fmt.Sprintf("Unexpected files display filter: %d", self.filter))
|
|
}
|
|
|
|
if self.textFilter != "" {
|
|
files = filterFilesByText(files, self.textFilter, self.useFuzzySearch)
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
func (self *FileTree) ForceShowUntracked() bool {
|
|
return self.filter == DisplayUntracked
|
|
}
|
|
|
|
func (self *FileTree) FilterFiles(test func(*models.File) bool) []*models.File {
|
|
return lo.Filter(self.getFiles(), func(file *models.File, _ int) bool { return test(file) })
|
|
}
|
|
|
|
func (self *FileTree) SetStatusFilter(filter FileTreeDisplayFilter) {
|
|
self.filter = filter
|
|
self.conflictedPaths = set.New[string]()
|
|
self.SetTree()
|
|
}
|
|
|
|
// RememberConflictedPaths records which files have conflicts right now, so that
|
|
// the DisplayConflicted filter keeps showing them once they are resolved.
|
|
func (self *FileTree) RememberConflictedPaths(paths []string) {
|
|
self.conflictedPaths.Add(paths...)
|
|
}
|
|
|
|
func (self *FileTree) ToggleShowTree() {
|
|
self.showTree = !self.showTree
|
|
self.SetTree()
|
|
}
|
|
|
|
func (self *FileTree) Get(index int) *FileNode {
|
|
// need to traverse the tree depth first until we get to the index.
|
|
return NewFileNode(self.tree.GetNodeAtIndex(index+1, self.collapsedPaths)) // ignoring root
|
|
}
|
|
|
|
func (self *FileTree) GetFile(path string) *models.File {
|
|
for _, file := range self.getFiles() {
|
|
if file.Path == path {
|
|
return file
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *FileTree) GetIndexForPath(path string) (int, bool) {
|
|
index, found := self.tree.GetIndexForPath(path, self.collapsedPaths)
|
|
return index - 1, found
|
|
}
|
|
|
|
// note: this gets all items when the filter is taken into consideration. There may
|
|
// be hidden files that aren't included here. Files off the screen however will
|
|
// be included
|
|
func (self *FileTree) GetAllItems() []*FileNode {
|
|
if self.tree == nil {
|
|
return nil
|
|
}
|
|
|
|
// ignoring root
|
|
return lo.Map(self.tree.Flatten(self.collapsedPaths)[1:], func(node *Node[models.File], _ int) *FileNode {
|
|
return NewFileNode(node)
|
|
})
|
|
}
|
|
|
|
func (self *FileTree) Len() int {
|
|
// -1 because we're ignoring the root
|
|
return max(self.tree.Size(self.collapsedPaths)-1, 0)
|
|
}
|
|
|
|
func (self *FileTree) GetItem(index int) types.HasUrn {
|
|
// Unimplemented because we don't yet need to show inlines statuses in commit file views
|
|
return nil
|
|
}
|
|
|
|
func (self *FileTree) GetAllFiles() []*models.File {
|
|
return self.getFiles()
|
|
}
|
|
|
|
func (self *FileTree) SetTree() {
|
|
filesForDisplay := self.getFilesForDisplay()
|
|
guiConfig := self.common.UserConfig().Gui
|
|
showRootItem := guiConfig.ShowRootItemInFileTree
|
|
cmp := NodeSortComparator[models.File](guiConfig.FileTreeSortOrder, guiConfig.FileTreeSortCaseSensitive)
|
|
if self.showTree {
|
|
self.tree = BuildTreeFromFiles(filesForDisplay, showRootItem, cmp)
|
|
} else {
|
|
self.tree = BuildFlatTreeFromFiles(filesForDisplay, showRootItem, cmp)
|
|
}
|
|
}
|
|
|
|
func (self *FileTree) IsCollapsed(path string) bool {
|
|
return self.collapsedPaths.IsCollapsed(path)
|
|
}
|
|
|
|
func (self *FileTree) ToggleCollapsed(path string) {
|
|
self.collapsedPaths.ToggleCollapsed(path)
|
|
}
|
|
|
|
func (self *FileTree) CollapseAll() {
|
|
dirPaths := lo.FilterMap(self.GetAllItems(), func(file *FileNode, index int) (string, bool) {
|
|
return file.path, !file.IsFile()
|
|
})
|
|
|
|
for _, path := range dirPaths {
|
|
self.collapsedPaths.Collapse(path)
|
|
}
|
|
}
|
|
|
|
func (self *FileTree) ExpandAll() {
|
|
self.collapsedPaths.ExpandAll()
|
|
}
|
|
|
|
func (self *FileTree) Tree() *FileNode {
|
|
return NewFileNode(self.tree)
|
|
}
|
|
|
|
func (self *FileTree) GetRoot() *FileNode {
|
|
return NewFileNode(self.tree)
|
|
}
|
|
|
|
func (self *FileTree) CollapsedPaths() *CollapsedPaths {
|
|
return self.collapsedPaths
|
|
}
|
|
|
|
func (self *FileTree) GetVisualDepth(index int) int {
|
|
return self.tree.GetVisualDepthAtIndex(index+1, self.collapsedPaths) // +1 to skip root
|
|
}
|
|
|
|
func (self *FileTree) GetStatusFilter() FileTreeDisplayFilter {
|
|
return self.filter
|
|
}
|
|
|
|
func (self *FileTree) SetTextFilter(filter string, useFuzzySearch bool) {
|
|
self.textFilter = filter
|
|
self.useFuzzySearch = useFuzzySearch
|
|
self.SetTree()
|
|
}
|
|
|
|
func (self *FileTree) GetTextFilter() string {
|
|
return self.textFilter
|
|
}
|