summaryrefslogtreecommitdiff
path: root/lua/clever_f/motion_executor.lua
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 10:47:53 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 10:47:53 +0200
commit3770a731f500e9253bbcf300c213276359a15686 (patch)
tree07cbf7d10bfcdf0d146d2691343e0228ff7389e9 /lua/clever_f/motion_executor.lua
parent768cb71841e9c2b0989d454e6d6804c0f2330eff (diff)
Save command origins
Diffstat (limited to 'lua/clever_f/motion_executor.lua')
-rw-r--r--lua/clever_f/motion_executor.lua159
1 files changed, 159 insertions, 0 deletions
diff --git a/lua/clever_f/motion_executor.lua b/lua/clever_f/motion_executor.lua
index 212a362..5dba08f 100644
--- a/lua/clever_f/motion_executor.lua
+++ b/lua/clever_f/motion_executor.lua
@@ -1,12 +1,22 @@
+local destination_engine = require("clever_f.destination_engine")
local domain = require("clever_f.domain")
+local text_topology = require("clever_f.text_topology")
local M = {}
+local MotionExecutor = {}
+M.MotionExecutor = MotionExecutor
M.ExecutionPath = {
VISUAL = "visual",
COMMAND = "command",
}
+local executor_records = setmetatable({}, { __mode = "k" })
+
+local function fail(message, level)
+ error(message, (level or 1) + 1)
+end
+
function M.execution_path(context)
context = domain.ModeContext.from_full_mode(context)
if context.visual_kind ~= nil then
@@ -15,4 +25,153 @@ function M.execution_path(context)
return M.ExecutionPath.COMMAND
end
+local function copy_options(options)
+ local result = {}
+ for key, value in pairs(options or {}) do
+ result[key] = value
+ end
+ return result
+end
+
+local function normalize_options(options, dependencies)
+ if MotionExecutor.is(options) and dependencies == nil then
+ return options
+ end
+ if type(options) ~= "table" then
+ fail("MotionExecutor options must be a table", 3)
+ end
+ if options.host ~= nil then
+ if dependencies ~= nil then
+ fail("MotionExecutor dependencies must be part of its options", 3)
+ end
+ return options
+ end
+ local result = copy_options(dependencies)
+ result.host = options
+ return result
+end
+
+local function require_host(host)
+ if type(host) ~= "table"
+ or type(host.read_cursor) ~= "function"
+ then
+ fail("MotionExecutor host must provide read_cursor", 3)
+ end
+ return host
+end
+
+local function require_destination_engine(engine)
+ engine = engine or destination_engine.new()
+ if type(engine) ~= "table" or type(engine.calculate) ~= "function" then
+ fail("MotionExecutor destination engine must provide calculate", 3)
+ end
+ return engine
+end
+
+local executor_metatable = {
+ __index = MotionExecutor,
+ __newindex = function()
+ fail("MotionExecutor values are immutable", 2)
+ end,
+ __tostring = function()
+ return "motion-executor"
+ end,
+ __metatable = "clever_f.motion_executor.MotionExecutor",
+}
+
+function MotionExecutor.new(options, dependencies)
+ options = normalize_options(options, dependencies)
+ if MotionExecutor.is(options) then
+ return options
+ end
+
+ local executor = setmetatable({}, executor_metatable)
+ executor_records[executor] = {
+ host = require_host(options.host),
+ destination_engine = require_destination_engine(
+ options.destination_engine or options.engine
+ ),
+ }
+ return executor
+end
+
+function MotionExecutor.is(value)
+ return type(value) == "table" and executor_records[value] ~= nil
+end
+
+local function execution_request(view, context, plan, count, first_move)
+ if not text_topology.TextView.is(view) then
+ fail("motion execution view must be a TextView", 3)
+ end
+ context = domain.ModeContext.from_full_mode(context)
+ if not domain.ResolvedMotionPlan.is(plan) then
+ fail("motion execution plan must be a ResolvedMotionPlan", 3)
+ end
+ count = domain.Count.new(count)
+ if type(first_move) ~= "boolean" then
+ fail("motion execution first_move must be a Boolean", 3)
+ end
+ return {
+ view = view,
+ context = context,
+ plan = plan,
+ count = count,
+ first_move = first_move,
+ }
+end
+
+local function calculate(executor, request, origin)
+ return executor_records[executor].destination_engine:calculate(
+ request.view,
+ origin,
+ request.plan,
+ request.count,
+ request.first_move
+ )
+end
+
+function MotionExecutor:_execute_command(request)
+ local host = executor_records[self].host
+ local origin = host:read_cursor()
+ local outcome = calculate(self, request, origin)
+ return domain.ActionOutcome.from_search(outcome, request.plan.descriptor)
+end
+
+function MotionExecutor:_execute_visual(request)
+ local host = executor_records[self].host
+ local origin = host:read_cursor()
+ local outcome = calculate(self, request, origin)
+ 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)
+ if M.execution_path(request.context) == M.ExecutionPath.VISUAL then
+ return self:_execute_visual(request)
+ end
+ return self:_execute_command(request)
+end
+
+function M.new(options, dependencies)
+ return MotionExecutor.new(options, dependencies)
+end
+
+function M.execute(host, view, context, plan, count, first_move, dependencies)
+ return MotionExecutor.new(host, dependencies):execute(
+ view,
+ context,
+ plan,
+ count,
+ first_move
+ )
+end
+
+M.run = M.execute
+
+setmetatable(M, {
+ __call = function(_, options, dependencies)
+ return MotionExecutor.new(options, dependencies)
+ end,
+})
+
return M