Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement TreeMenu, an interactive REPL folding-tree menu #3

Merged
merged 2 commits into from
Jun 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name = "FoldingTrees"
uuid = "1eca21be-9b9b-4ed8-839a-6d8ae26b1781"
authors = ["Tim Holy <tim.holy@gmail.com>"]
version = "0.1.0"
version = "1.0.0"

[deps]
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

[compat]
AbstractTrees = "0.3"
Expand Down
54 changes: 52 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
[![Codecov](https://codecov.io/gh/JuliaCollections/FoldingTrees.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaCollections/FoldingTrees.jl)

FoldingTrees implements a dynamic [tree structure](https://en.wikipedia.org/wiki/Tree_%28data_structure%29) in which some nodes may be "folded," i.e., marked to avoid descent among that node's children.
It also supports interactive text menus based on folding trees.

## Creating trees with `Node`

For example, after saying `using FoldingTrees`, a "table of contents" like

Expand All @@ -30,13 +33,14 @@ You don't have to create them in this exact order, the only constraint is that y
In general you create a node as `Node(data, parent)`, where `data` can be any type.
The root node is the only one that you create without a parent, i.e., `root = Node(data)`, and the type of data used to create `root` is enforced for all leaves of the tree.
You can specify the type with `root = Node{T}(data)` if necessary.
There is no explicit limit on the number of children that a node may have.

Using the [AbstractTrees](https://github.com/JuliaCollections/AbstractTrees.jl) package,
the tree above displays as

```julia
julia> print_tree(root)

├─ Introduction
│ ├─ Some background
│ │ ├─ Stuff you should have learned in high school
Expand All @@ -52,7 +56,7 @@ julia> fold!(chap1A)
true

julia> print_tree(root)

├─ Introduction
│ ├─ + Some background
│ └─ Defining the problem
Expand All @@ -67,3 +71,49 @@ There are a few utilities that you can learn about by reading their docstrings:
- `count_open_leaves`: count the number of nodes in the tree above the first fold on all branches
- `next`, `prev`: efficient ordered visitation of open nodes (depth-first)
- `nodes`: access nodes, rather than their data, during iteration (example: `foreach(unfold!, nodes(root)))`

## TreeMenu

On suitable Julia versions (ones for which `isdefined(REPL.TerminalMenus, :ConfiguredMenu)` is `true`,
a.k.a. `1.6.0-DEV.201` or higher), you can use such trees to create interactive menus via
[TerminalMenus](https://docs.julialang.org/en/v1.6-dev/stdlib/REPL/#TerminalMenus-1).

Suppose `root` is the same `Node` we created above, in the original unfolded state.
Then

```julia
julia> using REPL.TerminalMenus

julia> menu = TreeMenu(root)

julia> choice = request(menu; cursor=2)

> Introduction
Some background
Stuff you should have learned in high school
Stuff even Einstein didn't know
Defining the problem
How to solve it
```

The initial blank line is due to our `root`, which displays as an empty string; we set the initial "cursor position,"
indicated visually by `>`, to skip over that item.
You can use the up/down arrow keys to navigate over different items in the menu.
Choose an item by hitting `Enter`, toggle folding at the cursor position by hitting the space bar:

```julia
julia> choice = request(menu; cursor=2)

Introduction
> + Some background
Defining the problem
How to solve it
```

One can create very large menus with thousands of options, in which case the menu scrolls with the arrow keys
and `PgUp`/`PgDn`.
As described in the `TerminalMenus` documentation, you can customize aspects of the menu's appearance,
such as the number of items visible simultaneously and the characters used to indicate scrolling and the cursor position.

For `Node` objects where `data` is not an `AbstractString`, you will most likely want to specialize `FoldingTrees.writeoption` for your type.
See `?FoldingTrees.writeoption` for details.
93 changes: 0 additions & 93 deletions docs/Manifest.toml

This file was deleted.

194 changes: 5 additions & 189 deletions src/FoldingTrees.jl
Original file line number Diff line number Diff line change
@@ -1,199 +1,15 @@
module FoldingTrees

using REPL.TerminalMenus
using AbstractTrees

export Node, toggle!, fold!, unfold!, isroot, count_open_leaves, next, prev, nodes

mutable struct Node{Data}
data::Data
children::Vector{Node{Data}}
foldchildren::Bool
lastbranch::Bool # used internally for working storage during `iterate`
parent::Node{Data}
include("foldingtree.jl")
include("abstracttrees.jl")

"""
Node(data, foldchildren::Bool=false)

Create the root of a folded tree. `data` holds the data associated with the root node,
and `foldchildren` specifies whether its children are initially folded.
"""
function Node{Data}(data, foldchildren::Bool=false) where Data
new(data, Node[], foldchildren, false)
end

"""
Node(data, parent::Node, foldchildren::Bool=false)

Add a new child node to `parent`. `data` holds the data associated with the child node,
and `foldchildren` specifies whether its children are initially folded.
"""
function Node{Data}(data, parent::Node{Data}, foldchildren::Bool=false) where Data
child = new(data, Node[], foldchildren, false, parent)
push!(parent.children, child)
return child
end
end

Node(data, foldchildren::Bool=false) = Node{typeof(data)}(data, foldchildren)
Node(data, parent::Node{Data}, foldchildren::Bool=false) where Data = Node{Data}(data, parent, foldchildren)

Base.eltype(::Type{Node{Data}}) where Data = Data
Base.IteratorSize(::Type{<:Node}) = Base.SizeUnknown()

"""
isroot(node)

Return `true` if `node` is the root node (meaning, it has no parent).
"""
isroot(node::Node) = !isdefined(node, :parent)

"""
toggle!(node)

Change the folding state of `node`. See also [`fold!`](@ref) and [`unfold!`](@ref).
"""
toggle!(node::Node) = (node.foldchildren = !node.foldchildren)

"""
fold!(node)

Fold the children of `node`. See also [`unfold!`](@ref) and [`toggle!`](@ref).
"""
fold!(node::Node) = node.foldchildren = true

"""
unfold!(node)

Unfold the children of `node`. See also [`fold!`](@ref) and [`toggle!`](@ref).
"""
unfold!(node::Node) = node.foldchildren = false

"""
count_open_leaves(node)

Return the number of unfolded descendants of `node`.
"""
function count_open_leaves(node::Node, count::Int=0)
count += 1 # count self
if !node.foldchildren
# count children (and their children...)
for child in node.children
count = count_open_leaves(child, count)
end
end
return count
end

"""
newnode, newdepth = next(node, depth::Int=0)

Return the next node in a depth-first search.
`depth` counts the number of levels below the root.
The parent is visited before any children.
"""
function next(node, depth::Int=0)
function upnext(node, depth)
# node.parent must be defined if we're calling this
p = node.parent
myidx = findfirst((==)(node), p.children)
if myidx < length(p.children)
return p.children[myidx+1], depth
end
return upnext(p, depth-1)
end

if !node.foldchildren && !isempty(node.children)
return first(node.children), depth+1
end
return upnext(node, depth)
end

"""
newnode, newdepth = prev(node, depth::Int=0)

Return the previous node in a depth-first search.
`depth` counts the number of levels below the root.

This traverses in the opposite direction as [`next`](@ref),
so last, deepest children are visited before their parents.
"""
function prev(node, depth::Int=0)
function lastchild(node, depth)
if node.foldchildren || isempty(node.children)
return node, depth
end
return lastchild(node.children[end], depth+1)
end

p = node.parent
myidx = findfirst((==)(node), p.children)
if myidx > 1
return lastchild(p.children[myidx-1], depth)
end
return p, depth-1
end

# During iteration, we mark each node as to whether it's on the terminal branch of all ancestors.
function Base.iterate(root::Node)
root.lastbranch = true # root has no ancestors so it is last by definition
return root.data, (root, 0)
end

function Base.iterate(root::Node, state)
node, depth = state
lb = node.lastbranch
thislb = node.foldchildren | isempty(node.children)
(lb & thislb) && return nothing
# We can use `next` safely because parents are visited before children,
# so `lastbranch` is guaranteed to be set properly.
node, depth = next(node, depth)
p = node.parent
node.lastbranch = p.lastbranch && node == p.children[end]
return node.data, (node, depth)
end

struct NodeWrapper{N}
node::N
end

function Base.iterate(rootw::NodeWrapper)
root = rootw.node
root.lastbranch = true # root has no ancestors so it is last by definition
return root, (root, 0)
if isdefined(TerminalMenus, :ConfiguredMenu)
include("treemenu.jl")
end

function Base.iterate(rootw::NodeWrapper, state)
node, depth = state
lb = node.lastbranch
thislb = node.foldchildren | isempty(node.children)
(lb & thislb) && return nothing
# We can use `next` safely because parents are visited before children,
# so `lastbranch` is guaranteed to be set properly.
node, depth = next(node, depth)
p = node.parent
node.lastbranch = p.lastbranch && node == p.children[end]
return node, (node, depth)
end

"""
itr = nodes(node)

Create an iterator `itr` that will return the nodes, rather than the node data.

# Example

```julia
julia> foreach(unfold!, nodes(root))
```

would ensure that each node in the tree is unfolded.
"""
nodes(node::Node) = NodeWrapper(node)

## AbstractTrees interface

AbstractTrees.children(node::Node) = node.foldchildren ? typeof(node)[] : node.children

AbstractTrees.printnode(io::IO, node::Node) = print(io, node.foldchildren ? "+ " : " ", node.data)

end # module
Loading