diff options
| author | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-03 22:46:36 +0200 |
|---|---|---|
| committer | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-03 22:46:36 +0200 |
| commit | 4ace71b059d8db228e05363cfee77586f52df136 (patch) | |
| tree | 377123298921d2bda7673e70da6f1203f1a3ecb5 /lua/clever_f/target_plan.lua | |
| parent | 6d9e5d7a852929f00b70b3dbb62b2f314f36242c (diff) | |
Implement base target matching
Diffstat (limited to 'lua/clever_f/target_plan.lua')
| -rw-r--r-- | lua/clever_f/target_plan.lua | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/lua/clever_f/target_plan.lua b/lua/clever_f/target_plan.lua new file mode 100644 index 0000000..385b111 --- /dev/null +++ b/lua/clever_f/target_plan.lua @@ -0,0 +1,306 @@ +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 TargetPlanFactory = {} +M.TargetPlanFactory = TargetPlanFactory + +M.SYMBOLS = "!\"#$%&'()=~|\\-^@`[]{};:+*<>,.?_/" + +local SYMBOL_CHARACTERS = {} +local SYMBOL_SET = {} +for index = 1, #M.SYMBOLS do + local character = M.SYMBOLS:sub(index, index) + SYMBOL_CHARACTERS[index] = character + SYMBOL_SET[character] = true +end + +local factory_records = setmetatable({}, { __mode = "k" }) + +local function fail(message, level) + error(message, (level or 1) + 1) +end + +local function require_target(target) + if not domain.TargetValue.is(target) then + fail("target plan target must be a TargetValue", 2) + end + return target +end + +local function require_boolean(value, name) + if type(value) ~= "boolean" then + fail("match policy " .. name .. " must be a Boolean", 2) + end + return value +end + +local function require_string(value, name) + if type(value) ~= "string" then + fail("match policy " .. name .. " must be a string", 2) + end + return value +end + +local function copy_list(values) + local result = {} + for index = 1, #values do + result[index] = values[index] + end + return result +end + +function M.symbol_characters() + return copy_list(SYMBOL_CHARACTERS) +end + +function M.is_symbol(character) + return type(character) == "string" and SYMBOL_SET[character] == true +end + +local function require_character_list(characters, source) + if type(characters) ~= "table" then + fail("editor character splitter must return a list", 3) + end + + local result = {} + local item_count = 0 + for key, character in pairs(characters) do + if type(key) ~= "number" + or key ~= math.floor(key) + or key < 1 + or key > #characters + or type(character) ~= "string" + or character == "" + then + fail("editor character splitter must return a list of nonempty strings", 3) + end + result[key] = character + item_count = item_count + 1 + end + if item_count ~= #characters or table.concat(result) ~= source then + fail("editor character splitter must preserve the configured trigger string", 3) + end + return result +end + +function M.parse_trigger_characters(value, splitter) + if type(value) ~= "string" then + fail("chars_match_any_signs must be a string", 2) + end + splitter = splitter or text_topology.split_editor_characters + if type(splitter) ~= "function" then + fail("editor character splitter must be a function", 2) + end + return require_character_list(splitter(value), value) +end + +local function trigger_set(value, splitter) + local result = {} + for _, character in ipairs(M.parse_trigger_characters(value, splitter)) do + result[character] = true + end + return result +end + +local function false_matcher() + return false +end + +local function symbol_matcher(candidate_character) + return M.is_symbol(candidate_character) +end + +local function normalize_factory_options(options) + if options == nil then + return {} + end + if type(options) == "function" then + return { lowercase = options } + end + if type(options) ~= "table" then + fail("TargetPlanFactory options must be a table", 3) + end + if type(options.sample_match) == "function" + and options.policy == nil + and options.policy_service == nil + and options.case_resolver == nil + and options.lowercase == nil + and options.splitter == nil + and options.split_editor_characters == nil + then + return { policy = options } + end + return options +end + +local function require_policy_service(service) + if service ~= nil and (type(service) ~= "table" + or type(service.sample_match) ~= "function") + then + fail("TargetPlanFactory policy must provide sample_match", 3) + end + return service +end + +local function require_case_resolver(resolver, options) + if resolver == nil then + return case_policy.new({ + lowercase = options.lowercase, + }) + end + if type(resolver) ~= "table" + or type(resolver.resolve) ~= "function" + or type(resolver.comparator) ~= "function" + then + fail("TargetPlanFactory case resolver is invalid", 3) + end + return resolver +end + +local function require_splitter(splitter) + splitter = splitter or text_topology.split_editor_characters + if type(splitter) ~= "function" then + fail("TargetPlanFactory editor character splitter must be a function", 3) + end + return splitter +end + +local factory_metatable = { + __index = TargetPlanFactory, + __newindex = function() + fail("TargetPlanFactory values are immutable", 2) + end, + __tostring = function() + return "target-plan-factory" + end, + __metatable = "clever_f.target_plan.TargetPlanFactory", +} + +function TargetPlanFactory.new(options) + if TargetPlanFactory.is(options) then + return options + end + options = normalize_factory_options(options) + local factory = setmetatable({}, factory_metatable) + factory_records[factory] = { + policy = require_policy_service(options.policy or options.policy_service), + case_resolver = require_case_resolver(options.case_resolver, options), + splitter = require_splitter( + options.split_editor_characters or options.splitter + ), + } + return factory +end + +function TargetPlanFactory.is(value) + return type(value) == "table" and factory_records[value] ~= nil +end + +function M.new(options) + return TargetPlanFactory.new(options) +end + +setmetatable(M, { + __call = function(_, options) + return TargetPlanFactory.new(options) + end, +}) + +local function default_match_policy() + return { + ignore_case = false, + smart_case = false, + chars_match_any_signs = "", + } +end + +local function sampled_policy(factory, target, match_policy) + local service + if match_policy == nil then + service = factory_records[factory].policy + if service == nil then + return default_match_policy() + end + elseif type(match_policy) == "table" + and type(match_policy.sample_match) == "function" + then + service = match_policy + end + + if service ~= nil then + match_policy = service:sample_match(target) + end + if type(match_policy) ~= "table" then + fail("target match policy must be a table", 3) + end + + return { + ignore_case = require_boolean(match_policy.ignore_case, "ignore_case"), + smart_case = require_boolean(match_policy.smart_case, "smart_case"), + chars_match_any_signs = require_string( + match_policy.chars_match_any_signs, + "chars_match_any_signs" + ), + } +end + +local function new_plan(target, kind, case_mode, matcher) + return domain.TargetPlan.new({ + target = target, + kind = kind, + case_mode = case_mode, + matcher = matcher, + }) +end + +function TargetPlanFactory:build(target, match_policy) + target = require_target(target) + local record = factory_records[self] + local sampled = sampled_policy(self, target, match_policy) + local case_mode = record.case_resolver:resolve( + target, + sampled.ignore_case, + sampled.smart_case + ) + + if target.first_code == 0x80 then + return new_plan( + target, + domain.TargetPlanKind.EMPTY, + case_mode, + false_matcher + ) + end + + local triggers = trigger_set(sampled.chars_match_any_signs, record.splitter) + if triggers[target.value] then + return new_plan( + target, + domain.TargetPlanKind.SYMBOL, + case_mode, + symbol_matcher + ) + end + + local kind = target.value == "\\" + and domain.TargetPlanKind.BACKSLASH + or domain.TargetPlanKind.LITERAL + return new_plan( + target, + kind, + case_mode, + record.case_resolver:comparator(target.value, case_mode) + ) +end + +function M.build(target, match_policy, options) + return TargetPlanFactory.new(options):build(target, match_policy) +end + +M.create = M.build +M.create_plan = M.build +M.SYMBOL_SET_STRING = M.SYMBOLS + +return M |
