From e52cec9cdf17ffb27fc3521f9abd977603d31753 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 21 Mar 2021 15:25:29 +1100 Subject: small refactor --- pkg/gui/file_change_manager.go | 138 --------------------------- pkg/gui/file_change_manager_test.go | 91 ------------------ pkg/gui/filetree/build_tree.go | 63 ++++++++++++ pkg/gui/filetree/file_change_manager.go | 138 +++++++++++++++++++++++++++ pkg/gui/filetree/file_change_manager_test.go | 91 ++++++++++++++++++ pkg/gui/gui.go | 5 +- pkg/gui/status_tree.go | 63 ------------ 7 files changed, 295 insertions(+), 294 deletions(-) delete mode 100644 pkg/gui/file_change_manager.go delete mode 100644 pkg/gui/file_change_manager_test.go create mode 100644 pkg/gui/filetree/build_tree.go create mode 100644 pkg/gui/filetree/file_change_manager.go create mode 100644 pkg/gui/filetree/file_change_manager_test.go delete mode 100644 pkg/gui/status_tree.go (limited to 'pkg/gui') diff --git a/pkg/gui/file_change_manager.go b/pkg/gui/file_change_manager.go deleted file mode 100644 index 1af1fb748..000000000 --- a/pkg/gui/file_change_manager.go +++ /dev/null @@ -1,138 +0,0 @@ -package gui - -import ( - "fmt" - "strings" - - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" - "github.com/sirupsen/logrus" -) - -const EXPANDED_ARROW = "▼" -const COLLAPSED_ARROW = "►" - -type FileChangeManager struct { - Files []*models.File - Tree *models.FileChangeNode - ShowTree bool - Log *logrus.Entry - CollapsedPaths map[string]bool -} - -func NewFileChangeManager(files []*models.File, log *logrus.Entry, showTree bool) *FileChangeManager { - return &FileChangeManager{ - Files: files, - Log: log, - ShowTree: showTree, - CollapsedPaths: map[string]bool{}, - } -} - -func (m *FileChangeManager) GetItemAtIndex(index int) *models.FileChangeNode { - // 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 *FileChangeManager) GetIndexForPath(path string) (int, bool) { - index, found := m.Tree.GetIndexForPath(path, m.CollapsedPaths) - return index - 1, found -} - -func (m *FileChangeManager) GetAllItems() []*models.FileChangeNode { - if m.Tree == nil { - return nil - } - - return m.Tree.Flatten(m.CollapsedPaths)[1:] // ignoring root -} - -func (m *FileChangeManager) GetItemsLength() int { - return m.Tree.Size(m.CollapsedPaths) - 1 // ignoring root -} - -func (m *FileChangeManager) GetAllFiles() []*models.File { - return m.Files -} - -func (m *FileChangeManager) SetFiles(files []*models.File) { - m.Files = files - - m.SetTree() -} - -func (m *FileChangeManager) SetTree() { - if m.ShowTree { - m.Tree = GetTreeFromFiles(m.Files) - } else { - m.Tree = GetFlatTreeFromFiles(m.Files) - } -} - -func (m *FileChangeManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string { - return m.renderAux(m.Tree, "", -1, diffName, submoduleConfigs) -} - -const INNER_ITEM = "├─ " -const LAST_ITEM = "└─ " -const NESTED = "│ " -const NOTHING = " " - -func (m *FileChangeManager) IsCollapsed(s *models.FileChangeNode) bool { - return m.CollapsedPaths[s.GetPath()] -} - -func (m *FileChangeManager) ToggleCollapsed(s *models.FileChangeNode) { - m.CollapsedPaths[s.GetPath()] = !m.CollapsedPaths[s.GetPath()] -} - -func (m *FileChangeManager) renderAux(s *models.FileChangeNode, prefix string, depth int, diffName string, submoduleConfigs []*models.SubmoduleConfig) []string { - isRoot := depth == -1 - if s == nil { - return []string{} - } - - getLine := func() string { - return prefix + presentation.GetFileLine(s.GetHasUnstagedChanges(), s.GetHasStagedChanges(), s.NameAtDepth(depth), diffName, submoduleConfigs, s.File) - } - - if s.IsLeaf() { - if isRoot { - return []string{} - } - return []string{getLine()} - } - - if m.IsCollapsed(s) { - return []string{fmt.Sprintf("%s %s", getLine(), COLLAPSED_ARROW)} - } - - arr := []string{} - if !isRoot { - arr = append(arr, fmt.Sprintf("%s %s", getLine(), 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.Children { - isLast := i == len(s.Children)-1 - - var childPrefix string - if isRoot { - childPrefix = newPrefix - } else if isLast { - childPrefix = newPrefix + LAST_ITEM - } else { - childPrefix = newPrefix + INNER_ITEM - } - - arr = append(arr, m.renderAux(child, childPrefix, depth+1+s.CompressionLevel, diffName, submoduleConfigs)...) - } - - return arr -} diff --git a/pkg/gui/file_change_manager_test.go b/pkg/gui/file_change_manager_test.go deleted file mode 100644 index f339aae0f..000000000 --- a/pkg/gui/file_change_manager_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package gui - -import ( - "testing" - - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/stretchr/testify/assert" -) - -func TestRender(t *testing.T) { - scenarios := []struct { - name string - root *models.FileChangeNode - collapsedPaths map[string]bool - expected []string - }{ - { - name: "nil node", - root: nil, - expected: []string{}, - }, - { - name: "leaf node", - root: &models.FileChangeNode{ - Path: "", - Children: []*models.FileChangeNode{ - {File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"}, - }, - }, - expected: []string{" M test"}, - }, - { - name: "big example", - root: &models.FileChangeNode{ - Path: "", - Children: []*models.FileChangeNode{ - { - Path: "dir1", - Children: []*models.FileChangeNode{ - { - 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: []*models.FileChangeNode{ - { - Path: "dir2/dir2", - Children: []*models.FileChangeNode{ - { - 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 := &FileChangeManager{Tree: s.root, CollapsedPaths: s.collapsedPaths} - result := mngr.Render("", nil) - assert.EqualValues(t, s.expected, result) - }) - } -} diff --git a/pkg/gui/filetree/build_tree.go b/pkg/gui/filetree/build_tree.go new file mode 100644 index 000000000..e8da844e0 --- /dev/null +++ b/pkg/gui/filetree/build_tree.go @@ -0,0 +1,63 @@ +package filetree + +import ( + "os" + "path/filepath" + "sort" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/models" +) + +func BuildTreeFromFiles(files []*models.File) *models.FileChangeNode { + root := &models.FileChangeNode{} + + var curr *models.FileChangeNode + for _, file := range files { + split := strings.Split(file.Name, string(os.PathSeparator)) + curr = root + outer: + for i := range split { + var setFile *models.File + isFile := i == len(split)-1 + if isFile { + setFile = file + } + + path := filepath.Join(split[:i+1]...) + + for _, existingChild := range curr.Children { + if existingChild.Path == path { + curr = existingChild + continue outer + } + } + + newChild := &models.FileChangeNode{ + Path: path, + File: setFile, + } + curr.Children = append(curr.Children, newChild) + + curr = newChild + } + } + + root.Sort() + root.Compress() + + return root +} + +func BuildFlatTreeFromFiles(files []*models.File) *models.FileChangeNode { + rootAux := BuildTreeFromFiles(files) + sortedFiles := rootAux.GetLeaves() + + // Move merge conflicts to top. This is the one way in which sorting + // differs between flat mode and tree mode + sort.SliceStable(sortedFiles, func(i, j int) bool { + return sortedFiles[i].File != nil && sortedFiles[i].File.HasMergeConflicts && !(sortedFiles[j].File != nil && sortedFiles[j].File.HasMergeConflicts) + }) + + return &models.FileChangeNode{Children: sortedFiles} +} diff --git a/pkg/gui/filetree/file_change_manager.go b/pkg/gui/filetree/file_change_manager.go new file mode 100644 index 000000000..925398827 --- /dev/null +++ b/pkg/gui/filetree/file_change_manager.go @@ -0,0 +1,138 @@ +package filetree + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/sirupsen/logrus" +) + +const EXPANDED_ARROW = "▼" +const COLLAPSED_ARROW = "►" + +type FileChangeManager struct { + Files []*models.File + Tree *models.FileChangeNode + ShowTree bool + Log *logrus.Entry + CollapsedPaths map[string]bool +} + +func NewFileChangeManager(files []*models.File, log *logrus.Entry, showTree bool) *FileChangeManager { + return &FileChangeManager{ + Files: files, + Log: log, + ShowTree: showTree, + CollapsedPaths: map[string]bool{}, + } +} + +func (m *FileChangeManager) GetItemAtIndex(index int) *models.FileChangeNode { + // 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 *FileChangeManager) GetIndexForPath(path string) (int, bool) { + index, found := m.Tree.GetIndexForPath(path, m.CollapsedPaths) + return index - 1, found +} + +func (m *FileChangeManager) GetAllItems() []*models.FileChangeNode { + if m.Tree == nil { + return nil + } + + return m.Tree.Flatten(m.CollapsedPaths)[1:] // ignoring root +} + +func (m *FileChangeManager) GetItemsLength() int { + return m.Tree.Size(m.CollapsedPaths) - 1 // ignoring root +} + +func (m *FileChangeManager) GetAllFiles() []*models.File { + return m.Files +} + +func (m *FileChangeManager) SetFiles(files []*models.File) { + m.Files = files + + m.SetTree() +} + +func (m *FileChangeManager) SetTree() { + if m.ShowTree { + m.Tree = BuildTreeFromFiles(m.Files) + } else { + m.Tree = BuildFlatTreeFromFiles(m.Files) + } +} + +func (m *FileChangeManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string { + return m.renderAux(m.Tree, "", -1, diffName, submoduleConfigs) +} + +const INNER_ITEM = "├─ " +const LAST_ITEM = "└─ " +const NESTED = "│ " +const NOTHING = " " + +func (m *FileChangeManager) IsCollapsed(s *models.FileChangeNode) bool { + return m.CollapsedPaths[s.GetPath()] +} + +func (m *FileChangeManager) ToggleCollapsed(s *models.FileChangeNode) { + m.CollapsedPaths[s.GetPath()] = !m.CollapsedPaths[s.GetPath()] +} + +func (m *FileChangeManager) renderAux(s *models.FileChangeNode, prefix string, depth int, diffName string, submoduleConfigs []*models.SubmoduleConfig) []string { + isRoot := depth == -1 + if s == nil { + return []string{} + } + + getLine := func() string { + return prefix + presentation.GetFileLine(s.GetHasUnstagedChanges(), s.GetHasStagedChanges(), s.NameAtDepth(depth), diffName, submoduleConfigs, s.File) + } + + if s.IsLeaf() { + if isRoot { + return []string{} + } + return []string{getLine()} + } + + if m.IsCollapsed(s) { + return []string{fmt.Sprintf("%s %s", getLine(), COLLAPSED_ARROW)} + } + + arr := []string{} + if !isRoot { + arr = append(arr, fmt.Sprintf("%s %s", getLine(), 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.Children { + isLast := i == len(s.Children)-1 + + var childPrefix string + if isRoot { + childPrefix = newPrefix + } else if isLast { + childPrefix = newPrefix + LAST_ITEM + } else { + childPrefix = newPrefix + INNER_ITEM + } + + arr = append(arr, m.renderAux(child, childPrefix, depth+1+s.CompressionLevel, diffName, submoduleConfigs)...) + } + + return arr +} diff --git a/pkg/gui/filetree/file_change_manager_test.go b/pkg/gui/filetree/file_change_manager_test.go new file mode 100644 index 000000000..8e8ba00cd --- /dev/null +++ b/pkg/gui/filetree/file_change_manager_test.go @@ -0,0 +1,91 @@ +package filetree + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/stretchr/testify/assert" +) + +func TestRender(t *testing.T) { + scenarios := []struct { + name string + root *models.FileChangeNode + collapsedPaths map[string]bool + expected []string + }{ + { + name: "nil node", + root: nil, + expected: []string{}, + }, + { + name: "leaf node", + root: &models.FileChangeNode{ + Path: "", + Children: []*models.FileChangeNode{ + {File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"}, + }, + }, + expected: []string{" M test"}, + }, + { + name: "big example", + root: &models.FileChangeNode{ + Path: "", + Children: []*models.FileChangeNode{ + { + Path: "dir1", + Children: []*models.FileChangeNode{ + { + 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: []*models.FileChangeNode{ + { + Path: "dir2/dir2", + Children: []*models.FileChangeNode{ + { + 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 := &FileChangeManager{Tree: s.root, CollapsedPaths: s.collapsedPaths} + result := mngr.Render("", nil) + assert.EqualValues(t, s.expected, result) + }) + } +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index b36ab14db..fbb2ab223 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -21,6 +21,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/tasks" @@ -303,7 +304,7 @@ type guiStateMutexes struct { } type guiState struct { - FileChangeManager *FileChangeManager + FileChangeManager *filetree.FileChangeManager Submodules []*models.SubmoduleConfig Branches []*models.Branch Commits []*models.Commit @@ -380,7 +381,7 @@ func (gui *Gui) resetState() { showTree := gui.Config.GetUserConfig().Gui.ShowFileTree gui.State = &guiState{ - FileChangeManager: NewFileChangeManager(make([]*models.File, 0), gui.Log, showTree), + FileChangeManager: filetree.NewFileChangeManager(make([]*models.File, 0), gui.Log, showTree), Commits: make([]*models.Commit, 0), FilteredReflogCommits: make([]*models.Commit, 0), ReflogCommits: make([]*models.Commit, 0), diff --git a/pkg/gui/status_tree.go b/pkg/gui/status_tree.go deleted file mode 100644 index 3e201b7d4..000000000 --- a/pkg/gui/status_tree.go +++ /dev/null @@ -1,63 +0,0 @@ -package gui - -import ( - "os" - "path/filepath" - "sort" - "strings" - - "github.com/jesseduffield/lazygit/pkg/commands/models" -) - -func GetTreeFromFiles(files []*models.File) *models.FileChangeNode { - root := &models.FileChangeNode{} - - var curr *models.FileChangeNode - for _, file := range files { - split := strings.Split(file.Name, string(os.PathSeparator)) - curr = root - outer: - for i := range split { - var setFile *models.File - isFile := i == len(split)-1 - if isFile { - setFile = file - } - - path := filepath.Join(split[:i+1]...) - - for _, existingChild := range curr.Children { - if existingChild.Path == path { - curr = existingChild - continue outer - } - } - - newChild := &models.FileChangeNode{ - Path: path, - File: setFile, - } - curr.Children = append(curr.Children, newChild) - - curr = newChild - } - } - - root.Sort() - root.Compress() - - return root -} - -func GetFlatTreeFromFiles(files []*models.File) *models.FileChangeNode { - rootAux := GetTreeFromFiles(files) - sortedFiles := rootAux.GetLeaves() - - // Move merge conflicts to top. This is the one way in which sorting - // differs between flat mode and tree mode - sort.SliceStable(sortedFiles, func(i, j int) bool { - return sortedFiles[i].File != nil && sortedFiles[i].File.HasMergeConflicts && !(sortedFiles[j].File != nil && sortedFiles[j].File.HasMergeConflicts) - }) - - return &models.FileChangeNode{Children: sortedFiles} -} -- cgit v1.2.3