summaryrefslogtreecommitdiff
path: root/lua/clever_f/destination_engine.lua
blob: 836d1901cce934191b68066323552d801434d80e (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
128
129
130
131
132
133
134
135
136
137
138
139
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)
  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

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)
  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