584 lines
18 KiB
Go
584 lines
18 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/viper"
|
|
"github.com/wagoodman/dive/utils"
|
|
"github.com/wagoodman/keybinding"
|
|
"log"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/jroimartin/gocui"
|
|
"github.com/lunixbochs/vtclean"
|
|
"github.com/wagoodman/dive/filetree"
|
|
)
|
|
|
|
const (
|
|
CompareLayer CompareType = iota
|
|
CompareAll
|
|
)
|
|
|
|
type CompareType int
|
|
|
|
// FileTreeView holds the UI objects and data models for populating the right pane. Specifically the pane that
|
|
// shows selected layer or aggregate file ASCII tree.
|
|
type FileTreeView struct {
|
|
Name string
|
|
gui *gocui.Gui
|
|
view *gocui.View
|
|
header *gocui.View
|
|
ModelTree *filetree.FileTree
|
|
ViewTree *filetree.FileTree
|
|
RefTrees []*filetree.FileTree
|
|
cache filetree.TreeCache
|
|
HiddenDiffTypes []bool
|
|
TreeIndex uint
|
|
bufferIndex uint
|
|
bufferIndexUpperBound uint
|
|
bufferIndexLowerBound uint
|
|
|
|
keybindingToggleCollapse []keybinding.Key
|
|
keybindingToggleAdded []keybinding.Key
|
|
keybindingToggleRemoved []keybinding.Key
|
|
keybindingToggleModified []keybinding.Key
|
|
keybindingToggleUnchanged []keybinding.Key
|
|
keybindingPageDown []keybinding.Key
|
|
keybindingPageUp []keybinding.Key
|
|
}
|
|
|
|
// NewFileTreeView creates a new view object attached the the global [gocui] screen object.
|
|
func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeView *FileTreeView) {
|
|
treeView = new(FileTreeView)
|
|
|
|
// populate main fields
|
|
treeView.Name = name
|
|
treeView.gui = gui
|
|
treeView.ModelTree = tree
|
|
treeView.RefTrees = refTrees
|
|
treeView.cache = cache
|
|
treeView.HiddenDiffTypes = make([]bool, 4)
|
|
|
|
hiddenTypes := viper.GetStringSlice("diff.hide")
|
|
for _, hType := range hiddenTypes {
|
|
switch t := strings.ToLower(hType); t {
|
|
case "added":
|
|
treeView.HiddenDiffTypes[filetree.Added] = true
|
|
case "removed":
|
|
treeView.HiddenDiffTypes[filetree.Removed] = true
|
|
case "changed":
|
|
treeView.HiddenDiffTypes[filetree.Changed] = true
|
|
case "unchanged":
|
|
treeView.HiddenDiffTypes[filetree.Unchanged] = true
|
|
default:
|
|
utils.PrintAndExit(fmt.Sprintf("unknown diff.hide value: %s", t))
|
|
}
|
|
}
|
|
|
|
var err error
|
|
treeView.keybindingToggleCollapse, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-dir"))
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
treeView.keybindingToggleAdded, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-added-files"))
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
treeView.keybindingToggleRemoved, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-removed-files"))
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
treeView.keybindingToggleModified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-modified-files"))
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
treeView.keybindingToggleUnchanged, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unchanged-files"))
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
treeView.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
treeView.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
return treeView
|
|
}
|
|
|
|
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
|
func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error {
|
|
|
|
// set view options
|
|
view.view = v
|
|
view.view.Editable = false
|
|
view.view.Wrap = false
|
|
view.view.Frame = false
|
|
|
|
view.header = header
|
|
view.header.Editable = false
|
|
view.header.Wrap = false
|
|
view.header.Frame = false
|
|
|
|
// set keybindings
|
|
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorDown() }); err != nil {
|
|
return err
|
|
}
|
|
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorUp() }); err != nil {
|
|
return err
|
|
}
|
|
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorLeft() }); err != nil {
|
|
return err
|
|
}
|
|
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorRight() }); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, key := range view.keybindingPageUp {
|
|
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.PageUp() }); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, key := range view.keybindingPageDown {
|
|
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.PageDown() }); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, key := range view.keybindingToggleCollapse {
|
|
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleCollapse() }); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, key := range view.keybindingToggleAdded {
|
|
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Added) }); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, key := range view.keybindingToggleRemoved {
|
|
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Removed) }); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, key := range view.keybindingToggleModified {
|
|
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Changed) }); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, key := range view.keybindingToggleUnchanged {
|
|
if err := view.gui.SetKeybinding(view.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Unchanged) }); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
view.bufferIndexLowerBound = 0
|
|
view.bufferIndexUpperBound = view.height() // don't include the header or footer in the view size
|
|
|
|
view.Update()
|
|
view.Render()
|
|
|
|
return nil
|
|
}
|
|
|
|
// height obtains the height of the current pane (taking into account the lost space due to headers and footers).
|
|
func (view *FileTreeView) height() uint {
|
|
_, height := view.view.Size()
|
|
return uint(height - 2)
|
|
}
|
|
|
|
// IsVisible indicates if the file tree view pane is currently initialized
|
|
func (view *FileTreeView) IsVisible() bool {
|
|
if view == nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
|
|
func (view *FileTreeView) resetCursor() {
|
|
view.view.SetCursor(0, 0)
|
|
view.TreeIndex = 0
|
|
view.bufferIndex = 0
|
|
view.bufferIndexLowerBound = 0
|
|
view.bufferIndexUpperBound = view.height()
|
|
}
|
|
|
|
// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
|
|
func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
|
|
if topTreeStop > len(view.RefTrees)-1 {
|
|
return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(view.RefTrees)-1)
|
|
}
|
|
newTree := view.cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
|
|
|
|
// preserve view state on copy
|
|
visitor := func(node *filetree.FileNode) error {
|
|
newNode, err := newTree.GetNode(node.Path())
|
|
if err == nil {
|
|
newNode.Data.ViewInfo = node.Data.ViewInfo
|
|
}
|
|
return nil
|
|
}
|
|
view.ModelTree.VisitDepthChildFirst(visitor, nil)
|
|
|
|
view.resetCursor()
|
|
|
|
view.ModelTree = newTree
|
|
view.Update()
|
|
return view.Render()
|
|
}
|
|
|
|
// doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer.
|
|
func (view *FileTreeView) doCursorUp() {
|
|
view.TreeIndex--
|
|
if view.TreeIndex < view.bufferIndexLowerBound {
|
|
view.bufferIndexUpperBound--
|
|
view.bufferIndexLowerBound--
|
|
}
|
|
|
|
if view.bufferIndex > 0 {
|
|
view.bufferIndex--
|
|
}
|
|
}
|
|
|
|
// doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer.
|
|
func (view *FileTreeView) doCursorDown() {
|
|
view.TreeIndex++
|
|
if view.TreeIndex > view.bufferIndexUpperBound {
|
|
view.bufferIndexUpperBound++
|
|
view.bufferIndexLowerBound++
|
|
}
|
|
view.bufferIndex++
|
|
if view.bufferIndex > view.height() {
|
|
view.bufferIndex = view.height()
|
|
}
|
|
}
|
|
|
|
// CursorDown moves the cursor down and renders the view.
|
|
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
|
|
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
|
|
// this range into the view buffer. This is much faster when tree sizes are large.
|
|
func (view *FileTreeView) CursorDown() error {
|
|
view.doCursorDown()
|
|
return view.Render()
|
|
}
|
|
|
|
// CursorUp moves the cursor up and renders the view.
|
|
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
|
|
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
|
|
// this range into the view buffer. This is much faster when tree sizes are large.
|
|
func (view *FileTreeView) CursorUp() error {
|
|
if view.TreeIndex > 0 {
|
|
view.doCursorUp()
|
|
return view.Render()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
|
|
func (view *FileTreeView) CursorLeft() error {
|
|
var visitor func(*filetree.FileNode) error
|
|
var evaluator func(*filetree.FileNode) bool
|
|
var dfsCounter, newIndex uint
|
|
oldIndex := view.TreeIndex
|
|
currentNode := view.getAbsPositionNode()
|
|
if currentNode == nil {
|
|
return nil
|
|
}
|
|
parentPath := currentNode.Parent.Path()
|
|
|
|
visitor = func(curNode *filetree.FileNode) error {
|
|
if strings.Compare(parentPath, curNode.Path()) == 0 {
|
|
newIndex = dfsCounter
|
|
}
|
|
dfsCounter++
|
|
return nil
|
|
}
|
|
var filterBytes []byte
|
|
var filterRegex *regexp.Regexp
|
|
read, err := Views.Filter.view.Read(filterBytes)
|
|
if read > 0 && err == nil {
|
|
regex, err := regexp.Compile(string(filterBytes))
|
|
if err == nil {
|
|
filterRegex = regex
|
|
}
|
|
}
|
|
|
|
evaluator = func(curNode *filetree.FileNode) bool {
|
|
regexMatch := true
|
|
if filterRegex != nil {
|
|
match := filterRegex.Find([]byte(curNode.Path()))
|
|
regexMatch = match != nil
|
|
}
|
|
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
|
|
}
|
|
|
|
err = view.ModelTree.VisitDepthParentFirst(visitor, evaluator)
|
|
if err != nil {
|
|
logrus.Panic(err)
|
|
}
|
|
|
|
view.TreeIndex = newIndex
|
|
moveIndex := oldIndex - newIndex
|
|
if newIndex < view.bufferIndexLowerBound {
|
|
view.bufferIndexUpperBound = view.TreeIndex + view.height()
|
|
view.bufferIndexLowerBound = view.TreeIndex
|
|
}
|
|
|
|
if view.bufferIndex > moveIndex {
|
|
view.bufferIndex = view.bufferIndex - moveIndex
|
|
} else {
|
|
view.bufferIndex = 0
|
|
}
|
|
|
|
view.Update()
|
|
return view.Render()
|
|
}
|
|
|
|
// CursorRight descends into directory expanding it if needed
|
|
func (view *FileTreeView) CursorRight() error {
|
|
node := view.getAbsPositionNode()
|
|
if node == nil {
|
|
return nil
|
|
}
|
|
if !node.Data.FileInfo.IsDir {
|
|
return nil
|
|
}
|
|
if len(node.Children) == 0 {
|
|
return nil
|
|
}
|
|
if node.Data.ViewInfo.Collapsed {
|
|
node.Data.ViewInfo.Collapsed = false
|
|
}
|
|
view.TreeIndex++
|
|
if view.TreeIndex > view.bufferIndexUpperBound {
|
|
view.bufferIndexUpperBound++
|
|
view.bufferIndexLowerBound++
|
|
}
|
|
view.bufferIndex++
|
|
if view.bufferIndex > view.height() {
|
|
view.bufferIndex = view.height()
|
|
}
|
|
view.Update()
|
|
return view.Render()
|
|
}
|
|
|
|
// PageDown moves to next page putting the cursor on top
|
|
func (view *FileTreeView) PageDown() error {
|
|
nextBufferIndexLowerBound := view.bufferIndexLowerBound + view.height()
|
|
nextBufferIndexUpperBound := view.bufferIndexUpperBound + view.height()
|
|
|
|
treeString := view.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, true)
|
|
lines := strings.Split(treeString, "\n")
|
|
|
|
newLines := uint(len(lines)) - 1
|
|
if view.height() >= newLines {
|
|
nextBufferIndexLowerBound = view.bufferIndexLowerBound + newLines
|
|
nextBufferIndexUpperBound = view.bufferIndexUpperBound + newLines
|
|
}
|
|
view.bufferIndexLowerBound = nextBufferIndexLowerBound
|
|
view.bufferIndexUpperBound = nextBufferIndexUpperBound
|
|
|
|
if view.TreeIndex < nextBufferIndexLowerBound {
|
|
view.bufferIndex = 0
|
|
view.TreeIndex = nextBufferIndexLowerBound
|
|
} else {
|
|
view.bufferIndex = view.bufferIndex - newLines
|
|
}
|
|
return view.Render()
|
|
}
|
|
|
|
// PageUp moves to previous page putting the cursor on top
|
|
func (view *FileTreeView) PageUp() error {
|
|
nextBufferIndexLowerBound := view.bufferIndexLowerBound - view.height()
|
|
nextBufferIndexUpperBound := view.bufferIndexUpperBound - view.height()
|
|
|
|
treeString := view.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, true)
|
|
lines := strings.Split(treeString, "\n")
|
|
|
|
newLines := uint(len(lines)) - 2
|
|
if view.height() >= newLines {
|
|
nextBufferIndexLowerBound = view.bufferIndexLowerBound - newLines
|
|
nextBufferIndexUpperBound = view.bufferIndexUpperBound - newLines
|
|
}
|
|
view.bufferIndexLowerBound = nextBufferIndexLowerBound
|
|
view.bufferIndexUpperBound = nextBufferIndexUpperBound
|
|
|
|
if view.TreeIndex > (nextBufferIndexUpperBound - 1) {
|
|
view.bufferIndex = 0
|
|
view.TreeIndex = nextBufferIndexLowerBound
|
|
} else {
|
|
view.bufferIndex = view.bufferIndex + newLines
|
|
}
|
|
return view.Render()
|
|
}
|
|
|
|
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
|
|
func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
|
|
var visitor func(*filetree.FileNode) error
|
|
var evaluator func(*filetree.FileNode) bool
|
|
var dfsCounter uint
|
|
|
|
visitor = func(curNode *filetree.FileNode) error {
|
|
if dfsCounter == view.TreeIndex {
|
|
node = curNode
|
|
}
|
|
dfsCounter++
|
|
return nil
|
|
}
|
|
var filterBytes []byte
|
|
var filterRegex *regexp.Regexp
|
|
read, err := Views.Filter.view.Read(filterBytes)
|
|
if read > 0 && err == nil {
|
|
regex, err := regexp.Compile(string(filterBytes))
|
|
if err == nil {
|
|
filterRegex = regex
|
|
}
|
|
}
|
|
|
|
evaluator = func(curNode *filetree.FileNode) bool {
|
|
regexMatch := true
|
|
if filterRegex != nil {
|
|
match := filterRegex.Find([]byte(curNode.Path()))
|
|
regexMatch = match != nil
|
|
}
|
|
return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch
|
|
}
|
|
|
|
err = view.ModelTree.VisitDepthParentFirst(visitor, evaluator)
|
|
if err != nil {
|
|
logrus.Panic(err)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
// toggleCollapse will collapse/expand the selected FileNode.
|
|
func (view *FileTreeView) toggleCollapse() error {
|
|
node := view.getAbsPositionNode()
|
|
if node != nil {
|
|
node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed
|
|
}
|
|
view.Update()
|
|
return view.Render()
|
|
}
|
|
|
|
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
|
|
func (view *FileTreeView) toggleShowDiffType(diffType filetree.DiffType) error {
|
|
view.HiddenDiffTypes[diffType] = !view.HiddenDiffTypes[diffType]
|
|
|
|
view.resetCursor()
|
|
|
|
Update()
|
|
Render()
|
|
return nil
|
|
}
|
|
|
|
// filterRegex will return a regular expression object to match the user's filter input.
|
|
func filterRegex() *regexp.Regexp {
|
|
if Views.Filter == nil || Views.Filter.view == nil {
|
|
return nil
|
|
}
|
|
filterString := strings.TrimSpace(Views.Filter.view.Buffer())
|
|
if len(filterString) == 0 {
|
|
return nil
|
|
}
|
|
|
|
regex, err := regexp.Compile(filterString)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return regex
|
|
}
|
|
|
|
// Update refreshes the state objects for future rendering.
|
|
func (view *FileTreeView) Update() error {
|
|
regex := filterRegex()
|
|
|
|
// keep the view selection in parity with the current DiffType selection
|
|
view.ModelTree.VisitDepthChildFirst(func(node *filetree.FileNode) error {
|
|
node.Data.ViewInfo.Hidden = view.HiddenDiffTypes[node.Data.DiffType]
|
|
visibleChild := false
|
|
for _, child := range node.Children {
|
|
if !child.Data.ViewInfo.Hidden {
|
|
visibleChild = true
|
|
}
|
|
}
|
|
if regex != nil && !visibleChild {
|
|
match := regex.FindString(node.Path())
|
|
node.Data.ViewInfo.Hidden = len(match) == 0
|
|
}
|
|
return nil
|
|
}, nil)
|
|
|
|
// make a new tree with only visible nodes
|
|
view.ViewTree = view.ModelTree.Copy()
|
|
view.ViewTree.VisitDepthParentFirst(func(node *filetree.FileNode) error {
|
|
if node.Data.ViewInfo.Hidden {
|
|
view.ViewTree.RemovePath(node.Path())
|
|
}
|
|
return nil
|
|
}, nil)
|
|
return nil
|
|
}
|
|
|
|
// Render flushes the state objects (file tree) to the pane.
|
|
func (view *FileTreeView) Render() error {
|
|
treeString := view.ViewTree.StringBetween(view.bufferIndexLowerBound, view.bufferIndexUpperBound, true)
|
|
lines := strings.Split(treeString, "\n")
|
|
|
|
// undo a cursor down that has gone past bottom of the visible tree
|
|
if view.bufferIndex >= uint(len(lines))-1 {
|
|
view.doCursorUp()
|
|
}
|
|
|
|
title := "Current Layer Contents"
|
|
if Views.Layer.CompareMode == CompareAll {
|
|
title = "Aggregated Layer Contents"
|
|
}
|
|
|
|
// indicate when selected
|
|
if view.gui.CurrentView() == view.view {
|
|
title = "● " + title
|
|
}
|
|
|
|
view.gui.Update(func(g *gocui.Gui) error {
|
|
// update the header
|
|
view.header.Clear()
|
|
width, _ := g.Size()
|
|
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
|
|
headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
|
|
fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
|
|
|
// update the contents
|
|
view.view.Clear()
|
|
for idx, line := range lines {
|
|
if uint(idx) == view.bufferIndex {
|
|
fmt.Fprintln(view.view, Formatting.Selected(vtclean.Clean(line, false)))
|
|
} else {
|
|
fmt.Fprintln(view.view, line)
|
|
}
|
|
}
|
|
// todo: should we check error on the view println?
|
|
return nil
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
|
func (view *FileTreeView) KeyHelp() string {
|
|
return renderStatusOption(view.keybindingToggleCollapse[0].String(), "Collapse dir", false) +
|
|
renderStatusOption(view.keybindingToggleAdded[0].String(), "Added files", !view.HiddenDiffTypes[filetree.Added]) +
|
|
renderStatusOption(view.keybindingToggleRemoved[0].String(), "Removed files", !view.HiddenDiffTypes[filetree.Removed]) +
|
|
renderStatusOption(view.keybindingToggleModified[0].String(), "Modified files", !view.HiddenDiffTypes[filetree.Changed]) +
|
|
renderStatusOption(view.keybindingToggleUnchanged[0].String(), "Unmodified files", !view.HiddenDiffTypes[filetree.Unchanged])
|
|
}
|