summaryrefslogtreecommitdiff
path: root/lua/clever_f
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-03 23:07:34 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-03 23:07:34 +0200
commit3b1447358ce9ddddd454e080003c94c494320e6d (patch)
tree9f7d906d63ca9ab7392015148e112b341e515ebf /lua/clever_f
parent4ace71b059d8db228e05363cfee77586f52df136 (diff)
Implement Migemo target matching
Diffstat (limited to 'lua/clever_f')
-rw-r--r--lua/clever_f/domain.lua8
-rw-r--r--lua/clever_f/migemo_catalog.lua527
-rw-r--r--lua/clever_f/target_plan.lua241
-rw-r--r--lua/clever_f/text_topology.lua22
4 files changed, 795 insertions, 3 deletions
diff --git a/lua/clever_f/domain.lua b/lua/clever_f/domain.lua
index a5b8d39..6a40092 100644
--- a/lua/clever_f/domain.lua
+++ b/lua/clever_f/domain.lua
@@ -987,6 +987,14 @@ function TargetPlan:matches(...)
return self.matcher(...)
end
+function TargetPlan:matches_at(text_view, position)
+ if type(text_view) ~= "table" or type(text_view.character_at) ~= "function" then
+ fail("target plan match requires a text view", 2)
+ end
+ position = Position.coerce(position)
+ return self.matcher(text_view:character_at(position), position, text_view)
+end
+
function TargetPlan:to_table()
return {
target = self.target:to_table(),
diff --git a/lua/clever_f/migemo_catalog.lua b/lua/clever_f/migemo_catalog.lua
new file mode 100644
index 0000000..18d9541
--- /dev/null
+++ b/lua/clever_f/migemo_catalog.lua
@@ -0,0 +1,527 @@
+local domain = require("clever_f.domain")
+local sequence_state = require("clever_f.sequence_state")
+local state_transitions = require("clever_f.state_transitions")
+local text_topology = require("clever_f.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_f#migemo#utf8#load_dict",
+ },
+ cp932 = {
+ file = "cp932.vim",
+ function_name = "clever_f#migemo#cp932#load_dict",
+ },
+ ["euc-jp"] = {
+ file = "eucjp.vim",
+ function_name = "clever_f#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_f/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_f/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_f.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_f.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-f: 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
diff --git a/lua/clever_f/target_plan.lua b/lua/clever_f/target_plan.lua
index 385b111..78d0b41 100644
--- a/lua/clever_f/target_plan.lua
+++ b/lua/clever_f/target_plan.lua
@@ -1,5 +1,6 @@
local case_policy = require("clever_f.case_policy")
local domain = require("clever_f.domain")
+local migemo_catalog = require("clever_f.migemo_catalog")
local text_topology = require("clever_f.text_topology")
local M = {}
@@ -167,6 +168,13 @@ local function require_splitter(splitter)
return splitter
end
+local function require_migemo_catalog(catalog)
+ if catalog ~= nil and not migemo_catalog.MigemoCatalog.is(catalog) then
+ fail("TargetPlanFactory migemo_catalog must be a MigemoCatalog", 3)
+ end
+ return catalog
+end
+
local factory_metatable = {
__index = TargetPlanFactory,
__newindex = function()
@@ -190,6 +198,9 @@ function TargetPlanFactory.new(options)
splitter = require_splitter(
options.split_editor_characters or options.splitter
),
+ migemo_catalog = require_migemo_catalog(
+ options.migemo_catalog or options.catalog
+ ),
}
return factory
end
@@ -212,6 +223,7 @@ local function default_match_policy()
return {
ignore_case = false,
smart_case = false,
+ use_migemo = false,
chars_match_any_signs = "",
}
end
@@ -236,9 +248,15 @@ local function sampled_policy(factory, target, match_policy)
fail("target match policy must be a table", 3)
end
+ local use_migemo = match_policy.use_migemo
+ if use_migemo == nil then
+ use_migemo = false
+ end
+
return {
ignore_case = require_boolean(match_policy.ignore_case, "ignore_case"),
smart_case = require_boolean(match_policy.smart_case, "smart_case"),
+ use_migemo = require_boolean(use_migemo, "use_migemo"),
chars_match_any_signs = require_string(
match_policy.chars_match_any_signs,
"chars_match_any_signs"
@@ -255,7 +273,169 @@ local function new_plan(target, kind, case_mode, matcher)
})
end
-function TargetPlanFactory:build(target, match_policy)
+local function is_ascii_alphabetic(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"))
+ or (code >= string.byte("A") and code <= string.byte("Z"))
+end
+
+M.is_ascii_alphabetic = is_ascii_alphabetic
+
+local function context_field(context, primary, alternate)
+ local value = context[primary]
+ if value == nil and alternate ~= nil then
+ value = context[alternate]
+ end
+ return value
+end
+
+local function context_table(match_policy, search_context)
+ if text_topology.TextView.is(search_context) then
+ return { text_view = search_context }
+ end
+ if search_context ~= nil and type(search_context) ~= "table" then
+ fail("target search context must be a table or TextView", 3)
+ end
+
+ local context = search_context or {}
+ if search_context == nil and type(match_policy) == "table" then
+ if match_policy.text_view ~= nil
+ or match_policy.view ~= nil
+ or match_policy.search_scope ~= nil
+ or match_policy.scope ~= nil
+ or match_policy.origin ~= nil
+ or match_policy.current_line ~= nil
+ or match_policy.effective_encoding ~= nil
+ or match_policy.encoding ~= nil
+ then
+ context = match_policy
+ end
+ end
+ return context
+end
+
+local function active_policy_service(factory, match_policy)
+ if type(match_policy) == "table"
+ and type(match_policy.sample_match) == "function"
+ then
+ return match_policy
+ end
+ return factory_records[factory].policy
+end
+
+local function search_scope(factory, match_policy, context)
+ local value = context_field(context, "search_scope", "scope")
+ if value == nil and type(match_policy) == "table" then
+ value = match_policy.search_scope
+ if value == nil and match_policy.search_current_line_only ~= nil then
+ value = match_policy.search_current_line_only
+ and domain.SearchScope.CURRENT_LINE
+ or domain.SearchScope.BUFFER
+ end
+ end
+ if value == nil then
+ local service = active_policy_service(factory, match_policy)
+ if service ~= nil and type(service.sample_search) == "function" then
+ value = service:sample_search().search_scope
+ end
+ end
+ if value == nil then
+ return domain.SearchScope.BUFFER
+ end
+ if value == "line" then
+ value = domain.SearchScope.CURRENT_LINE
+ end
+ return domain.SearchScope.from_string(value)
+end
+
+local function migemo_search_context(factory, match_policy, search_context)
+ local context = context_table(match_policy, search_context)
+ local view = context_field(context, "text_view", "view")
+ if not text_topology.TextView.is(view) then
+ fail("Migemo target planning requires a TextView", 3)
+ end
+
+ local scope = search_scope(factory, match_policy, context)
+ local origin = context.origin
+ if origin == nil then
+ origin = context.current_line
+ end
+ if scope == domain.SearchScope.CURRENT_LINE and origin == nil then
+ fail("current-line Migemo planning requires an origin line", 3)
+ end
+
+ local line_number
+ if scope == domain.SearchScope.CURRENT_LINE then
+ line_number = type(origin) == "number"
+ and origin
+ or domain.Position.coerce(origin).line
+ end
+
+ local encoding = context_field(context, "effective_encoding", "encoding")
+ or view.requested_encoding
+ or view.effective_encoding
+ return {
+ view = view,
+ scope = scope,
+ origin = origin,
+ line_number = line_number,
+ encoding = encoding,
+ bounds = view:match_start_bounds(scope, origin),
+ }
+end
+
+local function selected_migemo_catalog(factory, match_policy)
+ local record = factory_records[factory]
+ if record.migemo_catalog == nil then
+ record.migemo_catalog = migemo_catalog.new({
+ policy = active_policy_service(factory, match_policy),
+ })
+ end
+ return record.migemo_catalog
+end
+
+local function migemo_matcher(
+ target_character,
+ case_mode,
+ resolver,
+ dictionary,
+ context
+)
+ local target_equal = resolver:comparator(target_character, case_mode)
+ local assertion = dictionary:predicate(target_character, case_mode)
+
+ return function(candidate_character, candidate_position, candidate_view)
+ if candidate_position == nil then
+ fail("Migemo matching requires a candidate Position", 2)
+ end
+ local position = domain.Position.coerce(candidate_position)
+ local view = candidate_view or context.view
+ if not text_topology.TextView.is(view) then
+ fail("Migemo matching requires a TextView", 2)
+ end
+ if not context.bounds:contains(position)
+ or not view:is_character_start(position)
+ then
+ return false
+ end
+
+ local actual_character = view:character_at(position)
+ if candidate_character ~= actual_character then
+ return false
+ end
+ if is_ascii_alphabetic(actual_character)
+ and not target_equal(actual_character)
+ then
+ return false
+ end
+ return assertion(view:text_suffix(position))
+ end
+end
+
+function TargetPlanFactory:build(target, match_policy, search_context)
target = require_target(target)
local record = factory_records[self]
local sampled = sampled_policy(self, target, match_policy)
@@ -274,6 +454,31 @@ function TargetPlanFactory:build(target, match_policy)
)
end
+ if sampled.use_migemo and is_ascii_alphabetic(target.value) then
+ local context = migemo_search_context(self, match_policy, search_context)
+ local active = context.scope == domain.SearchScope.BUFFER
+ or context.view:line_byte_length(context.line_number)
+ > context.view:line_character_count(context.line_number)
+ if active then
+ local dictionary = selected_migemo_catalog(self, match_policy):get(
+ context.encoding,
+ active_policy_service(self, match_policy)
+ )
+ return new_plan(
+ target,
+ domain.TargetPlanKind.MIGEMO,
+ case_mode,
+ migemo_matcher(
+ target.value,
+ case_mode,
+ record.case_resolver,
+ dictionary,
+ context
+ )
+ )
+ end
+ end
+
local triggers = trigger_set(sampled.chars_match_any_signs, record.splitter)
if triggers[target.value] then
return new_plan(
@@ -295,8 +500,38 @@ function TargetPlanFactory:build(target, match_policy)
)
end
-function M.build(target, match_policy, options)
- return TargetPlanFactory.new(options):build(target, match_policy)
+local function is_search_context(value)
+ return text_topology.TextView.is(value)
+ or (type(value) == "table" and (
+ value.text_view ~= nil
+ or value.view ~= nil
+ or value.search_scope ~= nil
+ or value.scope ~= nil
+ or value.origin ~= nil
+ or value.current_line ~= nil
+ or value.effective_encoding ~= nil
+ or value.encoding ~= nil
+ ))
+end
+
+function M.build(target, match_policy, options, search_context)
+ if search_context == nil and is_search_context(options) then
+ search_context = options
+ options = nil
+ end
+ return TargetPlanFactory.new(options):build(
+ target,
+ match_policy,
+ search_context
+ )
+end
+
+function M.build_for_view(target, view, origin, scope, match_policy, options)
+ return TargetPlanFactory.new(options):build(target, match_policy, {
+ text_view = view,
+ origin = origin,
+ search_scope = scope,
+ })
end
M.create = M.build
diff --git a/lua/clever_f/text_topology.lua b/lua/clever_f/text_topology.lua
index 2941cf2..56f3b9c 100644
--- a/lua/clever_f/text_topology.lua
+++ b/lua/clever_f/text_topology.lua
@@ -418,6 +418,26 @@ function TextView:line_encoded_text(line_number)
return line_record(self, line_number).encoded
end
+function TextView:text_suffix(position)
+ position = domain.Position.coerce(position)
+ local record = view_record(self)
+ local line = line_record(self, position.line)
+ local character_index = self:character_index_for_byte_column(
+ position.line,
+ position.byte_column
+ )
+ local parts = {}
+
+ for index = character_index, line.character_count do
+ parts[#parts + 1] = line.entries[index].character
+ end
+ for line_number = position.line + 1, record.snapshot.line_count do
+ parts[#parts + 1] = "\n"
+ parts[#parts + 1] = record.lines[line_number].text
+ end
+ return table.concat(parts)
+end
+
function TextView:line_byte_length(line_number)
return line_record(self, line_number).byte_length
end
@@ -970,6 +990,8 @@ TextView.character_index_to_byte_column = TextView.byte_column_for_character_ind
TextView.byte_column_to_character_index = TextView.character_index_for_byte_column
TextView.character_count = TextView.line_character_count
TextView.byte_length = TextView.line_byte_length
+TextView.text_from = TextView.text_suffix
+TextView.suffix_from = TextView.text_suffix
TextView.character_span = TextView.byte_span_for_character_index
TextView.predecessor_endpoint = TextView.predecessor
TextView.successor_endpoint = TextView.successor