Skip to content

hl037/treeasy.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

treeasy.nvim

A minimal tree-view engine for Neovim. Handles rendering, expand/collapse, keyboard and mouse events, inline color and area tags. Does not provide any backend — file system, LSP symbols, DAP, etc. are implemented by the caller.

Features

  • Multi-line nodes
  • Lazy children (populate on first open)
  • Ghost nodes (invisible containers for multi-root layouts)
  • Inline <c:name> color tags and <a:name> clickable area tags, with nesting
  • Per-class keymaps, symbols and colors — hot-patched on live views
  • Per-symbol highlight groups for tree prefix characters
  • Cursor line via extmarks (colors always visible on top)
  • Multiple views on the same tree with independent open state
  • Mouse support via <LeftRelease>
  • Debounced rendering with per-node parse cache
  • goto_node for programmatic scroll-to
  • :TreeInspect buffer command for debugging

Installation

Using lazy.nvim:

{ "hl037/treeasy.nvim" }

Using packer.nvim:

use "hl037/treeasy.nvim"

Using vim-plug:

Plug 'hl037/treeasy.nvim'

Quick start

local treeasy = require("treeasy")
local tree_m  = treeasy.tree
local node_m  = treeasy.node

-- Always check before setting so user config is not overwritten.
if not treeasy.get_keymap("myclass") then
  treeasy.set_keymap("myclass", {
    toggle_collapse = { "<CR>", "<2-LeftMouse>" },
    click           = { "<LeftRelease>" },
    open_rec        = { "e" },
    collapse_rec    = { "c" },
  })
end

if not treeasy.get_colors("myclass") then
  treeasy.set_colors("myclass", {
    dir  = "#7aa2f7",
    leaf = "#9ece6a",
  })
end

local function open_text(n)
  return { "<c:dir>" .. n.label .. "</c>  <a:toggle>[-]</a>" }
end
local function collapsed_text(n)
  return { "<c:dir>" .. n.label .. "</c>  <a:toggle>[+]</a>" }
end

local function on_open(node, view, ctx)
  if node.children ~= nil then return end
  node.children = {}
  -- populate node.children here (lazy loading)
end

local function on_click(node, view, ctx)
  for _, area in ipairs(ctx.areas) do
    if area == "toggle" then
      view:_handle_event("toggle_collapse", node, ctx)
    end
  end
end

local root = node_m.new()
root.label          = "src/"
root.open_text      = open_text
root.collapsed_text = collapsed_text
root.handler["open"]  = on_open
root.handler["click"] = on_click

local t = tree_m.new({ class = "myclass", root = root })

vim.cmd("topleft 35vsplit")
local view = treeasy.attach_tree(vim.api.nvim_get_current_win(), t)
view:set_open(root, true)

Architecture

treeasy/
├── init.lua    public API: attach_tree, set_keymap, set_colors, set_symbols
├── tree.lua    tree manipulation: replace_node, insert_node, update_node
├── node.lua    node constructor, default open_rec/collapse_rec handlers
└── view.lua    rendering engine, tag parser, event dispatch

Tree / node lifecycle

A tree owns a root node and a list of views. When tree.replace_node, tree.insert_node or tree.update_node is called, all attached views are notified and schedule a debounced re-render automatically.

A node is a plain Lua table. The engine reads text, children, parent, index, ghost, open_text, collapsed_text, handler and internal. Everything else is userdata — add whatever fields you need.

A node can be freely mutated before it is added to the tree. Once part of a live tree, all structural changes must go through tree_m.* so views are notified. The one exception is lazy loading inside an open handler — see below.

Node validity:

  • Valid non-root: node.parent ~= nil
  • Valid root: node.parent == nil and node.tree ~= nil
  • Invalid (detached): node.parent == nil and node.tree == nil

Lazy loading

Set node.children = nil initially. Populate in the open handler — it fires before re-render, so children will be picked up automatically. The children == nil guard is important: it ensures the node has not been opened by another view already.

node.handler["open"] = function(n, view, ctx)
  if n.children ~= nil then return end
  n.children = {}
  for i, item in ipairs(fetch(n)) do
    local child = node_m.new({ parent = n, index = i })
    child.handler["open"] = on_open
    n.children[i] = child
  end
end

This direct mutation of node.children is a narrow exception — outside of this lazy-loading pattern, always use tree_m.insert_node / tree_m.replace_node.

Ghost nodes

A node with ghost = true is never rendered. Its children appear at the same depth without any connector symbol — useful for multi-root layouts (header + tree root as siblings):

local ghost  = node_m.new(); ghost.ghost = true
local header = node_m.new({ text = { "My Tree" } })
local root   = node_m.new({ ... })
ghost.children = { header, root }
header.parent = ghost; header.index = 1
root.parent   = ghost; root.index   = 2
local t = tree_m.new({ class = "myclass", root = ghost })

Text resolution

For each node, displayed text is resolved in order:

  1. node.open_text(node, view) / node.collapsed_text(node, view)
  2. tree.open_text(node, view) / tree.collapsed_text(node, view)
  3. node.text
  4. { "" }

Inline tags

<c:colorname>text</c>    apply a named color
<a:areaname>text</a>     define a clickable area

Tags may be nested. Inner <c:> tags get higher extmark priority. All <a:> tags matching the cursor position are collected in ctx.areas.

Rendering

All renders are debounced — view:schedule_render() batches multiple modifications into a single render via vim.schedule. view:_render() is private and must never be called directly. Parse results are cached per node; tree_m.update_node invalidates the cache.

view:schedule_render(cb) accepts an optional callback executed after the render, with the view as argument. Used internally by goto_node.

Navigating to a node

view:goto_node(node)  -- opens all ancestors, then scrolls to the node

Debugging

Run :TreeInspect in the view buffer to dump the full tree structure into a scratch split. Circular references (parent, tree) are shown as <circular ref>.

Highlight groups

Group Default Description
TreeasyCursorLine links to CursorLine Cursor line (extmark-based)
treeasy_{class}_{name} from set_colors Named label colors
treeasy_{class}_sym_mid empty ├─ symbol
treeasy_{class}_sym_last empty └─ symbol
treeasy_{class}_sym_vert empty continuation
treeasy_{class}_sym_space empty blank continuation

API reference

See :help treeasy for the full reference.

About

A lightweight yet full-featured Neovim tree-view with no opinion on what data you display.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages