summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 13:48:22 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 13:48:22 +0200
commit6c87eb3b6103c638cf76c7509ab2da6bcea67ec8 (patch)
treebc83e928dc48ff5aa41bc1ebf3ca435c90d68871
parent7ec26094fee806bf53f3eb253b57f3a06025c70d (diff)
Execute explicit repeat motions
-rw-r--r--lua/clever_f/action_facade.lua20
-rw-r--r--lua/clever_f/sequence_coordinator.lua63
-rw-r--r--tests/run.lua36
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