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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
|
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.marker_position(descriptor, target_position)
domain.Descriptor.from_string(descriptor)
return domain.Position.coerce(target_position)
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 is_upper_ascii(character)
if type(character) ~= "string" or #character ~= 1 then
return false
end
local code = character:byte(1)
return code >= string.byte("A") and code <= string.byte("Z")
end
local function increment_smart_counters(counters, character)
local exact_count = increment_counter(counters, character)
local lowercase_count
if is_upper_ascii(character) then
local lowercase = string.char(character:byte(1) + 32)
lowercase_count = increment_counter(counters, lowercase)
end
return exact_count, lowercase_count
end
M.is_upper_ascii = is_upper_ascii
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 M.case_grouping_settings(settings)
settings = settings or {}
if type(settings) ~= "table" then
fail("direct preview case settings must be a table", 2)
end
return {
ignore_case = require_case_setting(settings, "ignore_case"),
smart_case = require_case_setting(settings, "smart_case"),
}
end
function M.validate_marker_positions(view, positions)
require_view(view)
if type(positions) ~= "table" then
fail("direct preview markers must be a list of positions", 2)
end
local result = {}
local seen = {}
local item_count = 0
for key, position in pairs(positions) do
if type(key) ~= "number"
or key ~= math.floor(key)
or key < 1
or key > #positions
then
fail("direct preview markers must be a list of positions", 2)
end
position = domain.Position.coerce(position)
if not view:is_character_start(position) then
fail("direct preview marker must start an editor character", 2)
end
local identity = tostring(position.line) .. ":" .. tostring(position.byte_column)
if seen[identity] then
fail("direct preview marker positions must be unique", 2)
end
seen[identity] = true
result[key] = position
item_count = item_count + 1
end
if item_count ~= #positions then
fail("direct preview markers must be a list of positions", 2)
end
return result
end
function DirectPreviewPlanner:plan(view, origin, descriptor, count, settings)
local grouping = M.case_grouping_settings(settings)
local ignore_case = grouping.ignore_case
local smart_case = grouping.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
local exact_count, lowercase_count = increment_smart_counters(
counters,
record.character
)
selected = exact_count == selected_occurrence
or lowercase_count == selected_occurrence
else
selected = increment_counter(
counters,
record.character
) == selected_occurrence
end
if selected then
positions[#positions + 1] = M.marker_position(descriptor, record.position)
end
end
return M.validate_marker_positions(view, 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
|