1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-14 11:23:09 +02:00
lazygit/pkg/gui/presentation/files.go

260 lines
7.0 KiB
Go
Raw Normal View History

2022-01-21 15:13:51 +02:00
package presentation
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
2022-04-23 03:14:42 +02:00
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
2022-01-21 15:13:51 +02:00
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
2022-03-19 00:38:49 +02:00
const (
EXPANDED_ARROW = "▼"
2023-03-18 02:32:44 +02:00
COLLAPSED_ARROW = "▶"
2022-03-19 00:38:49 +02:00
)
2022-01-21 15:13:51 +02:00
// keeping these here as individual constants in case later on people want the old tree shape
2022-03-19 00:38:49 +02:00
const (
INNER_ITEM = " "
LAST_ITEM = " "
NESTED = " "
NOTHING = " "
2022-03-19 00:38:49 +02:00
)
2022-01-21 15:13:51 +02:00
func RenderFileTree(
tree filetree.IFileTree,
2022-01-21 15:13:51 +02:00
diffName string,
submoduleConfigs []*models.SubmoduleConfig,
) []string {
return renderAux(tree.GetRoot().Raw(), tree.CollapsedPaths(), "", -1, func(node *filetree.Node[models.File], depth int) string {
fileNode := filetree.NewFileNode(node)
return getFileLine(fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), fileNameAtDepth(node, depth), diffName, submoduleConfigs, node.File)
2022-01-21 15:13:51 +02:00
})
}
func RenderCommitFileTree(
tree *filetree.CommitFileTreeViewModel,
2022-01-21 15:13:51 +02:00
diffName string,
2023-03-19 07:09:03 +02:00
patchBuilder *patch.PatchBuilder,
2022-01-21 15:13:51 +02:00
) []string {
return renderAux(tree.GetRoot().Raw(), tree.CollapsedPaths(), "", -1, func(node *filetree.Node[models.CommitFile], depth int) string {
2022-01-21 15:13:51 +02:00
// 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 node.EveryFile(func(file *models.CommitFile) bool {
2023-03-19 07:09:03 +02:00
return patchBuilder.GetFileStatus(file.Name, tree.GetRef().RefName()) == patch.WHOLE
2022-01-21 15:13:51 +02:00
}) {
status = patch.WHOLE
} else if node.EveryFile(func(file *models.CommitFile) bool {
2023-03-19 07:09:03 +02:00
return patchBuilder.GetFileStatus(file.Name, tree.GetRef().RefName()) == patch.UNSELECTED
2022-01-21 15:13:51 +02:00
}) {
status = patch.UNSELECTED
} else {
status = patch.PART
}
return getCommitFileLine(commitFileNameAtDepth(node, depth), diffName, node.File, status)
2022-01-21 15:13:51 +02:00
})
}
func renderAux[T any](
node *filetree.Node[T],
2022-03-19 03:26:30 +02:00
collapsedPaths *filetree.CollapsedPaths,
2022-01-21 15:13:51 +02:00
prefix string,
depth int,
renderLine func(*filetree.Node[T], int) string,
2022-01-21 15:13:51 +02:00
) []string {
if node == nil {
2022-01-21 15:13:51 +02:00
return []string{}
}
isRoot := depth == -1
if node.IsFile() {
2022-01-21 15:13:51 +02:00
if isRoot {
return []string{}
}
return []string{prefix + renderLine(node, depth)}
2022-01-21 15:13:51 +02:00
}
if collapsedPaths.IsCollapsed(node.GetPath()) {
return []string{prefix + COLLAPSED_ARROW + " " + renderLine(node, depth)}
2022-01-21 15:13:51 +02:00
}
arr := []string{}
if !isRoot {
arr = append(arr, prefix+EXPANDED_ARROW+" "+renderLine(node, depth))
2022-01-21 15:13:51 +02:00
}
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 node.Children {
isLast := i == len(node.Children)-1
2022-01-21 15:13:51 +02:00
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+node.CompressionLevel, renderLine)...)
2022-01-21 15:13:51 +02:00
}
return arr
}
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 {
2022-01-29 01:15:55 +02:00
restColor = theme.UnstagedChangesColor
2022-01-21 15:13:51 +02:00
}
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 == "?" {
2022-01-29 01:15:55 +02:00
firstCharCl = theme.UnstagedChangesColor
2022-01-21 15:13:51 +02:00
} else if firstChar == " " {
firstCharCl = restColor
}
secondChar := file.ShortStatus[1:2]
2022-01-29 01:15:55 +02:00
secondCharCl := theme.UnstagedChangesColor
2022-01-21 15:13:51 +02:00
if secondChar == " " {
secondCharCl = restColor
}
output = firstCharCl.Sprint(firstChar)
output += secondCharCl.Sprint(secondChar)
output += restColor.Sprint(" ")
}
2022-04-23 03:14:42 +02:00
isSubmodule := file != nil && file.IsSubmodule(submoduleConfigs)
isLinkedWorktree := file != nil && file.IsWorktree
2022-04-23 03:14:42 +02:00
isDirectory := file == nil
2022-04-23 03:41:40 +02:00
if icons.IsIconEnabled() {
output += restColor.Sprintf("%s ", icons.IconForFile(name, isSubmodule, isLinkedWorktree, isDirectory))
2022-04-23 03:14:42 +02:00
}
2022-01-21 15:13:51 +02:00
output += restColor.Sprint(utils.EscapeSpecialChars(name))
2022-04-23 03:14:42 +02:00
if isSubmodule {
2022-01-21 15:13:51 +02:00
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
}
}
2022-04-23 03:14:42 +02:00
output := ""
2022-01-21 15:13:51 +02:00
name = utils.EscapeSpecialChars(name)
2022-04-23 03:14:42 +02:00
if commitFile != nil {
output += getColorForChangeStatus(commitFile.ChangeStatus).Sprint(commitFile.ChangeStatus) + " "
2022-01-21 15:13:51 +02:00
}
2022-04-29 10:53:43 +02:00
isSubmodule := false
isLinkedWorktree := false
2022-04-23 03:14:42 +02:00
isDirectory := commitFile == nil
2022-04-23 03:41:40 +02:00
if icons.IsIconEnabled() {
output += colour.Sprintf("%s ", icons.IconForFile(name, isSubmodule, isLinkedWorktree, isDirectory))
2022-04-23 03:14:42 +02:00
}
output += colour.Sprint(name)
return output
2022-01-21 15:13:51 +02:00
}
func getColorForChangeStatus(changeStatus string) style.TextStyle {
switch changeStatus {
case "A":
return style.FgGreen
case "M", "R":
return style.FgYellow
case "D":
2022-01-29 01:15:55 +02:00
return theme.UnstagedChangesColor
2022-01-21 15:13:51 +02:00
case "C":
return style.FgCyan
case "T":
return style.FgMagenta
default:
return theme.DefaultTextColor
}
}
func fileNameAtDepth(node *filetree.Node[models.File], depth int) string {
splitName := split(node.Path)
name := join(splitName[depth:])
if node.File != nil && node.File.IsRename() {
splitPrevName := split(node.File.PreviousName)
prevName := node.File.PreviousName
// if the file has just been renamed inside the same directory, we can shave off
// the prefix for the previous path too. Otherwise we'll keep it unchanged
sameParentDir := len(splitName) == len(splitPrevName) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
if sameParentDir {
prevName = join(splitPrevName[depth:])
}
return prevName + " → " + name
}
return name
}
func commitFileNameAtDepth(node *filetree.Node[models.CommitFile], depth int) string {
splitName := split(node.Path)
name := join(splitName[depth:])
return name
}
func split(str string) []string {
return strings.Split(str, "/")
}
func join(strs []string) string {
return strings.Join(strs, "/")
}