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