summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lua/clever_f/capabilities.lua1
-rw-r--r--lua/clever_f/policy.lua494
-rw-r--r--lua/clever_f/testing/memory_host.lua8
-rw-r--r--tests/run.lua273
4 files changed, 775 insertions, 1 deletions
diff --git a/lua/clever_f/capabilities.lua b/lua/clever_f/capabilities.lua
index 77ce130..23da397 100644
--- a/lua/clever_f/capabilities.lua
+++ b/lua/clever_f/capabilities.lua
@@ -15,6 +15,7 @@ M.read_methods = {
M.effect_methods = {
movement = { "apply_cursor", "apply_selection", "set_operator_inclusive" },
+ configuration = { "write_configuration" },
input = { "read_input" },
folds = { "open_fold" },
prompt = { "show_prompt" },
diff --git a/lua/clever_f/policy.lua b/lua/clever_f/policy.lua
new file mode 100644
index 0000000..b56487f
--- /dev/null
+++ b/lua/clever_f/policy.lua
@@ -0,0 +1,494 @@
+local domain = require("clever_f.domain")
+
+local M = {}
+local PolicyService = {}
+PolicyService.__index = PolicyService
+M.PolicyService = PolicyService
+
+M.ValueType = {
+ BOOLEAN = "boolean",
+ STRING = "string",
+ STRING_LIST = "string_list",
+ OPTIONAL_GROUP_NAME = "optional_group_name",
+ NONNEGATIVE_INTEGER = "nonnegative_integer",
+ PRESENCE = "presence",
+}
+
+M.Sampling = {
+ LIVE = "live",
+ ACTIVATION = "activation",
+ LINK_EVALUATION = "link_evaluation",
+}
+
+M.DEFAULT_MAP_SUPPRESSION_SENTINEL = "suppress_default_mappings"
+
+local NO_VALUE = {}
+local SCHEMA = {
+ search_current_line_only = {
+ value_type = M.ValueType.BOOLEAN,
+ default = false,
+ sampling = M.Sampling.LIVE,
+ },
+ ignore_case = {
+ value_type = M.ValueType.BOOLEAN,
+ default = false,
+ sampling = M.Sampling.LIVE,
+ },
+ smart_case = {
+ value_type = M.ValueType.BOOLEAN,
+ default = false,
+ sampling = M.Sampling.LIVE,
+ },
+ use_migemo = {
+ value_type = M.ValueType.BOOLEAN,
+ default = false,
+ sampling = M.Sampling.LIVE,
+ },
+ fix_key_direction = {
+ value_type = M.ValueType.BOOLEAN,
+ default = false,
+ sampling = M.Sampling.LIVE,
+ },
+ show_prompt = {
+ value_type = M.ValueType.BOOLEAN,
+ default = false,
+ sampling = M.Sampling.LIVE,
+ },
+ chars_match_any_signs = {
+ value_type = M.ValueType.STRING,
+ default = "",
+ sampling = M.Sampling.LIVE,
+ },
+ mark_cursor = {
+ value_type = M.ValueType.BOOLEAN,
+ default = true,
+ sampling = M.Sampling.LIVE,
+ },
+ mark_cursor_color = {
+ value_type = M.ValueType.OPTIONAL_GROUP_NAME,
+ default = NO_VALUE,
+ default_target = "Cursor",
+ feature_setting = "mark_cursor",
+ highlight_group = "CleverFCursor",
+ sampling = M.Sampling.LINK_EVALUATION,
+ },
+ hide_cursor_on_cmdline = {
+ value_type = M.ValueType.BOOLEAN,
+ default = true,
+ sampling = M.Sampling.LIVE,
+ },
+ repeat_timeout_ms = {
+ value_type = M.ValueType.NONNEGATIVE_INTEGER,
+ default = 0,
+ sampling = M.Sampling.LIVE,
+ },
+ mark_char = {
+ value_type = M.ValueType.BOOLEAN,
+ default = true,
+ sampling = M.Sampling.LIVE,
+ },
+ mark_char_color = {
+ value_type = M.ValueType.OPTIONAL_GROUP_NAME,
+ default = NO_VALUE,
+ default_target = "CleverFDefaultLabel",
+ feature_setting = "mark_char",
+ highlight_group = "CleverFChar",
+ sampling = M.Sampling.LINK_EVALUATION,
+ },
+ highlight_timeout_ms = {
+ value_type = M.ValueType.NONNEGATIVE_INTEGER,
+ default = 0,
+ sampling = M.Sampling.LIVE,
+ },
+ repeat_last_char_inputs = {
+ value_type = M.ValueType.STRING_LIST,
+ default = { "\r" },
+ sampling = M.Sampling.LIVE,
+ },
+ mark_direct = {
+ value_type = M.ValueType.BOOLEAN,
+ default = false,
+ sampling = M.Sampling.LIVE,
+ },
+ mark_direct_color = {
+ value_type = M.ValueType.OPTIONAL_GROUP_NAME,
+ default = NO_VALUE,
+ default_target = "CleverFDefaultLabel",
+ feature_setting = "mark_direct",
+ highlight_group = "CleverFDirect",
+ sampling = M.Sampling.LINK_EVALUATION,
+ },
+ clean_labels_eagerly = {
+ value_type = M.ValueType.BOOLEAN,
+ default = true,
+ sampling = M.Sampling.ACTIVATION,
+ },
+ [M.DEFAULT_MAP_SUPPRESSION_SENTINEL] = {
+ value_type = M.ValueType.PRESENCE,
+ default = false,
+ sampling = M.Sampling.ACTIVATION,
+ },
+}
+
+local COLOR_SETTINGS = {
+ "mark_cursor_color",
+ "mark_char_color",
+ "mark_direct_color",
+}
+
+local function fail(message, level)
+ error(message, (level or 1) + 1)
+end
+
+local function is_integer(value)
+ return type(value) == "number"
+ and value > -math.huge
+ and value < math.huge
+ and value == math.floor(value)
+end
+
+local function copy_list(values)
+ local result = {}
+ for index = 1, #values do
+ result[index] = values[index]
+ end
+ return result
+end
+
+local function copy_table(value)
+ local result = {}
+ for key, item in pairs(value) do
+ if type(item) == "table" and domain.type_of(item) == nil then
+ result[key] = copy_table(item)
+ else
+ result[key] = item
+ end
+ end
+ return result
+end
+
+local function schema_entry(name)
+ local entry = SCHEMA[name]
+ if entry == nil then
+ fail("unknown policy setting '" .. tostring(name) .. "'", 2)
+ end
+ return entry
+end
+
+local function default_value(entry)
+ if entry.default == NO_VALUE then
+ return nil
+ end
+ if type(entry.default) == "table" then
+ return copy_table(entry.default)
+ end
+ return entry.default
+end
+
+local function validate_boolean(value, name)
+ if type(value) ~= "boolean" then
+ fail("policy setting '" .. name .. "' must be a Boolean", 3)
+ end
+ return value
+end
+
+local function validate_string(value, name)
+ if type(value) ~= "string" then
+ fail("policy setting '" .. name .. "' must be a string", 3)
+ end
+ return value
+end
+
+local function validate_string_list(value, name)
+ if type(value) ~= "table" then
+ fail("policy setting '" .. name .. "' must be a list of strings", 3)
+ end
+
+ local length = #value
+ local item_count = 0
+ for key, item in pairs(value) do
+ if not is_integer(key) or key < 1 or key > length then
+ fail("policy setting '" .. name .. "' must be a list of strings", 3)
+ end
+ if type(item) ~= "string" then
+ fail("policy setting '" .. name .. "' must be a list of strings", 3)
+ end
+ item_count = item_count + 1
+ end
+ if item_count ~= length then
+ fail("policy setting '" .. name .. "' must be a list of strings", 3)
+ end
+ return copy_list(value)
+end
+
+local function validate_optional_group_name(value, name)
+ if value ~= nil and (type(value) ~= "string" or value == "") then
+ fail("policy setting '" .. name .. "' must be an optional group name", 3)
+ end
+ return value
+end
+
+local function validate_nonnegative_integer(value, name)
+ if not is_integer(value) or value < 0 then
+ fail("policy setting '" .. name .. "' must be a nonnegative integer", 3)
+ end
+ return value
+end
+
+local VALIDATORS = {
+ [M.ValueType.BOOLEAN] = validate_boolean,
+ [M.ValueType.STRING] = validate_string,
+ [M.ValueType.STRING_LIST] = validate_string_list,
+ [M.ValueType.OPTIONAL_GROUP_NAME] = validate_optional_group_name,
+ [M.ValueType.NONNEGATIVE_INTEGER] = validate_nonnegative_integer,
+}
+
+local function require_provider(provider)
+ if type(provider) ~= "table" then
+ fail("policy configuration provider must be a table", 2)
+ end
+ local required = {
+ "configuration_present",
+ "read_configuration",
+ "write_configuration",
+ }
+ for _, method_name in ipairs(required) do
+ if type(provider[method_name]) ~= "function" then
+ fail("policy configuration provider is missing " .. method_name, 2)
+ end
+ end
+ return provider
+end
+
+function M.setting_names()
+ local names = {}
+ for name in pairs(SCHEMA) do
+ names[#names + 1] = name
+ end
+ table.sort(names)
+ return names
+end
+
+function M.schema()
+ local result = {}
+ for name, entry in pairs(SCHEMA) do
+ local public_entry = {
+ value_type = entry.value_type,
+ sampling = entry.sampling,
+ has_default = true,
+ }
+ local value = default_value(entry)
+ if value ~= nil then
+ public_entry.default = value
+ end
+ if entry.default_target ~= nil then
+ public_entry.default_target = entry.default_target
+ public_entry.feature_setting = entry.feature_setting
+ public_entry.highlight_group = entry.highlight_group
+ end
+ result[name] = public_entry
+ end
+ return result
+end
+
+function M.default(name)
+ return default_value(schema_entry(name))
+end
+
+function M.defaults()
+ local result = {}
+ for name, entry in pairs(SCHEMA) do
+ local value = default_value(entry)
+ if value ~= nil then
+ result[name] = value
+ end
+ end
+ return result
+end
+
+function PolicyService.new(provider)
+ return setmetatable({
+ _provider = require_provider(provider),
+ _activation = nil,
+ }, PolicyService)
+end
+
+function M.new(provider)
+ return PolicyService.new(provider)
+end
+
+setmetatable(M, {
+ __call = function(_, provider)
+ return PolicyService.new(provider)
+ end,
+})
+
+function PolicyService:get(name)
+ local entry = schema_entry(name)
+ if entry.value_type == M.ValueType.PRESENCE then
+ return self._provider:configuration_present(name)
+ end
+
+ local value
+ if self._provider:configuration_present(name) then
+ value = self._provider:read_configuration(name)
+ else
+ value = default_value(entry)
+ end
+ return VALIDATORS[entry.value_type](value, name)
+end
+
+function PolicyService:_get_typed(name, expected_type)
+ local entry = schema_entry(name)
+ if entry.value_type ~= expected_type then
+ fail(
+ "policy setting '" .. name .. "' does not have type " .. expected_type,
+ 2
+ )
+ end
+ return self:get(name)
+end
+
+function PolicyService:get_boolean(name)
+ return self:_get_typed(name, M.ValueType.BOOLEAN)
+end
+
+function PolicyService:get_string(name)
+ return self:_get_typed(name, M.ValueType.STRING)
+end
+
+function PolicyService:get_string_list(name)
+ return self:_get_typed(name, M.ValueType.STRING_LIST)
+end
+
+function PolicyService:get_optional_group_name(name)
+ return self:_get_typed(name, M.ValueType.OPTIONAL_GROUP_NAME)
+end
+
+function PolicyService:get_nonnegative_integer(name)
+ return self:_get_typed(name, M.ValueType.NONNEGATIVE_INTEGER)
+end
+
+function PolicyService:get_presence(name)
+ return self:_get_typed(name, M.ValueType.PRESENCE)
+end
+
+function PolicyService:default_maps_suppressed()
+ return self:get_presence(M.DEFAULT_MAP_SUPPRESSION_SENTINEL)
+end
+
+function PolicyService:capture_activation()
+ if self._activation == nil then
+ self._activation = {
+ install_default_mappings = not self:default_maps_suppressed(),
+ clean_labels_eagerly = self:get_boolean("clean_labels_eagerly"),
+ }
+ end
+ return copy_table(self._activation)
+end
+
+function PolicyService:evaluate_highlight_links()
+ local result = {}
+ for _, color_setting in ipairs(COLOR_SETTINGS) do
+ local entry = SCHEMA[color_setting]
+ local configured_target = self:get_optional_group_name(color_setting)
+ result[entry.highlight_group] = {
+ enabled = self:get_boolean(entry.feature_setting),
+ feature_setting = entry.feature_setting,
+ color_setting = color_setting,
+ configured_target = configured_target,
+ target = configured_target or entry.default_target,
+ }
+ end
+ return result
+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
+
+function M.resolve_case_mode(target, ignore_case, smart_case)
+ target = require_target(target)
+ validate_boolean(ignore_case, "ignore_case")
+ validate_boolean(smart_case, "smart_case")
+
+ if ignore_case or (smart_case and target.value:match("^[a-z]$") ~= nil) then
+ return domain.CaseMode.INSENSITIVE
+ end
+ return domain.CaseMode.SENSITIVE
+end
+
+function PolicyService:case_mode(target)
+ return M.resolve_case_mode(
+ target,
+ self:get_boolean("ignore_case"),
+ self:get_boolean("smart_case")
+ )
+end
+
+function PolicyService:sample_search()
+ local current_line_only = self:get_boolean("search_current_line_only")
+ return {
+ search_current_line_only = current_line_only,
+ search_scope = current_line_only
+ and domain.SearchScope.CURRENT_LINE
+ or domain.SearchScope.BUFFER,
+ }
+end
+
+function PolicyService:sample_match(target)
+ local ignore_case = self:get_boolean("ignore_case")
+ local smart_case = self:get_boolean("smart_case")
+ return {
+ ignore_case = ignore_case,
+ smart_case = smart_case,
+ use_migemo = self:get_boolean("use_migemo"),
+ chars_match_any_signs = self:get_string("chars_match_any_signs"),
+ case_mode = M.resolve_case_mode(target, ignore_case, smart_case),
+ }
+end
+
+function PolicyService:sample_direction()
+ return {
+ fix_key_direction = self:get_boolean("fix_key_direction"),
+ }
+end
+
+function PolicyService:sample_acquisition()
+ return {
+ show_prompt = self:get_boolean("show_prompt"),
+ mark_cursor = self:get_boolean("mark_cursor"),
+ hide_cursor_on_cmdline = self:get_boolean("hide_cursor_on_cmdline"),
+ mark_direct = self:get_boolean("mark_direct"),
+ }
+end
+
+function PolicyService:sample_markers()
+ return {
+ mark_cursor = self:get_boolean("mark_cursor"),
+ mark_char = self:get_boolean("mark_char"),
+ mark_direct = self:get_boolean("mark_direct"),
+ }
+end
+
+function PolicyService:sample_timeouts()
+ return {
+ repeat_timeout_ms = self:get_nonnegative_integer("repeat_timeout_ms"),
+ highlight_timeout_ms = self:get_nonnegative_integer("highlight_timeout_ms"),
+ }
+end
+
+function PolicyService:sample_previous_input()
+ return {
+ repeat_last_char_inputs = self:get_string_list("repeat_last_char_inputs"),
+ }
+end
+
+function PolicyService:disable_migemo_for_unsupported_encoding()
+ self._provider:write_configuration("use_migemo", false)
+end
+
+return M
diff --git a/lua/clever_f/testing/memory_host.lua b/lua/clever_f/testing/memory_host.lua
index 726d244..57af1f1 100644
--- a/lua/clever_f/testing/memory_host.lua
+++ b/lua/clever_f/testing/memory_host.lua
@@ -268,6 +268,14 @@ function MemoryHost:read_configuration(name)
return value
end
+function MemoryHost:write_configuration(name, value)
+ if type(name) ~= "string" or name == "" then
+ error("configuration name must be a nonempty string", 2)
+ end
+ self._configuration[name] = copy(value)
+ self:_record("write_configuration", { name = name, value = value })
+end
+
function MemoryHost:read_encoding()
self:_record("read_encoding", { encoding = self._encoding })
return self._encoding
diff --git a/tests/run.lua b/tests/run.lua
index 6a42d00..f984123 100644
--- a/tests/run.lua
+++ b/tests/run.lua
@@ -8,6 +8,7 @@ package.path = table.concat({
local domain = require("clever_f.domain")
local capabilities = require("clever_f.capabilities")
+local policy = require("clever_f.policy")
local MemoryHost = require("clever_f.testing.memory_host")
local tests = {}
@@ -561,6 +562,276 @@ test("MemoryHost manages timers, events, mappings, and dot repeat", function()
same(2, count.value)
end)
+test("Policy schema contains every default and color target", function()
+ local defaults = policy.defaults()
+ local false_settings = {
+ "search_current_line_only",
+ "ignore_case",
+ "smart_case",
+ "use_migemo",
+ "fix_key_direction",
+ "show_prompt",
+ "mark_direct",
+ }
+ local true_settings = {
+ "mark_cursor",
+ "hide_cursor_on_cmdline",
+ "mark_char",
+ "clean_labels_eagerly",
+ }
+
+ for _, name in ipairs(false_settings) do
+ same(false, defaults[name], name)
+ end
+ for _, name in ipairs(true_settings) do
+ same(true, defaults[name], name)
+ end
+ same("", defaults.chars_match_any_signs)
+ same(0, defaults.repeat_timeout_ms)
+ same(0, defaults.highlight_timeout_ms)
+ list_same({ "\r" }, defaults.repeat_last_char_inputs)
+ same(nil, policy.default("mark_cursor_color"))
+ same(nil, policy.default("mark_char_color"))
+ same(nil, policy.default("mark_direct_color"))
+
+ local schema = policy.schema()
+ same("Cursor", schema.mark_cursor_color.default_target)
+ same("CleverFDefaultLabel", schema.mark_char_color.default_target)
+ same("CleverFDefaultLabel", schema.mark_direct_color.default_target)
+ same(policy.ValueType.OPTIONAL_GROUP_NAME, schema.mark_cursor_color.value_type)
+ same(policy.ValueType.PRESENCE,
+ schema[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL].value_type)
+
+ defaults.repeat_last_char_inputs[1] = "changed"
+ schema.repeat_last_char_inputs.default[1] = "changed"
+ list_same({ "\r" }, policy.default("repeat_last_char_inputs"))
+end)
+
+test("Policy typed accessors validate semantic values", function()
+ local host = MemoryHost.new({
+ configuration = {
+ search_current_line_only = true,
+ chars_match_any_signs = ";:",
+ repeat_last_char_inputs = { "x", "" },
+ mark_cursor_color = "IncSearch",
+ repeat_timeout_ms = 25,
+ },
+ })
+ local service = policy.new(host)
+
+ truthy(service:get_boolean("search_current_line_only"))
+ same(";:", service:get_string("chars_match_any_signs"))
+ list_same({ "x", "" }, service:get_string_list("repeat_last_char_inputs"))
+ same("IncSearch", service:get_optional_group_name("mark_cursor_color"))
+ same(25, service:get_nonnegative_integer("repeat_timeout_ms"))
+ same(nil, service:get_optional_group_name("mark_char_color"))
+
+ local inputs = service:get_string_list("repeat_last_char_inputs")
+ inputs[1] = "changed"
+ same("x", service:get_string_list("repeat_last_char_inputs")[1])
+
+ host:set_configuration("ignore_case", 1)
+ fails(function()
+ service:get_boolean("ignore_case")
+ end, "must be a Boolean")
+ host:set_configuration("chars_match_any_signs", {})
+ fails(function()
+ service:get_string("chars_match_any_signs")
+ end, "must be a string")
+ host:set_configuration("repeat_last_char_inputs", { "x", 2 })
+ fails(function()
+ service:get_string_list("repeat_last_char_inputs")
+ end, "list of strings")
+ host:set_configuration("mark_cursor_color", "")
+ fails(function()
+ service:get_optional_group_name("mark_cursor_color")
+ end, "optional group name")
+ host:set_configuration("repeat_timeout_ms", -1)
+ fails(function()
+ service:get_nonnegative_integer("repeat_timeout_ms")
+ end, "nonnegative integer")
+ host:set_configuration("repeat_timeout_ms", 1.5)
+ fails(function()
+ service:get_nonnegative_integer("repeat_timeout_ms")
+ end, "nonnegative integer")
+ fails(function()
+ service:get_string("ignore_case")
+ end, "does not have type string")
+ fails(function()
+ service:get("unknown")
+ end, "unknown policy setting")
+end)
+
+test("Default-map suppression is presence-based and activation values are retained", function()
+ local absent_host = MemoryHost.new()
+ local absent_policy = policy.new(absent_host)
+ falsy(absent_policy:default_maps_suppressed())
+ truthy(absent_policy:capture_activation().install_default_mappings)
+
+ for _, sentinel_value in ipairs({ false, 0 }) do
+ local configuration = {
+ clean_labels_eagerly = false,
+ }
+ configuration[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL] = sentinel_value
+ local host = MemoryHost.new({ configuration = configuration })
+ local service = policy.new(host)
+
+ host:clear_operations()
+ truthy(service:default_maps_suppressed())
+ local operations = host:operations()
+ same(1, #operations)
+ same("configuration_present", operations[1].operation)
+
+ local activation = service:capture_activation()
+ falsy(activation.install_default_mappings)
+ falsy(activation.clean_labels_eagerly)
+
+ host:unset_configuration(policy.DEFAULT_MAP_SUPPRESSION_SENTINEL)
+ host:set_configuration("clean_labels_eagerly", true)
+ local retained = service:capture_activation()
+ falsy(retained.install_default_mappings)
+ falsy(retained.clean_labels_eagerly)
+
+ local next_activation = policy.new(host):capture_activation()
+ truthy(next_activation.install_default_mappings)
+ truthy(next_activation.clean_labels_eagerly)
+ end
+end)
+
+test("Runtime policy samples read current behavior values", function()
+ local host = MemoryHost.new()
+ local service = policy.new(host)
+ local target = domain.TargetValue.character("a", 97)
+
+ same(domain.SearchScope.BUFFER, service:sample_search().search_scope)
+ same(domain.CaseMode.SENSITIVE, service:sample_match(target).case_mode)
+ falsy(service:sample_direction().fix_key_direction)
+ falsy(service:sample_acquisition().show_prompt)
+ truthy(service:sample_markers().mark_char)
+ same(0, service:sample_timeouts().repeat_timeout_ms)
+ list_same({ "\r" }, service:sample_previous_input().repeat_last_char_inputs)
+
+ host:set_configuration("search_current_line_only", true)
+ host:set_configuration("ignore_case", true)
+ host:set_configuration("smart_case", true)
+ host:set_configuration("use_migemo", true)
+ host:set_configuration("chars_match_any_signs", ";")
+ host:set_configuration("fix_key_direction", true)
+ host:set_configuration("show_prompt", true)
+ host:set_configuration("mark_cursor", false)
+ host:set_configuration("hide_cursor_on_cmdline", false)
+ host:set_configuration("mark_char", false)
+ host:set_configuration("mark_direct", true)
+ host:set_configuration("repeat_timeout_ms", 75)
+ host:set_configuration("highlight_timeout_ms", 125)
+ host:set_configuration("repeat_last_char_inputs", { "x", "yz" })
+
+ local search = service:sample_search()
+ truthy(search.search_current_line_only)
+ same(domain.SearchScope.CURRENT_LINE, search.search_scope)
+
+ local match = service:sample_match(target)
+ truthy(match.ignore_case)
+ truthy(match.smart_case)
+ truthy(match.use_migemo)
+ same(";", match.chars_match_any_signs)
+ same(domain.CaseMode.INSENSITIVE, match.case_mode)
+ truthy(service:sample_direction().fix_key_direction)
+
+ local acquisition = service:sample_acquisition()
+ truthy(acquisition.show_prompt)
+ falsy(acquisition.mark_cursor)
+ falsy(acquisition.hide_cursor_on_cmdline)
+ truthy(acquisition.mark_direct)
+
+ local markers = service:sample_markers()
+ falsy(markers.mark_cursor)
+ falsy(markers.mark_char)
+ truthy(markers.mark_direct)
+
+ local timeouts = service:sample_timeouts()
+ same(75, timeouts.repeat_timeout_ms)
+ same(125, timeouts.highlight_timeout_ms)
+ list_same({ "x", "yz" }, service:sample_previous_input().repeat_last_char_inputs)
+end)
+
+test("Highlight policy is reevaluated with live feature and color values", function()
+ local host = MemoryHost.new()
+ local service = policy.new(host)
+ service:capture_activation()
+
+ local initial = service:evaluate_highlight_links()
+ truthy(initial.CleverFCursor.enabled)
+ same(nil, initial.CleverFCursor.configured_target)
+ same("Cursor", initial.CleverFCursor.target)
+ truthy(initial.CleverFChar.enabled)
+ same("CleverFDefaultLabel", initial.CleverFChar.target)
+ falsy(initial.CleverFDirect.enabled)
+ same("CleverFDefaultLabel", initial.CleverFDirect.target)
+
+ host:set_configuration("mark_cursor", false)
+ host:set_configuration("mark_cursor_color", "Search")
+ host:set_configuration("mark_char", false)
+ host:set_configuration("mark_char_color", "ErrorMsg")
+ host:set_configuration("mark_direct", true)
+ host:set_configuration("mark_direct_color", "IncSearch")
+
+ local refreshed = service:evaluate_highlight_links()
+ falsy(refreshed.CleverFCursor.enabled)
+ same("Search", refreshed.CleverFCursor.target)
+ falsy(refreshed.CleverFChar.enabled)
+ same("ErrorMsg", refreshed.CleverFChar.target)
+ truthy(refreshed.CleverFDirect.enabled)
+ same("IncSearch", refreshed.CleverFDirect.target)
+end)
+
+test("Case policy resolves an explicit mode for each target", function()
+ local host = MemoryHost.new()
+ local service = policy.new(host)
+ local lower = domain.TargetValue.character("a", 97)
+ local upper = domain.TargetValue.character("A", 65)
+ local multibyte = domain.TargetValue.character("\227\129\130", 0x3042)
+
+ same(domain.CaseMode.SENSITIVE, service:case_mode(lower))
+ host:set_configuration("smart_case", true)
+ same(domain.CaseMode.INSENSITIVE, service:case_mode(lower))
+ same(domain.CaseMode.SENSITIVE, service:case_mode(upper))
+ same(domain.CaseMode.SENSITIVE, service:case_mode(multibyte))
+ host:set_configuration("ignore_case", true)
+ same(domain.CaseMode.INSENSITIVE, service:case_mode(upper))
+
+ local match = service:sample_match(upper)
+ local plan = domain.TargetPlan.new({
+ target = upper,
+ kind = domain.TargetPlanKind.LITERAL,
+ case_mode = match.case_mode,
+ matcher = function()
+ return true
+ end,
+ })
+ same(domain.CaseMode.INSENSITIVE, plan.case_mode)
+end)
+
+test("Unsupported Migemo policy mutation updates live configuration", function()
+ local host = MemoryHost.new({
+ configuration = { use_migemo = true },
+ })
+ local service = policy.new(host)
+ truthy(service:get_boolean("use_migemo"))
+
+ host:clear_operations()
+ service:disable_migemo_for_unsupported_encoding()
+ local operations = host:operations()
+ same(1, #operations)
+ same("write_configuration", operations[1].operation)
+ same("use_migemo", operations[1].name)
+ same(false, operations[1].value)
+ falsy(service:get_boolean("use_migemo"))
+
+ host:set_configuration("use_migemo", true)
+ truthy(service:get_boolean("use_migemo"))
+end)
+
for _, item in ipairs(tests) do
local ok, failure = xpcall(item.body, debug.traceback)
if not ok then
@@ -570,4 +841,4 @@ for _, item in ipairs(tests) do
passed = passed + 1
end
-io.stdout:write(string.format("Phase 2: %d tests passed\n", passed))
+io.stdout:write(string.format("Phase 3: %d tests passed\n", passed))