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 candidate_starts(request, origin) return request.view:iter_strict( origin, request.plan.descriptor.direction, request.bounds ) end local function regular_destination(request, target_position) local descriptor = request.plan.descriptor if descriptor.family == domain.Family.FIND then return target_position end if descriptor.direction == domain.Direction.FORWARD then return request.view:predecessor(target_position) end return request.view:successor(target_position) end local function target_destination(request, target_position) local descriptor = request.plan.descriptor if request.plan.endpoint_policy == domain.EndpointPolicy.VISUAL_EXCLUSIVE and descriptor.direction == domain.Direction.FORWARD then if descriptor.family == domain.Family.FIND then return request.view:successor(target_position) end return target_position end return regular_destination(request, target_position) end local function strict_destination(descriptor, destination, origin) local comparison = domain.Position.compare(destination, origin) if descriptor.direction == domain.Direction.FORWARD then return comparison > 0 end return comparison < 0 end local function acceptable_destination( request, destination, origin, allow_till_equality ) local descriptor = request.plan.descriptor if strict_destination(descriptor, destination, origin) then return true end return descriptor.family == domain.Family.TILL and allow_till_equality and domain.Position.equal(destination, origin) end local function next_destination(request, origin, allow_till_equality) local candidates = candidate_starts(request, origin) while true do local target_position, character = candidates() if target_position == nil then return nil end if request.plan.target_plan:matches( character, target_position, request.view ) then local destination = target_destination(request, target_position) if destination ~= nil and acceptable_destination( request, destination, origin, allow_till_equality ) then return destination end end end end local function till_equality_allowed(request, successful_steps) return request.first_move and successful_steps == 0 end function DestinationEngine:calculate(view, origin, plan, count, first_move) local request = calculation_inputs(view, origin, plan, count, first_move) local destination = next_destination( request, request.origin, till_equality_allowed(request, 0) ) if destination == nil then return domain.SearchOutcome.boundary_before_any(request.origin) end return domain.SearchOutcome.complete(destination, 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