summaryrefslogtreecommitdiff
path: root/lua/clever_f/target_plan.lua
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/target_plan.lua
parent4ace71b059d8db228e05363cfee77586f52df136 (diff)
Implement Migemo target matching
Diffstat (limited to 'lua/clever_f/target_plan.lua')
-rw-r--r--lua/clever_f/target_plan.lua241
1 files changed, 238 insertions, 3 deletions
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