diff options
Diffstat (limited to 'lua/clever_f/target_plan.lua')
| -rw-r--r-- | lua/clever_f/target_plan.lua | 241 |
1 files changed, 238 insertions, 3 deletions
diff --git a/lua/clever_f/target_plan.lua b/lua/clever_f/target_plan.lua index 385b111..78d0b41 100644 --- a/lua/clever_f/target_plan.lua +++ b/lua/clever_f/target_plan.lua @@ -1,5 +1,6 @@ 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 = {} @@ -167,6 +168,13 @@ local function require_splitter(splitter) 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() @@ -190,6 +198,9 @@ function TargetPlanFactory.new(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 @@ -212,6 +223,7 @@ local function default_match_policy() return { ignore_case = false, smart_case = false, + use_migemo = false, chars_match_any_signs = "", } end @@ -236,9 +248,15 @@ local function sampled_policy(factory, target, match_policy) 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" @@ -255,7 +273,169 @@ local function new_plan(target, kind, case_mode, matcher) }) end -function TargetPlanFactory:build(target, match_policy) +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) @@ -274,6 +454,31 @@ function TargetPlanFactory:build(target, match_policy) ) 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( @@ -295,8 +500,38 @@ function TargetPlanFactory:build(target, match_policy) ) end -function M.build(target, match_policy, options) - return TargetPlanFactory.new(options):build(target, match_policy) +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 |
