summaryrefslogtreecommitdiff
path: root/lua/clever_tee/destination_engine.lua
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 18:44:32 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 18:44:32 +0200
commitc7b14ffb6d14969c9c41864e827f33f8e80fc24e (patch)
treec7c0ac23dc93a4a8b75449d2be4d0e05c4f05f9e /lua/clever_tee/destination_engine.lua
parent9013636a57144e1f57c9339e7888d588430aae6a (diff)
Rename plugin to clever-tee
Diffstat (limited to 'lua/clever_tee/destination_engine.lua')
-rw-r--r--lua/clever_tee/destination_engine.lua201
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