mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-12-04 10:34:55 +02:00
Merge pull request #2075 from jesseduffield/generic-trees
This commit is contained in:
commit
69f4292fe3
@ -165,7 +165,7 @@ func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode)
|
||||
|
||||
// if there is any file that hasn't been fully added we'll fully add everything,
|
||||
// otherwise we'll remove everything
|
||||
adding := node.AnyFile(func(file *models.CommitFile) bool {
|
||||
adding := node.SomeFile(func(file *models.CommitFile) bool {
|
||||
return self.git.Patch.PatchManager.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE
|
||||
})
|
||||
|
||||
@ -203,8 +203,7 @@ func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode)
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) toggleAllForPatch(_ *filetree.CommitFileNode) error {
|
||||
// not a fan of type assertions but this will be fixed very soon thanks to generics
|
||||
root := self.context().CommitFileTreeViewModel.Tree().(*filetree.CommitFileNode)
|
||||
root := self.context().CommitFileTreeViewModel.GetRoot()
|
||||
return self.toggleForPatch(root)
|
||||
}
|
||||
|
||||
|
@ -247,7 +247,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
|
||||
self.mutexes.RefreshingFilesMutex.Lock()
|
||||
defer self.mutexes.RefreshingFilesMutex.Unlock()
|
||||
|
||||
if node.IsLeaf() {
|
||||
if node.IsFile() {
|
||||
file := node.File
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
|
@ -7,10 +7,10 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
root := &FileNode{}
|
||||
func BuildTreeFromFiles(files []*models.File) *Node[models.File] {
|
||||
root := &Node[models.File]{}
|
||||
|
||||
var curr *FileNode
|
||||
var curr *Node[models.File]
|
||||
for _, file := range files {
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
@ -30,7 +30,7 @@ func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
}
|
||||
}
|
||||
|
||||
newChild := &FileNode{
|
||||
newChild := &Node[models.File]{
|
||||
Path: path,
|
||||
File: setFile,
|
||||
}
|
||||
@ -46,17 +46,17 @@ func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] {
|
||||
rootAux := BuildTreeFromCommitFiles(files)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
return &CommitFileNode{Children: sortedFiles}
|
||||
return &Node[models.CommitFile]{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
root := &CommitFileNode{}
|
||||
func BuildTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] {
|
||||
root := &Node[models.CommitFile]{}
|
||||
|
||||
var curr *CommitFileNode
|
||||
var curr *Node[models.CommitFile]
|
||||
for _, file := range files {
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
@ -77,7 +77,7 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
}
|
||||
}
|
||||
|
||||
newChild := &CommitFileNode{
|
||||
newChild := &Node[models.CommitFile]{
|
||||
Path: path,
|
||||
File: setFile,
|
||||
}
|
||||
@ -93,7 +93,7 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromFiles(files []*models.File) *FileNode {
|
||||
func BuildFlatTreeFromFiles(files []*models.File) *Node[models.File] {
|
||||
rootAux := BuildTreeFromFiles(files)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
@ -128,7 +128,7 @@ func BuildFlatTreeFromFiles(files []*models.File) *FileNode {
|
||||
return false
|
||||
})
|
||||
|
||||
return &FileNode{Children: sortedFiles}
|
||||
return &Node[models.File]{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func split(str string) []string {
|
||||
|
@ -11,14 +11,14 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
expected *Node[models.File]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
Children: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -31,12 +31,12 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -60,12 +60,12 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Name: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir1/dir3",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir1/dir3/a"},
|
||||
Path: "dir1/dir3/a",
|
||||
@ -75,7 +75,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir2/dir4",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir2/dir4/b"},
|
||||
Path: "dir2/dir4/b",
|
||||
@ -96,9 +96,9 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
@ -124,11 +124,11 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
// it is a little strange that we're not bubbling up our merge conflict
|
||||
// here but we are technically still in in tree mode and that's the rule
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
@ -159,14 +159,14 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
expected *Node[models.File]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
Children: []*Node[models.File]{},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -179,9 +179,9 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -205,9 +205,9 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -231,9 +231,9 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
@ -273,9 +273,9 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Tracked: true,
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "c1", HasMergeConflicts: true},
|
||||
Path: "c1",
|
||||
@ -318,14 +318,14 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
expected *Node[models.CommitFile]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
Children: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -338,12 +338,12 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -367,12 +367,12 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
Path: "dir1/dir3",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/dir3/a"},
|
||||
Path: "dir1/dir3/a",
|
||||
@ -382,7 +382,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir2/dir4",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/dir4/b"},
|
||||
Path: "dir2/dir4/b",
|
||||
@ -403,9 +403,9 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
@ -432,14 +432,14 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
expected *Node[models.CommitFile]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
Children: []*Node[models.CommitFile]{},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -452,9 +452,9 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -478,9 +478,9 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -504,9 +504,9 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
|
@ -1,166 +1,21 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
import "github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
|
||||
// CommitFileNode wraps a node and provides some commit-file-specific methods for it.
|
||||
type CommitFileNode struct {
|
||||
Children []*CommitFileNode
|
||||
File *models.CommitFile
|
||||
Path string // e.g. '/path/to/mydir'
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
*Node[models.CommitFile]
|
||||
}
|
||||
|
||||
var (
|
||||
_ INode = &CommitFileNode{}
|
||||
_ types.ListItem = &CommitFileNode{}
|
||||
)
|
||||
|
||||
func (s *CommitFileNode) ID() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Description() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
func (s *CommitFileNode) IsNil() bool {
|
||||
return s == nil
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetPath() string {
|
||||
return s.Path
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetChildren() []INode {
|
||||
return slices.Map(s.Children, func(child *CommitFileNode) INode {
|
||||
return child
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) SetChildren(children []INode) {
|
||||
castChildren := slices.Map(children, func(child INode) *CommitFileNode {
|
||||
return child.(*CommitFileNode)
|
||||
})
|
||||
|
||||
s.Children = castChildren
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetCompressionLevel() int {
|
||||
return s.CompressionLevel
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) SetCompressionLevel(level int) {
|
||||
s.CompressionLevel = level
|
||||
}
|
||||
|
||||
// methods utilising generic functions for INodes
|
||||
|
||||
func (s *CommitFileNode) Sort() {
|
||||
sortNode(s)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) ForEachFile(cb func(*models.CommitFile) error) error {
|
||||
return forEachLeaf(s, func(n INode) error {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return cb(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Any(test func(node *CommitFileNode) bool) bool {
|
||||
return any(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Every(test func(node *CommitFileNode) bool) bool {
|
||||
return every(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) EveryFile(test func(file *models.CommitFile) bool) bool {
|
||||
return every(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
|
||||
return castNode.File == nil || test(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (n *CommitFileNode) Flatten(collapsedPaths *CollapsedPaths) []*CommitFileNode {
|
||||
results := flatten(n, collapsedPaths)
|
||||
|
||||
return slices.Map(results, func(result INode) *CommitFileNode {
|
||||
return result.(*CommitFileNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) GetNodeAtIndex(index int, collapsedPaths *CollapsedPaths) *CommitFileNode {
|
||||
func NewCommitFileNode(node *Node[models.CommitFile]) *CommitFileNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := getNodeAtIndex(node, index, collapsedPaths)
|
||||
if result == nil {
|
||||
// not sure how this can be nil: we probably are missing a mutex somewhere
|
||||
return nil
|
||||
}
|
||||
|
||||
return result.(*CommitFileNode)
|
||||
return &CommitFileNode{Node: node}
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) GetIndexForPath(path string, collapsedPaths *CollapsedPaths) (int, bool) {
|
||||
return getIndexForPath(node, path, collapsedPaths)
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) Size(collapsedPaths *CollapsedPaths) int {
|
||||
if node == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size(node, collapsedPaths)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Compress() {
|
||||
// with these functions I try to only have type conversion code on the actual struct,
|
||||
// but comparing interface values to nil is fraught with danger so I'm duplicating
|
||||
// that code here.
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
compressAux(s)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetLeaves() []*CommitFileNode {
|
||||
leaves := getLeaves(s)
|
||||
|
||||
return slices.Map(leaves, func(leaf INode) *CommitFileNode {
|
||||
return leaf.(*CommitFileNode)
|
||||
})
|
||||
}
|
||||
|
||||
// extra methods
|
||||
|
||||
func (s *CommitFileNode) AnyFile(test func(file *models.CommitFile) bool) bool {
|
||||
return s.Any(func(node *CommitFileNode) bool {
|
||||
return node.IsLeaf() && test(node.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) NameAtDepth(depth int) string {
|
||||
splitName := split(s.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
return name
|
||||
// returns the underlying node, without any commit-file-specific methods attached
|
||||
func (self *CommitFileNode) Raw() *Node[models.CommitFile] {
|
||||
return self.Node
|
||||
}
|
||||
|
@ -1,22 +1,24 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ICommitFileTree interface {
|
||||
ITree
|
||||
ITree[models.CommitFile]
|
||||
|
||||
Get(index int) *CommitFileNode
|
||||
GetFile(path string) *models.CommitFile
|
||||
GetAllItems() []*CommitFileNode
|
||||
GetAllFiles() []*models.CommitFile
|
||||
GetRoot() *CommitFileNode
|
||||
}
|
||||
|
||||
type CommitFileTree struct {
|
||||
getFiles func() []*models.CommitFile
|
||||
tree *CommitFileNode
|
||||
tree *Node[models.CommitFile]
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
collapsedPaths *CollapsedPaths
|
||||
@ -44,7 +46,7 @@ func (self *CommitFileTree) ToggleShowTree() {
|
||||
|
||||
func (self *CommitFileTree) Get(index int) *CommitFileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return self.tree.GetNodeAtIndex(index+1, self.collapsedPaths) // ignoring root
|
||||
return NewCommitFileNode(self.tree.GetNodeAtIndex(index+1, self.collapsedPaths)) // ignoring root
|
||||
}
|
||||
|
||||
func (self *CommitFileTree) GetIndexForPath(path string) (int, bool) {
|
||||
@ -57,7 +59,10 @@ func (self *CommitFileTree) GetAllItems() []*CommitFileNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root
|
||||
// ignoring root
|
||||
return slices.Map(self.tree.Flatten(self.collapsedPaths)[1:], func(node *Node[models.CommitFile]) *CommitFileNode {
|
||||
return NewCommitFileNode(node)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *CommitFileTree) Len() int {
|
||||
@ -84,8 +89,8 @@ func (self *CommitFileTree) ToggleCollapsed(path string) {
|
||||
self.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (self *CommitFileTree) Tree() INode {
|
||||
return self.tree
|
||||
func (self *CommitFileTree) GetRoot() *CommitFileNode {
|
||||
return NewCommitFileNode(self.tree)
|
||||
}
|
||||
|
||||
func (self *CommitFileTree) CollapsedPaths() *CollapsedPaths {
|
||||
|
@ -1,199 +1,47 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
import "github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
|
||||
// FileNode wraps a node and provides some file-specific methods for it.
|
||||
type FileNode struct {
|
||||
Children []*FileNode
|
||||
File *models.File
|
||||
Path string // e.g. '/path/to/mydir'
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
*Node[models.File]
|
||||
}
|
||||
|
||||
var (
|
||||
_ INode = &FileNode{}
|
||||
_ types.ListItem = &FileNode{}
|
||||
)
|
||||
var _ models.IFile = &FileNode{}
|
||||
|
||||
func (s *FileNode) ID() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
func (s *FileNode) Description() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
// interfaces values whose concrete value is nil are not themselves nil
|
||||
// hence the existence of this method
|
||||
func (s *FileNode) IsNil() bool {
|
||||
return s == nil
|
||||
}
|
||||
|
||||
func (s *FileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
|
||||
func (s *FileNode) GetPath() string {
|
||||
return s.Path
|
||||
}
|
||||
|
||||
func (s *FileNode) GetPreviousPath() string {
|
||||
if s.File != nil {
|
||||
return s.File.GetPreviousPath()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *FileNode) GetChildren() []INode {
|
||||
return slices.Map(s.Children, func(child *FileNode) INode {
|
||||
return child
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) SetChildren(children []INode) {
|
||||
castChildren := slices.Map(children, func(child INode) *FileNode {
|
||||
return child.(*FileNode)
|
||||
})
|
||||
|
||||
s.Children = castChildren
|
||||
}
|
||||
|
||||
func (s *FileNode) GetCompressionLevel() int {
|
||||
return s.CompressionLevel
|
||||
}
|
||||
|
||||
func (s *FileNode) SetCompressionLevel(level int) {
|
||||
s.CompressionLevel = level
|
||||
}
|
||||
|
||||
// methods utilising generic functions for INodes
|
||||
|
||||
func (s *FileNode) Sort() {
|
||||
sortNode(s)
|
||||
}
|
||||
|
||||
func (s *FileNode) ForEachFile(cb func(*models.File) error) error {
|
||||
return forEachLeaf(s, func(n INode) error {
|
||||
castNode := n.(*FileNode)
|
||||
return cb(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) Any(test func(node *FileNode) bool) bool {
|
||||
return any(s, func(n INode) bool {
|
||||
castNode := n.(*FileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (n *FileNode) Flatten(collapsedPaths *CollapsedPaths) []*FileNode {
|
||||
results := flatten(n, collapsedPaths)
|
||||
return slices.Map(results, func(result INode) *FileNode {
|
||||
return result.(*FileNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (node *FileNode) GetNodeAtIndex(index int, collapsedPaths *CollapsedPaths) *FileNode {
|
||||
func NewFileNode(node *Node[models.File]) *FileNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := getNodeAtIndex(node, index, collapsedPaths)
|
||||
if result == nil {
|
||||
// not sure how this can be nil: we probably are missing a mutex somewhere
|
||||
return nil
|
||||
return &FileNode{Node: node}
|
||||
}
|
||||
|
||||
// returns the underlying node, without any file-specific methods attached
|
||||
func (self *FileNode) Raw() *Node[models.File] {
|
||||
return self.Node
|
||||
}
|
||||
|
||||
func (self *FileNode) GetHasUnstagedChanges() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.HasUnstagedChanges })
|
||||
}
|
||||
|
||||
func (self *FileNode) GetHasStagedChanges() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.HasStagedChanges })
|
||||
}
|
||||
|
||||
func (self *FileNode) GetHasInlineMergeConflicts() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.HasInlineMergeConflicts })
|
||||
}
|
||||
|
||||
func (self *FileNode) GetIsTracked() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.Tracked })
|
||||
}
|
||||
|
||||
func (self *FileNode) GetPreviousPath() string {
|
||||
if self.File == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return result.(*FileNode)
|
||||
}
|
||||
|
||||
func (node *FileNode) GetIndexForPath(path string, collapsedPaths *CollapsedPaths) (int, bool) {
|
||||
return getIndexForPath(node, path, collapsedPaths)
|
||||
}
|
||||
|
||||
func (node *FileNode) Size(collapsedPaths *CollapsedPaths) int {
|
||||
if node == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size(node, collapsedPaths)
|
||||
}
|
||||
|
||||
func (s *FileNode) Compress() {
|
||||
// with these functions I try to only have type conversion code on the actual struct,
|
||||
// but comparing interface values to nil is fraught with danger so I'm duplicating
|
||||
// that code here.
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
compressAux(s)
|
||||
}
|
||||
|
||||
func (node *FileNode) GetFilePathsMatching(test func(*models.File) bool) []string {
|
||||
return getPathsMatching(node, func(n INode) bool {
|
||||
castNode := n.(*FileNode)
|
||||
if castNode.File == nil {
|
||||
return false
|
||||
}
|
||||
return test(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) GetLeaves() []*FileNode {
|
||||
leaves := getLeaves(s)
|
||||
|
||||
return slices.Map(leaves, func(leaf INode) *FileNode {
|
||||
return leaf.(*FileNode)
|
||||
})
|
||||
}
|
||||
|
||||
// extra methods
|
||||
|
||||
func (s *FileNode) GetHasUnstagedChanges() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasUnstagedChanges })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetHasStagedChanges() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasStagedChanges })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetHasInlineMergeConflicts() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasInlineMergeConflicts })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetIsTracked() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.Tracked })
|
||||
}
|
||||
|
||||
func (s *FileNode) AnyFile(test func(file *models.File) bool) bool {
|
||||
return s.Any(func(node *FileNode) bool {
|
||||
return node.IsLeaf() && test(node.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) NameAtDepth(depth int) string {
|
||||
splitName := split(s.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
if s.File != nil && s.File.IsRename() {
|
||||
splitPrevName := split(s.File.PreviousName)
|
||||
|
||||
prevName := s.File.PreviousName
|
||||
// if the file has just been renamed inside the same directory, we can shave off
|
||||
// the prefix for the previous path too. Otherwise we'll keep it unchanged
|
||||
sameParentDir := len(splitName) == len(splitPrevName) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
|
||||
if sameParentDir {
|
||||
prevName = join(splitPrevName[depth:])
|
||||
}
|
||||
|
||||
return prevName + " → " + name
|
||||
}
|
||||
|
||||
return name
|
||||
return self.File.PreviousName
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
func TestCompress(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *FileNode
|
||||
expected *FileNode
|
||||
root *Node[models.File]
|
||||
expected *Node[models.File]
|
||||
}{
|
||||
{
|
||||
name: "nil node",
|
||||
@ -20,27 +20,27 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "leaf node",
|
||||
root: &FileNode{
|
||||
root: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
root: &FileNode{
|
||||
root: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file2",
|
||||
@ -49,7 +49,7 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/file3",
|
||||
@ -62,10 +62,10 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir3",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir3/dir3-1",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir3/dir3-1/file5",
|
||||
@ -80,12 +80,12 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file2",
|
||||
@ -94,7 +94,7 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/file3",
|
||||
@ -108,7 +108,7 @@ func TestCompress(t *testing.T) {
|
||||
{
|
||||
Path: "dir3/dir3-1",
|
||||
CompressionLevel: 1,
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir3/dir3-1/file5",
|
||||
|
@ -18,7 +18,7 @@ const (
|
||||
DisplayConflicted
|
||||
)
|
||||
|
||||
type ITree interface {
|
||||
type ITree[T any] interface {
|
||||
InTreeMode() bool
|
||||
ExpandToPath(path string)
|
||||
ToggleShowTree()
|
||||
@ -27,12 +27,11 @@ type ITree interface {
|
||||
SetTree()
|
||||
IsCollapsed(path string) bool
|
||||
ToggleCollapsed(path string)
|
||||
Tree() INode
|
||||
CollapsedPaths() *CollapsedPaths
|
||||
}
|
||||
|
||||
type IFileTree interface {
|
||||
ITree
|
||||
ITree[models.File]
|
||||
|
||||
FilterFiles(test func(*models.File) bool) []*models.File
|
||||
SetFilter(filter FileTreeDisplayFilter)
|
||||
@ -46,13 +45,15 @@ type IFileTree interface {
|
||||
|
||||
type FileTree struct {
|
||||
getFiles func() []*models.File
|
||||
tree *FileNode
|
||||
tree *Node[models.File]
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
filter FileTreeDisplayFilter
|
||||
collapsedPaths *CollapsedPaths
|
||||
}
|
||||
|
||||
var _ IFileTree = &FileTree{}
|
||||
|
||||
func NewFileTree(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTree {
|
||||
return &FileTree{
|
||||
getFiles: getFiles,
|
||||
@ -102,7 +103,7 @@ func (self *FileTree) ToggleShowTree() {
|
||||
|
||||
func (self *FileTree) Get(index int) *FileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return self.tree.GetNodeAtIndex(index+1, self.collapsedPaths) // ignoring root
|
||||
return NewFileNode(self.tree.GetNodeAtIndex(index+1, self.collapsedPaths)) // ignoring root
|
||||
}
|
||||
|
||||
func (self *FileTree) GetFile(path string) *models.File {
|
||||
@ -128,7 +129,10 @@ func (self *FileTree) GetAllItems() []*FileNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root
|
||||
// ignoring root
|
||||
return slices.Map(self.tree.Flatten(self.collapsedPaths)[1:], func(node *Node[models.File]) *FileNode {
|
||||
return NewFileNode(node)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FileTree) Len() int {
|
||||
@ -156,12 +160,12 @@ func (self *FileTree) ToggleCollapsed(path string) {
|
||||
self.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (self *FileTree) Tree() INode {
|
||||
return self.tree
|
||||
func (self *FileTree) Tree() *FileNode {
|
||||
return NewFileNode(self.tree)
|
||||
}
|
||||
|
||||
func (self *FileTree) GetRoot() *FileNode {
|
||||
return self.tree
|
||||
return NewFileNode(self.tree)
|
||||
}
|
||||
|
||||
func (self *FileTree) CollapsedPaths() *CollapsedPaths {
|
||||
|
@ -1,206 +0,0 @@
|
||||
package filetree
|
||||
|
||||
import "github.com/jesseduffield/generics/slices"
|
||||
|
||||
type INode interface {
|
||||
IsNil() bool
|
||||
IsLeaf() bool
|
||||
GetPath() string
|
||||
GetChildren() []INode
|
||||
SetChildren([]INode)
|
||||
GetCompressionLevel() int
|
||||
SetCompressionLevel(int)
|
||||
}
|
||||
|
||||
func sortNode(node INode) {
|
||||
sortChildren(node)
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
sortNode(child)
|
||||
}
|
||||
}
|
||||
|
||||
func sortChildren(node INode) {
|
||||
if node.IsLeaf() {
|
||||
return
|
||||
}
|
||||
|
||||
sortedChildren := slices.Clone(node.GetChildren())
|
||||
|
||||
slices.SortFunc(sortedChildren, func(a, b INode) bool {
|
||||
if !a.IsLeaf() && b.IsLeaf() {
|
||||
return true
|
||||
}
|
||||
if a.IsLeaf() && !b.IsLeaf() {
|
||||
return false
|
||||
}
|
||||
|
||||
return a.GetPath() < b.GetPath()
|
||||
})
|
||||
|
||||
// TODO: think about making this in-place
|
||||
node.SetChildren(sortedChildren)
|
||||
}
|
||||
|
||||
func forEachLeaf(node INode, cb func(INode) error) error {
|
||||
if node.IsLeaf() {
|
||||
if err := cb(node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if err := forEachLeaf(child, cb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func any(node INode, test func(INode) bool) bool {
|
||||
if test(node) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if any(child, test) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func every(node INode, test func(INode) bool) bool {
|
||||
if !test(node) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if !every(child, test) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func flatten(node INode, collapsedPaths *CollapsedPaths) []INode {
|
||||
result := []INode{}
|
||||
result = append(result, node)
|
||||
|
||||
if !collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
for _, child := range node.GetChildren() {
|
||||
result = append(result, flatten(child, collapsedPaths)...)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getNodeAtIndex(node INode, index int, collapsedPaths *CollapsedPaths) INode {
|
||||
foundNode, _ := getNodeAtIndexAux(node, index, collapsedPaths)
|
||||
|
||||
return foundNode
|
||||
}
|
||||
|
||||
func getNodeAtIndexAux(node INode, index int, collapsedPaths *CollapsedPaths) (INode, int) {
|
||||
offset := 1
|
||||
|
||||
if index == 0 {
|
||||
return node, offset
|
||||
}
|
||||
|
||||
if !collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
for _, child := range node.GetChildren() {
|
||||
foundNode, offsetChange := getNodeAtIndexAux(child, index-offset, collapsedPaths)
|
||||
offset += offsetChange
|
||||
if foundNode != nil {
|
||||
return foundNode, offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, offset
|
||||
}
|
||||
|
||||
func getIndexForPath(node INode, path string, collapsedPaths *CollapsedPaths) (int, bool) {
|
||||
offset := 0
|
||||
|
||||
if node.GetPath() == path {
|
||||
return offset, true
|
||||
}
|
||||
|
||||
if !collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
for _, child := range node.GetChildren() {
|
||||
offsetChange, found := getIndexForPath(child, path, collapsedPaths)
|
||||
offset += offsetChange + 1
|
||||
if found {
|
||||
return offset, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return offset, false
|
||||
}
|
||||
|
||||
func size(node INode, collapsedPaths *CollapsedPaths) int {
|
||||
output := 1
|
||||
|
||||
if !collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
for _, child := range node.GetChildren() {
|
||||
output += size(child, collapsedPaths)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func compressAux(node INode) INode {
|
||||
if node.IsLeaf() {
|
||||
return node
|
||||
}
|
||||
|
||||
children := node.GetChildren()
|
||||
for i := range children {
|
||||
grandchildren := children[i].GetChildren()
|
||||
for len(grandchildren) == 1 && !grandchildren[0].IsLeaf() {
|
||||
grandchildren[0].SetCompressionLevel(children[i].GetCompressionLevel() + 1)
|
||||
children[i] = grandchildren[0]
|
||||
grandchildren = children[i].GetChildren()
|
||||
}
|
||||
}
|
||||
|
||||
for i := range children {
|
||||
children[i] = compressAux(children[i])
|
||||
}
|
||||
|
||||
node.SetChildren(children)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func getPathsMatching(node INode, test func(INode) bool) []string {
|
||||
paths := []string{}
|
||||
|
||||
if test(node) {
|
||||
paths = append(paths, node.GetPath())
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
paths = append(paths, getPathsMatching(child, test)...)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func getLeaves(node INode) []INode {
|
||||
if node.IsLeaf() {
|
||||
return []INode{node}
|
||||
}
|
||||
|
||||
return slices.FlatMap(node.GetChildren(), func(child INode) []INode {
|
||||
return getLeaves(child)
|
||||
})
|
||||
}
|
301
pkg/gui/filetree/node.go
Normal file
301
pkg/gui/filetree/node.go
Normal file
@ -0,0 +1,301 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// Represents a file or directory in a file tree.
|
||||
type Node[T any] struct {
|
||||
// File will be nil if the node is a directory.
|
||||
File *T
|
||||
|
||||
// If the node is a directory, Children contains the contents of the directory,
|
||||
// otherwise it's nil.
|
||||
Children []*Node[T]
|
||||
|
||||
// path of the file/directory
|
||||
Path string
|
||||
|
||||
// rather than render a tree as:
|
||||
// a/
|
||||
// b/
|
||||
// file.blah
|
||||
//
|
||||
// we instead render it as:
|
||||
// a/b/
|
||||
// file.blah
|
||||
// This saves vertical space. The CompressionLevel of a node is equal to the
|
||||
// number of times a 'compression' like the above has happened, where two
|
||||
// nodes are squished into one.
|
||||
CompressionLevel int
|
||||
}
|
||||
|
||||
var _ types.ListItem = &Node[models.File]{}
|
||||
|
||||
func (self *Node[T]) IsFile() bool {
|
||||
return self.File != nil
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetPath() string {
|
||||
return self.Path
|
||||
}
|
||||
|
||||
func (self *Node[T]) Sort() {
|
||||
self.SortChildren()
|
||||
|
||||
for _, child := range self.Children {
|
||||
child.Sort()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Node[T]) ForEachFile(cb func(*T) error) error {
|
||||
if self.IsFile() {
|
||||
if err := cb(self.File); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, child := range self.Children {
|
||||
if err := child.ForEachFile(cb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Node[T]) SortChildren() {
|
||||
if self.IsFile() {
|
||||
return
|
||||
}
|
||||
|
||||
children := slices.Clone(self.Children)
|
||||
|
||||
slices.SortFunc(children, func(a, b *Node[T]) bool {
|
||||
if !a.IsFile() && b.IsFile() {
|
||||
return true
|
||||
}
|
||||
if a.IsFile() && !b.IsFile() {
|
||||
return false
|
||||
}
|
||||
|
||||
return a.GetPath() < b.GetPath()
|
||||
})
|
||||
|
||||
// TODO: think about making this in-place
|
||||
self.Children = children
|
||||
}
|
||||
|
||||
func (self *Node[T]) Some(test func(*Node[T]) bool) bool {
|
||||
if test(self) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, child := range self.Children {
|
||||
if child.Some(test) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *Node[T]) SomeFile(test func(*T) bool) bool {
|
||||
if self.IsFile() {
|
||||
if test(self.File) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
for _, child := range self.Children {
|
||||
if child.SomeFile(test) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *Node[T]) Every(test func(*Node[T]) bool) bool {
|
||||
if !test(self) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, child := range self.Children {
|
||||
if !child.Every(test) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *Node[T]) EveryFile(test func(*T) bool) bool {
|
||||
if self.IsFile() {
|
||||
if !test(self.File) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
for _, child := range self.Children {
|
||||
if !child.EveryFile(test) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *Node[T]) Flatten(collapsedPaths *CollapsedPaths) []*Node[T] {
|
||||
result := []*Node[T]{self}
|
||||
|
||||
if len(self.Children) > 0 && !collapsedPaths.IsCollapsed(self.GetPath()) {
|
||||
result = append(result, slices.FlatMap(self.Children, func(child *Node[T]) []*Node[T] {
|
||||
return child.Flatten(collapsedPaths)
|
||||
})...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetNodeAtIndex(index int, collapsedPaths *CollapsedPaths) *Node[T] {
|
||||
if self == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
node, _ := self.getNodeAtIndexAux(index, collapsedPaths)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (self *Node[T]) getNodeAtIndexAux(index int, collapsedPaths *CollapsedPaths) (*Node[T], int) {
|
||||
offset := 1
|
||||
|
||||
if index == 0 {
|
||||
return self, offset
|
||||
}
|
||||
|
||||
if !collapsedPaths.IsCollapsed(self.GetPath()) {
|
||||
for _, child := range self.Children {
|
||||
foundNode, offsetChange := child.getNodeAtIndexAux(index-offset, collapsedPaths)
|
||||
offset += offsetChange
|
||||
if foundNode != nil {
|
||||
return foundNode, offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, offset
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetIndexForPath(path string, collapsedPaths *CollapsedPaths) (int, bool) {
|
||||
offset := 0
|
||||
|
||||
if self.GetPath() == path {
|
||||
return offset, true
|
||||
}
|
||||
|
||||
if !collapsedPaths.IsCollapsed(self.GetPath()) {
|
||||
for _, child := range self.Children {
|
||||
offsetChange, found := child.GetIndexForPath(path, collapsedPaths)
|
||||
offset += offsetChange + 1
|
||||
if found {
|
||||
return offset, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return offset, false
|
||||
}
|
||||
|
||||
func (self *Node[T]) Size(collapsedPaths *CollapsedPaths) int {
|
||||
if self == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
output := 1
|
||||
|
||||
if !collapsedPaths.IsCollapsed(self.GetPath()) {
|
||||
for _, child := range self.Children {
|
||||
output += child.Size(collapsedPaths)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (self *Node[T]) Compress() {
|
||||
if self == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.compressAux()
|
||||
}
|
||||
|
||||
func (self *Node[T]) compressAux() *Node[T] {
|
||||
if self.IsFile() {
|
||||
return self
|
||||
}
|
||||
|
||||
children := self.Children
|
||||
for i := range children {
|
||||
grandchildren := children[i].Children
|
||||
for len(grandchildren) == 1 && !grandchildren[0].IsFile() {
|
||||
grandchildren[0].CompressionLevel = children[i].CompressionLevel + 1
|
||||
children[i] = grandchildren[0]
|
||||
grandchildren = children[i].Children
|
||||
}
|
||||
}
|
||||
|
||||
for i := range children {
|
||||
children[i] = children[i].compressAux()
|
||||
}
|
||||
|
||||
self.Children = children
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetPathsMatching(test func(*Node[T]) bool) []string {
|
||||
paths := []string{}
|
||||
|
||||
if test(self) {
|
||||
paths = append(paths, self.GetPath())
|
||||
}
|
||||
|
||||
for _, child := range self.Children {
|
||||
paths = append(paths, child.GetPathsMatching(test)...)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetFilePathsMatching(test func(*T) bool) []string {
|
||||
matchingFileNodes := slices.Filter(self.GetLeaves(), func(node *Node[T]) bool {
|
||||
return test(node.File)
|
||||
})
|
||||
|
||||
return slices.Map(matchingFileNodes, func(node *Node[T]) string {
|
||||
return node.GetPath()
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetLeaves() []*Node[T] {
|
||||
if self.IsFile() {
|
||||
return []*Node[T]{self}
|
||||
}
|
||||
|
||||
return slices.FlatMap(self.Children, func(child *Node[T]) []*Node[T] {
|
||||
return child.GetLeaves()
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Node[T]) ID() string {
|
||||
return self.GetPath()
|
||||
}
|
||||
|
||||
func (self *Node[T]) Description() string {
|
||||
return self.GetPath()
|
||||
}
|
@ -30,9 +30,10 @@ func RenderFileTree(
|
||||
diffName string,
|
||||
submoduleConfigs []*models.SubmoduleConfig,
|
||||
) []string {
|
||||
return renderAux(tree.Tree(), tree.CollapsedPaths(), "", -1, func(n filetree.INode, depth int) string {
|
||||
castN := n.(*filetree.FileNode)
|
||||
return getFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File)
|
||||
return renderAux(tree.GetRoot().Raw(), tree.CollapsedPaths(), "", -1, func(node *filetree.Node[models.File], depth int) string {
|
||||
fileNode := filetree.NewFileNode(node)
|
||||
|
||||
return getFileLine(fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), fileNameAtDepth(node, depth), diffName, submoduleConfigs, node.File)
|
||||
})
|
||||
}
|
||||
|
||||
@ -41,19 +42,17 @@ func RenderCommitFileTree(
|
||||
diffName string,
|
||||
patchManager *patch.PatchManager,
|
||||
) []string {
|
||||
return renderAux(tree.Tree(), tree.CollapsedPaths(), "", -1, func(n filetree.INode, depth int) string {
|
||||
castN := n.(*filetree.CommitFileNode)
|
||||
|
||||
return renderAux(tree.GetRoot().Raw(), tree.CollapsedPaths(), "", -1, func(node *filetree.Node[models.CommitFile], depth int) string {
|
||||
// This is a little convoluted because we're dealing with either a leaf or a non-leaf.
|
||||
// But this code actually applies to both. If it's a leaf, the status will just
|
||||
// be whatever status it is, but if it's a non-leaf it will determine its status
|
||||
// based on the leaves of that subtree
|
||||
var status patch.PatchStatus
|
||||
if castN.EveryFile(func(file *models.CommitFile) bool {
|
||||
if node.EveryFile(func(file *models.CommitFile) bool {
|
||||
return patchManager.GetFileStatus(file.Name, tree.GetRef().RefName()) == patch.WHOLE
|
||||
}) {
|
||||
status = patch.WHOLE
|
||||
} else if castN.EveryFile(func(file *models.CommitFile) bool {
|
||||
} else if node.EveryFile(func(file *models.CommitFile) bool {
|
||||
return patchManager.GetFileStatus(file.Name, tree.GetRef().RefName()) == patch.UNSELECTED
|
||||
}) {
|
||||
status = patch.UNSELECTED
|
||||
@ -61,37 +60,37 @@ func RenderCommitFileTree(
|
||||
status = patch.PART
|
||||
}
|
||||
|
||||
return getCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status)
|
||||
return getCommitFileLine(commitFileNameAtDepth(node, depth), diffName, node.File, status)
|
||||
})
|
||||
}
|
||||
|
||||
func renderAux(
|
||||
s filetree.INode,
|
||||
func renderAux[T any](
|
||||
node *filetree.Node[T],
|
||||
collapsedPaths *filetree.CollapsedPaths,
|
||||
prefix string,
|
||||
depth int,
|
||||
renderLine func(filetree.INode, int) string,
|
||||
renderLine func(*filetree.Node[T], int) string,
|
||||
) []string {
|
||||
if s == nil || s.IsNil() {
|
||||
if node == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
isRoot := depth == -1
|
||||
|
||||
if s.IsLeaf() {
|
||||
if node.IsFile() {
|
||||
if isRoot {
|
||||
return []string{}
|
||||
}
|
||||
return []string{prefix + renderLine(s, depth)}
|
||||
return []string{prefix + renderLine(node, depth)}
|
||||
}
|
||||
|
||||
if collapsedPaths.IsCollapsed(s.GetPath()) {
|
||||
return []string{prefix + COLLAPSED_ARROW + " " + renderLine(s, depth)}
|
||||
if collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
return []string{prefix + COLLAPSED_ARROW + " " + renderLine(node, depth)}
|
||||
}
|
||||
|
||||
arr := []string{}
|
||||
if !isRoot {
|
||||
arr = append(arr, prefix+EXPANDED_ARROW+" "+renderLine(s, depth))
|
||||
arr = append(arr, prefix+EXPANDED_ARROW+" "+renderLine(node, depth))
|
||||
}
|
||||
|
||||
newPrefix := prefix
|
||||
@ -101,8 +100,8 @@ func renderAux(
|
||||
newPrefix = strings.TrimSuffix(prefix, INNER_ITEM) + NESTED
|
||||
}
|
||||
|
||||
for i, child := range s.GetChildren() {
|
||||
isLast := i == len(s.GetChildren())-1
|
||||
for i, child := range node.Children {
|
||||
isLast := i == len(node.Children)-1
|
||||
|
||||
var childPrefix string
|
||||
if isRoot {
|
||||
@ -113,7 +112,7 @@ func renderAux(
|
||||
childPrefix = newPrefix + INNER_ITEM
|
||||
}
|
||||
|
||||
arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+s.GetCompressionLevel(), renderLine)...)
|
||||
arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+node.CompressionLevel, renderLine)...)
|
||||
}
|
||||
|
||||
return arr
|
||||
@ -220,3 +219,39 @@ func getColorForChangeStatus(changeStatus string) style.TextStyle {
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
func fileNameAtDepth(node *filetree.Node[models.File], depth int) string {
|
||||
splitName := split(node.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
if node.File != nil && node.File.IsRename() {
|
||||
splitPrevName := split(node.File.PreviousName)
|
||||
|
||||
prevName := node.File.PreviousName
|
||||
// if the file has just been renamed inside the same directory, we can shave off
|
||||
// the prefix for the previous path too. Otherwise we'll keep it unchanged
|
||||
sameParentDir := len(splitName) == len(splitPrevName) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
|
||||
if sameParentDir {
|
||||
prevName = join(splitPrevName[depth:])
|
||||
}
|
||||
|
||||
return prevName + " → " + name
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func commitFileNameAtDepth(node *filetree.Node[models.CommitFile], depth int) string {
|
||||
splitName := split(node.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func split(str string) []string {
|
||||
return strings.Split(str, "/")
|
||||
}
|
||||
|
||||
func join(strs []string) string {
|
||||
return strings.Join(strs, "/")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user