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_f.domain") local acquisition_service = require("clever_f.acquisition_service") local capabilities = require("clever_f.capabilities") local destination_engine = require("clever_f.destination_engine") local direct_preview_planner = require("clever_f.direct_preview_planner") local feedback_service = require("clever_f.feedback_service") local case_policy = require("clever_f.case_policy") local policy = require("clever_f.policy") local repeat_resolver = require("clever_f.repeat_resolver") local migemo_catalog = require("clever_f.migemo_catalog") local motion_plan = require("clever_f.motion_plan") local motion_executor = require("clever_f.motion_executor") local sequence_state = require("clever_f.sequence_state") local state_transitions = require("clever_f.state_transitions") local text_topology = require("clever_f.text_topology") local target_plan = require("clever_f.target_plan") local MemoryHost = require("clever_f.testing.memory_host") local tests = {} local passed = 0 local function test(name, body) tests[#tests + 1] = { name = name, body = body } end local function same(expected, actual, message) if expected ~= actual then error((message or "values differ") .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual), 2) end end local function truthy(value, message) if not value then error(message or "value must be true", 2) end end local function falsy(value, message) if value then error(message or "value must be false", 2) end end local function fails(body, expected_text) local ok, failure = pcall(body) if ok then error("operation must fail", 2) end if expected_text and not tostring(failure):find(expected_text, 1, true) then error("failure does not contain '" .. expected_text .. "': " .. tostring(failure), 2) end end local function list_same(expected, actual) same(#expected, #actual, "list lengths differ") for index = 1, #expected do same(expected[index], actual[index], "list item " .. tostring(index) .. " differs") end 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("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) 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-f: ") host:redraw("screen") host:redraw("full") host:emit_diagnostic("error", "problem") same("clever-f: ", 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 = "CleverFChar", 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("CleverFDefaultLabel", schema.mark_char_color.default_target) same("CleverFDefaultLabel", 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.CleverFCursor.enabled) same(nil, initial.CleverFCursor.configured_target) same("Cursor", initial.CleverFCursor.target) truthy(initial.CleverFChar.enabled) same("CleverFDefaultLabel", initial.CleverFChar.target) falsy(initial.CleverFDirect.enabled) same("CleverFDefaultLabel", initial.CleverFDirect.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.CleverFCursor.enabled) same("Search", refreshed.CleverFCursor.target) falsy(refreshed.CleverFChar.enabled) same("ErrorMsg", refreshed.CleverFChar.target) truthy(refreshed.CleverFDirect.enabled) same("IncSearch", refreshed.CleverFDirect.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", "CleverFCursor") transitions:AddTemporaryOverlay("direct-1", "window-2", "CleverFDirect") 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("CleverFDirect", 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", "CleverFCursor") 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", "CleverFDirect") 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", "CleverFCursor") 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("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 = "fccd16452c105fcb7303d855b96d841f8174d37affa0c43d8b49a54e68289f3e", }, { encoding = "cp932", file = "cp932.vim", hash = "3823fda790f43876be78d53e116187f2e0eedc14d203756ea66a3814ca9707f1", }, { encoding = "euc-jp", file = "eucjp.vim", hash = "d6d7a2a795d7e3e14cdefd0cf2597bc48bd3d89dbdc41019f460027377855079", }, } 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-f: 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 = { CleverFDefaultLabel = 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().CleverFDefaultLabel.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().CleverFDefaultLabel 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 = { CleverFCursor = { guifg = "blue" }, }, }) local feedback = feedback_service.new(host) local results = feedback:evaluate_feature_links() same("configured", results.CleverFCursor.source) same("Search", results.CleverFCursor.target) same("Search", host:highlight_groups().CleverFCursor.link) local definition for _, operation in ipairs(host:operations()) do if operation.operation == "define_highlight_group" and operation.name == "CleverFCursor" then definition = operation end end truthy(definition ~= nil) truthy(definition.options.force) end) test("Feature link evaluation preserves colorscheme CleverF groups", function() local custom = { guifg = "magenta", gui = { reverse = true }, } local host = MemoryHost.new({ highlight_groups = { CleverFChar = custom, }, }) local feedback = feedback_service.new(host) local results = feedback:evaluate_feature_links() same("colorscheme", results.CleverFChar.source) falsy(results.CleverFChar.applied) same("magenta", results.CleverFChar.definition.guifg) truthy(results.CleverFChar.definition.gui.reverse) same("magenta", host:highlight_groups().CleverFChar.guifg) for _, operation in ipairs(host:operations()) do if operation.operation == "define_highlight_group" then falsy(operation.name == "CleverFChar") 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.CleverFCursor.source) truthy(results.CleverFCursor.applied) local groups = host:highlight_groups() same(results.CleverFCursor.target, groups.CleverFCursor.link) local fallback_definition for _, operation in ipairs(host:operations()) do if operation.operation == "define_highlight_group" and operation.name == "CleverFCursor" 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.CleverFCursor.target) same("Cursor", host:highlight_groups().CleverFCursor.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({ "CleverFChar", "CleverFDirect" }) do same("CleverFDefaultLabel", results[group].target) same("CleverFDefaultLabel", 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 = { CleverFCursor = { guifg = "one" }, CleverFChar = { guifg = "two" }, CleverFDirect = { 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.CleverFCursor.guifg) same("two", groups.CleverFChar.guifg) same("three", groups.CleverFDirect.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("CleverFCursor") ) same( feedback_service.Priority.HIGH, feedback_service.overlay_priority("CleverFChar") ) 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("CleverFDirect") ) 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("CleverFCursor", 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("CleverFDirect", 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( "CleverFDefaultLabel", evaluation.feature_links.CleverFDirect.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("CleverFDirect", 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-f: ", acquisition_service.PROMPT) list_same({ "clever-f: " }, 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 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("CleverFChar", 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("CleverFChar", resource.group) 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 = "CleverFChar", 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) 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 12: %d tests passed\n", passed))