Independent filetree buffer + partial tree rendering (#18)
This commit is contained in:
parent
9625c51aa4
commit
41b6da6e93
@ -84,5 +84,5 @@ func initLogging() {
|
||||
}else{
|
||||
log.SetOutput(f)
|
||||
}
|
||||
log.Info("Starting Dive...")
|
||||
log.Debug("Starting Dive...")
|
||||
}
|
@ -45,8 +45,6 @@ func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
|
||||
return node
|
||||
}
|
||||
|
||||
// todo: make more performant
|
||||
// todo: rewrite with visitor functions
|
||||
func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) string {
|
||||
var otherBranches string
|
||||
for _, space := range spaces {
|
||||
@ -70,34 +68,6 @@ func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) s
|
||||
return otherBranches + thisBranch + collapsedIndicator + node.String() + newLine
|
||||
}
|
||||
|
||||
// todo: make more performant
|
||||
// todo: rewrite with visitor functions
|
||||
func (node *FileNode) renderStringTree(spaces []bool, showAttributes bool, depth int) string {
|
||||
var result string
|
||||
var keys []string
|
||||
for key := range node.Children {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for idx, name := range keys {
|
||||
child := node.Children[name]
|
||||
if child.Data.ViewInfo.Hidden {
|
||||
continue
|
||||
}
|
||||
last := idx == (len(node.Children) - 1)
|
||||
showCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0
|
||||
if showAttributes {
|
||||
result += child.MetadataString() + " "
|
||||
}
|
||||
result += child.renderTreeLine(spaces, last, showCollapsed)
|
||||
if len(child.Children) > 0 && !child.Data.ViewInfo.Collapsed {
|
||||
spacesChild := append(spaces, last)
|
||||
result += child.renderStringTree(spacesChild, showAttributes, depth+1)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (node *FileNode) Copy(parent *FileNode) *FileNode {
|
||||
newNode := NewNode(parent, node.Name, node.Data.FileInfo)
|
||||
newNode.Data.ViewInfo = node.Data.ViewInfo
|
||||
@ -241,19 +211,6 @@ func (node *FileNode) IsWhiteout() bool {
|
||||
return strings.HasPrefix(node.Name, whiteoutPrefix)
|
||||
}
|
||||
|
||||
// todo: make path() more efficient, similar to so (buggy):
|
||||
// func (node *FileNode) Path() string {
|
||||
// if node.path == "" {
|
||||
// path := "/"
|
||||
//
|
||||
// if node.Parent != nil {
|
||||
// path = node.Parent.Path()
|
||||
// }
|
||||
// node.path = path + "/" + strings.TrimPrefix(node.Name, whiteoutPrefix)
|
||||
// }
|
||||
// return node.path
|
||||
// }
|
||||
|
||||
func (node *FileNode) Path() string {
|
||||
if node.path == "" {
|
||||
path := []string{}
|
||||
|
115
filetree/tree.go
115
filetree/tree.go
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/satori/go.uuid"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -35,8 +36,95 @@ func NewFileTree() (tree *FileTree) {
|
||||
return tree
|
||||
}
|
||||
|
||||
type renderParams struct{
|
||||
node *FileNode
|
||||
spaces []bool
|
||||
childSpaces []bool
|
||||
showCollapsed bool
|
||||
isLast bool
|
||||
}
|
||||
|
||||
func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttributes bool) string {
|
||||
// generate a list of nodes to render
|
||||
var params []renderParams = make([]renderParams,0)
|
||||
var result string
|
||||
|
||||
// visit from the front of the list
|
||||
var paramsToVisit = []renderParams{ renderParams{node: tree.Root, spaces: []bool{}, showCollapsed: false, isLast: false} }
|
||||
for currentRow := 0; len(paramsToVisit) > 0 && currentRow <= stopRow; currentRow++ {
|
||||
// pop the first node
|
||||
var currentParams renderParams
|
||||
currentParams, paramsToVisit = paramsToVisit[0], paramsToVisit[1:]
|
||||
|
||||
// take note of the next nodes to visit later
|
||||
var keys []string
|
||||
for key := range currentParams.node.Children {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var childParams = make([]renderParams,0)
|
||||
for idx, name := range keys {
|
||||
child := currentParams.node.Children[name]
|
||||
// don't visit this node...
|
||||
if child.Data.ViewInfo.Hidden || currentParams.node.Data.ViewInfo.Collapsed {
|
||||
continue
|
||||
}
|
||||
|
||||
// visit this node...
|
||||
isLast := idx == (len(currentParams.node.Children) - 1)
|
||||
showCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0
|
||||
|
||||
// completely copy the reference slice
|
||||
childSpaces := make([]bool, len(currentParams.childSpaces))
|
||||
copy(childSpaces, currentParams.childSpaces)
|
||||
|
||||
if len(child.Children) > 0 && !child.Data.ViewInfo.Collapsed {
|
||||
childSpaces = append(childSpaces, isLast)
|
||||
}
|
||||
|
||||
childParams = append(childParams, renderParams{
|
||||
node: child,
|
||||
spaces: currentParams.childSpaces,
|
||||
childSpaces: childSpaces,
|
||||
showCollapsed: showCollapsed,
|
||||
isLast: isLast,
|
||||
})
|
||||
}
|
||||
// keep the child nodes to visit later
|
||||
paramsToVisit = append(childParams, paramsToVisit...)
|
||||
|
||||
// never process the root node
|
||||
if currentParams.node == tree.Root {
|
||||
currentRow--
|
||||
continue
|
||||
}
|
||||
|
||||
// process the current node
|
||||
if currentRow >= startRow && currentRow <= stopRow {
|
||||
params = append(params, currentParams)
|
||||
}
|
||||
}
|
||||
|
||||
// render the result
|
||||
for idx := range params {
|
||||
currentParams := params[idx]
|
||||
|
||||
if showAttributes {
|
||||
result += currentParams.node.MetadataString() + " "
|
||||
}
|
||||
result += currentParams.node.renderTreeLine(currentParams.spaces, currentParams.isLast, currentParams.showCollapsed)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (tree *FileTree) String(showAttributes bool) string {
|
||||
return tree.Root.renderStringTree([]bool{}, showAttributes, 0)
|
||||
return tree.renderStringTreeBetween(0, tree.Size, showAttributes)
|
||||
}
|
||||
|
||||
func (tree *FileTree) StringBetween(start, stop uint, showAttributes bool) string {
|
||||
return tree.renderStringTreeBetween(int(start), int(stop), showAttributes)
|
||||
}
|
||||
|
||||
func (tree *FileTree) Copy() *FileTree {
|
||||
@ -166,37 +254,12 @@ func (tree *FileTree) MarkRemoved(path string) error {
|
||||
return node.AssignDiffType(Removed)
|
||||
}
|
||||
|
||||
// memoize StackRange for performance
|
||||
type stackRangeCacheKey struct {
|
||||
// Ids mapset.Set
|
||||
start, stop int
|
||||
}
|
||||
|
||||
var stackRangeCache = make(map[stackRangeCacheKey]*FileTree)
|
||||
|
||||
func StackRange(trees []*FileTree, start, stop int) *FileTree {
|
||||
|
||||
// var ids []interface{}
|
||||
//
|
||||
// for _, tree := range trees {
|
||||
// ids = append(ids, tree.Id)
|
||||
// }
|
||||
//mapset.NewSetFromSlice(ids)
|
||||
// key := stackRangeCacheKey{start, stop}
|
||||
//
|
||||
//
|
||||
// cachedResult, ok := stackRangeCache[key]
|
||||
// if ok {
|
||||
// return cachedResult
|
||||
// }
|
||||
|
||||
tree := trees[0].Copy()
|
||||
for idx := start; idx <= stop; idx++ {
|
||||
tree.Stack(trees[idx])
|
||||
}
|
||||
|
||||
// stackRangeCache[key] = tree
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
|
@ -23,18 +23,36 @@ func AssertDiffType(node *FileNode, expectedDiffType DiffType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPrintTree(t *testing.T) {
|
||||
func TestStringCollapsed(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
tree.Root.AddChild("first node!", FileInfo{})
|
||||
two := tree.Root.AddChild("second node!", FileInfo{})
|
||||
tree.Root.AddChild("third node!", FileInfo{})
|
||||
two.AddChild("forth, one level down...", FileInfo{})
|
||||
tree.Root.AddChild("1 node!", FileInfo{})
|
||||
two := tree.Root.AddChild("2 node!", FileInfo{})
|
||||
subTwo := two.AddChild("2 child!", FileInfo{})
|
||||
subTwo.AddChild("2 grandchild!", FileInfo{})
|
||||
subTwo.Data.ViewInfo.Collapsed = true
|
||||
three := tree.Root.AddChild("3 node!", FileInfo{})
|
||||
subThree := three.AddChild("3 child!", FileInfo{})
|
||||
three.AddChild("3 nested child 1!", FileInfo{})
|
||||
threeGc1 := subThree.AddChild("3 grandchild 1!", FileInfo{})
|
||||
threeGc1.AddChild("3 greatgrandchild 1!", FileInfo{})
|
||||
subThree.AddChild("3 grandchild 2!", FileInfo{})
|
||||
four := tree.Root.AddChild("4 node!", FileInfo{})
|
||||
four.Data.ViewInfo.Collapsed = true
|
||||
tree.Root.AddChild("5 node!", FileInfo{})
|
||||
four.AddChild("6, one level down...", FileInfo{})
|
||||
|
||||
expected :=
|
||||
`├── first node!
|
||||
├── second node!
|
||||
│ └── forth, one level down...
|
||||
└── third node!
|
||||
`├── 1 node!
|
||||
├── 2 node!
|
||||
│ └─⊕ 2 child!
|
||||
├── 3 node!
|
||||
│ ├── 3 child!
|
||||
│ │ ├── 3 grandchild 1!
|
||||
│ │ │ └── 3 greatgrandchild 1!
|
||||
│ │ └── 3 grandchild 2!
|
||||
│ └── 3 nested child 1!
|
||||
├─⊕ 4 node!
|
||||
└── 5 node!
|
||||
`
|
||||
actual := tree.String(false)
|
||||
|
||||
@ -44,6 +62,53 @@ func TestPrintTree(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
tree.Root.AddChild("1 node!", FileInfo{})
|
||||
tree.Root.AddChild("2 node!", FileInfo{})
|
||||
tree.Root.AddChild("3 node!", FileInfo{})
|
||||
four := tree.Root.AddChild("4 node!", FileInfo{})
|
||||
tree.Root.AddChild("5 node!", FileInfo{})
|
||||
four.AddChild("6, one level down...", FileInfo{})
|
||||
|
||||
expected :=
|
||||
`├── 1 node!
|
||||
├── 2 node!
|
||||
├── 3 node!
|
||||
├── 4 node!
|
||||
│ └── 6, one level down...
|
||||
└── 5 node!
|
||||
`
|
||||
actual := tree.String(false)
|
||||
|
||||
if expected != actual {
|
||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStringBetween(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
tree.AddPath("/etc/nginx/public", FileInfo{})
|
||||
tree.AddPath("/var/run/systemd", FileInfo{})
|
||||
tree.AddPath("/var/run/bashful", FileInfo{})
|
||||
tree.AddPath("/tmp", FileInfo{})
|
||||
tree.AddPath("/tmp/nonsense", FileInfo{})
|
||||
|
||||
expected :=
|
||||
`│ └── public
|
||||
├── tmp
|
||||
│ └── nonsense
|
||||
`
|
||||
actual := tree.StringBetween(3, 5, false)
|
||||
|
||||
if expected != actual {
|
||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAddPath(t *testing.T) {
|
||||
tree := NewFileTree()
|
||||
tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
||||
|
@ -133,7 +133,7 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
|
||||
var trees []*filetree.FileTree = make([]*filetree.FileTree, 0)
|
||||
|
||||
// save this image to disk temporarily to get the content info
|
||||
// fmt.Println("Fetching image...")
|
||||
fmt.Println("Fetching image...")
|
||||
imageTarPath, tmpDir := saveImage(imageID)
|
||||
// imageTarPath := "/tmp/dive932744808/image.tar"
|
||||
// tmpDir := "/tmp/dive031537738"
|
||||
|
@ -19,16 +19,18 @@ type CompareType int
|
||||
|
||||
|
||||
type FileTreeView struct {
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
ModelTree *filetree.FileTree
|
||||
ViewTree *filetree.FileTree
|
||||
RefTrees []*filetree.FileTree
|
||||
HiddenDiffTypes []bool
|
||||
TreeIndex int
|
||||
|
||||
Name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
ModelTree *filetree.FileTree
|
||||
ViewTree *filetree.FileTree
|
||||
RefTrees []*filetree.FileTree
|
||||
HiddenDiffTypes []bool
|
||||
TreeIndex uint
|
||||
bufferIndex uint
|
||||
bufferIndexUpperBound uint
|
||||
bufferIndexLowerBound uint
|
||||
}
|
||||
|
||||
func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree) (treeview *FileTreeView) {
|
||||
@ -80,12 +82,20 @@ func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error {
|
||||
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
|
||||
}
|
||||
|
||||
func (view *FileTreeView) height() uint {
|
||||
_, height := view.view.Size()
|
||||
return uint(height - 2)
|
||||
}
|
||||
|
||||
func (view *FileTreeView) IsVisible() bool {
|
||||
if view == nil {return false}
|
||||
return true
|
||||
@ -93,9 +103,9 @@ func (view *FileTreeView) IsVisible() bool {
|
||||
|
||||
|
||||
func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
|
||||
//if stopIdx > len(view.RefTrees)-1 {
|
||||
// return errors.New(fmt.Sprintf("Invalid layer index given: %d of %d", stopIdx, len(view.RefTrees)-1))
|
||||
//}
|
||||
if topTreeStop > len(view.RefTrees)-1 {
|
||||
return fmt.Errorf("Invalid layer index given: %d of %d", topTreeStop, len(view.RefTrees)-1)
|
||||
}
|
||||
newTree := filetree.StackRange(view.RefTrees, bottomTreeStart, bottomTreeStop)
|
||||
|
||||
for idx := topTreeStart; idx <= topTreeStop; idx++ {
|
||||
@ -119,30 +129,57 @@ func (view *FileTreeView) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTre
|
||||
return view.Render()
|
||||
}
|
||||
|
||||
func (view *FileTreeView) CursorDown() error {
|
||||
// cannot easily (quickly) check the model length, allow the view
|
||||
// to let us know what is a valid bounds (i.e. when it hits an empty line)
|
||||
err := CursorDown(view.gui, view.view)
|
||||
if err == nil {
|
||||
view.TreeIndex++
|
||||
func (view *FileTreeView) doCursorUp() {
|
||||
view.TreeIndex--
|
||||
if view.TreeIndex < view.bufferIndexLowerBound {
|
||||
view.bufferIndexUpperBound--
|
||||
view.bufferIndexLowerBound--
|
||||
}
|
||||
|
||||
if view.bufferIndex > 0 {
|
||||
view.bufferIndex--
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *FileTreeView) CursorDown() error {
|
||||
// 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.
|
||||
|
||||
view.doCursorDown()
|
||||
return view.Render()
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (view *FileTreeView) CursorUp() error {
|
||||
// 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.
|
||||
|
||||
if view.TreeIndex > 0 {
|
||||
err := CursorUp(view.gui, view.view)
|
||||
if err == nil {
|
||||
view.TreeIndex--
|
||||
}
|
||||
view.doCursorUp()
|
||||
return view.Render()
|
||||
}
|
||||
return view.Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) {
|
||||
var visiter func(*filetree.FileNode) error
|
||||
var evaluator func(*filetree.FileNode) bool
|
||||
var dfsCounter int
|
||||
var dfsCounter uint
|
||||
|
||||
visiter = func(curNode *filetree.FileNode) error {
|
||||
if dfsCounter == view.TreeIndex {
|
||||
@ -254,8 +291,14 @@ func (view *FileTreeView) KeyHelp() string {
|
||||
}
|
||||
|
||||
func (view *FileTreeView) Render() error {
|
||||
// print the tree to the view
|
||||
lines := strings.Split(view.ViewTree.String(true), "\n")
|
||||
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()
|
||||
}
|
||||
|
||||
view.gui.Update(func(g *gocui.Gui) error {
|
||||
// update the header
|
||||
view.header.Clear()
|
||||
@ -265,7 +308,7 @@ func (view *FileTreeView) Render() error {
|
||||
// update the contents
|
||||
view.view.Clear()
|
||||
for idx, line := range lines {
|
||||
if idx == view.TreeIndex {
|
||||
if uint(idx) == view.bufferIndex {
|
||||
fmt.Fprintln(view.view, Formatting.Selected(vtclean.Clean(line, false)))
|
||||
} else {
|
||||
fmt.Fprintln(view.view, line)
|
||||
|
Loading…
x
Reference in New Issue
Block a user