local script = debug.getinfo(1, "S").source:sub(2) local root = script:match("^(.*)/tests/phase16%.lua$") or "." package.path = table.concat({ root .. "/lua/?.lua", root .. "/lua/?/init.lua", package.path, }, ";") local action_facade = require("clever_tee.action_facade") local domain = require("clever_tee.domain") local sequence_state = require("clever_tee.sequence_state") local state_transitions = require("clever_tee.state_transitions") local text_topology = require("clever_tee.text_topology") local MemoryHost = require("clever_tee.testing.memory_host") local assertions = dofile(root .. "/tests/assertions.lua") local same = assertions.same local truthy = assertions.truthy local falsy = assertions.falsy local tests = {} local passed = 0 local function test(name, body) tests[#tests + 1] = { name = name, body = body } end local function fresh_state() local transitions = state_transitions.new() transitions:ClearTemporaryOverlays() transitions:DiagnosticFullReset() return sequence_state.get() end local function motion_host(options) options.configuration = vim.tbl_extend("force", { mark_char = false, mark_cursor = false, mark_direct = false, show_prompt = false, }, options.configuration or {}) options.emit_movement_events = false local host = MemoryHost.new(options) return host, action_facade.new({ host = host }) end test("Public actions execute in Normal and every Visual context", function() local cases = { { mode = "n" }, { mode = "v", kind = domain.SelectionKind.CHARACTER }, { mode = "V", kind = domain.SelectionKind.LINE }, { mode = string.char(0x16), kind = domain.SelectionKind.BLOCK }, } for _, case in ipairs(cases) do local state = fresh_state() local origin = domain.Position.new(1, 1) local active_selection = case.kind and domain.Selection.active( case.kind, origin, origin, domain.SelectionOption.INCLUSIVE ) or nil local host, facade = motion_host({ buffer_lines = { "axaxa" }, cursor = origin, mode = case.mode, selection = active_selection, count = 2, input_packets = { { kind = "text", text = "x" } }, }) local outcome = facade:start_find_forward() same(domain.ActionKind.MOVEMENT, outcome.kind, case.mode) same(domain.Position.new(1, 4), outcome.position, case.mode) same(domain.Position.new(1, 4), host:read_cursor(), case.mode) same(domain.Position.new(1, 4), state:get_previous_landing(case.mode), case.mode) if case.kind ~= nil then local selected = host:read_selection() truthy(selected.active, case.mode) same(case.kind, selected.kind, case.mode) same(origin, selected.anchor, case.mode) same(domain.Position.new(1, 4), selected.focus, case.mode) end end end) test("Visual selection options apply only their specified endpoint adjustment", function() local modes = { { mode = "v", kind = domain.SelectionKind.CHARACTER, adjusted = true }, { mode = "V", kind = domain.SelectionKind.LINE, adjusted = true }, { mode = string.char(0x16), kind = domain.SelectionKind.BLOCK, adjusted = false }, } for _, option in ipairs({ domain.SelectionOption.INCLUSIVE, domain.SelectionOption.EXCLUSIVE, }) do for _, case in ipairs(modes) do fresh_state() local origin = domain.Position.new(1, 1) local host, facade = motion_host({ buffer_lines = { "axz" }, cursor = origin, mode = case.mode, selection = domain.Selection.active( case.kind, origin, origin, option ), input_packets = { { kind = "text", text = "x" } }, }) local outcome = facade:start_find_forward() local expected_column = option == domain.SelectionOption.EXCLUSIVE and case.adjusted and 3 or 2 same(domain.ActionKind.MOVEMENT, outcome.kind, case.mode .. option.value) same(expected_column, outcome.position.byte_column, case.mode .. option.value) same(option, host:read_selection().option, case.mode .. option.value) end end end) test("Public actions execute in every Select context", function() local cases = { { mode = "s", kind = domain.SelectionKind.CHARACTER }, { mode = "S", kind = domain.SelectionKind.LINE }, { mode = string.char(0x13), kind = domain.SelectionKind.BLOCK }, } for _, case in ipairs(cases) do local state = fresh_state() local origin = domain.Position.new(1, 1) local host, facade = motion_host({ buffer_lines = { "axaxa" }, cursor = origin, mode = case.mode, selection = domain.Selection.active( case.kind, origin, origin, domain.SelectionOption.INCLUSIVE ), count = 2, input_packets = { { kind = "text", text = "x" } }, }) local outcome = facade:start_find_forward() same(domain.ActionKind.MOVEMENT, outcome.kind, case.mode) same(domain.Position.new(1, 4), outcome.position, case.mode) same(domain.Position.new(1, 4), host:read_cursor(), case.mode) same(domain.Position.new(1, 4), state:get_previous_landing(case.mode), case.mode) same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor(case.mode), case.mode) end end) test("Public actions keep byte columns valid in every supported encoding", function() local multibyte = "\227\129\130" local cases = { { encoding = "utf-8", final_column = 5 }, { encoding = "cp932", final_column = 4 }, { encoding = "euc-jp", final_column = 4 }, } for _, case in ipairs(cases) do fresh_state() local host, facade = motion_host({ buffer_lines = { "a" .. multibyte .. "a" }, effective_encoding = case.encoding, cursor = { line = 1, byte_column = 1 }, input_packets = { { kind = "text", text = "a" } }, }) local forward = facade:start_find_forward() same(domain.ActionKind.MOVEMENT, forward.kind, case.encoding) same(case.final_column, forward.position.byte_column, case.encoding) truthy( text_topology.from_host(host):is_character_start(forward.position), case.encoding ) fresh_state() host, facade = motion_host({ buffer_lines = { "a" .. multibyte .. "a" }, effective_encoding = case.encoding, cursor = { line = 1, byte_column = case.final_column }, input_packets = { { kind = "text", text = "a" } }, }) local backward = facade:start_find_backward() same(domain.ActionKind.MOVEMENT, backward.kind, case.encoding) same(domain.Position.new(1, 1), backward.position, case.encoding) truthy( text_topology.from_host(host):is_character_start(backward.position), case.encoding ) end end) test("Input and matching faults release temporary presentation resources", function() local cases = { { message = "input failed", packets = { { kind = "error", message = "input failed" } }, }, { message = "matching failed", packets = { { kind = "text", text = "x" } }, target_factory = { build = function() error("matching failed", 0) end, }, }, } for _, case in ipairs(cases) do local state = fresh_state() local host = MemoryHost.new({ buffer_lines = { "ax" }, cursor = { line = 1, byte_column = 1 }, configuration = { mark_char = false, mark_cursor = true, mark_direct = true, show_prompt = true, }, input_packets = case.packets, emit_movement_events = false, }) local facade = action_facade.new({ host = host, target_factory = case.target_factory, }) local outcome = facade:start_find_forward() same(domain.ActionKind.ERROR, outcome.kind, case.message) same(case.message, outcome.diagnostic, case.message) same(0, #state.temporary_overlays, case.message) same(nil, next(host:highlights()), case.message) falsy(host:cursor_presentation().hidden, case.message) end end) test("Movement faults leave acquisition temporaries released", function() local state = fresh_state() local host, facade = motion_host({ buffer_lines = { "ax" }, cursor = { line = 1, byte_column = 1 }, configuration = { mark_cursor = true, mark_direct = true, }, input_packets = { { kind = "text", text = "x" } }, }) host.apply_cursor = function() error("movement failed", 0) end local ok, failure = pcall(function() facade:start_find_forward() end) falsy(ok) truthy(tostring(failure):find("movement failed", 1, true) ~= nil) same(0, #state.temporary_overlays) same(nil, next(host:highlights())) falsy(host:cursor_presentation().hidden) end) test("Overlay creation faults release earlier temporary overlays", function() local state = fresh_state() local host = MemoryHost.new({ buffer_lines = { "ax" }, cursor = { line = 1, byte_column = 1 }, configuration = { mark_char = false, mark_cursor = true, mark_direct = true, show_prompt = true, }, input_packets = { { kind = "text", text = "x" } }, emit_movement_events = false, }) local create = host.create_highlight local calls = 0 host.create_highlight = function(self, specification) calls = calls + 1 if calls == 2 then error("overlay failed", 0) end return create(self, specification) end local facade = action_facade.new({ host = host }) local outcome = facade:start_find_forward() same(domain.ActionKind.ERROR, outcome.kind) same("overlay failed", outcome.diagnostic) same(0, #state.temporary_overlays) same(nil, next(host:highlights())) falsy(host:cursor_presentation().hidden) end) test("Timer faults retain owned feedback and release acquisition temporaries", function() local state = fresh_state() local host = MemoryHost.new({ buffer_lines = { "ax" }, cursor = { line = 1, byte_column = 1 }, configuration = { mark_char = true, mark_cursor = true, mark_direct = true, highlight_timeout_ms = 10, }, input_packets = { { kind = "text", text = "x" } }, emit_movement_events = false, }) host.start_timer = function() error("timer failed", 0) end local facade = action_facade.new({ host = host }) local ok, failure = pcall(function() facade:start_find_forward() end) falsy(ok) truthy(tostring(failure):find("timer failed", 1, true) ~= nil) same(0, #state.temporary_overlays) same(nil, state.highlight_timer) same(1, #state.target_overlays) same(1, #state.finalizers) falsy(host:cursor_presentation().hidden) end) test("Event-registration faults roll back unowned persistent feedback", function() local state = fresh_state() local host = MemoryHost.new({ buffer_lines = { "ax" }, cursor = { line = 1, byte_column = 1 }, configuration = { mark_char = true, mark_cursor = true, mark_direct = true, }, input_packets = { { kind = "text", text = "x" } }, emit_movement_events = false, }) host.register_events = function() error("event registration failed", 0) end local facade = action_facade.new({ host = host }) local outcome = facade:start_find_forward() same(domain.ActionKind.ERROR, outcome.kind) same("event registration failed", outcome.diagnostic) same(0, #state.temporary_overlays) same(0, #state.target_overlays) same(0, #state.finalizers) same(nil, next(host:highlights())) falsy(host:cursor_presentation().hidden) end) test("Public actions share one record across operator-pending variants", function() local variants = { "no", "nov", "noV", "no" .. string.char(0x16), } for _, mode in ipairs(variants) do local state = fresh_state() local host, facade = motion_host({ buffer_lines = { "axaxa" }, cursor = { line = 1, byte_column = 1 }, mode = mode, pending_operator = "delete", input_packets = { { kind = "text", text = "x" } }, }) local outcome = facade:start_find_forward() same(domain.ActionKind.MOVEMENT, outcome.kind, mode) same("axa", host:read_text():line(1), mode) same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("no"), mode) same(domain.TargetValue.character("x", string.byte("x")), state:get_previous_target("no"), mode) end end) for _, item in ipairs(tests) do local ok, failure = xpcall(item.body, debug.traceback) if not ok then io.stderr:write("FAIL: " .. item.name .. "\n" .. tostring(failure) .. "\n") os.exit(1) end passed = passed + 1 end io.stdout:write(string.format("Phase 16 matrices: %d tests passed\n", passed))