diff options
| author | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 18:44:32 +0200 |
|---|---|---|
| committer | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 18:44:32 +0200 |
| commit | c7b14ffb6d14969c9c41864e827f33f8e80fc24e (patch) | |
| tree | c7c0ac23dc93a4a8b75449d2be4d0e05c4f05f9e /lua/clever_tee/destination_engine.lua | |
| parent | 9013636a57144e1f57c9339e7888d588430aae6a (diff) | |
Rename plugin to clever-tee
Diffstat (limited to 'lua/clever_tee/destination_engine.lua')
| -rw-r--r-- | lua/clever_tee/destination_engine.lua | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/lua/clever_tee/destination_engine.lua b/lua/clever_tee/destination_engine.lua new file mode 100644 index 0000000..0469bb2 --- /dev/null +++ b/lua/clever_tee/destination_engine.lua @@ -0,0 +1,201 @@ +local domain = require("clever_tee.domain") +local text_topology = require("clever_tee.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_tee.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 + +local function boundary_outcome(request, endpoint, successful_steps) + if successful_steps > 0 then + return domain.SearchOutcome.boundary_after_partial( + endpoint, + successful_steps + ) + end + return domain.SearchOutcome.boundary_before_any(request.origin) +end + +function DestinationEngine:calculate(view, origin, plan, count, first_move) + local request = calculation_inputs(view, origin, plan, count, first_move) + local current_origin = request.origin + local successful_steps = 0 + + while successful_steps < request.count.value do + local destination = next_destination( + request, + current_origin, + till_equality_allowed(request, successful_steps) + ) + if destination == nil then + break + end + current_origin = destination + successful_steps = successful_steps + 1 + end + + if successful_steps == request.count.value then + return domain.SearchOutcome.complete(current_origin, successful_steps) + end + return boundary_outcome(request, current_origin, successful_steps) +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 |
