summaryrefslogtreecommitdiff
path: root/lua/clever_tee/policy.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/clever_tee/policy.lua')
-rw-r--r--lua/clever_tee/policy.lua488
1 files changed, 488 insertions, 0 deletions
diff --git a/lua/clever_tee/policy.lua b/lua/clever_tee/policy.lua
new file mode 100644
index 0000000..94bf6df
--- /dev/null
+++ b/lua/clever_tee/policy.lua
@@ -0,0 +1,488 @@
+local case_policy = require("clever_tee.case_policy")
+local domain = require("clever_tee.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 = "CleverTeeCursor",
+ 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 = "CleverTeeDefaultLabel",
+ feature_setting = "mark_char",
+ highlight_group = "CleverTeeChar",
+ 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 = "CleverTeeDefaultLabel",
+ feature_setting = "mark_direct",
+ highlight_group = "CleverTeeDirect",
+ 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
+
+function M.resolve_case_mode(target, ignore_case, smart_case)
+ return case_policy.resolve_case_mode(target, ignore_case, smart_case)
+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_direct_preview()
+ return {
+ ignore_case = self:get_boolean("ignore_case"),
+ smart_case = self:get_boolean("smart_case"),
+ }
+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