summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 10:57:10 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 10:57:10 +0200
commitb7af716a32e548987e3853f5e91198e53d051d27 (patch)
tree659bcd3372f8c015b02c1da9c4f4c32483aaea18
parentbe971036f82c688e0a07fa2e3ff286e414cc5c1d (diff)
Execute resolved dot payloads
-rw-r--r--lua/clever_f/motion_executor.lua96
-rw-r--r--lua/clever_f/testing/memory_host.lua4
-rw-r--r--tests/run.lua64
3 files changed, 159 insertions, 5 deletions
diff --git a/lua/clever_f/motion_executor.lua b/lua/clever_f/motion_executor.lua
index 062a57f..79cc2ab 100644
--- a/lua/clever_f/motion_executor.lua
+++ b/lua/clever_f/motion_executor.lua
@@ -52,6 +52,24 @@ function M.create_dot_payload(plan)
return domain.DotPayload.new(plan.descriptor, plan.target_plan.target)
end
+function M.plan_for_dot_payload(plan, payload)
+ if not domain.ResolvedMotionPlan.is(plan) then
+ fail("dot replay plan must be a ResolvedMotionPlan", 2)
+ end
+ if not domain.DotPayload.is(payload) then
+ fail("dot replay payload must be a DotPayload", 2)
+ end
+ if payload.target ~= plan.target_plan.target then
+ fail("dot replay payload target must match its resolved target plan", 2)
+ end
+ return domain.ResolvedMotionPlan.new({
+ target_plan = plan.target_plan,
+ descriptor = payload.descriptor,
+ search_scope = plan.search_scope,
+ endpoint_policy = plan.endpoint_policy,
+ })
+end
+
local function copy_options(options)
local result = {}
for key, value in pairs(options or {}) do
@@ -81,8 +99,10 @@ end
local function require_host(host)
if type(host) ~= "table"
or type(host.read_cursor) ~= "function"
+ or type(host.read_encoding) ~= "function"
or type(host.read_pending_operator) ~= "function"
or type(host.read_selection) ~= "function"
+ or type(host.read_text) ~= "function"
or type(host.apply_cursor) ~= "function"
or type(host.apply_selection) ~= "function"
or type(host.set_operator_inclusive) ~= "function"
@@ -169,7 +189,14 @@ function MotionExecutor.is(value)
return type(value) == "table" and executor_records[value] ~= nil
end
-local function execution_request(view, context, plan, count, first_move)
+local function execution_request(
+ view,
+ context,
+ plan,
+ count,
+ first_move,
+ execution_options
+)
if not text_topology.TextView.is(view) then
fail("motion execution view must be a TextView", 3)
end
@@ -181,12 +208,28 @@ local function execution_request(view, context, plan, count, first_move)
if type(first_move) ~= "boolean" then
fail("motion execution first_move must be a Boolean", 3)
end
+ execution_options = execution_options or {}
+ if type(execution_options) ~= "table" then
+ fail("motion execution options must be a table", 3)
+ end
+ local dot_payload = execution_options.dot_payload
+ if dot_payload ~= nil and not domain.DotPayload.is(dot_payload) then
+ fail("motion execution dot_payload must be a DotPayload", 3)
+ end
+ local register_dot_repeat = execution_options.register_dot_repeat
+ if register_dot_repeat == nil then
+ register_dot_repeat = true
+ elseif type(register_dot_repeat) ~= "boolean" then
+ fail("motion execution register_dot_repeat must be a Boolean", 3)
+ end
return {
view = view,
context = context,
plan = plan,
count = count,
first_move = first_move,
+ dot_payload = dot_payload,
+ register_dot_repeat = register_dot_repeat,
}
end
@@ -218,6 +261,19 @@ local function command_action(
})
end
+local function register_dot_replay(executor, request, payload)
+ local host = executor_records[executor].host
+ host:register_dot_repeat(payload, function(replayed_payload, replay_count)
+ return executor:execute_dot(
+ text_topology.from_host(host),
+ request.context,
+ request.plan,
+ replayed_payload,
+ replay_count
+ )
+ end)
+end
+
local function migrate_command_feedback(executor, request, origin, outcome)
local record = executor_records[executor]
local feedback = record.feedback_service
@@ -280,8 +336,10 @@ function MotionExecutor:_execute_command(request)
)
local dot_payload
if pending_operator ~= nil and pending_operator ~= "" then
- dot_payload = M.create_dot_payload(request.plan)
- host:register_dot_repeat(dot_payload)
+ dot_payload = request.dot_payload or M.create_dot_payload(request.plan)
+ if request.register_dot_repeat then
+ register_dot_replay(self, request, dot_payload)
+ end
end
return command_action(
host,
@@ -317,14 +375,42 @@ function MotionExecutor:_execute_visual(request)
return domain.ActionOutcome.from_search(outcome, request.plan.descriptor)
end
-function MotionExecutor:execute(view, context, plan, count, first_move)
- local request = execution_request(view, context, plan, count, first_move)
+function MotionExecutor:execute(
+ view,
+ context,
+ plan,
+ count,
+ first_move,
+ execution_options
+)
+ local request = execution_request(
+ view,
+ context,
+ plan,
+ count,
+ first_move,
+ execution_options
+ )
if M.execution_path(request.context) == M.ExecutionPath.VISUAL then
return self:_execute_visual(request)
end
return self:_execute_command(request)
end
+function MotionExecutor:execute_dot(view, context, plan, payload, count)
+ return self:execute(
+ view,
+ context,
+ M.plan_for_dot_payload(plan, payload),
+ count,
+ false,
+ {
+ dot_payload = payload,
+ register_dot_repeat = false,
+ }
+ )
+end
+
function M.new(options, dependencies)
return MotionExecutor.new(options, dependencies)
end
diff --git a/lua/clever_f/testing/memory_host.lua b/lua/clever_f/testing/memory_host.lua
index 8b473ad..42dab71 100644
--- a/lua/clever_f/testing/memory_host.lua
+++ b/lua/clever_f/testing/memory_host.lua
@@ -952,6 +952,8 @@ function MemoryHost:register_dot_repeat(payload, callback)
self._dot_repeat = {
payload = payload,
callback = callback,
+ operator = self._pending_operator,
+ mode = self._mode,
}
self:_record("register_dot_repeat", { payload = payload:to_table() })
return payload
@@ -965,6 +967,8 @@ function MemoryHost:replay_dot(count)
if self._dot_repeat == nil or self._dot_repeat.callback == nil then
error("dot repeat is not executable", 2)
end
+ self._pending_operator = self._dot_repeat.operator
+ self._mode = self._dot_repeat.mode
return self._dot_repeat.callback(self._dot_repeat.payload, domain.Count.new(count))
end
diff --git a/tests/run.lua b/tests/run.lua
index 5dfe425..a237bd9 100644
--- a/tests/run.lua
+++ b/tests/run.lua
@@ -3639,6 +3639,70 @@ test("Operator success creates a resolved dot payload", function()
same(outcome.dot_payload, host:dot_repeat_payload())
end)
+test("Dot payload replay uses resolved motions and current counts", function()
+ fresh_sequence_state()
+ local target_value = target("e")
+ local host = MemoryHost.new({
+ buffer_lines = { "hoge fuge piye poye" },
+ cursor = { line = 1, byte_column = 1 },
+ mode = "no",
+ pending_operator = "delete",
+ })
+ local plan = motion_plan.build(
+ target_plan.build(target_value, matching_policy()),
+ "f"
+ )
+ local executor = motion_executor.new(host)
+ local outcomes = {
+ executor:execute(
+ text_topology.from_host(host),
+ "no",
+ plan,
+ 1,
+ true
+ ),
+ }
+ local buffers = { host:read_text():line(1) }
+ for _ = 1, 3 do
+ outcomes[#outcomes + 1] = host:replay_dot(1)
+ buffers[#buffers + 1] = host:read_text():line(1)
+ end
+
+ list_same({
+ " fuge piye poye",
+ " piye poye",
+ " poye",
+ "",
+ }, buffers)
+ for _, outcome in ipairs(outcomes) do
+ same(domain.ActionKind.MOVEMENT, outcome.kind)
+ same(domain.Descriptor.FIND_FORWARD, outcome.effective_descriptor)
+ same(domain.Position.new(1, 1), outcome.position)
+ truthy(domain.DotPayload.is(outcome.dot_payload))
+ same(target_value, outcome.dot_payload.target)
+ end
+
+ fresh_sequence_state()
+ local counted_host = MemoryHost.new({
+ buffer_lines = { "hoge fuge piye poye" },
+ cursor = { line = 1, byte_column = 1 },
+ mode = "no",
+ pending_operator = "delete",
+ })
+ local counted_executor = motion_executor.new(counted_host)
+ counted_executor:execute(
+ text_topology.from_host(counted_host),
+ "no",
+ plan,
+ 1,
+ true
+ )
+ local counted = counted_host:replay_dot(2)
+ same(domain.ActionKind.MOVEMENT, counted.kind)
+ same(" poye", counted_host:read_text():line(1))
+ same(2, counted.successful_steps)
+end)
+
for _, item in ipairs(tests) do
local ok, failure = xpcall(item.body, debug.traceback)
if not ok then