local domain = require("clever_f.domain") local text_topology = require("clever_f.text_topology") local M = {} local DestinationEngine = {} M.DestinationEngine = DestinationEngine local engines = setmetatable({}, { __mode = "k" }) local function fail(message, level) error(message, (level or 1) + 1) end local engine_metatable = { __index = DestinationEngine, __newindex = function() fail("DestinationEngine values are immutable", 2) end, __tostring = function() return "destination-engine" end, __metatable = "clever_f.destination_engine.DestinationEngine", } function DestinationEngine.new() local engine = setmetatable({}, engine_metatable) engines[engine] = true return engine end function DestinationEngine.is(value) return type(value) == "table" and engines[value] == true end local function calculation_inputs(view, origin, plan, count, first_move) if not text_topology.TextView.is(view) then fail("destination calculation view must be a TextView", 3) end origin = domain.Position.coerce(origin) if not view:is_valid_cursor_position(origin) then fail("destination calculation origin must be a valid cursor position", 3) end if not domain.ResolvedMotionPlan.is(plan) then fail("destination calculation plan must be a ResolvedMotionPlan", 3) end count = domain.Count.new(count) if type(first_move) ~= "boolean" then fail("destination calculation first_move must be a Boolean", 3) end local bounds = view:match_start_bounds(plan.search_scope, origin) return { view = view, origin = origin, plan = plan, count = count, first_move = first_move, bounds = bounds, } end local function next_matching_start(request, origin) local candidates = request.view:iter_strict( origin, request.plan.descriptor.direction, request.bounds ) while true do local position, character = candidates() if position == nil then return nil end if request.plan.target_plan:matches(character, position, request.view) then return position end end end function DestinationEngine:calculate(view, origin, plan, count, first_move) local request = calculation_inputs(view, origin, plan, count, first_move) local target_position = next_matching_start(request, request.origin) if target_position == nil then return domain.SearchOutcome.boundary_before_any(request.origin) end return domain.SearchOutcome.complete(target_position, 1) end function M.new() return DestinationEngine.new() end function M.calculate(view, origin, plan, count, first_move) return DestinationEngine.new():calculate(view, origin, plan, count, first_move) end M.resolve = M.calculate M.search = M.calculate setmetatable(M, { __call = function() return DestinationEngine.new() end, }) return M