summaryrefslogtreecommitdiff
path: root/lua/clever_f/destination_engine.lua
blob: ed3b7b24baf848ecb2d00b8a5ffe76503c757ac5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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 next_matching_start(request, origin)
  local candidates = candidate_starts(request, origin)

  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

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)
  return regular_destination(request, target_position)
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
  local destination = target_destination(request, target_position)
  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