jesseduffield.lazygit/pkg/gui/filetree/file_tree.go
Stefan Haller b8d23dd5a5 Make file sort order and case sensitivity configurable
Set the sort order's default from the former foldersFirst to mixed, so this is a
change in behavior. I find this useful because it now matches git's order, so if
you look at the diff of a commit, the TOC at the top has the same order as the
file tree you see when entering the commit.
2026-03-30 17:53:25 +02:00

244 lines
6.4 KiB
Go

package filetree
import (
"fmt"
"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)
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
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,
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 })
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.SetTree()
}
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
}