current commit
260929.1 · feat(undotree): add tree_style option with :Undotree graph layout
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)