From 41b6da6e93f50c5a94238b68bc85a3cc9339daaa Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sun, 7 Oct 2018 19:50:23 -0400 Subject: [PATCH] Independent filetree buffer + partial tree rendering (#18) --- cmd/root.go | 2 +- filetree/node.go | 43 ---------------- filetree/tree.go | 115 ++++++++++++++++++++++++++++++++---------- filetree/tree_test.go | 83 ++++++++++++++++++++++++++---- image/image.go | 2 +- ui/filetreeview.go | 99 ++++++++++++++++++++++++++---------- 6 files changed, 236 insertions(+), 108 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index a0b828f..5fce13f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -84,5 +84,5 @@ func initLogging() { }else{ log.SetOutput(f) } - log.Info("Starting Dive...") + log.Debug("Starting Dive...") } \ No newline at end of file diff --git a/filetree/node.go b/filetree/node.go index 4231cae..b000f2e 100644 --- a/filetree/node.go +++ b/filetree/node.go @@ -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{} diff --git a/filetree/tree.go b/filetree/tree.go index 753f578..a7bfddb 100644 --- a/filetree/tree.go +++ b/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 } diff --git a/filetree/tree_test.go b/filetree/tree_test.go index a6b54a0..2711ede 100644 --- a/filetree/tree_test.go +++ b/filetree/tree_test.go @@ -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{}) diff --git a/image/image.go b/image/image.go index 38b5535..c2957b9 100644 --- a/image/image.go +++ b/image/image.go @@ -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" diff --git a/ui/filetreeview.go b/ui/filetreeview.go index 05eb56a..cec927c 100644 --- a/ui/filetreeview.go +++ b/ui/filetreeview.go @@ -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)