diff options
| author | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 10:33:19 +0200 |
|---|---|---|
| committer | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 10:33:19 +0200 |
| commit | b1dad49f9debfbacaec9bf209a1c4673c24c5c39 (patch) | |
| tree | ebbb7f7578fc78e38b93b8bcf13d648773770dd7 /tests/run.lua | |
| parent | 2b62f5654d6b9d939266dda369c52ba0aaa59c07 (diff) | |
Verify destination engine purity
Diffstat (limited to 'tests/run.lua')
| -rw-r--r-- | tests/run.lua | 225 |
1 files changed, 224 insertions, 1 deletions
diff --git a/tests/run.lua b/tests/run.lua index 7431de5..d24f3f0 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -2883,6 +2883,229 @@ test("Boundary-before-any outcomes preserve the original position", function() 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) + for _, item in ipairs(tests) do local ok, failure = xpcall(item.body, debug.traceback) if not ok then @@ -2892,4 +3115,4 @@ for _, item in ipairs(tests) do passed = passed + 1 end -io.stdout:write(string.format("Phase 7: %d tests passed\n", passed)) +io.stdout:write(string.format("Phase 8: %d tests passed\n", passed)) |
