diff options
Diffstat (limited to 'lua/clever_f')
| -rw-r--r-- | lua/clever_f/capabilities.lua | 1 | ||||
| -rw-r--r-- | lua/clever_f/policy.lua | 494 | ||||
| -rw-r--r-- | lua/clever_f/testing/memory_host.lua | 8 |
3 files changed, 503 insertions, 0 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 |
