summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 18:12:47 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 18:12:47 +0200
commit15bc29f2aebaa18e6cbf3e954c65a8c845fae2b9 (patch)
treeaf581bc534fbf15ace7796387a6609102867e75d
parentd6ac0e7ab3c48ffdf1e2ae15dcd6be4f9780e369 (diff)
Add native plugin conformance matrix
-rw-r--r--Makefile1
-rw-r--r--tests/plugin_conformance.lua196
2 files changed, 197 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index 1551f25..b1869ef 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,7 @@ test:
NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/phase16.lua
NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/host_adapter_contract.lua
NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/plugin_smoke.lua
+ NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/plugin_conformance.lua
profile:
NVIM_LOG_FILE=/dev/null $(NVIM) --headless -u NONE -i NONE -l tests/profile.lua
diff --git a/tests/plugin_conformance.lua b/tests/plugin_conformance.lua
new file mode 100644
index 0000000..90603a9
--- /dev/null
+++ b/tests/plugin_conformance.lua
@@ -0,0 +1,196 @@
+local script = debug.getinfo(1, "S").source:sub(2)
+local root = script:match("^(.*)/tests/plugin_conformance%.lua$") or "."
+vim.opt.runtimepath:prepend(root)
+
+local domain = require("clever_f.domain")
+local assertions = dofile(root .. "/tests/assertions.lua")
+local same = assertions.same
+local truthy = assertions.truthy
+
+vim.g.clever_f_mark_cursor = 0
+vim.g.clever_f_mark_char = 0
+vim.g.clever_f_mark_direct = 0
+vim.g.clever_f_hide_cursor_on_cmdline = 0
+vim.g.clever_f_clean_labels_eagerly = 1
+vim.g.clever_f_highlight_timeout_ms = 0
+vim.cmd.runtime("plugin/clever_f.lua")
+
+local clever_f = require("clever_f")
+local checks = 0
+
+local function check_same(expected, actual, message)
+ same(expected, actual, message)
+ checks = checks + 1
+end
+
+local function check_truthy(value, message)
+ truthy(value, message)
+ checks = checks + 1
+end
+
+local function feed(keys)
+ vim.api.nvim_feedkeys(vim.keycode(keys), "xt", false)
+ vim.cmd("redraw")
+ vim.wait(10)
+end
+
+local function setup(lines, column)
+ feed("<Esc>")
+ clever_f._diagnostic_full_reset()
+ vim.g.clever_f_mark_char = 0
+ vim.g.clever_f_highlight_timeout_ms = 0
+ vim.api.nvim_buf_set_lines(0, 0, -1, true, lines)
+ vim.api.nvim_win_set_cursor(0, { 1, (column or 1) - 1 })
+end
+
+local visual_cases = {
+ { prefix = "v", mode = "v", adjusted = true },
+ { prefix = "V", mode = "V", adjusted = true },
+ { prefix = "<C-v>", mode = string.char(0x16), adjusted = false },
+}
+for _, selection in ipairs({ "inclusive", "exclusive" }) do
+ vim.o.selection = selection
+ for _, case in ipairs(visual_cases) do
+ setup({ "axz", "second" })
+ feed(case.prefix .. "fx")
+ local expected_column = selection == "exclusive" and case.adjusted and 2 or 1
+ check_same(expected_column, vim.api.nvim_win_get_cursor(0)[2], selection .. case.prefix)
+ check_same(case.mode, vim.api.nvim_get_mode().mode, selection .. case.prefix)
+ end
+end
+
+vim.o.selection = "inclusive"
+local select_cases = {
+ { prefix = "gh", mode = "s" },
+ { prefix = "gH", mode = "S" },
+ { prefix = "g<C-h>", mode = string.char(0x13) },
+}
+for _, case in ipairs(select_cases) do
+ setup({ "axz", "second" })
+ feed(case.prefix)
+ check_same(case.mode, vim.api.nvim_get_mode().mode, case.prefix)
+ vim.api.nvim_feedkeys("x", "n", false)
+ local outcome = clever_f.StartFindForward()
+ check_same(domain.ActionKind.MOVEMENT, outcome.kind, case.prefix)
+ check_same(1, vim.api.nvim_win_get_cursor(0)[2], case.prefix)
+ check_same(case.mode, vim.api.nvim_get_mode().mode, case.prefix)
+end
+
+local source = "hoge fuge piye poye"
+local operator_cases = {
+ { keys = "dfe", column = 1, text = " fuge piye poye", cursor = 0 },
+ { keys = "dte", column = 1, text = "e fuge piye poye", cursor = 0 },
+ { keys = "dFe", column = 19, text = "hoge fuge piye", cursor = 13 },
+ { keys = "dTe", column = 19, text = "hoge fuge piyee", cursor = 14 },
+}
+for _, selection in ipairs({ "inclusive", "exclusive" }) do
+ vim.o.selection = selection
+ for _, case in ipairs(operator_cases) do
+ setup({ source }, case.column)
+ feed(case.keys)
+ check_same(case.text, vim.api.nvim_get_current_line(), selection .. case.keys)
+ check_same(case.cursor, vim.api.nvim_win_get_cursor(0)[2], selection .. case.keys)
+ check_same(selection, vim.o.selection, selection .. case.keys)
+ end
+end
+
+vim.o.selection = "inclusive"
+local multibyte = "\227\129\130"
+setup({ "x" .. multibyte .. "x" .. multibyte })
+feed("f" .. multibyte)
+check_same(1, vim.api.nvim_win_get_cursor(0)[2], "first multibyte target")
+feed("f")
+check_same(5, vim.api.nvim_win_get_cursor(0)[2], "second multibyte target")
+feed("F")
+check_same(1, vim.api.nvim_win_get_cursor(0)[2], "backward multibyte target")
+
+setup({ "abc" })
+feed("f<F1>")
+check_same(0, vim.api.nvim_win_get_cursor(0)[2], "encoded special key")
+check_same(
+ domain.TargetKind.SPECIAL_KEY.value,
+ clever_f.state():get_previous_target("n").kind.value,
+ "encoded special key target kind"
+)
+
+setup({ "1a 2a 3a 4a 5a" })
+vim.fn.setreg("q", "fax")
+feed("5@q")
+check_same("1 2 3 4 5", vim.api.nvim_get_current_line(), "macro acquisitions")
+
+local saved_foldmethod = vim.wo.foldmethod
+local saved_foldopen = vim.o.foldopen
+setup({ "one", "two", "xax", "four", "five" })
+vim.wo.foldmethod = "manual"
+vim.cmd("1,5fold")
+vim.cmd("2,4fold")
+vim.cmd("normal! zM")
+vim.o.foldopen = "hor"
+vim.api.nvim_win_set_cursor(0, { 3, 0 })
+check_truthy(vim.fn.foldclosed(3) ~= -1, "nested fold starts closed")
+feed("fa")
+check_same(-1, vim.fn.foldclosed(3), "nested folds opened")
+check_same(1, vim.api.nvim_win_get_cursor(0)[2], "fold motion endpoint")
+vim.cmd("normal! zE")
+vim.wo.foldmethod = saved_foldmethod
+vim.o.foldopen = saved_foldopen
+
+local function setup_feedback(timeout)
+ setup({ "ahahah" })
+ vim.g.clever_f_mark_char = 1
+ vim.g.clever_f_highlight_timeout_ms = timeout or 0
+ feed("fh")
+ local state = clever_f.state():to_table()
+ check_same(1, #state.target_overlays, "target overlay created")
+ check_same(1, #state.finalizers, "finalizer created")
+ return state
+end
+
+setup_feedback()
+vim.api.nvim_win_set_cursor(0, { 1, 2 })
+vim.api.nvim_exec_autocmds("CursorMoved", { buffer = 0 })
+local finalized = clever_f.state():to_table()
+check_same(0, #finalized.target_overlays, "CursorMoved overlay cleanup")
+check_same(0, #finalized.finalizers, "CursorMoved finalizer cleanup")
+check_same(nil, finalized.contexts.n.previous_landing, "CursorMoved landing cleanup")
+
+setup_feedback()
+vim.api.nvim_exec_autocmds("InsertEnter", { buffer = 0 })
+finalized = clever_f.state():to_table()
+check_same(0, #finalized.target_overlays, "InsertEnter overlay cleanup")
+check_same(nil, finalized.contexts.n.previous_landing, "InsertEnter landing cleanup")
+
+setup_feedback()
+vim.api.nvim_exec_autocmds("TextChanged", { buffer = 0 })
+finalized = clever_f.state():to_table()
+check_same(0, #finalized.target_overlays, "TextChanged overlay cleanup")
+check_same(nil, finalized.contexts.n.previous_landing, "TextChanged landing cleanup")
+
+for _, event in ipairs({ "WinEnter", "WinLeave", "CmdwinLeave" }) do
+ local before = setup_feedback()
+ vim.api.nvim_exec_autocmds(event, {})
+ local eager = clever_f.state():to_table()
+ check_same(0, #eager.target_overlays, event .. " overlay cleanup")
+ check_same(1, #eager.finalizers, event .. " finalizer retention")
+ check_same(
+ before.contexts.n.previous_landing.byte_column,
+ eager.contexts.n.previous_landing.byte_column,
+ event .. " landing retention"
+ )
+end
+
+setup_feedback(20)
+local timed_landing = clever_f.state():get_previous_landing("n")
+vim.wait(60)
+local timed = clever_f.state():to_table()
+check_same(0, #timed.target_overlays, "timer overlay cleanup")
+check_same(nil, timed.highlight_timer, "timer identity cleanup")
+check_same(timed_landing, clever_f.state():get_previous_landing("n"), "timer history retention")
+feed("f")
+local restored = clever_f.state():to_table()
+check_same(4, restored.contexts.n.previous_landing.byte_column, "repeat after timer cleanup")
+check_same(1, #restored.target_overlays, "repeat restores target overlay")
+check_truthy(restored.highlight_timer ~= nil, "repeat starts a replacement timer")
+
+clever_f._diagnostic_full_reset()
+io.stdout:write(string.format("Phase 16 plugin conformance: %d checks passed\n", checks))