local script = debug.getinfo(1, "S").source:sub(2) local root = script:match("^(.*)/tests/run%.lua$") or "." package.path = table.concat({ root .. "/lua/?.lua", root .. "/lua/?/init.lua", package.path, }, ";") local domain = require("clever_tee.domain") local action_facade = require("clever_tee.action_facade") local acquisition_service = require("clever_tee.acquisition_service") local capabilities = require("clever_tee.capabilities") local composition_root = require("clever_tee.composition_root") local destination_engine = require("clever_tee.destination_engine") local direct_preview_planner = require("clever_tee.direct_preview_planner") local feedback_service = require("clever_tee.feedback_service") local host_adapter = require("clever_tee.host_adapter") local case_policy = require("clever_tee.case_policy") local policy = require("clever_tee.policy") local repeat_resolver = require("clever_tee.repeat_resolver") local migemo_catalog = require("clever_tee.migemo_catalog") local motion_plan = require("clever_tee.motion_plan") local motion_executor = require("clever_tee.motion_executor") local sequence_coordinator = require("clever_tee.sequence_coordinator") 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 target_plan = require("clever_tee.target_plan") 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 fails = assertions.fails local list_same = assertions.list_same local tests = {} local passed = 0 local function test(name, body) tests[#tests + 1] = { name = name, body = body } end test("Position validates one-based byte coordinates", function() local position = domain.Position.new(1, 1) same(1, position.line) same(1, position.byte_column) same("Position", domain.type_of(position)) fails(function() domain.Position.new(0, 1) end, "line") fails(function() domain.Position.new(1, 0) end, "byte_column") fails(function() domain.Position.new(1.5, 1) end, "line") fails(function() position.line = 2 end, "immutable") end) test("Position comparison is lexicographic and detects stationarity", function() local first = domain.Position.new(1, 9) local later_column = domain.Position.new(1, 10) local later_line = domain.Position.new(2, 1) local same_place = domain.Position.new(1, 9) same(-1, domain.Position.compare(first, later_column)) same(-1, domain.Position.compare(later_column, later_line)) same(1, domain.Position.compare(later_line, first)) truthy(domain.Position.is_forward(later_line, first)) truthy(domain.Position.is_backward(first, later_line)) truthy(domain.Position.stationary(first, same_place)) falsy(domain.Position.is_forward(first, same_place)) falsy(domain.Position.is_backward(first, same_place)) truthy(first == same_place) local serialized = first:to_table() same(2, (function() local count = 0 for _ in pairs(serialized) do count = count + 1 end return count end)()) end) test("Descriptor conversion defines all families and directions", function() local cases = { { "f", domain.Family.FIND, domain.Direction.FORWARD, false }, { "F", domain.Family.FIND, domain.Direction.BACKWARD, true }, { "t", domain.Family.TILL, domain.Direction.FORWARD, false }, { "T", domain.Family.TILL, domain.Direction.BACKWARD, true }, } for _, case in ipairs(cases) do local descriptor = domain.Descriptor.from_string(case[1]) same(case[1], domain.Descriptor.to_string(descriptor)) same(case[2], descriptor.family) same(case[3], descriptor.direction) same(case[4], domain.Descriptor.is_uppercase(descriptor)) same(descriptor, domain.Descriptor.from_parts(case[2], case[3])) same(descriptor, domain.Descriptor.swap(domain.Descriptor.swap(descriptor))) end same(domain.Descriptor.FIND_BACKWARD, domain.Descriptor.swap("f")) same(domain.Descriptor.TILL_FORWARD, domain.Descriptor.swap("T")) same(domain.Descriptor.FIND_FORWARD, domain.Descriptor.lowercase("F")) same(domain.Descriptor.TILL_BACKWARD, domain.Descriptor.uppercase("t")) falsy(domain.Descriptor.is_valid("x")) fails(function() domain.Descriptor.from_string("x") end, "f, F, t, or T") end) test("Count normalizes absent input and rejects nonpositive values", function() same(domain.Count.ONE, domain.Count.new(nil)) same(1, domain.Count.to_number(nil)) same(3, domain.Count.to_number(domain.Count.new(3))) fails(function() domain.Count.new(0) end, "positive") fails(function() domain.Count.new(-1) end, "positive") fails(function() domain.Count.new(1.5) end, "positive") end) test("ModeContext normalizes every operator-pending variant", function() local operator = domain.ModeContext.from_full_mode("no") local variants = { "no", "nov", "noV", "no" .. string.char(0x16), } for _, mode in ipairs(variants) do local context = domain.ModeContext.from_full_mode(mode) same(operator, context) same("no", context.key) truthy(context.operator) truthy(context.command_path) end end) test("ModeContext preserves other full strings and exposes mode traits", function() local normal = domain.ModeContext.from_full_mode("n") local insert_normal = domain.ModeContext.from_full_mode("niI") local other_full_mode = domain.ModeContext.from_full_mode("normal-extension") local visual_character = domain.ModeContext.from_full_mode("v") local visual_line = domain.ModeContext.from_full_mode("V") local visual_block = domain.ModeContext.from_full_mode(string.char(0x16)) local visual_from_select = domain.ModeContext.from_full_mode("vs") local select_character = domain.ModeContext.from_full_mode("s") local select_line = domain.ModeContext.from_full_mode("S") local select_block = domain.ModeContext.from_full_mode(string.char(0x13)) same("n", normal.key) same("niI", insert_normal.key) same("normal-extension", other_full_mode.key) falsy(other_full_mode.operator) falsy(normal == insert_normal) same(domain.SelectionKind.CHARACTER, visual_character.visual_kind) same(domain.SelectionKind.LINE, visual_line.visual_kind) same(domain.SelectionKind.BLOCK, visual_block.visual_kind) same(domain.SelectionKind.CHARACTER, visual_from_select.visual_kind) falsy(visual_character.command_path) same(domain.SelectionKind.CHARACTER, select_character.select_kind) same(domain.SelectionKind.LINE, select_line.select_kind) same(domain.SelectionKind.BLOCK, select_block.select_kind) truthy(select_character.command_path) falsy(select_character.visual) truthy(select_character.select) end) test("Selection and text snapshots preserve semantic input values", function() local anchor = domain.Position.new(1, 1) local focus = domain.Position.new(1, 3) local selection = domain.Selection.active( domain.SelectionKind.CHARACTER, anchor, focus, domain.SelectionOption.EXCLUSIVE ) truthy(selection.active) same(domain.SelectionKind.CHARACTER, selection.kind) same(domain.SelectionOption.EXCLUSIVE, selection.option) same(domain.Position.new(1, 4), selection:with_focus(domain.Position.new(1, 4)).focus) local inactive = domain.Selection.inactive() falsy(inactive.active) same(domain.SelectionKind.NONE, inactive.kind) local text = domain.TextSnapshot.new({ "alpha", "beta" }) same(2, text.line_count) same("alpha", text:line(1)) local lines = text:lines() lines[1] = "changed" same("alpha", text:line(1)) end) test("TargetValue represents characters, special keys, and code fallback", function() local character = domain.TargetValue.character("a", 97) local special = domain.TargetValue.special_key(string.char(0x80, 0xfd, 0x01)) local fallback = domain.TargetValue.code_fallback() same(domain.TargetKind.CHARACTER, character.kind) same("a", character.value) same(97, character.first_code) same(domain.TargetKind.SPECIAL_KEY, special.kind) same(0x80, special.first_code) same(domain.TargetKind.CODE_FALLBACK, fallback.kind) same(0, fallback.first_code) same("", fallback.value) fails(function() domain.TargetValue.special_key("x") end, "hexadecimal 80") fails(function() character.value = "b" end, "immutable") end) test("InputPacket carries every input capability variant", function() local text = domain.InputPacket.text("a") local bytes = domain.InputPacket.raw_bytes({ 0x80, 0xfd, 0x60 }) local special = domain.InputPacket.special_key("Left", string.char(0x80, 1)) local special_from_table = domain.InputPacket.from_table({ kind = "special_key", name = "Escape", bytes = { 27 }, }) local failure = domain.InputPacket.error("input failed") same(domain.InputPacketKind.TEXT, text.kind) list_same({ 0x80, 0xfd, 0x60 }, bytes:bytes()) same("Left", special.name) list_same({ 0x80, 1 }, special:bytes()) same(string.char(27), special_from_table.encoded) list_same({ 27 }, special_from_table:bytes()) list_same({ 27 }, special_from_table:to_table().bytes) same("input failed", failure.message) end) test("Plans and requests carry typed immutable motion data", function() local target = domain.TargetValue.character("h", 104) local target_plan = domain.TargetPlan.new({ target = target, kind = domain.TargetPlanKind.LITERAL, case_mode = domain.CaseMode.SENSITIVE, matcher = function(character) return character == "h" end, }) truthy(target_plan:matches("h")) falsy(target_plan:matches("H")) local motion_plan = domain.ResolvedMotionPlan.new({ target_plan = target_plan, descriptor = "f", search_scope = domain.SearchScope.BUFFER, endpoint_policy = domain.EndpointPolicy.REGULAR, }) same(domain.Descriptor.FIND_FORWARD, motion_plan.descriptor) same(target_plan, motion_plan.target_plan) local policy = { search_current_line_only = false } local request = domain.MotionRequest.new({ context = domain.ModeContext.from_full_mode("n"), origin = domain.Position.new(1, 1), descriptor = "f", target = target, count = nil, policy = policy, first_move = true, }) same(1, request.count.value) truthy(request.first_move) same(policy, request.policy) fails(function() motion_plan.descriptor = domain.Descriptor.FIND_BACKWARD end, "immutable") end) test("SearchOutcome distinguishes complete, partial, and first-step failure", function() local origin = domain.Position.new(1, 1) local reached = domain.Position.new(1, 5) local complete = domain.SearchOutcome.complete(reached, 3) local partial = domain.SearchOutcome.boundary_after_partial(reached, 2) local before = domain.SearchOutcome.boundary_before_any(origin) truthy(complete.complete) same(domain.SearchStatus.COMPLETE, complete.status) falsy(partial.complete) same(domain.SearchStatus.BOUNDARY_AFTER_PARTIAL, partial.status) same(2, partial.successful_steps) same(origin, before.endpoint) same(0, before.successful_steps) fails(function() domain.SearchOutcome.complete(reached, 0) end, "successful step") end) test("ActionOutcome and DotPayload retain resolved result data", function() local target = domain.TargetValue.character("e", 101) local payload = domain.DotPayload.new("t", target) local destination = domain.Position.new(1, 4) local complete = domain.SearchOutcome.complete(destination, 1) local movement = domain.ActionOutcome.from_search(complete, "t", payload) same(domain.ActionKind.MOVEMENT, movement.kind) same(destination, movement.position) truthy(movement.complete) same(1, movement.successful_steps) same(domain.Descriptor.TILL_FORWARD, movement.effective_descriptor) same(payload, movement.dot_payload) same("t", payload:to_table().descriptor) local failed = domain.ActionOutcome.from_search( domain.SearchOutcome.boundary_before_any(destination), "T" ) same(domain.ActionKind.FAILED_SEARCH, failed.kind) same(false, failed.complete) same(domain.ActionKind.NEUTRAL, domain.ActionOutcome.neutral(destination).kind) same(domain.ActionKind.ESCAPE, domain.ActionOutcome.escape(destination).kind) same(domain.ActionKind.EMPTY, domain.ActionOutcome.empty(destination).kind) same("problem", domain.ActionOutcome.error(destination, "problem").diagnostic) end) test("Host adapter connects motion endpoints and dot payloads", function() local cursor local commands = {} local runtime = { api = { nvim_win_set_cursor = function(window, position) same(0, window) cursor = position end, nvim_cmd = function(command, options) commands[#commands + 1] = command truthy(vim.tbl_isempty(options)) end, }, v = { operator = "d" }, } local adapter = host_adapter.new({ runtime = runtime }) same("d", adapter:read_pending_operator()) adapter:apply_cursor(domain.Position.new(3, 5), { descriptor = domain.Descriptor.FIND_FORWARD, }) list_same({ 3, 4 }, cursor) adapter:apply_selection(domain.Selection.active( domain.SelectionKind.CHARACTER, domain.Position.new(3, 1), domain.Position.new(4, 2), domain.SelectionOption.INCLUSIVE )) list_same({ 4, 1 }, cursor) adapter:set_operator_inclusive(false) same(0, #commands) adapter:set_operator_inclusive(true) same(1, #commands) same("normal", commands[1].cmd) truthy(commands[1].bang) list_same({ "v" }, commands[1].args) local payload = domain.DotPayload.new( "t", domain.TargetValue.character("x", string.byte("x")) ) local replayed_payload local replayed_count same(payload, adapter:register_dot_repeat(payload, function(value, count) replayed_payload = value replayed_count = count return "replayed" end)) same(payload, adapter:dot_repeat_payload()) same("replayed", adapter:replay_dot(3)) same(payload, replayed_payload) same(3, replayed_count.value) end) test("Host adapter bridges native dot to resolved payload replay", function() local mappings = {} local deleted = {} local restored local ownership_autocmd local fed local runtime = { api = { nvim_create_augroup = function() return 9 end, nvim_create_autocmd = function(_, options) ownership_autocmd = options.callback return 12 end, nvim_feedkeys = function(keys, mode, escape_csi) fed = { keys = keys, mode = mode, escape_csi = escape_csi } end, }, fn = { exists = function(name) same("##CmdAtom", name) return 1 end, maparg = function(lhs, mode, abbreviation, dictionary) same(".", lhs) same("n", mode) falsy(abbreviation) truthy(dictionary) return { lhs = ".", rhs = "prior-dot" } end, mapset = function(mode, abbreviation, mapping) restored = { mode = mode, abbreviation = abbreviation, mapping = mapping } end, }, keymap = { set = function(mode, lhs, callback, options) mappings[mode .. "\0" .. lhs] = { callback = callback, options = options, } end, del = function(mode, lhs) deleted[#deleted + 1] = mode .. "\0" .. lhs mappings[mode .. "\0" .. lhs] = nil end, }, keycode = function(keys) return keys end, v = { operator = "d", count = 2, count1 = 2 }, } local adapter = host_adapter.new({ runtime = runtime }) local payload = domain.DotPayload.new( "f", domain.TargetValue.character("e", string.byte("e")) ) local replayed adapter:register_dot_repeat(payload, function(value, count) replayed = { payload = value, count = count } return domain.ActionOutcome.neutral(domain.Position.new(1, 1)) end) local dot = mappings["n\0."] local motion = mappings["o\0(clever-tee-dot-motion)"] truthy(dot ~= nil) truthy(motion ~= nil) dot.callback() same("2d(clever-tee-dot-motion)", fed.keys) same("n", fed.mode) falsy(fed.escape_csi) motion.callback() same(payload, replayed.payload) same(2, replayed.count.value) ownership_autocmd({ data = { changed = true } }) truthy(mappings["n\0."] ~= nil) ownership_autocmd({ data = { changed = true } }) same(nil, mappings["n\0."]) list_same({ "n\0." }, deleted) same("n", restored.mode) falsy(restored.abbreviation) same("prior-dot", restored.mapping.rhs) end) test("Host adapter reads encoding and uses Nvim case conversion", function() local lowered = {} local runtime = { api = { nvim_get_option_value = function(name, options) same("encoding", name) same("global", options.scope) return "utf-8" end, }, fn = { tolower = function(value) lowered[#lowered + 1] = value return string.lower(value) end, }, } local adapter = host_adapter.new({ runtime = runtime }) same("utf-8", adapter:read_encoding()) same("abc", adapter:lowercase("AbC")) list_same({ "AbC" }, lowered) end) test("Host adapter restores exact cursor presentation values", function() local options = { guicursor = "n:block,i:ver37-blinkon123", t_ve = "terminal-visible-sequence", } local writes = {} local runtime = { api = { nvim_get_option_value = function(name, request) same("global", request.scope) return options[name] end, nvim_set_option_value = function(name, value, request) same("global", request.scope) options[name] = value writes[#writes + 1] = { name = name, value = value } end, nvim_cmd = function(command) same("let", command.cmd) same("&t_ve", command.args[1]) same("=", command.args[2]) options.t_ve = command.args[3] writes[#writes + 1] = { name = "t_ve", value = command.args[3] } end, }, fn = { exists = function(name) same("+t_ve", name) return 1 end, eval = function(name) same("&t_ve", name) return options.t_ve end, string = function(value) return value end, }, } local adapter = host_adapter.new({ runtime = runtime }) truthy(adapter:supports_cursor_presentation()) local lease = adapter:suppress_cursor_presentation() truthy(lease ~= nil) same("a:ver1", options.guicursor) same("", options.t_ve) truthy(adapter:restore_cursor_presentation(lease)) same("n:block,i:ver37-blinkon123", options.guicursor) same("terminal-visible-sequence", options.t_ve) falsy(adapter:restore_cursor_presentation(lease)) same(4, #writes) end) test("Host adapter owns Nvim timers and event registrations", function() local timer_callbacks = {} local stopped_timers = {} local autocmds = {} local deleted_autocmds = {} local runtime = { api = { nvim_create_augroup = function(name, options) same("clever_tee", name) truthy(options.clear) return 7 end, nvim_create_autocmd = function(names, options) local id = #autocmds + 20 autocmds[#autocmds + 1] = { id = id, names = names, options = options, } return id end, nvim_del_autocmd = function(id) deleted_autocmds[#deleted_autocmds + 1] = id end, nvim_get_current_win = function() return 42 end, }, fn = { timer_start = function(delay, callback) local id = #timer_callbacks + 1 timer_callbacks[id] = { delay = delay, callback = callback } return id end, timer_stop = function(id) stopped_timers[#stopped_timers + 1] = id end, }, } local adapter = host_adapter.new({ runtime = runtime }) truthy(adapter:supports_timers()) local fired local first_timer = adapter:start_timer(25, function(identity) fired = identity end) same(25, timer_callbacks[1].delay) timer_callbacks[1].callback(1) same(first_timer, fired) falsy(adapter:stop_timer(first_timer)) local second_timer = adapter:start_timer(40, function() end) truthy(adapter:stop_timer(second_timer)) falsy(adapter:stop_timer(second_timer)) list_same({ 2 }, stopped_timers) local deliveries = {} local registration = adapter:register_events( { "CursorMoved", "TextChanged" }, function(name, payload) deliveries[#deliveries + 1] = { name = name, buffer = payload.buffer, window = payload.window, } end, { buffer = 3 } ) list_same({ "CursorMoved", "TextChanged" }, autocmds[1].names) same(3, autocmds[1].options.buffer) same(7, autocmds[1].options.group) local transition = adapter:begin_action_transition() autocmds[1].options.callback({ event = "CursorMoved", buf = 3, file = "file", match = "file", }) same(0, #deliveries) adapter:commit_action_transition(transition) same(1, #deliveries) same("CursorMoved", deliveries[1].name) same(3, deliveries[1].buffer) same(42, deliveries[1].window) adapter:deliver_event("TextChanged", { buffer = 3, window = 51 }) same(2, #deliveries) same("TextChanged", deliveries[2].name) same(51, deliveries[2].window) truthy(adapter:remove_event_registration(registration)) falsy(adapter:remove_event_registration(registration)) list_same({ 20 }, deleted_autocmds) end) test("Host adapter materializes window-local highlight resources", function() local groups = { Existing = { fg = 7 } } local added = {} local deleted = {} local next_match = 1000 local runtime = { api = { nvim_get_hl = function(_, options) return vim.deepcopy(groups[options.name] or {}) end, nvim_set_hl = function(_, name, definition) groups[name] = vim.deepcopy(definition) end, }, fn = { hlexists = function(name) return groups[name] ~= nil and 1 or 0 end, matchaddpos = function(group, positions, priority, id, options) local match_id = next_match next_match = next_match + 1 added[#added + 1] = { group = group, positions = positions, priority = priority, id = id, window = options.window, match_id = match_id, } return match_id end, matchdelete = function(match_id, window) deleted[#deleted + 1] = { match_id = match_id, window = window } return 0 end, }, } local adapter = host_adapter.new({ runtime = runtime }) same(7, adapter:read_highlight_group("Existing").fg) same(nil, adapter:read_highlight_group("Missing")) falsy(adapter:define_highlight_group( "Existing", { guifg = "red" }, { default = true } )) truthy(adapter:define_highlight_group("CleverTeeDefaultLabel", { guifg = "red", guibg = "NONE", gui = { bold = true, underline = true }, ctermfg = "red", ctermbg = "NONE", cterm = { bold = true, underline = true }, }, { default = true })) same("red", groups.CleverTeeDefaultLabel.fg) same("NONE", groups.CleverTeeDefaultLabel.bg) truthy(groups.CleverTeeDefaultLabel.bold) truthy(groups.CleverTeeDefaultLabel.underline) truthy(groups.CleverTeeDefaultLabel.default) local cursor = adapter:create_highlight({ group = "CleverTeeCursor", window = 42, position = domain.Position.new(3, 5), priority = "high", }) local character = adapter:create_highlight({ group = "CleverTeeChar", window = 51, positions = {}, priority = "ordinary", }) same(2, #added) list_same({ 3, 5 }, added[1].positions[1]) same(100, added[1].priority) same(42, added[1].window) list_same({ 0 }, added[2].positions[1]) same(10, added[2].priority) same(51, added[2].window) truthy(adapter:remove_highlight(cursor)) truthy(adapter:remove_highlight(character)) falsy(adapter:remove_highlight(character)) same(2, #deleted) same(1000, deleted[1].match_id) same(42, deleted[1].window) same(1001, deleted[2].match_id) same(51, deleted[2].window) end) test("Host adapter translates typed action outcomes to Nvim effects", function() local effects = {} local runtime = { api = { nvim_feedkeys = function(keys, mode, escape_csi) effects[#effects + 1] = { kind = "feedkeys", keys = keys, mode = mode, escape_csi = escape_csi, } end, }, keycode = function(key) same("", key) return string.char(27) end, log = { levels = { ERROR = 4 } }, notify = function(text, level, options) effects[#effects + 1] = { kind = "diagnostic", text = text, level = level, title = options.title, } end, } local adapter = host_adapter.new({ runtime = runtime }) local position = domain.Position.new(1, 1) local complete = domain.SearchOutcome.complete( domain.Position.new(1, 2), 1 ) local failed = domain.SearchOutcome.boundary_before_any(position) local no_effect = { domain.ActionOutcome.neutral(position), domain.ActionOutcome.empty(position), domain.ActionOutcome.from_search(complete, "f"), domain.ActionOutcome.from_search(failed, "f"), } for _, outcome in ipairs(no_effect) do same( host_adapter.ActionEffect.NONE, adapter:translate_action_outcome(outcome) ) end same(0, #effects) same( host_adapter.ActionEffect.ESCAPE, adapter:translate_action_outcome(domain.ActionOutcome.escape(position)) ) same("feedkeys", effects[1].kind) same(string.char(27), effects[1].keys) same("n", effects[1].mode) falsy(effects[1].escape_csi) same( host_adapter.ActionEffect.ERROR, adapter:translate_action_outcome( domain.ActionOutcome.error(position, "adapter error") ) ) same("diagnostic", effects[2].kind) same("adapter error", effects[2].text) same(4, effects[2].level) same("clever-tee", effects[2].title) end) test("Capability contract reports every semantic method", function() local host = MemoryHost.new() same(host, capabilities.assert_implements(host)) same(0, #capabilities.missing_methods(host)) local required = capabilities.required_methods() truthy(#required >= 30) truthy(vim.tbl_contains(required, "read_window")) local incomplete = {} local missing = capabilities.missing_methods(incomplete) truthy(#missing == #required) fails(function() capabilities.assert_implements(incomplete) end, "semantic capabilities") end) test("MemoryHost supplies all semantic reads with domain values", function() local host = MemoryHost.new({ buffer_lines = { "alpha", "beta" }, cursor = { line = 2, byte_column = 2 }, mode = "nov", selection = { active = false, kind = "none", anchor = nil, focus = nil, option = "exclusive", }, count = 3, configuration = { enabled = false, triggers = { "x", "y" }, }, effective_encoding = "cp932", macro_register = "q", fold_open_policy = { "horizontal" }, closed_fold_levels = 2, pending_operator = "delete", time_values_ms = { 10.5, 12 }, }) same("alpha", host:read_text():line(1)) same(domain.Position.new(2, 2), host:read_cursor()) same("nov", host:read_mode()) same("no", host:read_mode_context().key) same("delete", host:read_pending_operator()) same(domain.SelectionOption.EXCLUSIVE, host:read_selection().option) same(3, host:read_count().value) truthy(host:configuration_present("enabled")) same(false, host:read_configuration("enabled")) local triggers = host:read_configuration("triggers") triggers[1] = "changed" same("x", host:read_configuration("triggers")[1]) same("cp932", host:read_encoding()) truthy(host:read_macro_state().executing) truthy(host:read_fold_state():opens("horizontal")) same(2, host:read_fold_state().closed_levels) same(10.5, host:read_time_ms()) same(12, host:read_time_ms()) end) test("MemoryHost queues movement events until action state commits", function() local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor = { line = 1, byte_column = 1 }, }) local state = { committed = false } local observations = {} host:register_events("CursorMoved", function(name, payload) observations[#observations + 1] = { name = name, position = payload.cursor, committed = state.committed, } end) local token = host:begin_action_transition() host:apply_cursor(domain.Position.new(1, 2)) same(1, host:pending_event_count()) same(0, #observations) state.committed = true host:commit_action_transition(token) same(0, host:pending_event_count()) same(1, #observations) same("CursorMoved", observations[1].name) same(domain.Position.new(1, 2), observations[1].position) truthy(observations[1].committed) end) test("MemoryHost action invocation flushes events after the action callback", function() local host = MemoryHost.new({ buffer_lines = { "abc" } }) local state = { landing = nil } local observed_landing host:register_events("CursorMoved", function() observed_landing = state.landing end) host:register_action("Move", function() local destination = domain.Position.new(1, 3) host:apply_cursor(destination) state.landing = destination return "moved" end) same("moved", host:invoke_action("Move")) same(domain.Position.new(1, 3), observed_landing) end) test("MemoryHost models movement, input, folds, and visible effects", function() local host = MemoryHost.new({ buffer_lines = { "abcd" }, cursor = { line = 1, byte_column = 1 }, selection = { active = true, kind = "character", anchor = { line = 1, byte_column = 1 }, focus = { line = 1, byte_column = 1 }, option = "inclusive", }, input_packets = { { kind = "text", text = "d" }, { kind = "raw_bytes", bytes = { 0x80, 0xfd, 0x60 } }, { kind = "error", message = "read failed" }, }, fold_open_policy = { "all" }, closed_fold_levels = 2, }) host:apply_selection(domain.Position.new(1, 2), domain.SelectionKind.CHARACTER) same(domain.Position.new(1, 2), host:read_cursor()) same(domain.Position.new(1, 2), host:read_selection().focus) host:set_operator_inclusive(true) truthy(host:operator_inclusive()) same("d", host:read_input().text) list_same({ 0x80, 0xfd, 0x60 }, host:read_input():bytes()) fails(function() host:read_input() end, "read failed") truthy(host:open_fold()) truthy(host:open_fold()) falsy(host:open_fold()) same(0, host:read_fold_state().closed_levels) host:show_prompt("clever-tee: ") host:redraw("screen") host:redraw("full") host:emit_diagnostic("error", "problem") same("clever-tee: ", host:prompts()[1]) list_same({ "screen", "full" }, host:redraws()) same("problem", host:diagnostics()[1].text) end) test("MemoryHost manages highlight and cursor-presentation resources", function() local presentation = { guicursor = "n-v:block", terminal_cursor_visible = true, hidden = false, } local host = MemoryHost.new({ cursor_presentation = presentation }) local highlight = host:create_highlight({ group = "CleverTeeChar", identity = "char-1", position = domain.Position.new(1, 1), priority = "high", }) same("char-1", highlight) truthy(host:highlights()[highlight] ~= nil) truthy(host:remove_highlight(highlight)) falsy(host:remove_highlight(highlight)) local lease = host:suppress_cursor_presentation() truthy(host:cursor_presentation().hidden) truthy(host:restore_cursor_presentation(lease)) local restored = host:cursor_presentation() same(presentation.guicursor, restored.guicursor) same(presentation.terminal_cursor_visible, restored.terminal_cursor_visible) same(presentation.hidden, restored.hidden) end) test("MemoryHost manages timers, events, mappings, and dot repeat", function() local host = MemoryHost.new() local fired truthy(host:supports_timers()) local timer = host:start_timer(25, function(identity) fired = identity end) truthy(host:timers()[timer].active) truthy(host:fire_timer(timer)) same(timer, fired) falsy(host:timers()[timer].active) falsy(host:stop_timer(timer)) local delivered local registration = host:register_events({ "InsertEnter", "TextChanged" }, function(name) delivered = name end, { buffer = "buffer-1" }) host:deliver_event("InsertEnter", {}) same("InsertEnter", delivered) truthy(host:remove_event_registration(registration)) delivered = nil host:deliver_event("TextChanged", {}) same(nil, delivered) host:register_action("Neutral", function() return domain.ActionOutcome.neutral(domain.Position.new(1, 1)) end) local mapping = host:register_mapping( { "n", "x", "o" }, "f", "Neutral", { silent = true, remap = false } ) same("f", host:mappings()[mapping].lhs) truthy(host:mappings()[mapping].options.silent) local target = domain.TargetValue.character("a", 97) local payload = domain.DotPayload.new("f", target) host:register_dot_repeat(payload, function(replayed, count) return replayed, count end) same(payload, host:dot_repeat_payload()) local replayed, count = host:replay_dot(2) same(payload, replayed) same(2, count.value) end) test("Policy schema contains every default and color target", function() local defaults = policy.defaults() local false_settings = { "search_current_line_only", "ignore_case", "smart_case", "use_migemo", "fix_key_direction", "show_prompt", "mark_direct", } local true_settings = { "mark_cursor", "hide_cursor_on_cmdline", "mark_char", "clean_labels_eagerly", } for _, name in ipairs(false_settings) do same(false, defaults[name], name) end for _, name in ipairs(true_settings) do same(true, defaults[name], name) end same("", defaults.chars_match_any_signs) same(0, defaults.repeat_timeout_ms) same(0, defaults.highlight_timeout_ms) list_same({ "\r" }, defaults.repeat_last_char_inputs) same(nil, policy.default("mark_cursor_color")) same(nil, policy.default("mark_char_color")) same(nil, policy.default("mark_direct_color")) local schema = policy.schema() same("Cursor", schema.mark_cursor_color.default_target) same("CleverTeeDefaultLabel", schema.mark_char_color.default_target) same("CleverTeeDefaultLabel", schema.mark_direct_color.default_target) same(policy.ValueType.OPTIONAL_GROUP_NAME, schema.mark_cursor_color.value_type) same(policy.ValueType.PRESENCE, schema[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL].value_type) defaults.repeat_last_char_inputs[1] = "changed" schema.repeat_last_char_inputs.default[1] = "changed" list_same({ "\r" }, policy.default("repeat_last_char_inputs")) end) test("Policy typed accessors validate semantic values", function() local host = MemoryHost.new({ configuration = { search_current_line_only = true, chars_match_any_signs = ";:", repeat_last_char_inputs = { "x", "" }, mark_cursor_color = "IncSearch", repeat_timeout_ms = 25, }, }) local service = policy.new(host) truthy(service:get_boolean("search_current_line_only")) same(";:", service:get_string("chars_match_any_signs")) list_same({ "x", "" }, service:get_string_list("repeat_last_char_inputs")) same("IncSearch", service:get_optional_group_name("mark_cursor_color")) same(25, service:get_nonnegative_integer("repeat_timeout_ms")) same(nil, service:get_optional_group_name("mark_char_color")) local inputs = service:get_string_list("repeat_last_char_inputs") inputs[1] = "changed" same("x", service:get_string_list("repeat_last_char_inputs")[1]) host:set_configuration("ignore_case", 1) fails(function() service:get_boolean("ignore_case") end, "must be a Boolean") host:set_configuration("chars_match_any_signs", {}) fails(function() service:get_string("chars_match_any_signs") end, "must be a string") host:set_configuration("repeat_last_char_inputs", { "x", 2 }) fails(function() service:get_string_list("repeat_last_char_inputs") end, "list of strings") host:set_configuration("mark_cursor_color", "") fails(function() service:get_optional_group_name("mark_cursor_color") end, "optional group name") host:set_configuration("repeat_timeout_ms", -1) fails(function() service:get_nonnegative_integer("repeat_timeout_ms") end, "nonnegative integer") host:set_configuration("repeat_timeout_ms", 1.5) fails(function() service:get_nonnegative_integer("repeat_timeout_ms") end, "nonnegative integer") fails(function() service:get_string("ignore_case") end, "does not have type string") fails(function() service:get("unknown") end, "unknown policy setting") end) test("Default-map suppression is presence-based and activation values are retained", function() local absent_host = MemoryHost.new() local absent_policy = policy.new(absent_host) falsy(absent_policy:default_maps_suppressed()) truthy(absent_policy:capture_activation().install_default_mappings) for _, sentinel_value in ipairs({ false, 0 }) do local configuration = { clean_labels_eagerly = false, } configuration[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL] = sentinel_value local host = MemoryHost.new({ configuration = configuration }) local service = policy.new(host) host:clear_operations() truthy(service:default_maps_suppressed()) local operations = host:operations() same(1, #operations) same("configuration_present", operations[1].operation) local activation = service:capture_activation() falsy(activation.install_default_mappings) falsy(activation.clean_labels_eagerly) host:unset_configuration(policy.DEFAULT_MAP_SUPPRESSION_SENTINEL) host:set_configuration("clean_labels_eagerly", true) local retained = service:capture_activation() falsy(retained.install_default_mappings) falsy(retained.clean_labels_eagerly) local next_activation = policy.new(host):capture_activation() truthy(next_activation.install_default_mappings) truthy(next_activation.clean_labels_eagerly) end end) test("Runtime policy samples read current behavior values", function() local host = MemoryHost.new() local service = policy.new(host) local target = domain.TargetValue.character("a", 97) same(domain.SearchScope.BUFFER, service:sample_search().search_scope) same(domain.CaseMode.SENSITIVE, service:sample_match(target).case_mode) falsy(service:sample_direction().fix_key_direction) falsy(service:sample_acquisition().show_prompt) truthy(service:sample_markers().mark_char) same(0, service:sample_timeouts().repeat_timeout_ms) list_same({ "\r" }, service:sample_previous_input().repeat_last_char_inputs) host:set_configuration("search_current_line_only", true) host:set_configuration("ignore_case", true) host:set_configuration("smart_case", true) host:set_configuration("use_migemo", true) host:set_configuration("chars_match_any_signs", ";") host:set_configuration("fix_key_direction", true) host:set_configuration("show_prompt", true) host:set_configuration("mark_cursor", false) host:set_configuration("hide_cursor_on_cmdline", false) host:set_configuration("mark_char", false) host:set_configuration("mark_direct", true) host:set_configuration("repeat_timeout_ms", 75) host:set_configuration("highlight_timeout_ms", 125) host:set_configuration("repeat_last_char_inputs", { "x", "yz" }) local search = service:sample_search() truthy(search.search_current_line_only) same(domain.SearchScope.CURRENT_LINE, search.search_scope) local match = service:sample_match(target) truthy(match.ignore_case) truthy(match.smart_case) truthy(match.use_migemo) same(";", match.chars_match_any_signs) same(domain.CaseMode.INSENSITIVE, match.case_mode) truthy(service:sample_direction().fix_key_direction) local acquisition = service:sample_acquisition() truthy(acquisition.show_prompt) falsy(acquisition.mark_cursor) falsy(acquisition.hide_cursor_on_cmdline) truthy(acquisition.mark_direct) local markers = service:sample_markers() falsy(markers.mark_cursor) falsy(markers.mark_char) truthy(markers.mark_direct) local timeouts = service:sample_timeouts() same(75, timeouts.repeat_timeout_ms) same(125, timeouts.highlight_timeout_ms) list_same({ "x", "yz" }, service:sample_previous_input().repeat_last_char_inputs) end) test("Highlight policy is reevaluated with live feature and color values", function() local host = MemoryHost.new() local service = policy.new(host) service:capture_activation() local initial = service:evaluate_highlight_links() truthy(initial.CleverTeeCursor.enabled) same(nil, initial.CleverTeeCursor.configured_target) same("Cursor", initial.CleverTeeCursor.target) truthy(initial.CleverTeeChar.enabled) same("CleverTeeDefaultLabel", initial.CleverTeeChar.target) falsy(initial.CleverTeeDirect.enabled) same("CleverTeeDefaultLabel", initial.CleverTeeDirect.target) host:set_configuration("mark_cursor", false) host:set_configuration("mark_cursor_color", "Search") host:set_configuration("mark_char", false) host:set_configuration("mark_char_color", "ErrorMsg") host:set_configuration("mark_direct", true) host:set_configuration("mark_direct_color", "IncSearch") local refreshed = service:evaluate_highlight_links() falsy(refreshed.CleverTeeCursor.enabled) same("Search", refreshed.CleverTeeCursor.target) falsy(refreshed.CleverTeeChar.enabled) same("ErrorMsg", refreshed.CleverTeeChar.target) truthy(refreshed.CleverTeeDirect.enabled) same("IncSearch", refreshed.CleverTeeDirect.target) end) test("Case policy resolves an explicit mode for each target", function() local host = MemoryHost.new() local service = policy.new(host) local lower = domain.TargetValue.character("a", 97) local upper = domain.TargetValue.character("A", 65) local multibyte = domain.TargetValue.character("\227\129\130", 0x3042) same(domain.CaseMode.SENSITIVE, service:case_mode(lower)) host:set_configuration("smart_case", true) same(domain.CaseMode.INSENSITIVE, service:case_mode(lower)) same(domain.CaseMode.SENSITIVE, service:case_mode(upper)) same(domain.CaseMode.SENSITIVE, service:case_mode(multibyte)) host:set_configuration("ignore_case", true) same(domain.CaseMode.INSENSITIVE, service:case_mode(upper)) local match = service:sample_match(upper) local plan = domain.TargetPlan.new({ target = upper, kind = domain.TargetPlanKind.LITERAL, case_mode = match.case_mode, matcher = function() return true end, }) same(domain.CaseMode.INSENSITIVE, plan.case_mode) end) test("Unsupported Migemo policy mutation updates live configuration", function() local host = MemoryHost.new({ configuration = { use_migemo = true }, }) local service = policy.new(host) truthy(service:get_boolean("use_migemo")) host:clear_operations() service:disable_migemo_for_unsupported_encoding() local operations = host:operations() same(1, #operations) same("write_configuration", operations[1].operation) same("use_migemo", operations[1].name) same(false, operations[1].value) falsy(service:get_boolean("use_migemo")) host:set_configuration("use_migemo", true) truthy(service:get_boolean("use_migemo")) end) local function fresh_sequence_state() local transitions = state_transitions.new() transitions:ClearTemporaryOverlays() transitions:DiagnosticFullReset() return sequence_state.get(), transitions end local function map_size(values) local count = 0 for _ in pairs(values) do count = count + 1 end return count end test("SequenceState has one empty plugin-global instance", function() local state, transitions = fresh_sequence_state() same(state, sequence_state.new()) same(state, sequence_state.global) same(state, transitions:state()) same(state, state_transitions.new():state()) same(0, map_size(state.previous_descriptor)) same(0, map_size(state.previous_landing)) same(0, map_size(state.first_move)) same(0, map_size(state.previous_target)) same(0, map_size(state.migemo_cache)) same(nil, state.last_input_context) falsy(state.moved_forward) falsy(state.moved_forward_initialized) same(0, state.repeat_timestamp_ms) same(1, state.repeat_timestamp_ms + 1) same(nil, state.highlight_timer) same(0, #state.target_overlays) same(0, #state.temporary_overlays) same(0, #state.finalizers) end) test("Per-context transitions use normalized ModeContext keys", function() local state, transitions = fresh_sequence_state() local operator = domain.ModeContext.from_full_mode("no") local target = domain.TargetValue.character("x", 120) transitions:BeginAcquisition("nov", "F") transitions:CommitAcquiredTarget("no" .. string.char(0x16), target, 12.5) same(domain.Descriptor.FIND_BACKWARD, state:get_previous_descriptor("noV")) truthy(state:get_first_move(operator)) same(target, state:get_previous_target("no")) same(operator, state.last_input_context) same(12.5, state.repeat_timestamp_ms) same(1, map_size(state.previous_descriptor)) same(domain.Descriptor.FIND_BACKWARD, state.previous_descriptor[operator]) transitions:CommitCommandSuccess("noV", { line = 3, byte_column = 6 }, false) local serialized = state:to_table() same(3, serialized.contexts.no.previous_landing.line) same(6, serialized.contexts.no.previous_landing.byte_column) same(2, map_size(serialized.contexts.no.previous_landing)) same(nil, serialized.contexts.nov) local normal_target = domain.TargetValue.character("a", 97) local visual_target = domain.TargetValue.character("b", 98) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", normal_target) transitions:BeginAcquisition("v", "t") transitions:CommitAcquiredTarget("v", visual_target) same(normal_target, state:get_previous_target("n")) same(visual_target, state:get_previous_target("v")) same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) same(domain.Descriptor.TILL_FORWARD, state:get_previous_descriptor("v")) same(domain.ModeContext.from_full_mode("v"), state.last_input_context) end) test("Success transitions commit only their mode-sensitive fields", function() local state, transitions = fresh_sequence_state() local target = domain.TargetValue.character("h", 104) local command_destination = domain.Position.new(2, 4) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target, 31) transitions:CommitCommandSuccess( "n", command_destination, domain.Direction.FORWARD ) same(command_destination, state:get_previous_landing("n")) falsy(state:get_first_move("n")) truthy(state.moved_forward) truthy(state.moved_forward_initialized) local visual_destination = domain.Position.new(3, 2) transitions:BeginAcquisition("v", "T") transitions:CommitAcquiredTarget("v", target) transitions:CommitVisualSuccess("v", visual_destination) same(visual_destination, state:get_previous_landing("v")) falsy(state:get_first_move("v")) truthy(state.moved_forward, "Visual success must retain movement direction") same(31, state.repeat_timestamp_ms, "an absent acquisition time must be retained") transitions:CommitCommandSuccess("n", domain.Position.new(1, 1), "backward") falsy(state.moved_forward) truthy(state.moved_forward_initialized) end) test("Failed and partial outcomes preserve successful history silently", function() local state, transitions = fresh_sequence_state() local target = domain.TargetValue.character("a", 97) local landing = domain.Position.new(1, 5) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target, 17) transitions:CommitCommandSuccess("n", landing, true) local descriptor = state:get_previous_descriptor("n") local first_move = state:get_first_move("n") local moved_forward = state.moved_forward local before_timestamp = state.repeat_timestamp_ms local failed = domain.ActionOutcome.from_search( domain.SearchOutcome.boundary_before_any(landing), "f" ) local partial_endpoint = domain.Position.new(1, 7) local partial = domain.ActionOutcome.from_search( domain.SearchOutcome.boundary_after_partial(partial_endpoint, 2), "f" ) same(domain.ActionKind.FAILED_SEARCH, failed.kind) same(nil, failed.diagnostic) same(0, failed.successful_steps) same(domain.ActionKind.FAILED_SEARCH, partial.kind) same(partial_endpoint, partial.position) same(2, partial.successful_steps) same(nil, partial.diagnostic) same(descriptor, state:get_previous_descriptor("n")) same(landing, state:get_previous_landing("n")) same(first_move, state:get_first_move("n")) same(target, state:get_previous_target("n")) same(moved_forward, state.moved_forward) same(before_timestamp, state.repeat_timestamp_ms) end) test("State resource updates retain host locations and timer identity", function() local state, transitions = fresh_sequence_state() transitions:AddTargetOverlay("char-1", "window-1", 4) transitions:AddTargetOverlay("char-1", "window-2", 8) transitions:AddTemporaryOverlay("cursor-1", "window-1", "CleverTeeCursor") transitions:AddTemporaryOverlay("direct-1", "window-2", "CleverTeeDirect") transitions:AddFinalizer("finalizer-1", "buffer-1") transitions:AddFinalizer("finalizer-1", "buffer-2") local resources = state:resources() same("window-1", resources.target_overlays[1].window) same(4, resources.target_overlays[1].anchor_line) same("window-2", resources.target_overlays[2].window) same("CleverTeeDirect", resources.temporary_overlays[2].group) same("buffer-1", resources.finalizers[1].buffer) same("buffer-2", resources.finalizers[2].buffer) resources.target_overlays[1].window = "changed" same("window-1", state.target_overlays[1].window) fails(function() transitions:AddTargetOverlay("char-1", "window-1") end, "already active") fails(function() transitions:AddFinalizer("missing-location") end, "host location") same(nil, transitions:SetHighlightTimer("timer-1")) same("timer-1", transitions:SetHighlightTimer("timer-2")) local cleared, current = transitions:ClearHighlightTimer("timer-1") same(nil, cleared) falsy(current) same("timer-2", state.highlight_timer) cleared, current = transitions:ClearHighlightTimer("timer-2") same("timer-2", cleared) truthy(current) same(nil, state.highlight_timer) local removed = transitions:RemoveTargetOverlay("char-1", "window-1") same(1, #removed) same("window-1", removed[1].window) same(1, #state.target_overlays) same("window-2", state.target_overlays[1].window) end) test("SequenceState exposes copies and rejects direct mutation", function() local state, transitions = fresh_sequence_state() transitions:BeginAcquisition("n", "t") transitions:AddTargetOverlay("char-copy", "window-copy", 2) fails(function() state.moved_forward = true end, "StateTransitions") local descriptor_map = state.previous_descriptor descriptor_map[domain.ModeContext.from_full_mode("n")] = nil same(domain.Descriptor.TILL_FORWARD, state:get_previous_descriptor("n")) local overlays = state.target_overlays overlays[1].anchor_line = 99 overlays[1] = nil same(2, state.target_overlays[1].anchor_line) end) test("PublicReset applies its exact clear and retain sets", function() local state, transitions = fresh_sequence_state() local normal_target = domain.TargetValue.character("h", 104) local visual_target = domain.TargetValue.character("x", 120) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", normal_target, 77) transitions:CommitCommandSuccess("n", domain.Position.new(1, 2), true) transitions:BeginAcquisition("v", "t") transitions:CommitAcquiredTarget("v", visual_target) transitions:CommitVisualSuccess("v", domain.Position.new(2, 3)) transitions:CacheMigemo("utf-8", { dictionary = "utf-8" }) transitions:SetHighlightTimer("timer-1") transitions:AddTargetOverlay("char-1", "window-1", 1) transitions:AddTargetOverlay("char-2", "window-2", 2) transitions:AddTemporaryOverlay("cursor-1", "window-1", "CleverTeeCursor") transitions:AddFinalizer("finalizer-1", "buffer-1") local input_context = state.last_input_context local cleanup = transitions:PublicReset("window-1") same(nil, state:get_previous_descriptor("n")) same(nil, state:get_previous_descriptor("v")) same(nil, state:get_previous_landing("n")) same(nil, state:get_previous_landing("v")) same(nil, state:get_first_move("n")) same(nil, state:get_first_move("v")) same(normal_target, state:get_previous_target("n")) same(visual_target, state:get_previous_target("v")) same(input_context, state.last_input_context) truthy(state.moved_forward) truthy(state.moved_forward_initialized) same(0, map_size(state.migemo_cache)) same(0, state.repeat_timestamp_ms) same(nil, state.highlight_timer) same(1, #state.target_overlays) same("window-2", state.target_overlays[1].window) same(1, #state.temporary_overlays) same(1, #state.finalizers) same("timer-1", cleanup.highlight_timer) same(1, #cleanup.target_overlays) same("char-1", cleanup.target_overlays[1].identity) same("window-1", cleanup.target_overlays[1].window) same(0, #cleanup.finalizers) end) test("FullFinalization clears feedback and direction while retaining history", function() local state, transitions = fresh_sequence_state() local normal_target = domain.TargetValue.character("h", 104) local visual_target = domain.TargetValue.character("x", 120) local dictionary = { dictionary = "utf-8" } transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", normal_target, 91) transitions:CommitCommandSuccess("n", domain.Position.new(1, 2), true) transitions:BeginAcquisition("v", "T") transitions:CommitAcquiredTarget("v", visual_target) transitions:CommitVisualSuccess("v", domain.Position.new(2, 3)) transitions:CacheMigemo("utf-8", dictionary) transitions:SetHighlightTimer("timer-finalize") transitions:AddTargetOverlay("char-current", "window-1", 1) transitions:AddTargetOverlay("char-peer", "window-2", 2) transitions:AddTemporaryOverlay("direct-current", "window-1", "CleverTeeDirect") transitions:AddFinalizer("finalizer-a", "buffer-1") transitions:AddFinalizer("finalizer-b", "buffer-2") local input_context = state.last_input_context local cleanup = transitions:FullFinalization("window-1") same(nil, state:get_previous_landing("n")) same(nil, state:get_previous_landing("v")) falsy(state.moved_forward) truthy(state.moved_forward_initialized) same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) same(domain.Descriptor.TILL_BACKWARD, state:get_previous_descriptor("v")) same(normal_target, state:get_previous_target("n")) same(visual_target, state:get_previous_target("v")) falsy(state:get_first_move("n")) falsy(state:get_first_move("v")) same(input_context, state.last_input_context) same(dictionary, state:get_migemo("utf-8")) same(91, state.repeat_timestamp_ms) same(nil, state.highlight_timer) same(1, #state.target_overlays) same("window-2", state.target_overlays[1].window) same(1, #state.temporary_overlays) same(0, #state.finalizers) same("timer-finalize", cleanup.highlight_timer) same("window-1", cleanup.target_overlays[1].window) same(2, #cleanup.finalizers) same("buffer-1", cleanup.finalizers[1].buffer) end) test("DiagnosticFullReset adds only its diagnostic clear set", function() local state, transitions = fresh_sequence_state() local target = domain.TargetValue.character("z", 122) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target, 55) transitions:CommitCommandSuccess("n", domain.Position.new(4, 7), true) transitions:CacheMigemo("cp932", { dictionary = "cp932" }) transitions:SetHighlightTimer("timer-diagnostic") transitions:AddTargetOverlay("char-diagnostic", "window-3", 4) transitions:AddTemporaryOverlay("cursor-diagnostic", "window-3", "CleverTeeCursor") transitions:AddFinalizer("finalizer-diagnostic", "buffer-3") local cleanup = transitions:DiagnosticFullReset() same(nil, state:get_previous_descriptor("n")) same(nil, state:get_previous_landing("n")) same(nil, state:get_first_move("n")) same(nil, state:get_previous_target("n")) same(nil, state.last_input_context) falsy(state.moved_forward) falsy(state.moved_forward_initialized) same(0, map_size(state.migemo_cache)) same(0, state.repeat_timestamp_ms) same(nil, state.highlight_timer) same(0, #state.target_overlays) same(1, #state.temporary_overlays) same("window-3", state.temporary_overlays[1].window) same(0, #state.finalizers) same("timer-diagnostic", cleanup.highlight_timer) same("window-3", cleanup.target_overlays[1].window) same("buffer-3", cleanup.finalizers[1].buffer) transitions:ClearTemporaryOverlays() end) test("ClearAllLandingsAndDirection retains movement initialization", function() local state, transitions = fresh_sequence_state() local target = domain.TargetValue.character("a", 97) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target) transitions:CommitCommandSuccess("n", domain.Position.new(1, 9), true) transitions:BeginAcquisition("v", "t") transitions:CommitVisualSuccess("v", domain.Position.new(2, 1)) transitions:ClearAllLandingsAndDirection() same(nil, state:get_previous_landing("n")) same(nil, state:get_previous_landing("v")) falsy(state.moved_forward) truthy(state.moved_forward_initialized) same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) same(target, state:get_previous_target("n")) same(domain.Descriptor.TILL_FORWARD, state:get_previous_descriptor("v")) end) local function collect_iteration(iterator) local positions = {} local characters = {} local spans = {} while true do local position, character, span = iterator() if position == nil then break end positions[#positions + 1] = position characters[#characters + 1] = character spans[#spans + 1] = span end return positions, characters, spans end local function position_strings(positions) local result = {} for index, position in ipairs(positions) do result[index] = tostring(position) end return result end local MIXED_JAPANESE = "A\227\129\130\239\189\178\230\188\162B" local MIXED_CHARACTERS = { "A", "\227\129\130", "\239\189\178", "\230\188\162", "B", } test("TextView indexes editor characters in each required encoding", function() local cases = { { encoding = "utf-8", starts = { 1, 2, 5, 8, 11 }, lengths = { 1, 3, 3, 3, 1 }, line_length = 11, }, { encoding = "cp932", starts = { 1, 2, 4, 5, 7 }, lengths = { 1, 2, 1, 2, 1 }, line_length = 7, }, { encoding = "euc-jp", starts = { 1, 2, 4, 6, 8 }, lengths = { 1, 2, 2, 2, 1 }, line_length = 8, }, } for _, case in ipairs(cases) do local view = text_topology.new({ MIXED_JAPANESE }, case.encoding) same(case.encoding, view.encoding) same(1, view.line_count) same(5, view:line_character_count(1)) same(case.line_length, view:line_byte_length(1)) same(case.line_length, #view:line_encoded_text(1)) for character_index = 1, #MIXED_CHARACTERS do local column = view:byte_column_for_character_index(1, character_index) same(case.starts[character_index], column) same( character_index, view:character_index_for_byte_column(1, column) ) same(MIXED_CHARACTERS[character_index], view:character_at(1, column)) local span = view:byte_span_for_character_index(1, character_index) same(case.lengths[character_index], span.byte_length) same(column, span.byte_start) same(column + span.byte_length - 1, span.byte_end) truthy(view:is_character_start(span.position)) end end local ascii = text_topology.new({ "plain" }, "utf8") same("utf-8", ascii.encoding) same(5, ascii:line_byte_length(1)) same(5, ascii:line_character_count(1)) for index = 1, 5 do same(index, ascii:character_index_to_byte_column(1, index)) same(index, ascii:byte_column_to_character_index(1, index)) end end) test("TextView rejects interior and boundary bytes as character starts", function() local view = text_topology.new({ MIXED_JAPANESE }, "utf-8") local valid = { [1] = true, [2] = true, [5] = true, [8] = true, [11] = true, } for column = 1, view:line_byte_length(1) do same(valid[column] == true, view:is_character_start(1, column)) if not valid[column] then same(nil, view:try_character_index_for_byte_column(1, column)) fails(function() view:character_index_for_byte_column(1, column) end, "inside an editor character") end end falsy(view:is_character_start(1, 12)) fails(function() view:character_index_for_byte_column(1, 12) end, "does not identify") fails(function() view:byte_column_for_character_index(1, 0) end, "character_index") end) test("TextView treats Nvim grapheme clusters as editor characters", function() local combining = "e\204\129x" local view = text_topology.new({ combining }, "utf-8") same(2, view:line_character_count(1)) same(4, view:line_byte_length(1)) same(1, view:byte_column_for_character_index(1, 1)) same(4, view:byte_column_for_character_index(1, 2)) same("e\204\129", view:character_at_index(1, 1)) falsy(view:is_character_start(1, 2)) falsy(view:is_character_start(1, 3)) end) test("Forward and backward iteration enumerate reverse character starts", function() local view = text_topology.new({ "a\227\129\130", "", "\239\189\178z", }, "utf-8") local forward, characters, spans = collect_iteration(view:iter_buffer_forward()) list_same({ "(1,1)", "(1,2)", "(3,1)", "(3,4)" }, position_strings(forward)) list_same({ "a", "\227\129\130", "\239\189\178", "z" }, characters) for index, position in ipairs(forward) do truthy(view:is_character_start(position)) same(position, spans[index].position) end local backward = collect_iteration(view:iter_buffer_backward()) list_same({ "(3,4)", "(3,1)", "(1,2)", "(1,1)" }, position_strings(backward)) local line_forward = collect_iteration(view:iter_line_forward(1)) local line_backward = collect_iteration(view:iter_line_backward(1)) list_same({ "(1,1)", "(1,2)" }, position_strings(line_forward)) list_same({ "(1,2)", "(1,1)" }, position_strings(line_backward)) end) test("Bounded iterators include and stop at their selected boundaries", function() local view = text_topology.new({ "ab", "cd", "ef" }, "utf-8") local first = domain.Position.new(1, 2) local last = domain.Position.new(3, 1) local forward = collect_iteration(view:iter_forward(first, last)) list_same({ "(1,2)", "(2,1)", "(2,2)", "(3,1)" }, position_strings(forward)) local backward = collect_iteration(view:iter_backward(last, first)) list_same({ "(3,1)", "(2,2)", "(2,1)", "(1,2)" }, position_strings(backward)) fails(function() view:iter_forward(last, first) end, "must not follow") fails(function() view:iter_backward(first, last) end, "must not precede") fails(function() view:iter_forward(domain.Position.new(1, 3), last) end, "must start an editor character") end) test("Strict-side iteration obeys line and buffer match-start bounds", function() local view = text_topology.new({ "abc", "", "def" }, "utf-8") local origin = domain.Position.new(1, 2) local line_forward = collect_iteration(view:iter_strict_forward( origin, domain.SearchScope.CURRENT_LINE )) list_same({ "(1,3)" }, position_strings(line_forward)) local buffer_forward = collect_iteration(view:iter_strict_forward( origin, domain.SearchScope.BUFFER )) list_same({ "(1,3)", "(3,1)", "(3,2)", "(3,3)" }, position_strings(buffer_forward)) local line_backward = collect_iteration(view:iter_strict_backward( origin, "current_line" )) list_same({ "(1,1)" }, position_strings(line_backward)) local buffer_backward = collect_iteration(view:iter_strict_backward( domain.Position.new(3, 2), "buffer" )) list_same({ "(3,1)", "(1,3)", "(1,2)", "(1,1)" }, position_strings(buffer_backward)) local empty_line_bounds = view:line_match_start_bounds(2) truthy(empty_line_bounds.empty) same(nil, empty_line_bounds.first) same(nil, empty_line_bounds.last) same(0, #position_strings(collect_iteration(view:iter_line_forward(2)))) same(0, #position_strings(collect_iteration(view:iter_strict_forward( domain.Position.new(2, 1), "current_line" )))) local buffer_bounds = view:match_start_bounds("buffer") falsy(buffer_bounds.empty) same(domain.Position.new(1, 1), buffer_bounds.first) same(domain.Position.new(3, 3), buffer_bounds.last) truthy(buffer_bounds:contains(domain.Position.new(2, 1))) end) test("Endpoint adjacency crosses lines and normalizes cursor boundaries", function() local view = text_topology.new({ "a\227\129\130", "", "\239\189\178z", }, "utf-8") same(nil, view:predecessor(domain.Position.new(1, 1))) same(domain.Position.new(1, 1), view:predecessor(domain.Position.new(1, 2))) same(domain.Position.new(2, 1), view:successor(domain.Position.new(1, 2))) same(domain.Position.new(1, 2), view:predecessor(domain.Position.new(2, 1))) same(domain.Position.new(3, 1), view:successor(domain.Position.new(2, 1))) same(domain.Position.new(2, 1), view:predecessor(domain.Position.new(3, 1))) same(domain.Position.new(3, 4), view:successor(domain.Position.new(3, 1))) same(nil, view:successor(domain.Position.new(3, 4))) same(domain.Position.new(1, 2), view:normalize_endpoint(1, 3)) same(domain.Position.new(1, 2), view:normalize_endpoint(1, 5)) same(domain.Position.new(2, 1), view:normalize_endpoint(2, 7)) truthy(view:is_valid_cursor_position(view:normalize_endpoint(1, 3))) truthy(view:is_valid_cursor_position(view:normalize_endpoint(2, 7))) fails(function() view:successor(domain.Position.new(1, 3)) end, "inside an editor character") fails(function() view:predecessor(domain.Position.new(2, 2)) end, "empty line") end) test("TextView creation reads one operation-local snapshot", function() local host = MemoryHost.new({ buffer_lines = { "ab" }, effective_encoding = "utf-8", }) host:clear_operations() local first = text_topology.from_host(host) local operations = host:operations() same(2, #operations) same("read_text", operations[1].operation) same("read_encoding", operations[2].operation) host:set_text({ "xyz" }) local second = text_topology.from_host(host) same("ab", first:line_text(1)) same(2, first:line_character_count(1)) same("xyz", second:line_text(1)) same(3, second:line_character_count(1)) falsy(first == second) fails(function() first.encoding = "cp932" end, "immutable") end) test("TextView indexes only lines required by an operation", function() local indexed = {} local view = text_topology.new({ "aaa", "bbb", "ccc" }, "utf-8", { splitter = function(text) indexed[#indexed + 1] = text return text_topology.split_editor_characters(text) end, encoder = function(character) return character end, }) same(0, #indexed) local positions = collect_iteration(view:iter_strict_forward( domain.Position.new(2, 1), domain.SearchScope.CURRENT_LINE )) list_same({ "(2,2)", "(2,3)" }, position_strings(positions)) list_same({ "bbb" }, indexed) same("bb\nccc", view:text_suffix(domain.Position.new(2, 2))) list_same({ "bbb" }, indexed) same(3, view:line_character_count(1)) list_same({ "bbb", "aaa" }, indexed) end) test("All-empty buffers expose empty full-buffer iteration", function() local view = text_topology.new({ "", "", "" }, "utf-8") local bounds = view:buffer_match_start_bounds() truthy(bounds:is_empty()) same(nil, bounds.first) same(nil, bounds.last) same(0, #position_strings(collect_iteration(view:iter_buffer_forward()))) same(0, #position_strings(collect_iteration(view:iter_buffer_backward()))) end) local function target(character, first_code) return domain.TargetValue.character( character, first_code or string.byte(character, 1) ) end local function matching_policy(overrides) local result = { ignore_case = false, smart_case = false, chars_match_any_signs = "", } for key, value in pairs(overrides or {}) do result[key] = value end return result end test("Case mode follows ignore-case and lower-ASCII smart-case priority", function() local resolver = case_policy.new() for code = string.byte("a"), string.byte("z") do local lower = target(string.char(code), code) same( domain.CaseMode.INSENSITIVE, resolver:resolve(lower, false, true), "smart case must fold lower ASCII" ) same( domain.CaseMode.INSENSITIVE, policy.resolve_case_mode(lower, false, true) ) end for code = string.byte("A"), string.byte("Z") do local upper = target(string.char(code), code) same(domain.CaseMode.SENSITIVE, resolver:resolve(upper, false, true)) same(domain.CaseMode.INSENSITIVE, resolver:resolve(upper, true, true)) end local multibyte = target("\195\164", 0x00e4) local symbol = target(";", string.byte(";")) local control = target(string.char(1), 1) same(domain.CaseMode.SENSITIVE, resolver:resolve(multibyte, false, true)) same(domain.CaseMode.SENSITIVE, resolver:resolve(symbol, false, true)) same(domain.CaseMode.SENSITIVE, resolver:resolve(control, false, true)) same(domain.CaseMode.INSENSITIVE, resolver:resolve(multibyte, true, false)) truthy(case_policy.is_lower_ascii("a")) falsy(case_policy.is_lower_ascii("A")) falsy(case_policy.is_lower_ascii("aa")) falsy(case_policy.is_lower_ascii("\195\164")) fails(function() resolver.lowercase = string.lower end, "immutable") end) test("Case comparison uses editor lowercase conversion explicitly", function() local resolver = case_policy.new() local upper_a_umlaut = "\195\132" local lower_a_umlaut = "\195\164" truthy(resolver:equal("A", "a", domain.CaseMode.INSENSITIVE)) falsy(resolver:equal("A", "a", domain.CaseMode.SENSITIVE)) truthy(resolver:equal( upper_a_umlaut, lower_a_umlaut, domain.CaseMode.INSENSITIVE )) same(vim.fn.tolower(upper_a_umlaut), resolver:lowercase(upper_a_umlaut)) local calls = {} local injected = case_policy.new(function(value) calls[#calls + 1] = value return value == "UP" and "folded" or value end) local compare = injected:comparator("UP", domain.CaseMode.INSENSITIVE) truthy(compare("folded")) list_same({ "UP", "folded" }, calls) end) test("Trigger parsing uses complete editor characters", function() local combining_character = "e\204\129" local japanese_character = "\227\129\130" local configured = "x" .. combining_character .. japanese_character list_same( { "x", combining_character, japanese_character }, text_topology.split_editor_characters(configured) ) list_same( { "x", combining_character, japanese_character }, target_plan.parse_trigger_characters(configured) ) local factory = target_plan.new() local plan = factory:build( target(combining_character, string.byte("e")), matching_policy({ chars_match_any_signs = configured }) ) same(domain.TargetPlanKind.SYMBOL, plan.kind) truthy(plan:matches("!")) falsy(plan:matches(combining_character)) end) test("Symbol plans match the exact shared 32-character set", function() local expected_symbols = "!\"#$%&'()=~|\\-^@`[]{};:+*<>,.?_/" same(expected_symbols, target_plan.SYMBOLS) same(32, #target_plan.symbol_characters()) local expected = {} for index = 1, #expected_symbols do expected[expected_symbols:sub(index, index)] = true end local plan = target_plan.build( target(";"), matching_policy({ ignore_case = true, chars_match_any_signs = ";", }) ) same(domain.TargetPlanKind.SYMBOL, plan.kind) same(domain.CaseMode.INSENSITIVE, plan.case_mode) local accepted = 0 for code = 0, 127 do local character = string.char(code) local matches = plan:matches(character) same(expected[character] == true, matches, "ASCII code " .. tostring(code)) if matches then accepted = accepted + 1 end end same(32, accepted) falsy(plan:matches(" ")) falsy(plan:matches("a")) falsy(plan:matches("Z")) falsy(plan:matches("0")) falsy(plan:matches("9")) falsy(plan:matches("\227\129\130")) end) test("Every configured editor character selects the symbol branch", function() local factory = target_plan.new() local cases = { { target("a"), "a" }, { target(";"), ";" }, { target("\227\129\130", 0x3042), "x\227\129\130y" }, } for _, case in ipairs(cases) do local plan = factory:build( case[1], matching_policy({ chars_match_any_signs = case[2] }) ) same(domain.TargetPlanKind.SYMBOL, plan.kind) truthy(plan:matches("!")) truthy(plan:matches("/")) same(target_plan.is_symbol(case[1].value), plan:matches(case[1].value)) end end) test("Pattern punctuation stays literal when it is not a trigger", function() local factory = target_plan.new() local pattern_characters = { "^", "[", "]", "(", ")", ".", "*", "+", "?", "$", "%", "-", "|", } for _, character in ipairs(pattern_characters) do local plan = factory:build(target(character), matching_policy()) same(domain.TargetPlanKind.LITERAL, plan.kind, character) truthy(plan:matches(character), character) falsy(plan:matches("x"), character) end local backslash = factory:build(target("\\"), matching_policy()) same(domain.TargetPlanKind.BACKSLASH, backslash.kind) truthy(backslash:matches("\\")) falsy(backslash:matches("\\\\")) falsy(backslash:matches("/")) end) test("Symbol selection precedes the literal backslash branch", function() local factory = target_plan.new() local literal = factory:build(target("\\"), matching_policy()) local wildcard = factory:build( target("\\"), matching_policy({ chars_match_any_signs = "\\" }) ) same(domain.TargetPlanKind.BACKSLASH, literal.kind) same(domain.TargetPlanKind.SYMBOL, wildcard.kind) truthy(wildcard:matches(".")) truthy(wildcard:matches("\\")) falsy(wildcard:matches("a")) end) test("Literal plans apply ignore-case and smart-case modes", function() local factory = target_plan.new() local lower_smart = factory:build( target("a"), matching_policy({ smart_case = true }) ) local upper_smart = factory:build( target("A"), matching_policy({ smart_case = true }) ) local upper_ignored = factory:build( target("A"), matching_policy({ ignore_case = true, smart_case = true }) ) local multibyte_smart = factory:build( target("\195\164", 0x00e4), matching_policy({ smart_case = true }) ) local multibyte_ignored = factory:build( target("\195\132", 0x00c4), matching_policy({ ignore_case = true }) ) same(domain.CaseMode.INSENSITIVE, lower_smart.case_mode) truthy(lower_smart:matches("A")) same(domain.CaseMode.SENSITIVE, upper_smart.case_mode) falsy(upper_smart:matches("a")) same(domain.CaseMode.INSENSITIVE, upper_ignored.case_mode) truthy(upper_ignored:matches("a")) same(domain.CaseMode.SENSITIVE, multibyte_smart.case_mode) falsy(multibyte_smart:matches("\195\132")) same(domain.CaseMode.INSENSITIVE, multibyte_ignored.case_mode) truthy(multibyte_ignored:matches("\195\164")) end) test("Matching ignores ambient editor case options", function() local saved_ignorecase = vim.o.ignorecase local saved_smartcase = vim.o.smartcase vim.o.ignorecase = true vim.o.smartcase = true local sensitive = target_plan.build(target("a"), matching_policy()) local sensitive_match = sensitive:matches("A") vim.o.ignorecase = false vim.o.smartcase = false local insensitive = target_plan.build( target("a"), matching_policy({ ignore_case = true }) ) local insensitive_match = insensitive:matches("A") vim.o.ignorecase = saved_ignorecase vim.o.smartcase = saved_smartcase falsy(sensitive_match) truthy(insensitive_match) end) test("Special keys are empty plans and controls remain literal", function() local factory = target_plan.new() local special = domain.TargetValue.special_key(string.char(0x80, 0xfd, 1)) local empty = factory:build( special, matching_policy({ chars_match_any_signs = string.char(0x80) }) ) same(domain.TargetPlanKind.EMPTY, empty.kind) falsy(empty:matches("a")) falsy(empty:matches("!")) local first_code_controls = target(string.char(0x80), 0x80) same( domain.TargetPlanKind.EMPTY, factory:build(first_code_controls, matching_policy()).kind ) for _, code in ipairs({ 1, 9, 13, 26, 31, 127 }) do local character = string.char(code) local plan = factory:build(target(character, code), matching_policy()) same(domain.TargetPlanKind.LITERAL, plan.kind) truthy(plan:matches(character)) falsy(plan:matches(string.char((code + 1) % 128))) end local fallback = factory:build( domain.TargetValue.code_fallback(), matching_policy({ ignore_case = true }) ) same(domain.TargetPlanKind.LITERAL, fallback.kind) falsy(fallback:matches("")) falsy(fallback:matches(string.char(1))) end) test("Policy-backed factories sample live values into immutable plans", function() local host = MemoryHost.new() local service = policy.new(host) local factory = target_plan.new(service) local typed_target = target("a") local first = factory:build(typed_target) same(domain.TargetPlanKind.LITERAL, first.kind) same(domain.CaseMode.SENSITIVE, first.case_mode) falsy(first:matches("A")) host:set_configuration("ignore_case", true) host:set_configuration("chars_match_any_signs", "a") local second = factory:build(typed_target) same(domain.TargetPlanKind.SYMBOL, second.kind) same(domain.CaseMode.INSENSITIVE, second.case_mode) truthy(second:matches("!")) same(domain.TargetPlanKind.LITERAL, first.kind) falsy(first:matches("A")) local serialized = second:to_table() same("symbol", serialized.kind) same("insensitive", serialized.case_mode) fails(function() second.kind = domain.TargetPlanKind.LITERAL end, "immutable") end) test("Target plan inputs enforce matching contracts", function() local factory = target_plan.new() fails(function() factory:build({}, matching_policy()) end, "TargetValue") fails(function() factory:build(target("a"), matching_policy({ ignore_case = 1 })) end, "Boolean") fails(function() factory:build(target("a"), matching_policy({ chars_match_any_signs = {} })) end, "string") fails(function() target_plan.parse_trigger_characters("abc", function() return { "a", "c" } end) end, "preserve") end) local MIGEMO_HIRAGANA_A = "\227\129\130" local MIGEMO_KANJI = "\230\188\162" local MIGEMO_SHELTER = "\229\142\166" local MIGEMO_GATE = "\233\150\128" local function read_binary(path) local handle, open_error = io.open(path, "rb") if handle == nil then error("could not open " .. path .. ": " .. tostring(open_error), 2) end local contents = handle:read("*a") handle:close() return contents end local function migemo_components(configuration) local _, transitions = fresh_sequence_state() local host = MemoryHost.new({ configuration = configuration }) local service = policy.new(host) local catalog = migemo_catalog.new({ policy = service, transitions = transitions, }) local factory = target_plan.new({ policy = service, migemo_catalog = catalog, }) return host, service, catalog, factory, transitions end test("Bundled Migemo assets equal the normative source data", function() local assets = { { encoding = "utf-8", file = "utf8.vim", hash = "18b0e6cd454b4ec3c2b16d886b2e02eeae29e2c4a83746011d2f693e581e293b", }, { encoding = "cp932", file = "cp932.vim", hash = "ab02f0405dea08dd693138fbdf322e618f28d7bdb4c1d38ec4abf80e1c8730b5", }, { encoding = "euc-jp", file = "eucjp.vim", hash = "fd6fa831a5636dad71f70447b57be99705c54b7d50109a61e2569944722b7362", }, } for _, asset in ipairs(assets) do local bundled_path = migemo_catalog.bundled_asset_path(asset.encoding) local bundled = read_binary(bundled_path) same(asset.hash, vim.fn.sha256(bundled), asset.file .. " checksum") end end) test("Migemo dictionaries validate ordered keys and cache each encoding", function() local state, transitions = fresh_sequence_state() local catalog = migemo_catalog.new({ transitions = transitions }) local expected_keys = migemo_catalog.expected_keys() same(52, #expected_keys) same( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", table.concat(expected_keys) ) local encodings = { { requested = "UTF8", canonical = "utf-8" }, { requested = "CP-932", canonical = "cp932" }, { requested = "EUCJP", canonical = "euc-jp" }, } for _, item in ipairs(encodings) do local first = catalog:get(item.requested) local second = catalog:get(item.canonical) same(first, second) same(first, state:get_migemo(item.canonical)) same(item.canonical, first.encoding) same(52, first.entry_count) list_same(expected_keys, first:keys()) same(1, catalog:load_count(item.canonical)) for _, key in ipairs(expected_keys) do same("function", type(first[key])) truthy(first:has(key)) truthy(first:matches(key, key, domain.CaseMode.SENSITIVE)) end fails(function() first.encoding = "changed" end, "immutable") end same(3, map_size(state.migemo_cache)) end) test("Migemo catalog rejects wrong entry counts and key order", function() local _, transitions = fresh_sequence_state() local keys = migemo_catalog.expected_keys() local data = {} for _, key in ipairs(keys) do data[key] = key end local compiler = function() return function() return false end end local wrong_order = migemo_catalog.expected_keys() wrong_order[1], wrong_order[2] = wrong_order[2], wrong_order[1] local order_catalog = migemo_catalog.new({ transitions = transitions, asset_loader = function() return data, wrong_order, "ordered-test" end, pattern_compiler = compiler, }) fails(function() order_catalog:get("utf-8") end, "ordered a through z, then A through Z") data[keys[#keys]] = nil local count_catalog = migemo_catalog.new({ transitions = transitions, asset_loader = function() return data, keys, "count-test" end, pattern_compiler = compiler, }) fails(function() count_catalog:get("utf-8") end, "exactly 52 keys") end) test("Native Migemo predicates agree across a fixed encoding corpus", function() local _, transitions = fresh_sequence_state() local catalog = migemo_catalog.new({ transitions = transitions }) local corpus = { { "a", "a", domain.CaseMode.SENSITIVE, true }, { "a", "A", domain.CaseMode.SENSITIVE, false }, { "a", "A", domain.CaseMode.INSENSITIVE, true }, { "a", MIGEMO_HIRAGANA_A, domain.CaseMode.SENSITIVE, true }, { "k", MIGEMO_KANJI, domain.CaseMode.SENSITIVE, true }, { "a", MIGEMO_SHELTER .. "\n" .. MIGEMO_GATE, domain.CaseMode.SENSITIVE, true, }, { "a", "x" .. MIGEMO_HIRAGANA_A, domain.CaseMode.SENSITIVE, false }, } local saved_ignorecase = vim.o.ignorecase local saved_smartcase = vim.o.smartcase vim.o.ignorecase = true vim.o.smartcase = true for _, encoding in ipairs(migemo_catalog.supported_encodings()) do local dictionary = catalog:get(encoding) for index, item in ipairs(corpus) do same( item[4], dictionary:matches(item[1], item[2], item[3]), encoding .. " corpus item " .. tostring(index) ) end end vim.o.ignorecase = saved_ignorecase vim.o.smartcase = saved_smartcase end) test("Migemo activation uses live policy, scope, and branch precedence", function() local host, _, _, factory = migemo_components({ use_migemo = true, search_current_line_only = false, chars_match_any_signs = "a;", }) local ascii_view = text_topology.new({ "xax" }, "utf-8") local context = { text_view = ascii_view, origin = domain.Position.new(1, 1), } local migemo = factory:build(target("a"), nil, context) same(domain.TargetPlanKind.MIGEMO, migemo.kind) host:set_configuration("use_migemo", false) local symbol = factory:build(target("a"), nil, context) same(domain.TargetPlanKind.SYMBOL, symbol.kind) host:set_configuration("use_migemo", true) same(domain.TargetPlanKind.SYMBOL, factory:build(target(";"), nil, context).kind) same( domain.TargetPlanKind.LITERAL, factory:build(target(MIGEMO_HIRAGANA_A, 0x3042), nil, context).kind ) same(domain.TargetPlanKind.LITERAL, factory:build(target("ab", 97), nil, context).kind) host:set_configuration("chars_match_any_signs", "") host:set_configuration("search_current_line_only", true) local ascii_line = factory:build(target("a"), nil, context) same(domain.TargetPlanKind.LITERAL, ascii_line.kind) local multibyte_view = text_topology.new({ "x" .. MIGEMO_HIRAGANA_A }, "utf-8") local multibyte_line = factory:build(target("a"), nil, { text_view = multibyte_view, origin = domain.Position.new(1, 1), }) same(domain.TargetPlanKind.MIGEMO, multibyte_line.kind) end) test("Migemo plans enforce case and one-character candidate constraints", function() local host, _, catalog, factory = migemo_components({ use_migemo = true, ignore_case = false, smart_case = false, }) local view = text_topology.new({ "aA" .. MIGEMO_HIRAGANA_A .. "Es", }, "utf-8") local context = { text_view = view, search_scope = domain.SearchScope.BUFFER, } local sensitive = factory:build(target("a"), nil, context) same(domain.CaseMode.SENSITIVE, sensitive.case_mode) truthy(sensitive:matches_at(view, view:position_for_character_index(1, 1))) falsy(sensitive:matches_at(view, view:position_for_character_index(1, 2))) truthy(sensitive:matches_at(view, view:position_for_character_index(1, 3))) local other_letter = view:position_for_character_index(1, 4) truthy(catalog:get("utf-8"):matches( "a", view:text_suffix(other_letter), domain.CaseMode.SENSITIVE )) falsy(sensitive:matches_at(view, other_letter)) falsy(sensitive:matches("x", view:position_for_character_index(1, 1), view)) host:set_configuration("ignore_case", true) local insensitive = factory:build(target("a"), nil, context) same(domain.CaseMode.INSENSITIVE, insensitive.case_mode) truthy(insensitive:matches_at(view, view:position_for_character_index(1, 2))) end) test("Migemo assertions inspect later lines while limiting candidate starts", function() local _, _, _, factory = migemo_components({ use_migemo = true, search_current_line_only = true, }) local view = text_topology.new({ "x" .. MIGEMO_SHELTER, "", MIGEMO_GATE, MIGEMO_HIRAGANA_A, }, "utf-8") local candidate = view:position_for_character_index(1, 2) local plan = factory:build(target("a"), nil, { text_view = view, origin = domain.Position.new(1, 1), }) same(domain.TargetPlanKind.MIGEMO, plan.kind) same( MIGEMO_SHELTER .. "\n\n" .. MIGEMO_GATE .. "\n" .. MIGEMO_HIRAGANA_A, view:text_suffix(candidate) ) truthy(plan:matches_at(view, candidate)) local matched_start = plan:matches_at(view, candidate) and candidate or nil same(candidate, matched_start) falsy(plan:matches_at(view, domain.Position.new(4, 1))) end) test("Unsupported Migemo encoding disables live policy and raises exactly", function() local host, service, _, factory = migemo_components({ use_migemo = true, search_current_line_only = false, }) local view = text_topology.new({ "a" }, "latin1") local ok, diagnostic = pcall(function() factory:build(target("a"), nil, { text_view = view, search_scope = domain.SearchScope.BUFFER, }) end) falsy(ok) same( "clever-tee: Encoding 'latin1' is not supported. Migemo is disabled", diagnostic ) falsy(service:get_boolean("use_migemo")) same(false, host:read_configuration("use_migemo")) same(0, map_size(sequence_state.get().migemo_cache)) local literal = factory:build(target("a"), nil, { text_view = view, search_scope = domain.SearchScope.BUFFER, }) same(domain.TargetPlanKind.LITERAL, literal.kind) end) test("Public Reset discards loaded Migemo dictionary objects", function() local state, transitions = fresh_sequence_state() local catalog = migemo_catalog.new({ transitions = transitions }) local first = catalog:get("utf-8") same(first, state:get_migemo("utf-8")) transitions:PublicReset() same(nil, state:get_migemo("utf-8")) same(nil, catalog:cached("utf-8")) local second = catalog:get("utf-8") falsy(first == second) same(second, state:get_migemo("utf-8")) same(2, catalog:load_count("utf-8")) end) test("MotionPlanFactory combines resolved motion values", function() local target_match = target_plan.build(target("a"), matching_policy()) local factory = motion_plan.new() local descriptors = { "f", "F", "t", "T" } for _, descriptor in ipairs(descriptors) do local plan = factory:build( target_match, descriptor, domain.SearchScope.CURRENT_LINE, domain.EndpointPolicy.VISUAL_EXCLUSIVE ) same(target_match, plan.target_plan) same(domain.Descriptor.from_string(descriptor), plan.descriptor) same(domain.SearchScope.CURRENT_LINE, plan.search_scope) same(domain.EndpointPolicy.VISUAL_EXCLUSIVE, plan.endpoint_policy) end local defaulted = motion_plan.build(target_match, "f") same(domain.SearchScope.BUFFER, defaulted.search_scope) same(domain.EndpointPolicy.REGULAR, defaulted.endpoint_policy) fails(function() defaulted.search_scope = domain.SearchScope.CURRENT_LINE end, "immutable") fails(function() factory:build({}, "f", "buffer", "regular") end, "TargetPlan") end) test("DestinationEngine accepts pure calculation inputs", function() local view = text_topology.new({ "abc" }, "utf-8") local plan = motion_plan.build( target_plan.build(target("z"), matching_policy()), "f" ) local engine = destination_engine.new() local origin = domain.Position.new(1, 1) local outcome = engine:calculate(view, origin, plan, nil, true) same(domain.SearchStatus.BOUNDARY_BEFORE_ANY, outcome.status) same(origin, outcome.endpoint) same(0, outcome.successful_steps) truthy(destination_engine.DestinationEngine.is(engine)) fails(function() engine.mutable = true end, "immutable") fails(function() engine:calculate({}, origin, plan, 1, true) end, "TextView") fails(function() engine:calculate(view, origin, {}, 1, true) end, "ResolvedMotionPlan") fails(function() engine:calculate(view, origin, plan, 0, true) end, "positive") fails(function() engine:calculate(view, origin, plan, 1, nil) end, "Boolean") fails(function() engine:calculate(view, domain.Position.new(1, 4), plan, 1, true) end, "valid cursor") end) test("DestinationEngine enumerates target starts in motion order", function() local view = text_topology.new({ "abaca" }, "utf-8") local origin = domain.Position.new(1, 3) local visits = {} local target_match = domain.TargetPlan.new({ target = target("a"), kind = domain.TargetPlanKind.LITERAL, case_mode = domain.CaseMode.SENSITIVE, matcher = function(character, position) visits[#visits + 1] = character .. ":" .. tostring(position) return character == "a" end, }) local engine = destination_engine.new() local forward = engine:calculate( view, origin, motion_plan.build(target_match, "f"), 1, true ) same(domain.Position.new(1, 5), forward.endpoint) list_same({ "c:(1,4)", "a:(1,5)" }, visits) visits = {} local backward = engine:calculate( view, origin, motion_plan.build(target_match, "F"), 1, true ) same(domain.Position.new(1, 1), backward.endpoint) list_same({ "b:(1,2)", "a:(1,1)" }, visits) end) test("DestinationEngine stops target starts at selected boundaries", function() local view = text_topology.new({ "axa", "", "axa" }, "utf-8") local target_match = target_plan.build(target("a"), matching_policy()) local engine = destination_engine.new() local line_forward = motion_plan.build(target_match, "f", "current_line") local buffer_forward = motion_plan.build(target_match, "f", "buffer") local line_backward = motion_plan.build(target_match, "F", "current_line") local buffer_backward = motion_plan.build(target_match, "F", "buffer") same( domain.Position.new(1, 3), engine:calculate( view, domain.Position.new(1, 1), line_forward, 1, true ).endpoint ) same( domain.SearchStatus.BOUNDARY_BEFORE_ANY, engine:calculate( view, domain.Position.new(1, 3), line_forward, 1, false ).status ) same( domain.Position.new(3, 1), engine:calculate( view, domain.Position.new(1, 3), buffer_forward, 1, false ).endpoint ) same( domain.Position.new(3, 1), engine:calculate( view, domain.Position.new(3, 3), line_backward, 1, true ).endpoint ) same( domain.Position.new(1, 3), engine:calculate( view, domain.Position.new(3, 1), buffer_backward, 1, false ).endpoint ) same( domain.Position.new(3, 1), engine:calculate( view, domain.Position.new(2, 1), buffer_forward, 1, true ).endpoint ) end) test("FIND destinations use matching target positions", function() local view = text_topology.new({ "poge huga hiyo poyo" }, "utf-8") local engine = destination_engine.new() local h = target_plan.build(target("h"), matching_policy()) local forward = motion_plan.build(h, "f") local backward = motion_plan.build(h, "F") local first = engine:calculate( view, domain.Position.new(1, 1), forward, 1, true ) same(domain.Position.new(1, 6), first.endpoint) local second = engine:calculate(view, first.endpoint, forward, 1, false) same(domain.Position.new(1, 11), second.endpoint) same( domain.Position.new(1, 6), engine:calculate(view, second.endpoint, backward, 1, false).endpoint ) local o = target_plan.build(target("o"), matching_policy()) local find_o_backward = motion_plan.build(o, "F") local previous = engine:calculate( view, domain.Position.new(1, 19), find_o_backward, 1, true ) same(domain.Position.new(1, 17), previous.endpoint) same( domain.Position.new(1, 14), engine:calculate(view, previous.endpoint, find_o_backward, 1, false).endpoint ) end) test("Forward TILL destinations use target predecessors", function() local view = text_topology.new({ "poge huga hiyo poyo", "x", }, "utf-8") local engine = destination_engine.new() local till_h = motion_plan.build( target_plan.build(target("h"), matching_policy()), "t" ) same( domain.Position.new(1, 5), engine:calculate( view, domain.Position.new(1, 1), till_h, 1, true ).endpoint ) local till_x = motion_plan.build( target_plan.build(target("x"), matching_policy()), "t" ) local cross_line = engine:calculate( view, domain.Position.new(1, 18), till_x, 1, true ) same(domain.Position.new(1, 19), cross_line.endpoint) truthy(view:is_valid_cursor_position(cross_line.endpoint)) end) test("Backward TILL destinations use target successors", function() local engine = destination_engine.new() local same_line_view = text_topology.new({ "xabx" }, "utf-8") local till_a = motion_plan.build( target_plan.build(target("a"), matching_policy()), "T" ) same( domain.Position.new(1, 3), engine:calculate( same_line_view, domain.Position.new(1, 4), till_a, 1, true ).endpoint ) local cross_line_view = text_topology.new({ "x", "abc" }, "utf-8") local till_x = motion_plan.build( target_plan.build(target("x"), matching_policy()), "T" ) local cross_line = engine:calculate( cross_line_view, domain.Position.new(2, 2), till_x, 1, true ) same(domain.Position.new(2, 1), cross_line.endpoint) truthy(cross_line_view:is_valid_cursor_position(cross_line.endpoint)) end) test("Forward Visual-exclusive FIND uses target successors", function() local view = text_topology.new({ "abx", "cd" }, "utf-8") local engine = destination_engine.new() local target_match = target_plan.build(target("x"), matching_policy()) local regular = motion_plan.build( target_match, "f", "buffer", domain.EndpointPolicy.REGULAR ) local exclusive = motion_plan.build( target_match, "f", "buffer", domain.EndpointPolicy.VISUAL_EXCLUSIVE ) local origin = domain.Position.new(1, 1) same( domain.Position.new(1, 3), engine:calculate(view, origin, regular, 1, true).endpoint ) local adjusted = engine:calculate(view, origin, exclusive, 1, true) same(domain.Position.new(2, 1), adjusted.endpoint) truthy(view:is_valid_cursor_position(adjusted.endpoint)) end) test("Forward Visual-exclusive TILL uses target positions", function() local view = text_topology.new({ "abx", "x" }, "utf-8") local engine = destination_engine.new() local target_match = target_plan.build(target("x"), matching_policy()) local regular = motion_plan.build( target_match, "t", "buffer", domain.EndpointPolicy.REGULAR ) local exclusive = motion_plan.build( target_match, "t", "buffer", domain.EndpointPolicy.VISUAL_EXCLUSIVE ) local origin = domain.Position.new(1, 1) same( domain.Position.new(1, 2), engine:calculate(view, origin, regular, 1, true).endpoint ) same( domain.Position.new(1, 3), engine:calculate(view, origin, exclusive, 1, true).endpoint ) local cross_line_view = text_topology.new({ "abc", "x" }, "utf-8") local cross_line_origin = domain.Position.new(1, 1) same( domain.Position.new(1, 3), engine:calculate( cross_line_view, cross_line_origin, regular, 1, false ).endpoint ) same( domain.Position.new(2, 1), engine:calculate( cross_line_view, cross_line_origin, exclusive, 1, false ).endpoint ) end) test("MotionPlanFactory limits exclusive policy to character and line Visual", function() local view = text_topology.new({ "axz" }, "utf-8") local origin = domain.Position.new(1, 1) local target_match = target_plan.build(target("x"), matching_policy()) local factory = motion_plan.new() local engine = destination_engine.new() local cases = { { "v", domain.EndpointPolicy.VISUAL_EXCLUSIVE }, { "V", domain.EndpointPolicy.VISUAL_EXCLUSIVE }, { string.char(0x16), domain.EndpointPolicy.REGULAR }, { "s", domain.EndpointPolicy.REGULAR }, { "S", domain.EndpointPolicy.REGULAR }, { string.char(0x13), domain.EndpointPolicy.REGULAR }, { "n", domain.EndpointPolicy.REGULAR }, { "no", domain.EndpointPolicy.REGULAR }, } for _, case in ipairs(cases) do local context = domain.ModeContext.from_full_mode(case[1]) local kind = context.visual_kind or context.select_kind local selection = kind ~= nil and domain.Selection.active( kind, origin, origin, domain.SelectionOption.EXCLUSIVE ) or domain.Selection.inactive(domain.SelectionOption.EXCLUSIVE) local plan = factory:build_for_context( target_match, "f", context, selection, domain.SearchScope.BUFFER ) same(case[2], plan.endpoint_policy, case[1]) local expected_column = case[2] == domain.EndpointPolicy.VISUAL_EXCLUSIVE and 3 or 2 same( domain.Position.new(1, expected_column), engine:calculate(view, origin, plan, 1, true).endpoint, case[1] ) end local inclusive_visual = factory:build_for_context( target_match, "f", "v", domain.SelectionOption.INCLUSIVE, "buffer" ) same(domain.EndpointPolicy.REGULAR, inclusive_visual.endpoint_policy) local exclusive_backward = factory:build_for_context( target_match, "F", "v", domain.SelectionOption.EXCLUSIVE, "buffer" ) same( domain.Position.new(1, 2), engine:calculate( view, domain.Position.new(1, 3), exclusive_backward, 1, true ).endpoint ) end) test("FIND requires destinations on the strict motion side", function() local view = text_topology.new({ "axa" }, "utf-8") local engine = destination_engine.new() local target_match = target_plan.build(target("a"), matching_policy()) local forward = motion_plan.build(target_match, "f") local backward = motion_plan.build(target_match, "F") same( domain.Position.new(1, 3), engine:calculate( view, domain.Position.new(1, 1), forward, 1, true ).endpoint ) same( domain.Position.new(1, 1), engine:calculate( view, domain.Position.new(1, 3), backward, 1, true ).endpoint ) same( domain.SearchStatus.BOUNDARY_BEFORE_ANY, engine:calculate( view, domain.Position.new(1, 3), forward, 1, false ).status ) same( domain.SearchStatus.BOUNDARY_BEFORE_ANY, engine:calculate( view, domain.Position.new(1, 1), backward, 1, false ).status ) end) test("First TILL moves accept an adjacent stationary destination", function() local engine = destination_engine.new() local forward_view = text_topology.new({ "ab" }, "utf-8") local forward = motion_plan.build( target_plan.build(target("b"), matching_policy()), "t" ) local forward_origin = domain.Position.new(1, 1) local forward_outcome = engine:calculate( forward_view, forward_origin, forward, 1, true ) same(domain.SearchStatus.COMPLETE, forward_outcome.status) same(forward_origin, forward_outcome.endpoint) same(1, forward_outcome.successful_steps) local backward_view = text_topology.new({ "ba" }, "utf-8") local backward = motion_plan.build( target_plan.build(target("b"), matching_policy()), "T" ) local backward_origin = domain.Position.new(1, 2) local backward_outcome = engine:calculate( backward_view, backward_origin, backward, 1, true ) same(domain.SearchStatus.COMPLETE, backward_outcome.status) same(backward_origin, backward_outcome.endpoint) same(1, backward_outcome.successful_steps) end) test("Later TILL moves skip an adjacent stationary destination", function() local engine = destination_engine.new() local forward_view = text_topology.new({ "abxb" }, "utf-8") local forward_origin = domain.Position.new(1, 1) local forward = motion_plan.build( target_plan.build(target("b"), matching_policy()), "t" ) same( forward_origin, engine:calculate( forward_view, forward_origin, forward, 1, true ).endpoint ) same( domain.Position.new(1, 3), engine:calculate( forward_view, forward_origin, forward, 1, false ).endpoint ) local backward_view = text_topology.new({ "bxba" }, "utf-8") local backward_origin = domain.Position.new(1, 4) local backward = motion_plan.build( target_plan.build(target("b"), matching_policy()), "T" ) same( backward_origin, engine:calculate( backward_view, backward_origin, backward, 1, true ).endpoint ) same( domain.Position.new(1, 2), engine:calculate( backward_view, backward_origin, backward, 1, false ).endpoint ) end) test("Sequential counts reuse each accepted destination as origin", function() local view = text_topology.new({ "abxbxb" }, "utf-8") local matched_starts = {} local target_match = domain.TargetPlan.new({ target = target("b"), kind = domain.TargetPlanKind.LITERAL, case_mode = domain.CaseMode.SENSITIVE, matcher = function(character, position) if character == "b" then matched_starts[#matched_starts + 1] = tostring(position) return true end return false end, }) local outcome = destination_engine.calculate( view, domain.Position.new(1, 1), motion_plan.build(target_match, "t"), 3, true ) same(domain.SearchStatus.COMPLETE, outcome.status) same(domain.Position.new(1, 5), outcome.endpoint) same(3, outcome.successful_steps) list_same( { "(1,2)", "(1,2)", "(1,4)", "(1,4)", "(1,6)" }, matched_starts ) end) test("Count calculation stops when its fixed boundary is reached", function() local visits = 0 local view = text_topology.new({ "axaxa" }, "utf-8") local target_match = domain.TargetPlan.new({ target = target("a"), kind = domain.TargetPlanKind.LITERAL, case_mode = domain.CaseMode.SENSITIVE, matcher = function(character) visits = visits + 1 return character == "a" end, }) local plan = motion_plan.build(target_match, "f", "buffer") local engine = destination_engine.new() local origin = domain.Position.new(1, 1) local complete = engine:calculate(view, origin, plan, 2, true) same(domain.SearchStatus.COMPLETE, complete.status) same(domain.Position.new(1, 5), complete.endpoint) same(4, visits) visits = 0 local incomplete = engine:calculate(view, origin, plan, 3, true) falsy(incomplete.complete) same(4, visits) end) test("Complete outcomes report the last destination after every count unit", function() local view = text_topology.new({ "ababa" }, "utf-8") local target_match = target_plan.build(target("a"), matching_policy()) local engine = destination_engine.new() local forward = motion_plan.build(target_match, "f") local backward = motion_plan.build(target_match, "F") local forward_outcome = engine:calculate( view, domain.Position.new(1, 1), forward, domain.Count.new(2), true ) same(domain.SearchStatus.COMPLETE, forward_outcome.status) truthy(forward_outcome.complete) same(domain.Position.new(1, 5), forward_outcome.endpoint) same(2, forward_outcome.successful_steps) local backward_outcome = engine:calculate( view, forward_outcome.endpoint, backward, 2, false ) same(domain.SearchStatus.COMPLETE, backward_outcome.status) truthy(backward_outcome.complete) same(domain.Position.new(1, 1), backward_outcome.endpoint) same(2, backward_outcome.successful_steps) end) test("Partial outcomes retain the last intermediate destination", function() local engine = destination_engine.new() local find_view = text_topology.new({ "axaxa" }, "utf-8") local find_plan = motion_plan.build( target_plan.build(target("a"), matching_policy()), "f" ) local find_partial = engine:calculate( find_view, domain.Position.new(1, 1), find_plan, 3, true ) same(domain.SearchStatus.BOUNDARY_AFTER_PARTIAL, find_partial.status) falsy(find_partial.complete) same(domain.Position.new(1, 5), find_partial.endpoint) same(2, find_partial.successful_steps) local till_view = text_topology.new({ "abxb" }, "utf-8") local till_plan = motion_plan.build( target_plan.build(target("b"), matching_policy()), "t" ) local till_partial = engine:calculate( till_view, domain.Position.new(1, 1), till_plan, 3, true ) same(domain.SearchStatus.BOUNDARY_AFTER_PARTIAL, till_partial.status) same(domain.Position.new(1, 3), till_partial.endpoint) same(2, till_partial.successful_steps) end) test("Boundary-before-any outcomes preserve the original position", function() local engine = destination_engine.new() local view = text_topology.new({ "", "abc" }, "utf-8") local origin = domain.Position.new(1, 1) local missing = motion_plan.build( target_plan.build(target("z"), matching_policy()), "f", "buffer" ) local missing_outcome = engine:calculate(view, origin, missing, 4, true) same(domain.SearchStatus.BOUNDARY_BEFORE_ANY, missing_outcome.status) same(origin, missing_outcome.endpoint) same(0, missing_outcome.successful_steps) local adjacent_view = text_topology.new({ "ab" }, "utf-8") local adjacent_origin = domain.Position.new(1, 1) local repeated_till = motion_plan.build( target_plan.build(target("b"), matching_policy()), "t" ) local adjacent_outcome = engine:calculate( adjacent_view, adjacent_origin, repeated_till, 1, false ) same(domain.SearchStatus.BOUNDARY_BEFORE_ANY, adjacent_outcome.status) same(adjacent_origin, adjacent_outcome.endpoint) same(0, adjacent_outcome.successful_steps) local all_empty = text_topology.new({ "", "" }, "utf-8") local empty_origin = domain.Position.new(2, 1) local empty_outcome = engine:calculate( all_empty, empty_origin, missing, 1, true ) same(domain.SearchStatus.BOUNDARY_BEFORE_ANY, empty_outcome.status) same(empty_origin, empty_outcome.endpoint) end) test("MotionPlanFactory samples live search scope into each plan", function() local host = MemoryHost.new({ configuration = { search_current_line_only = false }, }) local service = policy.new(host) local factory = motion_plan.new(service) local target_match = target_plan.build(target("a"), matching_policy()) local buffer_plan = factory:build(target_match, "f") same(domain.SearchScope.BUFFER, buffer_plan.search_scope) host:set_configuration("search_current_line_only", true) local line_plan = factory:build(target_match, "f") same(domain.SearchScope.CURRENT_LINE, line_plan.search_scope) same(domain.SearchScope.BUFFER, buffer_plan.search_scope) end) test("DestinationEngine matches the required basic TILL sequences", function() local view = text_topology.new({ "poge huga hiyo poyo" }, "utf-8") local engine = destination_engine.new() local till_h = motion_plan.build( target_plan.build(target("h"), matching_policy()), "t" ) local first_h = engine:calculate( view, domain.Position.new(1, 1), till_h, 1, true ) same(domain.Position.new(1, 5), first_h.endpoint) local second_h = engine:calculate(view, first_h.endpoint, till_h, 1, false) same(domain.Position.new(1, 10), second_h.endpoint) local till_o = motion_plan.build( target_plan.build(target("o"), matching_policy()), "t" ) local first_o = engine:calculate( view, domain.Position.new(1, 14), till_o, 1, true ) same(domain.Position.new(1, 16), first_o.endpoint) local second_o = engine:calculate(view, first_o.endpoint, till_o, 1, false) same(domain.Position.new(1, 18), second_o.endpoint) end) test("Buffer-wide destinations cross lines and ignore ambient wrapping", function() local view = text_topology.new({ "foo bar baz", "poge huga hiyo poyo", }, "utf-8") local target_match = target_plan.build(target("a"), matching_policy()) local engine = destination_engine.new() local forward = motion_plan.build(target_match, "f", "buffer") local backward = motion_plan.build(target_match, "F", "buffer") local current = domain.Position.new(1, 1) local expected = { domain.Position.new(1, 6), domain.Position.new(1, 10), domain.Position.new(2, 9), domain.Position.new(1, 10), domain.Position.new(1, 6), } local plans = { forward, forward, forward, backward, backward } for index, plan in ipairs(plans) do local outcome = engine:calculate(view, current, plan, 1, index == 1) same(domain.SearchStatus.COMPLETE, outcome.status) same(expected[index], outcome.endpoint) current = outcome.endpoint end local line_forward = motion_plan.build(target_match, "f", "current_line") local line_last = engine:calculate( view, domain.Position.new(1, 6), line_forward, 1, false ) same(domain.Position.new(1, 10), line_last.endpoint) local line_boundary = engine:calculate( view, line_last.endpoint, line_forward, 1, false ) same(domain.SearchStatus.BOUNDARY_BEFORE_ANY, line_boundary.status) same(line_last.endpoint, line_boundary.endpoint) local saved_wrapscan = vim.o.wrapscan local outcomes = {} for index, wrapscan in ipairs({ false, true }) do vim.o.wrapscan = wrapscan outcomes[index] = engine:calculate( view, domain.Position.new(2, 9), forward, 1, false ) end vim.o.wrapscan = saved_wrapscan for _, outcome in ipairs(outcomes) do same(domain.SearchStatus.BOUNDARY_BEFORE_ANY, outcome.status) same(domain.Position.new(2, 9), outcome.endpoint) end end) test("Destination requests retain their initial match-start region", function() local view = text_topology.new({ "ax", "ax" }, "utf-8") local target_match = target_plan.build(target("x"), matching_policy()) local plan = motion_plan.build( target_match, "f", "current_line", domain.EndpointPolicy.VISUAL_EXCLUSIVE ) local outcome = destination_engine.calculate( view, domain.Position.new(1, 1), plan, 2, true ) same(domain.SearchStatus.BOUNDARY_AFTER_PARTIAL, outcome.status) same(domain.Position.new(2, 1), outcome.endpoint) same(1, outcome.successful_steps) end) test("Multibyte destination sequences stay on valid byte starts", function() local expected_columns = { ["utf-8"] = 4, ["cp932"] = 3, ["euc-jp"] = 3, } for encoding, expected_column in pairs(expected_columns) do local view = text_topology.new({ "a", MIGEMO_HIRAGANA_A .. "a", "a", }, encoding) local target_match = target_plan.build(target("a"), matching_policy()) local forward = motion_plan.build(target_match, "f", "buffer") local backward = motion_plan.build(target_match, "F", "buffer") local plans = { forward, forward, backward, backward } local expected = { domain.Position.new(2, expected_column), domain.Position.new(3, 1), domain.Position.new(2, expected_column), domain.Position.new(1, 1), } local current = domain.Position.new(1, 1) for index, plan in ipairs(plans) do local outcome = destination_engine.calculate( view, current, plan, 1, index == 1 ) same(domain.SearchStatus.COMPLETE, outcome.status, encoding) same(expected[index], outcome.endpoint, encoding) truthy(view:is_character_start(outcome.endpoint), encoding) current = outcome.endpoint end local till = motion_plan.build(target_match, "t", "buffer") local till_outcome = destination_engine.calculate( view, domain.Position.new(1, 1), till, 1, true ) same(domain.Position.new(2, 1), till_outcome.endpoint, encoding) truthy(view:is_valid_cursor_position(till_outcome.endpoint), encoding) end end) test("DestinationEngine leaves cursor and sequence state unchanged", function() local state, transitions = fresh_sequence_state() local target_value = target("a") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target_value) transitions:CommitCommandSuccess("n", domain.Position.new(1, 1), false) local host = MemoryHost.new({ buffer_lines = { "abaca" }, cursor = { line = 1, byte_column = 1 }, effective_encoding = "utf-8", }) local view = text_topology.from_host(host) local origin = host:read_cursor() host:clear_operations() local state_before = state:to_table() local plan = motion_plan.build( target_plan.build(target_value, matching_policy()), "f", "buffer", "regular" ) local plan_before = plan:to_table() local outcome = destination_engine.calculate(view, origin, plan, 2, true) same(domain.SearchStatus.COMPLETE, outcome.status) same(domain.Position.new(1, 5), outcome.endpoint) same(0, #host:operations()) truthy(vim.deep_equal(state_before, state:to_table())) truthy(vim.deep_equal(plan_before, plan:to_table())) same(origin, host:read_cursor()) end) test("Motion execution routes every Visual kind to the Visual path", function() local visual_modes = { "v", "V", string.char(0x16), } for _, mode in ipairs(visual_modes) do same( motion_executor.ExecutionPath.VISUAL, motion_executor.execution_path(mode), mode ) end end) test("Motion execution routes non-Visual contexts to the command path", function() local command_modes = { "n", "no", "nov", "noV", "no" .. string.char(0x16), "s", "S", string.char(0x13), "niI", "normal-extension", } for _, mode in ipairs(command_modes) do same( motion_executor.ExecutionPath.COMMAND, motion_executor.execution_path(mode), mode ) end end) test("Command execution saves its origin before destination calculation", function() local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor = { line = 1, byte_column = 1 }, }) local view = text_topology.from_host(host) local saved_origin local engine = { calculate = function(_, _, origin) saved_origin = origin host:set_cursor(domain.Position.new(1, 3)) return domain.SearchOutcome.boundary_before_any(origin) end, } local executor = motion_executor.new({ host = host, destination_engine = engine, }) local plan = motion_plan.build( target_plan.build(target("z"), matching_policy()), "f" ) local outcome = executor:execute(view, "n", plan, 1, true) same(domain.Position.new(1, 1), saved_origin) same(domain.ActionKind.FAILED_SEARCH, outcome.kind) same(domain.Position.new(1, 1), outcome.position) end) test("Command execution applies the last reached count endpoint", function() local host = MemoryHost.new({ buffer_lines = { "axaxa" }, cursor = { line = 1, byte_column = 1 }, }) local view = text_topology.from_host(host) local executor = motion_executor.new(host) local plan = motion_plan.build( target_plan.build(target("a"), matching_policy()), "f" ) local outcome = executor:execute(view, "n", plan, 3, true) same(domain.ActionKind.FAILED_SEARCH, outcome.kind) same(domain.SearchStatus.BOUNDARY_AFTER_PARTIAL, outcome.search_outcome.status) same(2, outcome.successful_steps) same(domain.Position.new(1, 5), host:read_cursor()) same(domain.Position.new(1, 5), outcome.position) end) test("Incomplete command execution preserves successful history", function() local state, transitions = fresh_sequence_state() local previous_target = target("a") local previous_landing = domain.Position.new(1, 1) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", previous_target, 25) transitions:CommitCommandSuccess("n", previous_landing, false) local host = MemoryHost.new({ buffer_lines = { "axaxa" }, cursor = previous_landing, }) local executor = motion_executor.new(host) local plan = motion_plan.build( target_plan.build(previous_target, matching_policy()), "f" ) local descriptor_before = state:get_previous_descriptor("n") local first_move_before = state:get_first_move("n") local direction_before = state.moved_forward local outcome = executor:execute( text_topology.from_host(host), "n", plan, 3, false ) same(domain.ActionKind.FAILED_SEARCH, outcome.kind) same(domain.Position.new(1, 5), host:read_cursor()) same(previous_landing, state:get_previous_landing("n")) same(descriptor_before, state:get_previous_descriptor("n")) same(first_move_before, state:get_first_move("n")) same(previous_target, state:get_previous_target("n")) same(direction_before, state.moved_forward) same(25, state.repeat_timestamp_ms) end) test("Complete command direction compares destination with saved origin", function() local origin = domain.Position.new(2, 3) truthy( motion_executor.moved_forward(origin, domain.Position.new(2, 4)) ) truthy( motion_executor.moved_forward(origin, domain.Position.new(3, 1)) ) falsy( motion_executor.moved_forward(origin, domain.Position.new(2, 2)) ) falsy(motion_executor.moved_forward(origin, origin)) end) test("A stationary first TILL completion is not forward movement", function() local origin = domain.Position.new(1, 1) falsy(motion_executor.command_moved_forward("t", origin, origin)) local host = MemoryHost.new({ buffer_lines = { "ab" }, cursor = origin, }) local plan = motion_plan.build( target_plan.build(target("b"), matching_policy()), "t" ) local outcome = motion_executor.new(host):execute( text_topology.from_host(host), "n", plan, 1, true ) same(domain.ActionKind.MOVEMENT, outcome.kind) same(origin, outcome.position) same(1, outcome.successful_steps) end) test("Command success requests feedback migration before state commit", function() local state, transitions = fresh_sequence_state() local old_landing = domain.Position.new(1, 1) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target("a")) transitions:CommitCommandSuccess("n", old_landing, false) local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = old_landing, }) local observed local feedback = { migrate_command = function(_, request) observed = { request = request, cursor = host:read_cursor(), landing = state:get_previous_landing("n"), moved_forward = state.moved_forward, } end, } local plan = motion_plan.build( target_plan.build(target("a"), matching_policy()), "f" ) local outcome = motion_executor.new({ host = host, feedback_service = feedback, }):execute(text_topology.from_host(host), "n", plan, 1, false) same(domain.ActionKind.MOVEMENT, outcome.kind) same(old_landing, observed.request.origin) same(domain.Position.new(1, 3), observed.request.destination) same(plan, observed.request.resolved_motion_plan) truthy(observed.request.moved_forward) falsy(observed.request.previous_moved_forward) same(domain.Position.new(1, 3), observed.cursor) same(old_landing, observed.landing) falsy(observed.moved_forward) end) test("Complete command execution commits direction and landing", function() local state, transitions = fresh_sequence_state() local target_value = target("a") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target_value) local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = { line = 1, byte_column = 1 }, }) local plan = motion_plan.build( target_plan.build(target_value, matching_policy()), "f" ) local outcome = motion_executor.new(host):execute( text_topology.from_host(host), "n", plan, 1, true ) same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(1, 3), state:get_previous_landing("n")) falsy(state:get_first_move("n")) truthy(state.moved_forward) truthy(state.moved_forward_initialized) end) test("Visual calculation retains the active selection", function() local origin = domain.Position.new(1, 1) local active = domain.Selection.active( domain.SelectionKind.CHARACTER, origin, origin, domain.SelectionOption.INCLUSIVE ) local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor = origin, mode = "v", selection = active, }) local observed_selection local engine = { calculate = function(_, _, calculation_origin) observed_selection = host:read_selection() return domain.SearchOutcome.boundary_before_any(calculation_origin) end, } local plan = motion_plan.build( target_plan.build(target("z"), matching_policy()), "f" ) local outcome = motion_executor.new({ host = host, destination_engine = engine, }):execute(text_topology.from_host(host), "v", plan, 1, true) same(domain.ActionKind.FAILED_SEARCH, outcome.kind) same(active, observed_selection) same(active, host:read_selection()) end) test("Visual execution applies exact endpoints for every selection kind", function() local origin = domain.Position.new(1, 1) local destination = domain.Position.new(1, 3) local cases = { { 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 host = MemoryHost.new({ buffer_lines = { "abx" }, cursor = origin, mode = case.mode, selection = domain.Selection.active( case.kind, origin, origin, domain.SelectionOption.INCLUSIVE ), }) local plan = motion_plan.build( target_plan.build(target("x"), matching_policy()), "f" ) local outcome = motion_executor.new(host):execute( text_topology.from_host(host), case.mode, plan, 1, true ) local selection = host:read_selection() same(domain.ActionKind.MOVEMENT, outcome.kind, case.mode) same(destination, outcome.position, case.mode) truthy(selection.active, case.mode) same(case.kind, selection.kind, case.mode) same(origin, selection.anchor, case.mode) same(destination, selection.focus, case.mode) same(destination, host:read_cursor(), case.mode) end end) test("Complete Visual execution commits landing and first-move state", function() local state, transitions = fresh_sequence_state() local target_value = target("x") transitions:BeginAcquisition("v", "f") transitions:CommitAcquiredTarget("v", target_value) local origin = domain.Position.new(1, 1) local host = MemoryHost.new({ buffer_lines = { "abx" }, cursor = origin, mode = "v", selection = domain.Selection.active( domain.SelectionKind.CHARACTER, origin, origin, domain.SelectionOption.INCLUSIVE ), }) local plan = motion_plan.build( target_plan.build(target_value, matching_policy()), "f" ) local outcome = motion_executor.new(host):execute( text_topology.from_host(host), "v", plan, 1, true ) same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(1, 3), state:get_previous_landing("v")) falsy(state:get_first_move("v")) same(target_value, state:get_previous_target("v")) end) test("Visual execution preserves command direction and feedback anchor", function() local state, transitions = fresh_sequence_state() transitions:BeginAcquisition("n", "f") transitions:CommitCommandSuccess("n", domain.Position.new(1, 1), true) transitions:AddTargetOverlay("char-visual", "window-1", 1) local target_value = target("x") transitions:BeginAcquisition("v", "f") transitions:CommitAcquiredTarget("v", target_value) local origin = domain.Position.new(1, 1) local host = MemoryHost.new({ buffer_lines = { "a", "x" }, cursor = origin, mode = "v", selection = domain.Selection.active( domain.SelectionKind.CHARACTER, origin, origin, domain.SelectionOption.INCLUSIVE ), }) local migration_count = 0 local feedback = { migrate_command = function() migration_count = migration_count + 1 end, } local plan = motion_plan.build( target_plan.build(target_value, matching_policy()), "f" ) motion_executor.new({ host = host, feedback_service = feedback, }):execute(text_topology.from_host(host), "v", plan, 1, true) truthy(state.moved_forward) truthy(state.moved_forward_initialized) same(0, migration_count) same(1, #state.target_overlays) same("char-visual", state.target_overlays[1].identity) same(1, state.target_overlays[1].anchor_line) same(domain.Position.new(2, 1), state:get_previous_landing("v")) end) test("Forward operator motions toggle characterwise inclusivity", function() for _, descriptor in ipairs({ "f", "t" }) do fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "hoge fuge" }, cursor = { line = 1, byte_column = 1 }, mode = "no", pending_operator = "delete", }) local plan = motion_plan.build( target_plan.build(target("e"), matching_policy()), descriptor ) local view = text_topology.from_host(host) host:clear_operations() local outcome = motion_executor.new(host):execute( view, "no", plan, 1, true ) same(domain.ActionKind.MOVEMENT, outcome.kind, descriptor) truthy(host:operator_inclusive(), descriptor) local effect_operations = {} for _, operation in ipairs(host:operations()) do if operation.operation == "set_operator_inclusive" or operation.operation == "apply_cursor" then effect_operations[#effect_operations + 1] = operation.operation end end list_same({ "set_operator_inclusive", "apply_cursor" }, effect_operations) end end) test("Operator execution preserves FIND and TILL endpoint conventions", function() local source = "hoge fuge piye poye" local cases = { { descriptor = "f", origin = 1, expected = " fuge piye poye", cursor = 1, }, { descriptor = "t", origin = 1, expected = "e fuge piye poye", cursor = 1, }, { descriptor = "F", origin = 19, expected = "hoge fuge piye", cursor = 14, }, { descriptor = "T", origin = 19, expected = "hoge fuge piyee", cursor = 14, }, } for _, case in ipairs(cases) do fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { source }, cursor = { line = 1, byte_column = case.origin }, mode = "no", pending_operator = "delete", }) local plan = motion_plan.build( target_plan.build(target("e"), matching_policy()), case.descriptor ) local outcome = motion_executor.new(host):execute( text_topology.from_host(host), "no", plan, 1, true ) same(domain.ActionKind.MOVEMENT, outcome.kind, case.descriptor) list_same({ case.expected }, host:read_text():lines()) same( domain.Position.new(1, case.cursor), host:read_cursor(), case.descriptor ) if case.descriptor == "F" or case.descriptor == "T" then falsy(host:operator_inclusive(), case.descriptor) local toggles = 0 for _, operation in ipairs(host:operations()) do if operation.operation == "set_operator_inclusive" then toggles = toggles + 1 end end same(0, toggles, case.descriptor) end end end) test("Operator success creates a resolved dot payload", function() fresh_sequence_state() local target_value = target("e") local host = MemoryHost.new({ buffer_lines = { "hoge fuge piye poye" }, cursor = { line = 1, byte_column = 1 }, mode = "no", pending_operator = "delete", }) local plan = motion_plan.build( target_plan.build(target_value, matching_policy()), "f" ) local direct_payload = motion_executor.create_dot_payload(plan) same(domain.Descriptor.FIND_FORWARD, direct_payload.descriptor) same(target_value, direct_payload.target) local outcome = motion_executor.new(host):execute( text_topology.from_host(host), "no", plan, 1, true ) same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(1, 1), outcome.position) same(domain.Position.new(1, 4), outcome.search_outcome.endpoint) truthy(domain.DotPayload.is(outcome.dot_payload)) same(domain.Descriptor.FIND_FORWARD, outcome.dot_payload.descriptor) same(target_value, outcome.dot_payload.target) same(outcome.dot_payload, host:dot_repeat_payload()) end) test("Dot payload replay uses resolved motions and current counts", function() fresh_sequence_state() local target_value = target("e") local host = MemoryHost.new({ buffer_lines = { "hoge fuge piye poye" }, cursor = { line = 1, byte_column = 1 }, mode = "no", pending_operator = "delete", }) local plan = motion_plan.build( target_plan.build(target_value, matching_policy()), "f" ) local executor = motion_executor.new(host) local outcomes = { executor:execute( text_topology.from_host(host), "no", plan, 1, true ), } local buffers = { host:read_text():line(1) } for _ = 1, 3 do outcomes[#outcomes + 1] = host:replay_dot(1) buffers[#buffers + 1] = host:read_text():line(1) end list_same({ " fuge piye poye", " piye poye", " poye", "", }, buffers) for _, outcome in ipairs(outcomes) do same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Descriptor.FIND_FORWARD, outcome.effective_descriptor) same(domain.Position.new(1, 1), outcome.position) truthy(domain.DotPayload.is(outcome.dot_payload)) same(target_value, outcome.dot_payload.target) end fresh_sequence_state() local counted_host = MemoryHost.new({ buffer_lines = { "hoge fuge piye poye" }, cursor = { line = 1, byte_column = 1 }, mode = "no", pending_operator = "delete", }) local counted_executor = motion_executor.new(counted_host) counted_executor:execute( text_topology.from_host(counted_host), "no", plan, 1, true ) local counted = counted_host:replay_dot(2) same(domain.ActionKind.MOVEMENT, counted.kind) same(" poye", counted_host:read_text():line(1)) same(2, counted.successful_steps) end) test("Dot replay consumes target input only for original acquisition", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "hoge fuge piye poye" }, cursor = { line = 1, byte_column = 1 }, mode = "no", pending_operator = "delete", input_packets = { { kind = "text", text = "e" }, }, }) local packet = host:read_input() local acquired_target = domain.TargetValue.character( packet.text, string.byte(packet.text, 1) ) local plan = motion_plan.build( target_plan.build(acquired_target, matching_policy()), "f" ) motion_executor.new(host):execute( text_topology.from_host(host), "no", plan, 1, true ) host:replay_dot(1) host:replay_dot(1) host:replay_dot(1) local input_reads = 0 for _, operation in ipairs(host:operations()) do if operation.operation == "read_input" then input_reads = input_reads + 1 end end same(1, input_reads) same("", host:read_text():line(1)) end) test("Select contexts execute through the command path", function() local state, transitions = fresh_sequence_state() local target_value = target("x") transitions:BeginAcquisition("s", "f") transitions:CommitAcquiredTarget("s", target_value) local origin = domain.Position.new(1, 1) local host = MemoryHost.new({ buffer_lines = { "abx" }, cursor = origin, mode = "s", selection = domain.Selection.active( domain.SelectionKind.CHARACTER, origin, origin, domain.SelectionOption.INCLUSIVE ), }) local plan = motion_plan.build( target_plan.build(target_value, matching_policy()), "f" ) local outcome = motion_executor.new(host):execute( text_topology.from_host(host), "s", plan, 1, true ) same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(1, 3), host:read_cursor()) same(domain.Position.new(1, 3), state:get_previous_landing("s")) truthy(state.moved_forward) local applied_selection = 0 for _, operation in ipairs(host:operations()) do if operation.operation == "apply_selection" then applied_selection = applied_selection + 1 end end same(0, applied_selection) end) test("RepeatResolver reads the normalized contextual landing", function() local _, transitions = fresh_sequence_state() local landing = domain.Position.new(3, 7) transitions:BeginAcquisition("nov", "f") transitions:CommitCommandSuccess("noV", landing, true) local resolver = repeat_resolver.new() same(landing, resolver:previous_landing("no" .. string.char(0x16))) same(nil, resolver:previous_landing("n")) truthy(repeat_resolver.RepeatResolver.is(resolver)) same(resolver, repeat_resolver.new(resolver)) fails(function() resolver.extra = true end, "immutable") end) test("Primary eligibility acquires for missing and different landings", function() local _, transitions = fresh_sequence_state() local resolver = repeat_resolver.new() local current = domain.Position.new(2, 4) same(repeat_resolver.Decision.ACQUIRE, resolver:decide("n", current)) transitions:BeginAcquisition("n", "f") transitions:CommitCommandSuccess("n", domain.Position.new(2, 5), true) same(repeat_resolver.Decision.ACQUIRE, resolver:decide("n", current)) end) test("Primary eligibility acquires while a macro executes", function() local _, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 6) transitions:BeginAcquisition("n", "f") transitions:CommitCommandSuccess("n", landing, true) local resolver = repeat_resolver.new() same( repeat_resolver.Decision.ACQUIRE, resolver:decide("n", landing, domain.MacroState.new("q")) ) end) test("Primary eligibility repeats at a matching numeric landing", function() local _, transitions = fresh_sequence_state() transitions:BeginAcquisition("v", "T") transitions:CommitVisualSuccess("v", domain.Position.new(4, 9)) same( repeat_resolver.Decision.REPEAT, repeat_resolver.decide( "v", { line = 4, byte_column = 9 }, domain.MacroState.new(nil) ) ) end) test("Repeated primary timeout is sampled from live policy", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { repeat_timeout_ms = 75 }, }) local resolver = repeat_resolver.new({ policy = policy.new(host), }) same(75, resolver:sample_repeat_timeout_ms()) host:set_configuration("repeat_timeout_ms", 125) same(125, resolver:sample_repeat_timeout_ms()) fails(function() repeat_resolver.new():sample_repeat_timeout_ms() end, "requires a policy") end) test("Positive repeat timeout reads and stores the current clock", function() local state, transitions = fresh_sequence_state() transitions:SetRepeatTimestamp(25.25) local host = MemoryHost.new({ configuration = { repeat_timeout_ms = 100 }, time_values_ms = { 125.75 }, }) local resolver = repeat_resolver.new({ clock = host, policy = policy.new(host), }) local decision, elapsed_ms = resolver:evaluate_timeout() same(repeat_resolver.Decision.REPEAT, decision) same(100, elapsed_ms) same(125.75, state.repeat_timestamp_ms) local reads = 0 for _, operation in ipairs(host:operations()) do if operation.operation == "read_time_ms" then reads = reads + 1 end end same(1, reads) end) test("Elapsed repeat milliseconds truncate toward zero", function() same(99, repeat_resolver.truncate_elapsed_ms(99.999)) same(-99, repeat_resolver.truncate_elapsed_ms(-99.999)) same(100, repeat_resolver.truncate_elapsed_ms(100)) same(0, repeat_resolver.truncate_elapsed_ms(-0.75)) fails(function() repeat_resolver.truncate_elapsed_ms(0 / 0) end, "finite") end) test("Timely repeat keeps equality and restarts each interval", function() local state, transitions = fresh_sequence_state() transitions:SetRepeatTimestamp(100) local host = MemoryHost.new({ configuration = { repeat_timeout_ms = 100 }, time_values_ms = { 200, 299.9 }, }) local resolver = repeat_resolver.new({ clock = host, policy = policy.new(host), }) local first_decision, first_elapsed = resolver:evaluate_timeout() same(repeat_resolver.Decision.REPEAT, first_decision) same(100, first_elapsed) same(200, state.repeat_timestamp_ms) local second_decision, second_elapsed = resolver:evaluate_timeout() same(repeat_resolver.Decision.REPEAT, second_decision) same(99, second_elapsed) same(299.9, state.repeat_timestamp_ms) end) test("Zero repeat timeout keeps repetition without reading time", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { repeat_timeout_ms = 0 }, time_values_ms = { 1000000 }, }) local resolver = repeat_resolver.new({ clock = host, policy = policy.new(host), }) same(repeat_resolver.Decision.REPEAT, resolver:evaluate_timeout()) for _, operation in ipairs(host:operations()) do falsy(operation.operation == "read_time_ms") end end) test("Expired repeat applies Public Reset and requests acquisition", function() local state, transitions = fresh_sequence_state() local retained_target = target("a") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", retained_target, 100) transitions:CommitCommandSuccess("n", domain.Position.new(1, 2), true) transitions:CacheMigemo("utf-8", { dictionary = true }) transitions:SetHighlightTimer("timer-expired") transitions:AddTargetOverlay("char-current", "window-1", 1) transitions:AddTargetOverlay("char-peer", "window-2", 1) transitions:AddFinalizer("finalizer-retained", "buffer-1") local timestamp_at_reset local observed_window local ordered_transitions = { SetRepeatTimestamp = function(_, time_ms) return transitions:SetRepeatTimestamp(time_ms) end, PublicReset = function(_, current_window) timestamp_at_reset = state.repeat_timestamp_ms observed_window = current_window return transitions:PublicReset(current_window) end, } local host = MemoryHost.new({ configuration = { repeat_timeout_ms = 100 }, time_values_ms = { 201 }, }) local resolver = repeat_resolver.new({ clock = host, policy = policy.new(host), transitions = ordered_transitions, }) local decision, elapsed_ms, cleanup = resolver:evaluate_timeout("window-1") same(repeat_resolver.Decision.ACQUIRE, decision) same(101, elapsed_ms) same(201, timestamp_at_reset) same("window-1", observed_window) same(nil, state:get_previous_descriptor("n")) same(nil, state:get_previous_landing("n")) same(nil, state:get_first_move("n")) same(retained_target, state:get_previous_target("n")) truthy(state.moved_forward) same(0, state.repeat_timestamp_ms) same(0, map_size(state.migemo_cache)) same(nil, state.highlight_timer) same(1, #state.target_overlays) same("window-2", state.target_overlays[1].window) same(1, #state.finalizers) same("timer-expired", cleanup.highlight_timer) same("char-current", cleanup.target_overlays[1].identity) end) test("Relative lower-case keys use the stored descriptor", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { fix_key_direction = false }, }) local resolver = repeat_resolver.new({ policy = policy.new(host) }) for _, stored in ipairs({ "f", "F", "t", "T" }) do for _, pressed in ipairs({ "f", "t" }) do same( domain.Descriptor.from_string(stored), resolver:resolve_primary_direction(stored, pressed), stored .. "/" .. pressed ) end end end) test("Relative upper-case keys swap the stored descriptor", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { fix_key_direction = false }, }) local resolver = repeat_resolver.new({ policy = policy.new(host) }) for _, stored in ipairs({ "f", "F", "t", "T" }) do for _, pressed in ipairs({ "F", "T" }) do local expected = domain.Descriptor.swap(stored) local actual = resolver:resolve_primary_direction(stored, pressed) same(expected, actual, stored .. "/" .. pressed) same(domain.Descriptor.from_string(stored).family, actual.family) end end end) test("Fixed lower-case keys use the lower-case stored form", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { fix_key_direction = true }, }) local resolver = repeat_resolver.new({ policy = policy.new(host) }) for _, stored in ipairs({ "f", "F", "t", "T" }) do for _, pressed in ipairs({ "f", "t" }) do same( domain.Descriptor.lowercase(stored), resolver:resolve_primary_direction(stored, pressed), stored .. "/" .. pressed ) end end end) test("Fixed upper-case keys use the upper-case stored form", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { fix_key_direction = true }, }) local resolver = repeat_resolver.new({ policy = policy.new(host) }) for _, stored in ipairs({ "f", "F", "t", "T" }) do for _, pressed in ipairs({ "F", "T" }) do local expected = domain.Descriptor.uppercase(stored) local actual = resolver:resolve_primary_direction(stored, pressed) same(expected, actual, stored .. "/" .. pressed) same(domain.Descriptor.from_string(stored).family, actual.family) end end end) test("Reverse-request algorithm matches every direction matrix pair", function() local descriptors = { "f", "F", "t", "T" } for _, fixed in ipairs({ false, true }) do local pairs_checked = 0 for _, stored in ipairs(descriptors) do for _, pressed in ipairs(descriptors) do local expected if fixed then expected = domain.Descriptor.is_lowercase(pressed) and domain.Descriptor.lowercase(stored) or domain.Descriptor.uppercase(stored) else expected = domain.Descriptor.is_lowercase(pressed) and domain.Descriptor.from_string(stored) or domain.Descriptor.swap(stored) end local reverse = repeat_resolver.reverse_request(stored, pressed, fixed) local effective = repeat_resolver.primary_direction(stored, pressed, fixed) same(expected, effective, stored .. "/" .. pressed .. "/" .. tostring(fixed)) same(reverse, effective == domain.Descriptor.swap(stored)) pairs_checked = pairs_checked + 1 end end same(16, pairs_checked) end end) test("Explicit same-direction requests use the stored descriptor", function() local _, transitions = fresh_sequence_state() local stored_target = target("h") local resolver = repeat_resolver.new() for _, stored in ipairs({ "f", "F", "t", "T" }) do transitions:BeginAcquisition("nov", stored) transitions:CommitAcquiredTarget("noV", stored_target) local request = resolver:same_direction_request( "no" .. string.char(0x16) ) truthy(domain.ExplicitRepeatRequest.is(request)) same(domain.Descriptor.from_string(stored), request.descriptor) same(request.descriptor, request.effective_descriptor) same(stored_target, request.target) falsy(request.neutral) end local request = repeat_resolver.build_same_direction_request("f", stored_target) same("f", request:to_table().descriptor) fails(function() request.descriptor = domain.Descriptor.FIND_BACKWARD end, "immutable") end) test("Explicit opposite-direction requests swap the stored descriptor", function() local _, transitions = fresh_sequence_state() local stored_target = target("x") local resolver = repeat_resolver.new() for _, stored in ipairs({ "f", "F", "t", "T" }) do transitions:BeginAcquisition("n", stored) transitions:CommitAcquiredTarget("n", stored_target) local request = resolver:opposite_direction_request("n") same(domain.Descriptor.swap(stored), request.descriptor) same(stored_target, request.target) falsy(request.neutral) end end) test("Missing explicit descriptors return a neutral request", function() local _, transitions = fresh_sequence_state() local resolver = repeat_resolver.new() local same_request = resolver:same_direction_request("n") local opposite_request = resolver:opposite_direction_request("n") same(same_request, opposite_request) truthy(domain.ExplicitRepeatRequest.is(same_request)) truthy(same_request.neutral) truthy(same_request:is_neutral()) same(nil, same_request.descriptor) same(nil, same_request.target) truthy(same_request:to_table().neutral) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target("a")) transitions:PublicReset() truthy(resolver:same_direction_request("n").neutral) truthy(resolver:opposite_direction_request("n").neutral) end) test("Missing explicit targets use character code zero", function() local _, transitions = fresh_sequence_state() transitions:BeginAcquisition("n", "T") local resolver = repeat_resolver.new() local same_request = resolver:same_direction_request("n") local opposite_request = resolver:opposite_direction_request("n") for _, request in ipairs({ same_request, opposite_request }) do falsy(request.neutral) same(domain.TargetKind.CODE_FALLBACK, request.target.kind) same(0, request.target.first_code) same("", request.target.value) end same(domain.Descriptor.TILL_BACKWARD, same_request.descriptor) same(domain.Descriptor.TILL_FORWARD, opposite_request.descriptor) end) test("Encoded explicit targets return a neutral request", function() local _, transitions = fresh_sequence_state() local special_target = domain.TargetValue.special_key( string.char(0x80, 0xfd, 0x01) ) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", special_target) local resolver = repeat_resolver.new() local same_request = resolver:same_direction_request("n") local opposite_request = resolver:opposite_direction_request("n") truthy(same_request.neutral) truthy(opposite_request.neutral) same(nil, same_request.target) same(nil, opposite_request.descriptor) end) test("Explicit requests stay outside fixed-direction policy", function() local _, transitions = fresh_sequence_state() local stored_target = target("a") transitions:BeginAcquisition("n", "F") transitions:CommitAcquiredTarget("n", stored_target) local direction_samples = 0 local policy_probe = { sample_timeouts = function() return { repeat_timeout_ms = 0 } end, sample_direction = function() direction_samples = direction_samples + 1 return { fix_key_direction = true } end, } local resolver = repeat_resolver.new({ policy = policy_probe }) local same_request = resolver:same_direction_request("n") local opposite_request = resolver:opposite_direction_request("n") same(0, direction_samples) same(domain.Descriptor.FIND_BACKWARD, same_request.descriptor) same(domain.Descriptor.FIND_FORWARD, opposite_request.descriptor) same(stored_target, same_request.target) same(stored_target, opposite_request.target) same( domain.Descriptor.FIND_FORWARD, resolver:resolve_primary_direction("F", "f") ) same(1, direction_samples) end) test("Default label fallback covers GUI and terminal rendering", function() local definition = feedback_service.default_label_definition() same("red", definition.guifg) same("NONE", definition.guibg) truthy(definition.gui.bold) truthy(definition.gui.underline) same("red", definition.ctermfg) same("NONE", definition.ctermbg) truthy(definition.cterm.bold) truthy(definition.cterm.underline) definition.guifg = "blue" definition.cterm.bold = false local fresh = feedback_service.default_label_definition() same("red", fresh.guifg) truthy(fresh.cterm.bold) end) test("Default label evaluation preserves a colorscheme definition", function() local supplied = { guifg = "blue", gui = { italic = true }, } local host = MemoryHost.new({ highlight_groups = { CleverTeeDefaultLabel = supplied, }, }) local feedback = feedback_service.new(host) local result = feedback:ensure_default_label() same("colorscheme", result.source) falsy(result.applied) same("blue", result.definition.guifg) truthy(result.definition.gui.italic) same("blue", host:highlight_groups().CleverTeeDefaultLabel.guifg) local operations = host:operations() same(1, #operations) same("read_highlight_group", operations[1].operation) end) test("Default label evaluation defines the fallback when absent", function() local host = MemoryHost.new() local feedback = feedback_service.new(host) local result = feedback:ensure_default_label() same("fallback", result.source) truthy(result.applied) local definition = host:highlight_groups().CleverTeeDefaultLabel same("red", definition.guifg) same("NONE", definition.guibg) truthy(definition.gui.bold) truthy(definition.gui.underline) same("red", definition.ctermfg) same("NONE", definition.ctermbg) truthy(definition.cterm.bold) truthy(definition.cterm.underline) local operations = host:operations() same(2, #operations) same("read_highlight_group", operations[1].operation) same("define_highlight_group", operations[2].operation) truthy(operations[2].options.default) end) test("Explicit color configuration forces its feature link", function() local host = MemoryHost.new({ configuration = { mark_cursor_color = "Search", }, highlight_groups = { CleverTeeCursor = { guifg = "blue" }, }, }) local feedback = feedback_service.new(host) local results = feedback:evaluate_feature_links() same("configured", results.CleverTeeCursor.source) same("Search", results.CleverTeeCursor.target) same("Search", host:highlight_groups().CleverTeeCursor.link) local definition for _, operation in ipairs(host:operations()) do if operation.operation == "define_highlight_group" and operation.name == "CleverTeeCursor" then definition = operation end end truthy(definition ~= nil) truthy(definition.options.force) end) test("Feature link evaluation preserves colorscheme CleverTee groups", function() local custom = { guifg = "magenta", gui = { reverse = true }, } local host = MemoryHost.new({ highlight_groups = { CleverTeeChar = custom, }, }) local feedback = feedback_service.new(host) local results = feedback:evaluate_feature_links() same("colorscheme", results.CleverTeeChar.source) falsy(results.CleverTeeChar.applied) same("magenta", results.CleverTeeChar.definition.guifg) truthy(results.CleverTeeChar.definition.gui.reverse) same("magenta", host:highlight_groups().CleverTeeChar.guifg) for _, operation in ipairs(host:operations()) do if operation.operation == "define_highlight_group" then falsy(operation.name == "CleverTeeChar") end end end) test("Missing feature groups receive fallback links", function() local host = MemoryHost.new() local feedback = feedback_service.new(host) local results = feedback:evaluate_feature_links() same("fallback", results.CleverTeeCursor.source) truthy(results.CleverTeeCursor.applied) local groups = host:highlight_groups() same(results.CleverTeeCursor.target, groups.CleverTeeCursor.link) local fallback_definition for _, operation in ipairs(host:operations()) do if operation.operation == "define_highlight_group" and operation.name == "CleverTeeCursor" then fallback_definition = operation end end truthy(fallback_definition ~= nil) truthy(fallback_definition.options.default) same(nil, fallback_definition.options.force) end) test("Cursor feedback falls back to the Cursor group", function() local host = MemoryHost.new() local feedback = feedback_service.new(host) local results = feedback:evaluate_feature_links() same("Cursor", results.CleverTeeCursor.target) same("Cursor", host:highlight_groups().CleverTeeCursor.link) end) test("Character and direct feedback fall back to the default label", function() local host = MemoryHost.new({ configuration = { mark_direct = true }, }) local feedback = feedback_service.new(host) local results = feedback:evaluate_feature_links() local groups = host:highlight_groups() for _, group in ipairs({ "CleverTeeChar", "CleverTeeDirect" }) do same("CleverTeeDefaultLabel", results[group].target) same("CleverTeeDefaultLabel", groups[group].link) end end) test("Feature links are evaluated only while their features are enabled", function() local host = MemoryHost.new({ configuration = { mark_cursor = false, mark_cursor_color = "Search", mark_char = false, mark_char_color = "IncSearch", mark_direct = false, mark_direct_color = "ErrorMsg", }, highlight_groups = { CleverTeeCursor = { guifg = "one" }, CleverTeeChar = { guifg = "two" }, CleverTeeDirect = { guifg = "three" }, }, }) local feedback = feedback_service.new(host) local results = feedback:evaluate_feature_links() same(0, map_size(results)) local groups = host:highlight_groups() same("one", groups.CleverTeeCursor.guifg) same("two", groups.CleverTeeChar.guifg) same("three", groups.CleverTeeDirect.guifg) for _, operation in ipairs(host:operations()) do falsy(operation.operation == "define_highlight_group") falsy(operation.operation == "read_highlight_group") end end) test("Cursor and character overlays use high priority", function() same( feedback_service.Priority.HIGH, feedback_service.overlay_priority("CleverTeeCursor") ) same( feedback_service.Priority.HIGH, feedback_service.overlay_priority("CleverTeeChar") ) fails(function() feedback_service.overlay_priority("Other") end, "unknown feedback overlay group") end) test("Direct overlays use ordinary priority", function() same( feedback_service.Priority.ORDINARY, feedback_service.overlay_priority("CleverTeeDirect") ) end) test("Cursor marker overlays the exact cursor byte position", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "a\227\129\130b" }, cursor = { line = 1, byte_column = 5 }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local cursor = host:read_cursor() local marker = feedback:create_cursor_marker(cursor, "window-1") local highlight = host:highlights()[marker.identity] same("CleverTeeCursor", highlight.group) same("window-1", highlight.window) same(domain.Position.new(1, 5), highlight.position) same(feedback_service.Priority.HIGH, highlight.priority) same(marker.identity, state:temporary_overlay_identities()[1]) truthy(feedback:remove_temporary_overlay(marker)) same(nil, host:highlights()[marker.identity]) same(0, #state:temporary_overlay_identities()) local operation_count = #host:operations() falsy(feedback:remove_temporary_overlay(marker)) same(operation_count, #host:operations()) end) test("Cursor presentation lease restores every prior value", function() local prior = { guicursor = "n-v:block,i:ver25", terminal_cursor_visible = false, terminal_cursor_shape = "beam", hidden = false, ui = { blinkon = 375, blinkoff = 225, }, } local host = MemoryHost.new({ cursor_presentation = prior }) local feedback = feedback_service.new(host) local lease = feedback:create_cursor_presentation_lease() truthy(feedback_service.CursorPresentationLease.is(lease)) truthy(lease.active) truthy(lease.identity ~= nil) local suppressed = host:cursor_presentation() truthy(suppressed.hidden) same(prior.guicursor, suppressed.guicursor) same(prior.terminal_cursor_visible, suppressed.terminal_cursor_visible) same(prior.ui.blinkon, suppressed.ui.blinkon) truthy(lease:release()) falsy(lease.active) local restored = host:cursor_presentation() same(prior.guicursor, restored.guicursor) same(prior.terminal_cursor_visible, restored.terminal_cursor_visible) same(prior.terminal_cursor_shape, restored.terminal_cursor_shape) same(prior.hidden, restored.hidden) same(prior.ui.blinkon, restored.ui.blinkon) same(prior.ui.blinkoff, restored.ui.blinkoff) falsy(lease:release()) end) test("Cursor suppression requires policy and runtime support", function() local policy_disabled = MemoryHost.new({ configuration = { hide_cursor_on_cmdline = false }, cursor_presentation_support = true, }) local disabled_lease = feedback_service.new(policy_disabled) :create_cursor_presentation_lease() falsy(disabled_lease.active) falsy(policy_disabled:cursor_presentation().hidden) local runtime_disabled = MemoryHost.new({ configuration = { hide_cursor_on_cmdline = true }, cursor_presentation_support = false, }) local unsupported_lease = feedback_service.new(runtime_disabled) :create_cursor_presentation_lease() falsy(unsupported_lease.active) falsy(runtime_disabled:cursor_presentation().hidden) local enabled = MemoryHost.new({ configuration = { hide_cursor_on_cmdline = true }, cursor_presentation_support = true, }) local enabled_lease = feedback_service.new(enabled) :create_cursor_presentation_lease() truthy(enabled_lease.active) truthy(enabled:cursor_presentation().hidden) truthy(enabled_lease:release()) local function operation_count(host, name) local count = 0 for _, operation in ipairs(host:operations()) do if operation.operation == name then count = count + 1 end end return count end same(0, operation_count(policy_disabled, "supports_cursor_presentation")) same(0, operation_count(policy_disabled, "suppress_cursor_presentation")) same(1, operation_count(runtime_disabled, "supports_cursor_presentation")) same(0, operation_count(runtime_disabled, "suppress_cursor_presentation")) same(1, operation_count(enabled, "supports_cursor_presentation")) same(1, operation_count(enabled, "suppress_cursor_presentation")) end) test("Direct preview scanning stays on the cursor line", function() local view = text_topology.new({ "abc", "de", "fgh" }, "utf-8") local planner = direct_preview_planner.new() truthy(direct_preview_planner.DirectPreviewPlanner.is(planner)) local records = planner:scan_current_line( view, domain.Position.new(2, 1), domain.Direction.FORWARD ) same(1, #records) same(domain.Position.new(2, 2), records[1].position) same("e", records[1].character) end) test("Direct preview scanning excludes the cursor and opposite side", function() local view = text_topology.new({ "abcde" }, "utf-8") local planner = direct_preview_planner.new() local origin = domain.Position.new(1, 3) local forward = planner:scan_current_line( view, origin, domain.Direction.FORWARD ) same(2, #forward) same(domain.Position.new(1, 4), forward[1].position) same(domain.Position.new(1, 5), forward[2].position) local backward = planner:scan_current_line( view, origin, domain.Direction.BACKWARD ) same(2, #backward) same(domain.Position.new(1, 2), backward[1].position) same(domain.Position.new(1, 1), backward[2].position) end) test("Lower-case descriptors scan direct previews forward", function() local view = text_topology.new({ "abcde" }, "utf-8") local planner = direct_preview_planner.new() local origin = domain.Position.new(1, 2) for _, descriptor in ipairs({ "f", "t" }) do local records = planner:scan_for_descriptor(view, origin, descriptor) same(3, #records) same("c", records[1].character) same(domain.Position.new(1, 3), records[1].position) same("e", records[3].character) same(domain.Position.new(1, 5), records[3].position) end end) test("Upper-case descriptors scan direct previews backward", function() local view = text_topology.new({ "abcde" }, "utf-8") local planner = direct_preview_planner.new() local origin = domain.Position.new(1, 5) for _, descriptor in ipairs({ "F", "T" }) do local records = planner:scan_for_descriptor(view, origin, descriptor) same(4, #records) same("d", records[1].character) same(domain.Position.new(1, 4), records[1].position) same("a", records[4].character) same(domain.Position.new(1, 1), records[4].position) end same( domain.Direction.BACKWARD, direct_preview_planner.direction_for_descriptor("F") ) end) test("Direct preview count normalizes absent and entered values", function() local planner = direct_preview_planner.new() same(1, planner:normalize_count(nil)) same(1, planner:normalize_count(domain.Count.ONE)) same(2, planner:normalize_count(2)) same(3, planner:normalize_count(domain.Count.new(3))) fails(function() planner:normalize_count(0) end, "positive integer") end) test("Default direct previews count each exact character", function() local view = text_topology.new({ "pOge huga Hiyo pOyo" }, "utf-8") local planner = direct_preview_planner.new() local origin = domain.Position.new(1, 1) local function columns(positions) local result = {} for index, position in ipairs(positions) do result[index] = position.byte_column end return result end list_same( { 2, 3, 4, 5, 6, 7, 9, 11, 12, 13, 14, 16 }, columns(planner:plan(view, origin, "f", 1)) ) list_same( { 8, 10, 17, 18, 19 }, columns(planner:plan(view, origin, "f", 2)) ) list_same({ 15 }, columns(planner:plan(view, origin, "f", 3))) end) test("Ignore-case previews count editor-folded character classes", function() local folded = {} local planner = direct_preview_planner.new({ lowercase = function(character) folded[#folded + 1] = character return string.lower(character) end, }) local view = text_topology.new({ "xAaBbC" }, "utf-8") local origin = domain.Position.new(1, 1) local positions = planner:plan(view, origin, "f", 2, { ignore_case = true, smart_case = false, }) same(2, #positions) same(domain.Position.new(1, 3), positions[1]) same(domain.Position.new(1, 5), positions[2]) list_same({ "A", "a", "B", "b", "C" }, folded) local exact = planner:plan(view, origin, "f", 2, { ignore_case = false, smart_case = false, }) same(0, #exact) end) test("Smart-case previews increment every exact character counter", function() local planner = direct_preview_planner.new({ lowercase = string.lower }) local view = text_topology.new({ "xAAcc" }, "utf-8") local positions = planner:plan( view, domain.Position.new(1, 1), "f", 2, { ignore_case = false, smart_case = true } ) same(2, #positions) same(domain.Position.new(1, 3), positions[1]) same(domain.Position.new(1, 5), positions[2]) end) test("Smart-case previews add ASCII uppercase to lowercase counters", function() local planner = direct_preview_planner.new({ lowercase = string.lower }) local view = text_topology.new({ "xAa" }, "utf-8") local positions = planner:plan( view, domain.Position.new(1, 1), "f", 2, { ignore_case = false, smart_case = true } ) same(1, #positions) same(domain.Position.new(1, 3), positions[1]) truthy(direct_preview_planner.is_upper_ascii("A")) falsy(direct_preview_planner.is_upper_ascii("a")) falsy(direct_preview_planner.is_upper_ascii("AA")) end) test("Smart-case previews mark the exact and lowercase union", function() local planner = direct_preview_planner.new({ lowercase = string.lower }) local origin = domain.Position.new(1, 1) local folded_position = planner:plan( text_topology.new({ "xaA" }, "utf-8"), origin, "f", 2, { ignore_case = false, smart_case = true } ) same(1, #folded_position) same(domain.Position.new(1, 3), folded_position[1]) local one_marker = planner:plan( text_topology.new({ "xAA" }, "utf-8"), origin, "f", 2, { ignore_case = false, smart_case = true } ) same(1, #one_marker) same(domain.Position.new(1, 3), one_marker[1]) end) test("TILL direct previews retain target occurrence positions", function() local planner = direct_preview_planner.new({ lowercase = string.lower }) local view = text_topology.new({ "xaxa" }, "utf-8") local origin = domain.Position.new(1, 1) local find_positions = planner:plan(view, origin, "f", 2) local till_positions = planner:plan(view, origin, "t", 2) same(1, #find_positions) same(1, #till_positions) same(domain.Position.new(1, 4), find_positions[1]) same(find_positions[1], till_positions[1]) same( domain.Position.new(1, 4), direct_preview_planner.marker_position("t", domain.Position.new(1, 4)) ) end) test("Direct preview counters ignore Migemo and symbol merging", function() local planner = direct_preview_planner.new({ lowercase = string.lower }) local positions = planner:plan( text_topology.new({ "x;!;!" }, "utf-8"), domain.Position.new(1, 1), "f", 2, { ignore_case = false, smart_case = false, use_migemo = true, chars_match_any_signs = ";", } ) same(2, #positions) same(domain.Position.new(1, 4), positions[1]) same(domain.Position.new(1, 5), positions[2]) local grouping = direct_preview_planner.case_grouping_settings({ ignore_case = true, smart_case = true, use_migemo = true, chars_match_any_signs = ";", }) truthy(grouping.ignore_case) truthy(grouping.smart_case) same(nil, grouping.use_migemo) same(nil, grouping.chars_match_any_signs) end) test("Direct previews return unique valid multibyte byte positions", function() local line = "x\227\129\130x\227\129\132x" local view = text_topology.new({ line }, "utf-8") local planner = direct_preview_planner.new() local forward = planner:plan(view, domain.Position.new(1, 1), "f", nil) local backward = planner:plan(view, domain.Position.new(1, 9), "F", nil) local function assert_columns(expected, positions) same(#expected, #positions) local seen = {} for index, position in ipairs(positions) do same(expected[index], position.byte_column) truthy(view:is_character_start(position)) falsy(seen[position.byte_column]) seen[position.byte_column] = true end end assert_columns({ 2, 5, 6 }, forward) assert_columns({ 6, 5, 2 }, backward) local empty_view = text_topology.new({ "" }, "utf-8") same( 0, #planner:plan(empty_view, domain.Position.new(1, 1), "f", nil) ) fails(function() direct_preview_planner.validate_marker_positions(view, { domain.Position.new(1, 3), }) end, "start an editor character") fails(function() direct_preview_planner.validate_marker_positions(view, { domain.Position.new(1, 2), domain.Position.new(1, 2), }) end, "must be unique") end) test("Feedback creates one ordinary direct marker set", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_direct = true }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local positions = { domain.Position.new(1, 2), domain.Position.new(1, 5), domain.Position.new(1, 6), } local marker = feedback:create_direct_markers(positions, "window-1") local highlight = host:highlights()[marker.identity] same("CleverTeeDirect", highlight.group) same(feedback_service.Priority.ORDINARY, highlight.priority) same(3, #highlight.positions) for index = 1, #positions do same(positions[index], highlight.positions[index]) end same(marker.identity, state:temporary_overlay_identities()[1]) truthy(feedback:remove_temporary_overlay(marker)) same(0, #state:temporary_overlay_identities()) same(nil, feedback:create_direct_markers({}, "window-1")) local evaluation = feedback:evaluate_highlights() same("fallback", evaluation.default_label.source) same( "CleverTeeDefaultLabel", evaluation.feature_links.CleverTeeDirect.target ) end) test("Acquisition accepts complete initiating action inputs", function() local service = acquisition_service.new(MemoryHost.new()) truthy(acquisition_service.AcquisitionService.is(service)) local request = service:request( "T", domain.ModeContext.from_full_mode("nov"), domain.Position.new(3, 5), 2, domain.MacroState.new("q") ) truthy(acquisition_service.AcquisitionRequest.is(request)) same(domain.Descriptor.TILL_BACKWARD, request.descriptor) same(domain.ModeContext.from_full_mode("no"), request.context) same(domain.Position.new(3, 5), request.position) same(2, request.count.value) same(acquisition_service.RepeatedDirection.SAME, request.repeated_direction) truthy(request.macro_state.executing) same("q", request.macro_state.register) local from_table = acquisition_service.AcquisitionRequest.new({ descriptor = "f", context = "n", origin = { line = 1, byte_column = 1 }, macro_state = { register = nil }, }) same(domain.Count.ONE, from_table.count) falsy(from_table.macro_state.executing) end) test("Acquisition starts one temporary resource scope", function() fresh_sequence_state() local service = acquisition_service.new(MemoryHost.new({ input_packets = { { kind = "text", text = "a" } }, })) local result = service:acquire( "f", "n", domain.Position.new(1, 1), 1, nil ) local scope = service:last_temporary_scope() truthy(acquisition_service.AcquisitionResult.is(result)) truthy(acquisition_service.AcquisitionRequest.is(result.request)) truthy(acquisition_service.TemporaryResourceScope.is(scope)) same(result.request, scope.request) falsy(scope.active) same(1, service:started_scope_count()) end) test("Acquisition creates cursor markers only when enabled", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ cursor = { line = 1, byte_column = 4 }, configuration = { mark_cursor = true }, input_packets = { { kind = "text", text = "a" } }, }) local service = acquisition_service.new({ host = host, transitions = transitions, }) service:acquire("F", "n", host:read_cursor(), nil, nil) local marker = service:last_temporary_scope().cursor_marker truthy(marker ~= nil) same(domain.Position.new(1, 4), marker.position) same("window-1", marker.window) same(0, #state:temporary_overlay_identities()) same(nil, host:highlights()[marker.identity]) fresh_sequence_state() local disabled = acquisition_service.new(MemoryHost.new({ configuration = { mark_cursor = false }, input_packets = { { kind = "text", text = "a" } }, })) disabled:acquire("f", "n", domain.Position.new(1, 1), nil, nil) same(nil, disabled:last_temporary_scope().cursor_marker) end) test("Interactive acquisition redraws after cursor marker creation", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_cursor = true, mark_char = false }, input_packets = { { kind = "text", text = "a" } }, }) local service = acquisition_service.new(host) service:acquire("f", "n", domain.Position.new(1, 1), nil, nil) local create_index local redraw_index for index, operation in ipairs(host:operations()) do if operation.operation == "create_highlight" then create_index = index elseif operation.operation == "redraw" then redraw_index = index end end truthy(create_index ~= nil) truthy(redraw_index > create_index) list_same({ "screen" }, host:redraws()) fresh_sequence_state() local macro_host = MemoryHost.new({ configuration = { mark_cursor = true }, input_packets = { { kind = "text", text = "a" } }, }) acquisition_service.new(macro_host):acquire( "f", "n", domain.Position.new(1, 1), nil, domain.MacroState.new("q") ) list_same({ "suppressed" }, macro_host:redraws()) same(0, map_size(macro_host:highlights())) end) test("Interactive acquisition creates enabled direct previews", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "xaxa" }, cursor = { line = 1, byte_column = 1 }, count = 2, configuration = { mark_cursor = false, mark_direct = true, mark_char = false, ignore_case = false, smart_case = false, }, input_packets = { { kind = "text", text = "a" } }, }) local service = acquisition_service.new({ host = host, transitions = transitions, }) service:acquire("t", "n", host:read_cursor(), host:read_count(), nil) local marker = service:last_temporary_scope().direct_marker truthy(marker ~= nil) same("CleverTeeDirect", marker.group) same(1, #marker.positions) same(domain.Position.new(1, 4), marker.positions[1]) same(0, #state:temporary_overlay_identities()) same(nil, host:highlights()[marker.identity]) list_same({ "screen" }, host:redraws()) local create_index local redraw_index for index, operation in ipairs(host:operations()) do if operation.operation == "create_highlight" then create_index = index elseif operation.operation == "redraw" then redraw_index = index end end truthy(redraw_index > create_index) fresh_sequence_state() local macro_host = MemoryHost.new({ buffer_lines = { "xaxa" }, configuration = { mark_cursor = false, mark_direct = true }, input_packets = { { kind = "text", text = "a" } }, }) local macro_service = acquisition_service.new(macro_host) macro_service:acquire( "f", "n", domain.Position.new(1, 1), nil, "q" ) same(nil, macro_service:last_temporary_scope().direct_marker) end) test("Acquisition emits the exact enabled prompt", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_cursor = false, mark_direct = false, show_prompt = true, }, input_packets = { { kind = "text", text = "a" } }, }) acquisition_service.new(host):acquire( "f", "n", domain.Position.new(1, 1), nil, nil ) same("clever-tee: ", acquisition_service.PROMPT) list_same({ "clever-tee: " }, host:prompts()) list_same({ "full" }, host:redraws()) fresh_sequence_state() local macro_host = MemoryHost.new({ configuration = { mark_cursor = false, show_prompt = true, }, input_packets = { { kind = "text", text = "a" } }, }) acquisition_service.new(macro_host):acquire( "f", "n", domain.Position.new(1, 1), nil, "q" ) same(0, #macro_host:prompts()) end) test("Acquisition begins sequence state before input", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_cursor = false }, input_packets = { { kind = "text", text = "a" } }, }) local original_read_input = host.read_input local record_at_read function host:read_input() record_at_read = state:get_context("no") return original_read_input(self) end acquisition_service.new({ host = host, transitions = transitions, }):acquire( "T", "nov", domain.Position.new(1, 1), nil, nil ) same(domain.Descriptor.TILL_BACKWARD, record_at_read.previous_descriptor) truthy(record_at_read.first_move) same(nil, record_at_read.previous_target) local record = state:get_context("no") same("a", record.previous_target.value) end) test("Acquisition reads one raw packet after sequence start", function() local state, transitions = fresh_sequence_state() local packet = domain.InputPacket.raw_bytes({ 0x41 }) local host = MemoryHost.new({ configuration = { mark_cursor = false }, input_packets = { packet }, }) local original_read_input = host.read_input local state_at_read function host:read_input() state_at_read = state:get_context("n") return original_read_input(self) end local service = acquisition_service.new({ host = host, transitions = transitions, }) service:acquire("f", "n", domain.Position.new(1, 1), nil, nil) same(domain.Descriptor.FIND_FORWARD, state_at_read.previous_descriptor) truthy(state_at_read.first_move) same(packet, service:last_temporary_scope().input_packet) local reads = 0 for _, operation in ipairs(host:operations()) do if operation.operation == "read_input" then reads = reads + 1 end end same(1, reads) end) test("Escape acquisition preserves the cursor and returns Escape", function() fresh_sequence_state() local origin = domain.Position.new(2, 3) local host = MemoryHost.new({ buffer_lines = { "abc", "abcd" }, cursor = origin, configuration = { mark_cursor = false, hide_cursor_on_cmdline = false, }, input_packets = { { kind = "special_key", name = "Escape", bytes = { 27 } }, }, }) local service = acquisition_service.new(host) local result = service:acquire("f", "n", origin, nil, nil) truthy(acquisition_service.AcquisitionResult.is(result)) truthy(result:has_outcome()) same(domain.ActionKind.ESCAPE, result.outcome.kind) same(origin, result.outcome.position) same(origin, host:read_cursor()) same(nil, result.target) falsy(service:last_temporary_scope().active) truthy(acquisition_service.is_escape( domain.InputPacket.raw_bytes({ 27 }) )) end) test("Acquisition discards the terminal artifact packet", function() fresh_sequence_state() local artifact = domain.InputPacket.raw_bytes({ 0x80, 0xfd, 0x60 }) local target_packet = domain.InputPacket.text("x") local host = MemoryHost.new({ configuration = { mark_cursor = false }, input_packets = { artifact, target_packet }, }) local service = acquisition_service.new(host) service:acquire("f", "n", domain.Position.new(1, 1), nil, nil) truthy(acquisition_service.is_terminal_artifact(artifact)) falsy(acquisition_service.is_terminal_artifact(target_packet)) same(target_packet, service:last_temporary_scope().input_packet) local reads = 0 for _, operation in ipairs(host:operations()) do if operation.operation == "read_input" then reads = reads + 1 end end same(2, reads) end) test("Acquisition normalizes ordinary input to editor characters", function() fresh_sequence_state() local multibyte = "\227\129\130" local host = MemoryHost.new({ configuration = { mark_cursor = false }, input_packets = { { kind = "text", text = multibyte }, }, }) local service = acquisition_service.new(host) service:acquire("f", "n", domain.Position.new(1, 1), nil, nil) local target = service:last_temporary_scope().acquired_target same(domain.TargetKind.CHARACTER, target.kind) same(multibyte, target.value) same(0x3042, target.first_code) local ascii = acquisition_service.normalize_ordinary_input( domain.InputPacket.raw_bytes({ 65 }) ) same("A", ascii.value) same(65, ascii.first_code) local control = acquisition_service.normalize_ordinary_input( domain.InputPacket.text(string.char(1)) ) same(1, control.first_code) end) test("Acquisition compares first codes with previous-input triggers", function() local state, transitions = fresh_sequence_state() transitions:BeginAcquisition("v", "f") transitions:CommitAcquiredTarget( "v", domain.TargetValue.character("h", 104) ) local host = MemoryHost.new({ configuration = { mark_cursor = false, repeat_last_char_inputs = { "xy", "\r", "\227\129\130tail" }, }, input_packets = { { kind = "text", text = "x" } }, }) local service = acquisition_service.new({ host = host, transitions = transitions, }) local result = service:acquire( "f", "n", domain.Position.new(1, 1), nil, nil ) same("xy", result.previous_input_trigger) same("xy", service:last_temporary_scope().previous_input_trigger) same( domain.TargetValue.character("h", 104), service:last_temporary_scope().cached_target ) same(domain.ModeContext.from_full_mode("v"), result.previous_target_source) same(service:last_temporary_scope().cached_target, result.cached_target) same("x", service:last_temporary_scope().acquired_target.value) same(result.cached_target, service:last_temporary_scope().resolved_target) same(result.cached_target, result.target) same(result.target, state:get_previous_target("n")) same(domain.ModeContext.from_full_mode("n"), state.last_input_context) local trigger, index = acquisition_service.match_previous_input_trigger( 0x3042, { "x", "\227\129\130more" } ) same("\227\129\130more", trigger) same(2, index) same(nil, acquisition_service.match_previous_input_trigger(122, { "xy" })) end) test("Missing previous input preserves cursor and emits exact diagnostic", function() fresh_sequence_state() local origin = domain.Position.new(1, 2) local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor = origin, configuration = { mark_cursor = false, hide_cursor_on_cmdline = false, repeat_last_char_inputs = { "\r" }, }, input_packets = { { kind = "text", text = "\r" } }, }) local service = acquisition_service.new(host) local result = service:acquire("f", "n", origin, nil, nil) truthy(result.missing_previous_input) same(nil, result.target) truthy(result:has_outcome()) same(domain.ActionKind.EMPTY, result.outcome.kind) same(origin, result.outcome.position) falsy(service:last_temporary_scope().active) same(origin, host:read_cursor()) same(1, #host:diagnostics()) same("error", host:diagnostics()[1].level) same("Previous input not found.", host:diagnostics()[1].text) same( acquisition_service.PREVIOUS_INPUT_NOT_FOUND, host:diagnostics()[1].text ) end) test("Positive repeat timeout stores acquisition time", function() local state, transitions = fresh_sequence_state() transitions:SetRepeatTimestamp(17) local host = MemoryHost.new({ time_ms = 84.75, configuration = { mark_cursor = false, repeat_timeout_ms = 50, }, input_packets = { { kind = "text", text = "a" } }, }) local result = acquisition_service.new({ host = host, transitions = transitions, }):acquire("f", "n", domain.Position.new(1, 1), nil, nil) same(84.75, result.acquisition_time_ms) same(84.75, state.repeat_timestamp_ms) fresh_sequence_state() transitions:SetRepeatTimestamp(17) local zero_host = MemoryHost.new({ configuration = { mark_cursor = false, repeat_timeout_ms = 0, }, input_packets = { { kind = "text", text = "a" } }, }) local zero_result = acquisition_service.new({ host = zero_host, transitions = transitions, }):acquire("f", "n", domain.Position.new(1, 1), nil, nil) same(nil, zero_result.acquisition_time_ms) same(17, state.repeat_timestamp_ms) for _, operation in ipairs(zero_host:operations()) do falsy(operation.operation == "read_time_ms") end end) test("Acquisition builds one target plan from live matching policy", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aA" }, configuration = { mark_cursor = false, ignore_case = false, smart_case = false, }, input_packets = { { kind = "text", text = "a" } }, }) local policy_service = policy.new(host) local underlying = target_plan.new({ policy = policy_service }) local build_count = 0 local factory = { build = function(_, ...) build_count = build_count + 1 return underlying:build(...) end, } local service = acquisition_service.new({ host = host, policy = policy_service, target_factory = factory, }) host:set_configuration("ignore_case", true) local result = service:acquire( "f", "n", domain.Position.new(1, 1), nil, nil ) same(1, build_count) truthy(domain.TargetPlan.is(result.target_plan)) same(domain.CaseMode.INSENSITIVE, result.target_plan.case_mode) truthy(result.target_plan:matches("A")) same(result.target, result.target_plan.target) same(result.target_plan, service:last_temporary_scope().target_plan) end) test("Acquisition builds a fresh initiating motion plan", function() fresh_sequence_state() local origin = domain.Position.new(1, 1) local host = MemoryHost.new({ buffer_lines = { "aha" }, cursor = origin, selection = { active = true, kind = "character", anchor = origin, focus = origin, option = "exclusive", }, configuration = { mark_cursor = false, search_current_line_only = true, }, input_packets = { { kind = "text", text = "h" } }, }) local service = acquisition_service.new(host) local first = service:acquire("T", "v", origin, nil, nil) truthy(domain.ResolvedMotionPlan.is(first.motion_plan)) same(first.motion_plan, first.resolved_motion_plan) same(first.motion_plan, service:last_temporary_scope().motion_plan) same(first.target_plan, first.motion_plan.target_plan) same(domain.Descriptor.TILL_BACKWARD, first.motion_plan.descriptor) same(domain.SearchScope.CURRENT_LINE, first.motion_plan.search_scope) same( domain.EndpointPolicy.VISUAL_EXCLUSIVE, first.motion_plan.endpoint_policy ) host:push_input({ kind = "text", text = "h" }) local second = service:acquire("T", "v", origin, nil, nil) falsy(first.motion_plan == second.motion_plan) end) test("Acquisition requests eligible persistent target feedback", function() local _, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aha" }, configuration = { mark_cursor = false, mark_char = true, }, input_packets = { { kind = "text", text = "h" }, { kind = "text", text = "h" }, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local service = acquisition_service.new({ host = host, transitions = transitions, feedback = feedback, }) local origin = domain.Position.new(1, 1) local normal = service:acquire("f", "n", origin, nil, nil) truthy(normal.persistent_feedback_request ~= nil) same(normal.target_plan, normal.persistent_feedback_request.target_plan) same(normal.motion_plan, normal.persistent_feedback_request.motion_plan) same("window-1", normal.persistent_feedback_request.window) truthy(normal.resolved) truthy(normal.completed) local target, target_plan_value, resolved_motion_plan = normal:resolved_values() same(normal.target, target) same(normal.target_plan, target_plan_value) same(normal.motion_plan, resolved_motion_plan) local operator = service:acquire("f", "no", origin, nil, nil) same(nil, operator.persistent_feedback_request) same(1, #feedback:persistent_requests()) truthy(feedback_service.persistent_context_eligible("n")) falsy(feedback_service.persistent_context_eligible("no")) end) test("Completed interactive prompt input requests a full redraw", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "ab" }, configuration = { mark_cursor = false, mark_direct = false, mark_char = false, show_prompt = true, }, input_packets = { { kind = "text", text = "b" } }, }) acquisition_service.new(host):acquire( "f", "n", domain.Position.new(1, 1), nil, nil ) list_same({ "full" }, host:redraws()) local input_index local redraw_index for index, operation in ipairs(host:operations()) do if operation.operation == "read_input" then input_index = index elseif operation.operation == "redraw" and operation.kind == "full" then redraw_index = index end end truthy(redraw_index > input_index) fresh_sequence_state() local escape_host = MemoryHost.new({ configuration = { mark_cursor = false, show_prompt = true, }, input_packets = { { kind = "special_key", name = "Escape", bytes = { 27 } }, }, }) acquisition_service.new(escape_host):acquire( "f", "n", domain.Position.new(1, 1), nil, nil ) same(0, #escape_host:redraws()) end) test("Acquisition releases direct cursor and presentation resources", function() local state = fresh_sequence_state() local prior = { hidden = false, guicursor = "n:block", terminal_cursor_visible = true, } local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor_presentation = prior, configuration = { mark_cursor = true, mark_direct = true, mark_char = false, }, input_packets = { { kind = "text", text = "b" } }, }) local service = acquisition_service.new(host) service:acquire("f", "n", domain.Position.new(1, 1), nil, nil) local scope = service:last_temporary_scope() falsy(scope.active) truthy(scope.direct_marker ~= nil) truthy(scope.cursor_marker ~= nil) same(0, #state:temporary_overlay_identities()) same(nil, host:highlights()[scope.direct_marker.identity]) same(nil, host:highlights()[scope.cursor_marker.identity]) same(prior.hidden, host:cursor_presentation().hidden) same(prior.guicursor, host:cursor_presentation().guicursor) same( prior.terminal_cursor_visible, host:cursor_presentation().terminal_cursor_visible ) local removals = {} local restore_index for index, operation in ipairs(host:operations()) do if operation.operation == "remove_highlight" then removals[#removals + 1] = { index = index, identity = operation.identity } elseif operation.operation == "restore_cursor_presentation" then restore_index = index end end same(2, #removals) same(scope.direct_marker.identity, removals[1].identity) same(scope.cursor_marker.identity, removals[2].identity) truthy(restore_index > removals[2].index) local operation_count = #host:operations() falsy(scope:release()) same(operation_count, #host:operations()) end) test("Acquisition cleanup runs from a finally block", function() local state = fresh_sequence_state() local prior = { hidden = false, guicursor = "n:block", } local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor_presentation = prior, configuration = { mark_cursor = true, mark_direct = true, mark_char = false, show_prompt = true, }, input_packets = { { kind = "error", message = "input failed" } }, }) local service = acquisition_service.new(host) local result = service:acquire( "f", "n", domain.Position.new(1, 1), nil, nil ) same(domain.ActionKind.ERROR, result.outcome.kind) same("input failed", result.outcome.diagnostic) falsy(service:last_temporary_scope().active) same(0, #state:temporary_overlay_identities()) same(0, map_size(host:highlights())) same(prior.hidden, host:cursor_presentation().hidden) same(prior.guicursor, host:cursor_presentation().guicursor) same(1, #host:diagnostics()) same("input failed", host:diagnostics()[1].text) local read_index local remove_index local restore_index for index, operation in ipairs(host:operations()) do if operation.operation == "read_input" then read_index = index elseif operation.operation == "remove_highlight" then remove_index = index elseif operation.operation == "restore_cursor_presentation" then restore_index = index end end truthy(remove_index > read_index) truthy(restore_index > remove_index) same(2, #host:redraws()) end) test("Each macro acquisition consumes and commits input", function() local state = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "abc" }, configuration = { mark_cursor = false, mark_char = false, }, input_packets = { { kind = "text", text = "a" }, { kind = "text", text = "b" }, }, }) local service = acquisition_service.new(host) local origin = domain.Position.new(1, 1) local first = service:acquire("f", "n", origin, nil, "q") same("a", first.target.value) same("a", state:get_previous_target("n").value) local second = service:acquire("t", "n", origin, nil, "q") same("b", second.target.value) same("b", state:get_previous_target("n").value) same(domain.Descriptor.TILL_FORWARD, state:get_previous_descriptor("n")) same(domain.ModeContext.from_full_mode("n"), state.last_input_context) local reads = 0 for _, operation in ipairs(host:operations()) do if operation.operation == "read_input" then reads = reads + 1 end end same(2, reads) end) test("Macro acquisition skips direct planning and all redraws", function() fresh_sequence_state() local direct_calls = 0 local direct_planner = { plan = function() direct_calls = direct_calls + 1 error("macro direct planner must stay idle") end, } local host = MemoryHost.new({ buffer_lines = { "abc" }, configuration = { mark_cursor = true, mark_direct = true, mark_char = false, show_prompt = true, }, input_packets = { { kind = "text", text = "b" } }, }) local feedback = feedback_service.new(host) local service = acquisition_service.new({ host = host, direct_planner = direct_planner, feedback = feedback, }) local result = service:acquire( "f", "n", domain.Position.new(1, 1), nil, "q" ) same("b", result.target.value) same(0, direct_calls) same(nil, service:last_temporary_scope().direct_marker) same(nil, service:last_temporary_scope().cursor_marker) same(0, map_size(host:highlights())) same(0, #feedback:persistent_requests()) list_same({ "suppressed" }, host:redraws()) end) test("Acquisition preserves descriptor writes after input failure", function() local state, transitions = fresh_sequence_state() local old_target = domain.TargetValue.character("z", 122) transitions:BeginAcquisition("n", "T") transitions:CommitAcquiredTarget("n", old_target) transitions:CommitCommandSuccess( "n", domain.Position.new(1, 2), domain.Direction.BACKWARD ) local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor = { line = 1, byte_column = 2 }, configuration = { mark_cursor = false, mark_char = false, }, input_packets = { { kind = "error", message = "read failed" } }, }) local service = acquisition_service.new({ host = host, transitions = transitions, }) local result = service:acquire( "f", "n", domain.Position.new(1, 2), nil, nil ) same(domain.ActionKind.ERROR, result.outcome.kind) same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) truthy(state:get_first_move("n")) same(old_target, state:get_previous_target("n")) end) test("Escape acquisition stays outside highlight timer start", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_cursor = false, mark_char = true, highlight_timeout_ms = 25, }, input_packets = { { kind = "special_key", name = "Escape", bytes = { 27 } }, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local result = acquisition_service.new({ host = host, transitions = transitions, feedback = feedback, }):acquire("f", "n", domain.Position.new(1, 1), nil, nil) same(domain.ActionKind.ESCAPE, result.outcome.kind) same(nil, state.highlight_timer) same(0, #feedback:persistent_requests()) for _, operation in ipairs(host:operations()) do falsy(operation.operation == "start_timer") end end) test("Target-plan errors remain inside acquisition cleanup guard", function() local state, transitions = fresh_sequence_state() local prior = { hidden = false, guicursor = "n:block,i:ver25", terminal_cursor_visible = true, } local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor_presentation = prior, configuration = { mark_cursor = true, mark_direct = true, mark_char = false, show_prompt = true, }, input_packets = { { kind = "text", text = "a" } }, }) local target_factory = { build = function() error("search planning failed", 0) end, } local service = acquisition_service.new({ host = host, transitions = transitions, target_factory = target_factory, }) local result = service:acquire( "f", "n", domain.Position.new(1, 1), nil, nil ) same(domain.ActionKind.ERROR, result.outcome.kind) same("search planning failed", result.outcome.diagnostic) same("a", state:get_previous_target("n").value) same(domain.ModeContext.from_full_mode("n"), state.last_input_context) same(0, #state:temporary_overlay_identities()) same(0, map_size(host:highlights())) same(prior.hidden, host:cursor_presentation().hidden) same(prior.guicursor, host:cursor_presentation().guicursor) same( prior.terminal_cursor_visible, host:cursor_presentation().terminal_cursor_visible ) falsy(service:last_temporary_scope().active) list_same({ "screen", "screen" }, host:redraws()) same(1, #host:diagnostics()) same("search planning failed", host:diagnostics()[1].text) end) test("Persistent feedback reuses the movement TargetPlan", function() fresh_sequence_state() local host = MemoryHost.new() local feedback = feedback_service.new(host) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local persistent = feedback:build_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) same(shared_target, persistent.target_plan) same(shared_target, persistent.motion_plan.target_plan) fails(function() feedback:build_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = target_plan.build(target("b"), matching_policy()), motion_plan = movement, window = "window-1", }) end, "reuse the movement TargetPlan") end) test("Persistent creation rolls back an unowned overlay", function() local state = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "ax" } }) host.register_events = function() error("event registration failed", 0) end local feedback = feedback_service.new(host) local shared_target = target_plan.build(target("x"), matching_policy()) local movement = motion_plan.build(shared_target, "f") fails(function() feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) end, "event registration failed") same(0, #state.target_overlays) same(0, #state.finalizers) same(0, map_size(host:highlights())) same(0, #feedback:persistent_requests()) end) test("Persistent feedback uses its selected descriptor and endpoint policy", function() fresh_sequence_state() local feedback = feedback_service.new(MemoryHost.new()) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local inherited = feedback:build_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) same(domain.Descriptor.FIND_FORWARD, inherited.descriptor) same(domain.EndpointPolicy.REGULAR, inherited.endpoint_policy) local selected = feedback:build_persistent({ context = "v", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, descriptor = "T", endpoint_policy = domain.EndpointPolicy.VISUAL_EXCLUSIVE, window = "window-1", }) same(domain.Descriptor.TILL_BACKWARD, selected.descriptor) same(domain.EndpointPolicy.VISUAL_EXCLUSIVE, selected.endpoint_policy) end) test("Persistent positions apply target predicates and endpoint transforms", function() local view = text_topology.new({ "aA!\\\227\129\130" }, "utf-8") local insensitive = target_plan.build(target("a"), { ignore_case = true, smart_case = false, use_migemo = false, chars_match_any_signs = "", }) local symbols = target_plan.build(target("!"), { ignore_case = false, smart_case = false, use_migemo = false, chars_match_any_signs = "!", }) local backslash = target_plan.build(target("\\"), matching_policy()) local migemo = domain.TargetPlan.new({ target = target("a"), kind = domain.TargetPlanKind.MIGEMO, case_mode = domain.CaseMode.SENSITIVE, matcher = function(character) return character == "\227\129\130" end, }) local function columns(plan) local result = {} for index, position in ipairs(feedback_service.persistent_match_positions( view, 1, plan, "f", domain.EndpointPolicy.REGULAR )) do result[index] = position.byte_column end return result end list_same({ 1, 2 }, columns(insensitive)) list_same({ 3, 4 }, columns(symbols)) list_same({ 4 }, columns(backslash)) list_same({ 5 }, columns(migemo)) same( domain.Position.new(1, 1), feedback_service.persistent_destination( view, domain.Position.new(1, 2), "t", domain.EndpointPolicy.REGULAR ) ) same( domain.Position.new(1, 2), feedback_service.persistent_destination( view, domain.Position.new(1, 2), "t", domain.EndpointPolicy.VISUAL_EXCLUSIVE ) ) end) test("Persistent match starts stay on the anchor line", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "axa", "aya", "aza" } }) local feedback = feedback_service.new(host) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local persistent = feedback:build_persistent({ context = "n", anchor = domain.Position.new(2, 2), target_plan = shared_target, motion_plan = movement, window = "window-1", }) same(2, persistent.match_start_line) same(2, #persistent.positions) same(domain.Position.new(2, 1), persistent.positions[1]) same(domain.Position.new(2, 3), persistent.positions[2]) end) test("Persistent matches share one high-priority character overlay", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "ababa" } }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local persistent = feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) local highlight = host:highlights()[persistent.identity] same("CleverTeeChar", highlight.group) same(feedback_service.Priority.HIGH, highlight.priority) same(shared_target, highlight.target_plan) same(3, #highlight.positions) list_same({ persistent.identity }, state:target_overlay_identities()) same(1, map_size(host:highlights())) end) test("Persistent feedback accepts every eligible mode context", function() local eligible = { "n", "v", "V", string.char(0x16), "s", "S", string.char(0x13), "cv", "cvr", } for _, context in ipairs(eligible) do truthy( feedback_service.persistent_context_eligible(context), "expected eligible context " .. string.format("%q", context) ) end for _, context in ipairs({ "no", "nov", "c", "niI" }) do falsy( feedback_service.persistent_context_eligible(context), "expected ineligible context " .. context ) end end) test("Operator acquisition keeps feedback temporary", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "abc" }, configuration = { mark_cursor = true, mark_direct = true, mark_char = true, }, input_packets = { { kind = "text", text = "b" } }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local service = acquisition_service.new({ host = host, transitions = transitions, feedback = feedback, }) local result = service:acquire( "f", "no", domain.Position.new(1, 1), nil, nil ) same(nil, result.persistent_feedback_request) truthy(service:last_temporary_scope().cursor_marker ~= nil) truthy(service:last_temporary_scope().direct_marker ~= nil) same(0, #state.target_overlays) same(0, #state.temporary_overlays) same(0, map_size(host:highlights())) end) test("Persistent feedback stores overlay identity and anchor", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "x", "axa" } }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local persistent = feedback:request_persistent({ context = "n", anchor = domain.Position.new(2, 2), target_plan = shared_target, motion_plan = movement, window = "window-anchor", }) local resource = state.target_overlays[1] same(persistent.identity, resource.identity) same("window-anchor", resource.window) same("CleverTeeChar", resource.group) same(2, persistent.anchor_line) same(2, host:highlights()[persistent.identity].anchor_line) same(2, resource.anchor_line) end) test("Persistent creation registers current-buffer finalizers", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer = "buffer-finalized", buffer_lines = { "aba" }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local persistent = feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) local registration = host:event_registrations()[persistent.finalizers.identity] same("buffer-finalized", persistent.finalizers.buffer) same("buffer-finalized", state.finalizers[1].buffer) list_same(feedback_service.FINALIZER_EVENTS, registration.names) same("buffer-finalized", registration.options.buffer) truthy(registration.active) end) test("Persistent feedback owns one finalizer set", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" } }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local function create() return feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) end local first = create() local second = create() same(first.finalizers.identity, second.finalizers.identity) same(1, #state.finalizers) host:set_buffer("buffer-2") local third = create() falsy(first.finalizers.identity == third.finalizers.identity) same(1, #state.finalizers) same(third.finalizers.identity, state.finalizers[1].identity) same("buffer-2", state.finalizers[1].buffer) local registrations = host:event_registrations() falsy(registrations[first.finalizers.identity].active) truthy(registrations[third.finalizers.identity].active) end) test("CursorMoved compares the actual cursor with the last-input landing", function() local state, transitions = fresh_sequence_state() local landing = domain.Position.new(2, 4) transitions:BeginAcquisition("v", "f") transitions:CommitAcquiredTarget("v", target("a")) transitions:CommitVisualSuccess("v", landing) transitions:CommitCommandSuccess("n", domain.Position.new(1, 2), true) local host = MemoryHost.new({ cursor = landing }) local feedback = feedback_service.new({ host = host, state = state, transitions = transitions, }) local equal = feedback:handle_finalizer_event("CursorMoved") same(domain.ModeContext.from_full_mode("v"), equal.context) same(landing, equal.expected) same(landing, equal.actual) truthy(equal.equal) host:set_cursor(domain.Position.new(2, 5)) local different = feedback:handle_finalizer_event("CursorMoved") same(landing, different.expected) same(domain.Position.new(2, 5), different.actual) falsy(different.equal) end) test("Matching CursorMoved preserves the active sequence", function() local state, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 3) local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = landing, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target("a")) feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) transitions:CommitCommandSuccess("n", landing, true) local before = state:snapshot() host:deliver_event("CursorMoved", { cursor = landing }) same(before.previous_descriptor[domain.ModeContext.from_full_mode("n")], state:get_previous_descriptor("n")) same(landing, state:get_previous_landing("n")) same(1, #state.target_overlays) same(1, #state.finalizers) end) test("Different CursorMoved applies full finalization", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = { line = 1, byte_column = 2 }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target("a")) transitions:CommitCommandSuccess("n", domain.Position.new(1, 3), true) transitions:BeginAcquisition("v", "t") transitions:CommitVisualSuccess("v", domain.Position.new(1, 2)) feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) host:deliver_event("CursorMoved", {}) same(nil, state:get_previous_landing("n")) same(nil, state:get_previous_landing("v")) falsy(state.moved_forward) same(0, #state.target_overlays) same(0, #state.finalizers) end) test("Insert and text events finalize feedback directly", function() for _, event_name in ipairs({ "InsertEnter", "TextChanged" }) do local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" } }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target("a")) transitions:CommitCommandSuccess( "n", domain.Position.new(1, 3), domain.Direction.FORWARD ) feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) host:deliver_event(event_name, {}) same(nil, state:get_previous_landing("n"), event_name) falsy(state.moved_forward, event_name) same(0, #state.target_overlays, event_name) same(0, #state.finalizers, event_name) end end) test("Full finalization removes owned registrations and window overlays", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" } }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local current = feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) local peer = host:create_highlight({ group = "CleverTeeChar", window = "window-2", positions = {}, priority = feedback_service.Priority.HIGH, }) transitions:AddTargetOverlay(peer, "window-2", 1) local cleanup = feedback:full_finalize("window-1") same(1, #cleanup.finalizers) same(current.finalizers.identity, cleanup.finalizers[1].identity) falsy(host:event_registrations()[current.finalizers.identity].active) same(nil, host:highlights()[current.identity]) truthy(host:highlights()[peer] ~= nil) same(1, #state.target_overlays) same(peer, state.target_overlays[1].identity) same(0, #state.finalizers) end) test("Full finalization cancels the active highlight timer", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new() local feedback = feedback_service.new({ host = host, transitions = transitions, }) local timer = host:start_timer(25, function() end) transitions:SetHighlightTimer(timer) local cleanup = feedback:full_finalize("window-1") same(timer, cleanup.highlight_timer) same(nil, state.highlight_timer) falsy(host:timers()[timer].active) local stopped for _, operation in ipairs(host:operations()) do if operation.operation == "stop_timer" and operation.identity == timer then stopped = operation end end truthy(stopped ~= nil) truthy(stopped.stopped) end) test("Full feedback finalization clears every landing and direction", function() local state, transitions = fresh_sequence_state() local target_value = target("a") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target_value) transitions:CommitCommandSuccess("n", domain.Position.new(1, 2), true) transitions:BeginAcquisition("v", "t") transitions:CommitVisualSuccess("v", domain.Position.new(2, 3)) transitions:BeginAcquisition("s", "F") transitions:CommitCommandSuccess("s", domain.Position.new(3, 1), false) truthy(state.moved_forward_initialized) feedback_service.new({ host = MemoryHost.new(), transitions = transitions, }):full_finalize("window-1") for _, context in ipairs({ "n", "v", "s" }) do same(nil, state:get_previous_landing(context), context) end falsy(state.moved_forward) truthy(state.moved_forward_initialized) end) test("Full feedback finalization preserves reusable sequence data", function() local state, transitions = fresh_sequence_state() local normal_target = target("a") local visual_target = target("b") local dictionary = { name = "cached-migemo" } transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", normal_target) transitions:BeginAcquisition("v", "T") transitions:CommitAcquiredTarget("v", visual_target) transitions:CommitVisualSuccess("v", domain.Position.new(2, 2)) transitions:CacheMigemo("utf-8", dictionary) local input_context = state.last_input_context feedback_service.new({ host = MemoryHost.new(), transitions = transitions, }):full_finalize("window-1") same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) same(domain.Descriptor.TILL_BACKWARD, state:get_previous_descriptor("v")) same(normal_target, state:get_previous_target("n")) same(visual_target, state:get_previous_target("v")) truthy(state:get_first_move("n")) falsy(state:get_first_move("v")) same(input_context, state.last_input_context) same(dictionary, state:get_migemo("utf-8")) end) test("Command feedback migration compares origin and destination lines", function() same( nil, feedback_service.command_migration_reason({ origin = domain.Position.new(2, 1), destination = domain.Position.new(2, 7), }) ) same( feedback_service.MigrationReason.LINE_CHANGE, feedback_service.command_migration_reason({ origin = domain.Position.new(2, 7), destination = domain.Position.new(3, 1), }) ) same( nil, feedback_service.command_migration_reason({ origin = domain.Position.new(2, 7), destination = domain.Position.new(3, 1), outcome = { complete = false }, }) ) end) test("Cross-line command movement rebuilds feedback at destination", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "a", "xax" } }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local first = feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) local migration = feedback:migrate_command({ context = domain.ModeContext.from_full_mode("n"), origin = domain.Position.new(1, 1), destination = domain.Position.new(2, 2), outcome = domain.SearchOutcome.complete(domain.Position.new(2, 2), 1), resolved_motion_plan = movement, first_move = true, moved_forward = true, }) truthy(migration.migrated) same(feedback_service.MigrationReason.LINE_CHANGE, migration.reason) same(2, migration.overlay.anchor.line) same(2, migration.overlay.match_start_line) same(nil, host:highlights()[first.identity]) truthy(host:highlights()[migration.overlay.identity] ~= nil) same(1, #state.target_overlays) same(2, state.target_overlays[1].anchor_line) end) test("TILL migration requires a repeated move", function() local shared_target = target_plan.build(target("a"), matching_policy()) local till = motion_plan.build(shared_target, "t") local find = motion_plan.build(shared_target, "f") falsy(feedback_service.repeated_till_migration_candidate({ resolved_motion_plan = till, first_move = true, })) truthy(feedback_service.repeated_till_migration_candidate({ resolved_motion_plan = till, first_move = false, })) falsy(feedback_service.repeated_till_migration_candidate({ resolved_motion_plan = find, first_move = false, })) end) test("Repeated TILL migration compares movement directions", function() local shared_target = target_plan.build(target("a"), matching_policy()) local till = motion_plan.build(shared_target, "T") local request = { resolved_motion_plan = till, first_move = false, moved_forward = false, previous_moved_forward = true, } truthy(feedback_service.till_direction_changed(request)) request.previous_moved_forward = false falsy(feedback_service.till_direction_changed(request)) request.first_move = true falsy(feedback_service.till_direction_changed(request)) end) test("Direction-changing repeated TILL rebuilds feedback", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "ababa" } }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("b"), matching_policy()) local forward = motion_plan.build(shared_target, "t") local backward = motion_plan.build(shared_target, "T") local first = feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = forward, window = "window-1", }) local migration = feedback:migrate_command({ context = "n", origin = domain.Position.new(1, 3), destination = domain.Position.new(1, 1), outcome = domain.SearchOutcome.complete(domain.Position.new(1, 1), 1), resolved_motion_plan = backward, first_move = false, moved_forward = false, previous_moved_forward = true, }) truthy(migration.migrated) same( feedback_service.MigrationReason.TILL_DIRECTION_CHANGE, migration.reason ) falsy(first.identity == migration.overlay.identity) same(domain.Descriptor.TILL_BACKWARD, migration.overlay.descriptor) local rebuilt = host:highlights()[migration.overlay.identity] same(domain.Descriptor.TILL_BACKWARD, rebuilt.descriptor) same(domain.EndpointPolicy.REGULAR, rebuilt.endpoint_policy) same(domain.Position.new(1, 3), rebuilt.positions[1]) same(domain.Position.new(1, 5), rebuilt.positions[2]) same(nil, host:highlights()[first.identity]) same(1, #state.target_overlays) end) test("Visual movement retains its persistent feedback anchor", function() local state, transitions = fresh_sequence_state() local origin = domain.Position.new(1, 1) local host = MemoryHost.new({ buffer_lines = { "a", "x" }, cursor = origin, mode = "v", selection = domain.Selection.active( domain.SelectionKind.CHARACTER, origin, origin, domain.SelectionOption.INCLUSIVE ), }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("x"), matching_policy()) local movement = motion_plan.build(shared_target, "f") transitions:BeginAcquisition("v", "f") transitions:CommitAcquiredTarget("v", target("x")) local persistent = feedback:request_persistent({ context = "v", anchor = origin, target_plan = shared_target, motion_plan = movement, window = "window-1", }) local token = host:begin_action_transition() local outcome = motion_executor.new({ host = host, feedback_service = feedback, transitions = transitions, }):execute(text_topology.from_host(host), "v", movement, 1, true) host:commit_action_transition(token) same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(2, 1), state:get_previous_landing("v")) same(persistent.identity, state.target_overlays[1].identity) same(1, state.target_overlays[1].anchor_line) truthy(host:highlights()[persistent.identity] ~= nil) end) test("Highlight timeout requires marks delay and timer support", function() for _, mark_char in ipairs({ false, true }) do for _, delay in ipairs({ 0, 25 }) do for _, supported in ipairs({ false, true }) do fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_char = mark_char, highlight_timeout_ms = delay, }, timer_support = supported, }) local actual = feedback_service.new(host):highlight_timer_delay() local expected = mark_char and delay > 0 and supported and delay or nil same( expected, actual, table.concat({ tostring(mark_char), tostring(delay), tostring(supported), }, ":") ) end end end end) test("A new primary timer stops the prior timer first", function() local _, transitions = fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_char = true, highlight_timeout_ms = 25, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local first = feedback:start_highlight_timer() host:clear_operations() local second = feedback:start_highlight_timer() local stop_index local start_index for index, operation in ipairs(host:operations()) do if operation.operation == "stop_timer" and operation.identity == first then stop_index = index elseif operation.operation == "start_timer" and operation.identity == second then start_index = index end end truthy(first ~= second) truthy(stop_index ~= nil) truthy(start_index ~= nil) truthy(stop_index < start_index) falsy(host:timers()[first].active) truthy(host:timers()[second].active) end) test("Each new highlight timer identity is stored in sequence state", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_char = true, highlight_timeout_ms = 40, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local first = feedback:start_highlight_timer() same(first, state.highlight_timer) local second = feedback:start_highlight_timer() same(second, state.highlight_timer) falsy(first == second) same(40, host:timers()[second].delay_ms) end) test("Timer callbacks compare their identity with current state", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_char = true, highlight_timeout_ms = 10, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local stale = feedback:start_highlight_timer() local current = feedback:start_highlight_timer() falsy(feedback:handle_highlight_timer(stale)) same(current, state.highlight_timer) truthy(feedback:handle_highlight_timer(current)) same(nil, state.highlight_timer) end) test("Current timer callback clears timer and character overlays", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, configuration = { mark_char = true, highlight_timeout_ms = 30, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local persistent = feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) local timer = feedback:start_highlight_timer("window-1") truthy(host:fire_timer(timer)) same(nil, state.highlight_timer) same(0, #state.target_overlays) same(nil, host:highlights()[persistent.identity]) same(1, #state.finalizers) falsy(host:timers()[timer].active) end) test("Timer cleanup preserves primary movement history", function() local state, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 3) local target_value = target("a") local host = MemoryHost.new({ buffer_lines = { "aba" }, configuration = { mark_char = true, highlight_timeout_ms = 20, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local shared_target = target_plan.build(target_value, matching_policy()) local movement = motion_plan.build(shared_target, "f") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target_value) transitions:CommitCommandSuccess("n", landing, true) feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) local timer = feedback:start_highlight_timer("window-1") host:fire_timer(timer) same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) same(target_value, state:get_previous_target("n")) same(landing, state:get_previous_landing("n")) falsy(state:get_first_move("n")) truthy(state.moved_forward) same(domain.ModeContext.from_full_mode("n"), state.last_input_context) end) test("Primary restoration samples marker and context eligibility", function() local _, transitions = fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_char = false }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) falsy(feedback:primary_restoration_active("n", "window-1")) host:set_configuration("mark_char", true) falsy(feedback:primary_restoration_active("no", "window-1")) truthy(feedback:primary_restoration_active("n", "window-1")) truthy(feedback:primary_restoration_active("v", "window-1")) truthy(feedback:primary_restoration_active("s", "window-1")) end) test("Primary restoration combines action target with stored motion policy", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aA" }, configuration = { mark_char = true }, }) local feedback = feedback_service.new(host) local action_target = target_plan.build(target("a"), { ignore_case = true, smart_case = false, use_migemo = false, chars_match_any_signs = "", }) local action_motion = motion_plan.build(action_target, "f") local restoration = feedback:build_primary_restoration({ context = "v", anchor = domain.Position.new(1, 1), target_plan = action_target, motion_plan = action_motion, stored_descriptor = "T", endpoint_policy = domain.EndpointPolicy.VISUAL_EXCLUSIVE, window = "window-1", }) same(action_target, restoration.target_plan) same(action_target, restoration.motion_plan.target_plan) same(domain.Descriptor.TILL_BACKWARD, restoration.descriptor) same(domain.Descriptor.TILL_BACKWARD, restoration.motion_plan.descriptor) same(domain.EndpointPolicy.VISUAL_EXCLUSIVE, restoration.endpoint_policy) same( domain.EndpointPolicy.VISUAL_EXCLUSIVE, restoration.motion_plan.endpoint_policy ) end) test("Primary repetition restores feedback before movement", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = { line = 1, byte_column = 1 }, configuration = { mark_char = true }, emit_movement_events = false, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local action_target = target_plan.build(target("a"), matching_policy()) local action_motion = motion_plan.build(action_target, "f") host:clear_operations() local restored = feedback:restore_primary({ context = "n", anchor = domain.Position.new(1, 1), target_plan = action_target, motion_plan = action_motion, stored_descriptor = "f", endpoint_policy = domain.EndpointPolicy.REGULAR, window = "window-1", }) host:apply_cursor(domain.Position.new(1, 3), { descriptor = domain.Descriptor.FIND_FORWARD, origin = domain.Position.new(1, 1), }) truthy(restored ~= nil) same(restored.identity, state.target_overlays[1].identity) local create_index local movement_index for index, operation in ipairs(host:operations()) do if operation.operation == "create_highlight" then create_index = index elseif operation.operation == "apply_cursor" then movement_index = index end end truthy(create_index < movement_index) end) test("Resolved primary actions refresh the highlight timer", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_char = true, highlight_timeout_ms = 15, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local resolved = target("a") local fresh = feedback:refresh_primary(resolved, "window-1") same(fresh, state.highlight_timer) local repeated = feedback:refresh_primary(resolved, "window-1") same(repeated, state.highlight_timer) falsy(fresh == repeated) falsy(host:timers()[fresh].active) truthy(host:timers()[repeated].active) same(nil, feedback:refresh_primary(nil, "window-1")) same(repeated, state.highlight_timer) end) test("Explicit motion execution bypasses primary feedback operations", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = { line = 1, byte_column = 1 }, emit_movement_events = false, }) local restore_count = 0 local refresh_count = 0 local migration_count = 0 local feedback = { migrate_command = function() migration_count = migration_count + 1 end, restore_primary = function() restore_count = restore_count + 1 end, refresh_primary = function() refresh_count = refresh_count + 1 end, } local target_value = target("a") local plan = motion_plan.build( target_plan.build(target_value, matching_policy()), "f" ) local outcome = motion_executor.new({ host = host, feedback_service = feedback, }):execute(text_topology.from_host(host), "n", plan, 1, false) same(domain.ActionKind.MOVEMENT, outcome.kind) same(1, migration_count) same(0, restore_count) same(0, refresh_count) end) test("Activation installs eager hooks from sampled setup policy", function() fresh_sequence_state() local enabled_host = MemoryHost.new({ configuration = { clean_labels_eagerly = true }, }) local enabled = feedback_service.new(enabled_host) local activation = enabled:activate() truthy(activation.clean_labels_eagerly) truthy(activation.eager_registration ~= nil) local registration = enabled_host:event_registrations()[ activation.eager_registration ] list_same(feedback_service.EAGER_EVENTS, registration.names) same("eager", registration.options.lifecycle) enabled_host:set_configuration("clean_labels_eagerly", false) local retained = enabled:activate() same(activation.eager_registration, retained.eager_registration) truthy(retained.clean_labels_eagerly) fresh_sequence_state() local disabled_host = MemoryHost.new({ configuration = { clean_labels_eagerly = false }, }) local disabled = feedback_service.new(disabled_host):activate() falsy(disabled.clean_labels_eagerly) same(nil, disabled.eager_registration) same(0, map_size(disabled_host:event_registrations())) end) test("Every eager event samples live character-marker policy", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { clean_labels_eagerly = true, mark_char = false, }, }) local feedback = feedback_service.new(host) feedback:activate() for _, event_name in ipairs(feedback_service.EAGER_EVENTS) do host:set_configuration("mark_char", false) host:deliver_event(event_name, { window = "window-1" }) local disabled = feedback:last_eager_decision() same(event_name, disabled.event) falsy(disabled.mark_char) host:set_configuration("mark_char", true) host:deliver_event(event_name, { window = "window-1" }) local enabled = feedback:last_eager_decision() same(event_name, enabled.event) truthy(enabled.mark_char) end end) test("Enabled eager events remove overlays and cancel timers", function() for _, event_name in ipairs(feedback_service.EAGER_EVENTS) do local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, configuration = { clean_labels_eagerly = true, mark_char = true, highlight_timeout_ms = 25, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local activation = feedback:activate() local shared_target = target_plan.build(target("a"), matching_policy()) local movement = motion_plan.build(shared_target, "f") local persistent = feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) local timer = feedback:start_highlight_timer("window-1") host:deliver_event(event_name, { window = "window-1" }) local decision = feedback:last_eager_decision() truthy(decision.cleaned, event_name) same(timer, decision.cleanup.highlight_timer, event_name) same(nil, state.highlight_timer, event_name) same(0, #state.target_overlays, event_name) same(nil, host:highlights()[persistent.identity], event_name) falsy(host:timers()[timer].active, event_name) truthy( host:event_registrations()[activation.eager_registration].active, event_name ) end end) test("MemoryHost limits scoped events to their buffer", function() local host = MemoryHost.new({ buffer = "buffer-1" }) local deliveries = 0 host:register_events("TextChanged", function() deliveries = deliveries + 1 end, { buffer = "buffer-1" }) host:deliver_event("TextChanged", {}) same(1, deliveries) host:set_buffer("buffer-2") host:deliver_event("TextChanged", {}) same(1, deliveries) host:deliver_event("TextChanged", { buffer = "buffer-1" }) same(2, deliveries) end) test("Eager cleanup preserves history and finalizer registrations", function() local state, transitions = fresh_sequence_state() local target_value = target("a") local landing = domain.Position.new(1, 3) local dictionary = { name = "utf-8" } local host = MemoryHost.new({ buffer_lines = { "aba" }, configuration = { clean_labels_eagerly = true, mark_char = true, highlight_timeout_ms = 25, }, }) local feedback = feedback_service.new({ host = host, transitions = transitions, }) local activation = feedback:activate() local shared_target = target_plan.build(target_value, matching_policy()) local movement = motion_plan.build(shared_target, "f") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target_value, 87) transitions:CommitCommandSuccess("n", landing, true) transitions:CacheMigemo("utf-8", dictionary) local persistent = feedback:request_persistent({ context = "n", anchor = domain.Position.new(1, 1), target_plan = shared_target, motion_plan = movement, window = "window-1", }) feedback:start_highlight_timer("window-1") host:deliver_event("WinLeave", { window = "window-1" }) same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) same(target_value, state:get_previous_target("n")) same(landing, state:get_previous_landing("n")) falsy(state:get_first_move("n")) truthy(state.moved_forward) truthy(state.moved_forward_initialized) same(domain.ModeContext.from_full_mode("n"), state.last_input_context) same(dictionary, state:get_migemo("utf-8")) same(87, state.repeat_timestamp_ms) same(1, #state.finalizers) same(persistent.finalizers.identity, state.finalizers[1].identity) truthy(host:event_registrations()[persistent.finalizers.identity].active) truthy(host:event_registrations()[activation.eager_registration].active) same(0, #state.target_overlays) same(nil, state.highlight_timer) end) test("Core composition injects host case conversion into matching", function() fresh_sequence_state() local conversions = {} local host = MemoryHost.new({ buffer_lines = { "xA" }, cursor = { line = 1, byte_column = 1 }, lowercase = function(value) conversions[#conversions + 1] = value if value == "a" or value == "A" then return "folded-a" end return value end, input_packets = { { kind = "text", text = "a" } }, configuration = { ignore_case = true, mark_cursor = false, mark_char = false, }, emit_movement_events = false, }) local outcome = composition_root.new({ host = host }) :invoke_descriptor("f") same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(1, 2), outcome.position) truthy(#conversions >= 2) same("a", conversions[1]) same("A", conversions[#conversions]) end) test("Core activation owns the plugin-global sequence state", function() local state = fresh_sequence_state() local host = MemoryHost.new() local root = composition_root.new({ host = host }) local activation = root:activate() truthy(composition_root.CompositionRoot.is(root)) same(state, activation.state) same(state, root:state()) same(state, root:transitions():state()) same(root:coordinator(), root:facade():coordinator()) same(activation, root:activate()) end) test("Core activation evaluates default and enabled feature highlights", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_cursor = true, mark_char = false, mark_direct = true, }, }) local activation = composition_root.new({ host = host }):activate() local groups = host:highlight_groups() same("fallback", activation.highlights.default_label.source) same("red", groups.CleverTeeDefaultLabel.guifg) same("Cursor", groups.CleverTeeCursor.link) same(nil, groups.CleverTeeChar) same("CleverTeeDefaultLabel", groups.CleverTeeDirect.link) end) test("Core activation refreshes highlights after colorscheme changes", function() fresh_sequence_state() local host = MemoryHost.new({ configuration = { mark_char = true }, }) local root = composition_root.new({ host = host }) local activation = root:activate() local registration = host:event_registrations()[ activation.colorscheme_registration ] list_same({ "ColorScheme" }, registration.names) same("colorscheme", registration.options.lifecycle) host:set_configuration("mark_char_color", "Search") host:deliver_event("ColorScheme", {}) local refresh = root:last_highlight_refresh() truthy(refresh ~= nil) same("configured", refresh.feature_links.CleverTeeChar.source) same("Search", host:highlight_groups().CleverTeeChar.link) end) test("Core activation installs sampled eager window hooks", function() fresh_sequence_state() local enabled_host = MemoryHost.new({ configuration = { clean_labels_eagerly = true }, }) local enabled_root = composition_root.new({ host = enabled_host }) local enabled = enabled_root:activate() truthy(enabled.feedback.eager_registration ~= nil) list_same( feedback_service.EAGER_EVENTS, enabled_host:event_registrations()[ enabled.feedback.eager_registration ].names ) enabled_host:set_configuration("clean_labels_eagerly", false) same( enabled.feedback.eager_registration, enabled_root:activate().feedback.eager_registration ) fresh_sequence_state() local disabled_host = MemoryHost.new({ configuration = { clean_labels_eagerly = false }, }) local disabled = composition_root.new({ host = disabled_host }):activate() same(nil, disabled.feedback.eager_registration) end) test("Core activation exposes all seven logical actions", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, input_packets = { { kind = "text", text = "b" } }, configuration = { mark_cursor = false, mark_char = false, }, emit_movement_events = false, }) local activation = composition_root.new({ host = host }):activate() same(7, #composition_root.ACTION_NAMES) local registered = {} for _, operation in ipairs(host:operations()) do if operation.operation == "register_action" then registered[#registered + 1] = operation.name end end list_same(composition_root.ACTION_NAMES, registered) for _, name in ipairs(composition_root.ACTION_NAMES) do same(name, activation.actions[name]) end local outcome = host:invoke_action("StartFindForward") same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(1, 2), outcome.position) same(domain.ActionKind.NEUTRAL, host:invoke_action("Reset").kind) end) test("Core activation checks mapping suppression by sentinel presence", function() fresh_sequence_state() local absent = composition_root.new({ host = MemoryHost.new() }):activate() truthy(absent.setup.install_default_mappings) for _, value in ipairs({ false, 0 }) do fresh_sequence_state() local configuration = {} configuration[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL] = value local host = MemoryHost.new({ configuration = configuration }) local root = composition_root.new({ host = host }) local activation = root:activate() falsy(activation.setup.install_default_mappings) host:unset_configuration(policy.DEFAULT_MAP_SUPPRESSION_SENTINEL) falsy(root:activate().setup.install_default_mappings) end end) test("Core activation installs four default mappings in required modes", function() fresh_sequence_state() local host = MemoryHost.new() local activation = composition_root.new({ host = host }):activate() local mappings = host:mappings() same(4, map_size(activation.mappings)) for _, expected in ipairs(composition_root.DEFAULT_MAPPINGS) do local identity = activation.mappings[expected.lhs] local actual = mappings[identity] same(expected.lhs, actual.lhs) same(expected.action, actual.action) list_same(composition_root.DEFAULT_MAPPING_MODES, actual.modes) truthy(actual.options.silent) falsy(actual.options.remap) truthy(actual.options.preserve_count) end fresh_sequence_state() local configuration = {} configuration[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL] = false local suppressed_host = MemoryHost.new({ configuration = configuration }) local suppressed = composition_root.new({ host = suppressed_host }):activate() same(0, map_size(suppressed.mappings)) same(0, map_size(suppressed_host:mappings())) end) test("Default mappings are silent non-recursive and count-preserving", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "ababa" }, cursor = { line = 1, byte_column = 1 }, count = 2, input_packets = { { kind = "text", text = "a" } }, configuration = { mark_cursor = false, mark_char = false, }, emit_movement_events = false, }) local activation = composition_root.new({ host = host }):activate() local mapping = host:mappings()[activation.mappings.f] truthy(mapping.options.silent) falsy(mapping.options.remap) truthy(mapping.options.preserve_count) local outcome = host:invoke_action(mapping.action) same(2, outcome.successful_steps) same(domain.Position.new(1, 5), outcome.position) end) test("Primary coordination accepts only the four motion descriptors", function() local coordinator = sequence_coordinator.new({ host = MemoryHost.new() }) for _, value in ipairs({ "f", "F", "t", "T" }) do same( domain.Descriptor.from_string(value), coordinator:validate_primary_descriptor(value) ) end fails(function() coordinator:primary("x") end, "clever-tee: Invalid mapping 'x'") end) test("Primary coordination reads normalized invocation state", function() local host = MemoryHost.new({ mode = "nov", cursor = { line = 2, byte_column = 4 }, count = 3, macro_register = "q", buffer_lines = { "one", "text" }, }) local invocation = sequence_coordinator.new({ host = host }) :read_primary_invocation() same(domain.ModeContext.from_full_mode("no"), invocation.context) same(domain.Position.new(2, 4), invocation.position) same(invocation.position, invocation.origin) same(3, invocation.count.value) truthy(invocation.macro_state.executing) same("q", invocation.macro_state.register) end) test("Primary coordination inspects fold policy during preflight", function() local host = MemoryHost.new({ fold_open_policy = { "horizontal" }, closed_fold_levels = 1, }) local coordinator = sequence_coordinator.new({ host = host }) local invocation = coordinator:read_primary_invocation() local folds = coordinator:inspect_fold_open_policy(invocation) local operations = host:operations() same("read_fold_state", operations[#operations].operation) truthy(folds:opens("horizontal")) end) test("Primary preflight opens folds for horizontal and all policies", function() for _, open_policy in ipairs({ "horizontal", "all" }) do local host = MemoryHost.new({ fold_open_policy = { open_policy }, closed_fold_levels = 2, }) sequence_coordinator.new({ host = host }):primary("f") same(0, host:read_fold_state().closed_levels, open_policy) end local host = MemoryHost.new({ fold_open_policy = { "jump" }, closed_fold_levels = 2, }) sequence_coordinator.new({ host = host }):primary("f") same(2, host:read_fold_state().closed_levels) end) test("Primary preflight repeats fold opening until the line is visible", function() local host = MemoryHost.new({ fold_open_policy = { "horizontal" }, closed_fold_levels = 3, }) sequence_coordinator.new({ host = host }):primary("t") local fold_operations = {} for _, operation in ipairs(host:operations()) do if operation.operation == "read_fold_state" or operation.operation == "open_fold" then fold_operations[#fold_operations + 1] = operation.operation end end list_same({ "read_fold_state", "open_fold", "read_fold_state", "open_fold", "read_fold_state", "open_fold", "read_fold_state", }, fold_operations) same(0, host:read_fold_state().closed_levels) end) test("Primary coordination asks RepeatResolver for acquisition or repetition", function() local state, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 2) local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = landing, }) local coordinator = sequence_coordinator.new({ host = host, state = state, transitions = transitions, }) local invocation = coordinator:read_primary_invocation() same(repeat_resolver.Decision.ACQUIRE, coordinator:decide_primary(invocation)) transitions:CommitCommandSuccess("n", landing, true) same(repeat_resolver.Decision.REPEAT, coordinator:decide_primary(invocation)) host:set_macro_state("q") invocation = coordinator:read_primary_invocation() same(repeat_resolver.Decision.ACQUIRE, coordinator:decide_primary(invocation)) end) test("Primary repetition guard runs after fold preflight", function() fresh_sequence_state() local host = MemoryHost.new({ fold_open_policy = { "all" }, closed_fold_levels = 2, }) local observed_levels local resolver = { decide = function() observed_levels = host:read_fold_state().closed_levels return repeat_resolver.Decision.ACQUIRE end, } sequence_coordinator.new({ host = host, repeat_resolver = resolver, }):primary("f") same(0, observed_levels) end) test("Acquiring primary coordination passes the pressed descriptor", function() local state, transitions = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = { line = 1, byte_column = 1 }, input_packets = { { kind = "text", text = "a" } }, configuration = { mark_cursor = false, mark_char = false, }, }) local coordinator = sequence_coordinator.new({ host = host, state = state, transitions = transitions, }) local invocation = coordinator:read_primary_invocation() local result = coordinator:acquire_primary("t", invocation) truthy(acquisition_service.AcquisitionResult.is(result)) truthy(result.resolved) same(domain.Descriptor.TILL_FORWARD, result.request.descriptor) same(domain.Descriptor.TILL_FORWARD, state:get_previous_descriptor("n")) same("a", result.target.value) end) test("Primary coordination stops on each acquisition outcome", function() local cases = { { packet = { kind = "special_key", name = "Escape", bytes = { 27 } }, kind = domain.ActionKind.ESCAPE, }, { packet = { kind = "text", text = "\r" }, kind = domain.ActionKind.EMPTY, diagnostic = acquisition_service.PREVIOUS_INPUT_NOT_FOUND, }, { packet = { kind = "error", message = "input unavailable" }, kind = domain.ActionKind.ERROR, diagnostic = "input unavailable", }, } for _, case in ipairs(cases) do fresh_sequence_state() local origin = domain.Position.new(1, 1) local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = origin, input_packets = { case.packet }, configuration = { mark_cursor = false, mark_char = false, }, }) local outcome = sequence_coordinator.new({ host = host }):primary("f") truthy(domain.ActionOutcome.is(outcome)) same(case.kind, outcome.kind) same(origin, outcome.position) same(origin, host:read_cursor()) if case.diagnostic ~= nil then same(case.diagnostic, host:diagnostics()[1].text) end end end) local function run_coordinated_primary(coordinator, descriptor) local outcome = coordinator:primary(descriptor) truthy(domain.ActionOutcome.is(outcome)) return coordinator:last_primary_resolution(), outcome end test("Fresh primary resolution uses its initiating descriptor", function() for _, pressed in ipairs({ "f", "F", "t", "T" }) do fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, input_packets = { { kind = "text", text = "a" } }, configuration = { mark_cursor = false, mark_char = false, }, }) local coordinator = sequence_coordinator.new({ host = host }) local resolution = run_coordinated_primary(coordinator, pressed) same("fresh", resolution.kind) same(domain.Descriptor.from_string(pressed), resolution.effective_descriptor) same(resolution.effective_descriptor, resolution.motion_plan.descriptor) end end) test("Fresh primary resolution reuses acquired plans and marks empty motion", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, input_packets = { { kind = "special_key", name = "Left", bytes = { 0x80, 0x01 } }, }, configuration = { mark_cursor = false, mark_char = false, }, }) local coordinator = sequence_coordinator.new({ host = host }) local invocation = coordinator:read_primary_invocation() local acquired = coordinator:acquire_primary("f", invocation) local resolution = coordinator:fresh_primary_resolution( "f", acquired, invocation ) same(acquired.target_plan, resolution.target_plan) same(acquired.motion_plan, resolution.motion_plan) same(domain.TargetPlanKind.EMPTY, resolution.target_plan.kind) truthy(resolution.skip_destination) end) test("Repeated primary coordination evaluates timeout before reuse", function() local _, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 3) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target("a"), 100) transitions:CommitCommandSuccess("n", landing, true) local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = landing, time_values_ms = { 150 }, configuration = { repeat_timeout_ms = 75 }, }) local coordinator = sequence_coordinator.new({ host = host }) local resolution = run_coordinated_primary(coordinator, "f") same("repeat", resolution.kind) same(repeat_resolver.Decision.REPEAT, resolution.timeout.decision) same(50, resolution.timeout.elapsed_ms) local time_read for index, operation in ipairs(host:operations()) do if operation.operation == "read_time_ms" then time_read = index end end truthy(time_read ~= nil) end) test("Expired primary repetition resets resources and reacquires pressed key", function() local state, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 3) local old_target = target("a") local host = MemoryHost.new({ buffer_lines = { "ababa" }, cursor = landing, input_packets = { { kind = "text", text = "b" } }, time_values_ms = { 120 }, configuration = { repeat_timeout_ms = 10, mark_cursor = false, mark_char = false, }, }) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", old_target, 100) transitions:CommitCommandSuccess("n", landing, true) local overlay = host:create_highlight({ group = "CleverTeeChar", window = "window-1", positions = { landing }, }) transitions:AddTargetOverlay(overlay, "window-1", 1) local timer = host:start_timer(50, function() end) transitions:SetHighlightTimer(timer) local coordinator = sequence_coordinator.new({ host = host }) local resolution = run_coordinated_primary(coordinator, "T") same("fresh", resolution.kind) same(domain.Descriptor.TILL_BACKWARD, resolution.effective_descriptor) same("b", resolution.target.value) same(domain.Descriptor.TILL_BACKWARD, state:get_previous_descriptor("n")) same(nil, host:highlights()[overlay]) falsy(host:timers()[timer].active) end) test("Timely primary repetition reads current-context sequence values", function() local _, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 3) local normal_target = target("a") local visual_target = target("b") transitions:BeginAcquisition("n", "F") transitions:CommitAcquiredTarget("n", normal_target) transitions:CommitCommandSuccess("n", landing, false) transitions:BeginAcquisition("v", "t") transitions:CommitAcquiredTarget("v", visual_target) transitions:CommitVisualSuccess("v", landing) local host = MemoryHost.new({ buffer_lines = { "ababa" }, cursor = landing, mode = "n", }) local coordinator = sequence_coordinator.new({ host = host }) local resolution = run_coordinated_primary(coordinator, "t") same("repeat", resolution.kind) same(domain.Descriptor.FIND_BACKWARD, resolution.stored_descriptor) same(normal_target, resolution.target) same(domain.Descriptor.TILL_FORWARD, resolution.pressed_descriptor) end) test("Timely primary repetition resolves live effective direction", function() local _, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 3) transitions:BeginAcquisition("n", "F") transitions:CommitAcquiredTarget("n", target("a")) transitions:CommitCommandSuccess("n", landing, false) local host = MemoryHost.new({ buffer_lines = { "ababa" }, cursor = landing, configuration = { fix_key_direction = false }, }) local coordinator = sequence_coordinator.new({ host = host }) local relative = run_coordinated_primary(coordinator, "f") same(domain.Descriptor.FIND_BACKWARD, relative.effective_descriptor) host:set_configuration("fix_key_direction", true) local fixed = run_coordinated_primary(coordinator, "f") same(domain.Descriptor.FIND_FORWARD, fixed.effective_descriptor) end) test("Primary repetition preserves the stored motion family", function() for _, stored in ipairs({ "f", "F", "t", "T" }) do for _, pressed in ipairs({ "f", "F", "t", "T" }) do local _, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 3) transitions:BeginAcquisition("n", stored) transitions:CommitAcquiredTarget("n", target("a")) transitions:CommitCommandSuccess("n", landing, true) local host = MemoryHost.new({ buffer_lines = { "ababa" }, cursor = landing, }) local coordinator = sequence_coordinator.new({ host = host }) local resolution = run_coordinated_primary(coordinator, pressed) same( domain.Descriptor.from_string(stored).family, resolution.effective_descriptor.family, stored .. "/" .. pressed ) end end end) test("Primary repetition builds one target plan from live matching policy", function() local _, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 2) local stored_target = target("a") transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", stored_target) transitions:CommitCommandSuccess("n", landing, true) local host = MemoryHost.new({ buffer_lines = { "xAx" }, cursor = landing, configuration = { ignore_case = false }, }) local underlying = target_plan.new({ policy = policy.new(host) }) local build_count = 0 local factory = { build = function(_, ...) build_count = build_count + 1 return underlying:build(...) end, } local coordinator = sequence_coordinator.new({ host = host, target_factory = factory, }) local sensitive = run_coordinated_primary(coordinator, "f") same(1, build_count) same(domain.CaseMode.SENSITIVE, sensitive.target_plan.case_mode) falsy(sensitive.target_plan:matches("A")) host:set_configuration("ignore_case", true) local insensitive = run_coordinated_primary(coordinator, "f") same(2, build_count) same(domain.CaseMode.INSENSITIVE, insensitive.target_plan.case_mode) truthy(insensitive.target_plan:matches("A")) end) test("Primary repetition builds its effective contextual motion plan", function() local _, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 3) transitions:BeginAcquisition("v", "t") transitions:CommitAcquiredTarget("v", target("a")) transitions:CommitVisualSuccess("v", landing) local host = MemoryHost.new({ buffer_lines = { "ababa" }, cursor = landing, mode = "v", selection = domain.Selection.active( domain.SelectionKind.CHARACTER, domain.Position.new(1, 1), landing, domain.SelectionOption.EXCLUSIVE ), }) local coordinator = sequence_coordinator.new({ host = host }) local resolution = run_coordinated_primary(coordinator, "F") same(resolution.target_plan, resolution.motion_plan.target_plan) same(domain.Descriptor.TILL_BACKWARD, resolution.effective_descriptor) same(resolution.effective_descriptor, resolution.motion_plan.descriptor) same(domain.SearchScope.BUFFER, resolution.motion_plan.search_scope) same( domain.EndpointPolicy.VISUAL_EXCLUSIVE, resolution.motion_plan.endpoint_policy ) end) test("Primary repetition restores stored-descriptor feedback before movement", function() local state, transitions = fresh_sequence_state() local landing = domain.Position.new(1, 3) transitions:BeginAcquisition("n", "t") transitions:CommitAcquiredTarget("n", target("b")) transitions:CommitCommandSuccess("n", landing, true) local host = MemoryHost.new({ buffer_lines = { "ababa" }, cursor = landing, configuration = { mark_char = true }, emit_movement_events = false, }) local coordinator = sequence_coordinator.new({ host = host }) local resolution = run_coordinated_primary(coordinator, "T") truthy(resolution.restored_feedback ~= nil) same( domain.Descriptor.TILL_FORWARD, resolution.restored_feedback.descriptor ) same(resolution.target_plan, resolution.restored_feedback.target_plan) same( domain.Descriptor.TILL_BACKWARD, resolution.motion_plan.descriptor ) same( resolution.restored_feedback.identity, state.target_overlays[1].identity ) end) test("Primary resolutions carry current first-move state", function() local _, transitions = fresh_sequence_state() local fresh_host = MemoryHost.new({ buffer_lines = { "aba" }, input_packets = { { kind = "text", text = "a" } }, configuration = { mark_cursor = false, mark_char = false, }, }) local fresh_coordinator = sequence_coordinator.new({ host = fresh_host }) local fresh = run_coordinated_primary(fresh_coordinator, "t") truthy(fresh.first_move) local landing = domain.Position.new(1, 3) transitions:CommitCommandSuccess("n", landing, true) local repeated_host = MemoryHost.new({ buffer_lines = { "ababa" }, cursor = landing, configuration = { mark_char = false }, }) local repeated_coordinator = sequence_coordinator.new({ host = repeated_host }) local repeated = run_coordinated_primary(repeated_coordinator, "t") falsy(repeated.first_move) end) test("Primary coordination executes resolved movement through MotionExecutor", function() local state = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "poge huga hiyo poyo" }, cursor = { line = 1, byte_column = 1 }, input_packets = { { kind = "text", text = "h" } }, configuration = { mark_cursor = false, mark_char = false, }, emit_movement_events = false, }) local coordinator = sequence_coordinator.new({ host = host }) local outcome = coordinator:primary("f") same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(1, 6), outcome.position) same(outcome.position, host:read_cursor()) same(outcome.position, state:get_previous_landing("n")) falsy(state:get_first_move("n")) same( coordinator:last_primary_resolution().motion_plan, coordinator:last_primary_resolution().acquisition_result.motion_plan ) end) test("Fresh and repeated primary actions refresh highlight timeout", function() local state = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aaaa" }, cursor = { line = 1, byte_column = 1 }, input_packets = { { kind = "text", text = "a" } }, configuration = { mark_cursor = false, mark_char = true, highlight_timeout_ms = 25, }, emit_movement_events = false, }) local coordinator = sequence_coordinator.new({ host = host }) same(domain.ActionKind.MOVEMENT, coordinator:primary("f").kind) local fresh_timer = state.highlight_timer truthy(fresh_timer ~= nil) same(fresh_timer, coordinator:last_primary_resolution().highlight_timer) same(domain.ActionKind.MOVEMENT, coordinator:primary("f").kind) local repeated_timer = state.highlight_timer truthy(repeated_timer ~= nil) falsy(fresh_timer == repeated_timer) falsy(host:timers()[fresh_timer].active) truthy(host:timers()[repeated_timer].active) same(repeated_timer, coordinator:last_primary_resolution().highlight_timer) end) test("Escape primary action starts no highlight timer", function() local state = fresh_sequence_state() local host = MemoryHost.new({ input_packets = { { kind = "special_key", name = "Escape", bytes = { 27 } }, }, configuration = { mark_cursor = false, mark_char = true, highlight_timeout_ms = 30, }, }) local outcome = sequence_coordinator.new({ host = host }):primary("f") same(domain.ActionKind.ESCAPE, outcome.kind) same(nil, state.highlight_timer) for _, operation in ipairs(host:operations()) do falsy(operation.operation == "start_timer") end end) test("Failed resolved primary search still refreshes highlight timeout", function() local state = fresh_sequence_state() local origin = domain.Position.new(1, 1) local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor = origin, input_packets = { { kind = "text", text = "z" } }, configuration = { mark_cursor = false, mark_char = true, highlight_timeout_ms = 40, }, emit_movement_events = false, }) local coordinator = sequence_coordinator.new({ host = host }) local outcome = coordinator:primary("f") same(domain.ActionKind.FAILED_SEARCH, outcome.kind) same(origin, outcome.position) same(origin, host:read_cursor()) truthy(state.highlight_timer ~= nil) same(state.highlight_timer, coordinator:last_primary_resolution().highlight_timer) truthy(host:timers()[state.highlight_timer].active) end) test("Initiating action methods supply their fixed descriptors", function() local cases = { { method = "start_find_forward", descriptor = "f", origin = 1 }, { method = "start_find_backward", descriptor = "F", origin = 3 }, { method = "start_till_forward", descriptor = "t", origin = 1 }, { method = "start_till_backward", descriptor = "T", origin = 3 }, } for _, case in ipairs(cases) do fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aaa" }, cursor = { line = 1, byte_column = case.origin }, input_packets = { { kind = "text", text = "a" } }, configuration = { mark_cursor = false, mark_char = false, }, emit_movement_events = false, }) local facade = action_facade.new({ host = host }) local outcome = facade[case.method](facade) same(domain.ActionKind.MOVEMENT, outcome.kind, case.method) same( domain.Descriptor.from_string(case.descriptor), facade:coordinator():last_primary_resolution().effective_descriptor, case.method ) end end) test("Free-form action adapter accepts every valid descriptor", function() local origins = { f = 1, F = 3, t = 1, T = 3 } for _, descriptor in ipairs({ "f", "F", "t", "T" }) do local state = fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aaa" }, cursor = { line = 1, byte_column = origins[descriptor] }, input_packets = { { kind = "text", text = "a" } }, configuration = { mark_cursor = false, mark_char = false, }, emit_movement_events = false, }) local root = composition_root.new({ host = host }) local outcome = root:invoke_descriptor(descriptor) same(domain.ActionKind.MOVEMENT, outcome.kind, descriptor) same( domain.Descriptor.from_string(descriptor), state:get_previous_descriptor("n"), descriptor ) end end) test("Free-form action adapter raises the exact invalid mapping error", function() fresh_sequence_state() local root = composition_root.new({ host = MemoryHost.new() }) local ok, diagnostic = pcall(function() root:invoke_descriptor("X") end) falsy(ok) same("clever-tee: Invalid mapping 'X'", diagnostic) end) test("ActionFacade returns typed primary action outcomes", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "aba" }, input_packets = { { kind = "text", text = "b" } }, configuration = { mark_cursor = false, mark_char = false, }, emit_movement_events = false, }) local facade = action_facade.new({ host = host }) local outcome = facade:primary("f") truthy(domain.ActionOutcome.is(outcome)) same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(1, 2), outcome.position) fails(function() facade:primary("x") end, "clever-tee: Invalid mapping 'x'") end) test("ActionFacade Reset applies public cleanup and returns neutral", function() local state, transitions = fresh_sequence_state() local target_value = target("a") local landing = domain.Position.new(1, 3) local host = MemoryHost.new({ buffer_lines = { "aba" }, cursor = landing, }) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target_value, 25) transitions:CommitCommandSuccess("n", landing, true) local overlay = host:create_highlight({ group = "CleverTeeChar", window = "window-1", positions = { landing }, }) transitions:AddTargetOverlay(overlay, "window-1", 1) local timer = host:start_timer(30, function() end) transitions:SetHighlightTimer(timer) local outcome = action_facade.new({ host = host }):reset() same(domain.ActionKind.NEUTRAL, outcome.kind) same(landing, outcome.position) same(nil, state:get_previous_descriptor("n")) same(nil, state:get_previous_landing("n")) same(target_value, state:get_previous_target("n")) same(nil, host:highlights()[overlay]) falsy(host:timers()[timer].active) end) test("Diagnostic full reset is available only through diagnostic API", function() local state, transitions = fresh_sequence_state() transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target("a"), 10) transitions:CommitCommandSuccess( "n", domain.Position.new(1, 2), domain.Direction.FORWARD ) local host = MemoryHost.new({ cursor = { line = 1, byte_column = 2 } }) local root = composition_root.new({ host = host }) local activation = root:activate() same(nil, activation.actions.DiagnosticFullReset) local outcome = root:diagnostic_full_reset() same(domain.ActionKind.NEUTRAL, outcome.kind) same(nil, state:get_previous_descriptor("n")) same(nil, state:get_previous_target("n")) same(nil, state.last_input_context) falsy(state.moved_forward) falsy(state.moved_forward_initialized) fails(function() host:invoke_action("DiagnosticFullReset") end, "not registered") end) test("Same-direction explicit coordination reads current-context state", function() local _, transitions = fresh_sequence_state() local normal_target = target("a") local visual_target = target("b") transitions:BeginAcquisition("n", "F") transitions:CommitAcquiredTarget("n", normal_target) transitions:BeginAcquisition("v", "t") transitions:CommitAcquiredTarget("v", visual_target) local host = MemoryHost.new({ mode = "v", count = 2, cursor = { line = 1, byte_column = 3 }, }) local resolution = sequence_coordinator.new({ host = host }) :resolve_explicit_same() same("explicit_same", resolution.kind) same(domain.ModeContext.from_full_mode("v"), resolution.invocation.context) same(2, resolution.invocation.count.value) same(domain.Descriptor.TILL_FORWARD, resolution.request.descriptor) same(visual_target, resolution.request.target) end) test("Opposite explicit coordination swaps current-context descriptor", function() for _, stored in ipairs({ "f", "F", "t", "T" }) do local _, transitions = fresh_sequence_state() local stored_target = target("x") transitions:BeginAcquisition("n", stored) transitions:CommitAcquiredTarget("n", stored_target) local host = MemoryHost.new({ configuration = { fix_key_direction = true }, count = 3, }) local resolution = sequence_coordinator.new({ host = host }) :resolve_explicit_opposite() same("explicit_opposite", resolution.kind) same(domain.Descriptor.swap(stored), resolution.request.descriptor) same(stored_target, resolution.request.target) same(3, resolution.invocation.count.value) end end) test("Explicit coordination keeps code-zero target fallback", function() local _, transitions = fresh_sequence_state() transitions:BeginAcquisition("n", "T") local host = MemoryHost.new() local coordinator = sequence_coordinator.new({ host = host }) local same_repeat = coordinator:resolve_explicit_same() local opposite_repeat = coordinator:resolve_explicit_opposite() for _, resolution in ipairs({ same_repeat, opposite_repeat }) do falsy(resolution.request.neutral) same(domain.TargetKind.CODE_FALLBACK, resolution.request.target.kind) same(0, resolution.request.target.first_code) same("", resolution.request.target.value) end end) test("Explicit coordination builds target and resolved motion plans", function() local _, transitions = fresh_sequence_state() local stored_target = target("a") transitions:BeginAcquisition("n", "t") transitions:CommitAcquiredTarget("n", stored_target) local host = MemoryHost.new({ buffer_lines = { "aA" }, configuration = { ignore_case = true, search_current_line_only = true, }, }) local coordinator = sequence_coordinator.new({ host = host }) local resolution = coordinator:resolve_explicit_opposite() same(stored_target, resolution.target) same(domain.CaseMode.INSENSITIVE, resolution.target_plan.case_mode) same(resolution.target_plan, resolution.motion_plan.target_plan) same(domain.Descriptor.TILL_BACKWARD, resolution.effective_descriptor) same(resolution.effective_descriptor, resolution.motion_plan.descriptor) same(domain.SearchScope.CURRENT_LINE, resolution.motion_plan.search_scope) same(domain.EndpointPolicy.REGULAR, resolution.motion_plan.endpoint_policy) end) test("Explicit repeat actions execute through the resolved motion path", function() local state, transitions = fresh_sequence_state() local prior_landing = domain.Position.new(1, 1) transitions:BeginAcquisition("n", "f") transitions:CommitAcquiredTarget("n", target("a")) transitions:CommitCommandSuccess("n", prior_landing, true) local host = MemoryHost.new({ buffer_lines = { "abaca" }, cursor = prior_landing, count = 2, configuration = { mark_char = false }, emit_movement_events = false, }) local facade = action_facade.new({ host = host }) local outcome = facade:repeat_same_direction() same(domain.ActionKind.MOVEMENT, outcome.kind) same(domain.Position.new(1, 5), outcome.position) same(outcome.position, host:read_cursor()) same(outcome.position, state:get_previous_landing("n")) local resolution = facade:coordinator():last_explicit_resolution() same(domain.Descriptor.FIND_FORWARD, resolution.motion_plan.descriptor) same(2, resolution.invocation.count.value) end) test("Explicit repeats bypass every primary-only coordinator stage", function() local _, transitions = fresh_sequence_state() transitions:BeginAcquisition("n", "F") transitions:CommitAcquiredTarget("n", target("a"), 5) transitions:CommitCommandSuccess("n", domain.Position.new(1, 3), false) local host = MemoryHost.new({ buffer_lines = { "ababa" }, cursor = { line = 1, byte_column = 3 }, fold_open_policy = { "all" }, closed_fold_levels = 2, time_values_ms = { 1000 }, configuration = { repeat_timeout_ms = 1, mark_char = true, highlight_timeout_ms = 20, }, emit_movement_events = false, }) local coordinator = sequence_coordinator.new({ host = host }) local outcome = coordinator:repeat_opposite_direction() same(domain.ActionKind.MOVEMENT, outcome.kind) local operations = host:operations() local forbidden = { read_macro_state = true, read_input = true, read_time_ms = true, open_fold = true, create_highlight = true, start_timer = true, } for _, operation in ipairs(operations) do falsy(forbidden[operation.operation] == true, operation.operation) end same(2, host:read_fold_state().closed_levels) same(nil, coordinator:last_primary_resolution()) truthy(coordinator:last_explicit_resolution() ~= nil) end) test("Coordinated operator changes record resolved dot payload", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "hoge fuge" }, cursor = { line = 1, byte_column = 1 }, mode = "no", pending_operator = "delete", input_packets = { { kind = "text", text = "e" } }, configuration = { mark_cursor = false, mark_char = false, }, emit_movement_events = false, }) local facade = action_facade.new({ host = host }) local outcome = facade:primary("f") same(domain.ActionKind.MOVEMENT, outcome.kind) truthy(domain.DotPayload.is(outcome.dot_payload)) same(domain.Descriptor.FIND_FORWARD, outcome.dot_payload.descriptor) same("e", outcome.dot_payload.target.value) same(outcome.dot_payload, host:dot_repeat_payload()) same(" fuge", host:read_text():line(1)) end) test("Coordinated dot payload replays directly without target input", function() fresh_sequence_state() local host = MemoryHost.new({ buffer_lines = { "hoge fuge piye poye" }, cursor = { line = 1, byte_column = 1 }, mode = "no", pending_operator = "delete", input_packets = { { kind = "text", text = "e" } }, configuration = { mark_cursor = false, mark_char = false, }, emit_movement_events = false, }) local facade = action_facade.new({ host = host }) local first = facade:primary("f") same(domain.ActionKind.MOVEMENT, first.kind) local expected = { " fuge piye poye", " piye poye", " poye", "", } same(expected[1], host:read_text():line(1)) for index = 2, #expected do local replay = host:replay_dot(1) same(domain.ActionKind.MOVEMENT, replay.kind) same(first.dot_payload, replay.dot_payload) same(expected[index], host:read_text():line(1)) end local input_reads = 0 for _, operation in ipairs(host:operations()) do if operation.operation == "read_input" then input_reads = input_reads + 1 end end same(1, input_reads) 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 15 core: %d tests passed\n", passed))