summaryrefslogtreecommitdiff
path: root/lua/clever_f/case_policy.lua
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-03 22:46:36 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-03 22:46:36 +0200
commit4ace71b059d8db228e05363cfee77586f52df136 (patch)
tree377123298921d2bda7673e70da6f1203f1a3ecb5 /lua/clever_f/case_policy.lua
parent6d9e5d7a852929f00b70b3dbb62b2f314f36242c (diff)
Implement base target matching
Diffstat (limited to 'lua/clever_f/case_policy.lua')
-rw-r--r--lua/clever_f/case_policy.lua190
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