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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
|
local domain = require("clever_tee.domain")
local sequence_state = require("clever_tee.sequence_state")
local state_transitions = require("clever_tee.state_transitions")
local text_topology = require("clever_tee.text_topology")
local M = {}
local MigemoCatalog = {}
local MigemoDictionary = {}
M.MigemoCatalog = MigemoCatalog
M.MigemoDictionary = MigemoDictionary
local catalog_records = setmetatable({}, { __mode = "k" })
local dictionary_records = setmetatable({}, { __mode = "k" })
local function fail(message, level)
error(message, (level or 1) + 1)
end
local function copy_list(values)
local result = {}
for index = 1, #values do
result[index] = values[index]
end
return result
end
local EXPECTED_KEYS = {}
for code = string.byte("a"), string.byte("z") do
EXPECTED_KEYS[#EXPECTED_KEYS + 1] = string.char(code)
end
for code = string.byte("A"), string.byte("Z") do
EXPECTED_KEYS[#EXPECTED_KEYS + 1] = string.char(code)
end
local EXPECTED_KEY_SET = {}
for _, key in ipairs(EXPECTED_KEYS) do
EXPECTED_KEY_SET[key] = true
end
local ASSETS = {
["utf-8"] = {
file = "utf8.vim",
function_name = "clever_tee#migemo#utf8#load_dict",
},
cp932 = {
file = "cp932.vim",
function_name = "clever_tee#migemo#cp932#load_dict",
},
["euc-jp"] = {
file = "eucjp.vim",
function_name = "clever_tee#migemo#eucjp#load_dict",
},
}
local module_source = debug.getinfo(1, "S").source
local module_file = module_source:sub(1, 1) == "@" and module_source:sub(2) or module_source
local bundled_root = module_file:match("^(.*)/lua/clever_tee/migemo_catalog%.lua$")
local function require_nonempty_string(value, name)
if type(value) ~= "string" or value == "" then
fail((name or "value") .. " must be a nonempty string", 2)
end
return value
end
local function asset_path(asset)
if bundled_root == nil then
fail("Migemo catalog could not locate its bundled assets", 2)
end
return bundled_root .. "/autoload/clever_tee/migemo/" .. asset.file
end
local function read_asset_key_order(path)
local handle, open_error = io.open(path, "rb")
if handle == nil then
fail("Migemo asset could not be opened: " .. tostring(open_error), 2)
end
local keys = {}
for line in handle:lines() do
local key = line:match("^%s*\\%s*'([A-Za-z])'%s*:")
if key ~= nil then
keys[#keys + 1] = key
end
end
handle:close()
return keys
end
local function assert_key_order(keys, encoding)
if type(keys) ~= "table" or #keys ~= #EXPECTED_KEYS then
fail(
"Migemo " .. encoding .. " asset must contain exactly 52 ordered keys",
2
)
end
for index, expected in ipairs(EXPECTED_KEYS) do
if keys[index] ~= expected then
fail(
"Migemo " .. encoding
.. " asset keys must be ordered a through z, then A through Z",
2
)
end
end
end
local function nvim_runtime()
local runtime = rawget(_G, "vim")
if type(runtime) ~= "table"
or runtime.cmd == nil
or type(runtime.fn) ~= "table"
or type(runtime.fn.fnameescape) ~= "function"
then
fail("Migemo dictionary loading requires Nvim", 2)
end
return runtime
end
local function default_asset_loader(encoding, asset)
local runtime = nvim_runtime()
local path = asset_path(asset)
local keys = read_asset_key_order(path)
assert_key_order(keys, encoding)
runtime.cmd("silent source " .. runtime.fn.fnameescape(path))
local loader = runtime.fn[asset.function_name]
if type(loader) ~= "function" then
fail("Migemo " .. encoding .. " asset did not define its dictionary loader", 2)
end
local dictionary = loader()
return dictionary, keys, path
end
local function explicit_pattern(pattern, case_mode)
local case_flag = case_mode == domain.CaseMode.INSENSITIVE and "\\c" or "\\C"
return "\\m" .. case_flag .. "^" .. pattern
end
local function default_pattern_compiler(pattern, key, encoding)
local runtime = nvim_runtime()
if type(runtime.regex) ~= "function" or type(runtime.fn.match) ~= "function" then
fail("Migemo pattern evaluation requires Nvim regular expressions", 2)
end
local sensitive = explicit_pattern(pattern, domain.CaseMode.SENSITIVE)
local insensitive = explicit_pattern(pattern, domain.CaseMode.INSENSITIVE)
local ok, compile_error = pcall(runtime.regex, sensitive)
if ok then
ok, compile_error = pcall(runtime.regex, insensitive)
end
if not ok then
fail(
"Migemo " .. encoding .. " pattern for '" .. key
.. "' could not be compiled: " .. tostring(compile_error),
2
)
end
return function(text, case_mode)
if type(text) ~= "string" then
fail("Migemo assertion text must be a string", 2)
end
case_mode = domain.CaseMode.from_string(case_mode)
local selected = case_mode == domain.CaseMode.INSENSITIVE
and insensitive
or sensitive
local matched, start_or_error = pcall(runtime.fn.match, text, selected)
if not matched then
fail(
"Migemo " .. encoding .. " pattern for '" .. key
.. "' could not be evaluated: " .. tostring(start_or_error),
2
)
end
return start_or_error == 0
end
end
local dictionary_metatable = {
__index = function(dictionary, key)
local method = MigemoDictionary[key]
if method ~= nil then
return method
end
local record = dictionary_records[dictionary]
if key == "encoding" or key == "effective_encoding" then
return record.encoding
end
if key == "entry_count" then
return #record.keys
end
if key == "asset_path" then
return record.asset_path
end
if EXPECTED_KEY_SET[key] then
return record.predicates[key]
end
return nil
end,
__newindex = function()
fail("MigemoDictionary values are immutable", 2)
end,
__tostring = function(dictionary)
return "migemo-dictionary:" .. dictionary_records[dictionary].encoding
end,
__metatable = "clever_tee.migemo_catalog.MigemoDictionary",
}
local function validate_dictionary_data(data, ordered_keys, encoding)
if type(data) ~= "table" then
fail("Migemo " .. encoding .. " asset must return a dictionary", 3)
end
assert_key_order(ordered_keys, encoding)
local count = 0
for key, pattern in pairs(data) do
count = count + 1
if EXPECTED_KEY_SET[key] ~= true then
fail("Migemo " .. encoding .. " asset contains an unexpected key", 3)
end
if type(pattern) ~= "string" or pattern == "" then
fail("Migemo " .. encoding .. " patterns must be nonempty strings", 3)
end
end
if count ~= #EXPECTED_KEYS then
fail("Migemo " .. encoding .. " asset must contain exactly 52 keys", 3)
end
for _, key in ipairs(EXPECTED_KEYS) do
if data[key] == nil then
fail("Migemo " .. encoding .. " asset is missing key '" .. key .. "'", 3)
end
end
end
local function new_dictionary(encoding, data, ordered_keys, path, compiler)
validate_dictionary_data(data, ordered_keys, encoding)
local patterns = {}
local predicates = {}
for _, key in ipairs(EXPECTED_KEYS) do
local pattern = data[key]
patterns[key] = pattern
local predicate = compiler(pattern, key, encoding)
if type(predicate) ~= "function" then
fail("Migemo pattern compiler must return a predicate", 3)
end
predicates[key] = predicate
end
local dictionary = setmetatable({}, dictionary_metatable)
dictionary_records[dictionary] = {
encoding = encoding,
keys = copy_list(ordered_keys),
patterns = patterns,
predicates = predicates,
asset_path = path,
}
return dictionary
end
function MigemoDictionary.is(value)
return type(value) == "table" and dictionary_records[value] ~= nil
end
local function dictionary_record(dictionary)
if not MigemoDictionary.is(dictionary) then
fail("value must be a MigemoDictionary", 3)
end
return dictionary_records[dictionary]
end
function MigemoDictionary:keys()
return copy_list(dictionary_record(self).keys)
end
function MigemoDictionary:has(key)
return type(key) == "string"
and dictionary_record(self).predicates[key] ~= nil
end
function MigemoDictionary:pattern(key)
require_nonempty_string(key, "Migemo dictionary key")
local pattern = dictionary_record(self).patterns[key]
if pattern == nil then
fail("Migemo dictionary key must be one ASCII alphabetic character", 2)
end
return pattern
end
function MigemoDictionary:predicate(key, case_mode)
require_nonempty_string(key, "Migemo dictionary key")
local predicate = dictionary_record(self).predicates[key]
if predicate == nil then
fail("Migemo dictionary key must be one ASCII alphabetic character", 2)
end
if case_mode == nil then
return predicate
end
case_mode = domain.CaseMode.from_string(case_mode)
return function(text)
return predicate(text, case_mode)
end
end
function MigemoDictionary:matches(key, text, case_mode)
return self:predicate(key)(text, case_mode)
end
function MigemoDictionary:to_table()
local record = dictionary_record(self)
return {
encoding = record.encoding,
entry_count = #record.keys,
keys = copy_list(record.keys),
asset_path = record.asset_path,
}
end
local function normalize_catalog_options(options)
if options == nil then
return {}
end
if type(options) ~= "table" then
fail("MigemoCatalog options must be a table", 3)
end
if type(options.disable_migemo_for_unsupported_encoding) == "function"
and options.policy == nil
and options.policy_service == nil
and options.transitions == nil
and options.state == nil
and options.asset_loader == nil
and options.pattern_compiler == nil
then
return { policy = options }
end
return options
end
local function require_policy(service)
if service ~= nil and (type(service) ~= "table"
or type(service.disable_migemo_for_unsupported_encoding) ~= "function")
then
fail(
"MigemoCatalog policy must provide disable_migemo_for_unsupported_encoding",
3
)
end
return service
end
local function require_transitions(transitions, state)
transitions = transitions or state_transitions.new(state)
if type(transitions) ~= "table"
or type(transitions.CacheMigemo) ~= "function"
or type(transitions.state) ~= "function"
or transitions:state() ~= state
then
fail("MigemoCatalog transitions must mutate its SequenceState", 3)
end
return transitions
end
local function selected_function(value, fallback, name)
value = value or fallback
if type(value) ~= "function" then
fail("MigemoCatalog " .. name .. " must be a function", 3)
end
return value
end
local catalog_metatable = {
__index = MigemoCatalog,
__newindex = function()
fail("MigemoCatalog values are immutable", 2)
end,
__tostring = function()
return "migemo-catalog"
end,
__metatable = "clever_tee.migemo_catalog.MigemoCatalog",
}
function MigemoCatalog.new(options)
if MigemoCatalog.is(options) then
return options
end
options = normalize_catalog_options(options)
local state = options.state or sequence_state.get()
if not sequence_state.is(state) then
fail("MigemoCatalog requires the plugin-global SequenceState", 2)
end
local catalog = setmetatable({}, catalog_metatable)
catalog_records[catalog] = {
state = state,
transitions = require_transitions(options.transitions, state),
policy = require_policy(options.policy or options.policy_service),
disable_migemo = options.disable_migemo,
asset_loader = selected_function(
options.asset_loader,
default_asset_loader,
"asset_loader"
),
pattern_compiler = selected_function(
options.pattern_compiler,
default_pattern_compiler,
"pattern_compiler"
),
load_counts = {},
}
if catalog_records[catalog].disable_migemo ~= nil
and type(catalog_records[catalog].disable_migemo) ~= "function"
then
fail("MigemoCatalog disable_migemo must be a function", 2)
end
return catalog
end
function MigemoCatalog.is(value)
return type(value) == "table" and catalog_records[value] ~= nil
end
function M.new(options)
return MigemoCatalog.new(options)
end
setmetatable(M, {
__call = function(_, options)
return MigemoCatalog.new(options)
end,
})
local function catalog_record(catalog)
if not MigemoCatalog.is(catalog) then
fail("value must be a MigemoCatalog", 3)
end
return catalog_records[catalog]
end
local function unsupported(catalog, requested_encoding, policy_override)
local record = catalog_record(catalog)
local active_policy = policy_override or record.policy
if active_policy ~= nil then
require_policy(active_policy):disable_migemo_for_unsupported_encoding()
elseif record.disable_migemo ~= nil then
record.disable_migemo()
end
error(
"clever-tee: Encoding '" .. requested_encoding
.. "' is not supported. Migemo is disabled",
0
)
end
function MigemoCatalog:get(effective_encoding, policy_override)
local requested = require_nonempty_string(effective_encoding, "effective encoding")
local encoding = text_topology.normalize_encoding(requested)
local asset = ASSETS[encoding]
if asset == nil then
return unsupported(self, requested, policy_override)
end
local record = catalog_record(self)
local cached = record.state:get_migemo(encoding)
if cached ~= nil then
if not MigemoDictionary.is(cached) then
fail("Migemo cache contains an invalid dictionary", 2)
end
return cached
end
local data, ordered_keys, path = record.asset_loader(encoding, asset)
local dictionary = new_dictionary(
encoding,
data,
ordered_keys,
path,
record.pattern_compiler
)
record.transitions:CacheMigemo(encoding, dictionary)
record.load_counts[encoding] = (record.load_counts[encoding] or 0) + 1
return dictionary
end
function MigemoCatalog:load_count(effective_encoding)
local encoding = text_topology.normalize_encoding(effective_encoding)
return catalog_record(self).load_counts[encoding] or 0
end
function MigemoCatalog:cached(effective_encoding)
local encoding = text_topology.normalize_encoding(effective_encoding)
local value = catalog_record(self).state:get_migemo(encoding)
if value ~= nil and not MigemoDictionary.is(value) then
fail("Migemo cache contains an invalid dictionary", 2)
end
return value
end
MigemoCatalog.load = MigemoCatalog.get
MigemoCatalog.select = MigemoCatalog.get
MigemoCatalog.dictionary = MigemoCatalog.get
function M.expected_keys()
return copy_list(EXPECTED_KEYS)
end
function M.supported_encodings()
return { "utf-8", "cp932", "euc-jp" }
end
function M.bundled_asset_path(effective_encoding)
local encoding = text_topology.normalize_encoding(effective_encoding)
local asset = ASSETS[encoding]
if asset == nil then
return nil
end
return asset_path(asset)
end
M.load = function(effective_encoding, options)
return MigemoCatalog.new(options):get(effective_encoding)
end
M.EXPECTED_ENTRY_COUNT = #EXPECTED_KEYS
return M
|