260929.1 · 9e2d65cf972730470051c39973b2029ddca5882d
feat(undotree): add tree_style option with :Undotree graph layout
- Add opts.tree_style = "tree" | "graph" (default "tree") in defaults.lua.
- Keep the existing box-drawing tree as the default.
- Port the :Undotree graph-line algorithm for tree_style="graph",
including connector rows between branches.
- Use Unicode box-drawing glyphs (│ ● ╲ ╱) instead of ASCII so the
graph stays upright even when the highlight group is italicized.
- Skip connector rows (which contain no sequence number) in actions.undo
and both undotree previewers.
- Strip ANSI color codes before matching digits in actions.undo and both
undotree previewers so escape-code digits cannot be mistaken for the
undo sequence.
- Pass the fzf layout-derived reverse flag into the graph renderer and swap
diagonal glyphs when the default (reversed) layout is active.
- Avoid goto/::continue:: in draw_graph for PUC Lua compatibility.
- Add tests covering tree and graph rendering, connector rows, nested
branches, and grandchild ordering.
PR ready notes:
- make test glob=undotree passes all 13 cases.
- make emmylua-check reports no warnings from the modified files.
- docs/superpowers markdown files were omitted from this commit.
Fix #2806
reading declarations
Changes to tests/undotree_spec.lua. All files
@@ -0,0 +1,269 @@+local helpers = require("fzf-lua.test.helpers")+local assert = helpers.assert+local eq = assert.are.same++local undotree = require("fzf-lua.providers.undotree")+local utils = require("fzf-lua.utils")++local FROZEN_TIME = 1700000000++local function freeze_time()+ local orig = os.time+ _G.os.time = function() return FROZEN_TIME end+ return orig+end++local function setup_ansi_identity()+ local orig = {}+ for _, hl in ipairs({ "Directory", "Number", "Comment" }) do+ orig[hl] = utils.ansi_codes[hl]+ utils.ansi_codes[hl] = function(s) return s end+ end+ return orig+end++local function restore_ansi_identity(orig)+ for hl, fn in pairs(orig) do+ utils.ansi_codes[hl] = fn+ end+end++local opts = {+ hls = {+ dir_part = "Directory",+ buf_name = "Number",+ path_linenr = "Comment",+ },+}++local function collect_tree(tree, reverse)+ local rows = {}+ local cb = function(seq, line)+ table.insert(rows, { seq = seq, line = utils.strip_ansi_coloring(line) })+ end+ undotree._draw_tree(opts, cb, tree, { 0 }, reverse or false)+ return rows+end++local function collect_graph(tree)+ local rows = {}+ local cb = function(seq, line)+ table.insert(rows, { seq = seq, line = utils.strip_ansi_coloring(line) })+ end+ undotree._draw_graph(opts, cb, tree, 0, false)+ return rows+end++local function origin()+ return { [0] = { child = {}, time = -1 } }+end++local function linear_chain()+ return {+ [0] = { child = { 1 }, time = -1 },+ [1] = { child = { 2 }, time = FROZEN_TIME },+ [2] = { child = {}, time = FROZEN_TIME + 10 },+ }+end++local function simple_branch()+ return {+ [0] = { child = { 1 }, time = -1 },+ [1] = { child = { 2, 3 }, time = FROZEN_TIME },+ [2] = { child = {}, time = FROZEN_TIME + 5 },+ [3] = { child = {}, time = FROZEN_TIME + 10 },+ }+end++local function triple_branch()+ return {+ [0] = { child = { 1 }, time = -1 },+ [1] = { child = { 2, 3, 4 }, time = FROZEN_TIME },+ [2] = { child = {}, time = FROZEN_TIME + 5 },+ [3] = { child = {}, time = FROZEN_TIME + 10 },+ [4] = { child = {}, time = FROZEN_TIME + 15 },+ }+end++local function nested_branches()+ return {+ [0] = { child = { 1, 2 }, time = -1 },+ [1] = { child = { 3, 4 }, time = FROZEN_TIME },+ [2] = { child = {}, time = FROZEN_TIME + 5 },+ [3] = { child = {}, time = FROZEN_TIME + 10 },+ [4] = { child = {}, time = FROZEN_TIME + 15 },+ }+end++local function grandchild_branch()+ return {+ [0] = { child = { 1 }, time = -1 },+ [1] = { child = { 2, 3, 4 }, time = FROZEN_TIME },+ [2] = { child = {}, time = FROZEN_TIME + 5 },+ [3] = { child = { 5 }, time = FROZEN_TIME + 10 },+ [4] = { child = {}, time = FROZEN_TIME + 15 },+ [5] = { child = {}, time = FROZEN_TIME + 20 },+ }+end++describe("undotree draw_tree", function()+ local orig_time, orig_ansi++ before_each(function()+ orig_time = freeze_time()+ orig_ansi = setup_ansi_identity()+ end)++ after_each(function()+ _G.os.time = orig_time+ restore_ansi_identity(orig_ansi)+ end)++ it("renders a single origin node", function()+ local rows = collect_tree(origin())+ eq({+ { seq = 0, line = "0\t\torigin" },+ }, rows)+ end)++ it("renders a linear chain without branch glyphs", function()+ local rows = collect_tree(linear_chain())+ eq({+ { seq = 0, line = "0\t\torigin" },+ { seq = 1, line = "1\t\tjust now" },+ { seq = 2, line = "2\t\tjust now" },+ }, rows)+ end)++ it("renders branch children with box-drawing glyphs", function()+ local rows = collect_tree(simple_branch())+ eq({+ { seq = 0, line = "0\t\torigin" },+ { seq = 1, line = "1\t\tjust now" },+ { seq = 2, line = "├── 2\t\tjust now" },+ { seq = 3, line = "└── 3\t\tjust now" },+ }, rows)+ end)++ it("uses reversed leaf glyphs when reverse is true", function()+ local rows = collect_tree(simple_branch(), true)+ eq({+ { seq = 0, line = "0\t\torigin" },+ { seq = 1, line = "1\t\tjust now" },+ { seq = 2, line = "├── 2\t\tjust now" },+ { seq = 3, line = "┌── 3\t\tjust now" },+ }, rows)+ end)++ it("handles three siblings", function()+ local rows = collect_tree(triple_branch())+ eq({+ { seq = 0, line = "0\t\torigin" },+ { seq = 1, line = "1\t\tjust now" },+ { seq = 2, line = "├── 2\t\tjust now" },+ { seq = 3, line = "├── 3\t\tjust now" },+ { seq = 4, line = "└── 4\t\tjust now" },+ }, rows)+ end)++ it("renders nested branches in pre-order", function()+ local rows = collect_tree(nested_branches())+ eq({+ { seq = 0, line = "0\t\torigin" },+ { seq = 1, line = "├── 1\t\tjust now" },+ { seq = 3, line = "│ ├── 3\t\tjust now" },+ { seq = 4, line = "│ └── 4\t\tjust now" },+ { seq = 2, line = "└── 2\t\tjust now" },+ }, rows)+ end)++ it("renders a grandchild in pre-order", function()+ local rows = collect_tree(grandchild_branch())+ eq({+ { seq = 0, line = "0\t\torigin" },+ { seq = 1, line = "1\t\tjust now" },+ { seq = 2, line = "├── 2\t\tjust now" },+ { seq = 3, line = "├── 3\t\tjust now" },+ { seq = 5, line = "│ 5\t\tjust now" },+ { seq = 4, line = "└── 4\t\tjust now" },+ }, rows)+ end)+end)++describe("undotree draw_graph", function()+ local orig_time, orig_ansi++ before_each(function()+ orig_time = freeze_time()+ orig_ansi = setup_ansi_identity()+ end)++ after_each(function()+ _G.os.time = orig_time+ restore_ansi_identity(orig_ansi)+ end)++ it("renders a single origin node", function()+ local rows = collect_graph(origin())+ eq({+ { seq = 0, line = "● 0 (origin)" },+ }, rows)+ end)++ it("renders a linear chain", function()+ local rows = collect_graph(linear_chain())+ eq({+ { seq = 0, line = "● 0 (origin)" },+ { seq = 1, line = "● 1 (just now)" },+ { seq = 2, line = "● 2 (just now)" },+ }, rows)+ end)++ it("renders a branch with connector and :Undotree-style columns", function()+ local rows = collect_graph(simple_branch())+ eq({+ { seq = 0, line = "● 0 (origin)" },+ { seq = 1, line = "● 1 (just now)" },+ { seq = nil, line = "│╲" },+ { seq = 2, line = "│ ● 2 (just now)" },+ { seq = 3, line = "● 3 (just now)" },+ }, rows)+ end)++ it("renders three siblings", function()+ local rows = collect_graph(triple_branch())+ eq({+ { seq = 0, line = "● 0 (origin)" },+ { seq = 1, line = "● 1 (just now)" },+ { seq = nil, line = "│╲" },+ { seq = 2, line = "│ ● 2 (just now)" },+ { seq = nil, line = "│╲" },+ { seq = 3, line = "│ ● 3 (just now)" },+ { seq = 4, line = "● 4 (just now)" },+ }, rows)+ end)++ it("renders nested branches with remove/branch connectors", function()+ local rows = collect_graph(nested_branches())+ eq({+ { seq = 0, line = "● 0 (origin)" },+ { seq = nil, line = "│╲" },+ { seq = 1, line = "│ ● 1 (just now)" },+ { seq = 2, line = "● │ 2 (just now)" },+ { seq = nil, line = " ╱│" },+ { seq = 3, line = "│ ● 3 (just now)" },+ { seq = 4, line = "● 4 (just now)" },+ }, rows)+ end)++ it("emits connector rows with no digits and node rows with digits", function()+ local rows = collect_graph(simple_branch())+ for _, r in ipairs(rows) do+ if r.seq == nil then+ eq(nil, r.line:match("%d"))+ else+ eq(true, r.line:match("%d+") ~= nil)+ end+ end+ end)+end)