summaryrefslogtreecommitdiffstats
path: root/pkg/gui/filetree
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2022-01-22 00:13:51 +1100
committerJesse Duffield <jessedduffield@gmail.com>2022-01-22 10:48:51 +1100
commit5b7dd9e43ccd2b82f07f0f0ff0ee8d54d0ecba11 (patch)
tree2afa1c595dad6ae8c0503217eac3f40e2a925924 /pkg/gui/filetree
parent4ab5e5413944a92699a9540148666f0de26aa44b (diff)
properly resolve cyclic dependency
Diffstat (limited to 'pkg/gui/filetree')
-rw-r--r--pkg/gui/filetree/README.md22
-rw-r--r--pkg/gui/filetree/commit_file_manager.go118
-rw-r--r--pkg/gui/filetree/commit_file_node.go13
-rw-r--r--pkg/gui/filetree/commit_file_tree_view_model.go101
-rw-r--r--pkg/gui/filetree/constants.go9
-rw-r--r--pkg/gui/filetree/file_manager.go140
-rw-r--r--pkg/gui/filetree/file_manager_test.go156
-rw-r--r--pkg/gui/filetree/file_node.go17
-rw-r--r--pkg/gui/filetree/file_tree_view_model.go139
-rw-r--r--pkg/gui/filetree/file_tree_view_model_test.go67
-rw-r--r--pkg/gui/filetree/inode.go51
-rw-r--r--pkg/gui/filetree/presentation.go96
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_ARROW))
- }
-
- newPrefix := prefix
- if strings.HasSuffix(prefix, LAST_ITEM) {
- newPrefix = strings.TrimSuffix(prefix, LAST_ITEM) + NOTHING
- } else if strings.HasSuffix(prefix, INNER_ITEM) {
- newPrefix = strings.TrimSuffix(prefix, INNER_ITEM) + NESTED
- }
-
- for i, child := range s.GetChildren() {
- isLast := i == len(s.GetChildren())-1
-
- var childPrefix string
- if isRoot {
- childPrefix = newPrefix
- } else if isLast {
- childPrefix = newPrefix + LAST_ITEM
- } else {
- childPrefix = newPrefix + INNER_ITEM
- }
-
- arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+s.GetCompressionLevel(), renderLine)...)
- }
-
- return arr
-}
diff --git a/pkg/gui