diff options
| author | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 13:48:22 +0200 |
|---|---|---|
| committer | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 13:48:22 +0200 |
| commit | 6c87eb3b6103c638cf76c7509ab2da6bcea67ec8 (patch) | |
| tree | bc83e928dc48ff5aa41bc1ebf3ca435c90d68871 | |
| parent | 7ec26094fee806bf53f3eb253b57f3a06025c70d (diff) | |
Execute explicit repeat motions
| -rw-r--r-- | lua/clever_f/action_facade.lua | 20 | ||||
| -rw-r--r-- | lua/clever_f/sequence_coordinator.lua | 63 | ||||
| -rw-r--r-- | tests/run.lua | 36 |
3 files changed, 94 insertions, 25 deletions
diff --git a/lua/clever_f/action_facade.lua b/lua/clever_f/action_facade.lua index 5d8da67..1dd5052 100644 --- a/lua/clever_f/action_facade.lua +++ b/lua/clever_f/action_facade.lua @@ -54,6 +54,26 @@ end ActionFacade.invoke_primary = ActionFacade.primary ActionFacade.start = ActionFacade.primary +local function explicit_outcome(facade, method_name) + local coordinator = facade:coordinator() + local outcome = coordinator[method_name](coordinator) + if not domain.ActionOutcome.is(outcome) then + fail("SequenceCoordinator must return an ActionOutcome", 3) + end + return outcome +end + +function ActionFacade:repeat_same_direction() + return explicit_outcome(self, "repeat_same_direction") +end + +function ActionFacade:repeat_opposite_direction() + return explicit_outcome(self, "repeat_opposite_direction") +end + +ActionFacade.RepeatSameDirection = ActionFacade.repeat_same_direction +ActionFacade.RepeatOppositeDirection = ActionFacade.repeat_opposite_direction + function M.new(options) return ActionFacade.new(options) end diff --git a/lua/clever_f/sequence_coordinator.lua b/lua/clever_f/sequence_coordinator.lua index 4d64e51..59dc332 100644 --- a/lua/clever_f/sequence_coordinator.lua +++ b/lua/clever_f/sequence_coordinator.lua @@ -126,6 +126,7 @@ function SequenceCoordinator.new(options) acquisition = acquisition, motion_executor = executor, last_primary_resolution = nil, + last_explicit_resolution = nil, } return coordinator end @@ -437,31 +438,36 @@ function SequenceCoordinator:refresh_primary_feedback(resolution) return record.feedback:refresh_primary(resolution.target, window) end -function SequenceCoordinator:execute_primary_resolution(resolution) +function SequenceCoordinator:execute_resolved_motion(resolution, execution_options) 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) + fail("motion execution requires a resolved motion", 2) end - local record = coordinator_records[self] - record.last_primary_resolution = resolution - local outcome if resolution.skip_destination then - outcome = domain.ActionOutcome.empty(resolution.invocation.position) - else - local view = resolution.text_view or text_topology.from_host(record.host) - 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 domain.ActionOutcome.empty(resolution.invocation.position) end + local record = coordinator_records[self] + 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, + execution_options + ) + if not domain.ActionOutcome.is(outcome) then + fail("MotionExecutor must return an ActionOutcome", 2) + end + return outcome +end + +function SequenceCoordinator:execute_primary_resolution(resolution) + local record = coordinator_records[self] + record.last_primary_resolution = resolution + local outcome = self:execute_resolved_motion(resolution) resolution.highlight_timer = self:refresh_primary_feedback(resolution) return outcome end @@ -470,6 +476,10 @@ function SequenceCoordinator:last_primary_resolution() return coordinator_records[self].last_primary_resolution end +function SequenceCoordinator:last_explicit_resolution() + return coordinator_records[self].last_explicit_resolution +end + function SequenceCoordinator:read_explicit_invocation() local host = coordinator_records[self].host if type(host) ~= "table" @@ -579,15 +589,28 @@ function SequenceCoordinator:primary(value) ) end +function SequenceCoordinator:execute_explicit_resolution(resolution) + if type(resolution) ~= "table" + or not domain.ExplicitRepeatRequest.is(resolution.request) + then + fail("explicit execution requires a resolved repeat request", 2) + end + coordinator_records[self].last_explicit_resolution = resolution + if resolution.request.neutral then + return domain.ActionOutcome.empty(resolution.invocation.position) + end + return self:execute_resolved_motion(resolution) +end + function SequenceCoordinator:repeat_same_direction() - return self:resolve_explicit_same() + return self:execute_explicit_resolution(self:resolve_explicit_same()) end SequenceCoordinator.RepeatSameDirection = SequenceCoordinator.repeat_same_direction function SequenceCoordinator:repeat_opposite_direction() - return self:resolve_explicit_opposite() + return self:execute_explicit_resolution(self:resolve_explicit_opposite()) end SequenceCoordinator.RepeatOppositeDirection = diff --git a/tests/run.lua b/tests/run.lua index 204a0f3..945d712 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -7721,7 +7721,7 @@ test("Same-direction explicit coordination reads current-context state", functio }) local resolution = sequence_coordinator.new({ host = host }) - :repeat_same_direction() + :resolve_explicit_same() same("explicit_same", resolution.kind) same(domain.ModeContext.from_full_mode("v"), resolution.invocation.context) @@ -7742,7 +7742,7 @@ test("Opposite explicit coordination swaps current-context descriptor", function }) local resolution = sequence_coordinator.new({ host = host }) - :repeat_opposite_direction() + :resolve_explicit_opposite() same("explicit_opposite", resolution.kind) same(domain.Descriptor.swap(stored), resolution.request.descriptor) @@ -7757,8 +7757,8 @@ test("Explicit coordination keeps code-zero target fallback", function() local host = MemoryHost.new() local coordinator = sequence_coordinator.new({ host = host }) - local same_repeat = coordinator:repeat_same_direction() - local opposite_repeat = coordinator:repeat_opposite_direction() + local same_repeat = coordinator:resolve_explicit_same() + local opposite_repeat = coordinator:resolve_explicit_opposite() for _, resolution in ipairs({ same_repeat, opposite_repeat }) do falsy(resolution.request.neutral) @@ -7782,7 +7782,7 @@ test("Explicit coordination builds target and resolved motion plans", function() }) local coordinator = sequence_coordinator.new({ host = host }) - local resolution = coordinator:repeat_opposite_direction() + local resolution = coordinator:resolve_explicit_opposite() same(stored_target, resolution.target) same(domain.CaseMode.INSENSITIVE, resolution.target_plan.case_mode) @@ -7793,6 +7793,32 @@ test("Explicit coordination builds target and resolved motion plans", function() same(domain.EndpointPolicy.REGULAR, resolution.motion_plan.endpoint_policy) end) +test("Explicit repeat actions execute through the resolved motion path", function() + local state, transitions = fresh_sequence_state() + local prior_landing = domain.Position.new(1, 1) + transitions:BeginAcquisition("n", "f") + transitions:CommitAcquiredTarget("n", target("a")) + transitions:CommitCommandSuccess("n", prior_landing, true) + local host = MemoryHost.new({ + buffer_lines = { "abaca" }, + cursor = prior_landing, + count = 2, + configuration = { mark_char = false }, + emit_movement_events = false, + }) + local facade = action_facade.new({ host = host }) + + local outcome = facade:repeat_same_direction() + + same(domain.ActionKind.MOVEMENT, outcome.kind) + same(domain.Position.new(1, 5), outcome.position) + same(outcome.position, host:read_cursor()) + same(outcome.position, state:get_previous_landing("n")) + local resolution = facade:coordinator():last_explicit_resolution() + same(domain.Descriptor.FIND_FORWARD, resolution.motion_plan.descriptor) + same(2, resolution.invocation.count.value) +end) + for _, item in ipairs(tests) do local ok, failure = xpcall(item.body, debug.traceback) if not ok then |
