summaryrefslogtreecommitdiff
path: root/lua/clever_f/direct_preview_planner.lua
blob: a034c3c991e02728c7844697cec6bdabf6168f3e (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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
local case_policy = require("clever_f.case_policy")
local domain = require("clever_f.domain")
local text_topology = require("clever_f.text_topology")

local M = {}
local DirectPreviewPlanner = {}
DirectPreviewPlanner.__index = DirectPreviewPlanner
M.DirectPreviewPlanner = DirectPreviewPlanner

local planner_records = setmetatable({}, { __mode = "k" })

local function fail(message, level)
  error(message, (level or 1) + 1)
end

local function require_view(view)
  if not text_topology.TextView.is(view) then
    fail("direct preview text must be a TextView", 3)
  end
  return view
end

local function require_case_resolver(options)
  options = options or {}
  if type(options) == "function" then
    options = { lowercase = options }
  end
  if type(options) ~= "table" then
    fail("DirectPreviewPlanner options must be a table", 3)
  end
  local resolver = options.case_resolver
  if resolver == nil then
    resolver = case_policy.new({ lowercase = options.lowercase })
  end
  if type(resolver) ~= "table" or type(resolver.lowercase) ~= "function" then
    fail("DirectPreviewPlanner case resolver must provide lowercase", 3)
  end
  return resolver
end

function DirectPreviewPlanner.new(options)
  if DirectPreviewPlanner.is(options) then
    return options
  end
  local planner = setmetatable({}, DirectPreviewPlanner)
  planner_records[planner] = {
    case_resolver = require_case_resolver(options),
  }
  return planner
end

function DirectPreviewPlanner.is(value)
  return type(value) == "table" and planner_records[value] ~= nil
end

function DirectPreviewPlanner:scan_current_line(view, origin, direction)
  require_view(view)
  origin = domain.Position.coerce(origin)
  direction = domain.Direction.from_string(direction)
  local records = {}
  for position, character, span in view:iter_strict(
    origin,
    direction,
    domain.SearchScope.CURRENT_LINE
  ) do
    records[#records + 1] = {
      position = position,
      character = character,
      span = span,
    }
  end
  return records
end

function M.normalize_count(count)
  return domain.Count.to_number(count)
end

function DirectPreviewPlanner:normalize_count(count)
  return M.normalize_count(count)
end

function M.direction_for_descriptor(descriptor)
  descriptor = domain.Descriptor.from_string(descriptor)
  if domain.Descriptor.is_uppercase(descriptor) then
    return domain.Direction.BACKWARD
  end
  return domain.Direction.FORWARD
end

function DirectPreviewPlanner:scan_for_descriptor(view, origin, descriptor)
  return self:scan_current_line(
    view,
    origin,
    M.direction_for_descriptor(descriptor)
  )
end

local function increment_counter(counters, character)
  local value = (counters[character] or 0) + 1
  counters[character] = value
  return value
end

local function increment_smart_counters(counters, character)
  return increment_counter(counters, character)
end

local function require_case_setting(settings, name)
  local value = settings[name]
  if value == nil then
    return false
  end
  if type(value) ~= "boolean" then
    fail("direct preview " .. name .. " must be a Boolean", 3)
  end
  return value
end

function DirectPreviewPlanner:plan(view, origin, descriptor, count, settings)
  settings = settings or {}
  if type(settings) ~= "table" then
    fail("direct preview case settings must be a table", 2)
  end
  local ignore_case = require_case_setting(settings, "ignore_case")
  local smart_case = require_case_setting(settings, "smart_case")
  local selected_occurrence = self:normalize_count(count)
  local counters = {}
  local positions = {}
  local resolver = planner_records[self].case_resolver
  for _, record in ipairs(self:scan_for_descriptor(view, origin, descriptor)) do
    local selected
    if ignore_case then
      selected = increment_counter(
        counters,
        resolver:lowercase(record.character)
      ) == selected_occurrence
    elseif smart_case then
      selected = increment_smart_counters(
        counters,
        record.character
      ) == selected_occurrence
    else
      selected = increment_counter(
        counters,
        record.character
      ) == selected_occurrence
    end
    if selected then
      positions[#positions + 1] = record.position
    end
  end
  return positions
end

function M.new(options)
  return DirectPreviewPlanner.new(options)
end

function M.scan_current_line(view, origin, direction)
  return DirectPreviewPlanner.new():scan_current_line(view, origin, direction)
end

function M.scan_for_descriptor(view, origin, descriptor)
  return DirectPreviewPlanner.new():scan_for_descriptor(view, origin, descriptor)
end

function M.plan(view, origin, descriptor, count, settings, options)
  return DirectPreviewPlanner.new(options):plan(
    view,
    origin,
    descriptor,
    count,
    settings
  )
end

setmetatable(M, {
  __call = function(_, options)
    return DirectPreviewPlanner.new(options)
  end,
})

return M