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/case_policy.lua | |
| parent | 6d9e5d7a852929f00b70b3dbb62b2f314f36242c (diff) | |
Implement base target matching
Diffstat (limited to 'lua/clever_f/case_policy.lua')
| -rw-r--r-- | lua/clever_f/case_policy.lua | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/lua/clever_f/case_policy.lua b/lua/clever_f/case_policy.lua new file mode 100644 index 0000000..2631d5b --- /dev/null +++ b/lua/clever_f/case_policy.lua @@ -0,0 +1,190 @@ +local domain = require("clever_f.domain") + +local M = {} +local CasePolicyResolver = {} +M.CasePolicyResolver = CasePolicyResolver + +local resolver_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("case policy target must be a TargetValue", 2) + end + return target +end + +local function require_boolean(value, name) + if type(value) ~= "boolean" then + fail((name or "value") .. " must be a Boolean", 2) + end + return value +end + +local function require_string(value, name, allow_empty) + if type(value) ~= "string" or (not allow_empty and value == "") then + fail((name or "value") .. " must be a string", 2) + end + return value +end + +local function default_lowercase(value) + local runtime = rawget(_G, "vim") + if type(runtime) ~= "table" + or type(runtime.fn) ~= "table" + or type(runtime.fn.tolower) ~= "function" + then + fail("editor-compatible case conversion requires Nvim or a lowercase converter", 2) + end + return runtime.fn.tolower(value) +end + +local function lowercase_function(options) + if options == nil then + return default_lowercase + end + if type(options) == "function" then + return options + end + if type(options) ~= "table" then + fail("case policy options must be a table or lowercase function", 3) + end + local lowercase = options.lowercase or options.to_lower + if lowercase == nil then + return default_lowercase + end + if type(lowercase) ~= "function" then + fail("case policy lowercase converter must be a function", 3) + end + return lowercase +end + +local resolver_metatable = { + __index = CasePolicyResolver, + __newindex = function() + fail("CasePolicyResolver values are immutable", 2) + end, + __tostring = function() + return "case-policy-resolver" + end, + __metatable = "clever_f.case_policy.CasePolicyResolver", +} + +function CasePolicyResolver.new(options) + if CasePolicyResolver.is(options) then + return options + end + local resolver = setmetatable({}, resolver_metatable) + resolver_records[resolver] = { + lowercase = lowercase_function(options), + } + return resolver +end + +function CasePolicyResolver.is(value) + return type(value) == "table" and resolver_records[value] ~= nil +end + +function M.new(options) + return CasePolicyResolver.new(options) +end + +setmetatable(M, { + __call = function(_, options) + return CasePolicyResolver.new(options) + end, +}) + +function M.is_lower_ascii(value) + if domain.TargetValue.is(value) then + value = value.value + end + if type(value) ~= "string" or #value ~= 1 then + return false + end + local byte = value:byte(1) + return byte >= string.byte("a") and byte <= string.byte("z") +end + +function M.resolve_case_mode(target, ignore_case, smart_case) + target = require_target(target) + require_boolean(ignore_case, "ignore_case") + require_boolean(smart_case, "smart_case") + + if ignore_case then + return domain.CaseMode.INSENSITIVE + end + if smart_case and M.is_lower_ascii(target) then + return domain.CaseMode.INSENSITIVE + end + return domain.CaseMode.SENSITIVE +end + +function CasePolicyResolver:resolve(target, ignore_case, smart_case) + if type(ignore_case) == "table" and smart_case == nil then + local match_policy = ignore_case + ignore_case = match_policy.ignore_case + smart_case = match_policy.smart_case + end + return M.resolve_case_mode(target, ignore_case, smart_case) +end + +function CasePolicyResolver:lowercase(value) + require_string(value, "case comparison value", true) + local lowercase = resolver_records[self].lowercase(value) + if type(lowercase) ~= "string" then + fail("case policy lowercase converter must return a string", 2) + end + return lowercase +end + +function CasePolicyResolver:equal(left, right, case_mode) + require_string(left, "left case comparison value", true) + require_string(right, "right case comparison value", true) + case_mode = domain.CaseMode.from_string(case_mode) + + if case_mode == domain.CaseMode.SENSITIVE then + return left == right + end + return self:lowercase(left) == self:lowercase(right) +end + +function CasePolicyResolver:comparator(target_character, case_mode) + require_string(target_character, "target character", true) + case_mode = domain.CaseMode.from_string(case_mode) + + if target_character == "" then + return function() + return false + end + end + + if case_mode == domain.CaseMode.SENSITIVE then + return function(candidate_character) + return type(candidate_character) == "string" + and candidate_character ~= "" + and candidate_character == target_character + end + end + + local folded_target = self:lowercase(target_character) + local lowercase = resolver_records[self].lowercase + return function(candidate_character) + if type(candidate_character) ~= "string" or candidate_character == "" then + return false + end + local folded_candidate = lowercase(candidate_character) + if type(folded_candidate) ~= "string" then + fail("case policy lowercase converter must return a string", 2) + end + return folded_candidate == folded_target + end +end + +M.resolve = M.resolve_case_mode +M.is_lowercase_ascii = M.is_lower_ascii + +return M |
