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 return M.ExecutionPath.VISUAL end return M.ExecutionPath.COMMAND end function M.moved_forward(origin, destination) origin = domain.Position.coerce(origin) destination = domain.Position.coerce(destination) return domain.Position.compare(destination, origin) > 0 end function M.command_moved_forward(descriptor, origin, destination) descriptor = domain.Descriptor.from_string(descriptor) origin = domain.Position.coerce(origin) destination = domain.Position.coerce(destination) if descriptor.family == domain.Family.TILL and domain.Position.stationary(origin, destination) then return false end return M.moved_forward(origin, destination) 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" or type(host.apply_cursor) ~= "function" then fail("MotionExecutor host must provide cursor movement", 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) if outcome.successful_steps > 0 then host:apply_cursor(outcome.endpoint) end if not outcome.complete then return domain.ActionOutcome.from_search(outcome, request.plan.descriptor) end request.moved_forward = M.command_moved_forward( request.plan.descriptor, origin, outcome.endpoint ) 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