Independent filetree buffer + partial tree rendering (#18)

This commit is contained in:
Alex Goodman 2018-10-07 19:50:23 -04:00 committed by GitHub
parent 9625c51aa4
commit 41b6da6e93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 236 additions and 108 deletions

View File

@ -84,5 +84,5 @@ func initLogging() {
}else{
log.SetOutput(f)
}
log.Info("Starting Dive...")
log.Debug("Starting Dive...")
}

View File

@ -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{}

View File

@ -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
}

View File

@ -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{})

View File

@ -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"

View File

@ -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)