diff options
Diffstat (limited to 'lua/clever_f/target_plan.lua')
| -rw-r--r-- | lua/clever_f/target_plan.lua | 541 |
1 files changed, 0 insertions, 541 deletions
diff --git a/lua/clever_f/target_plan.lua b/lua/clever_f/target_plan.lua deleted file mode 100644 index 78d0b41..0000000 --- a/lua/clever_f/target_plan.lua +++ /dev/null @@ -1,541 +0,0 @@ -local case_policy = require("clever_f.case_policy") -local domain = require("clever_f.domain") -local migemo_catalog = require("clever_f.migemo_catalog") -local text_topology = require("clever_f.text_topology") - -local M = {} -local TargetPlanFactory = {} -M.TargetPlanFactory = TargetPlanFactory - -M.SYMBOLS = "!\"#$%&'()=~|\\-^@`[]{};:+*<>,.?_/" - -local SYMBOL_CHARACTERS = {} -local SYMBOL_SET = {} -for index = 1, #M.SYMBOLS do - local character = M.SYMBOLS:sub(index, index) - SYMBOL_CHARACTERS[index] = character - SYMBOL_SET[character] = true -end - -local factory_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function require_target(target) - if not domain.TargetValue.is(target) then - fail("target plan target must be a TargetValue", 2) - end - return target -end - -local function require_boolean(value, name) - if type(value) ~= "boolean" then - fail("match policy " .. name .. " must be a Boolean", 2) - end - return value -end - -local function require_string(value, name) - if type(value) ~= "string" then - fail("match policy " .. name .. " must be a string", 2) - end - return value -end - -local function copy_list(values) - local result = {} - for index = 1, #values do - result[index] = values[index] - end - return result -end - -function M.symbol_characters() - return copy_list(SYMBOL_CHARACTERS) -end - -function M.is_symbol(character) - return type(character) == "string" and SYMBOL_SET[character] == true -end - -local function require_character_list(characters, source) - if type(characters) ~= "table" then - fail("editor character splitter must return a list", 3) - end - - local result = {} - local item_count = 0 - for key, character in pairs(characters) do - if type(key) ~= "number" - or key ~= math.floor(key) - or key < 1 - or key > #characters - or type(character) ~= "string" - or character == "" - then - fail("editor character splitter must return a list of nonempty strings", 3) - end - result[key] = character - item_count = item_count + 1 - end - if item_count ~= #characters or table.concat(result) ~= source then - fail("editor character splitter must preserve the configured trigger string", 3) - end - return result -end - -function M.parse_trigger_characters(value, splitter) - if type(value) ~= "string" then - fail("chars_match_any_signs must be a string", 2) - end - splitter = splitter or text_topology.split_editor_characters - if type(splitter) ~= "function" then - fail("editor character splitter must be a function", 2) - end - return require_character_list(splitter(value), value) -end - -local function trigger_set(value, splitter) - local result = {} - for _, character in ipairs(M.parse_trigger_characters(value, splitter)) do - result[character] = true - end - return result -end - -local function false_matcher() - return false -end - -local function symbol_matcher(candidate_character) - return M.is_symbol(candidate_character) -end - -local function normalize_factory_options(options) - if options == nil then - return {} - end - if type(options) == "function" then - return { lowercase = options } - end - if type(options) ~= "table" then - fail("TargetPlanFactory options must be a table", 3) - end - if type(options.sample_match) == "function" - and options.policy == nil - and options.policy_service == nil - and options.case_resolver == nil - and options.lowercase == nil - and options.splitter == nil - and options.split_editor_characters == nil - then - return { policy = options } - end - return options -end - -local function require_policy_service(service) - if service ~= nil and (type(service) ~= "table" - or type(service.sample_match) ~= "function") - then - fail("TargetPlanFactory policy must provide sample_match", 3) - end - return service -end - -local function require_case_resolver(resolver, options) - if resolver == nil then - return case_policy.new({ - lowercase = options.lowercase, - }) - end - if type(resolver) ~= "table" - or type(resolver.resolve) ~= "function" - or type(resolver.comparator) ~= "function" - then - fail("TargetPlanFactory case resolver is invalid", 3) - end - return resolver -end - -local function require_splitter(splitter) - splitter = splitter or text_topology.split_editor_characters - if type(splitter) ~= "function" then - fail("TargetPlanFactory editor character splitter must be a function", 3) - end - return splitter -end - -local function require_migemo_catalog(catalog) - if catalog ~= nil and not migemo_catalog.MigemoCatalog.is(catalog) then - fail("TargetPlanFactory migemo_catalog must be a MigemoCatalog", 3) - end - return catalog -end - -local factory_metatable = { - __index = TargetPlanFactory, - __newindex = function() - fail("TargetPlanFactory values are immutable", 2) - end, - __tostring = function() - return "target-plan-factory" - end, - __metatable = "clever_f.target_plan.TargetPlanFactory", -} - -function TargetPlanFactory.new(options) - if TargetPlanFactory.is(options) then - return options - end - options = normalize_factory_options(options) - local factory = setmetatable({}, factory_metatable) - factory_records[factory] = { - policy = require_policy_service(options.policy or options.policy_service), - case_resolver = require_case_resolver(options.case_resolver, options), - splitter = require_splitter( - options.split_editor_characters or options.splitter - ), - migemo_catalog = require_migemo_catalog( - options.migemo_catalog or options.catalog - ), - } - return factory -end - -function TargetPlanFactory.is(value) - return type(value) == "table" and factory_records[value] ~= nil -end - -function M.new(options) - return TargetPlanFactory.new(options) -end - -setmetatable(M, { - __call = function(_, options) - return TargetPlanFactory.new(options) - end, -}) - -local function default_match_policy() - return { - ignore_case = false, - smart_case = false, - use_migemo = false, - chars_match_any_signs = "", - } -end - -local function sampled_policy(factory, target, match_policy) - local service - if match_policy == nil then - service = factory_records[factory].policy - if service == nil then - return default_match_policy() - end - elseif type(match_policy) == "table" - and type(match_policy.sample_match) == "function" - then - service = match_policy - end - - if service ~= nil then - match_policy = service:sample_match(target) - end - if type(match_policy) ~= "table" then - fail("target match policy must be a table", 3) - end - - local use_migemo = match_policy.use_migemo - if use_migemo == nil then - use_migemo = false - end - - return { - ignore_case = require_boolean(match_policy.ignore_case, "ignore_case"), - smart_case = require_boolean(match_policy.smart_case, "smart_case"), - use_migemo = require_boolean(use_migemo, "use_migemo"), - chars_match_any_signs = require_string( - match_policy.chars_match_any_signs, - "chars_match_any_signs" - ), - } -end - -local function new_plan(target, kind, case_mode, matcher) - return domain.TargetPlan.new({ - target = target, - kind = kind, - case_mode = case_mode, - matcher = matcher, - }) -end - -local function is_ascii_alphabetic(character) - if type(character) ~= "string" or #character ~= 1 then - return false - end - local code = character:byte(1) - return (code >= string.byte("a") and code <= string.byte("z")) - or (code >= string.byte("A") and code <= string.byte("Z")) -end - -M.is_ascii_alphabetic = is_ascii_alphabetic - -local function context_field(context, primary, alternate) - local value = context[primary] - if value == nil and alternate ~= nil then - value = context[alternate] - end - return value -end - -local function context_table(match_policy, search_context) - if text_topology.TextView.is(search_context) then - return { text_view = search_context } - end - if search_context ~= nil and type(search_context) ~= "table" then - fail("target search context must be a table or TextView", 3) - end - - local context = search_context or {} - if search_context == nil and type(match_policy) == "table" then - if match_policy.text_view ~= nil - or match_policy.view ~= nil - or match_policy.search_scope ~= nil - or match_policy.scope ~= nil - or match_policy.origin ~= nil - or match_policy.current_line ~= nil - or match_policy.effective_encoding ~= nil - or match_policy.encoding ~= nil - then - context = match_policy - end - end - return context -end - -local function active_policy_service(factory, match_policy) - if type(match_policy) == "table" - and type(match_policy.sample_match) == "function" - then - return match_policy - end - return factory_records[factory].policy -end - -local function search_scope(factory, match_policy, context) - local value = context_field(context, "search_scope", "scope") - if value == nil and type(match_policy) == "table" then - value = match_policy.search_scope - if value == nil and match_policy.search_current_line_only ~= nil then - value = match_policy.search_current_line_only - and domain.SearchScope.CURRENT_LINE - or domain.SearchScope.BUFFER - end - end - if value == nil then - local service = active_policy_service(factory, match_policy) - if service ~= nil and type(service.sample_search) == "function" then - value = service:sample_search().search_scope - end - end - if value == nil then - return domain.SearchScope.BUFFER - end - if value == "line" then - value = domain.SearchScope.CURRENT_LINE - end - return domain.SearchScope.from_string(value) -end - -local function migemo_search_context(factory, match_policy, search_context) - local context = context_table(match_policy, search_context) - local view = context_field(context, "text_view", "view") - if not text_topology.TextView.is(view) then - fail("Migemo target planning requires a TextView", 3) - end - - local scope = search_scope(factory, match_policy, context) - local origin = context.origin - if origin == nil then - origin = context.current_line - end - if scope == domain.SearchScope.CURRENT_LINE and origin == nil then - fail("current-line Migemo planning requires an origin line", 3) - end - - local line_number - if scope == domain.SearchScope.CURRENT_LINE then - line_number = type(origin) == "number" - and origin - or domain.Position.coerce(origin).line - end - - local encoding = context_field(context, "effective_encoding", "encoding") - or view.requested_encoding - or view.effective_encoding - return { - view = view, - scope = scope, - origin = origin, - line_number = line_number, - encoding = encoding, - bounds = view:match_start_bounds(scope, origin), - } -end - -local function selected_migemo_catalog(factory, match_policy) - local record = factory_records[factory] - if record.migemo_catalog == nil then - record.migemo_catalog = migemo_catalog.new({ - policy = active_policy_service(factory, match_policy), - }) - end - return record.migemo_catalog -end - -local function migemo_matcher( - target_character, - case_mode, - resolver, - dictionary, - context -) - local target_equal = resolver:comparator(target_character, case_mode) - local assertion = dictionary:predicate(target_character, case_mode) - - return function(candidate_character, candidate_position, candidate_view) - if candidate_position == nil then - fail("Migemo matching requires a candidate Position", 2) - end - local position = domain.Position.coerce(candidate_position) - local view = candidate_view or context.view - if not text_topology.TextView.is(view) then - fail("Migemo matching requires a TextView", 2) - end - if not context.bounds:contains(position) - or not view:is_character_start(position) - then - return false - end - - local actual_character = view:character_at(position) - if candidate_character ~= actual_character then - return false - end - if is_ascii_alphabetic(actual_character) - and not target_equal(actual_character) - then - return false - end - return assertion(view:text_suffix(position)) - end -end - -function TargetPlanFactory:build(target, match_policy, search_context) - target = require_target(target) - local record = factory_records[self] - local sampled = sampled_policy(self, target, match_policy) - local case_mode = record.case_resolver:resolve( - target, - sampled.ignore_case, - sampled.smart_case - ) - - if target.first_code == 0x80 then - return new_plan( - target, - domain.TargetPlanKind.EMPTY, - case_mode, - false_matcher - ) - end - - if sampled.use_migemo and is_ascii_alphabetic(target.value) then - local context = migemo_search_context(self, match_policy, search_context) - local active = context.scope == domain.SearchScope.BUFFER - or context.view:line_byte_length(context.line_number) - > context.view:line_character_count(context.line_number) - if active then - local dictionary = selected_migemo_catalog(self, match_policy):get( - context.encoding, - active_policy_service(self, match_policy) - ) - return new_plan( - target, - domain.TargetPlanKind.MIGEMO, - case_mode, - migemo_matcher( - target.value, - case_mode, - record.case_resolver, - dictionary, - context - ) - ) - end - end - - local triggers = trigger_set(sampled.chars_match_any_signs, record.splitter) - if triggers[target.value] then - return new_plan( - target, - domain.TargetPlanKind.SYMBOL, - case_mode, - symbol_matcher - ) - end - - local kind = target.value == "\\" - and domain.TargetPlanKind.BACKSLASH - or domain.TargetPlanKind.LITERAL - return new_plan( - target, - kind, - case_mode, - record.case_resolver:comparator(target.value, case_mode) - ) -end - -local function is_search_context(value) - return text_topology.TextView.is(value) - or (type(value) == "table" and ( - value.text_view ~= nil - or value.view ~= nil - or value.search_scope ~= nil - or value.scope ~= nil - or value.origin ~= nil - or value.current_line ~= nil - or value.effective_encoding ~= nil - or value.encoding ~= nil - )) -end - -function M.build(target, match_policy, options, search_context) - if search_context == nil and is_search_context(options) then - search_context = options - options = nil - end - return TargetPlanFactory.new(options):build( - target, - match_policy, - search_context - ) -end - -function M.build_for_view(target, view, origin, scope, match_policy, options) - return TargetPlanFactory.new(options):build(target, match_policy, { - text_view = view, - origin = origin, - search_scope = scope, - }) -end - -M.create = M.build -M.create_plan = M.build -M.SYMBOL_SET_STRING = M.SYMBOLS - -return M |
