summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 13:43:51 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 13:43:51 +0200
commitf3000e4cab6255680e77256140c96a6decdba9c9 (patch)
tree3e1f17dd1f6c9808b6cbbfcfcdcc7aa740384578
parentebe532dc47dd094e33fa157a11ce192754eb473f (diff)
Execute resolved primary motions
-rw-r--r--lua/clever_f/sequence_coordinator.lua60
-rw-r--r--tests/run.lua68
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