mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-02-25 18:55:28 -06:00
properly resolve cyclic dependency
This commit is contained in:
22
pkg/gui/filetree/README.md
Normal file
22
pkg/gui/filetree/README.md
Normal file
@@ -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.
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
101
pkg/gui/filetree/commit_file_tree_view_model.go
Normal file
101
pkg/gui/filetree/commit_file_tree_view_model.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package filetree
|
||||
|
||||
const EXPANDED_ARROW = "▼"
|
||||
const COLLAPSED_ARROW = "►"
|
||||
|
||||
const INNER_ITEM = "├─ "
|
||||
const LAST_ITEM = "└─ "
|
||||
const NESTED = "│ "
|
||||
const NOTHING = " "
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
139
pkg/gui/filetree/file_tree_view_model.go
Normal file
139
pkg/gui/filetree/file_tree_view_model.go
Normal file
@@ -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
|
||||
}
|
||||
67
pkg/gui/filetree/file_tree_view_model_test.go
Normal file
67
pkg/gui/filetree/file_tree_view_model_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// TODO: move this back into presentation package and fix the import cycle
|
||||
|
||||
func getFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
partiallyModifiedColor := style.FgYellow
|
||||
|
||||
restColor := style.FgGreen
|
||||
if name == diffName {
|
||||
restColor = theme.DiffTerminalColor
|
||||
} else if file == nil && hasStagedChanges && hasUnstagedChanges {
|
||||
restColor = partiallyModifiedColor
|
||||
} else if hasUnstagedChanges {
|
||||
restColor = style.FgRed
|
||||
}
|
||||
|
||||
output := ""
|
||||
if file != nil {
|
||||
// this is just making things look nice when the background attribute is 'reverse'
|
||||
firstChar := file.ShortStatus[0:1]
|
||||
firstCharCl := style.FgGreen
|
||||
if firstChar == "?" {
|
||||
firstCharCl = style.FgRed
|
||||
} else if firstChar == " " {
|
||||
firstCharCl = restColor
|
||||
}
|
||||
|
||||
secondChar := file.ShortStatus[1:2]
|
||||
secondCharCl := style.FgRed
|
||||
if secondChar == " " {
|
||||
secondCharCl = restColor
|
||||
}
|
||||
|
||||
output = firstCharCl.Sprint(firstChar)
|
||||
output += secondCharCl.Sprint(secondChar)
|
||||
output += restColor.Sprint(" ")
|
||||
}
|
||||
|
||||
output += restColor.Sprint(utils.EscapeSpecialChars(name))
|
||||
|
||||
if file != nil && file.IsSubmodule(submoduleConfigs) {
|
||||
output += theme.DefaultTextColor.Sprint(" (submodule)")
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func getCommitFileLine(name string, diffName string, commitFile *models.CommitFile, status patch.PatchStatus) string {
|
||||
var colour style.TextStyle
|
||||
if diffName == name {
|
||||
colour = theme.DiffTerminalColor
|
||||
} else {
|
||||
switch status {
|
||||
case patch.WHOLE:
|
||||
colour = style.FgGreen
|
||||
case patch.PART:
|
||||
colour = style.FgYellow
|
||||
case patch.UNSELECTED:
|
||||
colour = theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
name = utils.EscapeSpecialChars(name)
|
||||
if commitFile == nil {
|
||||
return colour.Sprint(name)
|
||||
}
|
||||
|
||||
return getColorForChangeStatus(commitFile.ChangeStatus).Sprint(commitFile.ChangeStatus) + " " + colour.Sprint(name)
|
||||
}
|
||||
|
||||
func getColorForChangeStatus(changeStatus string) style.TextStyle {
|
||||
switch changeStatus {
|
||||
case "A":
|
||||
return style.FgGreen
|
||||
case "M", "R":
|
||||
return style.FgYellow
|
||||
case "D":
|
||||
return style.FgRed
|
||||
case "C":
|
||||
return style.FgCyan
|
||||
case "T":
|
||||
return style.FgMagenta
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user