diff options
| -rw-r--r-- | lua/clever_f/sequence_coordinator.lua | 60 | ||||
| -rw-r--r-- | tests/run.lua | 68 |
2 files changed, 112 insertions, 16 deletions
diff --git a/lua/clever_f/sequence_coordinator.lua b/lua/clever_f/sequence_coordinator.lua index 5a05f53..ee50fbd 100644 --- a/lua/clever_f/sequence_coordinator.lua +++ b/lua/clever_f/sequence_coordinator.lua @@ -1,6 +1,7 @@ local acquisition_service_factory = require("clever_f.acquisition_service") local domain = require("clever_f.domain") local feedback_service_factory = require("clever_f.feedback_service") +local motion_executor_factory = require("clever_f.motion_executor") local motion_plan_factory = require("clever_f.motion_plan") local policy = require("clever_f.policy") local repeat_resolver_factory = require("clever_f.repeat_resolver") @@ -100,6 +101,17 @@ function SequenceCoordinator.new(options) if type(acquisition) ~= "table" or type(acquisition.acquire) ~= "function" then fail("SequenceCoordinator acquisition service must provide acquire", 2) end + local executor = options.motion_executor + or options.executor + or motion_executor_factory.new({ + host = host, + state = state, + transitions = transitions, + feedback = feedback, + }) + if type(executor) ~= "table" or type(executor.execute) ~= "function" then + fail("SequenceCoordinator motion executor must provide execute", 2) + end local coordinator = setmetatable({}, SequenceCoordinator) coordinator_records[coordinator] = { @@ -112,6 +124,8 @@ function SequenceCoordinator.new(options) target_factory = target_factory, motion_factory = motion_factory, acquisition = acquisition, + motion_executor = executor, + last_primary_resolution = nil, } return coordinator end @@ -410,6 +424,36 @@ function SequenceCoordinator:stored_primary_resolution( return resolution end +function SequenceCoordinator:execute_primary_resolution(resolution) + if type(resolution) ~= "table" + or not domain.ModeContext.is(resolution.invocation.context) + or not domain.ResolvedMotionPlan.is(resolution.motion_plan) + then + fail("primary execution requires a resolved motion", 2) + end + local record = coordinator_records[self] + record.last_primary_resolution = resolution + if resolution.skip_destination then + return domain.ActionOutcome.empty(resolution.invocation.position) + end + local view = resolution.text_view or text_topology.from_host(record.host) + local outcome = record.motion_executor:execute( + view, + resolution.invocation.context, + resolution.motion_plan, + resolution.invocation.count, + resolution.first_move + ) + if not domain.ActionOutcome.is(outcome) then + fail("MotionExecutor must return an ActionOutcome", 2) + end + return outcome +end + +function SequenceCoordinator:last_primary_resolution() + return coordinator_records[self].last_primary_resolution +end + function SequenceCoordinator:primary(value) local descriptor = self:validate_primary_descriptor(value) local invocation = self:read_primary_invocation() @@ -420,7 +464,11 @@ function SequenceCoordinator:primary(value) ) invocation.repeat_decision = self:decide_primary(invocation) if invocation.repeat_decision == repeat_resolver_factory.Decision.ACQUIRE then - return self:resolve_acquisition(descriptor, invocation) + local acquired = self:resolve_acquisition(descriptor, invocation) + if domain.ActionOutcome.is(acquired) then + return acquired + end + return self:execute_primary_resolution(acquired) end local timeout = self:evaluate_repeat_timeout(invocation) if timeout.decision == repeat_resolver_factory.Decision.ACQUIRE then @@ -431,9 +479,15 @@ function SequenceCoordinator:primary(value) end record.feedback:release_transition_cleanup(timeout.cleanup) end - return self:resolve_acquisition(descriptor, invocation) + local acquired = self:resolve_acquisition(descriptor, invocation) + if domain.ActionOutcome.is(acquired) then + return acquired + end + return self:execute_primary_resolution(acquired) end - return self:stored_primary_resolution(invocation, descriptor, timeout) + return self:execute_primary_resolution( + self:stored_primary_resolution(invocation, descriptor, timeout) + ) end function M.new(options) diff --git a/tests/run.lua b/tests/run.lua index 3c45045..cf7edb9 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -7266,6 +7266,12 @@ test("Primary coordination stops on each acquisition outcome", function() end end) +local function run_coordinated_primary(coordinator, descriptor) + local outcome = coordinator:primary(descriptor) + truthy(domain.ActionOutcome.is(outcome)) + return coordinator:last_primary_resolution(), outcome +end + test("Fresh primary resolution uses its initiating descriptor", function() for _, pressed in ipairs({ "f", "F", "t", "T" }) do fresh_sequence_state() @@ -7277,7 +7283,8 @@ test("Fresh primary resolution uses its initiating descriptor", function() mark_char = false, }, }) - local resolution = sequence_coordinator.new({ host = host }):primary(pressed) + local coordinator = sequence_coordinator.new({ host = host }) + local resolution = run_coordinated_primary(coordinator, pressed) same("fresh", resolution.kind) same(domain.Descriptor.from_string(pressed), resolution.effective_descriptor) same(resolution.effective_descriptor, resolution.motion_plan.descriptor) @@ -7324,7 +7331,8 @@ test("Repeated primary coordination evaluates timeout before reuse", function() configuration = { repeat_timeout_ms = 75 }, }) - local resolution = sequence_coordinator.new({ host = host }):primary("f") + local coordinator = sequence_coordinator.new({ host = host }) + local resolution = run_coordinated_primary(coordinator, "f") same("repeat", resolution.kind) same(repeat_resolver.Decision.REPEAT, resolution.timeout.decision) @@ -7365,7 +7373,8 @@ test("Expired primary repetition resets resources and reacquires pressed key", f local timer = host:start_timer(50, function() end) transitions:SetHighlightTimer(timer) - local resolution = sequence_coordinator.new({ host = host }):primary("T") + local coordinator = sequence_coordinator.new({ host = host }) + local resolution = run_coordinated_primary(coordinator, "T") same("fresh", resolution.kind) same(domain.Descriptor.TILL_BACKWARD, resolution.effective_descriptor) @@ -7392,7 +7401,8 @@ test("Timely primary repetition reads current-context sequence values", function mode = "n", }) - local resolution = sequence_coordinator.new({ host = host }):primary("t") + local coordinator = sequence_coordinator.new({ host = host }) + local resolution = run_coordinated_primary(coordinator, "t") same("repeat", resolution.kind) same(domain.Descriptor.FIND_BACKWARD, resolution.stored_descriptor) @@ -7413,10 +7423,10 @@ test("Timely primary repetition resolves live effective direction", function() }) local coordinator = sequence_coordinator.new({ host = host }) - local relative = coordinator:primary("f") + local relative = run_coordinated_primary(coordinator, "f") same(domain.Descriptor.FIND_BACKWARD, relative.effective_descriptor) host:set_configuration("fix_key_direction", true) - local fixed = coordinator:primary("f") + local fixed = run_coordinated_primary(coordinator, "f") same(domain.Descriptor.FIND_FORWARD, fixed.effective_descriptor) end) @@ -7432,7 +7442,8 @@ test("Primary repetition preserves the stored motion family", function() buffer_lines = { "ababa" }, cursor = landing, }) - local resolution = sequence_coordinator.new({ host = host }):primary(pressed) + local coordinator = sequence_coordinator.new({ host = host }) + local resolution = run_coordinated_primary(coordinator, pressed) same( domain.Descriptor.from_string(stored).family, resolution.effective_descriptor.family, @@ -7467,12 +7478,12 @@ test("Primary repetition builds one target plan from live matching policy", func target_factory = factory, }) - local sensitive = coordinator:primary("f") + local sensitive = run_coordinated_primary(coordinator, "f") same(1, build_count) same(domain.CaseMode.SENSITIVE, sensitive.target_plan.case_mode) falsy(sensitive.target_plan:matches("A")) host:set_configuration("ignore_case", true) - local insensitive = coordinator:primary("f") + local insensitive = run_coordinated_primary(coordinator, "f") same(2, build_count) same(domain.CaseMode.INSENSITIVE, insensitive.target_plan.case_mode) truthy(insensitive.target_plan:matches("A")) @@ -7496,7 +7507,8 @@ test("Primary repetition builds its effective contextual motion plan", function( ), }) - local resolution = sequence_coordinator.new({ host = host }):primary("F") + local coordinator = sequence_coordinator.new({ host = host }) + local resolution = run_coordinated_primary(coordinator, "F") same(resolution.target_plan, resolution.motion_plan.target_plan) same(domain.Descriptor.TILL_BACKWARD, resolution.effective_descriptor) @@ -7521,7 +7533,8 @@ test("Primary repetition restores stored-descriptor feedback before movement", f emit_movement_events = false, }) - local resolution = sequence_coordinator.new({ host = host }):primary("T") + local coordinator = sequence_coordinator.new({ host = host }) + local resolution = run_coordinated_primary(coordinator, "T") truthy(resolution.restored_feedback ~= nil) same( @@ -7549,7 +7562,8 @@ test("Primary resolutions carry current first-move state", function() mark_char = false, }, }) - local fresh = sequence_coordinator.new({ host = fresh_host }):primary("t") + local fresh_coordinator = sequence_coordinator.new({ host = fresh_host }) + local fresh = run_coordinated_primary(fresh_coordinator, "t") truthy(fresh.first_move) local landing = domain.Position.new(1, 3) @@ -7559,10 +7573,38 @@ test("Primary resolutions carry current first-move state", function() cursor = landing, configuration = { mark_char = false }, }) - local repeated = sequence_coordinator.new({ host = repeated_host }):primary("t") + local repeated_coordinator = sequence_coordinator.new({ host = repeated_host }) + local repeated = run_coordinated_primary(repeated_coordinator, "t") falsy(repeated.first_move) end) +test("Primary coordination executes resolved movement through MotionExecutor", function() + local state = fresh_sequence_state() + local host = MemoryHost.new({ + buffer_lines = { "poge huga hiyo poyo" }, + cursor = { line = 1, byte_column = 1 }, + input_packets = { { kind = "text", text = "h" } }, + configuration = { + mark_cursor = false, + mark_char = false, + }, + emit_movement_events = false, + }) + local coordinator = sequence_coordinator.new({ host = host }) + + local outcome = coordinator:primary("f") + + same(domain.ActionKind.MOVEMENT, outcome.kind) + same(domain.Position.new(1, 6), outcome.position) + same(outcome.position, host:read_cursor()) + same(outcome.position, state:get_previous_landing("n")) + falsy(state:get_first_move("n")) + same( + coordinator:last_primary_resolution().motion_plan, + coordinator:last_primary_resolution().acquisition_result.motion_plan + ) +end) + for _, item in ipairs(tests) do local ok, failure = xpcall(item.body, debug.traceback) if not ok then |
