summaryrefslogtreecommitdiff
path: root/tests/run.lua
diff options
context:
space:
mode:
Diffstat (limited to 'tests/run.lua')
-rw-r--r--tests/run.lua338
1 files changed, 337 insertions, 1 deletions
diff --git a/tests/run.lua b/tests/run.lua
index 1cf9053..9034b97 100644
--- a/tests/run.lua
+++ b/tests/run.lua
@@ -8,10 +8,12 @@ package.path = table.concat({
local domain = require("clever_f.domain")
local capabilities = require("clever_f.capabilities")
+local case_policy = require("clever_f.case_policy")
local policy = require("clever_f.policy")
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 target_plan = require("clever_f.target_plan")
local MemoryHost = require("clever_f.testing.memory_host")
local tests = {}
@@ -1483,6 +1485,340 @@ test("All-empty buffers expose empty full-buffer iteration", function()
same(0, #position_strings(collect_iteration(view:iter_buffer_backward())))
end)
+local function target(character, first_code)
+ return domain.TargetValue.character(
+ character,
+ first_code or string.byte(character, 1)
+ )
+end
+
+local function matching_policy(overrides)
+ local result = {
+ ignore_case = false,
+ smart_case = false,
+ chars_match_any_signs = "",
+ }
+ for key, value in pairs(overrides or {}) do
+ result[key] = value
+ end
+ return result
+end
+
+test("Case mode follows ignore-case and lower-ASCII smart-case priority", function()
+ local resolver = case_policy.new()
+
+ for code = string.byte("a"), string.byte("z") do
+ local lower = target(string.char(code), code)
+ same(
+ domain.CaseMode.INSENSITIVE,
+ resolver:resolve(lower, false, true),
+ "smart case must fold lower ASCII"
+ )
+ same(
+ domain.CaseMode.INSENSITIVE,
+ policy.resolve_case_mode(lower, false, true)
+ )
+ end
+
+ for code = string.byte("A"), string.byte("Z") do
+ local upper = target(string.char(code), code)
+ same(domain.CaseMode.SENSITIVE, resolver:resolve(upper, false, true))
+ same(domain.CaseMode.INSENSITIVE, resolver:resolve(upper, true, true))
+ end
+
+ local multibyte = target("\195\164", 0x00e4)
+ local symbol = target(";", string.byte(";"))
+ local control = target(string.char(1), 1)
+ same(domain.CaseMode.SENSITIVE, resolver:resolve(multibyte, false, true))
+ same(domain.CaseMode.SENSITIVE, resolver:resolve(symbol, false, true))
+ same(domain.CaseMode.SENSITIVE, resolver:resolve(control, false, true))
+ same(domain.CaseMode.INSENSITIVE, resolver:resolve(multibyte, true, false))
+ truthy(case_policy.is_lower_ascii("a"))
+ falsy(case_policy.is_lower_ascii("A"))
+ falsy(case_policy.is_lower_ascii("aa"))
+ falsy(case_policy.is_lower_ascii("\195\164"))
+ fails(function()
+ resolver.lowercase = string.lower
+ end, "immutable")
+end)
+
+test("Case comparison uses editor lowercase conversion explicitly", function()
+ local resolver = case_policy.new()
+ local upper_a_umlaut = "\195\132"
+ local lower_a_umlaut = "\195\164"
+
+ truthy(resolver:equal("A", "a", domain.CaseMode.INSENSITIVE))
+ falsy(resolver:equal("A", "a", domain.CaseMode.SENSITIVE))
+ truthy(resolver:equal(
+ upper_a_umlaut,
+ lower_a_umlaut,
+ domain.CaseMode.INSENSITIVE
+ ))
+ same(vim.fn.tolower(upper_a_umlaut), resolver:lowercase(upper_a_umlaut))
+
+ local calls = {}
+ local injected = case_policy.new(function(value)
+ calls[#calls + 1] = value
+ return value == "UP" and "folded" or value
+ end)
+ local compare = injected:comparator("UP", domain.CaseMode.INSENSITIVE)
+ truthy(compare("folded"))
+ list_same({ "UP", "folded" }, calls)
+end)
+
+test("Trigger parsing uses complete editor characters", function()
+ local combining_character = "e\204\129"
+ local japanese_character = "\227\129\130"
+ local configured = "x" .. combining_character .. japanese_character
+ list_same(
+ { "x", combining_character, japanese_character },
+ text_topology.split_editor_characters(configured)
+ )
+ list_same(
+ { "x", combining_character, japanese_character },
+ target_plan.parse_trigger_characters(configured)
+ )
+
+ local factory = target_plan.new()
+ local plan = factory:build(
+ target(combining_character, string.byte("e")),
+ matching_policy({ chars_match_any_signs = configured })
+ )
+ same(domain.TargetPlanKind.SYMBOL, plan.kind)
+ truthy(plan:matches("!"))
+ falsy(plan:matches(combining_character))
+end)
+
+test("Symbol plans match the exact shared 32-character set", function()
+ local expected_symbols = "!\"#$%&'()=~|\\-^@`[]{};:+*<>,.?_/"
+ same(expected_symbols, target_plan.SYMBOLS)
+ same(32, #target_plan.symbol_characters())
+
+ local expected = {}
+ for index = 1, #expected_symbols do
+ expected[expected_symbols:sub(index, index)] = true
+ end
+
+ local plan = target_plan.build(
+ target(";"),
+ matching_policy({
+ ignore_case = true,
+ chars_match_any_signs = ";",
+ })
+ )
+ same(domain.TargetPlanKind.SYMBOL, plan.kind)
+ same(domain.CaseMode.INSENSITIVE, plan.case_mode)
+
+ local accepted = 0
+ for code = 0, 127 do
+ local character = string.char(code)
+ local matches = plan:matches(character)
+ same(expected[character] == true, matches, "ASCII code " .. tostring(code))
+ if matches then
+ accepted = accepted + 1
+ end
+ end
+ same(32, accepted)
+ falsy(plan:matches(" "))
+ falsy(plan:matches("a"))
+ falsy(plan:matches("Z"))
+ falsy(plan:matches("0"))
+ falsy(plan:matches("9"))
+ falsy(plan:matches("\227\129\130"))
+end)
+
+test("Every configured editor character selects the symbol branch", function()
+ local factory = target_plan.new()
+ local cases = {
+ { target("a"), "a" },
+ { target(";"), ";" },
+ { target("\227\129\130", 0x3042), "x\227\129\130y" },
+ }
+
+ for _, case in ipairs(cases) do
+ local plan = factory:build(
+ case[1],
+ matching_policy({ chars_match_any_signs = case[2] })
+ )
+ same(domain.TargetPlanKind.SYMBOL, plan.kind)
+ truthy(plan:matches("!"))
+ truthy(plan:matches("/"))
+ same(target_plan.is_symbol(case[1].value), plan:matches(case[1].value))
+ end
+end)
+
+test("Pattern punctuation stays literal when it is not a trigger", function()
+ local factory = target_plan.new()
+ local pattern_characters = {
+ "^", "[", "]", "(", ")", ".", "*", "+", "?", "$", "%", "-", "|",
+ }
+
+ for _, character in ipairs(pattern_characters) do
+ local plan = factory:build(target(character), matching_policy())
+ same(domain.TargetPlanKind.LITERAL, plan.kind, character)
+ truthy(plan:matches(character), character)
+ falsy(plan:matches("x"), character)
+ end
+
+ local backslash = factory:build(target("\\"), matching_policy())
+ same(domain.TargetPlanKind.BACKSLASH, backslash.kind)
+ truthy(backslash:matches("\\"))
+ falsy(backslash:matches("\\\\"))
+ falsy(backslash:matches("/"))
+end)
+
+test("Symbol selection precedes the literal backslash branch", function()
+ local factory = target_plan.new()
+ local literal = factory:build(target("\\"), matching_policy())
+ local wildcard = factory:build(
+ target("\\"),
+ matching_policy({ chars_match_any_signs = "\\" })
+ )
+
+ same(domain.TargetPlanKind.BACKSLASH, literal.kind)
+ same(domain.TargetPlanKind.SYMBOL, wildcard.kind)
+ truthy(wildcard:matches("."))
+ truthy(wildcard:matches("\\"))
+ falsy(wildcard:matches("a"))
+end)
+
+test("Literal plans apply ignore-case and smart-case modes", function()
+ local factory = target_plan.new()
+ local lower_smart = factory:build(
+ target("a"),
+ matching_policy({ smart_case = true })
+ )
+ local upper_smart = factory:build(
+ target("A"),
+ matching_policy({ smart_case = true })
+ )
+ local upper_ignored = factory:build(
+ target("A"),
+ matching_policy({ ignore_case = true, smart_case = true })
+ )
+ local multibyte_smart = factory:build(
+ target("\195\164", 0x00e4),
+ matching_policy({ smart_case = true })
+ )
+ local multibyte_ignored = factory:build(
+ target("\195\132", 0x00c4),
+ matching_policy({ ignore_case = true })
+ )
+
+ same(domain.CaseMode.INSENSITIVE, lower_smart.case_mode)
+ truthy(lower_smart:matches("A"))
+ same(domain.CaseMode.SENSITIVE, upper_smart.case_mode)
+ falsy(upper_smart:matches("a"))
+ same(domain.CaseMode.INSENSITIVE, upper_ignored.case_mode)
+ truthy(upper_ignored:matches("a"))
+ same(domain.CaseMode.SENSITIVE, multibyte_smart.case_mode)
+ falsy(multibyte_smart:matches("\195\132"))
+ same(domain.CaseMode.INSENSITIVE, multibyte_ignored.case_mode)
+ truthy(multibyte_ignored:matches("\195\164"))
+end)
+
+test("Matching ignores ambient editor case options", function()
+ local saved_ignorecase = vim.o.ignorecase
+ local saved_smartcase = vim.o.smartcase
+ vim.o.ignorecase = true
+ vim.o.smartcase = true
+ local sensitive = target_plan.build(target("a"), matching_policy())
+ local sensitive_match = sensitive:matches("A")
+
+ vim.o.ignorecase = false
+ vim.o.smartcase = false
+ local insensitive = target_plan.build(
+ target("a"),
+ matching_policy({ ignore_case = true })
+ )
+ local insensitive_match = insensitive:matches("A")
+ vim.o.ignorecase = saved_ignorecase
+ vim.o.smartcase = saved_smartcase
+
+ falsy(sensitive_match)
+ truthy(insensitive_match)
+end)
+
+test("Special keys are empty plans and controls remain literal", function()
+ local factory = target_plan.new()
+ local special = domain.TargetValue.special_key(string.char(0x80, 0xfd, 1))
+ local empty = factory:build(
+ special,
+ matching_policy({ chars_match_any_signs = string.char(0x80) })
+ )
+ same(domain.TargetPlanKind.EMPTY, empty.kind)
+ falsy(empty:matches("a"))
+ falsy(empty:matches("!"))
+
+ local first_code_controls = target(string.char(0x80), 0x80)
+ same(
+ domain.TargetPlanKind.EMPTY,
+ factory:build(first_code_controls, matching_policy()).kind
+ )
+
+ for _, code in ipairs({ 1, 9, 13, 26, 31, 127 }) do
+ local character = string.char(code)
+ local plan = factory:build(target(character, code), matching_policy())
+ same(domain.TargetPlanKind.LITERAL, plan.kind)
+ truthy(plan:matches(character))
+ falsy(plan:matches(string.char((code + 1) % 128)))
+ end
+
+ local fallback = factory:build(
+ domain.TargetValue.code_fallback(),
+ matching_policy({ ignore_case = true })
+ )
+ same(domain.TargetPlanKind.LITERAL, fallback.kind)
+ falsy(fallback:matches(""))
+ falsy(fallback:matches(string.char(1)))
+end)
+
+test("Policy-backed factories sample live values into immutable plans", function()
+ local host = MemoryHost.new()
+ local service = policy.new(host)
+ local factory = target_plan.new(service)
+ local typed_target = target("a")
+
+ local first = factory:build(typed_target)
+ same(domain.TargetPlanKind.LITERAL, first.kind)
+ same(domain.CaseMode.SENSITIVE, first.case_mode)
+ falsy(first:matches("A"))
+
+ host:set_configuration("ignore_case", true)
+ host:set_configuration("chars_match_any_signs", "a")
+ local second = factory:build(typed_target)
+ same(domain.TargetPlanKind.SYMBOL, second.kind)
+ same(domain.CaseMode.INSENSITIVE, second.case_mode)
+ truthy(second:matches("!"))
+
+ same(domain.TargetPlanKind.LITERAL, first.kind)
+ falsy(first:matches("A"))
+ local serialized = second:to_table()
+ same("symbol", serialized.kind)
+ same("insensitive", serialized.case_mode)
+ fails(function()
+ second.kind = domain.TargetPlanKind.LITERAL
+ end, "immutable")
+end)
+
+test("Target plan inputs enforce matching contracts", function()
+ local factory = target_plan.new()
+ fails(function()
+ factory:build({}, matching_policy())
+ end, "TargetValue")
+ fails(function()
+ factory:build(target("a"), matching_policy({ ignore_case = 1 }))
+ end, "Boolean")
+ fails(function()
+ factory:build(target("a"), matching_policy({ chars_match_any_signs = {} }))
+ end, "string")
+ fails(function()
+ target_plan.parse_trigger_characters("abc", function()
+ return { "a", "c" }
+ end)
+ end, "preserve")
+end)
+
for _, item in ipairs(tests) do
local ok, failure = xpcall(item.body, debug.traceback)
if not ok then
@@ -1492,4 +1828,4 @@ for _, item in ipairs(tests) do
passed = passed + 1
end
-io.stdout:write(string.format("Phase 5: %d tests passed\n", passed))
+io.stdout:write(string.format("Phase 6: %d tests passed\n", passed))