diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2022-01-22 00:13:51 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2022-01-22 10:48:51 +1100 |
commit | 5b7dd9e43ccd2b82f07f0f0ff0ee8d54d0ecba11 (patch) | |
tree | 2afa1c595dad6ae8c0503217eac3f40e2a925924 /pkg/gui/filetree | |
parent | 4ab5e5413944a92699a9540148666f0de26aa44b (diff) |
properly resolve cyclic dependency
Diffstat (limited to 'pkg/gui/filetree')
-rw-r--r-- | pkg/gui/filetree/README.md | 22 | ||||
-rw-r--r-- | pkg/gui/filetree/commit_file_manager.go | 118 | ||||
-rw-r--r-- | pkg/gui/filetree/commit_file_node.go | 13 | ||||
-rw-r--r-- | pkg/gui/filetree/commit_file_tree_view_model.go | 101 | ||||
-rw-r--r-- | pkg/gui/filetree/constants.go | 9 | ||||
-rw-r--r-- | pkg/gui/filetree/file_manager.go | 140 | ||||
-rw-r--r-- | pkg/gui/filetree/file_manager_test.go | 156 | ||||
-rw-r--r-- | pkg/gui/filetree/file_node.go | 17 | ||||
-rw-r--r-- | pkg/gui/filetree/file_tree_view_model.go | 139 | ||||
-rw-r--r-- | pkg/gui/filetree/file_tree_view_model_test.go | 67 | ||||
-rw-r--r-- | pkg/gui/filetree/inode.go | 51 | ||||
-rw-r--r-- | pkg/gui/filetree/presentation.go | 96 |
12 files changed, 350 insertions, 579 deletions
diff --git a/pkg/gui/filetree/README.md b/pkg/gui/filetree/README.md new file mode 100644 index 000000000..d2d16ace6 --- /dev/null +++ b/pkg/gui/filetree/README.md @@ -0,0 +1,22 @@ +## FileTree Package + +This package handles the representation of file trees. There are two ways to render files: one is to render them flat, so something like this: + +``` +dir1/file1 +dir1/file2 +file3 +``` + +And the other is to render them as a tree + +``` +dir1/ + file1 + file2 +file3 +``` + +Internally we represent each of the above as a tree, but with the flat approach there's just a single root node and every path is a direct child of that root. Viewing in 'tree' mode (as opposed to 'flat' mode) allows for collapsing and expanding directories, and lets you perform actions on directories e.g. staging a whole directory. But it takes up more vertical space and sometimes you just want to have a flat view where you can go flick through your files one by one to see the diff. + +This package is not concerned about rendering the tree: only representing its internal state. diff --git a/pkg/gui/filetree/commit_file_manager.go b/pkg/gui/filetree/commit_file_manager.go deleted file mode 100644 index 852a67b09..000000000 --- a/pkg/gui/filetree/commit_file_manager.go +++ /dev/null @@ -1,118 +0,0 @@ -package filetree - -import ( - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/commands/patch" - "github.com/sirupsen/logrus" -) - -type CommitFileManager struct { - files []*models.CommitFile - tree *CommitFileNode - showTree bool - log *logrus.Entry - collapsedPaths CollapsedPaths - // parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}' - parent string -} - -func (m *CommitFileManager) GetParent() string { - return m.parent -} - -func NewCommitFileManager(files []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileManager { - return &CommitFileManager{ - files: files, - log: log, - showTree: showTree, - collapsedPaths: CollapsedPaths{}, - } -} - -func (m *CommitFileManager) ExpandToPath(path string) { - m.collapsedPaths.ExpandToPath(path) -} - -func (m *CommitFileManager) ToggleShowTree() { - m.showTree = !m.showTree - m.SetTree() -} - -func (m *CommitFileManager) GetItemAtIndex(index int) *CommitFileNode { - // need to traverse the three depth first until we get to the index. - return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root -} - -func (m *CommitFileManager) GetIndexForPath(path string) (int, bool) { - index, found := m.tree.GetIndexForPath(path, m.collapsedPaths) - return index - 1, found -} - -func (m *CommitFileManager) GetAllItems() []*CommitFileNode { - if m.tree == nil { - return nil - } - - return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root -} - -func (m *CommitFileManager) GetItemsLength() int { - return m.tree.Size(m.collapsedPaths) - 1 // ignoring root -} - -func (m *CommitFileManager) GetAllFiles() []*models.CommitFile { - return m.files -} - -func (m *CommitFileManager) SetFiles(files []*models.CommitFile, parent string) { - m.files = files - m.parent = parent - - m.SetTree() -} - -func (m *CommitFileManager) SetTree() { - if m.showTree { - m.tree = BuildTreeFromCommitFiles(m.files) - } else { - m.tree = BuildFlatTreeFromCommitFiles(m.files) - } -} - -func (m *CommitFileManager) IsCollapsed(path string) bool { - return m.collapsedPaths.IsCollapsed(path) -} - -func (m *CommitFileManager) ToggleCollapsed(path string) { - m.collapsedPaths.ToggleCollapsed(path) -} - -func (m *CommitFileManager) Render(diffName string, patchManager *patch.PatchManager) []string { - // can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil - if m.tree == nil { - return []string{} - } - - return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string { - castN := n.(*CommitFileNode) - - // 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 { - return patchManager.GetFileStatus(file.Name, m.parent) == patch.WHOLE - }) { - status = patch.WHOLE - } else if castN.EveryFile(func(file *models.CommitFile) bool { - return patchManager.GetFileStatus(file.Name, m.parent) == patch.UNSELECTED - }) { - status = patch.UNSELECTED - } else { - status = patch.PART - } - - return getCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status) - }) -} diff --git a/pkg/gui/filetree/commit_file_node.go b/pkg/gui/filetree/commit_file_node.go index 173281035..14960ee30 100644 --- a/pkg/gui/filetree/commit_file_node.go +++ b/pkg/gui/filetree/commit_file_node.go @@ -11,6 +11,8 @@ type CommitFileNode struct { CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode } +var _ INode = &CommitFileNode{} + // methods satisfying ListItem interface func (s *CommitFileNode) ID() string { @@ -23,6 +25,10 @@ func (s *CommitFileNode) Description() string { // methods satisfying INode interface +func (s *CommitFileNode) IsNil() bool { + return s == nil +} + func (s *CommitFileNode) IsLeaf() bool { return s.File != nil } @@ -139,13 +145,6 @@ func (s *CommitFileNode) Compress() { compressAux(s) } -// This ignores the root -func (node *CommitFileNode) GetPathsMatching(test func(*CommitFileNode) bool) []string { - return getPathsMatching(node, func(n INode) bool { - return test(n.(*CommitFileNode)) - }) -} - func (s *CommitFileNode) GetLeaves() []*CommitFileNode { leaves := getLeaves(s) castLeaves := make([]*CommitFileNode, len(leaves)) diff --git a/pkg/gui/filetree/commit_file_tree_view_model.go b/pkg/gui/filetree/commit_file_tree_view_model.go new file mode 100644 index 000000000..301396462 --- /dev/null +++ b/pkg/gui/filetree/commit_file_tree_view_model.go @@ -0,0 +1,101 @@ +package filetree + +import ( + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/sirupsen/logrus" +) + +type CommitFileTreeViewModel struct { + files []*models.CommitFile + tree *CommitFileNode + showTree bool + log *logrus.Entry + collapsedPaths CollapsedPaths + // parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}' + parent string +} + +func (self *CommitFileTreeViewModel) GetParent() string { + return self.parent +} + +func (self *CommitFileTreeViewModel) SetParent(parent string) { + self.parent = parent +} + +func NewCommitFileTreeViewModel(files []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileTreeViewModel { + viewModel := &CommitFileTreeViewModel{ + log: log, + showTree: showTree, + collapsedPaths: CollapsedPaths{}, + } + + viewModel.SetFiles(files) + + return viewModel +} + +func (self *CommitFileTreeViewModel) ExpandToPath(path string) { + self.collapsedPaths.ExpandToPath(path) +} + +func (self *CommitFileTreeViewModel) ToggleShowTree() { + self.showTree = !self.showTree + self.SetTree() +} + +func (self *CommitFileTreeViewModel) GetItemAtIndex(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 +} + +func (self *CommitFileTreeViewModel) GetIndexForPath(path string) (int, bool) { + index, found := self.tree.GetIndexForPath(path, self.collapsedPaths) + return index - 1, found +} + +func (self *CommitFileTreeViewModel) GetAllItems() []*CommitFileNode { + if self.tree == nil { + return nil + } + + return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root +} + +func (self *CommitFileTreeViewModel) GetItemsLength() int { + return self.tree.Size(self.collapsedPaths) - 1 // ignoring root +} + +func (self *CommitFileTreeViewModel) GetAllFiles() []*models.CommitFile { + return self.files +} + +func (self *CommitFileTreeViewModel) SetFiles(files []*models.CommitFile) { + self.files = files + + self.SetTree() +} + +func (self *CommitFileTreeViewModel) SetTree() { + if self.showTree { + self.tree = BuildTreeFromCommitFiles(self.files) + } else { + self.tree = BuildFlatTreeFromCommitFiles(self.files) + } +} + +func (self *CommitFileTreeViewModel) IsCollapsed(path string) bool { + return self.collapsedPaths.IsCollapsed(path) +} + +func (self *CommitFileTreeViewModel) ToggleCollapsed(path string) { + self.collapsedPaths.ToggleCollapsed(path) +} + +func (self *CommitFileTreeViewModel) Tree() INode { + return self.tree +} + +func (self *CommitFileTreeViewModel) CollapsedPaths() CollapsedPaths { + return self.collapsedPaths +} diff --git a/pkg/gui/filetree/constants.go b/pkg/gui/filetree/constants.go deleted file mode 100644 index d510650e2..000000000 --- a/pkg/gui/filetree/constants.go +++ /dev/null @@ -1,9 +0,0 @@ -package filetree - -const EXPANDED_ARROW = "▼" -const COLLAPSED_ARROW = "►" - -const INNER_ITEM = "├─ " -const LAST_ITEM = "└─ " -const NESTED = "│ " -const NOTHING = " " diff --git a/pkg/gui/filetree/file_manager.go b/pkg/gui/filetree/file_manager.go deleted file mode 100644 index b028ef961..000000000 --- a/pkg/gui/filetree/file_manager.go +++ /dev/null @@ -1,140 +0,0 @@ -package filetree - -import ( - "sync" - - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/sirupsen/logrus" -) - -type FileManagerDisplayFilter int - -const ( - DisplayAll FileManagerDisplayFilter = iota - DisplayStaged - DisplayUnstaged -) - -type FileManager struct { - files []*models.File - tree *FileNode - showTree bool - log *logrus.Entry - filter FileManagerDisplayFilter - collapsedPaths CollapsedPaths - sync.RWMutex -} - -func NewFileManager(files []*models.File, log *logrus.Entry, showTree bool) *FileManager { - return &FileManager{ - files: files, - log: log, - showTree: showTree, - filter: DisplayAll, - collapsedPaths: CollapsedPaths{}, - RWMutex: sync.RWMutex{}, - } -} - -func (m *FileManager) InTreeMode() bool { - return m.showTree -} - -func (m *FileManager) ExpandToPath(path string) { - m.collapsedPaths.ExpandToPath(path) -} - -func (m *FileManager) GetFilesForDisplay() []*models.File { - files := m.files - if m.filter == DisplayAll { - return files - } - - result := make([]*models.File, 0) - if m.filter == DisplayStaged { - for _, file := range files { - if file.HasStagedChanges { - result = append(result, file) - } - } - } else { - for _, file := range files { - if !file.HasStagedChanges { - result = append(result, file) - } - } - } - - return result -} - -func (m *FileManager) SetDisplayFilter(filter FileManagerDisplayFilter) { - m.filter = filter - m.SetTree() -} - -func (m *FileManager) ToggleShowTree() { - m.showTree = !m.showTree - m.SetTree() -} - -func (m *FileManager) GetItemAtIndex(index int) *FileNode { - // need to traverse the three depth first until we get to the index. - return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root -} - -func (m *FileManager) GetIndexForPath(path string) (int, bool) { - index, found := m.tree.GetIndexForPath(path, m.collapsedPaths) - return index - 1, found -} - -func (m *FileManager) GetAllItems() []*FileNode { - if m.tree == nil { - return nil - } - - return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root -} - -func (m *FileManager) GetItemsLength() int { - return m.tree.Size(m.collapsedPaths) - 1 // ignoring root -} - -func (m *FileManager) GetAllFiles() []*models.File { - return m.files -} - -func (m *FileManager) SetFiles(files []*models.File) { - m.files = files - - m.SetTree() -} - -func (m *FileManager) SetTree() { - filesForDisplay := m.GetFilesForDisplay() - if m.showTree { - m.tree = BuildTreeFromFiles(filesForDisplay) - } else { - m.tree = BuildFlatTreeFromFiles(filesForDisplay) - } -} - -func (m *FileManager) IsCollapsed(path string) bool { - return m.collapsedPaths.IsCollapsed(path) -} - -func (m *FileManager) ToggleCollapsed(path string) { - m.collapsedPaths.ToggleCollapsed(path) -} - -func (m *FileManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string { - // can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil - if m.tree == nil { - return []string{} - } - - return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string { - castN := n.(*FileNode) - return getFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File) - }) -} diff --git a/pkg/gui/filetree/file_manager_test.go b/pkg/gui/filetree/file_manager_test.go deleted file mode 100644 index 83ba5b086..000000000 --- a/pkg/gui/filetree/file_manager_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package filetree - -import ( - "testing" - - "github.com/gookit/color" - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/stretchr/testify/assert" - "github.com/xo/terminfo" -) - -func init() { - color.ForceSetColorLevel(terminfo.ColorLevelNone) -} - -func TestRender(t *testing.T) { - scenarios := []struct { - name string - root *FileNode - collapsedPaths map[string]bool - expected []string - }{ - { - name: "nil node", - root: nil, - expected: []string{}, - }, - { - name: "leaf node", - root: &FileNode{ - Path: "", - Children: []*FileNode{ - {File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"}, - }, - }, - expected: []string{" M test"}, - }, - { - name: "big example", - root: &FileNode{ - Path: "", - Children: []*FileNode{ - { - Path: "dir1", - Children: []*FileNode{ - { - File: &models.File{Name: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir1/file2", - }, - { - File: &models.File{Name: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir1/file3", - }, - }, - }, - { - Path: "dir2", - Children: []*FileNode{ - { - Path: "dir2/dir2", - Children: []*FileNode{ - { - File: &models.File{Name: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true}, - Path: "dir2/dir2/file3", - }, - { - File: &models.File{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir2/dir2/file4", - }, - }, - }, - { - File: &models.File{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "dir2/file5", - }, - }, - }, - { - File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, - Path: "file1", - }, - }, - }, - expected: []string{"dir1 ►", "dir2 ▼", "├─ dir2 ▼", "│ ├─ M file3", "│ └─ M file4", "└─ M file5", "M file1"}, - collapsedPaths: map[string]bool{"dir1": true}, - }, - } - - for _, s := range scenarios { - s := s - t.Run(s.name, func(t *testing.T) { - mngr := &FileManager{tree: s.root, collapsedPaths: s.collapsedPaths} - result := mngr.Render("", nil) - assert.EqualValues(t, s.expected, result) - }) - } -} - -func TestFilterAction(t *testing.T) { - scenarios := []struct { - name string - filter FileManagerDisplayFilter - files []*models.File - expected []*models.File - }{ - { - name: "filter files with unstaged changes", - filter: DisplayUnstaged, - files: []*models.File{ - {Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, - {Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: true}, - {Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, - }, - expected: []*models.File{ - {Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, - {Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, - }, - }, - { - name: "filter files with staged changes", - filter: DisplayStaged, - files: []*models.File{ - {Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true}, - {Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: false}, - {Name: "file1", ShortStatus: "M ", HasStagedChanges: true}, - }, - expected: []*models.File{ - {Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true}, - {Name: "file1", ShortStatus: "M ", HasStagedChanges: true}, - }, - }, - { - name: "filter all files", - filter: DisplayAll, - files: []*models.File{ - {Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, - {Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, - {Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, - }, - expected: []*models.File{ - {Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, - {Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, - {Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, - }, - }, - } - - for _, s := range scenarios { - s := s - t.Run(s.name, func(t *testing.T) { - mngr := &FileManager{files: s.files, filter: s.filter} - result := mngr.GetFilesForDisplay() - assert.EqualValues(t, s.expected, result) - }) - } -} diff --git a/pkg/gui/filetree/file_node.go b/pkg/gui/filetree/file_node.go index cb545d391..f332f0a76 100644 --- a/pkg/gui/filetree/file_node.go +++ b/pkg/gui/filetree/file_node.go @@ -11,6 +11,8 @@ type FileNode struct { CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode } +var _ INode = &FileNode{} + // methods satisfying ListItem interface func (s *FileNode) ID() string { @@ -23,6 +25,12 @@ func (s *FileNode) Description() string { // 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 } @@ -124,10 +132,13 @@ func (s *FileNode) Compress() { compressAux(s) } -// This ignores the root -func (node *FileNode) GetPathsMatching(test func(*FileNode) bool) []string { +func (node *FileNode) GetFilePathsMatching(test func(*models.File) bool) []string { return getPathsMatching(node, func(n INode) bool { - return test(n.(*FileNode)) + castNode := n.(*FileNode) + if castNode.File == nil { + return false + } + return test(castNode.File) }) } diff --git a/pkg/gui/filetree/file_tree_view_model.go b/pkg/gui/filetree/file_tree_view_model.go new file mode 100644 index 000000000..d12814976 --- /dev/null +++ b/pkg/gui/filetree/file_tree_view_model.go @@ -0,0 +1,139 @@ +package filetree + +import ( + "sync" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/sirupsen/logrus" +) + +type FileTreeDisplayFilter int + +const ( + DisplayAll FileTreeDisplayFilter = iota + DisplayStaged + DisplayUnstaged +) + +type FileTreeViewModel struct { + files []*models.File + tree *FileNode + showTree bool + log *logrus.Entry + filter FileTreeDisplayFilter + collapsedPaths CollapsedPaths + sync.RWMutex +} + +func NewFileTreeViewModel(files []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel { + viewModel := &FileTreeViewModel{ + log: log, + showTree: showTree, + filter: DisplayAll, + collapsedPaths: CollapsedPaths{}, + RWMutex: sync.RWMutex{}, + } + + viewModel.SetFiles(files) + + return viewModel +} + +func (self *FileTreeViewModel) InTreeMode() bool { + return self.showTree +} + +func (self *FileTreeViewModel) ExpandToPath(path string) { + self.collapsedPaths.ExpandToPath(path) +} + +func (self *FileTreeViewModel) GetFilesForDisplay() []*models.File { + files := self.files + if self.filter == DisplayAll { + return files + } + + result := make([]*models.File, 0) + if self.filter == DisplayStaged { + for _, file := range files { + if file.HasStagedChanges { + result = append(result, file) + } + } + } else { + for _, file := range files { + if !file.HasStagedChanges { + result = append(result, file) + } + } + } + + return result +} + +func (self *FileTreeViewModel) SetDisplayFilter(filter FileTreeDisplayFilter) { + self.filter = filter + self.SetTree() +} + +func (self *FileTreeViewModel) ToggleShowTree() { + self.showTree = !self.showTree + self.SetTree() +} + +func (self *FileTreeViewModel) GetItemAtIndex(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 +} + +func (self *FileTreeViewModel) GetIndexForPath(path string) (int, bool) { + index, found := self.tree.GetIndexForPath(path, self.collapsedPaths) + return index - 1, found +} + +func (self *FileTreeViewModel) GetAllItems() []*FileNode { + if self.tree == nil { + return nil + } + + return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root +} + +func (self *FileTreeViewModel) GetItemsLength() int { + return self.tree.Size(self.collapsedPaths) - 1 // ignoring root +} + +func (self *FileTreeViewModel) GetAllFiles() []*models.File { + return self.files +} + +func (self *FileTreeViewModel) SetFiles(files []*models.File) { + self.files = files + + self.SetTree() +} + +func (self *FileTreeViewModel) SetTree() { + filesForDisplay := self.GetFilesForDisplay() + if self.showTree { + self.tree = BuildTreeFromFiles(filesForDisplay) + } else { + self.tree = BuildFlatTreeFromFiles(filesForDisplay) + } +} + +func (self *FileTreeViewModel) IsCollapsed(path string) bool { + return self.collapsedPaths.IsCollapsed(path) +} + +func (self *FileTreeViewModel) ToggleCollapsed(path string) { + self.collapsedPaths.ToggleCollapsed(path) +} + +func (self *FileTreeViewModel) Tree() INode { + return self.tree +} + +func (self *FileTreeViewModel) CollapsedPaths() CollapsedPaths { + return self.collapsedPaths +} diff --git a/pkg/gui/filetree/file_tree_view_model_test.go b/pkg/gui/filetree/file_tree_view_model_test.go new file mode 100644 index 000000000..10c32d31d --- /dev/null +++ b/pkg/gui/filetree/file_tree_view_model_test.go @@ -0,0 +1,67 @@ +package filetree + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/stretchr/testify/assert" +) + +func TestFilterAction(t *testing.T) { + scenarios := []struct { + name string + filter FileTreeDisplayFilter + files []*models.File + expected []*models.File + }{ + { + name: "filter files with unstaged changes", + filter: DisplayUnstaged, + files: []*models.File{ + {Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, + {Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: true}, + {Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, + }, + expected: []*models.File{ + {Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, + {Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, + }, + }, + { + name: "filter files with staged changes", + filter: DisplayStaged, + files: []*models.File{ + {Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true}, + {Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: false}, + {Name: "file1", ShortStatus: "M ", HasStagedChanges: true}, + }, + expected: []*models.File{ + {Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true}, + {Name: "file1", ShortStatus: "M ", HasStagedChanges: true}, + }, + }, + { + name: "filter all files", + filter: DisplayAll, + files: []*models.File{ + {Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, + {Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, + {Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, + }, + expected: []*models.File{ + {Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true}, + {Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, + {Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true}, + }, + }, + } + + for _, s := range scenarios { + s := s + t.Run(s.name, func(t *testing.T) { + mngr := &FileTreeViewModel{files: s.files, filter: s.filter} + result := mngr.GetFilesForDisplay() + assert.EqualValues(t, s.expected, result) + }) + } +} diff --git a/pkg/gui/filetree/inode.go b/pkg/gui/filetree/inode.go index f7bfc0518..7d9035fe3 100644 --- a/pkg/gui/filetree/inode.go +++ b/pkg/gui/filetree/inode.go @@ -1,12 +1,11 @@ package filetree import ( - "fmt" "sort" - "strings" ) type INode interface { + IsNil() bool IsLeaf() bool GetPath() string GetChildren() []INode @@ -212,51 +211,3 @@ func getLeaves(node INode) []INode { return output } - -func renderAux(s INode, collapsedPaths CollapsedPaths, prefix string, depth int, renderLine func(INode, int) string) []string { - isRoot := depth == -1 - - renderLineWithPrefix := func() string { - return prefix + renderLine(s, depth) - } - - if s.IsLeaf() { - if isRoot { - return []string{} - } - return []string{renderLineWithPrefix()} - } - - if collapsedPaths.IsCollapsed(s.GetPath()) { - return []string{fmt.Sprintf("%s %s", renderLineWithPrefix(), COLLAPSED_ARROW)} - } - - arr := []string{} - if !isRoot { - arr = append(arr, fmt.Sprintf("%s %s", renderLineWithPrefix(), EXPANDED_ |