Meh I'll figure out submodules later

This commit is contained in:
mustard 2025-09-16 01:01:02 +02:00
parent 4ca9d44a90
commit 8cb281f436
352 changed files with 66107 additions and 0 deletions

View file

@ -0,0 +1,312 @@
local function to_string(text)
if type(text) == "string" then
return text
end
if type(text) == "table" then
if text.content then
return text:content()
end
return text[1]
end
error("unsupported text")
end
local popup = {}
local mod = {}
mod.popup = popup
function mod.eq(...)
return assert.are.same(...)
end
function mod.approx(...)
return assert.are.near(...)
end
function mod.neq(...)
return assert["not"].are.same(...)
end
---@param fn fun(): nil
---@param error string
---@param is_plain boolean
function mod.errors(fn, error, is_plain)
assert.matches_error(fn, error, 1, is_plain)
end
---@param keys string
---@param mode string
function mod.feedkeys(keys, mode)
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(keys, true, false, true), mode or "", true)
end
---@param tbl table
---@param keys string[]
function mod.tbl_pick(tbl, keys)
if not keys or #keys == 0 then
return tbl
end
local new_tbl = {}
for _, key in ipairs(keys) do
new_tbl[key] = tbl[key]
end
return new_tbl
end
---@param tbl table
---@param keys string[]
function mod.tbl_omit(tbl, keys)
if not keys or #keys == 0 then
return tbl
end
local new_tbl = vim.deepcopy(tbl)
for _, key in ipairs(keys) do
rawset(new_tbl, key, nil)
end
return new_tbl
end
---@param bufnr number
---@param ns_id integer
---@param linenr integer (1-indexed)
---@param byte_start? integer (0-indexed)
---@param byte_end? integer (0-indexed, inclusive)
function mod.get_line_extmarks(bufnr, ns_id, linenr, byte_start, byte_end)
return vim.api.nvim_buf_get_extmarks(
bufnr,
ns_id,
{ linenr - 1, byte_start or 0 },
{ linenr - 1, byte_end and byte_end + 1 or -1 },
{ details = true }
)
end
---@param bufnr number
---@param ns_id integer
---@param linenr integer (1-indexed)
---@param text string
---@return table[]
---@return { byte_start: integer, byte_end: integer } info (byte range: 0-indexed, inclusive)
function mod.get_text_extmarks(bufnr, ns_id, linenr, text)
local line = vim.api.nvim_buf_get_lines(bufnr, linenr - 1, linenr, false)[1]
local byte_start = string.find(line, text) -- 1-indexed
byte_start = byte_start - 1 -- 0-indexed
local byte_end = byte_start + #text - 1 -- inclusive
local extmarks = vim.api.nvim_buf_get_extmarks(
bufnr,
ns_id,
{ linenr - 1, byte_start },
{ linenr - 1, byte_end },
{ details = true }
)
return extmarks, { byte_start = byte_start, byte_end = byte_end }
end
---@param bufnr number
---@param lines string[]
---@param linenr_start? integer (1-indexed)
---@param linenr_end? integer (1-indexed, inclusive)
function mod.assert_buf_lines(bufnr, lines, linenr_start, linenr_end)
mod.eq(vim.api.nvim_buf_get_lines(bufnr, linenr_start and linenr_start - 1 or 0, linenr_end or -1, false), lines)
end
---@param bufnr number
---@param options table
function mod.assert_buf_options(bufnr, options)
for name, value in pairs(options) do
mod.eq(vim.api.nvim_buf_get_option(bufnr, name), value)
end
end
---@param winid number
---@param options table
function mod.assert_win_options(winid, options)
for name, value in pairs(options) do
mod.eq(vim.api.nvim_win_get_option(winid, name), value)
end
end
---@param extmark table
---@param linenr number (1-indexed)
---@param text string
---@param hl_group string
function mod.assert_extmark(extmark, linenr, text, hl_group)
mod.eq(extmark[2], linenr - 1)
if text then
local start_col = extmark[3]
mod.eq(extmark[4].end_col - start_col, #text)
end
mod.eq(mod.tbl_pick(extmark[4], { "end_row", "hl_group" }), {
end_row = linenr - 1,
hl_group = hl_group,
})
end
---@param bufnr number
---@param ns_id integer
---@param linenr integer (1-indexed)
---@param text string
---@param hl_group string
function mod.assert_highlight(bufnr, ns_id, linenr, text, hl_group)
local extmarks, info = mod.get_text_extmarks(bufnr, ns_id, linenr, text)
mod.eq(#extmarks, 1)
mod.eq(extmarks[1][3], info.byte_start)
mod.assert_extmark(extmarks[1], linenr, text, hl_group)
end
---@param feature_name string
---@param desc string
---@param func fun(is_available: boolean):nil
function mod.describe_flipping_feature(feature_name, desc, func)
local initial_value = require("nui.utils")._.feature[feature_name]
describe(string.format("(w/ %s) %s", feature_name, desc), function()
require("nui.utils")._.feature[feature_name] = true
func(true)
require("nui.utils")._.feature[feature_name] = initial_value
end)
describe(string.format("(w/o %s) %s", feature_name, desc), function()
require("nui.utils")._.feature[feature_name] = false
func(false)
require("nui.utils")._.feature[feature_name] = initial_value
end)
end
function popup.create_border_style_list()
return { "", "", "", "", "", "", "", "" }
end
function popup.create_border_style_map()
return {
top_left = "",
top = "",
top_right = "",
left = "",
right = "",
bottom_left = "",
bottom = "",
bottom_right = "",
}
end
function popup.create_border_style_map_with_tuple(hl_group)
local style = popup.create_border_style_map()
for k, v in pairs(style) do
style[k] = { v, hl_group .. "_" .. k }
end
return style
end
function popup.create_border_style_map_with_nui_text(hl_group)
local Text = require("nui.text")
local style = popup.create_border_style_map()
for k, v in pairs(style) do
style[k] = Text(v, hl_group .. "_" .. k)
end
return style
end
function popup.assert_border_lines(options, border_bufnr)
local size = { width = options.size.width, height = options.size.height }
-- `vim.tbl_islist` will be removed in the future
local islist = vim.islist or vim.tbl_islist
local style = vim.deepcopy(options.border.style)
if islist(style) then
style = {
top_left = style[1],
top = style[2],
top_right = style[3],
left = style[8],
right = style[4],
bottom_left = style[7],
bottom = style[6],
bottom_right = style[5],
}
end
local expected_lines = {}
table.insert(
expected_lines,
string.format(
"%s%s%s",
to_string(style.top_left),
string.rep(to_string(style.top), size.width),
to_string(style.top_right)
)
)
for _ = 1, size.height do
table.insert(
expected_lines,
string.format("%s%s%s", to_string(style.left), string.rep(" ", size.width), to_string(style.right))
)
end
table.insert(
expected_lines,
string.format(
"%s%s%s",
to_string(style.bottom_left),
string.rep(to_string(style.bottom), size.width),
to_string(style.bottom_right)
)
)
mod.assert_buf_lines(border_bufnr, expected_lines)
end
function popup.assert_border_highlight(options, border_bufnr, hl_group, no_hl_group_suffix)
local size = { width = options.size.width, height = options.size.height }
for linenr = 1, size.height + 2 do
local is_top_line = linenr == 1
local is_bottom_line = linenr == size.height + 2
local extmarks = mod.get_line_extmarks(border_bufnr, options.ns_id, linenr)
mod.eq(#extmarks, (is_top_line or is_bottom_line) and 4 or 2)
local function with_suffix(hl_group_name, suffix)
if no_hl_group_suffix then
return hl_group_name
end
return hl_group_name .. suffix
end
mod.assert_extmark(
extmarks[1],
linenr,
nil,
with_suffix(hl_group, (is_top_line and "_top_left" or is_bottom_line and "_bottom_left" or "_left"))
)
if is_top_line or is_bottom_line then
mod.assert_extmark(extmarks[2], linenr, nil, with_suffix(hl_group, (is_top_line and "_top" or "_bottom")))
mod.assert_extmark(extmarks[3], linenr, nil, with_suffix(hl_group, (is_top_line and "_top" or "_bottom")))
end
mod.assert_extmark(
extmarks[#extmarks],
linenr,
nil,
with_suffix(hl_group, (is_top_line and "_top_right" or is_bottom_line and "_bottom_right" or "_right"))
)
end
end
return mod

View file

@ -0,0 +1,24 @@
-- mimic startup option `--clean`
local function clean_startup()
for _, path in ipairs(vim.split(vim.o.runtimepath, ",")) do
if
string.find(path, vim.fn.expand("~/.config/nvim"))
or string.find(path, vim.fn.expand("~/.local/share/nvim/site"))
then
vim.opt.packpath:remove(path)
vim.opt.runtimepath:remove(path)
end
end
end
clean_startup()
local root_dir = vim.fn.fnamemodify(vim.trim(vim.fn.system("git rev-parse --show-toplevel")), ":p"):gsub("/$", "")
package.path = string.format("%s;%s/?.lua;%s/?/init.lua", package.path, root_dir, root_dir)
vim.opt.packpath:prepend(root_dir .. "/.tests/site")
vim.cmd([[
packadd plenary.nvim
]])

View file

@ -0,0 +1,285 @@
pcall(require, "luacov")
local Input = require("nui.input")
local Text = require("nui.text")
local h = require("tests.helpers")
local eq, feedkeys = h.eq, h.feedkeys
-- Input's functionalities are not testable using headless nvim.
-- Not sure what to do about it.
describe("nui.input", function()
local parent_winid, parent_bufnr
local popup_options
local input
before_each(function()
parent_winid = vim.api.nvim_get_current_win()
parent_bufnr = vim.api.nvim_get_current_buf()
popup_options = {
relative = "win",
position = "50%",
size = 20,
}
end)
after_each(function()
if input then
input:unmount()
input = nil
end
end)
pending("o.prompt", function()
it("supports NuiText", function()
local prompt_text = "> "
local hl_group = "NuiInputTest"
input = Input(popup_options, {
prompt = Text(prompt_text, hl_group),
})
input:mount()
vim.wait(100, function() end)
h.assert_buf_lines(input.bufnr, {
prompt_text,
})
h.assert_highlight(input.bufnr, input.ns_id, 1, prompt_text, hl_group)
end)
end)
describe("o.on_change", function()
it("works", function()
local done = false
local values = {}
input = Input(popup_options, {
on_change = function(value)
values[#values + 1] = value
end,
on_close = function()
done = true
end,
})
input:mount()
vim.wait(100, function() end)
feedkeys("aa", "x") -- append a
feedkeys("ab", "x") -- append b
feedkeys("ac", "x") -- append c
vim.fn.wait(100, function()
return done
end)
eq(values, { "a", "ab", "abc" })
end)
end)
describe("o.on_close", function()
it("is called on <C-c>", function()
local done = false
input = Input(popup_options, {
on_close = function()
done = true
end,
})
input:mount()
vim.wait(100, function() end)
feedkeys("i<C-c>", "x")
vim.fn.wait(2000, function()
return done
end)
eq(done, true)
end)
it("is called on unmount", function()
local done = false
input = Input(popup_options, {
on_close = function()
done = true
end,
})
input:mount()
vim.wait(100, function() end)
input:unmount()
vim.fn.wait(200, function()
return done
end)
eq(done, true)
end)
end)
describe("cursor_position_patch", function()
local initial_cursor
local function setup()
vim.api.nvim_buf_set_lines(parent_bufnr, 0, -1, false, {
"1 nui.nvim",
"2 nui.nvim",
"3 nui.nvim",
})
initial_cursor = { 2, 4 }
vim.api.nvim_win_set_cursor(parent_winid, initial_cursor)
end
it("works after submitting from insert mode", function()
setup()
local done = false
input = Input(popup_options, {
on_submit = function()
done = true
end,
})
input:mount()
vim.wait(100, function() end)
feedkeys("<cr>", "x")
vim.fn.wait(1000, function()
return done
end)
eq(done, true)
eq(vim.api.nvim_win_get_cursor(parent_winid), initial_cursor)
end)
it("works after submitting from normal mode", function()
setup()
local done = false
input = Input(popup_options, {
on_submit = function()
done = true
end,
})
input:mount()
vim.wait(100, function() end)
feedkeys("<esc><cr>", "x")
vim.fn.wait(1000, function()
return done
end)
eq(done, true)
eq(vim.api.nvim_win_get_cursor(parent_winid), initial_cursor)
end)
it("works after closing from insert mode", function()
setup()
local done = false
input = Input(popup_options, {
on_close = function()
done = true
end,
})
input:mount()
vim.wait(100, function() end)
input:map("i", "<esc>", function()
input:unmount()
end, { nowait = true, noremap = true })
feedkeys("i<esc>", "x")
vim.fn.wait(1000, function()
return done
end)
eq(done, true)
eq(vim.api.nvim_win_get_cursor(parent_winid), initial_cursor)
end)
it("works after closing from normal mode", function()
setup()
local done = false
input = Input(popup_options, {
on_close = function()
done = true
end,
})
input:mount()
vim.wait(100, function() end)
input:map("n", "<esc>", function()
input:unmount()
end, { nowait = true, noremap = true })
feedkeys("<esc>", "x")
vim.fn.wait(1000, function()
return done
end)
eq(done, true)
eq(vim.api.nvim_win_get_cursor(parent_winid), initial_cursor)
end)
end)
describe("method :mount", function()
it("is idempotent", function()
input = Input(popup_options, {})
input:mount()
vim.wait(100, function() end)
local bufnr, winid = input.bufnr, input.winid
eq(type(bufnr), "number")
eq(type(winid), "number")
input:mount()
eq(bufnr, input.bufnr)
eq(winid, input.winid)
end)
end)
describe("method :unmount", function()
it("is idempotent", function()
local done = 0
input = Input(popup_options, {
on_close = function()
done = done + 1
end,
})
input:mount()
vim.wait(100, function() end)
input:unmount()
input:unmount()
input:unmount()
vim.fn.wait(200, function()
return done > 1
end)
eq(done, 1)
end)
end)
end)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,144 @@
pcall(require, "luacov")
local utils = require("nui.layout.utils")
local h = require("tests.helpers")
local eq = h.eq
describe("nui.layout", function()
describe("utils", function()
describe("parse_relative", function()
local fallback_winid = 17
it("works for type=buf", function()
local relative = {
type = "buf",
position = { row = 2, col = 4 },
winid = 42,
}
local result = utils.parse_relative(relative, fallback_winid)
eq(result, {
relative = "win",
win = relative.winid,
bufpos = {
relative.position.row,
relative.position.col,
},
})
end)
it("works for type=cursor", function()
local relative = {
type = "cursor",
winid = 42,
}
local result = utils.parse_relative(relative, fallback_winid)
eq(result, {
relative = relative.type,
win = relative.winid,
})
end)
it("works for type=editor", function()
local relative = {
type = "editor",
winid = 42,
}
local result = utils.parse_relative(relative, fallback_winid)
eq(result, {
relative = relative.type,
win = relative.winid,
})
end)
it("works for type=win", function()
local relative = {
type = "win",
winid = 42,
}
local result = utils.parse_relative(relative, fallback_winid)
eq(result, {
relative = relative.type,
win = relative.winid,
})
end)
it("uses fallback_winid if relative.winid is nil", function()
local relative = {
type = "win",
}
local result = utils.parse_relative(relative, fallback_winid)
eq(result, {
relative = relative.type,
win = fallback_winid,
})
end)
end)
describe("get_container_info", function()
it("works for relative=editor", function()
local result = utils.get_container_info({
relative = "editor",
})
eq(result, {
relative = "editor",
size = {
width = vim.o.columns,
height = vim.o.lines,
},
type = "editor",
})
end)
it("works for relative=cursor", function()
local winid = vim.api.nvim_get_current_win()
local result = utils.get_container_info({
relative = "cursor",
win = 0,
})
eq(result, {
relative = "cursor",
size = {
width = vim.api.nvim_win_get_width(winid),
height = vim.api.nvim_win_get_height(winid),
},
type = "window",
winid = winid,
})
end)
it("works for relative=win w/ bufpos", function()
local winid = vim.api.nvim_get_current_win()
local result = utils.get_container_info({
relative = "win",
win = winid,
bufpos = { 2, 4 },
})
eq(result, {
relative = "buf",
size = {
width = vim.api.nvim_win_get_width(winid),
height = vim.api.nvim_win_get_height(winid),
},
type = "window",
winid = winid,
})
end)
end)
end)
end)

View file

@ -0,0 +1,164 @@
pcall(require, "luacov")
local Line = require("nui.line")
local Text = require("nui.text")
local h = require("tests.helpers")
local eq = h.eq
describe("nui.line", function()
it("can accept initial nui.text objects", function()
local t1, t2 = Text("One"), Text("Two")
local line = Line({ t1, t2 })
eq(#line._texts, 2)
end)
describe("method :append", function()
it("returns nui.text for string parameter", function()
local line = Line()
local text = line:append("One")
eq(type(text.content), "function")
end)
it("returns nui.text for nui.text parameter", function()
local line = Line()
local text = Text("One")
local ret_text = line:append(text)
eq(text == ret_text, true)
eq(type(ret_text.content), "function")
end)
it("returns nui.line for nui.line parameter", function()
local line = Line()
local content_line = Line({ Text("One"), Text("Two") })
local ret_content_line = line:append(content_line)
eq(content_line == ret_content_line, true)
eq(type(ret_content_line.append), "function")
end)
it("stores and returns block with same reference", function()
local line = Line()
local text_one = line:append("One")
eq(line._texts[1] == text_one, true)
local text_two = Text("Two")
local ret_text_two = line:append(text_two)
eq(text_two == ret_text_two, true)
eq(line._texts[2] == text_two, true)
eq(line._texts[2] == ret_text_two, true)
local text_three = Text("Three")
local text_four = Text("Four")
local content_line = Line({ text_three, text_four })
local ret_content_line = line:append(content_line)
eq(content_line == ret_content_line, true)
eq(line._texts[3] == content_line._texts[1], true)
eq(line._texts[4] == content_line._texts[2], true)
end)
end)
describe("method :content", function()
it("returns whole text content", function()
local line = Line()
line:append("One")
line:append("Two")
eq(line:content(), "OneTwo")
end)
end)
describe("method :width", function()
it("returns whole text width", function()
local line = Line()
line:append("One")
line:append("Two")
eq(line:width(), 6)
end)
end)
describe("method", function()
local winid, bufnr
before_each(function()
winid = vim.api.nvim_get_current_win()
bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_win_set_buf(winid, bufnr)
end)
after_each(function()
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
describe(":highlight", function()
local hl_group_one, hl_group_two, ns, ns_id
local linenr
local t1, t2, t3, t4
local line
before_each(function()
hl_group_one = "NuiTextTestOne"
hl_group_two = "NuiTextTestTwo"
ns = "NuiTest"
ns_id = vim.api.nvim_create_namespace(ns)
linenr = 1
t1 = Text("One")
t2 = Text("Two", hl_group_one)
t3 = Text("Three", hl_group_two)
t4 = Text("Four")
line = Line({ t1, t2, t3, t4 })
end)
it("is applied with :render", function()
line:render(bufnr, ns_id, linenr)
h.assert_highlight(bufnr, ns_id, linenr, t2:content(), hl_group_one)
h.assert_highlight(bufnr, ns_id, linenr, t3:content(), hl_group_two)
end)
it("can highlight existing buffer line", function()
vim.api.nvim_buf_set_lines(
bufnr,
linenr - 1,
-1,
false,
{ t1:content() .. t2:content() .. t3:content() .. t4:content() }
)
line:highlight(bufnr, ns_id, linenr)
h.assert_highlight(bufnr, ns_id, linenr, t2:content(), hl_group_one)
h.assert_highlight(bufnr, ns_id, linenr, t3:content(), hl_group_two)
end)
end)
describe(":render", function()
it("works", function()
local linenr = 1
local line = Line()
line:append("4")
line:append("2")
line:render(bufnr, -1, linenr)
h.assert_buf_lines(bufnr, {
"42",
})
end)
end)
end)
end)

View file

@ -0,0 +1,620 @@
pcall(require, "luacov")
local Menu = require("nui.menu")
local Layout = require("nui.layout")
local Line = require("nui.line")
local Text = require("nui.text")
local h = require("tests.helpers")
local spy = require("luassert.spy")
local eq, feedkeys = h.eq, h.feedkeys
describe("nui.menu", function()
local callbacks
local popup_options
local menu
before_each(function()
callbacks = {
on_change = function() end,
on_submit = function() end,
}
popup_options = {
relative = "win",
position = "50%",
}
end)
after_each(function()
if menu then
menu:unmount()
menu = nil
end
end)
describe("method :new", function()
it("works with menu", function()
menu = Menu:new(popup_options, {
lines = {
Menu.item("a"),
},
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"a",
})
end)
end)
describe("o.keymap", function()
it("supports multiple keys as table", function()
local on_change = spy.on(callbacks, "on_change")
local lines = {
Menu.item("Item 1", { id = 1 }),
Menu.item("Item 2", { id = 2 }),
Menu.item("Item 3", { id = 3 }),
}
menu = Menu(popup_options, {
keymap = {
focus_next = { "j", "s" },
focus_prev = { "k", "w" },
},
lines = lines,
on_change = on_change,
})
menu:mount()
feedkeys("j", "x")
assert.spy(on_change).called_with(lines[2], menu)
on_change:clear()
feedkeys("s", "x")
assert.spy(on_change).called_with(lines[3], menu)
on_change:clear()
feedkeys("w", "x")
assert.spy(on_change).called_with(lines[2], menu)
on_change:clear()
feedkeys("k", "x")
assert.spy(on_change).called_with(lines[1], menu)
on_change:clear()
end)
it("supports single key as string", function()
local on_change = spy.on(callbacks, "on_change")
local lines = {
Menu.item("Item 1", { id = 1 }),
Menu.item("Item 2", { id = 2 }),
Menu.item("Item 3", { id = 3 }),
}
menu = Menu(popup_options, {
keymap = {
focus_next = "s",
focus_prev = "w",
},
lines = lines,
on_change = on_change,
})
menu:mount()
feedkeys("s", "x")
assert.spy(on_change).called_with(lines[2], menu)
on_change:clear()
feedkeys("w", "x")
assert.spy(on_change).called_with(lines[1], menu)
on_change:clear()
end)
end)
describe("size", function()
it("respects o.min_width", function()
local min_width = 3
local items = {
Menu.item("A"),
Menu.separator("*"),
Menu.item("B"),
}
menu = Menu(popup_options, {
lines = items,
min_width = min_width,
})
menu:mount()
eq(vim.api.nvim_win_get_width(menu.winid), min_width)
h.assert_buf_lines(menu.bufnr, {
"A",
" * ",
"B",
})
end)
it("respects o.max_width", function()
local max_width = 6
local items = {
Menu.item("Item 1"),
Menu.separator("*"),
Menu.item("Item Number Two"),
}
menu = Menu(popup_options, {
lines = items,
max_width = max_width,
})
menu:mount()
eq(vim.api.nvim_win_get_width(menu.winid), max_width)
h.assert_buf_lines(menu.bufnr, {
"Item 1",
" * ",
"Item …",
})
end)
it("respects o.min_height", function()
local min_height = 3
local items = {
Menu.item("A"),
Menu.separator("*"),
Menu.item("B"),
}
menu = Menu(popup_options, {
lines = items,
min_height = min_height,
})
menu:mount()
eq(vim.api.nvim_win_get_height(menu.winid), min_height)
end)
it("respects o.max_height", function()
local max_height = 2
local items = {
Menu.item("A"),
Menu.separator("*"),
Menu.item("B"),
}
menu = Menu(popup_options, {
lines = items,
max_height = max_height,
})
menu:mount()
eq(vim.api.nvim_win_get_height(menu.winid), max_height)
end)
end)
it("calls o.on_change item focus is changed", function()
local on_change = spy.on(callbacks, "on_change")
local lines = {
Menu.item("Item 1", { id = 1 }),
Menu.item("Item 2", { id = 2 }),
}
menu = Menu(popup_options, {
lines = lines,
on_change = on_change,
})
menu:mount()
-- initial focus
assert.spy(on_change).called_with(lines[1], menu)
on_change:clear()
feedkeys("j", "x")
assert.spy(on_change).called_with(lines[2], menu)
on_change:clear()
feedkeys("j", "x")
assert.spy(on_change).called_with(lines[1], menu)
on_change:clear()
feedkeys("k", "x")
assert.spy(on_change).called_with(lines[2], menu)
on_change:clear()
end)
it("calls o.on_submit when item is submitted", function()
local on_submit = spy.on(callbacks, "on_submit")
local lines = {
Menu.item("Item 1", { id = 1 }),
Menu.item("Item 2", { id = 2 }),
}
menu = Menu(popup_options, {
lines = lines,
on_submit = on_submit,
})
menu:mount()
feedkeys("j", "x")
feedkeys("<CR>", "x")
assert.spy(on_submit).called_with(lines[2])
end)
it("calls o.on_close when menu is closed", function()
local on_close = spy.on(callbacks, "on_close")
local lines = {
Menu.item("Item 1", { id = 1 }),
Menu.item("Item 2", { id = 2 }),
}
menu = Menu(popup_options, {
lines = lines,
on_close = on_close,
})
menu:mount()
feedkeys("<Esc>", "x")
assert.spy(on_close).called_with()
end)
describe("item", function()
it("is prepared using o.prepare_item if provided", function()
local items = {
Menu.item("A"),
Menu.separator("*"),
Menu.item("B"),
}
local function prepare_item(item)
return "-" .. item.text .. "-"
end
menu = Menu(popup_options, {
lines = items,
prepare_item = prepare_item,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, vim.tbl_map(prepare_item, items))
end)
it("is prepared when o.prepare_item is not provided", function()
local items = {
Menu.item("A"),
Menu.separator("*"),
Menu.item("B"),
}
popup_options.border = "single"
menu = Menu(popup_options, {
lines = items,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"A",
"─*──",
"B",
})
end)
it("is skipped respecting o.should_skip_item if provided", function()
local on_change = spy.on(callbacks, "on_change")
local items = {
Menu.item("-"),
Menu.item("A", { id = 1 }),
Menu.item("-"),
Menu.item("B", { id = 2 }),
Menu.item("-"),
}
menu = Menu(popup_options, {
lines = items,
on_change = on_change,
should_skip_item = function(item)
return not item.id
end,
})
menu:mount()
assert.spy(on_change).called_with(items[2], menu)
on_change:clear()
feedkeys("j", "x")
assert.spy(on_change).called_with(items[4], menu)
on_change:clear()
feedkeys("j", "x")
assert.spy(on_change).called_with(items[2], menu)
on_change:clear()
end)
it("supports table with key .text", function()
local text = "text"
local items = {
Menu.item({ text = text }),
}
menu = Menu(popup_options, {
lines = items,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
text,
})
end)
it("supports nui.text", function()
local hl_group = "NuiMenuTest"
local text = "text"
local items = {
Menu.item(Text(text, hl_group)),
}
menu = Menu(popup_options, {
lines = items,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
text,
})
h.assert_highlight(menu.bufnr, menu.ns_id, 1, text, hl_group)
end)
it("supports nui.line", function()
local hl_group = "NuiMenuTest"
local text = "text"
local items = {
Menu.item(Line({ Text(text, hl_group) })),
}
menu = Menu(popup_options, {
lines = items,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
text,
})
h.assert_highlight(menu.bufnr, menu.ns_id, 1, text, hl_group)
end)
it("content longer than max_width is truncated", function()
local items = {
Menu.item({ text = "Item 10 -" }),
Menu.item(Text("Item 20 -")),
Menu.item(Line({ Text("Item 30 -") })),
Menu.item(Line({ Text("Item 40"), Text(" -") })),
Menu.item(Line({ Text("Item 50 -"), Text(" -") })),
}
menu = Menu(popup_options, {
max_width = 7,
lines = items,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"Item 1…",
"Item 2…",
"Item 3…",
"Item 4…",
"Item 5…",
})
end)
end)
it("can truncate content longer than max_width w/ multi-byte chars", function()
menu = Menu(popup_options, {
lines = {
Menu.item("中文长度测试"),
Menu.item("Test中英文测试"),
Menu.item("Long Long Group"),
},
max_width = 11,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"中文长度测…",
"Test中英文…",
"Long Long …",
})
end)
describe("separator", function()
it("text supports string", function()
menu = Menu(popup_options, {
lines = {
Menu.item("A"),
Menu.separator("Group"),
},
min_width = 10,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"A",
" Group ",
})
end)
it("content longer than max_width is truncated", function()
menu = Menu(popup_options, {
lines = {
Menu.item("A"),
Menu.separator("Long Long Group"),
},
max_width = 10,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"A",
" Long Lo… ",
})
end)
it("text supports nui.text", function()
local hl_group = "NuiMenuTest"
local text = "Group"
menu = Menu(popup_options, {
lines = {
Menu.item("A"),
Menu.separator(Text(text, hl_group)),
},
min_width = 10,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"A",
" Group ",
})
h.assert_highlight(menu.bufnr, menu.ns_id, 2, text, hl_group)
end)
it("text supports nui.line", function()
local hl_group = "NuiMenuTest"
local text = "Group"
menu = Menu(popup_options, {
lines = {
Menu.item("A"),
Menu.separator(Line({ Text(text, hl_group), Text(" nui.text") })),
},
min_width = 10,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"A",
" Group nui.t… ",
})
h.assert_highlight(menu.bufnr, menu.ns_id, 2, text, hl_group)
end)
it("o.char supports string", function()
menu = Menu(popup_options, {
lines = {
Menu.item("A"),
Menu.separator("Group", {
char = "*",
text_align = "right",
}),
},
min_width = 10,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"A",
"****Group*",
})
end)
it("o.char supports nui.text", function()
local hl_group = "NuiMenuTest"
menu = Menu(popup_options, {
lines = {
Menu.item("A"),
Menu.separator("Group", {
char = Text("*", hl_group),
text_align = "center",
}),
},
min_width = 10,
})
menu:mount()
h.assert_buf_lines(menu.bufnr, {
"A",
"**Group***",
})
local linenr = 2
local extmarks = h.get_line_extmarks(menu.bufnr, menu.ns_id, linenr)
eq(#extmarks, 4)
h.assert_extmark(extmarks[1], linenr, "*", hl_group)
h.assert_extmark(extmarks[2], linenr, "*", hl_group)
h.assert_extmark(extmarks[3], linenr, "**", hl_group)
h.assert_extmark(extmarks[4], linenr, "*", hl_group)
end)
end)
describe("w/ Layout", function()
it("can be used", function()
menu = Menu({}, {
lines = {
Menu.item("A"),
},
})
local layout = Layout(
{
position = "50%",
size = "100%",
},
Layout.Box({
Layout.Box(menu, { size = "100%" }),
})
)
layout:mount()
h.assert_buf_lines(menu.bufnr, {
"A",
})
end)
end)
end)

View file

@ -0,0 +1,413 @@
pcall(require, "luacov")
local h = require("tests.helpers")
local Object = require("nui.object")
local spy = require("luassert.spy")
local function assert_class(Class, SuperClass, name)
h.eq(type(Class), "table")
h.eq(Class.super, SuperClass)
h.eq(Class.name, name)
h.eq(tostring(Class), "class " .. name)
h.eq(type(Class.new), "function")
h.eq(type(Class.extend), "function")
local is_callable = pcall(function()
return Class()
end)
h.eq(is_callable, true)
end
local function assert_instance(instance, Class)
h.eq(instance.class, Class)
h.eq(tostring(instance), "instance of class " .. Class.name)
h.eq(instance.name, nil)
h.eq(instance.super, nil)
h.eq(instance.static, nil)
h.eq(instance.new, nil)
h.eq(instance.extend, nil)
end
local function create_classes(...)
local by_name = {}
local classes = {}
for i, def in ipairs({ ... }) do
if type(def) == "string" then
local class = Object(def)
assert_class(class, nil, def)
by_name[def] = class
classes[i] = class
elseif type(def) == "table" then
local super = type(def[2]) == "table" and def[2] or (by_name[def[2]] and by_name[def[2]] or nil)
local class = super and super:extend(def[1]) or Object(def[1])
assert_class(class, super, def[1])
by_name[def[1]] = class
classes[i] = class
else
error("invalid argument")
end
end
return unpack(classes)
end
describe("nui.object", function()
describe("class", function()
it("can be created", function()
local Class = Object("Class")
assert_class(Class, nil, "Class")
end)
describe("static", function()
describe("method", function()
describe(":new", function()
it("is called when creating instance", function()
local Class = Object("Class")
spy.on(Class.static, "new")
Class()
assert.spy(Class.static.new).called_with(Class)
Class.static.new:revert()
spy.on(Class.static, "new")
Class:new()
assert.spy(Class.static.new).called_with(Class)
Class.static.new:revert()
end)
it("creates new instance", function()
local Class = Object("Class")
local instance = Class:new()
assert_instance(instance, Class)
end)
end)
describe(":extend", function()
it("creates subclass", function()
local Class = Object("Class")
local SubClass = Class:extend("SubClass")
assert_class(SubClass, Class, "SubClass")
end)
end)
describe(":is_subclass_of", function()
it("works", function()
local A, B, C = create_classes("A", { "B", "A" }, { "C", "B" })
for _, class in ipairs({ A, B, C }) do
h.eq(class.is_subclass_of, Object.is_subclass)
end
h.eq(A:is_subclass_of(A), false)
h.eq(A:is_subclass_of(B), false)
h.eq(A:is_subclass_of(C), false)
h.eq(B:is_subclass_of(A), true)
h.eq(B:is_subclass_of(B), false)
h.eq(B:is_subclass_of(C), false)
h.eq(C:is_subclass_of(A), true)
h.eq(C:is_subclass_of(B), true)
h.eq(C:is_subclass_of(C), false)
end)
end)
end)
local function define_static_say_level(A)
A.static.level = 1
function A.static.say_level(class)
return "Level: " .. class.level
end
h.eq(A.level, 1)
h.eq(A:say_level(), "Level: 1")
end
it("can be defined for class", function()
local A = create_classes("A")
define_static_say_level(A)
end)
it("is inherited by subclass", function()
local A, B = create_classes("A", { "B", "A" })
define_static_say_level(A)
h.eq(B.level, 1)
h.eq(B:say_level(), "Level: 1")
local C, D = create_classes({ "C", A }, { "D", B })
h.eq(C.level, 1)
h.eq(C:say_level(), "Level: 1")
h.eq(D.level, 1)
h.eq(D:say_level(), "Level: 1")
end)
it("can be redefined for subclass", function()
local A = create_classes("A")
define_static_say_level(A)
local B = create_classes({ "B", A })
B.static.level = 2
h.eq(B:say_level(), "Level: 2")
function B.static.say_level(class)
return "LEVEL: " .. class.level
end
h.eq(B:say_level(), "LEVEL: 2")
local C, D = create_classes({ "C", A }, { "D", B })
C.static.level = 2
h.eq(C:say_level(), "Level: 2")
D.static.level = 3
h.eq(D:say_level(), "LEVEL: 3")
end)
it("for subclass does not affect super", function()
local A = create_classes("A")
define_static_say_level(A)
local B = create_classes({ "B", A })
B.static.level = 2
function B.static.say_level(class)
return "LEVEL: " .. class.level
end
h.eq(A:say_level(), "Level: 1")
local C = create_classes({ "C", B })
function C.static.say_name(class)
return class.name
end
h.eq(C:say_name(), "C")
h.eq(type(C.say_name), "function")
h.eq(type(B.say_name), "nil")
h.eq(type(A.say_name), "nil")
end)
end)
describe("instance", function()
it("can be created", function()
local A = create_classes("A")
local a = A:new()
assert_instance(a, A)
end)
describe("method", function()
describe(":is_instance_of", function()
it("works", function()
local A, B, C, D = create_classes("A", { "B", "A" }, { "C", "B" }, "D")
local a, b, c, d = A:new(), B:new(), C:new(), D:new()
for _, instance in ipairs({ a, b, c, d }) do
h.eq(instance.is_instance_of, Object.is_instance)
end
h.eq(a:is_instance_of(A), true)
h.eq(a:is_instance_of(B), false)
h.eq(a:is_instance_of(C), false)
h.eq(a:is_instance_of(D), false)
h.eq(b:is_instance_of(A), true)
h.eq(b:is_instance_of(B), true)
h.eq(b:is_instance_of(C), false)
h.eq(b:is_instance_of(D), false)
h.eq(c:is_instance_of(A), true)
h.eq(c:is_instance_of(B), true)
h.eq(c:is_instance_of(C), true)
h.eq(c:is_instance_of(D), false)
h.eq(d:is_instance_of(A), false)
h.eq(d:is_instance_of(B), false)
h.eq(d:is_instance_of(C), false)
h.eq(d:is_instance_of(D), true)
end)
end)
it("can be defined", function()
local A = create_classes("A")
function A:before_instance_creation()
return "before " .. self.class.name .. " instance"
end
local a = A:new()
function A:after_instance_creation()
return "after " .. self.class.name .. " instance"
end
h.eq(a:before_instance_creation(), "before A instance")
h.eq(a:after_instance_creation(), "after A instance")
end)
it("can be inherited", function()
local A, B = create_classes("A", { "B", "A" })
function A:say_class_name()
return self.class.name
end
local a = A:new()
h.eq(a:say_class_name(), "A")
local b = B:new()
h.eq(b:say_class_name(), "B")
local C = create_classes({ "C", B })
local c = C:new()
h.eq(c:say_class_name(), "C")
end)
it("can be redefined", function()
local A, B = create_classes("A", { "B", "A" })
function A:say_class_name()
return self.class.name
end
local a = A:new()
h.eq(a:say_class_name(), "A")
function B:say_class_name()
return string.lower(self.class.name)
end
local b = B:new()
h.eq(b:say_class_name(), "b")
local C = create_classes({ "C", B })
local c = C:new()
h.eq(c:say_class_name(), "c")
function C:say_class_name()
return string.rep(self.class.name, 3)
end
h.eq(c:say_class_name(), "CCC")
C.say_class_name = nil
h.eq(c:say_class_name(), "c")
B.say_class_name = nil
h.eq(c:say_class_name(), "C")
end)
end)
describe("metamethod", function()
describe("__index", function()
it("can be set to table", function()
local A = create_classes("A")
function A:upper(str) -- luacheck: no unused args
return string.upper(str)
end
A.__index = {
upper = function(_, str)
return str
end,
lower = function(_, str)
return string.lower(str)
end,
}
local a = A()
h.eq(a:upper("y"), "Y")
h.eq(a:lower("Y"), "y")
A.__index = nil
h.eq(type(a.lower), "nil")
end)
it("can be set to function", function()
local A = create_classes("A")
function A:upper(str) -- luacheck: no unused args
return string.upper(str)
end
local index = {
upper = function(self, str) -- luacheck: no unused args
return str
end,
lower = function(self, str) -- luacheck: no unused args
return string.lower(str)
end,
}
A.__index = function(self, key) -- luacheck: no unused args
return index[key]
end
local a = A()
h.eq(a:upper("y"), "Y")
h.eq(a:lower("Y"), "y")
A.__index = nil
h.eq(type(a.lower), "nil")
end)
end)
describe("__tostring", function()
it("can be redefined", function()
local A, B = create_classes("A", { "B", "A" })
local a = A()
h.eq(tostring(a), "instance of class A")
function A:__tostring()
return "class " .. self.class.name .. "'s child"
end
h.eq(tostring(a), "class A's child")
local b = B()
h.eq(tostring(b), "class B's child")
function B:__tostring()
return "child of " .. self.class.name
end
h.eq(tostring(b), "child of B")
B.__tostring = nil
h.eq(tostring(b), "class B's child")
end)
end)
end)
end)
end)
end)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,632 @@
pcall(require, "luacov")
local Line = require("nui.line")
local Table = require("nui.table")
local Text = require("nui.text")
local h = require("tests.helpers")
local eq = h.eq
describe("nui.table", function()
---@type number, number
local winid, bufnr
before_each(function()
winid = vim.api.nvim_get_current_win()
bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_win_set_buf(winid, bufnr)
end)
after_each(function()
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
describe("o.bufnr", function()
it("throws if missing", function()
local ok, err = pcall(Table, {})
eq(ok, false)
eq(type(string.match(err, "missing bufnr")), "string")
end)
it("throws if invalid", function()
local ok, err = pcall(Table, { bufnr = 999 })
eq(ok, false)
eq(type(string.match(err, "invalid bufnr ")), "string")
end)
it("sets t.bufnr properly", function()
local table = Table({ bufnr = bufnr })
eq(table.bufnr, bufnr)
end)
end)
describe("o.buf_options", function()
it("sets default buf options emulating scratch-buffer", function()
local table = Table({ bufnr = bufnr })
h.assert_buf_options(table.bufnr, {
bufhidden = "hide",
buflisted = false,
buftype = "nofile",
swapfile = false,
})
end)
it("locks buffer by default", function()
local table = Table({ bufnr = bufnr })
h.assert_buf_options(table.bufnr, {
modifiable = false,
readonly = true,
undolevels = 0,
})
end)
it("sets values", function()
local table = Table({
bufnr = bufnr,
buf_options = {
undolevels = -1,
},
})
h.assert_buf_options(table.bufnr, {
undolevels = -1,
})
end)
end)
describe("o.ns_id", function()
it("sets t.ns_id if o.ns_id is string", function()
local ns = "NuiTest"
local table = Table({ bufnr = bufnr, ns_id = ns })
local namespaces = vim.api.nvim_get_namespaces()
eq(table.ns_id, namespaces[ns])
end)
it("sets t.ns_id if o.ns_id is number", function()
local ns = "NuiTest"
local ns_id = vim.api.nvim_create_namespace(ns)
local table = Table({ bufnr = bufnr, ns_id = ns_id })
eq(table.ns_id, ns_id)
end)
end)
describe("o.columns", function()
describe(".id", function()
it("fallbacks t o .accessor_key", function()
local table = Table({
bufnr = bufnr,
columns = { { accessor_key = "ID" } },
data = { { ID = 42 } },
})
table:render()
vim.api.nvim_win_set_cursor(winid, { 2, 3 })
eq(table:get_cell().column.id, "ID")
end)
for header_type, header in pairs({
string = "ID",
NuiText = Text("ID"),
NuiLine = Line({ Text("I"), Text("D") }),
}) do
it(string.format("fallbacks to .header (%s)", header_type), function()
local table = Table({
bufnr = bufnr,
columns = {
{
header = header,
accessor_fn = function()
return ""
end,
},
},
data = { {} },
})
table:render()
vim.api.nvim_win_set_cursor(winid, { 4, 3 })
eq(table:get_cell().column.id, "ID")
end)
end
it("throws if missing", function()
local ok, err = pcall(function()
return Table({
bufnr = bufnr,
columns = { {} },
})
end)
eq(ok, false)
eq(type(string.match(err, "missing column id")), "string")
end)
end)
end)
describe("method :render", function()
local columns
local data
before_each(function()
columns = {
{
header = "First Name",
accessor_key = "firstName",
footer = "firstName",
},
{
header = "Last Name",
accessor_key = "lastName",
footer = "lastName",
},
}
data = {
{
firstName = "tanner",
lastName = "linsley",
age = 24,
visits = 100,
status = "In Relationship",
progress = 50,
},
{
firstName = "tandy",
lastName = "miller",
age = 40,
visits = 40,
status = "Single",
progress = 80,
},
{
firstName = "joe",
lastName = "dirte",
age = 45,
visits = 20,
status = "Complicated",
progress = 10,
},
}
end)
it("can handle empty columns", function()
local table = Table({
bufnr = bufnr,
data = data,
})
table:render()
h.assert_buf_lines(table.bufnr, { "" })
end)
it("can handle empty data", function()
local table = Table({
bufnr = bufnr,
columns = {
{
accessor_key = "firstName",
},
},
})
table:render()
h.assert_buf_lines(table.bufnr, { "" })
end)
it("can handle empty columns and data", function()
local table = Table({ bufnr = bufnr })
table:render()
h.assert_buf_lines(table.bufnr, { "" })
end)
it("works w/ header w/ footer", function()
local table = Table({
bufnr = bufnr,
columns = columns,
data = data,
})
table:render()
h.assert_buf_lines(table.bufnr, {
"┌──────────┬─────────┐",
"│First Name│Last Name│",
"├──────────┼─────────┤",
"│tanner │linsley │",
"├──────────┼─────────┤",
"│tandy │miller │",
"├──────────┼─────────┤",
"│joe │dirte │",
"├──────────┼─────────┤",
"│firstName │lastName │",
"└──────────┴─────────┘",
})
end)
it("works w/ header w/o footer", function()
for _, column in ipairs(columns) do
column.align = "center"
column.footer = nil
end
local table = Table({
bufnr = bufnr,
columns = columns,
data = data,
})
table:render()
h.assert_buf_lines(table.bufnr, {
"┌──────────┬─────────┐",
"│First Name│Last Name│",
"├──────────┼─────────┤",
"│ tanner │ linsley │",
"├──────────┼─────────┤",
"│ tandy │ miller │",
"├──────────┼─────────┤",
"│ joe │ dirte │",
"└──────────┴─────────┘",
})
end)
it("works w/o header w/ footer", function()
for _, column in ipairs(columns) do
column.header = nil
end
local table = Table({
bufnr = bufnr,
columns = columns,
data = data,
})
table:render()
h.assert_buf_lines(table.bufnr, {
"┌─────────┬────────┐",
"│tanner │linsley │",
"├─────────┼────────┤",
"│tandy │miller │",
"├─────────┼────────┤",
"│joe │dirte │",
"├─────────┼────────┤",
"│firstName│lastName│",
"└─────────┴────────┘",
})
end)
it("works w/o header w/o footer", function()
for _, column in ipairs(columns) do
column.header = nil
column.footer = nil
end
local table = Table({
bufnr = bufnr,
columns = columns,
data = data,
})
table:render()
h.assert_buf_lines(table.bufnr, {
"┌──────┬───────┐",
"│tanner│linsley│",
"├──────┼───────┤",
"│tandy │miller │",
"├──────┼───────┤",
"│joe │dirte │",
"└──────┴───────┘",
})
end)
it("supports param linenr_start", function()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
"START: NuiTest",
"",
"END: NuiTest",
})
local table = Table({
bufnr = bufnr,
columns = columns,
data = { data[1] },
})
table:render(2)
h.assert_buf_lines(table.bufnr, {
"START: NuiTest",
"┌──────────┬─────────┐",
"│First Name│Last Name│",
"├──────────┼─────────┤",
"│tanner │linsley │",
"├──────────┼─────────┤",
"│firstName │lastName │",
"└──────────┴─────────┘",
"END: NuiTest",
})
table:render(4)
h.assert_buf_lines(table.bufnr, {
"START: NuiTest",
"",
"",
"┌──────────┬─────────┐",
"│First Name│Last Name│",
"├──────────┼─────────┤",
"│tanner │linsley │",
"├──────────┼─────────┤",
"│firstName │lastName │",
"└──────────┴─────────┘",
"END: NuiTest",
})
table:render(3)
h.assert_buf_lines(table.bufnr, {
"START: NuiTest",
"",
"┌──────────┬─────────┐",
"│First Name│Last Name│",
"├──────────┼─────────┤",
"│tanner │linsley │",
"├──────────┼─────────┤",
"│firstName │lastName │",
"└──────────┴─────────┘",
"END: NuiTest",
})
end)
describe("grouped columns", function()
local grouped_columns
before_each(function()
grouped_columns = {
{
header = "Name",
footer = function(info)
return info.column.id
end,
columns = {
{
accessor_key = "firstName",
footer = "firstName",
},
{
id = "lastName",
header = "Last Name",
accessor_key = "lastName",
footer = function(info)
return info.column.id
end,
},
},
},
{
header = "Info",
footer = function(info)
return info.column.id
end,
columns = {
{
header = "Age",
accessor_key = "age",
footer = "age",
},
{
header = "More Info",
footer = function(info)
return info.column.id
end,
columns = {
{
accessor_key = "visits",
header = "Visits",
footer = function(info)
return info.column.id
end,
},
{
accessor_key = "status",
header = "Status",
footer = function(info)
return info.column.id
end,
},
},
},
},
},
{
header = "Profile Progress",
accessor_key = "progress",
footer = function(info)
return info.column.id
end,
},
}
end)
it("is drawn correctly", function()
local table = Table({
bufnr = bufnr,
columns = grouped_columns,
data = data,
})
table:render()
h.assert_buf_lines(table.bufnr, {
"┌───────────────────┬──────────────────────────┬────────────────┐",
"│Name │Info │ │",
"├─────────┬─────────┼───┬──────────────────────┤ │",
"│ │ │ │More Info │ │",
"│ │ │ ├──────┬───────────────┤ │",
"│firstName│Last Name│Age│Visits│Status │Profile Progress│",
"├─────────┼─────────┼───┼──────┼───────────────┼────────────────┤",
"│tanner │linsley │24 │100 │In Relationship│50 │",
"├─────────┼─────────┼───┼──────┼───────────────┼────────────────┤",
"│tandy │miller │40 │40 │Single │80 │",
"├─────────┼─────────┼───┼──────┼───────────────┼────────────────┤",
"│joe │dirte │45 │20 │Complicated │10 │",
"├─────────┼─────────┼───┼──────┼───────────────┼────────────────┤",
"│firstName│lastName │age│visits│status │progress │",
"│ │ │ ├──────┴───────────────┤ │",
"│ │ │ │More Info │ │",
"├─────────┴─────────┼───┴──────────────────────┤ │",
"│Name │Info │ │",
"└───────────────────┴──────────────────────────┴────────────────┘",
})
end)
end)
end)
describe("method :get_cell", function()
it("returns nil on border", function()
local table = Table({
bufnr = bufnr,
columns = { { accessor_key = "value" } },
data = { { value = "Such Value!" } },
})
table:render()
vim.api.nvim_win_set_cursor(winid, { 1, 5 })
local cell = table:get_cell()
eq(cell, nil)
end)
it("works after shifting", function()
local table = Table({
bufnr = bufnr,
columns = { { accessor_key = "value" } },
data = { { id = 0, value = "Such Value!" } },
})
table:render()
local cell
vim.api.nvim_win_set_cursor(winid, { 2, 5 })
cell = table:get_cell()
eq(type(cell), "table")
eq(cell.row.original.id, 0)
table:render(2)
vim.api.nvim_win_set_cursor(winid, { 2, 5 })
cell = table:get_cell()
eq(type(cell), "nil")
vim.api.nvim_win_set_cursor(winid, { 3, 5 })
cell = table:get_cell()
eq(type(cell), "table")
eq(cell.row.original.id, 0)
end)
it("can take position", function()
local table = Table({
bufnr = bufnr,
columns = {
{ accessor_key = "id" },
{ accessor_key = "value" },
},
data = {
{ id = 1, value = "One" },
{ id = 2, value = "Two" },
},
})
table:render()
local cell
vim.api.nvim_win_set_cursor(winid, { 2, 3 })
cell = table:get_cell()
eq(cell.get_value(), 1)
cell = table:get_cell({ 1, 1 })
eq(cell.get_value(), "Two")
end)
end)
describe("method :refresh_cell", function()
it("can truncate NuiText on refesh", function()
local table = Table({
bufnr = bufnr,
columns = { { accessor_key = "value" } },
data = { { value = "Such Value!" } },
})
table:render()
h.assert_buf_lines(table.bufnr, {
"┌───────────┐",
"│Such Value!│",
"└───────────┘",
})
vim.api.nvim_win_set_cursor(winid, { 2, 5 })
local cell = table:get_cell()
cell.row.original.value = "Such Looooooog Value!"
table:refresh_cell(cell)
h.assert_buf_lines(table.bufnr, {
"┌───────────┐",
"│Such Loooo…│",
"└───────────┘",
})
end)
it("can truncate NuiLine on refesh", function()
local table = Table({
bufnr = bufnr,
columns = {
{
accessor_key = "value",
cell = function(cell)
return Line({ Text(tostring(cell.get_value()), "NuiTest"), Text(" years old") })
end,
},
},
data = { { value = 42 } },
})
table:render()
h.assert_buf_lines(table.bufnr, {
"┌────────────┐",
"│42 years old│",
"└────────────┘",
})
vim.api.nvim_win_set_cursor(winid, { 2, 5 })
local cell = table:get_cell()
eq(type(cell), "table")
cell.row.original.value = 100
table:refresh_cell(cell)
h.assert_buf_lines(table.bufnr, {
"┌────────────┐",
"│100 years o…│",
"└────────────┘",
})
end)
end)
end)

View file

@ -0,0 +1,284 @@
pcall(require, "luacov")
local Text = require("nui.text")
local h = require("tests.helpers")
local spy = require("luassert.spy")
local eq, tbl_omit = h.eq, h.tbl_omit
describe("nui.text", function()
local multibyte_char
before_each(function()
multibyte_char = ""
end)
it("can clone nui.text object", function()
local hl_group = "NuiTextTest"
local t1 = Text("42", hl_group)
t1.extmark.id = 42
local t2 = Text(t1)
eq(t2:content(), t1:content())
eq(t2.extmark, tbl_omit(t1.extmark, { "id" }))
t2.extmark.id = 42
local t3 = Text(t2)
eq(t3:content(), t2:content())
eq(t3.extmark, tbl_omit(t2.extmark, { "id" }))
end)
it("can clone nui.text object overriding extmark", function()
local hl_group = "NuiTextTest"
local hl_group_override = "NuiTextTestOverride"
local t1 = Text("42", hl_group)
t1.extmark.id = 42
local t2 = Text(t1, hl_group_override)
eq(t2:content(), t1:content())
eq(t2.extmark, { hl_group = hl_group_override })
local t3 = Text(t2, { id = 42, hl_group = hl_group })
eq(t3:content(), t2:content())
eq(t3.extmark, { hl_group = hl_group })
end)
describe("method :set", function()
it("works", function()
local hl_group = "NuiTextTest"
local hl_group_override = "NuiTextTestOverride"
local text = Text("42", hl_group)
eq(text:content(), "42")
eq(text:length(), 2)
eq(text.extmark, {
hl_group = hl_group,
})
text.extmark.id = 42
text:set("3")
eq(text:content(), "3")
eq(text:length(), 1)
eq(text.extmark, {
hl_group = hl_group,
id = 42,
})
text:set("9", hl_group_override)
eq(text:content(), "9")
eq(text.extmark, {
hl_group = hl_group_override,
id = 42,
})
text:set("11", { hl_group = hl_group })
eq(text:content(), "11")
eq(text.extmark, {
hl_group = hl_group,
id = 42,
})
text.extmark.id = nil
text:set("42", { id = 42, hl_group = hl_group })
eq(text:content(), "42")
eq(text.extmark, { hl_group = hl_group })
end)
end)
describe("method :content", function()
it("works", function()
local content = "42"
local text = Text(content)
eq(text:content(), content)
local multibyte_content = multibyte_char
local multibyte_text = Text(multibyte_content)
eq(multibyte_text:content(), multibyte_content)
end)
end)
describe("method :length", function()
it("works", function()
local content = "42"
local text = Text(content)
eq(text:length(), 2)
eq(text:length(), vim.fn.strlen(content))
local multibyte_content = multibyte_char
local multibyte_text = Text(multibyte_content)
eq(multibyte_text:length(), 3)
eq(multibyte_text:length(), vim.fn.strlen(multibyte_content))
end)
end)
describe("method :width", function()
it("works", function()
local content = "42"
local text = Text(content)
eq(text:width(), 2)
eq(text:width(), vim.fn.strwidth(content))
local multibyte_content = multibyte_char
local multibyte_text = Text(multibyte_content)
eq(multibyte_text:width(), 1)
eq(multibyte_text:width(), vim.fn.strwidth(multibyte_content))
end)
end)
describe("method", function()
local winid, bufnr
local initial_lines
before_each(function()
winid = vim.api.nvim_get_current_win()
bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_win_set_buf(winid, bufnr)
initial_lines = { " 1", multibyte_char .. " 2", " 3" }
end)
after_each(function()
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
local function reset_lines(lines)
initial_lines = lines or initial_lines
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, initial_lines)
end
describe(":highlight", function()
local hl_group, ns, ns_id
local linenr, byte_start
local text
before_each(function()
hl_group = "NuiTextTest"
ns = "NuiTest"
ns_id = vim.api.nvim_create_namespace(ns)
end)
it("is applied with :render", function()
reset_lines()
linenr, byte_start = 1, 0
text = Text("a", hl_group)
text:render(bufnr, ns_id, linenr, byte_start)
h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group)
end)
it("is applied with :render_char", function()
reset_lines()
linenr, byte_start = 1, 0
text = Text(multibyte_char, hl_group)
text:render_char(bufnr, ns_id, linenr, byte_start)
h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group)
end)
it("can highlight existing buffer text", function()
reset_lines()
linenr, byte_start = 2, 0
text = Text(initial_lines[linenr], hl_group)
text:highlight(bufnr, ns_id, linenr, byte_start)
h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group)
end)
it("does not create multiple extmarks", function()
reset_lines()
linenr, byte_start = 2, 0
text = Text(initial_lines[linenr], hl_group)
text:highlight(bufnr, ns_id, linenr, byte_start)
h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group)
text:highlight(bufnr, ns_id, linenr, byte_start)
h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group)
text:highlight(bufnr, ns_id, linenr, byte_start)
h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group)
end)
end)
describe(":render", function()
it("works on line with singlebyte characters", function()
reset_lines()
local text = Text("a")
spy.on(text, "highlight")
text:render(bufnr, -1, 1, 1)
assert.spy(text.highlight).was_called(1)
assert.spy(text.highlight).was_called_with(text, bufnr, -1, 1, 1)
h.assert_buf_lines(bufnr, {
" a1",
initial_lines[2],
initial_lines[3],
})
end)
it("works on line with multibyte characters", function()
reset_lines()
local text = Text("a")
spy.on(text, "highlight")
text:render(bufnr, -1, 2, vim.fn.strlen(multibyte_char))
assert.spy(text.highlight).was_called(1)
assert.spy(text.highlight).was_called_with(text, bufnr, -1, 2, vim.fn.strlen(multibyte_char))
h.assert_buf_lines(bufnr, {
initial_lines[1],
multibyte_char .. "a2",
initial_lines[3],
})
end)
end)
describe(":render_char", function()
it("works on line with singlebyte characters", function()
reset_lines()
local text = Text("a")
spy.on(text, "highlight")
text:render_char(bufnr, -1, 1, 1)
assert.spy(text.highlight).was_called(1)
assert.spy(text.highlight).was_called_with(text, bufnr, -1, 1, 1)
h.assert_buf_lines(bufnr, {
" a1",
initial_lines[2],
initial_lines[3],
})
end)
it("works on line with multibyte characters", function()
reset_lines()
local text = Text("a")
spy.on(text, "highlight")
text:render_char(bufnr, -1, 2, 1)
assert.spy(text.highlight).was_called(1)
assert.spy(text.highlight).was_called_with(text, bufnr, -1, 2, vim.fn.strlen(multibyte_char))
h.assert_buf_lines(bufnr, {
initial_lines[1],
multibyte_char .. "a2",
initial_lines[3],
})
end)
end)
end)
end)

View file

@ -0,0 +1,912 @@
pcall(require, "luacov")
local Text = require("nui.text")
local Tree = require("nui.tree")
local h = require("tests.helpers")
local eq = h.eq
describe("nui.tree", function()
local winid, bufnr
before_each(function()
winid = vim.api.nvim_get_current_win()
bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_win_set_buf(winid, bufnr)
end)
after_each(function()
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
describe("(#deprecated) o.winid", function()
it("throws if missing", function()
local ok, err = pcall(function()
return Tree({})
end)
eq(ok, false)
eq(type(string.match(err, "missing bufnr")), "string")
end)
it("throws if invalid", function()
local ok, err = pcall(function()
return Tree({ winid = 999 })
end)
eq(ok, false)
eq(type(string.match(err, "invalid winid ")), "string")
end)
it("sets t.winid and t.bufnr properly", function()
local tree = Tree({ winid = winid })
eq(tree.winid, winid)
eq(tree.bufnr, bufnr)
end)
end)
describe("o.bufnr", function()
it("throws if missing", function()
local ok, err = pcall(function()
return Tree({})
end)
eq(ok, false)
eq(type(string.match(err, "missing bufnr")), "string")
end)
it("throws if invalid", function()
local ok, err = pcall(function()
return Tree({ bufnr = 999 })
end)
eq(ok, false)
eq(type(string.match(err, "invalid bufnr ")), "string")
end)
it("sets t.bufnr properly", function()
local tree = Tree({ bufnr = bufnr })
eq(tree.winid, nil)
eq(tree.bufnr, bufnr)
end)
end)
it("throws on duplicated node id", function()
local ok, err = pcall(function()
return Tree({
bufnr = bufnr,
nodes = {
Tree.Node({ id = "id", text = "text" }),
Tree.Node({ id = "id", text = "text" }),
},
})
end)
eq(ok, false)
eq(type(err), "string")
end)
it("sets default buf options emulating scratch-buffer", function()
local tree = Tree({ bufnr = bufnr })
h.assert_buf_options(tree.bufnr, {
bufhidden = "hide",
buflisted = false,
buftype = "nofile",
swapfile = false,
})
end)
describe("(#deprecated) o.win_options", function()
it("sets default values for handling folds", function()
local tree = Tree({ winid = winid })
h.assert_win_options(tree.winid, {
foldmethod = "manual",
foldcolumn = "0",
wrap = false,
})
end)
it("sets values", function()
local initial_statusline = vim.api.nvim_win_get_option(winid, "statusline")
local statusline = "test: win_options " .. math.random()
local tree = Tree({
winid = winid,
win_options = {
statusline = statusline,
},
})
h.assert_win_options(tree.winid, {
statusline = statusline,
})
vim.api.nvim_win_set_option(tree.winid, "statusline", initial_statusline)
end)
it("has no effect if o.bufnr is present", function()
local initial_statusline = vim.api.nvim_win_get_option(winid, "statusline")
Tree({
bufnr = bufnr,
win_options = {
statusline = "test: win_options" .. math.random(),
},
})
h.assert_win_options(winid, {
statusline = initial_statusline,
})
end)
end)
it("sets t.ns_id if o.ns_id is string", function()
local ns = "NuiTreeTest"
local tree = Tree({ bufnr = bufnr, ns_id = ns })
local namespaces = vim.api.nvim_get_namespaces()
eq(tree.ns_id, namespaces[ns])
end)
it("sets t.ns_id if o.ns_id is number", function()
local ns = "NuiTreeTest"
local ns_id = vim.api.nvim_create_namespace(ns)
local tree = Tree({ bufnr = bufnr, ns_id = ns_id })
eq(tree.ns_id, ns_id)
end)
it("uses o.get_node_id if provided", function()
local node_d2 = Tree.Node({ key = "depth two" })
local node_d1 = Tree.Node({ key = "depth one" }, { node_d2 })
Tree({
bufnr = bufnr,
nodes = { node_d1 },
get_node_id = function(node)
return node.key
end,
})
eq(node_d1:get_id(), node_d1.key)
eq(node_d2:get_id(), node_d2.key)
end)
describe("default get_node_id", function()
it("returns id using n.id", function()
local node = Tree.Node({ id = "id", text = "text" })
Tree({ bufnr = bufnr, nodes = { node } })
eq(node:get_id(), "-id")
end)
it("returns id using parent_id + depth + n.text", function()
local node_d2 = Tree.Node({ text = { "depth two a", Text("depth two b") } })
local node_d1 = Tree.Node({ text = "depth one" }, { node_d2 })
Tree({ bufnr = bufnr, nodes = { node_d1 } })
eq(node_d1:get_id(), string.format("-%s-%s", node_d1:get_depth(), node_d1.text))
eq(
node_d2:get_id(),
string.format(
"%s-%s-%s",
node_d2:get_parent_id(),
node_d2:get_depth(),
table.concat({ node_d2.text[1], node_d2.text[2]:content() }, "-")
)
)
end)
it("returns id using random number", function()
math.randomseed(0)
local expected_id = "-" .. math.random()
math.randomseed(0)
local node = Tree.Node({})
Tree({ bufnr = bufnr, nodes = { node } })
eq(node:get_id(), expected_id)
end)
end)
it("uses o.prepare_node if provided", function()
local function prepare_node(node, parent_node)
if not parent_node then
return node.text
end
return parent_node.text .. ":" .. node.text
end
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, {
Tree.Node({ text = "b-1" }),
Tree.Node({ text = "b-2" }),
}),
Tree.Node({ text = "c" }),
}
nodes[2]:expand()
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
prepare_node = prepare_node,
})
tree:render()
h.assert_buf_lines(tree.bufnr, {
"a",
"b",
"b:b-1",
"b:b-2",
"c",
})
end)
describe("default prepare_node", function()
it("throws if missing n.text", function()
local nodes = {
Tree.Node({ txt = "a" }),
Tree.Node({ txt = "b" }),
Tree.Node({ txt = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
})
local ok, err = pcall(tree.render, tree)
eq(ok, false)
eq(type(err), "string")
end)
it("uses n.text", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = { "b-1", "b-2" } }),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
})
tree:render()
h.assert_buf_lines(tree.bufnr, {
" a",
" b-1",
" b-2",
" c",
})
end)
it("renders arrow if children are present", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, {
Tree.Node({ text = "b-1" }),
Tree.Node({ text = { "b-2", "b-3" } }),
}),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
})
tree:render()
h.assert_buf_lines(tree.bufnr, {
" a",
" b",
" c",
})
nodes[2]:expand()
tree:render()
h.assert_buf_lines(tree.bufnr, {
" a",
" b",
" b-1",
" b-2",
" b-3",
" c",
})
end)
end)
describe("method :get_node", function()
it("can get node under cursor", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
})
tree:render()
local linenr = 3
vim.api.nvim_win_set_cursor(winid, { linenr, 0 })
eq({ tree:get_node() }, { nodes[3], linenr, linenr })
end)
it("can get node with id", function()
local b_node_children = {
Tree.Node({ text = "b-1" }),
Tree.Node({ text = { "b-2", "b-3" } }),
}
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, b_node_children),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return type(node.text) == "table" and table.concat(node.text, "-") or node.text
end,
})
tree:render()
eq({ tree:get_node("b") }, { nodes[2], 2, 2 })
tree:get_node("b"):expand()
tree:render()
eq({ tree:get_node("b-2-b-3") }, { b_node_children[2], 4, 5 })
end)
it("can get node on linenr", function()
local b_node_children = {
Tree.Node({ id = "b-1-b-2", text = { "b-1", "b-2" } }),
}
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, b_node_children),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
})
tree:render()
eq({ tree:get_node(1) }, { nodes[1], 1, 1 })
tree:get_node(2):expand()
tree:render()
eq({ tree:get_node(3) }, { b_node_children[1], 3, 4 })
eq({ tree:get_node(4) }, { b_node_children[1], 3, 4 })
end)
end)
describe("method :get_nodes", function()
it("can get nodes at root", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, {
Tree.Node({ text = "b-1" }),
}),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return node.text
end,
})
eq(tree:get_nodes(), nodes)
end)
it("can get nodes under parent node", function()
local child_nodes = {
Tree.Node({ text = "b-1" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, child_nodes),
},
get_node_id = function(node)
return node.text
end,
})
eq(tree:get_nodes("b"), child_nodes)
end)
end)
describe("method :add_node", function()
it("throw if invalid parent_id", function()
local tree = Tree({
bufnr = bufnr,
nodes = {
Tree.Node({ text = "x" }),
},
})
local ok, err = pcall(tree.add_node, tree, Tree.Node({ text = "y" }), "invalid_parent_id")
eq(ok, false)
eq(type(err), "string")
end)
it("can add node at root", function()
local tree = Tree({
bufnr = bufnr,
nodes = {
Tree.Node({ text = "x" }),
},
})
tree:add_node(Tree.Node({ text = "y" }))
tree:render()
h.assert_buf_lines(tree.bufnr, {
" x",
" y",
})
tree:add_node(Tree.Node({ text = "z" }))
tree:render()
h.assert_buf_lines(tree.bufnr, {
" x",
" y",
" z",
})
end)
it("can add node under parent node", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, {
Tree.Node({ text = "b-1" }),
}),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return node.text
end,
})
tree:add_node(Tree.Node({ text = "b-2" }), "b")
tree:get_node("b"):expand()
tree:add_node(Tree.Node({ text = "c-1" }), "c")
tree:get_node("c"):expand()
tree:render()
h.assert_buf_lines(tree.bufnr, {
" a",
" b",
" b-1",
" b-2",
" c",
" c-1",
})
end)
end)
describe("method :set_nodes", function()
it("throw if invalid parent_id", function()
local tree = Tree({
bufnr = bufnr,
nodes = {
Tree.Node({ text = "x" }),
},
})
local ok, err = pcall(tree.set_nodes, tree, {}, "invalid_parent_id")
eq(ok, false)
eq(type(err), "string")
end)
it("can set nodes at root", function()
local tree = Tree({
bufnr = bufnr,
nodes = {
Tree.Node({ text = "x" }),
},
})
tree:set_nodes({
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }),
})
tree:render()
h.assert_buf_lines(tree.bufnr, {
" a",
" b",
})
tree:set_nodes({
Tree.Node({ text = "c" }),
})
tree:render()
h.assert_buf_lines(tree.bufnr, {
" c",
})
end)
it("can set nodes under parent node", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, {
Tree.Node({ text = "b-1" }),
}),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return node.text
end,
})
tree:set_nodes({
Tree.Node({ text = "b-2" }),
}, "b")
tree:get_node("b"):expand()
tree:set_nodes({
Tree.Node({ text = "c-1" }),
Tree.Node({ text = "c-2" }),
}, "c")
tree:get_node("c"):expand()
tree:render()
h.assert_buf_lines(tree.bufnr, {
" a",
" b",
" b-2",
" c",
" c-1",
" c-2",
})
end)
end)
describe("method :remove_node", function()
it("can remove node w/o parent", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, {
Tree.Node({ text = "b-1" }),
}),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return node.text
end,
})
tree:remove_node("a")
tree:get_node("b"):expand()
tree:render()
eq(
vim.tbl_map(function(node)
return node:get_id()
end, tree:get_nodes()),
{ "b", "c" }
)
h.assert_buf_lines(tree.bufnr, {
" b",
" b-1",
" c",
})
end)
it("can remove node w/ parent", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, {
Tree.Node({ text = "b-1" }),
}),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return node.text
end,
})
tree:remove_node("b-1")
tree:render()
eq(tree:get_node("b"):get_child_ids(), {})
h.assert_buf_lines(tree.bufnr, {
" a",
" b",
" c",
})
end)
it("removes children nodes recursively", function()
local nodes = {
Tree.Node({ text = "a" }, {
Tree.Node({ text = "a-1" }, {
Tree.Node({ text = "a-1-x" }),
}),
}),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return node.text
end,
})
h.neq(tree:get_node("a"), nil)
h.neq(tree:get_node("a-1"), nil)
h.neq(tree:get_node("a-1-x"), nil)
tree:remove_node("a")
eq(tree:get_node("a"), nil)
eq(tree:get_node("a-1"), nil)
eq(tree:get_node("a-1-x"), nil)
end)
end)
describe("method :render", function()
it("handles unexpected case of missing node", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return node.text
end,
})
-- this should not happen normally
tree.nodes.by_id["a"] = nil
tree:render()
h.assert_buf_lines(tree.bufnr, {
" b",
" c",
})
end)
it("skips node if o.prepare_node returns nil", function()
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }),
Tree.Node({ text = "c" }),
}
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return node.text
end,
prepare_node = function(node)
if node:get_id() == "b" then
return nil
end
return node.text
end,
})
tree:render()
h.assert_buf_lines(tree.bufnr, {
"a",
"c",
})
end)
it("supports param linenr_start", function()
local b_node_children = {
Tree.Node({ text = "b-1" }),
Tree.Node({ text = "b-2" }),
}
local nodes = {
Tree.Node({ text = "a" }),
Tree.Node({ text = "b" }, b_node_children),
}
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
"NuiTreeTest",
"",
"NuiTreeTest",
})
local tree = Tree({
bufnr = bufnr,
nodes = nodes,
get_node_id = function(node)
return node.text
end,
})
tree:render(2)
h.assert_buf_lines(tree.bufnr, {
"NuiTreeTest",
" a",
" b",
"NuiTreeTest",
})
nodes[2]:expand()
tree:render()
h.assert_buf_lines(tree.bufnr, {
"NuiTreeTest",
" a",
" b",
" b-1",
" b-2",
"NuiTreeTest",
})
nodes[2]:collapse()
tree:render(3)
h.assert_buf_lines(tree.bufnr, {
"NuiTreeTest",
"",
" a",
" b",
"NuiTreeTest",
})
end)
end)
end)
describe("nui.tree.Node", function()
describe("method :has_children", function()
it("works before initialization", function()
local node_wo_children = Tree.Node({ text = "a" })
local node_w_children = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
eq(node_wo_children._initialized, false)
eq(node_wo_children:has_children(), false)
eq(node_w_children._initialized, false)
eq(type(node_w_children.__children), "table")
eq(node_w_children:has_children(), true)
end)
it("works after initialization", function()
local node_wo_children = Tree.Node({ text = "a" })
local node_w_children = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
Tree({
bufnr = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()),
nodes = { node_wo_children, node_w_children },
})
eq(node_wo_children._initialized, true)
eq(node_wo_children:has_children(), false)
eq(node_w_children._initialized, true)
eq(type(node_w_children.__children), "nil")
eq(node_w_children:has_children(), true)
end)
end)
describe("method :expand", function()
it("returns true if not already expanded", function()
local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
eq(node:is_expanded(), false)
eq(node:expand(), true)
eq(node:is_expanded(), true)
end)
it("returns false if already expanded", function()
local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
node:expand()
eq(node:is_expanded(), true)
eq(node:expand(), false)
eq(node:is_expanded(), true)
end)
it("does work w/ zero child", function()
local node = Tree.Node({ text = "a" }, {})
eq(node:is_expanded(), false)
eq(node:expand(), true)
eq(node:is_expanded(), true)
end)
it("does not work w/o children", function()
local node = Tree.Node({ text = "a" })
eq(node:is_expanded(), false)
eq(node:expand(), false)
eq(node:is_expanded(), false)
end)
end)
describe("method :collapse", function()
it("returns true if not already collapsed", function()
local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
node:expand()
eq(node:is_expanded(), true)
eq(node:collapse(), true)
eq(node:is_expanded(), false)
end)
it("returns false if already collapsed", function()
local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
eq(node:is_expanded(), false)
eq(node:collapse(), false)
eq(node:is_expanded(), false)
end)
it("does not work w/o children", function()
local node = Tree.Node({ text = "a" })
eq(node:is_expanded(), false)
eq(node:collapse(), false)
eq(node:is_expanded(), false)
end)
end)
end)