mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-01-10 04:07:18 +02:00
WIP
start moving to new interface WIP WIP WIP WIP WIP
This commit is contained in:
parent
049849264e
commit
45939171ea
@ -58,6 +58,7 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
|
137
pkg/commands/models/status_line_node.go
Normal file
137
pkg/commands/models/status_line_node.go
Normal file
@ -0,0 +1,137 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
type StatusLineNode struct {
|
||||
Children []*StatusLineNode
|
||||
File *File
|
||||
Name string
|
||||
Collapsed bool
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) GetShortStatus() string {
|
||||
// need to see if any child has unstaged changes.
|
||||
if s.IsLeaf() {
|
||||
return s.File.ShortStatus
|
||||
}
|
||||
|
||||
firstChar := " "
|
||||
secondChar := " "
|
||||
if s.HasStagedChanges() {
|
||||
firstChar = "M"
|
||||
}
|
||||
if s.HasUnstagedChanges() {
|
||||
secondChar = "M"
|
||||
}
|
||||
|
||||
return firstChar + secondChar
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) HasUnstagedChanges() bool {
|
||||
if s.IsLeaf() {
|
||||
return s.File.HasUnstagedChanges
|
||||
}
|
||||
|
||||
for _, child := range s.Children {
|
||||
if child.HasUnstagedChanges() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) HasStagedChanges() bool {
|
||||
if s.IsLeaf() {
|
||||
return s.File.HasStagedChanges
|
||||
}
|
||||
|
||||
for _, child := range s.Children {
|
||||
if child.HasStagedChanges() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) GetNodeAtIndex(index int) *StatusLineNode {
|
||||
node, _ := s.getNodeAtIndexAux(index)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) getNodeAtIndexAux(index int) (*StatusLineNode, int) {
|
||||
offset := 1
|
||||
|
||||
if index == 0 {
|
||||
return s, offset
|
||||
}
|
||||
|
||||
for _, child := range s.Children {
|
||||
node, offsetChange := child.getNodeAtIndexAux(index - offset)
|
||||
offset += offsetChange
|
||||
if node != nil {
|
||||
return node, offset
|
||||
}
|
||||
}
|
||||
|
||||
return nil, offset
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) IsLeaf() bool {
|
||||
return len(s.Children) == 0
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) Size() int {
|
||||
output := 1
|
||||
|
||||
for _, child := range s.Children {
|
||||
output += child.Size()
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) Flatten() []*StatusLineNode {
|
||||
arr := []*StatusLineNode{s}
|
||||
|
||||
for _, child := range s.Children {
|
||||
arr = append(arr, child.Flatten()...)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) SortTree() {
|
||||
s.sortChildren()
|
||||
|
||||
for _, child := range s.Children {
|
||||
child.SortTree()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StatusLineNode) sortChildren() {
|
||||
if s.IsLeaf() {
|
||||
return
|
||||
}
|
||||
|
||||
sortedChildren := make([]*StatusLineNode, len(s.Children))
|
||||
copy(sortedChildren, s.Children)
|
||||
|
||||
sort.Slice(sortedChildren, func(i, j int) bool {
|
||||
if !sortedChildren[i].IsLeaf() && sortedChildren[j].IsLeaf() {
|
||||
return true
|
||||
}
|
||||
if sortedChildren[i].IsLeaf() && !sortedChildren[j].IsLeaf() {
|
||||
return false
|
||||
}
|
||||
|
||||
return sortedChildren[i].Name < sortedChildren[j].Name
|
||||
})
|
||||
|
||||
// TODO: think about making this in-place
|
||||
s.Children = sortedChildren
|
||||
}
|
76
pkg/commands/models/status_line_node_test.go
Normal file
76
pkg/commands/models/status_line_node_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *StatusLineNode
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil node",
|
||||
root: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "leaf node",
|
||||
root: &StatusLineNode{
|
||||
Name: "",
|
||||
Children: []*StatusLineNode{
|
||||
{File: &File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Name: "test"},
|
||||
},
|
||||
},
|
||||
expected: []string{" M test"},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
root: &StatusLineNode{
|
||||
Name: "",
|
||||
Children: []*StatusLineNode{
|
||||
{
|
||||
Name: "dir1",
|
||||
Collapsed: true,
|
||||
Children: []*StatusLineNode{
|
||||
{
|
||||
File: &File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Name: "file2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "dir2",
|
||||
Children: []*StatusLineNode{
|
||||
{
|
||||
File: &File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Name: "file3",
|
||||
},
|
||||
{
|
||||
File: &File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Name: "file4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
File: &File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Name: "file1",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expected: []string{"M dir1 ►", "MM dir2 ▼", " M file3", " M file4", "M file1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := s.root.Render()[1:]
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
@ -21,13 +21,26 @@ import (
|
||||
|
||||
// list panel functions
|
||||
|
||||
// func (gui *Gui) getSelectedStatusNode() *models.StatusLineNode {
|
||||
// selectedLine := gui.State.Panels.Files.SelectedLineIdx
|
||||
// if selectedLine == -1 {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// return gui.State.StatusLineManager.GetItemAtIndex(selectedLine)
|
||||
// }
|
||||
|
||||
func (gui *Gui) getSelectedFile() *models.File {
|
||||
selectedLine := gui.State.Panels.Files.SelectedLineIdx
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.Files[selectedLine]
|
||||
node := gui.State.StatusLineManager.GetItemAtIndex(selectedLine)
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return node.File
|
||||
}
|
||||
|
||||
func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
@ -131,7 +144,7 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) stagedFiles() []*models.File {
|
||||
files := gui.State.Files
|
||||
files := gui.State.StatusLineManager.GetAllFiles()
|
||||
result := make([]*models.File, 0)
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
@ -142,7 +155,7 @@ func (gui *Gui) stagedFiles() []*models.File {
|
||||
}
|
||||
|
||||
func (gui *Gui) trackedFiles() []*models.File {
|
||||
files := gui.State.Files
|
||||
files := gui.State.StatusLineManager.GetAllFiles()
|
||||
result := make([]*models.File, 0, len(files))
|
||||
for _, file := range files {
|
||||
if file.Tracked {
|
||||
@ -216,7 +229,7 @@ func (gui *Gui) handleFilePress() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) allFilesStaged() bool {
|
||||
for _, file := range gui.State.Files {
|
||||
for _, file := range gui.State.StatusLineManager.GetAllFiles() {
|
||||
if file.HasUnstagedChanges {
|
||||
return false
|
||||
}
|
||||
@ -450,8 +463,11 @@ func (gui *Gui) refreshStateFiles() error {
|
||||
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
|
||||
|
||||
// get files to stage
|
||||
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
|
||||
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files, selectedFile)
|
||||
noRenames := gui.State.StatusLineManager.TreeMode
|
||||
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{NoRenames: noRenames})
|
||||
gui.State.StatusLineManager.SetFiles(
|
||||
gui.GitCommand.MergeStatusFiles(gui.State.StatusLineManager.GetAllFiles(), files, selectedFile),
|
||||
)
|
||||
|
||||
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
|
||||
return err
|
||||
@ -459,8 +475,9 @@ func (gui *Gui) refreshStateFiles() error {
|
||||
|
||||
// let's try to find our file again and move the cursor to that
|
||||
if selectedFile != nil {
|
||||
for idx, f := range gui.State.Files {
|
||||
selectedFileHasMoved := f.Matches(selectedFile) && idx != prevSelectedLineIdx
|
||||
for idx, node := range gui.State.StatusLineManager.GetAllItems() {
|
||||
// TODO: check that this works
|
||||
selectedFileHasMoved := node.File != nil && node.File.Matches(selectedFile) && idx != prevSelectedLineIdx
|
||||
if selectedFileHasMoved {
|
||||
gui.State.Panels.Files.SelectedLineIdx = idx
|
||||
break
|
||||
@ -468,7 +485,7 @@ func (gui *Gui) refreshStateFiles() error {
|
||||
}
|
||||
}
|
||||
|
||||
gui.refreshSelectedLine(gui.State.Panels.Files, len(gui.State.Files))
|
||||
gui.refreshSelectedLine(gui.State.Panels.Files, gui.State.StatusLineManager.GetItemsLength())
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -661,7 +678,7 @@ func (gui *Gui) openFile(filename string) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
for _, file := range gui.State.Files {
|
||||
for _, file := range gui.State.StatusLineManager.GetAllFiles() {
|
||||
if file.HasMergeConflicts {
|
||||
return true
|
||||
}
|
||||
|
@ -303,12 +303,12 @@ type guiStateMutexes struct {
|
||||
}
|
||||
|
||||
type guiState struct {
|
||||
Files []*models.File
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
Commits []*models.Commit
|
||||
StashEntries []*models.StashEntry
|
||||
CommitFiles []*models.CommitFile
|
||||
StatusLineManager *StatusLineManager
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
Commits []*models.Commit
|
||||
StashEntries []*models.StashEntry
|
||||
CommitFiles []*models.CommitFile
|
||||
// Suggestions will sometimes appear when typing into a prompt
|
||||
Suggestions []*types.Suggestion
|
||||
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
||||
@ -378,7 +378,7 @@ func (gui *Gui) resetState() {
|
||||
}
|
||||
|
||||
gui.State = &guiState{
|
||||
Files: make([]*models.File, 0),
|
||||
StatusLineManager: &StatusLineManager{Files: make([]*models.File, 0), Log: gui.Log, TreeMode: true},
|
||||
Commits: make([]*models.Commit, 0),
|
||||
FilteredReflogCommits: make([]*models.Commit, 0),
|
||||
ReflogCommits: make([]*models.Commit, 0),
|
||||
|
@ -262,7 +262,7 @@ func (gui *Gui) filesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "files",
|
||||
ContextKey: FILES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Files) },
|
||||
GetItemsLength: func() int { return gui.State.StatusLineManager.GetItemsLength() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
|
||||
OnFocus: gui.focusAndSelectFile,
|
||||
OnClickSelectedItem: gui.handleFilePress,
|
||||
@ -270,7 +270,15 @@ func (gui *Gui) filesListContext() *ListContext {
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
lines := gui.State.StatusLineManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
mappedLines[i] = []string{line}
|
||||
}
|
||||
|
||||
return mappedLines
|
||||
|
||||
return presentation.GetFileListDisplayStrings(gui.State.StatusLineManager.GetAllFiles(), gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedFile()
|
||||
|
@ -11,41 +11,45 @@ func GetFileListDisplayStrings(files []*models.File, diffName string, submoduleC
|
||||
lines := make([][]string, len(files))
|
||||
|
||||
for i := range files {
|
||||
diffed := files[i].Name == diffName
|
||||
lines[i] = getFileDisplayStrings(files[i], diffed, submoduleConfigs)
|
||||
lines[i] = getFileDisplayStrings(files[i], diffName, submoduleConfigs)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getFileDisplayStrings returns the display string of branch
|
||||
func getFileDisplayStrings(f *models.File, diffed bool, submoduleConfigs []*models.SubmoduleConfig) []string {
|
||||
func getFileDisplayStrings(f *models.File, diffName string, submoduleConfigs []*models.SubmoduleConfig) []string {
|
||||
output := GetStatusNodeLine(f.HasUnstagedChanges, f.ShortStatus, f.Name, diffName, submoduleConfigs, f)
|
||||
|
||||
return []string{output}
|
||||
}
|
||||
|
||||
func GetStatusNodeLine(hasUnstagedChanges bool, shortStatus string, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
red := color.New(color.FgRed)
|
||||
green := color.New(color.FgGreen)
|
||||
diffColor := color.New(theme.DiffTerminalColor)
|
||||
if !f.Tracked && !f.HasStagedChanges {
|
||||
return []string{red.Sprint(f.DisplayString)}
|
||||
}
|
||||
|
||||
var restColor *color.Color
|
||||
if diffed {
|
||||
if name == diffName {
|
||||
restColor = diffColor
|
||||
} else if f.HasUnstagedChanges {
|
||||
} else if hasUnstagedChanges {
|
||||
restColor = red
|
||||
} else {
|
||||
restColor = green
|
||||
}
|
||||
|
||||
// this is just making things look nice when the background attribute is 'reverse'
|
||||
firstChar := f.DisplayString[0:1]
|
||||
firstChar := shortStatus[0:1]
|
||||
firstCharCl := green
|
||||
if firstChar == " " {
|
||||
if firstChar == "?" {
|
||||
firstCharCl = red
|
||||
} else if firstChar == " " {
|
||||
firstCharCl = restColor
|
||||
}
|
||||
|
||||
secondChar := f.DisplayString[1:2]
|
||||
secondChar := shortStatus[1:2]
|
||||
secondCharCl := red
|
||||
if secondChar == " " {
|
||||
secondCharCl = restColor
|
||||
@ -53,11 +57,11 @@ func getFileDisplayStrings(f *models.File, diffed bool, submoduleConfigs []*mode
|
||||
|
||||
output := firstCharCl.Sprint(firstChar)
|
||||
output += secondCharCl.Sprint(secondChar)
|
||||
output += restColor.Sprintf(" %s", f.Name)
|
||||
output += restColor.Sprintf(" %s", name)
|
||||
|
||||
if f.IsSubmodule(submoduleConfigs) {
|
||||
if file != nil && file.IsSubmodule(submoduleConfigs) {
|
||||
output += utils.ColoredString(" (submodule)", theme.DefaultTextColor)
|
||||
}
|
||||
|
||||
return []string{output}
|
||||
return output
|
||||
}
|
||||
|
87
pkg/gui/status_line_manager.go
Normal file
87
pkg/gui/status_line_manager.go
Normal file
@ -0,0 +1,87 @@
|
||||
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 StatusLineManager struct {
|
||||
Files []*models.File
|
||||
Tree *models.StatusLineNode
|
||||
TreeMode bool
|
||||
Log *logrus.Entry
|
||||
}
|
||||
|
||||
func (m *StatusLineManager) GetItemAtIndex(index int) *models.StatusLineNode {
|
||||
if m.TreeMode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return m.Tree.GetNodeAtIndex(index + 1) // ignoring root
|
||||
}
|
||||
|
||||
m.Log.Warn(index)
|
||||
if index > len(m.Files)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.StatusLineNode{File: m.Files[index]}
|
||||
}
|
||||
|
||||
func (m *StatusLineManager) GetAllItems() []*models.StatusLineNode {
|
||||
return m.Tree.Flatten()[1:] // ignoring root
|
||||
}
|
||||
|
||||
func (m *StatusLineManager) GetItemsLength() int {
|
||||
return m.Tree.Size() - 1 // ignoring root
|
||||
}
|
||||
|
||||
func (m *StatusLineManager) GetAllFiles() []*models.File {
|
||||
return m.Files
|
||||
}
|
||||
|
||||
func (m *StatusLineManager) SetFiles(files []*models.File) {
|
||||
m.Files = files
|
||||
m.Tree = GetTreeFromStatusFiles(files)
|
||||
}
|
||||
|
||||
func (m *StatusLineManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string {
|
||||
return m.renderAux(m.Tree, -1, diffName, submoduleConfigs)
|
||||
}
|
||||
|
||||
func (m *StatusLineManager) renderAux(s *models.StatusLineNode, depth int, diffName string, submoduleConfigs []*models.SubmoduleConfig) []string {
|
||||
if s == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
getLine := func() string {
|
||||
return strings.Repeat(" ", depth) + presentation.GetStatusNodeLine(s.HasUnstagedChanges(), s.GetShortStatus(), s.Name, diffName, submoduleConfigs, s.File)
|
||||
}
|
||||
|
||||
if s.IsLeaf() {
|
||||
if depth == -1 {
|
||||
return []string{}
|
||||
}
|
||||
return []string{getLine()}
|
||||
}
|
||||
|
||||
if s.Collapsed {
|
||||
return []string{fmt.Sprintf("%s%s %s", strings.Repeat(" ", depth), s.Name, COLLAPSED_ARROW)}
|
||||
}
|
||||
|
||||
arr := []string{}
|
||||
if depth > -1 {
|
||||
arr = append(arr, fmt.Sprintf("%s%s %s", strings.Repeat(" ", depth), s.Name, EXPANDED_ARROW))
|
||||
}
|
||||
|
||||
for _, child := range s.Children {
|
||||
arr = append(arr, m.renderAux(child, depth+1, diffName, submoduleConfigs)...)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
47
pkg/gui/status_tree.go
Normal file
47
pkg/gui/status_tree.go
Normal file
@ -0,0 +1,47 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func GetTreeFromStatusFiles(files []*models.File) *models.StatusLineNode {
|
||||
root := &models.StatusLineNode{}
|
||||
|
||||
sort.SliceStable(files, func(i, j int) bool {
|
||||
return files[i].Name < files[j].Name
|
||||
})
|
||||
|
||||
var curr *models.StatusLineNode
|
||||
for _, file := range files {
|
||||
split := strings.Split(file.Name, string(os.PathSeparator))
|
||||
curr = root
|
||||
outer:
|
||||
for i, dir := range split {
|
||||
var setFile *models.File
|
||||
if i == len(split)-1 {
|
||||
setFile = file
|
||||
}
|
||||
for _, existingChild := range curr.Children {
|
||||
if existingChild.Name == dir {
|
||||
curr = existingChild
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
newChild := &models.StatusLineNode{
|
||||
Name: dir,
|
||||
File: setFile,
|
||||
}
|
||||
curr.Children = append(curr.Children, newChild)
|
||||
|
||||
curr = newChild
|
||||
}
|
||||
}
|
||||
|
||||
root.SortTree()
|
||||
|
||||
return root
|
||||
}
|
@ -98,7 +98,7 @@ func (gui *Gui) handleResetSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File {
|
||||
for _, file := range gui.State.Files {
|
||||
for _, file := range gui.State.StatusLineManager.GetAllFiles() {
|
||||
if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) {
|
||||
return file
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user