summaryrefslogtreecommitdiff
path: root/lua/clever_f/target_plan.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/clever_f/target_plan.lua')
-rw-r--r--lua/clever_f/target_plan.lua541
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