diff options
Diffstat (limited to 'lua/clever_f')
| -rw-r--r-- | lua/clever_f/acquisition_service.lua | 927 | ||||
| -rw-r--r-- | lua/clever_f/action_facade.lua | 134 | ||||
| -rw-r--r-- | lua/clever_f/capabilities.lua | 163 | ||||
| -rw-r--r-- | lua/clever_f/case_policy.lua | 190 | ||||
| -rw-r--r-- | lua/clever_f/composition_root.lua | 232 | ||||
| -rw-r--r-- | lua/clever_f/destination_engine.lua | 201 | ||||
| -rw-r--r-- | lua/clever_f/direct_preview_planner.lua | 250 | ||||
| -rw-r--r-- | lua/clever_f/domain.lua | 1390 | ||||
| -rw-r--r-- | lua/clever_f/feedback_service.lua | 1003 | ||||
| -rw-r--r-- | lua/clever_f/host_adapter.lua | 1165 | ||||
| -rw-r--r-- | lua/clever_f/init.lua | 118 | ||||
| -rw-r--r-- | lua/clever_f/migemo_catalog.lua | 527 | ||||
| -rw-r--r-- | lua/clever_f/motion_executor.lua | 436 | ||||
| -rw-r--r-- | lua/clever_f/motion_plan.lua | 188 | ||||
| -rw-r--r-- | lua/clever_f/policy.lua | 488 | ||||
| -rw-r--r-- | lua/clever_f/repeat_resolver.lua | 333 | ||||
| -rw-r--r-- | lua/clever_f/sequence_coordinator.lua | 675 | ||||
| -rw-r--r-- | lua/clever_f/sequence_state.lua | 302 | ||||
| -rw-r--r-- | lua/clever_f/state_transitions.lua | 493 | ||||
| -rw-r--r-- | lua/clever_f/target_plan.lua | 541 | ||||
| -rw-r--r-- | lua/clever_f/testing/memory_host.lua | 1075 | ||||
| -rw-r--r-- | lua/clever_f/text_topology.lua | 1031 |
22 files changed, 0 insertions, 11862 deletions
diff --git a/lua/clever_f/acquisition_service.lua b/lua/clever_f/acquisition_service.lua deleted file mode 100644 index e0a6291..0000000 --- a/lua/clever_f/acquisition_service.lua +++ /dev/null @@ -1,927 +0,0 @@ -local domain = require("clever_f.domain") -local direct_preview_planner = require("clever_f.direct_preview_planner") -local feedback_service = require("clever_f.feedback_service") -local motion_plan_factory = require("clever_f.motion_plan") -local policy = require("clever_f.policy") -local sequence_state = require("clever_f.sequence_state") -local state_transitions = require("clever_f.state_transitions") -local target_plan_factory = require("clever_f.target_plan") -local text_topology = require("clever_f.text_topology") - -local M = {} -local AcquisitionRequest = {} -local AcquisitionResult = {} -local AcquisitionService = {} -local TemporaryResourceScope = {} -AcquisitionService.__index = AcquisitionService -M.AcquisitionRequest = AcquisitionRequest -M.AcquisitionResult = AcquisitionResult -M.AcquisitionService = AcquisitionService -M.TemporaryResourceScope = TemporaryResourceScope -M.RepeatedDirection = { - SAME = "same", -} -M.PROMPT = "clever-f: " -M.PREVIOUS_INPUT_NOT_FOUND = "Previous input not found." - -local request_records = setmetatable({}, { __mode = "k" }) -local result_records = setmetatable({}, { __mode = "k" }) -local service_records = setmetatable({}, { __mode = "k" }) -local scope_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function normalize_macro_state(value) - if type(value) == "table" and not domain.MacroState.is(value) then - value = value.register - end - return domain.MacroState.new(value) -end - -local request_metatable = { - __index = function(request, key) - local method = AcquisitionRequest[key] - if method ~= nil then - return method - end - return request_records[request][key] - end, - __newindex = function() - fail("AcquisitionRequest values are immutable", 2) - end, - __tostring = function(request) - return "acquisition-request:" .. request_records[request].descriptor.value - end, - __metatable = "clever_f.acquisition_service.AcquisitionRequest", -} - -function AcquisitionRequest.new(descriptor, context, position, count, macro_state) - if AcquisitionRequest.is(descriptor) then - return descriptor - end - if type(descriptor) == "table" and not domain.Descriptor.is(descriptor) then - local options = descriptor - descriptor = options.descriptor - context = options.context - position = options.position or options.origin - count = options.count - macro_state = options.macro_state - end - - local request = setmetatable({}, request_metatable) - request_records[request] = { - descriptor = domain.Descriptor.from_string(descriptor), - context = domain.ModeContext.from_full_mode(context), - position = domain.Position.coerce(position), - count = domain.Count.new(count), - macro_state = normalize_macro_state(macro_state), - repeated_direction = M.RepeatedDirection.SAME, - } - return request -end - -function AcquisitionRequest.is(value) - return type(value) == "table" and request_records[value] ~= nil -end - -function AcquisitionRequest:to_table() - return { - descriptor = self.descriptor.value, - context = self.context.key, - position = self.position:to_table(), - count = self.count.value, - macro_register = self.macro_state.register, - repeated_direction = self.repeated_direction, - } -end - -local result_metatable = { - __index = function(result, key) - local method = AcquisitionResult[key] - if method ~= nil then - return method - end - return result_records[result][key] - end, - __newindex = function() - fail("AcquisitionResult values are immutable", 2) - end, - __tostring = function(result) - local outcome = result_records[result].outcome - return outcome and tostring(outcome) or "acquisition:resolved" - end, - __metatable = "clever_f.acquisition_service.AcquisitionResult", -} - -function AcquisitionResult.new(request, options) - if AcquisitionResult.is(request) and options == nil then - return request - end - if not AcquisitionRequest.is(request) then - fail("acquisition result requires an AcquisitionRequest", 2) - end - options = options or {} - if type(options) ~= "table" then - fail("acquisition result options must be a table", 2) - end - local outcome = options.outcome - if outcome ~= nil and not domain.ActionOutcome.is(outcome) then - fail("acquisition result outcome must be an ActionOutcome", 2) - end - local target = options.target - if target ~= nil and not domain.TargetValue.is(target) then - fail("acquisition result target must be a TargetValue", 2) - end - local target_plan = options.target_plan - if target_plan ~= nil and not domain.TargetPlan.is(target_plan) then - fail("acquisition result target_plan must be a TargetPlan", 2) - end - local motion_plan = options.motion_plan - if motion_plan ~= nil and not domain.ResolvedMotionPlan.is(motion_plan) then - fail("acquisition result motion_plan must be a ResolvedMotionPlan", 2) - end - local result = setmetatable({}, result_metatable) - result_records[result] = { - request = request, - outcome = outcome, - target = target, - target_plan = target_plan, - motion_plan = motion_plan, - resolved_motion_plan = motion_plan, - previous_input_trigger = options.previous_input_trigger, - previous_target_source = options.previous_target_source, - cached_target = options.cached_target, - missing_previous_input = options.missing_previous_input == true, - acquisition_time_ms = options.acquisition_time_ms, - persistent_feedback_request = options.persistent_feedback_request, - resolved = target ~= nil and target_plan ~= nil and motion_plan ~= nil, - completed = outcome ~= nil - or (target ~= nil and target_plan ~= nil and motion_plan ~= nil), - } - return result -end - -function AcquisitionResult.is(value) - return type(value) == "table" and result_records[value] ~= nil -end - -function AcquisitionResult:has_outcome() - return self.outcome ~= nil -end - -function AcquisitionResult:resolved_values() - return self.target, self.target_plan, self.motion_plan -end - -AcquisitionResult.unpack = AcquisitionResult.resolved_values - -local scope_metatable = { - __index = function(scope, key) - local method = TemporaryResourceScope[key] - if method ~= nil then - return method - end - return scope_records[scope][key] - end, - __newindex = function() - fail("TemporaryResourceScope values are read-only", 2) - end, - __metatable = "clever_f.acquisition_service.TemporaryResourceScope", -} - -function TemporaryResourceScope.new(request, feedback, host) - if not AcquisitionRequest.is(request) then - fail("temporary resource scope requires an AcquisitionRequest", 2) - end - local scope = setmetatable({}, scope_metatable) - scope_records[scope] = { - request = request, - feedback = feedback, - host = host, - active = true, - interactive = not request.macro_state.executing, - prompt_shown = false, - input_completed = false, - acquisition_completed = false, - cursor_marker = nil, - direct_marker = nil, - cursor_presentation_lease = nil, - input_packet = nil, - acquired_target = nil, - resolved_target = nil, - previous_input_trigger = nil, - previous_target_source = nil, - cached_target = nil, - missing_previous_input = false, - text_view = nil, - target_plan = nil, - motion_plan = nil, - outcome = nil, - } - return scope -end - -function TemporaryResourceScope.is(value) - return type(value) == "table" and scope_records[value] ~= nil -end - -local function set_scope_resource(scope, field, resource) - local record = scope_records[scope] - if record == nil or not record.active then - fail("temporary resource scope must be active", 3) - end - record[field] = resource - return resource -end - -function TemporaryResourceScope:set_cursor_marker(marker) - return set_scope_resource(self, "cursor_marker", marker) -end - -function TemporaryResourceScope:set_direct_marker(marker) - return set_scope_resource(self, "direct_marker", marker) -end - -function TemporaryResourceScope:set_cursor_presentation_lease(lease) - return set_scope_resource(self, "cursor_presentation_lease", lease) -end - -function TemporaryResourceScope:set_input_packet(packet) - return set_scope_resource(self, "input_packet", packet) -end - -function TemporaryResourceScope:request_redraw(kind) - local record = scope_records[self] - if record == nil then - fail("temporary resource scope is invalid", 2) - end - if not record.interactive then - return false - end - record.host:redraw(kind) - return true -end - -function TemporaryResourceScope:mark_prompt_shown() - return set_scope_resource(self, "prompt_shown", true) -end - -function TemporaryResourceScope:mark_input_completed() - return set_scope_resource(self, "input_completed", true) -end - -function TemporaryResourceScope:mark_acquisition_completed() - return set_scope_resource(self, "acquisition_completed", true) -end - -function TemporaryResourceScope:set_acquired_target(target) - return set_scope_resource(self, "acquired_target", target) -end - -function TemporaryResourceScope:set_resolved_target(target) - return set_scope_resource(self, "resolved_target", target) -end - -function TemporaryResourceScope:set_previous_input_trigger(trigger) - return set_scope_resource(self, "previous_input_trigger", trigger) -end - -function TemporaryResourceScope:set_cached_target(context, target) - set_scope_resource(self, "previous_target_source", context) - return set_scope_resource(self, "cached_target", target) -end - -function TemporaryResourceScope:set_missing_previous_input(missing) - if type(missing) ~= "boolean" then - fail("missing previous-input state must be a Boolean", 2) - end - return set_scope_resource(self, "missing_previous_input", missing) -end - -function TemporaryResourceScope:set_text_view(view) - if not text_topology.TextView.is(view) then - fail("temporary resource scope text must be a TextView", 2) - end - return set_scope_resource(self, "text_view", view) -end - -function TemporaryResourceScope:set_target_plan(target_plan) - if not domain.TargetPlan.is(target_plan) then - fail("temporary resource scope target plan must be a TargetPlan", 2) - end - return set_scope_resource(self, "target_plan", target_plan) -end - -function TemporaryResourceScope:set_motion_plan(motion_plan) - if not domain.ResolvedMotionPlan.is(motion_plan) then - fail("temporary resource scope motion plan must be a ResolvedMotionPlan", 2) - end - return set_scope_resource(self, "motion_plan", motion_plan) -end - -function TemporaryResourceScope:set_outcome(outcome) - if not domain.ActionOutcome.is(outcome) then - fail("temporary resource scope outcome must be an ActionOutcome", 2) - end - return set_scope_resource(self, "outcome", outcome) -end - -function TemporaryResourceScope:release() - local record = scope_records[self] - if record == nil then - fail("temporary resource scope is invalid", 2) - end - if not record.active then - return false - end - record.active = false - - local first_error - local function release_operation(operation) - local ok, failure = pcall(operation) - if not ok and first_error == nil then - first_error = failure - end - end - - if record.interactive - and record.prompt_shown - and record.input_completed - and record.acquisition_completed - then - release_operation(function() - self:request_redraw("full") - end) - end - if record.direct_marker ~= nil then - release_operation(function() - record.feedback:remove_temporary_overlay(record.direct_marker) - end) - end - if record.cursor_marker ~= nil then - release_operation(function() - record.feedback:remove_temporary_overlay(record.cursor_marker) - end) - end - if record.cursor_presentation_lease ~= nil then - release_operation(function() - record.cursor_presentation_lease:release() - end) - end - if first_error ~= nil then - error(first_error, 0) - end - return true -end - -local function require_policy(service, host) - service = service or policy.new(host) - if type(service) ~= "table" or type(service.sample_acquisition) ~= "function" then - fail("AcquisitionService policy must sample acquisition settings", 3) - end - return service -end - -local function require_state(state) - state = state or sequence_state.get() - if not sequence_state.is(state) then - fail("AcquisitionService state must be the plugin-global SequenceState", 3) - end - return state -end - -local function require_transitions(transitions, state) - transitions = transitions or state_transitions.new(state) - if type(transitions) ~= "table" - or type(transitions.BeginAcquisition) ~= "function" - or type(transitions.CommitAcquiredTarget) ~= "function" - then - fail("AcquisitionService transitions must commit acquisition state", 3) - end - return transitions -end - -local function require_direct_planner(planner) - planner = planner or direct_preview_planner.new() - if type(planner) ~= "table" or type(planner.plan) ~= "function" then - fail("AcquisitionService direct planner must provide plan", 3) - end - return planner -end - -local function require_target_factory(factory, policy_service) - factory = factory or target_plan_factory.new({ policy = policy_service }) - if type(factory) ~= "table" or type(factory.build) ~= "function" then - fail("AcquisitionService target factory must provide build", 3) - end - return factory -end - -local function require_motion_factory(factory, policy_service) - factory = factory or motion_plan_factory.new({ policy = policy_service }) - if type(factory) ~= "table" or type(factory.build_for_context) ~= "function" then - fail("AcquisitionService motion factory must build contextual plans", 3) - end - return factory -end - -local function require_feedback(feedback, host, policy_service, transitions) - feedback = feedback or feedback_service.new({ - host = host, - policy = policy_service, - transitions = transitions, - }) - if type(feedback) ~= "table" - or type(feedback.create_cursor_marker) ~= "function" - or type(feedback.request_persistent) ~= "function" - then - fail("AcquisitionService feedback must manage acquisition markers", 3) - end - return feedback -end - -local function normalize_options(options, dependencies) - if AcquisitionService.is(options) and dependencies == nil then - return options - end - if type(options) ~= "table" then - fail("AcquisitionService options must be a table", 3) - end - if options.host ~= nil then - if dependencies ~= nil then - fail("AcquisitionService dependencies must be part of its options", 3) - end - return options - end - local result = {} - for key, value in pairs(dependencies or {}) do - result[key] = value - end - result.host = options - return result -end - -function AcquisitionService.new(options, dependencies) - options = normalize_options(options, dependencies) - if AcquisitionService.is(options) then - return options - end - if type(options.host) ~= "table" then - fail("AcquisitionService host must be a table", 2) - end - local state = require_state(options.state) - local transitions = require_transitions( - options.transitions or options.state_transitions, - state - ) - local policy_service = require_policy( - options.policy or options.policy_service, - options.host - ) - local service = setmetatable({}, AcquisitionService) - service_records[service] = { - host = options.host, - policy = policy_service, - state = state, - transitions = transitions, - feedback = require_feedback( - options.feedback or options.feedback_service, - options.host, - policy_service, - transitions - ), - direct_planner = require_direct_planner( - options.direct_planner or options.direct_preview_planner - ), - target_factory = require_target_factory( - options.target_factory or options.target_plan_factory, - policy_service - ), - motion_factory = require_motion_factory( - options.motion_factory or options.motion_plan_factory, - policy_service - ), - window = options.window or options.current_window, - last_scope = nil, - started_scope_count = 0, - } - return service -end - -function AcquisitionService.is(value) - return type(value) == "table" and service_records[value] ~= nil -end - -function AcquisitionService:request(descriptor, context, position, count, macro_state) - return AcquisitionRequest.new(descriptor, context, position, count, macro_state) -end - -local function current_window(record) - local window = record.window - if type(window) == "function" then - window = window() - end - if window == nil and type(record.host.read_window) == "function" then - window = record.host:read_window() - end - if window == nil then - fail("AcquisitionService requires a current window identity", 3) - end - return window -end - -function AcquisitionService:start_temporary_scope(request) - request = AcquisitionRequest.new(request) - local record = service_records[self] - local scope = TemporaryResourceScope.new(request, record.feedback, record.host) - record.last_scope = scope - record.started_scope_count = record.started_scope_count + 1 - return scope -end - -function AcquisitionService:last_temporary_scope() - return service_records[self].last_scope -end - -function AcquisitionService:started_scope_count() - return service_records[self].started_scope_count -end - -local function utf8_first_code(character) - local first = string.byte(character, 1) - if first < 0x80 then - return first - end - local length - local code - if first >= 0xc2 and first <= 0xdf then - length = 2 - code = first - 0xc0 - elseif first >= 0xe0 and first <= 0xef then - length = 3 - code = first - 0xe0 - elseif first >= 0xf0 and first <= 0xf4 then - length = 4 - code = first - 0xf0 - else - fail("ordinary input must start with a valid editor character", 3) - end - for index = 2, length do - local byte = string.byte(character, index) - if byte == nil or byte < 0x80 or byte > 0xbf then - fail("ordinary input must contain a complete editor character", 3) - end - code = code * 0x40 + byte - 0x80 - end - return code -end - -local function first_editor_character(text) - local characters = text_topology.split_editor_characters(text) - if #characters == 0 then - fail("ordinary input must contain an editor character", 3) - end - return characters[1] -end - -function M.editor_character_code(character) - character = first_editor_character(character) - local runtime = rawget(_G, "vim") - if type(runtime) == "table" - and type(runtime.fn) == "table" - and type(runtime.fn.char2nr) == "function" - then - return runtime.fn.char2nr(character) - end - return utf8_first_code(character) -end - -function M.normalize_ordinary_input(packet) - packet = domain.InputPacket.from_table(packet) - local text - if packet.kind == domain.InputPacketKind.TEXT then - text = packet.text - elseif packet.kind == domain.InputPacketKind.RAW_BYTES then - local bytes = packet:bytes() - local characters = {} - for index = 1, #bytes do - characters[index] = string.char(bytes[index]) - end - text = table.concat(characters) - else - fail("ordinary input packet must contain text or raw bytes", 2) - end - local character = first_editor_character(text) - return domain.TargetValue.character( - character, - M.editor_character_code(character) - ) -end - -local function encoded_packet_value(packet) - if packet.encoded ~= nil then - return packet.encoded - end - local bytes = packet:bytes() - if bytes == nil then - return nil - end - local characters = {} - for index = 1, #bytes do - characters[index] = string.char(bytes[index]) - end - return table.concat(characters) -end - -function M.normalize_input_packet(packet) - packet = domain.InputPacket.from_table(packet) - if packet.kind == domain.InputPacketKind.ERROR then - fail(packet.message, 2) - end - if packet.kind == domain.InputPacketKind.TEXT then - return M.normalize_ordinary_input(packet) - end - - local encoded = encoded_packet_value(packet) - if encoded == nil then - return domain.TargetValue.code_fallback(0) - end - if string.byte(encoded, 1) == 0x80 then - return domain.TargetValue.special_key(encoded, 0x80) - end - return M.normalize_ordinary_input(domain.InputPacket.text(encoded)) -end - -function M.read_previous_target(state) - if not sequence_state.is(state) then - fail("previous-input reuse requires SequenceState", 2) - end - local context = state.last_input_context - if context == nil then - return nil, nil - end - return state:get_previous_target(context), context -end - -function M.match_previous_input_trigger(first_code, triggers) - if type(first_code) ~= "number" or first_code < 0 then - fail("acquired first code must be nonnegative", 2) - end - if type(triggers) ~= "table" then - fail("previous-input triggers must be a list", 2) - end - for index, trigger in ipairs(triggers) do - if type(trigger) ~= "string" then - fail("previous-input triggers must contain strings", 2) - end - if trigger ~= "" and M.editor_character_code(trigger) == first_code then - return trigger, index - end - end - return nil -end - -function M.is_escape(packet) - packet = domain.InputPacket.from_table(packet) - if packet.kind == domain.InputPacketKind.SPECIAL_KEY - and (packet.name == "Escape" or packet.name == "Esc") - then - return true - end - if packet.kind == domain.InputPacketKind.TEXT then - return packet.text == string.char(27) - end - local bytes = packet:bytes() - return bytes ~= nil and #bytes == 1 and bytes[1] == 27 -end - -function M.is_terminal_artifact(packet) - packet = domain.InputPacket.from_table(packet) - if packet.kind ~= domain.InputPacketKind.RAW_BYTES then - return false - end - local bytes = packet:bytes() - return #bytes == 3 - and bytes[1] == 0x80 - and bytes[2] == 0xfd - and bytes[3] == 0x60 -end - -local function read_input_packet(host) - while true do - local packet = domain.InputPacket.from_table(host:read_input()) - if not M.is_terminal_artifact(packet) then - return packet - end - end -end - -local function direct_preview_settings(policy_service) - if type(policy_service.sample_direct_preview) == "function" then - return policy_service:sample_direct_preview() - end - if type(policy_service.get_boolean) == "function" then - return { - ignore_case = policy_service:get_boolean("ignore_case"), - smart_case = policy_service:get_boolean("smart_case"), - } - end - fail("AcquisitionService policy must sample direct preview settings", 3) -end - -local function acquire_in_scope(record, request, scope) - local acquisition = record.policy:sample_acquisition() - local interactive = not request.macro_state.executing - scope:set_cursor_presentation_lease( - record.feedback:create_cursor_presentation_lease( - interactive and acquisition.hide_cursor_on_cmdline - ) - ) - if not interactive then - record.host:redraw("suppressed") - end - if acquisition.mark_cursor and interactive then - scope:set_cursor_marker(record.feedback:create_cursor_marker( - request.position, - current_window(record) - )) - scope:request_redraw("screen") - end - if acquisition.mark_direct and interactive then - local view = scope:set_text_view(text_topology.from_host(record.host)) - local positions = record.direct_planner:plan( - view, - request.position, - request.descriptor, - request.count, - direct_preview_settings(record.policy) - ) - local window = scope.cursor_marker - and scope.cursor_marker.window - or current_window(record) - scope:set_direct_marker(record.feedback:create_direct_markers( - positions, - window - )) - scope:request_redraw("screen") - end - if acquisition.show_prompt and interactive then - record.host:show_prompt(M.PROMPT) - scope:mark_prompt_shown() - end - record.transitions:BeginAcquisition(request.context, request.descriptor) - local packet = scope:set_input_packet(read_input_packet(record.host)) - scope:mark_input_completed() - if M.is_escape(packet) then - local outcome = scope:set_outcome(domain.ActionOutcome.escape(request.position)) - return AcquisitionResult.new(request, { outcome = outcome }) - end - local target = scope:set_acquired_target(M.normalize_input_packet(packet)) - local previous_input = record.policy:sample_previous_input() - local trigger = scope:set_previous_input_trigger( - M.match_previous_input_trigger( - target.first_code, - previous_input.repeat_last_char_inputs - ) - ) - if trigger ~= nil then - local cached_target, source = M.read_previous_target(record.state) - scope:set_cached_target(source, cached_target) - if cached_target ~= nil then - target = cached_target - else - target = nil - scope:set_missing_previous_input(true) - record.host:emit_diagnostic("error", M.PREVIOUS_INPUT_NOT_FOUND) - local outcome = scope:set_outcome( - domain.ActionOutcome.empty(request.position) - ) - return AcquisitionResult.new(request, { - outcome = outcome, - previous_input_trigger = trigger, - previous_target_source = scope.previous_target_source, - missing_previous_input = true, - }) - end - end - scope:set_resolved_target(target) - local acquisition_time_ms - if record.policy:sample_timeouts().repeat_timeout_ms > 0 then - acquisition_time_ms = record.host:read_time_ms() - end - record.transitions:CommitAcquiredTarget( - request.context, - target, - acquisition_time_ms - ) - local view = scope.text_view - or scope:set_text_view(text_topology.from_host(record.host)) - local search_scope = record.policy:sample_search().search_scope - local target_plan = scope:set_target_plan(record.target_factory:build( - target, - nil, - { - text_view = view, - origin = request.position, - search_scope = search_scope, - effective_encoding = view.effective_encoding, - } - )) - local selection = request.context.visual and record.host:read_selection() or nil - local motion_plan = scope:set_motion_plan( - record.motion_factory:build_for_context( - target_plan, - request.descriptor, - request.context, - selection, - search_scope - ) - ) - local persistent_feedback_request - if interactive - and record.policy:sample_markers().mark_char - and feedback_service.persistent_context_eligible(request.context) - then - local window = scope.cursor_marker - and scope.cursor_marker.window - or scope.direct_marker - and scope.direct_marker.window - or current_window(record) - persistent_feedback_request = record.feedback:request_persistent({ - context = request.context, - anchor = request.position, - target_plan = target_plan, - motion_plan = motion_plan, - window = window, - }) - end - local result = AcquisitionResult.new(request, { - target = target, - target_plan = target_plan, - motion_plan = motion_plan, - acquisition_time_ms = acquisition_time_ms, - persistent_feedback_request = persistent_feedback_request, - previous_input_trigger = trigger, - previous_target_source = scope.previous_target_source, - cached_target = scope.cached_target, - missing_previous_input = scope.missing_previous_input, - }) - scope:mark_acquisition_completed() - return result -end - -local function error_message(failure) - local message = tostring(failure) - if message == "" then - return "clever-f: Acquisition failed" - end - return message -end - -function AcquisitionService:acquire(descriptor, context, position, count, macro_state) - local request = self:request(descriptor, context, position, count, macro_state) - local scope = self:start_temporary_scope(request) - local record = service_records[self] - local ok, result = xpcall(function() - return acquire_in_scope(record, request, scope) - end, function(failure) - return failure - end) - - if not ok then - local diagnostic = error_message(result) - result = AcquisitionResult.new(request, { - outcome = domain.ActionOutcome.error(request.position, diagnostic), - }) - end - - local cleanup_ok, cleanup_error = pcall(function() - scope:release() - end) - if not cleanup_ok and ok then - local diagnostic = error_message(cleanup_error) - result = AcquisitionResult.new(request, { - outcome = domain.ActionOutcome.error(request.position, diagnostic), - }) - ok = false - end - - if not ok then - pcall(record.host.emit_diagnostic, record.host, "error", result.outcome.diagnostic) - end - return result -end - -function M.new(options, dependencies) - return AcquisitionService.new(options, dependencies) -end - -setmetatable(M, { - __call = function(_, options, dependencies) - return AcquisitionService.new(options, dependencies) - end, -}) - -return M diff --git a/lua/clever_f/action_facade.lua b/lua/clever_f/action_facade.lua deleted file mode 100644 index a3fe173..0000000 --- a/lua/clever_f/action_facade.lua +++ /dev/null @@ -1,134 +0,0 @@ -local domain = require("clever_f.domain") -local sequence_coordinator = require("clever_f.sequence_coordinator") - -local M = {} -local ActionFacade = {} -ActionFacade.__index = ActionFacade -M.ActionFacade = ActionFacade - -local facade_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -function ActionFacade.new(options) - if ActionFacade.is(options) then - return options - end - local coordinator - if sequence_coordinator.SequenceCoordinator.is(options) then - coordinator = options - elseif type(options) == "table" then - coordinator = options.coordinator - or options.sequence_coordinator - or sequence_coordinator.new(options) - else - fail("ActionFacade options must be a table", 2) - end - if not sequence_coordinator.SequenceCoordinator.is(coordinator) then - fail("ActionFacade requires a SequenceCoordinator", 2) - end - - local facade = setmetatable({}, ActionFacade) - facade_records[facade] = { coordinator = coordinator } - return facade -end - -function ActionFacade.is(value) - return type(value) == "table" and facade_records[value] ~= nil -end - -function ActionFacade:coordinator() - return facade_records[self].coordinator -end - -function ActionFacade:primary(descriptor) - local outcome = self:coordinator():primary(descriptor) - if not domain.ActionOutcome.is(outcome) then - fail("SequenceCoordinator must return an ActionOutcome", 2) - end - return outcome -end - -function ActionFacade:invoke_descriptor(value) - local descriptor = sequence_coordinator.validate_primary_descriptor(value) - return self:primary(descriptor) -end - -ActionFacade.invoke_primary = ActionFacade.invoke_descriptor -ActionFacade.start = ActionFacade.invoke_descriptor -ActionFacade.FreeForm = ActionFacade.invoke_descriptor - -function ActionFacade:start_find_forward() - return self:primary("f") -end - -function ActionFacade:start_find_backward() - return self:primary("F") -end - -function ActionFacade:start_till_forward() - return self:primary("t") -end - -function ActionFacade:start_till_backward() - return self:primary("T") -end - -ActionFacade.StartFindForward = ActionFacade.start_find_forward -ActionFacade.StartFindBackward = ActionFacade.start_find_backward -ActionFacade.StartTillForward = ActionFacade.start_till_forward -ActionFacade.StartTillBackward = ActionFacade.start_till_backward - -function ActionFacade:reset() - local outcome = self:coordinator():reset() - if not domain.ActionOutcome.is(outcome) then - fail("SequenceCoordinator must return an ActionOutcome", 2) - end - return outcome -end - -ActionFacade.Reset = ActionFacade.reset - -function ActionFacade:diagnostic_full_reset() - local outcome = self:coordinator():diagnostic_full_reset() - if not domain.ActionOutcome.is(outcome) then - fail("SequenceCoordinator must return an ActionOutcome", 2) - end - return outcome -end - -ActionFacade.DiagnosticFullReset = ActionFacade.diagnostic_full_reset - -local function explicit_outcome(facade, method_name) - local coordinator = facade:coordinator() - local outcome = coordinator[method_name](coordinator) - if not domain.ActionOutcome.is(outcome) then - fail("SequenceCoordinator must return an ActionOutcome", 3) - end - return outcome -end - -function ActionFacade:repeat_same_direction() - return explicit_outcome(self, "repeat_same_direction") -end - -function ActionFacade:repeat_opposite_direction() - return explicit_outcome(self, "repeat_opposite_direction") -end - -ActionFacade.RepeatSameDirection = ActionFacade.repeat_same_direction -ActionFacade.RepeatOppositeDirection = ActionFacade.repeat_opposite_direction - -function M.new(options) - return ActionFacade.new(options) -end - -setmetatable(M, { - __call = function(_, options) - return ActionFacade.new(options) - end, -}) - -return M diff --git a/lua/clever_f/capabilities.lua b/lua/clever_f/capabilities.lua deleted file mode 100644 index 325d015..0000000 --- a/lua/clever_f/capabilities.lua +++ /dev/null @@ -1,163 +0,0 @@ -local M = {} - -M.read_methods = { - text = { "read_text" }, - buffer = { "read_buffer", "read_window" }, - cursor = { "read_cursor" }, - mode = { "read_mode", "read_pending_operator" }, - selection = { "read_selection" }, - count = { "read_count" }, - configuration = { "read_configuration", "configuration_present" }, - encoding = { "read_encoding" }, - case_conversion = { "lowercase" }, - macro_state = { "read_macro_state" }, - fold_state = { "read_fold_state" }, - time = { "read_time_ms" }, - highlight_groups = { "read_highlight_group" }, -} - -M.effect_methods = { - movement = { "apply_cursor", "apply_selection", "set_operator_inclusive" }, - configuration = { "write_configuration" }, - input = { "read_input" }, - folds = { "open_fold" }, - prompt = { "show_prompt" }, - redraw = { "redraw" }, - diagnostics = { "emit_diagnostic" }, - highlights = { - "define_highlight_group", - "create_highlight", - "remove_highlight", - }, - cursor_presentation = { - "supports_cursor_presentation", - "suppress_cursor_presentation", - "restore_cursor_presentation", - }, - timers = { "supports_timers", "start_timer", "stop_timer" }, - events = { - "register_events", - "remove_event_registration", - "deliver_event", - "begin_action_transition", - "commit_action_transition", - }, - mappings = { "register_action", "register_mapping" }, - dot_repeat = { "register_dot_repeat" }, -} - -local function collect_methods(groups) - local result = {} - local group_names = {} - for group_name in pairs(groups) do - group_names[#group_names + 1] = group_name - end - table.sort(group_names) - for _, group_name in ipairs(group_names) do - for _, method_name in ipairs(groups[group_name]) do - result[#result + 1] = method_name - end - end - return result -end - -local all_methods = collect_methods(M.read_methods) -for _, method_name in ipairs(collect_methods(M.effect_methods)) do - all_methods[#all_methods + 1] = method_name -end -table.sort(all_methods) - -function M.required_methods() - local result = {} - for index = 1, #all_methods do - result[index] = all_methods[index] - end - return result -end - -function M.missing_methods(host) - local missing = {} - for _, method_name in ipairs(all_methods) do - if type(host) ~= "table" or type(host[method_name]) ~= "function" then - missing[#missing + 1] = method_name - end - end - return missing -end - -function M.assert_implements(host) - local missing = M.missing_methods(host) - if #missing > 0 then - error("host is missing semantic capabilities: " .. table.concat(missing, ", "), 2) - end - return host -end - -local EventQueue = {} -EventQueue.__index = EventQueue -M.EventQueue = EventQueue - -function EventQueue.new(deliver) - if type(deliver) ~= "function" then - error("event delivery must be a function", 2) - end - return setmetatable({ - _deliver = deliver, - _active_token = nil, - _pending = {}, - _next_token = 1, - }, EventQueue) -end - -function EventQueue:begin_transition() - if self._active_token ~= nil then - error("an action transition is already active", 2) - end - local token = "action-transition-" .. tostring(self._next_token) - self._next_token = self._next_token + 1 - self._active_token = token - self._pending = {} - return token -end - -function EventQueue:is_transition_active() - return self._active_token ~= nil -end - -function EventQueue:pending_count() - return #self._pending -end - -function EventQueue:emit(name, payload) - if type(name) ~= "string" or name == "" then - error("event name must be a nonempty string", 2) - end - if self._active_token ~= nil then - self._pending[#self._pending + 1] = { - name = name, - payload = payload, - } - return false - end - self._deliver(name, payload) - return true -end - -function EventQueue:commit_transition(token) - if self._active_token == nil then - error("no action transition is active", 2) - end - if token ~= self._active_token then - error("action transition token does not match", 2) - end - - local pending = self._pending - self._active_token = nil - self._pending = {} - for index = 1, #pending do - local event = pending[index] - self._deliver(event.name, event.payload) - end -end - -return M diff --git a/lua/clever_f/case_policy.lua b/lua/clever_f/case_policy.lua deleted file mode 100644 index 2631d5b..0000000 --- a/lua/clever_f/case_policy.lua +++ /dev/null @@ -1,190 +0,0 @@ -local domain = require("clever_f.domain") - -local M = {} -local CasePolicyResolver = {} -M.CasePolicyResolver = CasePolicyResolver - -local resolver_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("case policy target must be a TargetValue", 2) - end - return target -end - -local function require_boolean(value, name) - if type(value) ~= "boolean" then - fail((name or "value") .. " must be a Boolean", 2) - end - return value -end - -local function require_string(value, name, allow_empty) - if type(value) ~= "string" or (not allow_empty and value == "") then - fail((name or "value") .. " must be a string", 2) - end - return value -end - -local function default_lowercase(value) - local runtime = rawget(_G, "vim") - if type(runtime) ~= "table" - or type(runtime.fn) ~= "table" - or type(runtime.fn.tolower) ~= "function" - then - fail("editor-compatible case conversion requires Nvim or a lowercase converter", 2) - end - return runtime.fn.tolower(value) -end - -local function lowercase_function(options) - if options == nil then - return default_lowercase - end - if type(options) == "function" then - return options - end - if type(options) ~= "table" then - fail("case policy options must be a table or lowercase function", 3) - end - local lowercase = options.lowercase or options.to_lower - if lowercase == nil then - return default_lowercase - end - if type(lowercase) ~= "function" then - fail("case policy lowercase converter must be a function", 3) - end - return lowercase -end - -local resolver_metatable = { - __index = CasePolicyResolver, - __newindex = function() - fail("CasePolicyResolver values are immutable", 2) - end, - __tostring = function() - return "case-policy-resolver" - end, - __metatable = "clever_f.case_policy.CasePolicyResolver", -} - -function CasePolicyResolver.new(options) - if CasePolicyResolver.is(options) then - return options - end - local resolver = setmetatable({}, resolver_metatable) - resolver_records[resolver] = { - lowercase = lowercase_function(options), - } - return resolver -end - -function CasePolicyResolver.is(value) - return type(value) == "table" and resolver_records[value] ~= nil -end - -function M.new(options) - return CasePolicyResolver.new(options) -end - -setmetatable(M, { - __call = function(_, options) - return CasePolicyResolver.new(options) - end, -}) - -function M.is_lower_ascii(value) - if domain.TargetValue.is(value) then - value = value.value - end - if type(value) ~= "string" or #value ~= 1 then - return false - end - local byte = value:byte(1) - return byte >= string.byte("a") and byte <= string.byte("z") -end - -function M.resolve_case_mode(target, ignore_case, smart_case) - target = require_target(target) - require_boolean(ignore_case, "ignore_case") - require_boolean(smart_case, "smart_case") - - if ignore_case then - return domain.CaseMode.INSENSITIVE - end - if smart_case and M.is_lower_ascii(target) then - return domain.CaseMode.INSENSITIVE - end - return domain.CaseMode.SENSITIVE -end - -function CasePolicyResolver:resolve(target, ignore_case, smart_case) - if type(ignore_case) == "table" and smart_case == nil then - local match_policy = ignore_case - ignore_case = match_policy.ignore_case - smart_case = match_policy.smart_case - end - return M.resolve_case_mode(target, ignore_case, smart_case) -end - -function CasePolicyResolver:lowercase(value) - require_string(value, "case comparison value", true) - local lowercase = resolver_records[self].lowercase(value) - if type(lowercase) ~= "string" then - fail("case policy lowercase converter must return a string", 2) - end - return lowercase -end - -function CasePolicyResolver:equal(left, right, case_mode) - require_string(left, "left case comparison value", true) - require_string(right, "right case comparison value", true) - case_mode = domain.CaseMode.from_string(case_mode) - - if case_mode == domain.CaseMode.SENSITIVE then - return left == right - end - return self:lowercase(left) == self:lowercase(right) -end - -function CasePolicyResolver:comparator(target_character, case_mode) - require_string(target_character, "target character", true) - case_mode = domain.CaseMode.from_string(case_mode) - - if target_character == "" then - return function() - return false - end - end - - if case_mode == domain.CaseMode.SENSITIVE then - return function(candidate_character) - return type(candidate_character) == "string" - and candidate_character ~= "" - and candidate_character == target_character - end - end - - local folded_target = self:lowercase(target_character) - local lowercase = resolver_records[self].lowercase - return function(candidate_character) - if type(candidate_character) ~= "string" or candidate_character == "" then - return false - end - local folded_candidate = lowercase(candidate_character) - if type(folded_candidate) ~= "string" then - fail("case policy lowercase converter must return a string", 2) - end - return folded_candidate == folded_target - end -end - -M.resolve = M.resolve_case_mode -M.is_lowercase_ascii = M.is_lower_ascii - -return M diff --git a/lua/clever_f/composition_root.lua b/lua/clever_f/composition_root.lua deleted file mode 100644 index 35aaf1f..0000000 --- a/lua/clever_f/composition_root.lua +++ /dev/null @@ -1,232 +0,0 @@ -local action_facade = require("clever_f.action_facade") -local capabilities = require("clever_f.capabilities") -local feedback_service = require("clever_f.feedback_service") -local policy = require("clever_f.policy") -local sequence_coordinator = require("clever_f.sequence_coordinator") -local sequence_state = require("clever_f.sequence_state") -local state_transitions = require("clever_f.state_transitions") - -local M = {} -local CompositionRoot = {} -CompositionRoot.__index = CompositionRoot -M.CompositionRoot = CompositionRoot -M.ACTION_NAMES = { - "StartFindForward", - "StartFindBackward", - "StartTillForward", - "StartTillBackward", - "Reset", - "RepeatSameDirection", - "RepeatOppositeDirection", -} -M.DEFAULT_MAPPING_MODES = { "n", "x", "o" } -M.DEFAULT_MAPPING_OPTIONS = { - silent = true, - remap = false, - preserve_count = true, -} -M.DEFAULT_MAPPINGS = { - { lhs = "f", action = "StartFindForward" }, - { lhs = "F", action = "StartFindBackward" }, - { lhs = "t", action = "StartTillForward" }, - { lhs = "T", action = "StartTillBackward" }, -} - -local ACTION_INVOCATIONS = { - StartFindForward = function(facade) - return facade:start_find_forward() - end, - StartFindBackward = function(facade) - return facade:start_find_backward() - end, - StartTillForward = function(facade) - return facade:start_till_forward() - end, - StartTillBackward = function(facade) - return facade:start_till_backward() - end, - Reset = function(facade) - return facade:reset() - end, - RepeatSameDirection = function(facade) - return facade:repeat_same_direction() - end, - RepeatOppositeDirection = function(facade) - return facade:repeat_opposite_direction() - end, -} - -local root_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function normalize_options(options) - if type(options) ~= "table" then - fail("CompositionRoot options must be a table", 3) - end - if options.host == nil then - return { host = options } - end - return options -end - -function CompositionRoot.new(options) - if CompositionRoot.is(options) then - return options - end - options = normalize_options(options) - local host = capabilities.assert_implements(options.host) - local state = sequence_state.new() - local transitions = options.transitions - or options.state_transitions - or state_transitions.new(state) - local policy_service = options.policy - or options.policy_service - or policy.new(host) - local feedback = options.feedback - or options.feedback_service - or feedback_service.new({ - host = host, - state = state, - transitions = transitions, - policy = policy_service, - }) - local coordinator = options.coordinator - or options.sequence_coordinator - or sequence_coordinator.new({ - host = host, - state = state, - transitions = transitions, - policy = policy_service, - feedback = feedback, - }) - local facade = options.facade - or options.action_facade - or action_facade.new({ coordinator = coordinator }) - - local root = setmetatable({}, CompositionRoot) - root_records[root] = { - host = host, - state = state, - transitions = transitions, - policy = policy_service, - feedback = feedback, - coordinator = coordinator, - facade = facade, - activation = nil, - last_highlight_refresh = nil, - } - return root -end - -function CompositionRoot.is(value) - return type(value) == "table" and root_records[value] ~= nil -end - -local function register_logical_actions(record) - local registrations = {} - for _, name in ipairs(M.ACTION_NAMES) do - local invoke = ACTION_INVOCATIONS[name] - registrations[name] = record.host:register_action(name, function(...) - return invoke(record.facade, ...) - end) - end - return registrations -end - -local function register_default_mappings(record, setup) - local registrations = {} - if not setup.install_default_mappings then - return registrations - end - for _, mapping in ipairs(M.DEFAULT_MAPPINGS) do - registrations[mapping.lhs] = record.host:register_mapping( - M.DEFAULT_MAPPING_MODES, - mapping.lhs, - mapping.action, - M.DEFAULT_MAPPING_OPTIONS - ) - end - return registrations -end - -function CompositionRoot:activate() - local record = root_records[self] - if record.activation == nil then - local setup = record.policy:capture_activation() - local feedback_activation = record.feedback:activate() - local highlights = record.feedback:evaluate_highlights() - local colorscheme_registration = record.host:register_events( - "ColorScheme", - function() - record.last_highlight_refresh = record.feedback:evaluate_highlights() - end, - { owner = "clever_f", lifecycle = "colorscheme" } - ) - local actions = register_logical_actions(record) - record.activation = { - state = record.state, - setup = setup, - feedback = feedback_activation, - highlights = highlights, - colorscheme_registration = colorscheme_registration, - actions = actions, - mappings = register_default_mappings(record, setup), - } - end - return record.activation -end - -function CompositionRoot:last_highlight_refresh() - return root_records[self].last_highlight_refresh -end - -function CompositionRoot:host() - return root_records[self].host -end - -function CompositionRoot:state() - return root_records[self].state -end - -function CompositionRoot:transitions() - return root_records[self].transitions -end - -function CompositionRoot:policy() - return root_records[self].policy -end - -function CompositionRoot:feedback() - return root_records[self].feedback -end - -function CompositionRoot:coordinator() - return root_records[self].coordinator -end - -function CompositionRoot:facade() - return root_records[self].facade -end - -function CompositionRoot:invoke_descriptor(value) - return self:facade():invoke_descriptor(value) -end - -function CompositionRoot:diagnostic_full_reset() - return self:facade():diagnostic_full_reset() -end - -function M.new(options) - return CompositionRoot.new(options) -end - -setmetatable(M, { - __call = function(_, options) - return CompositionRoot.new(options) - end, -}) - -return M diff --git a/lua/clever_f/destination_engine.lua b/lua/clever_f/destination_engine.lua deleted file mode 100644 index 53cb654..0000000 --- a/lua/clever_f/destination_engine.lua +++ /dev/null @@ -1,201 +0,0 @@ -local domain = require("clever_f.domain") -local text_topology = require("clever_f.text_topology") - -local M = {} -local DestinationEngine = {} -M.DestinationEngine = DestinationEngine - -local engines = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local engine_metatable = { - __index = DestinationEngine, - __newindex = function() - fail("DestinationEngine values are immutable", 2) - end, - __tostring = function() - return "destination-engine" - end, - __metatable = "clever_f.destination_engine.DestinationEngine", -} - -function DestinationEngine.new() - local engine = setmetatable({}, engine_metatable) - engines[engine] = true - return engine -end - -function DestinationEngine.is(value) - return type(value) == "table" and engines[value] == true -end - -local function calculation_inputs(view, origin, plan, count, first_move) - if not text_topology.TextView.is(view) then - fail("destination calculation view must be a TextView", 3) - end - - origin = domain.Position.coerce(origin) - if not view:is_valid_cursor_position(origin) then - fail("destination calculation origin must be a valid cursor position", 3) - end - if not domain.ResolvedMotionPlan.is(plan) then - fail("destination calculation plan must be a ResolvedMotionPlan", 3) - end - count = domain.Count.new(count) - if type(first_move) ~= "boolean" then - fail("destination calculation first_move must be a Boolean", 3) - end - - local bounds = view:match_start_bounds(plan.search_scope, origin) - return { - view = view, - origin = origin, - plan = plan, - count = count, - first_move = first_move, - bounds = bounds, - } -end - -local function candidate_starts(request, origin) - return request.view:iter_strict( - origin, - request.plan.descriptor.direction, - request.bounds - ) -end - -local function regular_destination(request, target_position) - local descriptor = request.plan.descriptor - if descriptor.family == domain.Family.FIND then - return target_position - end - if descriptor.direction == domain.Direction.FORWARD then - return request.view:predecessor(target_position) - end - return request.view:successor(target_position) -end - -local function target_destination(request, target_position) - local descriptor = request.plan.descriptor - if request.plan.endpoint_policy == domain.EndpointPolicy.VISUAL_EXCLUSIVE - and descriptor.direction == domain.Direction.FORWARD - then - if descriptor.family == domain.Family.FIND then - return request.view:successor(target_position) - end - return target_position - end - return regular_destination(request, target_position) -end - -local function strict_destination(descriptor, destination, origin) - local comparison = domain.Position.compare(destination, origin) - if descriptor.direction == domain.Direction.FORWARD then - return comparison > 0 - end - return comparison < 0 -end - -local function acceptable_destination( - request, - destination, - origin, - allow_till_equality -) - local descriptor = request.plan.descriptor - if strict_destination(descriptor, destination, origin) then - return true - end - return descriptor.family == domain.Family.TILL - and allow_till_equality - and domain.Position.equal(destination, origin) -end - -local function next_destination(request, origin, allow_till_equality) - local candidates = candidate_starts(request, origin) - - while true do - local target_position, character = candidates() - if target_position == nil then - return nil - end - if request.plan.target_plan:matches( - character, - target_position, - request.view - ) then - local destination = target_destination(request, target_position) - if destination ~= nil - and acceptable_destination( - request, - destination, - origin, - allow_till_equality - ) - then - return destination - end - end - end -end - -local function till_equality_allowed(request, successful_steps) - return request.first_move and successful_steps == 0 -end - -local function boundary_outcome(request, endpoint, successful_steps) - if successful_steps > 0 then - return domain.SearchOutcome.boundary_after_partial( - endpoint, - successful_steps - ) - end - return domain.SearchOutcome.boundary_before_any(request.origin) -end - -function DestinationEngine:calculate(view, origin, plan, count, first_move) - local request = calculation_inputs(view, origin, plan, count, first_move) - local current_origin = request.origin - local successful_steps = 0 - - while successful_steps < request.count.value do - local destination = next_destination( - request, - current_origin, - till_equality_allowed(request, successful_steps) - ) - if destination == nil then - break - end - current_origin = destination - successful_steps = successful_steps + 1 - end - - if successful_steps == request.count.value then - return domain.SearchOutcome.complete(current_origin, successful_steps) - end - return boundary_outcome(request, current_origin, successful_steps) -end - -function M.new() - return DestinationEngine.new() -end - -function M.calculate(view, origin, plan, count, first_move) - return DestinationEngine.new():calculate(view, origin, plan, count, first_move) -end - -M.resolve = M.calculate -M.search = M.calculate - -setmetatable(M, { - __call = function() - return DestinationEngine.new() - end, -}) - -return M diff --git a/lua/clever_f/direct_preview_planner.lua b/lua/clever_f/direct_preview_planner.lua deleted file mode 100644 index 6735924..0000000 --- a/lua/clever_f/direct_preview_planner.lua +++ /dev/null @@ -1,250 +0,0 @@ -local case_policy = require("clever_f.case_policy") -local domain = require("clever_f.domain") -local text_topology = require("clever_f.text_topology") - -local M = {} -local DirectPreviewPlanner = {} -DirectPreviewPlanner.__index = DirectPreviewPlanner -M.DirectPreviewPlanner = DirectPreviewPlanner - -local planner_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function require_view(view) - if not text_topology.TextView.is(view) then - fail("direct preview text must be a TextView", 3) - end - return view -end - -local function require_case_resolver(options) - options = options or {} - if type(options) == "function" then - options = { lowercase = options } - end - if type(options) ~= "table" then - fail("DirectPreviewPlanner options must be a table", 3) - end - local resolver = options.case_resolver - if resolver == nil then - resolver = case_policy.new({ lowercase = options.lowercase }) - end - if type(resolver) ~= "table" or type(resolver.lowercase) ~= "function" then - fail("DirectPreviewPlanner case resolver must provide lowercase", 3) - end - return resolver -end - -function DirectPreviewPlanner.new(options) - if DirectPreviewPlanner.is(options) then - return options - end - local planner = setmetatable({}, DirectPreviewPlanner) - planner_records[planner] = { - case_resolver = require_case_resolver(options), - } - return planner -end - -function DirectPreviewPlanner.is(value) - return type(value) == "table" and planner_records[value] ~= nil -end - -function DirectPreviewPlanner:scan_current_line(view, origin, direction) - require_view(view) - origin = domain.Position.coerce(origin) - direction = domain.Direction.from_string(direction) - local records = {} - for position, character, span in view:iter_strict( - origin, - direction, - domain.SearchScope.CURRENT_LINE - ) do - records[#records + 1] = { - position = position, - character = character, - span = span, - } - end - return records -end - -function M.marker_position(descriptor, target_position) - domain.Descriptor.from_string(descriptor) - return domain.Position.coerce(target_position) -end - -function M.normalize_count(count) - return domain.Count.to_number(count) -end - -function DirectPreviewPlanner:normalize_count(count) - return M.normalize_count(count) -end - -function M.direction_for_descriptor(descriptor) - descriptor = domain.Descriptor.from_string(descriptor) - if domain.Descriptor.is_uppercase(descriptor) then - return domain.Direction.BACKWARD - end - return domain.Direction.FORWARD -end - -function DirectPreviewPlanner:scan_for_descriptor(view, origin, descriptor) - return self:scan_current_line( - view, - origin, - M.direction_for_descriptor(descriptor) - ) -end - -local function increment_counter(counters, character) - local value = (counters[character] or 0) + 1 - counters[character] = value - return value -end - -local function is_upper_ascii(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") -end - -local function increment_smart_counters(counters, character) - local exact_count = increment_counter(counters, character) - local lowercase_count - if is_upper_ascii(character) then - local lowercase = string.char(character:byte(1) + 32) - lowercase_count = increment_counter(counters, lowercase) - end - return exact_count, lowercase_count -end - -M.is_upper_ascii = is_upper_ascii - -local function require_case_setting(settings, name) - local value = settings[name] - if value == nil then - return false - end - if type(value) ~= "boolean" then - fail("direct preview " .. name .. " must be a Boolean", 3) - end - return value -end - -function M.case_grouping_settings(settings) - settings = settings or {} - if type(settings) ~= "table" then - fail("direct preview case settings must be a table", 2) - end - return { - ignore_case = require_case_setting(settings, "ignore_case"), - smart_case = require_case_setting(settings, "smart_case"), - } -end - -function M.validate_marker_positions(view, positions) - require_view(view) - if type(positions) ~= "table" then - fail("direct preview markers must be a list of positions", 2) - end - - local result = {} - local seen = {} - local item_count = 0 - for key, position in pairs(positions) do - if type(key) ~= "number" - or key ~= math.floor(key) - or key < 1 - or key > #positions - then - fail("direct preview markers must be a list of positions", 2) - end - position = domain.Position.coerce(position) - if not view:is_character_start(position) then - fail("direct preview marker must start an editor character", 2) - end - local identity = tostring(position.line) .. ":" .. tostring(position.byte_column) - if seen[identity] then - fail("direct preview marker positions must be unique", 2) - end - seen[identity] = true - result[key] = position - item_count = item_count + 1 - end - if item_count ~= #positions then - fail("direct preview markers must be a list of positions", 2) - end - return result -end - -function DirectPreviewPlanner:plan(view, origin, descriptor, count, settings) - local grouping = M.case_grouping_settings(settings) - local ignore_case = grouping.ignore_case - local smart_case = grouping.smart_case - local selected_occurrence = self:normalize_count(count) - local counters = {} - local positions = {} - local resolver = planner_records[self].case_resolver - for _, record in ipairs(self:scan_for_descriptor(view, origin, descriptor)) do - local selected - if ignore_case then - selected = increment_counter( - counters, - resolver:lowercase(record.character) - ) == selected_occurrence - elseif smart_case then - local exact_count, lowercase_count = increment_smart_counters( - counters, - record.character - ) - selected = exact_count == selected_occurrence - or lowercase_count == selected_occurrence - else - selected = increment_counter( - counters, - record.character - ) == selected_occurrence - end - if selected then - positions[#positions + 1] = M.marker_position(descriptor, record.position) - end - end - return M.validate_marker_positions(view, positions) -end - -function M.new(options) - return DirectPreviewPlanner.new(options) -end - -function M.scan_current_line(view, origin, direction) - return DirectPreviewPlanner.new():scan_current_line(view, origin, direction) -end - -function M.scan_for_descriptor(view, origin, descriptor) - return DirectPreviewPlanner.new():scan_for_descriptor(view, origin, descriptor) -end - -function M.plan(view, origin, descriptor, count, settings, options) - return DirectPreviewPlanner.new(options):plan( - view, - origin, - descriptor, - count, - settings - ) -end - -setmetatable(M, { - __call = function(_, options) - return DirectPreviewPlanner.new(options) - end, -}) - -return M diff --git a/lua/clever_f/domain.lua b/lua/clever_f/domain.lua deleted file mode 100644 index 0d802b3..0000000 --- a/lua/clever_f/domain.lua +++ /dev/null @@ -1,1390 +0,0 @@ -local M = {} - -local records = setmetatable({}, { __mode = "k" }) -local record_types = setmetatable({}, { __mode = "k" }) -local methods = {} -local formatters = {} -local equalities = {} -local metatables = {} - -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 register_type(type_name, type_methods, formatter, equality) - methods[type_name] = type_methods or {} - formatters[type_name] = formatter - equalities[type_name] = equality - - local mt = { - __index = function(value, key) - local field = records[value][key] - if field ~= nil then - return field - end - return methods[type_name][key] - end, - __newindex = function() - fail(type_name .. " values are immutable", 2) - end, - __tostring = function(value) - local format = formatters[type_name] - if format then - return format(records[value]) - end - return type_name - end, - __eq = function(left, right) - if record_types[left] ~= type_name or record_types[right] ~= type_name then - return false - end - local equal = equalities[type_name] - if equal then - return equal(records[left], records[right]) - end - return rawequal(left, right) - end, - __metatable = "clever_f.domain." .. type_name, - } - metatables[type_name] = mt -end - -local function new_record(type_name, fields) - local value = {} - records[value] = fields - record_types[value] = type_name - return setmetatable(value, metatables[type_name]) -end - -local function is_record(value, type_name) - return record_types[value] == type_name -end - -local function require_record(value, type_name, name) - if not is_record(value, type_name) then - fail((name or "value") .. " must be a " .. type_name, 2) - end - return value -end - -local function require_string(value, name, allow_empty) - if type(value) ~= "string" or (not allow_empty and value == "") then - fail(name .. " must be " .. (allow_empty and "a string" or "a nonempty string"), 2) - end - return value -end - -local function require_boolean(value, name) - if type(value) ~= "boolean" then - fail(name .. " must be a Boolean", 2) - end - return value -end - -local function require_nonnegative_integer(value, name) - if not is_integer(value) or value < 0 then - fail(name .. " must be a nonnegative integer", 2) - end - return value -end - -function M.type_of(value) - return record_types[value] -end - -local function define_enum(type_name, entries) - local enum_methods = {} - local namespace = {} - local by_value = {} - - register_type(type_name, enum_methods, function(data) - return data.value - end) - - for constant, serialized in pairs(entries) do - local value = new_record(type_name, { - name = constant, - value = serialized, - }) - namespace[constant] = value - by_value[serialized] = value - end - - function namespace.from_string(value) - if is_record(value, type_name) then - return value - end - local result = by_value[value] - if result == nil then - fail("value must be a valid " .. type_name, 2) - end - return result - end - - function namespace.is(value) - return is_record(value, type_name) - end - - function enum_methods:to_string() - return records[self].value - end - - return namespace -end - -M.Family = define_enum("Family", { - FIND = "FIND", - TILL = "TILL", -}) - -M.Direction = define_enum("Direction", { - FORWARD = "forward", - BACKWARD = "backward", -}) - -M.SelectionKind = define_enum("SelectionKind", { - NONE = "none", - CHARACTER = "character", - LINE = "line", - BLOCK = "block", -}) - -M.SelectionOption = define_enum("SelectionOption", { - INCLUSIVE = "inclusive", - EXCLUSIVE = "exclusive", -}) - -M.TargetKind = define_enum("TargetKind", { - CHARACTER = "character", - SPECIAL_KEY = "special_key", - CODE_FALLBACK = "code_fallback", -}) - -M.CaseMode = define_enum("CaseMode", { - SENSITIVE = "sensitive", - INSENSITIVE = "insensitive", -}) - -M.TargetPlanKind = define_enum("TargetPlanKind", { - EMPTY = "empty", - LITERAL = "literal", - BACKSLASH = "backslash", - SYMBOL = "symbol", - MIGEMO = "migemo", -}) - -M.SearchScope = define_enum("SearchScope", { - BUFFER = "buffer", - CURRENT_LINE = "current_line", -}) - -M.EndpointPolicy = define_enum("EndpointPolicy", { - REGULAR = "regular", - VISUAL_EXCLUSIVE = "visual_exclusive", -}) - -M.SearchStatus = define_enum("SearchStatus", { - COMPLETE = "complete", - BOUNDARY_AFTER_PARTIAL = "boundary_after_partial", - BOUNDARY_BEFORE_ANY = "boundary_before_any", -}) - -M.ActionKind = define_enum("ActionKind", { - MOVEMENT = "movement", - NEUTRAL = "neutral", - ESCAPE = "escape", - FAILED_SEARCH = "failed_search", - ERROR = "error", - EMPTY = "empty", -}) - -local Position = {} -M.Position = Position - -register_type("Position", Position, function(data) - return string.format("(%d,%d)", data.line, data.byte_column) -end, function(left, right) - return left.line == right.line and left.byte_column == right.byte_column -end) - -function Position.new(line, byte_column) - if not is_integer(line) or line < 1 then - fail("line must be a positive one-based integer", 2) - end - if not is_integer(byte_column) or byte_column < 1 then - fail("byte_column must be a positive one-based integer", 2) - end - return new_record("Position", { - line = line, - byte_column = byte_column, - }) -end - -function Position.coerce(value) - if Position.is(value) then - return value - end - if type(value) ~= "table" then - fail("position must be a Position or position table", 2) - end - return Position.new(value.line, value.byte_column) -end - -function Position.is(value) - return is_record(value, "Position") -end - -function Position.compare(left, right) - require_record(left, "Position", "left") - require_record(right, "Position", "right") - if left.line < right.line then - return -1 - end - if left.line > right.line then - return 1 - end - if left.byte_column < right.byte_column then - return -1 - end - if left.byte_column > right.byte_column then - return 1 - end - return 0 -end - -function Position.equal(left, right) - return Position.compare(left, right) == 0 -end - -function Position.stationary(left, right) - return Position.equal(left, right) -end - -function Position.is_forward(candidate, origin) - return Position.compare(candidate, origin) > 0 -end - -function Position.is_backward(candidate, origin) - return Position.compare(candidate, origin) < 0 -end - -function Position:to_table() - return { - line = self.line, - byte_column = self.byte_column, - } -end - -local Descriptor = {} -M.Descriptor = Descriptor - -register_type("Descriptor", Descriptor, function(data) - return data.value -end) - -local descriptors_by_string = {} -local descriptors_by_parts = {} - -local function descriptor_key(family, direction) - return family.value .. ":" .. direction.value -end - -local function define_descriptor(name, serialized, family, direction) - local descriptor = new_record("Descriptor", { - name = name, - value = serialized, - family = family, - direction = direction, - uppercase = serialized:match("%u") ~= nil, - }) - descriptors_by_string[serialized] = descriptor - descriptors_by_parts[descriptor_key(family, direction)] = descriptor - Descriptor[name] = descriptor - Descriptor[serialized] = descriptor - return descriptor -end - -Descriptor.FIND_FORWARD = define_descriptor( - "FIND_FORWARD", - "f", - M.Family.FIND, - M.Direction.FORWARD -) -Descriptor.FIND_BACKWARD = define_descriptor( - "FIND_BACKWARD", - "F", - M.Family.FIND, - M.Direction.BACKWARD -) -Descriptor.TILL_FORWARD = define_descriptor( - "TILL_FORWARD", - "t", - M.Family.TILL, - M.Direction.FORWARD -) -Descriptor.TILL_BACKWARD = define_descriptor( - "TILL_BACKWARD", - "T", - M.Family.TILL, - M.Direction.BACKWARD -) - -function Descriptor.is(value) - return is_record(value, "Descriptor") -end - -function Descriptor.is_valid(value) - return Descriptor.is(value) or descriptors_by_string[value] ~= nil -end - -function Descriptor.from_string(value) - if Descriptor.is(value) then - return value - end - local descriptor = descriptors_by_string[value] - if descriptor == nil then - fail("descriptor must be one of f, F, t, or T", 2) - end - return descriptor -end - -function Descriptor.try_from_string(value) - if Descriptor.is(value) then - return value - end - return descriptors_by_string[value] -end - -function Descriptor.from_parts(family, direction) - family = M.Family.from_string(family) - direction = M.Direction.from_string(direction) - return descriptors_by_parts[descriptor_key(family, direction)] -end - -function Descriptor.to_string(value) - return Descriptor.from_string(value).value -end - -function Descriptor.is_uppercase(value) - return Descriptor.from_string(value).uppercase -end - -function Descriptor.is_lowercase(value) - return not Descriptor.is_uppercase(value) -end - -function Descriptor.swap(value) - local descriptor = Descriptor.from_string(value) - local direction = descriptor.direction == M.Direction.FORWARD - and M.Direction.BACKWARD - or M.Direction.FORWARD - return Descriptor.from_parts(descriptor.family, direction) -end - -function Descriptor.lowercase(value) - local descriptor = Descriptor.from_string(value) - return Descriptor.from_parts(descriptor.family, M.Direction.FORWARD) -end - -function Descriptor.uppercase(value) - local descriptor = Descriptor.from_string(value) - return Descriptor.from_parts(descriptor.family, M.Direction.BACKWARD) -end - -local Count = {} -M.Count = Count - -register_type("Count", Count, function(data) - return tostring(data.value) -end, function(left, right) - return left.value == right.value -end) - -local count_one - -function Count.new(value) - if Count.is(value) then - return value - end - if value == nil then - value = 1 - end - if not is_integer(value) or value < 1 then - fail("count must be a positive integer", 2) - end - if value == 1 and count_one ~= nil then - return count_one - end - local count = new_record("Count", { value = value }) - if value == 1 then - count_one = count - end - return count -end - -function Count.is(value) - return is_record(value, "Count") -end - -function Count.to_number(value) - return Count.new(value).value -end - -Count.ONE = Count.new(1) - -local ModeContext = {} -M.ModeContext = ModeContext - -register_type("ModeContext", ModeContext, function(data) - return data.key -end) - -local mode_contexts = {} -local CTRL_V = string.char(0x16) -local CTRL_S = string.char(0x13) - -local operator_modes = { - no = true, - nov = true, - noV = true, - ["no" .. CTRL_V] = true, -} - -local function mode_traits(full_mode) - local operator = operator_modes[full_mode] == true - local visual_kind - local select_kind - - if not operator then - local lead = full_mode:sub(1, 1) - if lead == "v" then - visual_kind = M.SelectionKind.CHARACTER - elseif lead == "V" then - visual_kind = M.SelectionKind.LINE - elseif lead == CTRL_V then - visual_kind = M.SelectionKind.BLOCK - elseif lead == "s" then - select_kind = M.SelectionKind.CHARACTER - elseif lead == "S" then - select_kind = M.SelectionKind.LINE - elseif lead == CTRL_S then - select_kind = M.SelectionKind.BLOCK - end - end - - return operator, visual_kind, select_kind -end - -function ModeContext.from_full_mode(full_mode) - if ModeContext.is(full_mode) then - return full_mode - end - require_string(full_mode, "full_mode", false) - - local operator, visual_kind, select_kind = mode_traits(full_mode) - local key = operator and "no" or full_mode - local context = mode_contexts[key] - if context ~= nil then - return context - end - - context = new_record("ModeContext", { - key = key, - full_mode = key, - operator = operator, - visual_kind = visual_kind, - select_kind = select_kind, - visual = visual_kind ~= nil, - select = select_kind ~= nil, - command_path = visual_kind == nil, - }) - mode_contexts[key] = context - return context -end - -function ModeContext.is(value) - return is_record(value, "ModeContext") -end - -function ModeContext.equal(left, right) - require_record(left, "ModeContext", "left") - require_record(right, "ModeContext", "right") - return left.key == right.key -end - -function ModeContext:to_key() - return self.key -end - -local Selection = {} -M.Selection = Selection - -register_type("Selection", Selection, function(data) - return data.active and ("selection:" .. data.kind.value) or "selection:none" -end, function(left, right) - return left.active == right.active - and left.kind == right.kind - and left.anchor == right.anchor - and left.focus == right.focus - and left.option == right.option -end) - -function Selection.new(options) - if Selection.is(options) then - return options - end - if type(options) ~= "table" then - fail("selection options must be a table", 2) - end - - local active = require_boolean(options.active, "selection.active") - local kind = M.SelectionKind.from_string(options.kind) - local option = M.SelectionOption.from_string(options.option or "inclusive") - local anchor = options.anchor - local focus = options.focus - - if active then - if kind == M.SelectionKind.NONE then - fail("an active selection must have a selection kind", 2) - end - anchor = Position.coerce(anchor) - focus = Position.coerce(focus) - else - if kind ~= M.SelectionKind.NONE then - fail("an inactive selection must use the none kind", 2) - end - if anchor ~= nil or focus ~= nil then - fail("an inactive selection must have empty endpoints", 2) - end - end - - return new_record("Selection", { - active = active, - kind = kind, - anchor = anchor, - focus = focus, - option = option, - }) -end - -function Selection.inactive(option) - return Selection.new({ - active = false, - kind = M.SelectionKind.NONE, - option = option or M.SelectionOption.INCLUSIVE, - }) -end - -function Selection.active(kind, anchor, focus, option) - return Selection.new({ - active = true, - kind = kind, - anchor = anchor, - focus = focus, - option = option or M.SelectionOption.INCLUSIVE, - }) -end - -function Selection.is(value) - return is_record(value, "Selection") -end - -function Selection:with_focus(focus, kind) - if not self.active then - fail("selection must be active", 2) - end - return Selection.active(kind or self.kind, self.anchor, focus, self.option) -end - -function Selection:to_table() - return { - active = self.active, - kind = self.kind.value, - anchor = self.anchor and self.anchor:to_table() or nil, - focus = self.focus and self.focus:to_table() or nil, - option = self.option.value, - } -end - -local TextSnapshot = {} -M.TextSnapshot = TextSnapshot -local text_lines = setmetatable({}, { __mode = "k" }) - -register_type("TextSnapshot", TextSnapshot, function(data) - return "text:" .. tostring(data.line_count) .. " lines" -end, function(left, right) - if left.line_count ~= right.line_count then - return false - end - local left_lines = text_lines[left.identity] - local right_lines = text_lines[right.identity] - for index = 1, left.line_count do - if left_lines[index] ~= right_lines[index] then - return false - end - end - return true -end) - -function TextSnapshot.new(lines) - if TextSnapshot.is(lines) then - return lines - end - if type(lines) ~= "table" or #lines < 1 then - fail("text lines must be a nonempty list", 2) - end - local copy = {} - for index = 1, #lines do - if type(lines[index]) ~= "string" then - fail("each text line must be a string", 2) - end - copy[index] = lines[index] - end - local identity = {} - text_lines[identity] = copy - return new_record("TextSnapshot", { - identity = identity, - line_count = #copy, - }) -end - -function TextSnapshot.is(value) - return is_record(value, "TextSnapshot") -end - -function TextSnapshot:line(line_number) - if not is_integer(line_number) or line_number < 1 or line_number > self.line_count then - fail("line_number must identify a line in the text snapshot", 2) - end - return text_lines[self.identity][line_number] -end - -function TextSnapshot:lines() - local result = {} - local source = text_lines[self.identity] - for index = 1, self.line_count do - result[index] = source[index] - end - return result -end - -function TextSnapshot:to_table() - return { lines = self:lines() } -end - -local MacroState = {} -M.MacroState = MacroState - -register_type("MacroState", MacroState, function(data) - return data.executing and ("macro:" .. data.register) or "macro:inactive" -end, function(left, right) - return left.register == right.register -end) - -function MacroState.new(register) - if MacroState.is(register) then - return register - end - if register == "" then - register = nil - end - if register ~= nil then - require_string(register, "macro register", false) - end - return new_record("MacroState", { - register = register, - executing = register ~= nil, - }) -end - -function MacroState.is(value) - return is_record(value, "MacroState") -end - -local FoldState = {} -M.FoldState = FoldState -local fold_policies = setmetatable({}, { __mode = "k" }) - -register_type("FoldState", FoldState, function(data) - return "folds:" .. tostring(data.closed_levels) -end) - -function FoldState.new(open_policy, closed_levels) - if FoldState.is(open_policy) and closed_levels == nil then - return open_policy - end - if type(open_policy) ~= "table" then - fail("fold open policy must be a list", 2) - end - require_nonnegative_integer(closed_levels, "closed fold levels") - - local identity = {} - local policies = {} - local seen = {} - for index = 1, #open_policy do - local policy = require_string(open_policy[index], "fold policy item", false) - if not seen[policy] then - seen[policy] = true - policies[#policies + 1] = policy - end - end - fold_policies[identity] = { - list = policies, - set = seen, - } - return new_record("FoldState", { - identity = identity, - closed_levels = closed_levels, - }) -end - -function FoldState.is(value) - return is_record(value, "FoldState") -end - -function FoldState:opens(policy) - require_string(policy, "fold policy", false) - return fold_policies[self.identity].set[policy] == true -end - -function FoldState:policies() - local result = {} - local source = fold_policies[self.identity].list - for index = 1, #source do - result[index] = source[index] - end - return result -end - -local InputPacket = {} -M.InputPacket = InputPacket -local packet_bytes = setmetatable({}, { __mode = "k" }) - -M.InputPacketKind = define_enum("InputPacketKind", { - TEXT = "text", - RAW_BYTES = "raw_bytes", - SPECIAL_KEY = "special_key", - ERROR = "error", -}) - -register_type("InputPacket", InputPacket, function(data) - return "input:" .. data.kind.value -end) - -function InputPacket.text(text) - require_string(text, "input text", false) - return new_record("InputPacket", { - kind = M.InputPacketKind.TEXT, - text = text, - }) -end - -local function validated_packet_bytes(bytes, kind) - if type(bytes) ~= "table" or #bytes < 1 then - fail(kind .. " bytes must be a nonempty list", 3) - end - local copy = {} - for index = 1, #bytes do - local byte = bytes[index] - if not is_integer(byte) or byte < 0 or byte > 255 then - fail(kind .. " bytes must contain byte values", 3) - end - copy[index] = byte - end - return copy -end - -local function bytes_from_string(value) - local bytes = {} - for index = 1, #value do - bytes[index] = string.byte(value, index) - end - return bytes -end - -local function string_from_bytes(bytes) - local characters = {} - for index = 1, #bytes do - characters[index] = string.char(bytes[index]) - end - return table.concat(characters) -end - -function InputPacket.raw_bytes(bytes) - local identity = {} - packet_bytes[identity] = validated_packet_bytes(bytes, "raw input") - return new_record("InputPacket", { - kind = M.InputPacketKind.RAW_BYTES, - identity = identity, - }) -end - -function InputPacket.special_key(name, encoded) - require_string(name, "special key name", false) - local bytes - if type(encoded) == "table" then - bytes = validated_packet_bytes(encoded, "special key") - encoded = string_from_bytes(bytes) - elseif encoded ~= nil then - require_string(encoded, "encoded special key", false) - bytes = bytes_from_string(encoded) - end - local identity - if bytes ~= nil then - identity = {} - packet_bytes[identity] = bytes - end - return new_record("InputPacket", { - kind = M.InputPacketKind.SPECIAL_KEY, - name = name, - encoded = encoded, - identity = identity, - }) -end - -function InputPacket.error(message) - require_string(message, "input error message", false) - return new_record("InputPacket", { - kind = M.InputPacketKind.ERROR, - message = message, - }) -end - -function InputPacket.from_table(packet) - if InputPacket.is(packet) then - return packet - end - if type(packet) ~= "table" then - fail("input packet must be an InputPacket or packet table", 2) - end - local kind = M.InputPacketKind.from_string(packet.kind) - if kind == M.InputPacketKind.TEXT then - return InputPacket.text(packet.text) - end - if kind == M.InputPacketKind.RAW_BYTES then - return InputPacket.raw_bytes(packet.bytes) - end - if kind == M.InputPacketKind.SPECIAL_KEY then - return InputPacket.special_key(packet.name, packet.bytes or packet.encoded) - end - return InputPacket.error(packet.message) -end - -function InputPacket.is(value) - return is_record(value, "InputPacket") -end - -function InputPacket:bytes() - if self.kind ~= M.InputPacketKind.RAW_BYTES - and self.kind ~= M.InputPacketKind.SPECIAL_KEY - then - return nil - end - local source = self.identity and packet_bytes[self.identity] or nil - if source == nil then - return nil - end - local result = {} - for index = 1, #source do - result[index] = source[index] - end - return result -end - -function InputPacket:to_table() - local result = { kind = self.kind.value } - if self.kind == M.InputPacketKind.TEXT then - result.text = self.text - elseif self.kind == M.InputPacketKind.RAW_BYTES then - result.bytes = self:bytes() - elseif self.kind == M.InputPacketKind.SPECIAL_KEY then - result.name = self.name - result.bytes = self:bytes() - else - result.message = self.message - end - return result -end - -local TargetValue = {} -M.TargetValue = TargetValue - -register_type("TargetValue", TargetValue, function(data) - return "target:" .. data.kind.value .. ":" .. tostring(data.first_code) -end, function(left, right) - return left.kind == right.kind - and left.value == right.value - and left.first_code == right.first_code -end) - -function TargetValue.character(value, first_code) - require_string(value, "target character", false) - require_nonnegative_integer(first_code, "target first code") - return new_record("TargetValue", { - kind = M.TargetKind.CHARACTER, - value = value, - first_code = first_code, - }) -end - -function TargetValue.special_key(value, first_code) - require_string(value, "encoded special key", false) - first_code = first_code or string.byte(value, 1) - require_nonnegative_integer(first_code, "target first code") - if first_code ~= 0x80 then - fail("an encoded special key must start with hexadecimal 80", 2) - end - return new_record("TargetValue", { - kind = M.TargetKind.SPECIAL_KEY, - value = value, - first_code = first_code, - }) -end - -function TargetValue.code_fallback(first_code) - first_code = first_code or 0 - require_nonnegative_integer(first_code, "fallback character code") - return new_record("TargetValue", { - kind = M.TargetKind.CODE_FALLBACK, - value = "", - first_code = first_code, - }) -end - -function TargetValue.from_table(target) - if TargetValue.is(target) then - return target - end - if type(target) ~= "table" then - fail("target must be a TargetValue or target table", 2) - end - local kind = M.TargetKind.from_string(target.kind) - if kind == M.TargetKind.CHARACTER then - return TargetValue.character(target.value, target.first_code) - end - if kind == M.TargetKind.SPECIAL_KEY then - return TargetValue.special_key(target.value, target.first_code) - end - return TargetValue.code_fallback(target.first_code) -end - -function TargetValue.is(value) - return is_record(value, "TargetValue") -end - -function TargetValue:to_table() - return { - kind = self.kind.value, - value = self.value, - first_code = self.first_code, - } -end - -local TargetPlan = {} -M.TargetPlan = TargetPlan - -register_type("TargetPlan", TargetPlan, function(data) - return "target-plan:" .. data.kind.value -end) - -function TargetPlan.new(options) - if TargetPlan.is(options) then - return options - end - if type(options) ~= "table" then - fail("target plan options must be a table", 2) - end - local target = require_record(options.target, "TargetValue", "target plan target") - local kind = M.TargetPlanKind.from_string(options.kind) - local case_mode = M.CaseMode.from_string(options.case_mode) - if type(options.matcher) ~= "function" then - fail("target plan matcher must be a function", 2) - end - return new_record("TargetPlan", { - target = target, - kind = kind, - case_mode = case_mode, - matcher = options.matcher, - }) -end - -function TargetPlan.is(value) - return is_record(value, "TargetPlan") -end - -function TargetPlan:matches(...) - return self.matcher(...) -end - -function TargetPlan:matches_at(text_view, position) - if type(text_view) ~= "table" or type(text_view.character_at) ~= "function" then - fail("target plan match requires a text view", 2) - end - position = Position.coerce(position) - return self.matcher(text_view:character_at(position), position, text_view) -end - -function TargetPlan:to_table() - return { - target = self.target:to_table(), - kind = self.kind.value, - case_mode = self.case_mode.value, - } -end - -local ResolvedMotionPlan = {} -M.ResolvedMotionPlan = ResolvedMotionPlan - -register_type("ResolvedMotionPlan", ResolvedMotionPlan, function(data) - return "motion-plan:" .. data.descriptor.value -end) - -function ResolvedMotionPlan.new(options) - if ResolvedMotionPlan.is(options) then - return options - end - if type(options) ~= "table" then - fail("resolved motion plan options must be a table", 2) - end - return new_record("ResolvedMotionPlan", { - target_plan = require_record(options.target_plan, "TargetPlan", "target plan"), - descriptor = Descriptor.from_string(options.descriptor), - search_scope = M.SearchScope.from_string(options.search_scope), - endpoint_policy = M.EndpointPolicy.from_string(options.endpoint_policy), - }) -end - -function ResolvedMotionPlan.is(value) - return is_record(value, "ResolvedMotionPlan") -end - -function ResolvedMotionPlan:to_table() - return { - target_plan = self.target_plan:to_table(), - descriptor = self.descriptor.value, - search_scope = self.search_scope.value, - endpoint_policy = self.endpoint_policy.value, - } -end - -local MotionRequest = {} -M.MotionRequest = MotionRequest - -register_type("MotionRequest", MotionRequest, function(data) - return "motion-request:" .. data.descriptor.value -end) - -function MotionRequest.new(options) - if MotionRequest.is(options) then - return options - end - if type(options) ~= "table" then - fail("motion request options must be a table", 2) - end - if options.policy == nil then - fail("motion request policy is required", 2) - end - return new_record("MotionRequest", { - context = require_record(options.context, "ModeContext", "motion context"), - origin = Position.coerce(options.origin), - descriptor = Descriptor.from_string(options.descriptor), - target = require_record(options.target, "TargetValue", "motion target"), - count = Count.new(options.count), - policy = options.policy, - first_move = require_boolean(options.first_move, "first_move"), - }) -end - -function MotionRequest.is(value) - return is_record(value, "MotionRequest") -end - -local SearchOutcome = {} -M.SearchOutcome = SearchOutcome - -register_type("SearchOutcome", SearchOutcome, function(data) - return "search:" .. data.status.value -end) - -local function new_search_outcome(status, endpoint, successful_steps) - status = M.SearchStatus.from_string(status) - endpoint = Position.coerce(endpoint) - require_nonnegative_integer(successful_steps, "successful_steps") - - if status == M.SearchStatus.COMPLETE and successful_steps < 1 then - fail("a complete search must contain a successful step", 3) - end - if status == M.SearchStatus.BOUNDARY_AFTER_PARTIAL and successful_steps < 1 then - fail("a partial search must contain a successful step", 3) - end - if status == M.SearchStatus.BOUNDARY_BEFORE_ANY and successful_steps ~= 0 then - fail("a boundary-before-any search must contain zero successful steps", 3) - end - - return new_record("SearchOutcome", { - status = status, - endpoint = endpoint, - successful_steps = successful_steps, - complete = status == M.SearchStatus.COMPLETE, - }) -end - -function SearchOutcome.new(options) - if SearchOutcome.is(options) then - return options - end - if type(options) ~= "table" then - fail("search outcome options must be a table", 2) - end - return new_search_outcome(options.status, options.endpoint, options.successful_steps) -end - -function SearchOutcome.complete(endpoint, successful_steps) - return new_search_outcome(M.SearchStatus.COMPLETE, endpoint, successful_steps) -end - -function SearchOutcome.boundary_after_partial(endpoint, successful_steps) - return new_search_outcome( - M.SearchStatus.BOUNDARY_AFTER_PARTIAL, - endpoint, - successful_steps - ) -end - -function SearchOutcome.boundary_before_any(origin) - return new_search_outcome(M.SearchStatus.BOUNDARY_BEFORE_ANY, origin, 0) -end - -function SearchOutcome.is(value) - return is_record(value, "SearchOutcome") -end - -function SearchOutcome:to_table() - return { - status = self.status.value, - endpoint = self.endpoint:to_table(), - successful_steps = self.successful_steps, - complete = self.complete, - } -end - -local DotPayload = {} -M.DotPayload = DotPayload - -register_type("DotPayload", DotPayload, function(data) - return "dot:" .. data.descriptor.value -end, function(left, right) - return left.descriptor == right.descriptor and left.target == right.target -end) - -function DotPayload.new(descriptor, target) - if DotPayload.is(descriptor) and target == nil then - return descriptor - end - return new_record("DotPayload", { - descriptor = Descriptor.from_string(descriptor), - target = require_record(target, "TargetValue", "dot target"), - }) -end - -function DotPayload.is(value) - return is_record(value, "DotPayload") -end - -function DotPayload:to_table() - return { - descriptor = self.descriptor.value, - target = self.target:to_table(), - } -end - -local ExplicitRepeatRequest = {} -M.ExplicitRepeatRequest = ExplicitRepeatRequest - -register_type("ExplicitRepeatRequest", ExplicitRepeatRequest, function(data) - if data.neutral then - return "explicit-repeat:neutral" - end - return "explicit-repeat:" .. data.descriptor.value -end, function(left, right) - return left.neutral == right.neutral - and left.descriptor == right.descriptor - and left.target == right.target -end) - -local neutral_explicit_repeat_request - -function ExplicitRepeatRequest.new(descriptor, target) - if ExplicitRepeatRequest.is(descriptor) and target == nil then - return descriptor - end - descriptor = Descriptor.from_string(descriptor) - target = require_record(target, "TargetValue", "explicit repeat target") - return new_record("ExplicitRepeatRequest", { - descriptor = descriptor, - effective_descriptor = descriptor, - target = target, - neutral = false, - }) -end - -function ExplicitRepeatRequest.neutral() - if neutral_explicit_repeat_request == nil then - neutral_explicit_repeat_request = new_record("ExplicitRepeatRequest", { - descriptor = nil, - effective_descriptor = nil, - target = nil, - neutral = true, - }) - end - return neutral_explicit_repeat_request -end - -function ExplicitRepeatRequest.is(value) - return is_record(value, "ExplicitRepeatRequest") -end - -function ExplicitRepeatRequest:is_neutral() - return self.neutral -end - -function ExplicitRepeatRequest:to_table() - if self.neutral then - return { neutral = true } - end - return { - descriptor = self.descriptor.value, - target = self.target:to_table(), - neutral = false, - } -end - -local ActionOutcome = {} -M.ActionOutcome = ActionOutcome - -register_type("ActionOutcome", ActionOutcome, function(data) - return "action:" .. data.kind.value -end) - -local function new_action_outcome(options) - local kind = M.ActionKind.from_string(options.kind) - local position = Position.coerce(options.position) - local search_outcome = options.search_outcome - local descriptor = options.effective_descriptor - local dot_payload = options.dot_payload - - if search_outcome ~= nil then - require_record(search_outcome, "SearchOutcome", "search outcome") - end - if descriptor ~= nil then - descriptor = Descriptor.from_string(descriptor) - end - if dot_payload ~= nil then - require_record(dot_payload, "DotPayload", "dot payload") - end - if options.diagnostic ~= nil then - require_string(options.diagnostic, "diagnostic", false) - end - - if kind == M.ActionKind.MOVEMENT then - if search_outcome == nil or not search_outcome.complete then - fail("a movement action requires a complete search outcome", 3) - end - elseif kind == M.ActionKind.FAILED_SEARCH then - if search_outcome == nil or search_outcome.complete then - fail("a failed-search action requires an incomplete search outcome", 3) - end - elseif search_outcome ~= nil then - fail("only movement and failed-search actions can contain a search outcome", 3) - end - - if kind == M.ActionKind.ERROR and options.diagnostic == nil then - fail("an error action requires a diagnostic", 3) - end - - local complete - if search_outcome ~= nil then - complete = search_outcome.complete - end - - return new_record("ActionOutcome", { - kind = kind, - position = position, - search_outcome = search_outcome, - complete = complete, - successful_steps = search_outcome and search_outcome.successful_steps or 0, - effective_descriptor = descriptor, - dot_payload = dot_payload, - diagnostic = options.diagnostic, - }) -end - -function ActionOutcome.new(options) - if ActionOutcome.is(options) then - return options - end - if type(options) ~= "table" then - fail("action outcome options must be a table", 2) - end - return new_action_outcome(options) -end - -function ActionOutcome.from_search(search_outcome, descriptor, dot_payload) - require_record(search_outcome, "SearchOutcome", "search outcome") - return new_action_outcome({ - kind = search_outcome.complete and M.ActionKind.MOVEMENT or M.ActionKind.FAILED_SEARCH, - position = search_outcome.endpoint, - search_outcome = search_outcome, - effective_descriptor = descriptor, - dot_payload = dot_payload, - }) -end - -local function simple_action(kind, position, diagnostic) - return new_action_outcome({ - kind = kind, - position = position, - diagnostic = diagnostic, - }) -end - -function ActionOutcome.neutral(position) - return simple_action(M.ActionKind.NEUTRAL, position) -end - -function ActionOutcome.escape(position) - return simple_action(M.ActionKind.ESCAPE, position) -end - -function ActionOutcome.empty(position) - return simple_action(M.ActionKind.EMPTY, position) -end - -function ActionOutcome.error(position, diagnostic) - return simple_action(M.ActionKind.ERROR, position, diagnostic) -end - -function ActionOutcome.is(value) - return is_record(value, "ActionOutcome") -end - -function ActionOutcome:to_table() - return { - kind = self.kind.value, - position = self.position:to_table(), - complete = self.complete, - successful_steps = self.successful_steps, - effective_descriptor = self.effective_descriptor and self.effective_descriptor.value or nil, - dot_payload = self.dot_payload and self.dot_payload:to_table() or nil, - diagnostic = self.diagnostic, - } -end - -return M diff --git a/lua/clever_f/feedback_service.lua b/lua/clever_f/feedback_service.lua deleted file mode 100644 index ca26746..0000000 --- a/lua/clever_f/feedback_service.lua +++ /dev/null @@ -1,1003 +0,0 @@ -local domain = require("clever_f.domain") -local policy = require("clever_f.policy") -local sequence_state = require("clever_f.sequence_state") -local state_transitions = require("clever_f.state_transitions") -local text_topology = require("clever_f.text_topology") - -local M = {} -local FeedbackService = {} -FeedbackService.__index = FeedbackService -M.FeedbackService = FeedbackService -local CursorPresentationLease = {} -M.CursorPresentationLease = CursorPresentationLease - -M.DEFAULT_LABEL_GROUP = "CleverFDefaultLabel" -M.Priority = { - HIGH = "high", - ORDINARY = "ordinary", -} -M.FINALIZER_EVENTS = { - "CursorMoved", - "InsertEnter", - "TextChanged", -} -M.EAGER_EVENTS = { - "WinEnter", - "WinLeave", - "CmdwinLeave", -} -M.FinalizerAction = { - PRESERVE = "preserve", - FINALIZE = "finalize", -} -M.MigrationReason = { - LINE_CHANGE = "line_change", - TILL_DIRECTION_CHANGE = "till_direction_change", -} - -local OVERLAY_PRIORITIES = { - CleverFCursor = M.Priority.HIGH, - CleverFChar = M.Priority.HIGH, - CleverFDirect = M.Priority.ORDINARY, -} - -local service_records = setmetatable({}, { __mode = "k" }) -local cursor_lease_records = setmetatable({}, { __mode = "k" }) -local temporary_release_records = setmetatable({}, { __mode = "k" }) - -local FEATURE_GROUPS = { - "CleverFCursor", - "CleverFChar", - "CleverFDirect", -} - -local LEGACY_NORMAL_EX_CONTEXTS = { - cv = true, - cvr = true, -} - -local DIRECT_FINALIZER_EVENTS = { - InsertEnter = true, - TextChanged = true, -} - -local EAGER_EVENT_SET = {} -for _, event_name in ipairs(M.EAGER_EVENTS) do - EAGER_EVENT_SET[event_name] = true -end - -local DEFAULT_LABEL_DEFINITION = { - guifg = "red", - guibg = "NONE", - gui = { - bold = true, - underline = true, - }, - ctermfg = "red", - ctermbg = "NONE", - cterm = { - bold = true, - underline = true, - }, -} - -local function copy(value) - if type(value) ~= "table" then - return value - end - local result = {} - for key, item in pairs(value) do - result[key] = copy(item) - end - return result -end - -function M.default_label_definition() - return copy(DEFAULT_LABEL_DEFINITION) -end - -function M.overlay_priority(group) - local priority = OVERLAY_PRIORITIES[group] - if priority == nil then - error("unknown feedback overlay group '" .. tostring(group) .. "'", 2) - end - return priority -end - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function normalize_options(options) - if type(options) ~= "table" then - fail("FeedbackService options must be a table", 3) - end - if options.host == nil then - return { host = options } - end - return options -end - -local function require_host(host) - if type(host) ~= "table" - or type(host.read_highlight_group) ~= "function" - or type(host.define_highlight_group) ~= "function" - or type(host.create_highlight) ~= "function" - or type(host.remove_highlight) ~= "function" - or type(host.read_buffer) ~= "function" - or type(host.read_cursor) ~= "function" - or type(host.read_window) ~= "function" - or type(host.register_events) ~= "function" - or type(host.remove_event_registration) ~= "function" - or type(host.start_timer) ~= "function" - or type(host.stop_timer) ~= "function" - or type(host.supports_timers) ~= "function" - or type(host.supports_cursor_presentation) ~= "function" - or type(host.suppress_cursor_presentation) ~= "function" - or type(host.restore_cursor_presentation) ~= "function" - then - fail("FeedbackService host must provide highlight groups", 3) - end - return host -end - -local function require_transitions(transitions, state) - transitions = transitions or state_transitions.new(state) - if type(transitions) ~= "table" - or type(transitions.AddTemporaryOverlay) ~= "function" - or type(transitions.RemoveTemporaryOverlay) ~= "function" - or type(transitions.AddTargetOverlay) ~= "function" - or type(transitions.ClearTargetOverlays) ~= "function" - or type(transitions.AddFinalizer) ~= "function" - or type(transitions.RemoveFinalizer) ~= "function" - or type(transitions.FullFinalization) ~= "function" - or type(transitions.ClearTargetFeedback) ~= "function" - or type(transitions.SetHighlightTimer) ~= "function" - or type(transitions.ClearHighlightTimer) ~= "function" - then - fail("FeedbackService transitions must manage overlay resources", 3) - end - return transitions -end - -local function require_policy(service, host) - service = service or policy.new(host) - if type(service) ~= "table" - or type(service.evaluate_highlight_links) ~= "function" - or type(service.sample_acquisition) ~= "function" - or type(service.sample_markers) ~= "function" - or type(service.sample_timeouts) ~= "function" - or type(service.capture_activation) ~= "function" - then - fail("FeedbackService policy must evaluate highlight links", 3) - end - return service -end - -function FeedbackService.new(options) - if FeedbackService.is(options) then - return options - end - options = normalize_options(options) - local service = setmetatable({}, FeedbackService) - local host = require_host(options.host) - local state = options.state or sequence_state.get() - if not sequence_state.is(state) then - fail("FeedbackService state must be the plugin-global SequenceState", 2) - end - service_records[service] = { - host = host, - policy = require_policy(options.policy or options.policy_service, host), - state = state, - transitions = require_transitions( - options.transitions or options.state_transitions, - state - ), - persistent_requests = {}, - owned_finalizer = nil, - activation = nil, - eager_registration = nil, - last_eager_decision = nil, - } - return service -end - -function FeedbackService.is(value) - return type(value) == "table" and service_records[value] ~= nil -end - -local cursor_lease_metatable = { - __index = function(lease, key) - local method = CursorPresentationLease[key] - if method ~= nil then - return method - end - local record = cursor_lease_records[lease] - if key == "identity" then - return record.identity - end - if key == "active" then - return record.active - end - return nil - end, - __newindex = function() - fail("cursor presentation leases are read-only", 2) - end, - __metatable = "clever_f.feedback_service.CursorPresentationLease", -} - -local function new_cursor_presentation_lease(host, suppress) - local lease = setmetatable({}, cursor_lease_metatable) - local identity = suppress and host:suppress_cursor_presentation() or nil - cursor_lease_records[lease] = { - host = host, - identity = identity, - active = identity ~= nil, - } - return lease -end - -function CursorPresentationLease.is(value) - return type(value) == "table" and cursor_lease_records[value] ~= nil -end - -function CursorPresentationLease:release() - local record = cursor_lease_records[self] - if record == nil then - fail("cursor presentation lease is invalid", 2) - end - if not record.active then - return false - end - record.active = false - record.host:restore_cursor_presentation(record.identity) - return true -end - -function FeedbackService:create_cursor_presentation_lease(enabled) - local record = service_records[self] - if enabled == nil then - enabled = record.policy:sample_acquisition().hide_cursor_on_cmdline - elseif type(enabled) ~= "boolean" then - fail("cursor presentation policy must be a Boolean", 2) - end - local supported = enabled and record.host:supports_cursor_presentation() - return new_cursor_presentation_lease(record.host, supported == true) -end - -local function position_list(positions) - if type(positions) ~= "table" then - fail("direct marker positions must be a list", 3) - end - local result = {} - local item_count = 0 - for key, position in pairs(positions) do - if type(key) ~= "number" - or key ~= math.floor(key) - or key < 1 - or key > #positions - then - fail("direct marker positions must be a list", 3) - end - result[key] = domain.Position.coerce(position) - item_count = item_count + 1 - end - if item_count ~= #positions then - fail("direct marker positions must be a list", 3) - end - return result -end - -function FeedbackService:create_direct_markers(positions, window) - positions = position_list(positions) - if #positions == 0 then - return nil - end - if window == nil then - fail("direct marker window must identify its host window", 2) - end - - local record = service_records[self] - local identity = record.host:create_highlight({ - group = "CleverFDirect", - window = window, - positions = positions, - priority = M.overlay_priority("CleverFDirect"), - }) - record.transitions:AddTemporaryOverlay(identity, window, "CleverFDirect") - local resource = { - identity = identity, - window = window, - group = "CleverFDirect", - positions = positions, - } - temporary_release_records[resource] = false - return resource -end - -function FeedbackService:create_cursor_marker(position, window) - position = domain.Position.coerce(position) - if window == nil then - fail("cursor marker window must identify its host window", 2) - end - - local record = service_records[self] - local identity = record.host:create_highlight({ - group = "CleverFCursor", - window = window, - position = position, - priority = M.overlay_priority("CleverFCursor"), - }) - record.transitions:AddTemporaryOverlay(identity, window, "CleverFCursor") - local resource = { - identity = identity, - window = window, - group = "CleverFCursor", - position = position, - } - temporary_release_records[resource] = false - return resource -end - -function FeedbackService:remove_temporary_overlay(resource) - if type(resource) ~= "table" or resource.identity == nil then - fail("temporary overlay resource must identify its highlight", 2) - end - if temporary_release_records[resource] == true then - return false - end - temporary_release_records[resource] = true - local record = service_records[self] - local ok, removed = pcall( - record.host.remove_highlight, - record.host, - resource.identity - ) - record.transitions:RemoveTemporaryOverlay(resource.identity, resource.window) - if not ok then - error(removed, 0) - end - return removed -end - -function M.persistent_context_eligible(context) - context = domain.ModeContext.from_full_mode(context) - return context.key == "n" - or context.visual_kind ~= nil - or context.select_kind ~= nil - or LEGACY_NORMAL_EX_CONTEXTS[context.key] == true -end - -function M.persistent_destination( - view, - target_position, - descriptor, - endpoint_policy -) - if not text_topology.TextView.is(view) then - fail("persistent feedback requires a TextView", 2) - end - target_position = domain.Position.coerce(target_position) - descriptor = domain.Descriptor.from_string(descriptor) - endpoint_policy = domain.EndpointPolicy.from_string(endpoint_policy) - - if endpoint_policy == domain.EndpointPolicy.VISUAL_EXCLUSIVE - and descriptor.direction == domain.Direction.FORWARD - then - if descriptor.family == domain.Family.FIND then - return view:successor(target_position) - end - return target_position - end - if descriptor.family == domain.Family.FIND then - return target_position - end - if descriptor.direction == domain.Direction.FORWARD then - return view:predecessor(target_position) - end - return view:successor(target_position) -end - -function M.persistent_match_positions( - view, - match_start_line, - target_plan, - descriptor, - endpoint_policy -) - if not text_topology.TextView.is(view) then - fail("persistent feedback requires a TextView", 2) - end - if not domain.TargetPlan.is(target_plan) then - fail("persistent feedback requires a TargetPlan", 2) - end - local positions = {} - local seen = {} - local candidates = view:iter_line_forward(match_start_line) - while true do - local position, character = candidates() - if position == nil then - break - end - if target_plan:matches(character, position, view) then - local destination = M.persistent_destination( - view, - position, - descriptor, - endpoint_policy - ) - if destination ~= nil then - local key = tostring(destination.line) - .. ":" - .. tostring(destination.byte_column) - if not seen[key] then - seen[key] = true - positions[#positions + 1] = destination - end - end - end - end - return positions -end - -function FeedbackService:build_persistent(specification) - if type(specification) ~= "table" then - fail("persistent feedback request must be a table", 2) - end - local context = domain.ModeContext.from_full_mode(specification.context) - if not M.persistent_context_eligible(context) then - fail("persistent feedback request requires an eligible context", 2) - end - if not domain.TargetPlan.is(specification.target_plan) then - fail("persistent feedback request requires a TargetPlan", 2) - end - if not domain.ResolvedMotionPlan.is(specification.motion_plan) then - fail("persistent feedback request requires a ResolvedMotionPlan", 2) - end - if specification.motion_plan.target_plan ~= specification.target_plan then - fail("persistent feedback must reuse the movement TargetPlan", 2) - end - local descriptor = domain.Descriptor.from_string( - specification.descriptor or specification.motion_plan.descriptor - ) - local endpoint_policy = domain.EndpointPolicy.from_string( - specification.endpoint_policy or specification.motion_plan.endpoint_policy - ) - local anchor = domain.Position.coerce(specification.anchor) - local view = specification.text_view - or text_topology.from_host(service_records[self].host) - return { - context = context, - anchor = anchor, - target_plan = specification.target_plan, - motion_plan = specification.motion_plan, - descriptor = descriptor, - endpoint_policy = endpoint_policy, - anchor_line = anchor.line, - match_start_line = anchor.line, - positions = M.persistent_match_positions( - view, - anchor.line, - specification.target_plan, - descriptor, - endpoint_policy - ), - text_view = view, - window = specification.window, - } -end - -local function release_target_overlays(record, resources) - for _, resource in ipairs(resources) do - record.host:remove_highlight(resource.identity) - end -end - -local function remove_target_overlays(record, window) - local resources = record.transitions:ClearTargetOverlays(window) - release_target_overlays(record, resources) - return resources -end - -function FeedbackService:remove_character_overlays(window) - if window == nil then - fail("character overlay window must identify its host window", 2) - end - return remove_target_overlays(service_records[self], window) -end - -function FeedbackService:cursor_moved_decision() - local record = service_records[self] - local context = record.state.last_input_context - local expected = context and record.state:get_previous_landing(context) or nil - local actual = record.host:read_cursor() - return { - context = context, - expected = expected, - actual = actual, - equal = expected ~= nil and domain.Position.equal(actual, expected), - } -end - -local function release_finalizers(record, resources) - for _, resource in ipairs(resources) do - record.host:remove_event_registration(resource.identity) - end -end - -local function release_highlight_timer(record, identity) - if identity == nil then - return false - end - return record.host:stop_timer(identity) -end - -function FeedbackService:release_transition_cleanup(cleanup) - if type(cleanup) ~= "table" then - fail("feedback transition cleanup must be a table", 2) - end - local record = service_records[self] - release_finalizers(record, cleanup.finalizers or {}) - release_highlight_timer(record, cleanup.highlight_timer) - release_target_overlays(record, cleanup.target_overlays or {}) - if #(cleanup.finalizers or {}) > 0 then - record.owned_finalizer = nil - end - return cleanup -end - -function FeedbackService:full_finalize(window) - local record = service_records[self] - window = window or record.host:read_window() - local cleanup = record.transitions:FullFinalization(window) - self:release_transition_cleanup(cleanup) - record.owned_finalizer = nil - return cleanup -end - -function FeedbackService:handle_finalizer_event(name, payload) - if name == "CursorMoved" then - local decision = self:cursor_moved_decision() - if decision.equal then - decision.action = M.FinalizerAction.PRESERVE - else - decision.action = M.FinalizerAction.FINALIZE - decision.cleanup = self:full_finalize(payload and payload.window) - end - return decision - end - if DIRECT_FINALIZER_EVENTS[name] then - return { - action = M.FinalizerAction.FINALIZE, - cleanup = self:full_finalize(payload and payload.window), - } - end - return false -end - -local function register_finalizers(service, record) - local buffer = record.host:read_buffer() - local owned = record.owned_finalizer - if owned ~= nil and owned.buffer == buffer then - return owned - end - if owned ~= nil then - record.host:remove_event_registration(owned.identity) - record.transitions:RemoveFinalizer(owned.identity, owned.buffer) - end - - local identity = record.host:register_events( - M.FINALIZER_EVENTS, - function(name, payload) - service:handle_finalizer_event(name, payload) - end, - { buffer = buffer } - ) - record.transitions:AddFinalizer(identity, buffer) - owned = { - identity = identity, - buffer = buffer, - } - record.owned_finalizer = owned - return owned -end - -local function materialize_persistent(service, request) - if request.window == nil then - fail("persistent feedback window must identify its host window", 3) - end - local record = service_records[service] - remove_target_overlays(record, request.window) - request.identity = record.host:create_highlight({ - group = "CleverFChar", - window = request.window, - positions = request.positions, - priority = M.overlay_priority("CleverFChar"), - target_plan = request.target_plan, - descriptor = request.descriptor, - endpoint_policy = request.endpoint_policy, - anchor_line = request.anchor_line, - match_start_line = request.match_start_line, - }) - request.group = "CleverFChar" - request.priority = M.overlay_priority("CleverFChar") - local owned, ownership_error = pcall(function() - record.transitions:AddTargetOverlay( - request.identity, - request.window, - request.anchor.line - ) - request.finalizers = register_finalizers(service, record) - end) - if not owned then - local resources = record.transitions:RemoveTargetOverlay( - request.identity, - request.window - ) - if #resources == 0 then - resources[1] = { - identity = request.identity, - window = request.window, - } - end - pcall(release_target_overlays, record, resources) - error(ownership_error, 0) - end - local requests = record.persistent_requests - requests[#requests + 1] = request - return request -end - -function FeedbackService:request_persistent(specification) - return materialize_persistent(self, self:build_persistent(specification)) -end - -function FeedbackService:restore_primary(specification) - local restoration = self:build_primary_restoration(specification) - if restoration == nil then - return nil - end - return materialize_persistent(self, restoration) -end - -function FeedbackService:persistent_requests() - local result = {} - for index, request in ipairs(service_records[self].persistent_requests) do - result[index] = request - end - return result -end - -function M.repeated_till_migration_candidate(request) - if type(request) ~= "table" then - fail("command feedback migration request must be a table", 2) - end - local plan = request.resolved_motion_plan or request.plan - return domain.ResolvedMotionPlan.is(plan) - and plan.descriptor.family == domain.Family.TILL - and request.first_move == false -end - -function M.till_direction_changed(request) - if not M.repeated_till_migration_candidate(request) then - return false - end - if type(request.moved_forward) ~= "boolean" - or type(request.previous_moved_forward) ~= "boolean" - then - fail("TILL feedback migration requires movement directions", 2) - end - return request.moved_forward ~= request.previous_moved_forward -end - -function M.command_migration_reason(request) - if type(request) ~= "table" then - fail("command feedback migration request must be a table", 2) - end - local origin = domain.Position.coerce(request.origin) - local destination = domain.Position.coerce(request.destination) - if request.outcome ~= nil and request.outcome.complete ~= true then - return nil - end - if origin.line ~= destination.line then - return M.MigrationReason.LINE_CHANGE - end - if M.till_direction_changed(request) then - return M.MigrationReason.TILL_DIRECTION_CHANGE - end - return nil -end - -local function has_target_overlay(record, window) - for _, resource in ipairs(record.state.target_overlays) do - if resource.window == window then - return true - end - end - return false -end - -function FeedbackService:primary_restoration_active(context, window) - local record = service_records[self] - context = domain.ModeContext.from_full_mode(context) - local mark_char = record.policy:sample_markers().mark_char - if not mark_char or not M.persistent_context_eligible(context) then - return false - end - window = window or record.host:read_window() - return not has_target_overlay(record, window) -end - -function FeedbackService:build_primary_restoration(specification) - if type(specification) ~= "table" then - fail("primary feedback restoration must be a table", 2) - end - local window = specification.window - or service_records[self].host:read_window() - if not self:primary_restoration_active(specification.context, window) then - return nil - end - if not domain.TargetPlan.is(specification.target_plan) then - fail("primary feedback restoration requires an action TargetPlan", 2) - end - local action_motion_plan = specification.motion_plan - local search_scope = specification.search_scope - or (domain.ResolvedMotionPlan.is(action_motion_plan) - and action_motion_plan.search_scope) - or domain.SearchScope.BUFFER - local motion_plan = domain.ResolvedMotionPlan.new({ - target_plan = specification.target_plan, - descriptor = specification.stored_descriptor, - search_scope = search_scope, - endpoint_policy = specification.endpoint_policy, - }) - return self:build_persistent({ - context = specification.context, - anchor = specification.anchor, - target_plan = specification.target_plan, - motion_plan = motion_plan, - descriptor = specification.stored_descriptor, - endpoint_policy = specification.endpoint_policy, - text_view = specification.text_view, - window = window, - }) -end - -function FeedbackService:migrate_command(request) - local reason = M.command_migration_reason(request) - local record = service_records[self] - local window = request.window or record.host:read_window() - if reason == nil or not has_target_overlay(record, window) then - return { - migrated = false, - reason = reason, - } - end - - local plan = request.resolved_motion_plan or request.plan - if not domain.ResolvedMotionPlan.is(plan) then - fail("command feedback migration requires a ResolvedMotionPlan", 2) - end - local overlay = self:request_persistent({ - context = request.context, - anchor = request.destination, - target_plan = plan.target_plan, - motion_plan = plan, - descriptor = plan.descriptor, - endpoint_policy = plan.endpoint_policy, - window = window, - }) - return { - migrated = true, - reason = reason, - overlay = overlay, - } -end - -function FeedbackService:highlight_timer_delay() - local record = service_records[self] - if not record.policy:sample_markers().mark_char then - return nil - end - local delay = record.policy:sample_timeouts().highlight_timeout_ms - if delay == 0 or record.host:supports_timers() ~= true then - return nil - end - return delay -end - -function FeedbackService:cancel_highlight_timer() - local record = service_records[self] - local identity = record.transitions:ClearHighlightTimer() - release_highlight_timer(record, identity) - return identity -end - -function FeedbackService:handle_highlight_timer(callback_identity, window) - if callback_identity == nil then - fail("highlight timer callback identity must be present", 2) - end - local record = service_records[self] - if callback_identity ~= record.state.highlight_timer then - return false - end - local _, current = record.transitions:ClearHighlightTimer(callback_identity) - if not current then - return false - end - self:remove_character_overlays(window or record.host:read_window()) - return true -end - -function FeedbackService:start_highlight_timer(window) - local record = service_records[self] - local delay = self:highlight_timer_delay() - if delay == nil then - return nil - end - window = window or record.host:read_window() - self:cancel_highlight_timer() - local identity - identity = record.host:start_timer(delay, function(callback_identity) - self:handle_highlight_timer(callback_identity or identity, window) - end) - record.transitions:SetHighlightTimer(identity) - return identity -end - -function FeedbackService:refresh_primary(resolved_target, window) - if resolved_target == nil then - return nil - end - if not domain.TargetValue.is(resolved_target) then - fail("primary timer refresh requires a resolved TargetValue", 2) - end - return self:start_highlight_timer(window) -end - -function FeedbackService:handle_eager_event(name, payload) - if not EAGER_EVENT_SET[name] then - return false - end - local record = service_records[self] - local decision = { - event = name, - payload = payload, - mark_char = record.policy:sample_markers().mark_char, - cleaned = false, - } - if decision.mark_char then - local window = payload and payload.window or record.host:read_window() - local cleanup = record.transitions:ClearTargetFeedback(window) - release_highlight_timer(record, cleanup.highlight_timer) - release_target_overlays(record, cleanup.target_overlays) - decision.cleaned = true - decision.cleanup = cleanup - end - record.last_eager_decision = decision - return decision -end - -function FeedbackService:last_eager_decision() - local decision = service_records[self].last_eager_decision - return decision and copy(decision) or nil -end - -function FeedbackService:activate() - local record = service_records[self] - if record.activation ~= nil then - return copy(record.activation) - end - local sampled = record.policy:capture_activation() - local activation = { - clean_labels_eagerly = sampled.clean_labels_eagerly, - eager_registration = nil, - } - if sampled.clean_labels_eagerly then - local identity = record.host:register_events( - M.EAGER_EVENTS, - function(name, payload) - self:handle_eager_event(name, payload) - end, - { owner = "clever_f", lifecycle = "eager" } - ) - record.eager_registration = identity - activation.eager_registration = identity - end - record.activation = activation - return copy(activation) -end - -function FeedbackService:evaluate_feature_links() - local record = service_records[self] - local rules = record.policy:evaluate_highlight_links() - local results = {} - for _, group in ipairs(FEATURE_GROUPS) do - local rule = rules[group] - if rule.enabled then - if rule.configured_target ~= nil then - record.host:define_highlight_group( - group, - { link = rule.configured_target }, - { force = true } - ) - results[group] = { - group = group, - target = rule.configured_target, - source = "configured", - applied = true, - } - else - local existing = record.host:read_highlight_group(group) - if existing ~= nil then - results[group] = { - group = group, - definition = existing, - source = "colorscheme", - applied = false, - } - else - record.host:define_highlight_group( - group, - { link = rule.target }, - { default = true } - ) - results[group] = { - group = group, - target = rule.target, - source = "fallback", - applied = true, - } - end - end - end - end - return results -end - -function FeedbackService:ensure_default_label() - local existing = service_records[self].host:read_highlight_group( - M.DEFAULT_LABEL_GROUP - ) - if existing ~= nil then - return { - group = M.DEFAULT_LABEL_GROUP, - definition = existing, - source = "colorscheme", - applied = false, - } - end - - local definition = M.default_label_definition() - service_records[self].host:define_highlight_group( - M.DEFAULT_LABEL_GROUP, - definition, - { default = true } - ) - return { - group = M.DEFAULT_LABEL_GROUP, - definition = definition, - source = "fallback", - applied = true, - } -end - -function FeedbackService:evaluate_highlights() - return { - default_label = self:ensure_default_label(), - feature_links = self:evaluate_feature_links(), - } -end - -function M.new(options) - return FeedbackService.new(options) -end - -setmetatable(M, { - __call = function(_, options) - return FeedbackService.new(options) - end, -}) - -return M diff --git a/lua/clever_f/host_adapter.lua b/lua/clever_f/host_adapter.lua deleted file mode 100644 index a4ec77d..0000000 --- a/lua/clever_f/host_adapter.lua +++ /dev/null @@ -1,1165 +0,0 @@ -local capabilities = require("clever_f.capabilities") -local domain = require("clever_f.domain") - -local M = {} -local unpack_values = table.unpack or unpack -local HostAdapter = {} -HostAdapter.__index = HostAdapter -M.HostAdapter = HostAdapter - -M.ActionEffect = { - NONE = "none", - ESCAPE = "escape", - ERROR = "error", -} -M.CONFIGURATION_PREFIX = "clever_f_" -M.CONFIGURATION_GLOBALS = { - suppress_default_mappings = "clever_f_not_overwrites_standard_mappings", -} - -local BOOLEAN_CONFIGURATION = { - search_current_line_only = true, - ignore_case = true, - smart_case = true, - use_migemo = true, - fix_key_direction = true, - show_prompt = true, - mark_cursor = true, - hide_cursor_on_cmdline = true, - mark_char = true, - mark_direct = true, - clean_labels_eagerly = true, -} - -local adapter_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function current_runtime(options) - if type(options) == "table" then - if options.runtime ~= nil then - return options.runtime - end - if options.api ~= nil then - return options - end - return rawget(_G, "vim") - end - if options ~= nil and options ~= HostAdapter then - return options - end - return rawget(_G, "vim") -end - -local function require_runtime(runtime) - if type(runtime) ~= "table" or type(runtime.api) ~= "table" then - fail("HostAdapter requires the Nvim Lua runtime", 3) - end - return runtime -end - -function HostAdapter.new(options) - if HostAdapter.is(options) then - return options - end - local adapter = setmetatable({}, HostAdapter) - local record = { - runtime = require_runtime(current_runtime(options)), - next_identity = 1, - highlights = {}, - timers = {}, - cursor_presentations = {}, - events = {}, - actions = {}, - mappings = {}, - dot_repeat = nil, - dot_bridge = nil, - event_order = {}, - action_diagnostics = nil, - augroup = nil, - } - record.event_queue = capabilities.EventQueue.new(function(_, delivery) - delivery.callback(delivery.name, delivery.payload) - end) - adapter_records[adapter] = record - return adapter -end - -function HostAdapter.is(value) - return type(value) == "table" and adapter_records[value] ~= nil -end - -function HostAdapter:runtime() - return adapter_records[self].runtime -end - -function HostAdapter:read_text() - local lines = self:runtime().api.nvim_buf_get_lines(0, 0, -1, true) - if #lines == 0 then - lines = { "" } - end - return domain.TextSnapshot.new(lines) -end - -function HostAdapter:read_buffer() - return self:runtime().api.nvim_get_current_buf() -end - -function HostAdapter:read_window() - return self:runtime().api.nvim_get_current_win() -end - -function HostAdapter:read_cursor() - local position = self:runtime().api.nvim_win_get_cursor(0) - return domain.Position.new(position[1], position[2] + 1) -end - -function HostAdapter:read_mode() - return self:runtime().api.nvim_get_mode().mode -end - -local function selection_option(runtime) - local value = runtime.api.nvim_get_option_value( - "selection", - { scope = "global" } - ) - if value == "exclusive" then - return domain.SelectionOption.EXCLUSIVE - end - return domain.SelectionOption.INCLUSIVE -end - -function HostAdapter:read_selection() - local runtime = self:runtime() - local context = domain.ModeContext.from_full_mode(self:read_mode()) - local kind = context.visual_kind or context.select_kind - local option = selection_option(runtime) - if kind == nil then - return domain.Selection.inactive(option) - end - local raw_anchor = runtime.fn.getpos("v") - local focus = self:read_cursor() - local anchor - if type(raw_anchor) == "table" - and type(raw_anchor[2]) == "number" - and raw_anchor[2] > 0 - and type(raw_anchor[3]) == "number" - and raw_anchor[3] > 0 - then - anchor = domain.Position.new(raw_anchor[2], raw_anchor[3]) - else - anchor = focus - end - return domain.Selection.active(kind, anchor, focus, option) -end - -function HostAdapter:read_count() - local runtime = self:runtime() - local count = runtime.v.count1 - if type(count) ~= "number" or count < 1 then - count = 1 - end - return domain.Count.new(count) -end - -local function configuration_global(name) - if type(name) ~= "string" or name == "" then - fail("configuration name must be a nonempty string", 3) - end - return M.CONFIGURATION_GLOBALS[name] or M.CONFIGURATION_PREFIX .. name -end - -function M.configuration_global(name) - return configuration_global(name) -end - -function HostAdapter:configuration_present(name) - local global = configuration_global(name) - return self:runtime().fn.exists("g:" .. global) == 1 -end - -local function normalize_configuration(name, value) - if BOOLEAN_CONFIGURATION[name] and type(value) == "number" then - return value ~= 0 - end - return value -end - -function HostAdapter:read_configuration(name) - local global = configuration_global(name) - return normalize_configuration(name, self:runtime().g[global]) -end - -function HostAdapter:write_configuration(name, value) - local global = configuration_global(name) - self:runtime().g[global] = value -end - -function HostAdapter:read_encoding() - local runtime = self:runtime() - return runtime.api.nvim_get_option_value( - "encoding", - { scope = "global" } - ) -end - -function HostAdapter:lowercase(value) - if type(value) ~= "string" then - fail("case conversion value must be a string", 2) - end - local result = self:runtime().fn.tolower(value) - if type(result) ~= "string" then - fail("Nvim case conversion must return a string", 2) - end - return result -end - -function HostAdapter:read_macro_state() - local register = self:runtime().fn.reg_executing() - return domain.MacroState.new(register ~= "" and register or nil) -end - -local function fold_open_policies(value) - local result = {} - for item in tostring(value):gmatch("[^,]+") do - if item == "hor" then - item = "horizontal" - end - result[#result + 1] = item - end - return result -end - -function HostAdapter:read_fold_state() - local runtime = self:runtime() - local foldopen = runtime.api.nvim_get_option_value( - "foldopen", - { scope = "global" } - ) - local line = self:read_cursor().line - local closed_levels = runtime.fn.foldclosed(line) == -1 and 0 or 1 - return domain.FoldState.new( - fold_open_policies(foldopen), - closed_levels - ) -end - -function HostAdapter:read_time_ms() - local runtime = self:runtime() - local uv = runtime.uv or runtime.loop - if type(uv) ~= "table" or type(uv.hrtime) ~= "function" then - fail("HostAdapter runtime must provide a monotonic clock", 2) - end - return uv.hrtime() / 1000000 -end - -function HostAdapter:read_pending_operator() - local operator = self:runtime().v.operator - if operator == nil then - return "" - end - return operator -end - -function HostAdapter:apply_cursor(position) - position = domain.Position.coerce(position) - self:runtime().api.nvim_win_set_cursor( - 0, - { position.line, position.byte_column - 1 } - ) -end - -function HostAdapter:apply_selection(position) - if domain.Selection.is(position) then - if not position.active then - fail("selection movement requires an active selection", 2) - end - position = position.focus - end - return self:apply_cursor(position) -end - -function HostAdapter:set_operator_inclusive(enabled) - if type(enabled) ~= "boolean" then - fail("operator inclusivity must be a Boolean", 2) - end - if not enabled then - return - end - - local runtime = self:runtime() - local api = runtime.api - local selection - if type(api.nvim_get_option_value) == "function" then - selection = api.nvim_get_option_value("selection", { scope = "global" }) - end - if selection ~= "exclusive" then - api.nvim_cmd({ - cmd = "normal", - bang = true, - args = { "v" }, - }, {}) - return - end - if type(api.nvim_set_option_value) ~= "function" - or type(runtime.schedule) ~= "function" - then - fail("HostAdapter cannot preserve exclusive selection during an operator", 2) - end - - api.nvim_set_option_value("selection", "inclusive", { scope = "global" }) - local ok, command_error = pcall(api.nvim_cmd, { - cmd = "normal", - bang = true, - args = { "v" }, - }, {}) - if not ok then - api.nvim_set_option_value("selection", selection, { scope = "global" }) - error(command_error, 0) - end - runtime.schedule(function() - api.nvim_set_option_value("selection", selection, { scope = "global" }) - end) -end - -local function string_bytes(value) - local bytes = {} - for index = 1, #value do - bytes[index] = string.byte(value, index) - end - return bytes -end - -function HostAdapter:read_input() - local runtime = self:runtime() - local value = runtime.fn.getcharstr() - if type(value) ~= "string" or value == "" then - fail("Nvim target input must be a nonempty string", 2) - end - local bytes = string_bytes(value) - if #bytes == 3 - and bytes[1] == 0x80 - and bytes[2] == 0xfd - and bytes[3] == 0x60 - then - return domain.InputPacket.raw_bytes(bytes) - end - if #bytes == 1 and bytes[1] == 27 then - return domain.InputPacket.special_key("Escape", bytes) - end - if bytes[1] == 0x80 then - local name = type(runtime.fn.keytrans) == "function" - and runtime.fn.keytrans(value) - or "Special" - return domain.InputPacket.special_key(name, value) - end - return domain.InputPacket.text(value) -end - -function HostAdapter:open_fold(position) - position = position and domain.Position.coerce(position) or self:read_cursor() - local runtime = self:runtime() - if runtime.fn.foldclosed(position.line) == -1 then - return false - end - runtime.api.nvim_cmd({ - cmd = "normal", - bang = true, - args = { "zo" }, - }, {}) - return true -end - -function HostAdapter:show_prompt(text) - if type(text) ~= "string" then - fail("prompt must be a string", 2) - end - self:runtime().api.nvim_echo({ { text } }, false, {}) -end - -function HostAdapter:redraw(kind) - if kind == "suppressed" then - return false - end - if kind ~= "screen" and kind ~= "full" then - fail("redraw kind must be screen, full, or suppressed", 2) - end - self:runtime().api.nvim_cmd({ - cmd = "redraw", - bang = kind == "full", - }, {}) - return true -end - -local DIAGNOSTIC_LEVELS = { - error = "ERROR", - warning = "WARN", - info = "INFO", -} - -function HostAdapter:emit_diagnostic(level, text) - local level_name = DIAGNOSTIC_LEVELS[level] - if level_name == nil then - fail("diagnostic level must be error, warning, or info", 2) - end - if type(text) ~= "string" or text == "" then - fail("diagnostic text must be a nonempty string", 2) - end - local record = adapter_records[self] - local runtime = record.runtime - if type(runtime.notify) ~= "function" then - fail("HostAdapter runtime must provide notify", 2) - end - local levels = type(runtime.log) == "table" and runtime.log.levels or {} - runtime.notify(text, levels[level_name], { title = "clever-f" }) - if record.action_diagnostics ~= nil then - record.action_diagnostics[level .. "\0" .. text] = true - end -end - -local install_dot_bridge - -function HostAdapter:register_dot_repeat(payload, callback) - if not domain.DotPayload.is(payload) then - fail("dot-repeat payload must be a DotPayload", 2) - end - if callback ~= nil and type(callback) ~= "function" then - fail("dot-repeat callback must be a function", 2) - end - local record = adapter_records[self] - record.dot_repeat = { - payload = payload, - callback = callback, - operator = self:read_pending_operator(), - } - if install_dot_bridge ~= nil then - install_dot_bridge(self) - end - if record.dot_bridge ~= nil then - record.dot_bridge.awaiting_change = true - end - return payload -end - -function HostAdapter:dot_repeat_payload() - local registration = adapter_records[self].dot_repeat - return registration and registration.payload or nil -end - -function HostAdapter:replay_dot(count) - local registration = adapter_records[self].dot_repeat - if registration == nil or registration.callback == nil then - fail("dot repeat is not executable", 2) - end - return registration.callback( - registration.payload, - domain.Count.new(count) - ) -end - -local function next_identity(adapter, prefix) - local record = adapter_records[adapter] - local identity = prefix .. "-" .. tostring(record.next_identity) - record.next_identity = record.next_identity + 1 - return identity -end - -local function highlight_exists(runtime, name) - if type(runtime.fn) == "table" and type(runtime.fn.hlexists) == "function" then - return runtime.fn.hlexists(name) == 1 - end - local definition = runtime.api.nvim_get_hl(0, { - name = name, - link = true, - create = false, - }) - return next(definition) ~= nil -end - -function HostAdapter:read_highlight_group(name) - if type(name) ~= "string" or name == "" then - fail("highlight group name must be a nonempty string", 2) - end - local runtime = self:runtime() - if not highlight_exists(runtime, name) then - return nil - end - return runtime.api.nvim_get_hl(0, { - name = name, - link = true, - create = false, - }) -end - -local function native_highlight_definition(definition, options) - if type(definition) ~= "table" then - fail("highlight group definition must be a table", 3) - end - options = options or {} - if type(options) ~= "table" then - fail("highlight group options must be a table", 3) - end - local native = {} - for key, value in pairs(definition) do - if key ~= "guifg" and key ~= "guibg" and key ~= "gui" then - native[key] = value - end - end - if definition.guifg ~= nil then - native.fg = definition.guifg - end - if definition.guibg ~= nil then - native.bg = definition.guibg - end - for key, value in pairs(definition.gui or {}) do - native[key] = value - end - if options.default ~= nil then - native.default = options.default - end - if options.force ~= nil then - native.force = options.force - end - return native -end - -function HostAdapter:define_highlight_group(name, definition, options) - if type(name) ~= "string" or name == "" then - fail("highlight group name must be a nonempty string", 2) - end - options = options or {} - local runtime = self:runtime() - if options.default and highlight_exists(runtime, name) then - return false - end - runtime.api.nvim_set_hl( - 0, - name, - native_highlight_definition(definition, options) - ) - return true -end - -local function overlay_positions(specification) - local positions = specification.positions - if positions == nil and specification.position ~= nil then - positions = { specification.position } - end - if type(positions) ~= "table" then - fail("highlight positions must be a list", 3) - end - local native = {} - for index, position in ipairs(positions) do - position = domain.Position.coerce(position) - native[index] = { position.line, position.byte_column } - end - if #native == 0 then - native[1] = { 0 } - end - return native -end - -local function overlay_priority(value) - if value == "high" then - return 100 - end - if value == "ordinary" or value == nil then - return 10 - end - if type(value) == "number" then - return value - end - fail("highlight priority must be high, ordinary, or numeric", 3) -end - -function HostAdapter:create_highlight(specification) - if type(specification) ~= "table" then - fail("highlight specification must be a table", 2) - end - if type(specification.group) ~= "string" or specification.group == "" then - fail("highlight group must be a nonempty string", 2) - end - if specification.window == nil then - fail("highlight window must identify its Nvim window", 2) - end - local record = adapter_records[self] - local identity = specification.identity or next_identity(self, "highlight") - if record.highlights[identity] ~= nil then - fail("highlight identity is already active", 2) - end - local match_id = record.runtime.fn.matchaddpos( - specification.group, - overlay_positions(specification), - overlay_priority(specification.priority), - -1, - { window = specification.window } - ) - if type(match_id) ~= "number" or match_id < 0 then - fail("Nvim could not create the window-local highlight", 2) - end - record.highlights[identity] = { - match_id = match_id, - window = specification.window, - } - return identity -end - -function HostAdapter:remove_highlight(identity) - local record = adapter_records[self] - local resource = record.highlights[identity] - if resource == nil then - return false - end - record.highlights[identity] = nil - record.runtime.fn.matchdelete(resource.match_id, resource.window) - return true -end - -local function nonnegative_integer(value, name) - if type(value) ~= "number" - or value < 0 - or value ~= math.floor(value) - or value == math.huge - then - fail((name or "value") .. " must be a nonnegative integer", 3) - end - return value -end - -function HostAdapter:supports_timers() - local fn = self:runtime().fn - return type(fn) == "table" - and type(fn.timer_start) == "function" - and type(fn.timer_stop) == "function" -end - -function HostAdapter:start_timer(delay_ms, callback) - nonnegative_integer(delay_ms, "timer delay") - if type(callback) ~= "function" then - fail("timer callback must be a function", 2) - end - if not self:supports_timers() then - return nil - end - local record = adapter_records[self] - local identity = next_identity(self, "timer") - local timer_id = record.runtime.fn.timer_start(delay_ms, function() - local resource = record.timers[identity] - if resource == nil or not resource.active then - return - end - record.timers[identity] = nil - callback(identity) - end) - if type(timer_id) ~= "number" or timer_id < 0 then - fail("Nvim could not start the timer", 2) - end - record.timers[identity] = { - timer_id = timer_id, - active = true, - } - return identity -end - -function HostAdapter:stop_timer(identity) - local record = adapter_records[self] - local resource = record.timers[identity] - if resource == nil or not resource.active then - return false - end - record.timers[identity] = nil - record.runtime.fn.timer_stop(resource.timer_id) - return true -end - -local function event_names(value) - if type(value) == "string" then - value = { value } - end - if type(value) ~= "table" or #value == 0 then - fail("event names must be a nonempty list", 3) - end - local names = {} - local set = {} - for index, name in ipairs(value) do - if type(name) ~= "string" or name == "" then - fail("event name must be a nonempty string", 3) - end - if not set[name] then - names[#names + 1] = name - set[name] = true - end - end - return names, set -end - -local function event_payload(adapter, event) - local payload = { - buffer = event.buf, - file = event.file, - match = event.match, - data = event.data, - } - local api = adapter:runtime().api - if type(api.nvim_get_current_win) == "function" then - payload.window = api.nvim_get_current_win() - end - return payload -end - -local function event_augroup(record) - if record.augroup == nil then - record.augroup = record.runtime.api.nvim_create_augroup( - "clever_f", - { clear = true } - ) - end - return record.augroup -end - -local function queue_event(record, name, payload, callback) - return record.event_queue:emit(name, { - name = name, - payload = payload, - callback = callback, - }) -end - -function HostAdapter:register_events(names, callback, options) - local name_set - names, name_set = event_names(names) - if type(callback) ~= "function" then - fail("event callback must be a function", 2) - end - options = options or {} - if type(options) ~= "table" then - fail("event registration options must be a table", 2) - end - local record = adapter_records[self] - local identity = next_identity(self, "event-registration") - local autocmd_options = { - group = event_augroup(record), - desc = "clever-f " .. table.concat(names, "/"), - callback = function(event) - local resource = record.events[identity] - if resource ~= nil and resource.active then - queue_event( - record, - event.event, - event_payload(self, event), - resource.callback - ) - end - end, - } - if options.buffer ~= nil then - autocmd_options.buffer = options.buffer - end - local autocmd_id = record.runtime.api.nvim_create_autocmd( - names, - autocmd_options - ) - record.events[identity] = { - autocmd_id = autocmd_id, - names = names, - name_set = name_set, - callback = callback, - buffer = options.buffer, - active = true, - } - record.event_order[#record.event_order + 1] = identity - return identity -end - -function HostAdapter:remove_event_registration(identity) - local record = adapter_records[self] - local resource = record.events[identity] - if resource == nil or not resource.active then - return false - end - resource.active = false - record.runtime.api.nvim_del_autocmd(resource.autocmd_id) - return true -end - -local DOT_MOTION_MAPPING = "<Plug>(clever-f-dot-motion)" - -local function dot_bridge_supported(runtime) - return type(runtime.keymap) == "table" - and type(runtime.keymap.set) == "function" - and type(runtime.keymap.del) == "function" - and type(runtime.fn.maparg) == "function" - and type(runtime.fn.mapset) == "function" - and type(runtime.api.nvim_feedkeys) == "function" -end - -local function restore_dot_mapping(record) - local bridge = record.dot_bridge - if bridge == nil or not bridge.active then - return false - end - bridge.active = false - pcall(record.runtime.keymap.del, "n", ".") - if type(bridge.previous_mapping) == "table" - and next(bridge.previous_mapping) ~= nil - then - record.runtime.fn.mapset("n", false, bridge.previous_mapping) - end - return true -end - -local function dot_replay_keys(runtime, count, operator) - local prefix = count > 0 and tostring(count) or "" - local keys = prefix .. operator .. DOT_MOTION_MAPPING - if type(runtime.keycode) == "function" then - return runtime.keycode(keys) - end - return runtime.api.nvim_replace_termcodes(keys, true, false, true) -end - -install_dot_bridge = function(adapter) - local record = adapter_records[adapter] - local runtime = record.runtime - if not dot_bridge_supported(runtime) then - return nil - end - local bridge = record.dot_bridge - if bridge == nil then - bridge = { - active = false, - awaiting_change = false, - previous_mapping = nil, - } - record.dot_bridge = bridge - runtime.keymap.set("o", DOT_MOTION_MAPPING, function() - local registration = record.dot_repeat - if registration == nil or registration.callback == nil then - return - end - local outcome = registration.callback( - registration.payload, - adapter:read_count() - ) - if domain.ActionOutcome.is(outcome) then - adapter:translate_action_outcome(outcome) - end - end, { - silent = true, - remap = false, - desc = "clever-f dot motion", - }) - local has_cmd_atom = type(runtime.fn.exists) == "function" - and runtime.fn.exists("##CmdAtom") == 1 - local ownership_events = has_cmd_atom - and "CmdAtom" - or { "TextChanged", "TextChangedI", "TextChangedP" } - runtime.api.nvim_create_autocmd(ownership_events, { - group = event_augroup(record), - desc = "clever-f dot ownership", - callback = function(event) - if has_cmd_atom and not (event.data and event.data.changed) then - return - end - if bridge.awaiting_change then - bridge.awaiting_change = false - return - end - restore_dot_mapping(record) - end, - }) - end - if not bridge.active then - bridge.previous_mapping = runtime.fn.maparg(".", "n", false, true) - runtime.keymap.set("n", ".", function() - local registration = record.dot_repeat - if registration == nil or registration.operator == "" then - restore_dot_mapping(record) - runtime.api.nvim_feedkeys(".", "n", false) - return - end - bridge.awaiting_change = true - local count = runtime.v.count or 0 - runtime.api.nvim_feedkeys( - dot_replay_keys(runtime, count, registration.operator), - "n", - false - ) - end, { - silent = true, - remap = false, - desc = "clever-f dot repeat", - }) - bridge.active = true - end - return bridge -end - -function HostAdapter:deliver_event(name, payload) - if type(name) ~= "string" or name == "" then - fail("event name must be a nonempty string", 2) - end - payload = payload or {} - local record = adapter_records[self] - local event_buffer = payload.buffer - for _, identity in ipairs(record.event_order) do - local resource = record.events[identity] - if resource.active - and resource.name_set[name] - and (resource.buffer == nil - or event_buffer == nil - or resource.buffer == event_buffer) - then - queue_event(record, name, payload, resource.callback) - end - end -end - -function HostAdapter:begin_action_transition() - local record = adapter_records[self] - record.action_diagnostics = {} - return record.event_queue:begin_transition() -end - -function HostAdapter:commit_action_transition(token) - local record = adapter_records[self] - local result = record.event_queue:commit_transition(token) - record.action_diagnostics = nil - return result -end - -local function terminal_cursor_option(runtime) - return runtime.fn.eval("&t_ve") -end - -local function set_terminal_cursor_option(runtime, value) - runtime.api.nvim_cmd({ - cmd = "let", - args = { "&t_ve", "=", runtime.fn.string(value) }, - }, {}) -end - -function HostAdapter:supports_cursor_presentation() - local runtime = self:runtime() - return type(runtime.api.nvim_get_option_value) == "function" - and type(runtime.api.nvim_set_option_value) == "function" - and type(runtime.api.nvim_cmd) == "function" - and type(runtime.fn) == "table" - and type(runtime.fn.exists) == "function" - and runtime.fn.exists("+t_ve") == 1 - and type(runtime.fn.eval) == "function" - and type(runtime.fn.string) == "function" -end - -function HostAdapter:suppress_cursor_presentation() - if not self:supports_cursor_presentation() then - return nil - end - local record = adapter_records[self] - local runtime = record.runtime - local identity = next_identity(self, "cursor-presentation") - local saved = { - guicursor = runtime.api.nvim_get_option_value( - "guicursor", - { scope = "global" } - ), - terminal_cursor = terminal_cursor_option(runtime), - } - runtime.api.nvim_set_option_value( - "guicursor", - "a:ver1", - { scope = "global" } - ) - local ok, failure = pcall(set_terminal_cursor_option, runtime, "") - if not ok then - runtime.api.nvim_set_option_value( - "guicursor", - saved.guicursor, - { scope = "global" } - ) - error(failure, 0) - end - record.cursor_presentations[identity] = saved - return identity -end - -function HostAdapter:restore_cursor_presentation(identity) - local record = adapter_records[self] - local saved = record.cursor_presentations[identity] - if saved == nil then - return false - end - record.cursor_presentations[identity] = nil - record.runtime.api.nvim_set_option_value( - "guicursor", - saved.guicursor, - { scope = "global" } - ) - set_terminal_cursor_option(record.runtime, saved.terminal_cursor) - return true -end - -local function escape_key(runtime) - if type(runtime.keycode) == "function" then - return runtime.keycode("<Esc>") - end - if type(runtime.api.nvim_replace_termcodes) == "function" then - return runtime.api.nvim_replace_termcodes("<Esc>", true, false, true) - end - return string.char(27) -end - -function HostAdapter:return_escape() - local runtime = self:runtime() - if type(runtime.api.nvim_feedkeys) ~= "function" then - fail("HostAdapter runtime must provide nvim_feedkeys", 2) - end - runtime.api.nvim_feedkeys(escape_key(runtime), "n", false) -end - -function HostAdapter:emit_action_error(text) - return self:emit_diagnostic("error", text) -end - -function HostAdapter:translate_action_outcome(outcome) - if not domain.ActionOutcome.is(outcome) then - fail("host action translation requires an ActionOutcome", 2) - end - if outcome.kind == domain.ActionKind.ESCAPE then - self:return_escape() - return M.ActionEffect.ESCAPE - end - if outcome.kind == domain.ActionKind.ERROR then - local diagnostics = adapter_records[self].action_diagnostics - local key = "error\0" .. outcome.diagnostic - if diagnostics == nil or not diagnostics[key] then - self:emit_action_error(outcome.diagnostic) - end - return M.ActionEffect.ERROR - end - return M.ActionEffect.NONE -end - -local function packed(...) - return { n = select("#", ...), ... } -end - -local function invoke_callback(adapter, callback, ...) - local arguments = packed(...) - local token = adapter:begin_action_transition() - local results = packed(pcall(function() - local values = packed(callback(unpack_values(arguments, 1, arguments.n))) - if domain.ActionOutcome.is(values[1]) then - adapter:translate_action_outcome(values[1]) - end - return unpack_values(values, 1, values.n) - end)) - local commit = packed(pcall(adapter.commit_action_transition, adapter, token)) - if not results[1] then - error(results[2], 0) - end - if not commit[1] then - error(commit[2], 0) - end - return unpack_values(results, 2, results.n) -end - -function HostAdapter:register_action(name, callback) - if type(name) ~= "string" or name == "" then - fail("action name must be a nonempty string", 2) - end - if type(callback) ~= "function" then - fail("action callback must be a function", 2) - end - local actions = adapter_records[self].actions - if actions[name] ~= nil then - fail("action is already registered", 2) - end - actions[name] = callback - return name -end - -function HostAdapter:invoke_action(name, ...) - local callback = adapter_records[self].actions[name] - if callback == nil then - fail("action is not registered", 2) - end - return invoke_callback(self, callback, ...) -end - -function HostAdapter:invoke_callback(callback, ...) - if type(callback) ~= "function" then - fail("action callback must be a function", 2) - end - return invoke_callback(self, callback, ...) -end - -local function mapping_modes(value) - if type(value) == "string" then - value = { value } - end - if type(value) ~= "table" or #value == 0 then - fail("mapping modes must be a nonempty list", 3) - end - local result = {} - for index, mode in ipairs(value) do - if type(mode) ~= "string" or mode == "" then - fail("mapping mode must be a nonempty string", 3) - end - result[index] = mode - end - return result -end - -function HostAdapter:register_mapping(modes, lhs, action, options) - modes = mapping_modes(modes) - if type(lhs) ~= "string" or lhs == "" then - fail("mapping lhs must be a nonempty string", 2) - end - if type(action) ~= "string" and type(action) ~= "function" then - fail("mapping action must be an action name or function", 2) - end - options = options or {} - if type(options) ~= "table" then - fail("mapping options must be a table", 2) - end - local callback - if type(action) == "string" then - callback = function() - return self:invoke_action(action) - end - else - callback = function(...) - return invoke_callback(self, action, ...) - end - end - local native_options = { - silent = options.silent == true, - remap = options.remap == true, - desc = options.desc - or ("clever-f " .. (type(action) == "string" and action or lhs)), - } - self:runtime().keymap.set(modes, lhs, callback, native_options) - local identity = next_identity(self, "mapping") - adapter_records[self].mappings[identity] = { - modes = modes, - lhs = lhs, - action = action, - options = options, - callback = callback, - } - return identity -end - -function M.new(options) - return HostAdapter.new(options) -end - -setmetatable(M, { - __call = function(_, options) - return HostAdapter.new(options) - end, -}) - -return M diff --git a/lua/clever_f/init.lua b/lua/clever_f/init.lua deleted file mode 100644 index a1272fc..0000000 --- a/lua/clever_f/init.lua +++ /dev/null @@ -1,118 +0,0 @@ -local composition_root = require("clever_f.composition_root") -local host_adapter = require("clever_f.host_adapter") - -local M = {} -local active_root -local active_activation - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function build_root(options) - if composition_root.CompositionRoot.is(options) then - return options - end - if host_adapter.HostAdapter.is(options) then - return composition_root.new({ host = options }) - end - options = options or {} - if type(options) ~= "table" then - fail("clever-f activation options must be a table", 3) - end - if options.host == nil then - local adapter_options = options.runtime ~= nil - and { runtime = options.runtime } - or nil - options = { host = host_adapter.new(adapter_options) } - end - return composition_root.new(options) -end - -function M.activate(options) - if active_root == nil then - active_root = build_root(options) - active_activation = active_root:activate() - end - return active_activation -end - -local function root() - M.activate() - return active_root -end - -function M.root() - return root() -end - -function M.state() - return root():state() -end - -local function invoke(name) - local instance = root() - return instance:host():invoke_action(name) -end - -function M.StartFindForward() - return invoke("StartFindForward") -end - -function M.StartFindBackward() - return invoke("StartFindBackward") -end - -function M.StartTillForward() - return invoke("StartTillForward") -end - -function M.StartTillBackward() - return invoke("StartTillBackward") -end - -function M.Reset() - return invoke("Reset") -end - -function M.RepeatSameDirection() - return invoke("RepeatSameDirection") -end - -function M.RepeatOppositeDirection() - return invoke("RepeatOppositeDirection") -end - -local function invoke_direct(callback) - local instance = root() - local host = instance:host() - if type(host.invoke_callback) ~= "function" then - fail("clever-f host must invoke direct action callbacks", 2) - end - return host:invoke_callback(function() - return callback(instance) - end) -end - -function M.invoke_descriptor(value) - return invoke_direct(function(instance) - return instance:invoke_descriptor(value) - end) -end - -function M._diagnostic_full_reset() - return invoke_direct(function(instance) - return instance:diagnostic_full_reset() - end) -end - -M.start_find_forward = M.StartFindForward -M.start_find_backward = M.StartFindBackward -M.start_till_forward = M.StartTillForward -M.start_till_backward = M.StartTillBackward -M.reset = M.Reset -M.repeat_same_direction = M.RepeatSameDirection -M.repeat_opposite_direction = M.RepeatOppositeDirection -M.free_form = M.invoke_descriptor - -return M diff --git a/lua/clever_f/migemo_catalog.lua b/lua/clever_f/migemo_catalog.lua deleted file mode 100644 index 18d9541..0000000 --- a/lua/clever_f/migemo_catalog.lua +++ /dev/null @@ -1,527 +0,0 @@ -local domain = require("clever_f.domain") -local sequence_state = require("clever_f.sequence_state") -local state_transitions = require("clever_f.state_transitions") -local text_topology = require("clever_f.text_topology") - -local M = {} -local MigemoCatalog = {} -local MigemoDictionary = {} -M.MigemoCatalog = MigemoCatalog -M.MigemoDictionary = MigemoDictionary - -local catalog_records = setmetatable({}, { __mode = "k" }) -local dictionary_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function copy_list(values) - local result = {} - for index = 1, #values do - result[index] = values[index] - end - return result -end - -local EXPECTED_KEYS = {} -for code = string.byte("a"), string.byte("z") do - EXPECTED_KEYS[#EXPECTED_KEYS + 1] = string.char(code) -end -for code = string.byte("A"), string.byte("Z") do - EXPECTED_KEYS[#EXPECTED_KEYS + 1] = string.char(code) -end - -local EXPECTED_KEY_SET = {} -for _, key in ipairs(EXPECTED_KEYS) do - EXPECTED_KEY_SET[key] = true -end - -local ASSETS = { - ["utf-8"] = { - file = "utf8.vim", - function_name = "clever_f#migemo#utf8#load_dict", - }, - cp932 = { - file = "cp932.vim", - function_name = "clever_f#migemo#cp932#load_dict", - }, - ["euc-jp"] = { - file = "eucjp.vim", - function_name = "clever_f#migemo#eucjp#load_dict", - }, -} - -local module_source = debug.getinfo(1, "S").source -local module_file = module_source:sub(1, 1) == "@" and module_source:sub(2) or module_source -local bundled_root = module_file:match("^(.*)/lua/clever_f/migemo_catalog%.lua$") - -local function require_nonempty_string(value, name) - if type(value) ~= "string" or value == "" then - fail((name or "value") .. " must be a nonempty string", 2) - end - return value -end - -local function asset_path(asset) - if bundled_root == nil then - fail("Migemo catalog could not locate its bundled assets", 2) - end - return bundled_root .. "/autoload/clever_f/migemo/" .. asset.file -end - -local function read_asset_key_order(path) - local handle, open_error = io.open(path, "rb") - if handle == nil then - fail("Migemo asset could not be opened: " .. tostring(open_error), 2) - end - - local keys = {} - for line in handle:lines() do - local key = line:match("^%s*\\%s*'([A-Za-z])'%s*:") - if key ~= nil then - keys[#keys + 1] = key - end - end - handle:close() - return keys -end - -local function assert_key_order(keys, encoding) - if type(keys) ~= "table" or #keys ~= #EXPECTED_KEYS then - fail( - "Migemo " .. encoding .. " asset must contain exactly 52 ordered keys", - 2 - ) - end - for index, expected in ipairs(EXPECTED_KEYS) do - if keys[index] ~= expected then - fail( - "Migemo " .. encoding - .. " asset keys must be ordered a through z, then A through Z", - 2 - ) - end - end -end - -local function nvim_runtime() - local runtime = rawget(_G, "vim") - if type(runtime) ~= "table" - or runtime.cmd == nil - or type(runtime.fn) ~= "table" - or type(runtime.fn.fnameescape) ~= "function" - then - fail("Migemo dictionary loading requires Nvim", 2) - end - return runtime -end - -local function default_asset_loader(encoding, asset) - local runtime = nvim_runtime() - local path = asset_path(asset) - local keys = read_asset_key_order(path) - assert_key_order(keys, encoding) - - runtime.cmd("silent source " .. runtime.fn.fnameescape(path)) - local loader = runtime.fn[asset.function_name] - if type(loader) ~= "function" then - fail("Migemo " .. encoding .. " asset did not define its dictionary loader", 2) - end - local dictionary = loader() - return dictionary, keys, path -end - -local function explicit_pattern(pattern, case_mode) - local case_flag = case_mode == domain.CaseMode.INSENSITIVE and "\\c" or "\\C" - return "\\m" .. case_flag .. "^" .. pattern -end - -local function default_pattern_compiler(pattern, key, encoding) - local runtime = nvim_runtime() - if type(runtime.regex) ~= "function" or type(runtime.fn.match) ~= "function" then - fail("Migemo pattern evaluation requires Nvim regular expressions", 2) - end - - local sensitive = explicit_pattern(pattern, domain.CaseMode.SENSITIVE) - local insensitive = explicit_pattern(pattern, domain.CaseMode.INSENSITIVE) - local ok, compile_error = pcall(runtime.regex, sensitive) - if ok then - ok, compile_error = pcall(runtime.regex, insensitive) - end - if not ok then - fail( - "Migemo " .. encoding .. " pattern for '" .. key - .. "' could not be compiled: " .. tostring(compile_error), - 2 - ) - end - - return function(text, case_mode) - if type(text) ~= "string" then - fail("Migemo assertion text must be a string", 2) - end - case_mode = domain.CaseMode.from_string(case_mode) - local selected = case_mode == domain.CaseMode.INSENSITIVE - and insensitive - or sensitive - local matched, start_or_error = pcall(runtime.fn.match, text, selected) - if not matched then - fail( - "Migemo " .. encoding .. " pattern for '" .. key - .. "' could not be evaluated: " .. tostring(start_or_error), - 2 - ) - end - return start_or_error == 0 - end -end - -local dictionary_metatable = { - __index = function(dictionary, key) - local method = MigemoDictionary[key] - if method ~= nil then - return method - end - - local record = dictionary_records[dictionary] - if key == "encoding" or key == "effective_encoding" then - return record.encoding - end - if key == "entry_count" then - return #record.keys - end - if key == "asset_path" then - return record.asset_path - end - if EXPECTED_KEY_SET[key] then - return record.predicates[key] - end - return nil - end, - __newindex = function() - fail("MigemoDictionary values are immutable", 2) - end, - __tostring = function(dictionary) - return "migemo-dictionary:" .. dictionary_records[dictionary].encoding - end, - __metatable = "clever_f.migemo_catalog.MigemoDictionary", -} - -local function validate_dictionary_data(data, ordered_keys, encoding) - if type(data) ~= "table" then - fail("Migemo " .. encoding .. " asset must return a dictionary", 3) - end - assert_key_order(ordered_keys, encoding) - - local count = 0 - for key, pattern in pairs(data) do - count = count + 1 - if EXPECTED_KEY_SET[key] ~= true then - fail("Migemo " .. encoding .. " asset contains an unexpected key", 3) - end - if type(pattern) ~= "string" or pattern == "" then - fail("Migemo " .. encoding .. " patterns must be nonempty strings", 3) - end - end - if count ~= #EXPECTED_KEYS then - fail("Migemo " .. encoding .. " asset must contain exactly 52 keys", 3) - end - for _, key in ipairs(EXPECTED_KEYS) do - if data[key] == nil then - fail("Migemo " .. encoding .. " asset is missing key '" .. key .. "'", 3) - end - end -end - -local function new_dictionary(encoding, data, ordered_keys, path, compiler) - validate_dictionary_data(data, ordered_keys, encoding) - - local patterns = {} - local predicates = {} - for _, key in ipairs(EXPECTED_KEYS) do - local pattern = data[key] - patterns[key] = pattern - local predicate = compiler(pattern, key, encoding) - if type(predicate) ~= "function" then - fail("Migemo pattern compiler must return a predicate", 3) - end - predicates[key] = predicate - end - - local dictionary = setmetatable({}, dictionary_metatable) - dictionary_records[dictionary] = { - encoding = encoding, - keys = copy_list(ordered_keys), - patterns = patterns, - predicates = predicates, - asset_path = path, - } - return dictionary -end - -function MigemoDictionary.is(value) - return type(value) == "table" and dictionary_records[value] ~= nil -end - -local function dictionary_record(dictionary) - if not MigemoDictionary.is(dictionary) then - fail("value must be a MigemoDictionary", 3) - end - return dictionary_records[dictionary] -end - -function MigemoDictionary:keys() - return copy_list(dictionary_record(self).keys) -end - -function MigemoDictionary:has(key) - return type(key) == "string" - and dictionary_record(self).predicates[key] ~= nil -end - -function MigemoDictionary:pattern(key) - require_nonempty_string(key, "Migemo dictionary key") - local pattern = dictionary_record(self).patterns[key] - if pattern == nil then - fail("Migemo dictionary key must be one ASCII alphabetic character", 2) - end - return pattern -end - -function MigemoDictionary:predicate(key, case_mode) - require_nonempty_string(key, "Migemo dictionary key") - local predicate = dictionary_record(self).predicates[key] - if predicate == nil then - fail("Migemo dictionary key must be one ASCII alphabetic character", 2) - end - if case_mode == nil then - return predicate - end - - case_mode = domain.CaseMode.from_string(case_mode) - return function(text) - return predicate(text, case_mode) - end -end - -function MigemoDictionary:matches(key, text, case_mode) - return self:predicate(key)(text, case_mode) -end - -function MigemoDictionary:to_table() - local record = dictionary_record(self) - return { - encoding = record.encoding, - entry_count = #record.keys, - keys = copy_list(record.keys), - asset_path = record.asset_path, - } -end - -local function normalize_catalog_options(options) - if options == nil then - return {} - end - if type(options) ~= "table" then - fail("MigemoCatalog options must be a table", 3) - end - if type(options.disable_migemo_for_unsupported_encoding) == "function" - and options.policy == nil - and options.policy_service == nil - and options.transitions == nil - and options.state == nil - and options.asset_loader == nil - and options.pattern_compiler == nil - then - return { policy = options } - end - return options -end - -local function require_policy(service) - if service ~= nil and (type(service) ~= "table" - or type(service.disable_migemo_for_unsupported_encoding) ~= "function") - then - fail( - "MigemoCatalog policy must provide disable_migemo_for_unsupported_encoding", - 3 - ) - end - return service -end - -local function require_transitions(transitions, state) - transitions = transitions or state_transitions.new(state) - if type(transitions) ~= "table" - or type(transitions.CacheMigemo) ~= "function" - or type(transitions.state) ~= "function" - or transitions:state() ~= state - then - fail("MigemoCatalog transitions must mutate its SequenceState", 3) - end - return transitions -end - -local function selected_function(value, fallback, name) - value = value or fallback - if type(value) ~= "function" then - fail("MigemoCatalog " .. name .. " must be a function", 3) - end - return value -end - -local catalog_metatable = { - __index = MigemoCatalog, - __newindex = function() - fail("MigemoCatalog values are immutable", 2) - end, - __tostring = function() - return "migemo-catalog" - end, - __metatable = "clever_f.migemo_catalog.MigemoCatalog", -} - -function MigemoCatalog.new(options) - if MigemoCatalog.is(options) then - return options - end - options = normalize_catalog_options(options) - local state = options.state or sequence_state.get() - if not sequence_state.is(state) then - fail("MigemoCatalog requires the plugin-global SequenceState", 2) - end - - local catalog = setmetatable({}, catalog_metatable) - catalog_records[catalog] = { - state = state, - transitions = require_transitions(options.transitions, state), - policy = require_policy(options.policy or options.policy_service), - disable_migemo = options.disable_migemo, - asset_loader = selected_function( - options.asset_loader, - default_asset_loader, - "asset_loader" - ), - pattern_compiler = selected_function( - options.pattern_compiler, - default_pattern_compiler, - "pattern_compiler" - ), - load_counts = {}, - } - if catalog_records[catalog].disable_migemo ~= nil - and type(catalog_records[catalog].disable_migemo) ~= "function" - then - fail("MigemoCatalog disable_migemo must be a function", 2) - end - return catalog -end - -function MigemoCatalog.is(value) - return type(value) == "table" and catalog_records[value] ~= nil -end - -function M.new(options) - return MigemoCatalog.new(options) -end - -setmetatable(M, { - __call = function(_, options) - return MigemoCatalog.new(options) - end, -}) - -local function catalog_record(catalog) - if not MigemoCatalog.is(catalog) then - fail("value must be a MigemoCatalog", 3) - end - return catalog_records[catalog] -end - -local function unsupported(catalog, requested_encoding, policy_override) - local record = catalog_record(catalog) - local active_policy = policy_override or record.policy - if active_policy ~= nil then - require_policy(active_policy):disable_migemo_for_unsupported_encoding() - elseif record.disable_migemo ~= nil then - record.disable_migemo() - end - error( - "clever-f: Encoding '" .. requested_encoding - .. "' is not supported. Migemo is disabled", - 0 - ) -end - -function MigemoCatalog:get(effective_encoding, policy_override) - local requested = require_nonempty_string(effective_encoding, "effective encoding") - local encoding = text_topology.normalize_encoding(requested) - local asset = ASSETS[encoding] - if asset == nil then - return unsupported(self, requested, policy_override) - end - - local record = catalog_record(self) - local cached = record.state:get_migemo(encoding) - if cached ~= nil then - if not MigemoDictionary.is(cached) then - fail("Migemo cache contains an invalid dictionary", 2) - end - return cached - end - - local data, ordered_keys, path = record.asset_loader(encoding, asset) - local dictionary = new_dictionary( - encoding, - data, - ordered_keys, - path, - record.pattern_compiler - ) - record.transitions:CacheMigemo(encoding, dictionary) - record.load_counts[encoding] = (record.load_counts[encoding] or 0) + 1 - return dictionary -end - -function MigemoCatalog:load_count(effective_encoding) - local encoding = text_topology.normalize_encoding(effective_encoding) - return catalog_record(self).load_counts[encoding] or 0 -end - -function MigemoCatalog:cached(effective_encoding) - local encoding = text_topology.normalize_encoding(effective_encoding) - local value = catalog_record(self).state:get_migemo(encoding) - if value ~= nil and not MigemoDictionary.is(value) then - fail("Migemo cache contains an invalid dictionary", 2) - end - return value -end - -MigemoCatalog.load = MigemoCatalog.get -MigemoCatalog.select = MigemoCatalog.get -MigemoCatalog.dictionary = MigemoCatalog.get - -function M.expected_keys() - return copy_list(EXPECTED_KEYS) -end - -function M.supported_encodings() - return { "utf-8", "cp932", "euc-jp" } -end - -function M.bundled_asset_path(effective_encoding) - local encoding = text_topology.normalize_encoding(effective_encoding) - local asset = ASSETS[encoding] - if asset == nil then - return nil - end - return asset_path(asset) -end - -M.load = function(effective_encoding, options) - return MigemoCatalog.new(options):get(effective_encoding) -end -M.EXPECTED_ENTRY_COUNT = #EXPECTED_KEYS - -return M diff --git a/lua/clever_f/motion_executor.lua b/lua/clever_f/motion_executor.lua deleted file mode 100644 index 79cc2ab..0000000 --- a/lua/clever_f/motion_executor.lua +++ /dev/null @@ -1,436 +0,0 @@ -local destination_engine = require("clever_f.destination_engine") -local domain = require("clever_f.domain") -local sequence_state = require("clever_f.sequence_state") -local state_transitions = require("clever_f.state_transitions") -local text_topology = require("clever_f.text_topology") - -local M = {} -local MotionExecutor = {} -M.MotionExecutor = MotionExecutor - -M.ExecutionPath = { - VISUAL = "visual", - COMMAND = "command", -} - -local executor_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -function M.execution_path(context) - context = domain.ModeContext.from_full_mode(context) - if context.visual_kind ~= nil then - return M.ExecutionPath.VISUAL - end - return M.ExecutionPath.COMMAND -end - -function M.moved_forward(origin, destination) - origin = domain.Position.coerce(origin) - destination = domain.Position.coerce(destination) - return domain.Position.compare(destination, origin) > 0 -end - -function M.command_moved_forward(descriptor, origin, destination) - descriptor = domain.Descriptor.from_string(descriptor) - origin = domain.Position.coerce(origin) - destination = domain.Position.coerce(destination) - if descriptor.family == domain.Family.TILL - and domain.Position.stationary(origin, destination) - then - return false - end - return M.moved_forward(origin, destination) -end - -function M.create_dot_payload(plan) - if not domain.ResolvedMotionPlan.is(plan) then - fail("dot payload plan must be a ResolvedMotionPlan", 2) - end - return domain.DotPayload.new(plan.descriptor, plan.target_plan.target) -end - -function M.plan_for_dot_payload(plan, payload) - if not domain.ResolvedMotionPlan.is(plan) then - fail("dot replay plan must be a ResolvedMotionPlan", 2) - end - if not domain.DotPayload.is(payload) then - fail("dot replay payload must be a DotPayload", 2) - end - if payload.target ~= plan.target_plan.target then - fail("dot replay payload target must match its resolved target plan", 2) - end - return domain.ResolvedMotionPlan.new({ - target_plan = plan.target_plan, - descriptor = payload.descriptor, - search_scope = plan.search_scope, - endpoint_policy = plan.endpoint_policy, - }) -end - -local function copy_options(options) - local result = {} - for key, value in pairs(options or {}) do - result[key] = value - end - return result -end - -local function normalize_options(options, dependencies) - if MotionExecutor.is(options) and dependencies == nil then - return options - end - if type(options) ~= "table" then - fail("MotionExecutor options must be a table", 3) - end - if options.host ~= nil then - if dependencies ~= nil then - fail("MotionExecutor dependencies must be part of its options", 3) - end - return options - end - local result = copy_options(dependencies) - result.host = options - return result -end - -local function require_host(host) - if type(host) ~= "table" - or type(host.read_cursor) ~= "function" - or type(host.read_encoding) ~= "function" - or type(host.read_pending_operator) ~= "function" - or type(host.read_selection) ~= "function" - or type(host.read_text) ~= "function" - or type(host.apply_cursor) ~= "function" - or type(host.apply_selection) ~= "function" - or type(host.set_operator_inclusive) ~= "function" - or type(host.register_dot_repeat) ~= "function" - then - fail("MotionExecutor host must provide movement state", 3) - end - return host -end - -local function require_destination_engine(engine) - engine = engine or destination_engine.new() - if type(engine) ~= "table" or type(engine.calculate) ~= "function" then - fail("MotionExecutor destination engine must provide calculate", 3) - end - return engine -end - -local function require_feedback_service(service) - if service ~= nil and (type(service) ~= "table" - or type(service.migrate_command) ~= "function") - then - fail("MotionExecutor feedback service must provide migrate_command", 3) - end - return service -end - -local function require_state(state) - state = state or sequence_state.get() - if not sequence_state.is(state) then - fail("MotionExecutor state must be the plugin-global SequenceState", 3) - end - return state -end - -local function require_transitions(transitions, state) - transitions = transitions or state_transitions.new(state) - if type(transitions) ~= "table" - or type(transitions.CommitCommandSuccess) ~= "function" - or type(transitions.CommitVisualSuccess) ~= "function" - then - fail("MotionExecutor transitions must commit motion success", 3) - end - return transitions -end - -local executor_metatable = { - __index = MotionExecutor, - __newindex = function() - fail("MotionExecutor values are immutable", 2) - end, - __tostring = function() - return "motion-executor" - end, - __metatable = "clever_f.motion_executor.MotionExecutor", -} - -function MotionExecutor.new(options, dependencies) - options = normalize_options(options, dependencies) - if MotionExecutor.is(options) then - return options - end - - local executor = setmetatable({}, executor_metatable) - local state = require_state(options.state) - executor_records[executor] = { - host = require_host(options.host), - destination_engine = require_destination_engine( - options.destination_engine or options.engine - ), - feedback_service = require_feedback_service( - options.feedback_service or options.feedback - ), - state = state, - transitions = require_transitions( - options.transitions or options.state_transitions, - state - ), - } - return executor -end - -function MotionExecutor.is(value) - return type(value) == "table" and executor_records[value] ~= nil -end - -local function execution_request( - view, - context, - plan, - count, - first_move, - execution_options -) - if not text_topology.TextView.is(view) then - fail("motion execution view must be a TextView", 3) - end - context = domain.ModeContext.from_full_mode(context) - if not domain.ResolvedMotionPlan.is(plan) then - fail("motion execution plan must be a ResolvedMotionPlan", 3) - end - count = domain.Count.new(count) - if type(first_move) ~= "boolean" then - fail("motion execution first_move must be a Boolean", 3) - end - execution_options = execution_options or {} - if type(execution_options) ~= "table" then - fail("motion execution options must be a table", 3) - end - local dot_payload = execution_options.dot_payload - if dot_payload ~= nil and not domain.DotPayload.is(dot_payload) then - fail("motion execution dot_payload must be a DotPayload", 3) - end - local register_dot_repeat = execution_options.register_dot_repeat - if register_dot_repeat == nil then - register_dot_repeat = true - elseif type(register_dot_repeat) ~= "boolean" then - fail("motion execution register_dot_repeat must be a Boolean", 3) - end - return { - view = view, - context = context, - plan = plan, - count = count, - first_move = first_move, - dot_payload = dot_payload, - register_dot_repeat = register_dot_repeat, - } -end - -local function calculate(executor, request, origin) - return executor_records[executor].destination_engine:calculate( - request.view, - origin, - request.plan, - request.count, - request.first_move - ) -end - -local function command_action( - host, - outcome, - descriptor, - dot_payload, - use_current_position -) - return domain.ActionOutcome.new({ - kind = outcome.complete - and domain.ActionKind.MOVEMENT - or domain.ActionKind.FAILED_SEARCH, - position = use_current_position and host:read_cursor() or outcome.endpoint, - search_outcome = outcome, - effective_descriptor = descriptor, - dot_payload = dot_payload, - }) -end - -local function register_dot_replay(executor, request, payload) - local host = executor_records[executor].host - host:register_dot_repeat(payload, function(replayed_payload, replay_count) - return executor:execute_dot( - text_topology.from_host(host), - request.context, - request.plan, - replayed_payload, - replay_count - ) - end) -end - -local function migrate_command_feedback(executor, request, origin, outcome) - local record = executor_records[executor] - local feedback = record.feedback_service - if feedback == nil then - return - end - feedback:migrate_command({ - context = request.context, - origin = origin, - destination = outcome.endpoint, - plan = request.plan, - resolved_motion_plan = request.plan, - outcome = outcome, - count = request.count, - first_move = request.first_move, - moved_forward = request.moved_forward, - previous_moved_forward = record.state.moved_forward, - previous_moved_forward_initialized = record.state.moved_forward_initialized, - }) -end - -function MotionExecutor:_execute_command(request) - local host = executor_records[self].host - local origin = host:read_cursor() - local pending_operator = request.context.operator - and host:read_pending_operator() - or nil - local outcome = calculate(self, request, origin) - if outcome.successful_steps > 0 then - if request.context.operator - and request.plan.descriptor.direction == domain.Direction.FORWARD - then - host:set_operator_inclusive(true) - end - host:apply_cursor(outcome.endpoint, { - context = request.context, - descriptor = request.plan.descriptor, - origin = origin, - }) - end - if not outcome.complete then - return command_action( - host, - outcome, - request.plan.descriptor, - nil, - pending_operator ~= nil and pending_operator ~= "" - ) - end - request.moved_forward = M.command_moved_forward( - request.plan.descriptor, - origin, - outcome.endpoint - ) - migrate_command_feedback(self, request, origin, outcome) - executor_records[self].transitions:CommitCommandSuccess( - request.context, - outcome.endpoint, - request.moved_forward - ) - local dot_payload - if pending_operator ~= nil and pending_operator ~= "" then - dot_payload = request.dot_payload or M.create_dot_payload(request.plan) - if request.register_dot_repeat then - register_dot_replay(self, request, dot_payload) - end - end - return command_action( - host, - outcome, - request.plan.descriptor, - dot_payload, - pending_operator ~= nil and pending_operator ~= "" - ) -end - -function MotionExecutor:_execute_visual(request) - local host = executor_records[self].host - local selection = host:read_selection() - if not domain.Selection.is(selection) - or not selection.active - or selection.kind ~= request.context.visual_kind - then - fail("Visual motion execution requires its active selection kind", 2) - end - local origin = host:read_cursor() - local outcome = calculate(self, request, origin) - request.selection = selection - if outcome.successful_steps > 0 then - host:apply_selection(selection:with_focus(outcome.endpoint)) - end - if not outcome.complete then - return domain.ActionOutcome.from_search(outcome, request.plan.descriptor) - end - executor_records[self].transitions:CommitVisualSuccess( - request.context, - outcome.endpoint - ) - return domain.ActionOutcome.from_search(outcome, request.plan.descriptor) -end - -function MotionExecutor:execute( - view, - context, - plan, - count, - first_move, - execution_options -) - local request = execution_request( - view, - context, - plan, - count, - first_move, - execution_options - ) - if M.execution_path(request.context) == M.ExecutionPath.VISUAL then - return self:_execute_visual(request) - end - return self:_execute_command(request) -end - -function MotionExecutor:execute_dot(view, context, plan, payload, count) - return self:execute( - view, - context, - M.plan_for_dot_payload(plan, payload), - count, - false, - { - dot_payload = payload, - register_dot_repeat = false, - } - ) -end - -function M.new(options, dependencies) - return MotionExecutor.new(options, dependencies) -end - -function M.execute(host, view, context, plan, count, first_move, dependencies) - return MotionExecutor.new(host, dependencies):execute( - view, - context, - plan, - count, - first_move - ) -end - -M.run = M.execute - -setmetatable(M, { - __call = function(_, options, dependencies) - return MotionExecutor.new(options, dependencies) - end, -}) - -return M diff --git a/lua/clever_f/motion_plan.lua b/lua/clever_f/motion_plan.lua deleted file mode 100644 index 1d0b4e3..0000000 --- a/lua/clever_f/motion_plan.lua +++ /dev/null @@ -1,188 +0,0 @@ -local domain = require("clever_f.domain") - -local M = {} -local MotionPlanFactory = {} -M.MotionPlanFactory = MotionPlanFactory - -local factory_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function normalize_options(options) - if options == nil then - return {} - end - if type(options) == "table" and type(options.sample_search) == "function" then - return { policy = options } - end - if type(options) ~= "table" then - fail("MotionPlanFactory options must be a table", 3) - end - return options -end - -local function require_policy(service) - if service ~= nil and (type(service) ~= "table" - or type(service.sample_search) ~= "function") - then - fail("MotionPlanFactory policy must provide sample_search", 3) - end - return service -end - -local factory_metatable = { - __index = MotionPlanFactory, - __newindex = function() - fail("MotionPlanFactory values are immutable", 2) - end, - __tostring = function() - return "motion-plan-factory" - end, - __metatable = "clever_f.motion_plan.MotionPlanFactory", -} - -function MotionPlanFactory.new(options) - if MotionPlanFactory.is(options) then - return options - end - options = normalize_options(options) - local factory = setmetatable({}, factory_metatable) - factory_records[factory] = { - policy = require_policy(options.policy or options.policy_service), - } - return factory -end - -function MotionPlanFactory.is(value) - return type(value) == "table" and factory_records[value] ~= nil -end - -local function resolved_scope(factory, search_scope) - if search_scope == nil then - local policy = factory_records[factory].policy - if policy ~= nil then - search_scope = policy:sample_search().search_scope - else - search_scope = domain.SearchScope.BUFFER - end - elseif search_scope == "line" then - search_scope = domain.SearchScope.CURRENT_LINE - end - return domain.SearchScope.from_string(search_scope) -end - -function MotionPlanFactory:build( - target_plan, - effective_descriptor, - search_scope, - endpoint_policy -) - if not domain.TargetPlan.is(target_plan) then - fail("motion target plan must be a TargetPlan", 2) - end - - return domain.ResolvedMotionPlan.new({ - target_plan = target_plan, - descriptor = effective_descriptor, - search_scope = resolved_scope(self, search_scope), - endpoint_policy = endpoint_policy or domain.EndpointPolicy.REGULAR, - }) -end - -local function selection_option(selection) - if selection == nil then - return domain.SelectionOption.INCLUSIVE - end - if domain.Selection.is(selection) then - return selection.option - end - if domain.SelectionOption.is(selection) then - return selection - end - if type(selection) == "table" and selection.option ~= nil then - return domain.SelectionOption.from_string(selection.option) - end - return domain.SelectionOption.from_string(selection) -end - -function M.endpoint_policy(context, selection) - context = domain.ModeContext.from_full_mode(context) - local option = selection_option(selection) - local visual_kind = context.visual_kind - if option == domain.SelectionOption.EXCLUSIVE - and (visual_kind == domain.SelectionKind.CHARACTER - or visual_kind == domain.SelectionKind.LINE) - then - return domain.EndpointPolicy.VISUAL_EXCLUSIVE - end - return domain.EndpointPolicy.REGULAR -end - -function MotionPlanFactory:endpoint_policy(context, selection) - return M.endpoint_policy(context, selection) -end - -function MotionPlanFactory:build_for_context( - target_plan, - effective_descriptor, - context, - selection, - search_scope -) - return self:build( - target_plan, - effective_descriptor, - search_scope, - self:endpoint_policy(context, selection) - ) -end - -function M.new(options) - return MotionPlanFactory.new(options) -end - -function M.build( - target_plan, - effective_descriptor, - search_scope, - endpoint_policy, - options -) - return MotionPlanFactory.new(options):build( - target_plan, - effective_descriptor, - search_scope, - endpoint_policy - ) -end - -function M.build_for_context( - target_plan, - effective_descriptor, - context, - selection, - search_scope, - options -) - return MotionPlanFactory.new(options):build_for_context( - target_plan, - effective_descriptor, - context, - selection, - search_scope - ) -end - -M.create = M.build -M.resolve = M.build -M.for_context = M.build_for_context - -setmetatable(M, { - __call = function(_, options) - return MotionPlanFactory.new(options) - end, -}) - -return M diff --git a/lua/clever_f/policy.lua b/lua/clever_f/policy.lua deleted file mode 100644 index df9fb7a..0000000 --- a/lua/clever_f/policy.lua +++ /dev/null @@ -1,488 +0,0 @@ -local case_policy = require("clever_f.case_policy") -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 - -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 diff --git a/lua/clever_f/repeat_resolver.lua b/lua/clever_f/repeat_resolver.lua deleted file mode 100644 index a5374d6..0000000 --- a/lua/clever_f/repeat_resolver.lua +++ /dev/null @@ -1,333 +0,0 @@ -local domain = require("clever_f.domain") -local sequence_state = require("clever_f.sequence_state") -local state_transitions = require("clever_f.state_transitions") - -local M = {} -local RepeatResolver = {} -M.RepeatResolver = RepeatResolver - -M.Decision = { - ACQUIRE = "acquire", - REPEAT = "repeat", -} -M.ACQUIRE = M.Decision.ACQUIRE -M.REPEAT = M.Decision.REPEAT - -local resolver_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function is_nonnegative_integer(value) - return type(value) == "number" - and value >= 0 - and value < math.huge - and value == math.floor(value) -end - -local function require_policy(service) - if service ~= nil and (type(service) ~= "table" - or type(service.sample_timeouts) ~= "function" - or type(service.sample_direction) ~= "function") - then - fail("RepeatResolver policy must sample timeouts and direction", 3) - end - return service -end - -local function require_clock(clock) - if clock ~= nil and (type(clock) ~= "table" - or type(clock.read_time_ms) ~= "function") - then - fail("RepeatResolver clock must provide read_time_ms", 3) - end - return clock -end - -local function require_transitions(transitions, state) - transitions = transitions or state_transitions.new(state) - if type(transitions) ~= "table" - or type(transitions.SetRepeatTimestamp) ~= "function" - or type(transitions.PublicReset) ~= "function" - then - fail("RepeatResolver transitions must set repeat time and apply Public Reset", 3) - end - return transitions -end - -local function finite_time(value) - return type(value) == "number" - and value == value - and value > -math.huge - and value < math.huge -end - -function M.truncate_elapsed_ms(elapsed_ms) - if not finite_time(elapsed_ms) then - fail("elapsed milliseconds must be finite", 2) - end - local integer_part = math.modf(elapsed_ms) - return integer_part -end - -M.truncate_milliseconds = M.truncate_elapsed_ms - -local resolver_metatable = { - __index = RepeatResolver, - __newindex = function() - fail("RepeatResolver values are immutable", 2) - end, - __tostring = function() - return "repeat-resolver" - end, - __metatable = "clever_f.repeat_resolver.RepeatResolver", -} - -function RepeatResolver.new(options) - if RepeatResolver.is(options) then - return options - end - if options == nil then - options = {} - elseif sequence_state.is(options) then - options = { state = options } - elseif type(options) ~= "table" then - fail("RepeatResolver options must be a table", 2) - end - - local state = options.state or sequence_state.get() - if not sequence_state.is(state) then - fail("RepeatResolver state must be the plugin-global SequenceState", 2) - end - - local resolver = setmetatable({}, resolver_metatable) - resolver_records[resolver] = { - clock = require_clock(options.clock or options.time_provider or options.host), - policy = require_policy(options.policy or options.policy_service), - state = state, - transitions = require_transitions( - options.transitions or options.state_transitions, - state - ), - } - return resolver -end - -function RepeatResolver.is(value) - return type(value) == "table" and resolver_records[value] ~= nil -end - -function RepeatResolver:previous_landing(context) - context = domain.ModeContext.from_full_mode(context) - return resolver_records[self].state:get_previous_landing(context) -end - -function RepeatResolver:decide(context, current_position, macro_state) - current_position = domain.Position.coerce(current_position) - local landing = self:previous_landing(context) - if landing == nil or not domain.Position.equal(landing, current_position) then - return M.Decision.ACQUIRE - end - if domain.MacroState.new(macro_state).executing then - return M.Decision.ACQUIRE - end - return M.Decision.REPEAT -end - -RepeatResolver.eligibility = RepeatResolver.decide -RepeatResolver.resolve_eligibility = RepeatResolver.decide - -function RepeatResolver:sample_repeat_timeout_ms() - local service = resolver_records[self].policy - if service == nil then - fail("RepeatResolver requires a policy to sample repeat timeout", 2) - end - local sampled = service:sample_timeouts() - local timeout = type(sampled) == "table" and sampled.repeat_timeout_ms or nil - if not is_nonnegative_integer(timeout) then - fail("repeat_timeout_ms sample must be a nonnegative integer", 2) - end - return timeout -end - -RepeatResolver.sample_repeat_timeout = RepeatResolver.sample_repeat_timeout_ms - -function RepeatResolver:evaluate_timeout(current_window) - local timeout = self:sample_repeat_timeout_ms() - if timeout == 0 then - return M.Decision.REPEAT, nil - end - - local clock = resolver_records[self].clock - if clock == nil then - fail("RepeatResolver requires a clock for positive repeat timeout", 2) - end - local current_time = clock:read_time_ms() - if not finite_time(current_time) then - fail("repeat clock must return finite milliseconds", 2) - end - local record = resolver_records[self] - local elapsed_ms = M.truncate_elapsed_ms( - current_time - record.state.repeat_timestamp_ms - ) - record.transitions:SetRepeatTimestamp(current_time) - if elapsed_ms <= timeout then - return M.Decision.REPEAT, elapsed_ms - end - local cleanup = record.transitions:PublicReset(current_window) - return M.Decision.ACQUIRE, elapsed_ms, cleanup -end - -RepeatResolver.check_timeout = RepeatResolver.evaluate_timeout -RepeatResolver.resolve_timeout = RepeatResolver.evaluate_timeout - -local function sampled_fixed_direction(resolver) - local service = resolver_records[resolver].policy - if service == nil then - fail("RepeatResolver requires a policy to resolve primary direction", 3) - end - local sampled = service:sample_direction() - local fixed - if type(sampled) == "table" then - fixed = sampled.fix_key_direction - end - if type(fixed) ~= "boolean" then - fail("fix_key_direction sample must be a Boolean", 3) - end - return fixed -end - -function M.reverse_request(stored_descriptor, pressed_key, fix_key_direction) - local stored = domain.Descriptor.from_string(stored_descriptor) - local pressed = domain.Descriptor.from_string(pressed_key) - if type(fix_key_direction) ~= "boolean" then - fail("fix_key_direction must be a Boolean", 2) - end - - local reverse = domain.Descriptor.is_uppercase(pressed) - if fix_key_direction and domain.Descriptor.is_uppercase(stored) then - reverse = not reverse - end - return reverse -end - -function M.primary_direction(stored_descriptor, pressed_key, fix_key_direction) - local stored = domain.Descriptor.from_string(stored_descriptor) - if M.reverse_request(stored, pressed_key, fix_key_direction) then - return domain.Descriptor.swap(stored) - end - return stored -end - -M.resolve_primary_direction = M.primary_direction -M.effective_primary_descriptor = M.primary_direction - -function RepeatResolver:resolve_primary_direction(stored_descriptor, pressed_key) - return M.primary_direction( - stored_descriptor, - pressed_key, - sampled_fixed_direction(self) - ) -end - -RepeatResolver.primary_direction = RepeatResolver.resolve_primary_direction -RepeatResolver.effective_primary_descriptor = - RepeatResolver.resolve_primary_direction - -function M.explicit_target(stored_target) - if stored_target == nil then - return domain.TargetValue.code_fallback(0) - end - return stored_target -end - -local function build_explicit_request(descriptor, stored_target) - if descriptor == nil then - return domain.ExplicitRepeatRequest.neutral() - end - local target = M.explicit_target(stored_target) - if target.first_code == 0x80 then - return domain.ExplicitRepeatRequest.neutral() - end - return domain.ExplicitRepeatRequest.new(descriptor, target) -end - -function M.build_same_direction_request(stored_descriptor, stored_target) - return build_explicit_request(stored_descriptor, stored_target) -end - -M.explicit_same_direction = M.build_same_direction_request -M.same_direction_request = M.build_same_direction_request - -function RepeatResolver:same_direction_request(context) - context = domain.ModeContext.from_full_mode(context) - local state = resolver_records[self].state - return M.build_same_direction_request( - state:get_previous_descriptor(context), - state:get_previous_target(context) - ) -end - -RepeatResolver.resolve_explicit_same = RepeatResolver.same_direction_request -RepeatResolver.explicit_same = RepeatResolver.same_direction_request - -function M.build_opposite_direction_request(stored_descriptor, stored_target) - if stored_descriptor == nil then - return domain.ExplicitRepeatRequest.neutral() - end - return build_explicit_request( - domain.Descriptor.swap(stored_descriptor), - stored_target - ) -end - -M.explicit_opposite_direction = M.build_opposite_direction_request -M.opposite_direction_request = M.build_opposite_direction_request - -function RepeatResolver:opposite_direction_request(context) - context = domain.ModeContext.from_full_mode(context) - local state = resolver_records[self].state - return M.build_opposite_direction_request( - state:get_previous_descriptor(context), - state:get_previous_target(context) - ) -end - -RepeatResolver.resolve_explicit_opposite = - RepeatResolver.opposite_direction_request -RepeatResolver.explicit_opposite = RepeatResolver.opposite_direction_request - -function M.new(options) - return RepeatResolver.new(options) -end - -M.landing = function(context, options) - return RepeatResolver.new(options):previous_landing(context) -end - -function M.decide(context, current_position, macro_state, options) - return RepeatResolver.new(options):decide( - context, - current_position, - macro_state - ) -end - -M.eligibility = M.decide - -function M.sample_repeat_timeout_ms(options) - return RepeatResolver.new(options):sample_repeat_timeout_ms() -end - -function M.evaluate_timeout(options, current_window) - return RepeatResolver.new(options):evaluate_timeout(current_window) -end - -setmetatable(M, { - __call = function(_, options) - return RepeatResolver.new(options) - end, -}) - -return M diff --git a/lua/clever_f/sequence_coordinator.lua b/lua/clever_f/sequence_coordinator.lua deleted file mode 100644 index d8cf7f0..0000000 --- a/lua/clever_f/sequence_coordinator.lua +++ /dev/null @@ -1,675 +0,0 @@ -local acquisition_service_factory = require("clever_f.acquisition_service") -local case_policy = require("clever_f.case_policy") -local direct_preview_planner = require("clever_f.direct_preview_planner") -local domain = require("clever_f.domain") -local feedback_service_factory = require("clever_f.feedback_service") -local motion_executor_factory = require("clever_f.motion_executor") -local motion_plan_factory = require("clever_f.motion_plan") -local policy = require("clever_f.policy") -local repeat_resolver_factory = require("clever_f.repeat_resolver") -local sequence_state = require("clever_f.sequence_state") -local state_transitions = require("clever_f.state_transitions") -local target_plan_factory = require("clever_f.target_plan") -local text_topology = require("clever_f.text_topology") - -local M = {} -local SequenceCoordinator = {} -SequenceCoordinator.__index = SequenceCoordinator -M.SequenceCoordinator = SequenceCoordinator - -local coordinator_records = setmetatable({}, { __mode = "k" }) - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function descriptor_text(value) - if domain.Descriptor.is(value) then - return value.value - end - return tostring(value) -end - -function M.validate_primary_descriptor(value) - local descriptor = domain.Descriptor.try_from_string(value) - if descriptor == nil then - error("clever-f: Invalid mapping '" .. descriptor_text(value) .. "'", 0) - end - return descriptor -end - -function SequenceCoordinator.new(options) - if SequenceCoordinator.is(options) then - return options - end - if type(options) ~= "table" then - fail("SequenceCoordinator options must be a table", 2) - end - local host = options.host or options - local state = options.state or sequence_state.get() - if not sequence_state.is(state) then - fail("SequenceCoordinator state must be the plugin-global SequenceState", 2) - end - local transitions = options.transitions - or options.state_transitions - or state_transitions.new(state) - local policy_service = options.policy - or options.policy_service - or policy.new(host) - local resolver = options.repeat_resolver - or options.resolver - or repeat_resolver_factory.new({ - state = state, - transitions = transitions, - policy = policy_service, - clock = host, - }) - if type(resolver) ~= "table" or type(resolver.decide) ~= "function" then - fail("SequenceCoordinator repeat resolver must provide decide", 2) - end - local feedback = options.feedback - or options.feedback_service - or feedback_service_factory.new({ - host = host, - state = state, - transitions = transitions, - policy = policy_service, - }) - local lowercase = options.lowercase - if lowercase == nil and type(host.lowercase) == "function" then - lowercase = function(value) - return host:lowercase(value) - end - end - local case_resolver = options.case_resolver - or case_policy.new({ lowercase = lowercase }) - local target_factory = options.target_factory - or options.target_plan_factory - or target_plan_factory.new({ - policy = policy_service, - case_resolver = case_resolver, - }) - if type(target_factory) ~= "table" or type(target_factory.build) ~= "function" then - fail("SequenceCoordinator target factory must provide build", 2) - end - local motion_factory = options.motion_factory - or options.motion_plan_factory - or motion_plan_factory.new({ policy = policy_service }) - if type(motion_factory) ~= "table" - or type(motion_factory.build_for_context) ~= "function" - then - fail("SequenceCoordinator motion factory must build contextual plans", 2) - end - local direct_planner = options.direct_planner - or options.direct_preview_planner - or direct_preview_planner.new({ case_resolver = case_resolver }) - local acquisition = options.acquisition - or options.acquisition_service - or acquisition_service_factory.new({ - host = host, - state = state, - transitions = transitions, - policy = policy_service, - feedback = feedback, - direct_planner = direct_planner, - target_factory = target_factory, - motion_factory = motion_factory, - }) - if type(acquisition) ~= "table" or type(acquisition.acquire) ~= "function" then - fail("SequenceCoordinator acquisition service must provide acquire", 2) - end - local executor = options.motion_executor - or options.executor - or motion_executor_factory.new({ - host = host, - state = state, - transitions = transitions, - feedback = feedback, - }) - if type(executor) ~= "table" or type(executor.execute) ~= "function" then - fail("SequenceCoordinator motion executor must provide execute", 2) - end - - local coordinator = setmetatable({}, SequenceCoordinator) - coordinator_records[coordinator] = { - host = host, - state = state, - transitions = transitions, - policy = policy_service, - repeat_resolver = resolver, - feedback = feedback, - target_factory = target_factory, - motion_factory = motion_factory, - acquisition = acquisition, - motion_executor = executor, - last_primary_resolution = nil, - last_explicit_resolution = nil, - } - return coordinator -end - -function SequenceCoordinator.is(value) - return type(value) == "table" and coordinator_records[value] ~= nil -end - -function SequenceCoordinator:validate_primary_descriptor(value) - return M.validate_primary_descriptor(value) -end - -local function require_primary_reader(host) - if type(host) ~= "table" - or type(host.read_mode) ~= "function" - or type(host.read_cursor) ~= "function" - or type(host.read_count) ~= "function" - or type(host.read_macro_state) ~= "function" - then - fail("SequenceCoordinator host must provide primary action state", 3) - end - return host -end - -function SequenceCoordinator:read_primary_invocation() - local host = require_primary_reader(coordinator_records[self].host) - local context = domain.ModeContext.from_full_mode(host:read_mode()) - local position = domain.Position.coerce(host:read_cursor()) - local count = domain.Count.new(host:read_count()) - local macro_state = domain.MacroState.new(host:read_macro_state()) - return { - context = context, - position = position, - origin = position, - count = count, - macro_state = macro_state, - } -end - -function SequenceCoordinator:inspect_fold_open_policy(invocation) - if type(invocation) ~= "table" or not domain.Position.is(invocation.position) then - fail("fold preflight requires primary invocation state", 2) - end - local host = coordinator_records[self].host - if type(host.read_fold_state) ~= "function" then - fail("SequenceCoordinator host must provide fold state", 2) - end - local fold_state = host:read_fold_state() - if not domain.FoldState.is(fold_state) then - fail("SequenceCoordinator host must return FoldState", 2) - end - return fold_state -end - -local function fold_open_enabled(fold_state) - return fold_state:opens("horizontal") or fold_state:opens("all") -end - -function SequenceCoordinator:open_enclosing_folds(invocation, fold_state) - if type(invocation) ~= "table" or not domain.Position.is(invocation.position) then - fail("fold opening requires primary invocation state", 2) - end - if not domain.FoldState.is(fold_state) then - fail("fold opening requires FoldState", 2) - end - if not fold_open_enabled(fold_state) then - return 0 - end - - local host = coordinator_records[self].host - if type(host.open_fold) ~= "function" then - fail("SequenceCoordinator host must open folds", 2) - end - local opened = 0 - while fold_state.closed_levels > 0 do - if host:open_fold(invocation.position) ~= true then - break - end - opened = opened + 1 - fold_state = self:inspect_fold_open_policy(invocation) - end - return opened -end - -function SequenceCoordinator:decide_primary(invocation) - if type(invocation) ~= "table" - or not domain.ModeContext.is(invocation.context) - or not domain.Position.is(invocation.position) - or not domain.MacroState.is(invocation.macro_state) - then - fail("primary decision requires invocation state", 2) - end - return coordinator_records[self].repeat_resolver:decide( - invocation.context, - invocation.position, - invocation.macro_state - ) -end - -function SequenceCoordinator:acquire_primary(descriptor, invocation) - descriptor = self:validate_primary_descriptor(descriptor) - if type(invocation) ~= "table" then - fail("primary acquisition requires invocation state", 2) - end - return coordinator_records[self].acquisition:acquire( - descriptor, - invocation.context, - invocation.position, - invocation.count, - invocation.macro_state - ) -end - -function SequenceCoordinator:fresh_primary_resolution( - initiating_descriptor, - acquisition_result, - invocation -) - initiating_descriptor = self:validate_primary_descriptor(initiating_descriptor) - if type(acquisition_result) ~= "table" - or acquisition_result.resolved ~= true - or not domain.TargetValue.is(acquisition_result.target) - or not domain.TargetPlan.is(acquisition_result.target_plan) - or not domain.ResolvedMotionPlan.is(acquisition_result.motion_plan) - then - fail("fresh primary resolution requires acquired motion plans", 2) - end - return { - kind = "fresh", - invocation = invocation, - acquisition_result = acquisition_result, - target = acquisition_result.target, - target_plan = acquisition_result.target_plan, - motion_plan = acquisition_result.motion_plan, - effective_descriptor = initiating_descriptor, - first_move = coordinator_records[self].state:get_first_move( - invocation.context - ) == true, - skip_destination = acquisition_result.target_plan.kind - == domain.TargetPlanKind.EMPTY, - } -end - -function SequenceCoordinator:resolve_acquisition(descriptor, invocation) - local result = self:acquire_primary(descriptor, invocation) - if type(result.has_outcome) ~= "function" then - fail("AcquisitionService must return an AcquisitionResult", 2) - end - if result:has_outcome() then - return result.outcome - end - return self:fresh_primary_resolution(descriptor, result, invocation) -end - -function SequenceCoordinator:evaluate_repeat_timeout(invocation) - if type(invocation) ~= "table" then - fail("repeat timeout requires primary invocation state", 2) - end - local record = coordinator_records[self] - if type(record.repeat_resolver.evaluate_timeout) ~= "function" then - fail("SequenceCoordinator repeat resolver must evaluate timeout", 2) - end - local window = record.host:read_window() - local decision, elapsed_ms, cleanup = - record.repeat_resolver:evaluate_timeout(window) - return { - decision = decision, - elapsed_ms = elapsed_ms, - cleanup = cleanup, - window = window, - } -end - -function SequenceCoordinator:build_live_target_plan(target, invocation) - if not domain.TargetValue.is(target) then - fail("primary target planning requires a TargetValue", 2) - end - if type(invocation) ~= "table" or not domain.Position.is(invocation.position) then - fail("primary target planning requires invocation state", 2) - end - local record = coordinator_records[self] - local view = text_topology.from_host(record.host) - local sampled_search = record.policy:sample_search() - local target_plan = record.target_factory:build(target, nil, { - text_view = view, - origin = invocation.position, - search_scope = sampled_search.search_scope, - effective_encoding = view.effective_encoding, - }) - if not domain.TargetPlan.is(target_plan) then - fail("TargetPlanFactory must return a TargetPlan", 2) - end - return target_plan, view, sampled_search.search_scope -end - -function SequenceCoordinator:build_movement_plan( - target_plan, - effective_descriptor, - invocation, - search_scope -) - if not domain.TargetPlan.is(target_plan) then - fail("movement planning requires a TargetPlan", 2) - end - if type(invocation) ~= "table" or not domain.ModeContext.is(invocation.context) then - fail("movement planning requires invocation state", 2) - end - local record = coordinator_records[self] - local selection = invocation.context.visual and record.host:read_selection() or nil - local motion_plan = record.motion_factory:build_for_context( - target_plan, - effective_descriptor, - invocation.context, - selection, - search_scope - ) - if not domain.ResolvedMotionPlan.is(motion_plan) then - fail("MotionPlanFactory must return a ResolvedMotionPlan", 2) - end - return motion_plan -end - -function SequenceCoordinator:restore_repeated_feedback(resolution) - if type(resolution) ~= "table" - or not domain.TargetPlan.is(resolution.target_plan) - or not domain.ResolvedMotionPlan.is(resolution.motion_plan) - then - fail("feedback restoration requires a repeated primary resolution", 2) - end - local feedback = coordinator_records[self].feedback - if type(feedback.restore_primary) ~= "function" then - fail("FeedbackService must restore primary feedback", 2) - end - return feedback:restore_primary({ - context = resolution.invocation.context, - anchor = resolution.invocation.position, - target_plan = resolution.target_plan, - motion_plan = resolution.motion_plan, - stored_descriptor = resolution.stored_descriptor, - endpoint_policy = resolution.motion_plan.endpoint_policy, - text_view = resolution.text_view, - window = resolution.timeout.window, - }) -end - -function SequenceCoordinator:stored_primary_resolution( - invocation, - pressed_descriptor, - timeout -) - if type(invocation) ~= "table" or not domain.ModeContext.is(invocation.context) then - fail("stored primary resolution requires invocation state", 2) - end - pressed_descriptor = self:validate_primary_descriptor(pressed_descriptor) - local state = coordinator_records[self].state - local stored_descriptor = state:get_previous_descriptor(invocation.context) - local stored_target = state:get_previous_target(invocation.context) - if stored_descriptor == nil or stored_target == nil then - fail("repeat-eligible primary state must contain descriptor and target", 2) - end - local resolver = coordinator_records[self].repeat_resolver - if type(resolver.resolve_primary_direction) ~= "function" then - fail("SequenceCoordinator repeat resolver must resolve primary direction", 2) - end - local effective_descriptor = domain.Descriptor.from_string( - resolver:resolve_primary_direction(stored_descriptor, pressed_descriptor) - ) - if effective_descriptor.family ~= stored_descriptor.family then - fail("primary repetition must preserve the stored motion family", 2) - end - local target_plan, text_view, search_scope = self:build_live_target_plan( - stored_target, - invocation - ) - local motion_plan = self:build_movement_plan( - target_plan, - effective_descriptor, - invocation, - search_scope - ) - local resolution = { - kind = "repeat", - invocation = invocation, - pressed_descriptor = pressed_descriptor, - timeout = timeout, - stored_descriptor = stored_descriptor, - target = stored_target, - target_plan = target_plan, - motion_plan = motion_plan, - text_view = text_view, - search_scope = search_scope, - effective_descriptor = effective_descriptor, - first_move = state:get_first_move(invocation.context) == true, - } - resolution.restored_feedback = self:restore_repeated_feedback(resolution) - return resolution -end - -function SequenceCoordinator:refresh_primary_feedback(resolution) - if type(resolution) ~= "table" or not domain.TargetValue.is(resolution.target) then - fail("primary feedback refresh requires a resolved target", 2) - end - local record = coordinator_records[self] - if type(record.feedback.refresh_primary) ~= "function" then - fail("FeedbackService must refresh primary feedback", 2) - end - local window = resolution.timeout and resolution.timeout.window - or record.host:read_window() - return record.feedback:refresh_primary(resolution.target, window) -end - -function SequenceCoordinator:execute_resolved_motion(resolution, execution_options) - if type(resolution) ~= "table" - or not domain.ModeContext.is(resolution.invocation.context) - or not domain.ResolvedMotionPlan.is(resolution.motion_plan) - then - fail("motion execution requires a resolved motion", 2) - end - if resolution.skip_destination then - return domain.ActionOutcome.empty(resolution.invocation.position) - end - local record = coordinator_records[self] - local view = resolution.text_view or text_topology.from_host(record.host) - local outcome = record.motion_executor:execute( - view, - resolution.invocation.context, - resolution.motion_plan, - resolution.invocation.count, - resolution.first_move, - execution_options - ) - if not domain.ActionOutcome.is(outcome) then - fail("MotionExecutor must return an ActionOutcome", 2) - end - return outcome -end - -function SequenceCoordinator:execute_primary_resolution(resolution) - local record = coordinator_records[self] - record.last_primary_resolution = resolution - local outcome = self:execute_resolved_motion(resolution) - resolution.highlight_timer = self:refresh_primary_feedback(resolution) - return outcome -end - -function SequenceCoordinator:last_primary_resolution() - return coordinator_records[self].last_primary_resolution -end - -function SequenceCoordinator:last_explicit_resolution() - return coordinator_records[self].last_explicit_resolution -end - -function SequenceCoordinator:reset() - local record = coordinator_records[self] - local position = domain.Position.coerce(record.host:read_cursor()) - local cleanup = record.transitions:PublicReset(record.host:read_window()) - if type(record.feedback.release_transition_cleanup) ~= "function" then - fail("FeedbackService must release reset cleanup", 2) - end - record.feedback:release_transition_cleanup(cleanup) - return domain.ActionOutcome.neutral(position) -end - -SequenceCoordinator.Reset = SequenceCoordinator.reset - -function SequenceCoordinator:diagnostic_full_reset() - local record = coordinator_records[self] - local position = domain.Position.coerce(record.host:read_cursor()) - local cleanup = record.transitions:DiagnosticFullReset( - record.host:read_window() - ) - if type(record.feedback.release_transition_cleanup) ~= "function" then - fail("FeedbackService must release diagnostic cleanup", 2) - end - record.feedback:release_transition_cleanup(cleanup) - return domain.ActionOutcome.neutral(position) -end - -SequenceCoordinator.DiagnosticFullReset = - SequenceCoordinator.diagnostic_full_reset - -function SequenceCoordinator:read_explicit_invocation() - local host = coordinator_records[self].host - if type(host) ~= "table" - or type(host.read_mode) ~= "function" - or type(host.read_cursor) ~= "function" - or type(host.read_count) ~= "function" - then - fail("SequenceCoordinator host must provide explicit action state", 2) - end - local position = domain.Position.coerce(host:read_cursor()) - return { - context = domain.ModeContext.from_full_mode(host:read_mode()), - position = position, - origin = position, - count = domain.Count.new(host:read_count()), - } -end - -function SequenceCoordinator:resolve_explicit(kind, resolver_method) - if type(kind) ~= "string" or kind == "" then - fail("explicit repeat kind must be a nonempty string", 2) - end - if type(resolver_method) ~= "string" or resolver_method == "" then - fail("explicit repeat resolver method must be a nonempty string", 2) - end - local invocation = self:read_explicit_invocation() - local resolver = coordinator_records[self].repeat_resolver - if type(resolver[resolver_method]) ~= "function" then - fail("RepeatResolver must build " .. kind .. " requests", 2) - end - local request = resolver[resolver_method](resolver, invocation.context) - local resolution = { - kind = kind, - invocation = invocation, - request = request, - } - if request.neutral then - return resolution - end - local target_plan, text_view, search_scope = self:build_live_target_plan( - request.target, - invocation - ) - local motion_plan = self:build_movement_plan( - target_plan, - request.descriptor, - invocation, - search_scope - ) - resolution.target = request.target - resolution.target_plan = target_plan - resolution.motion_plan = motion_plan - resolution.text_view = text_view - resolution.search_scope = search_scope - resolution.effective_descriptor = request.descriptor - resolution.first_move = coordinator_records[self].state:get_first_move( - invocation.context - ) == true - resolution.skip_destination = target_plan.kind == domain.TargetPlanKind.EMPTY - return resolution -end - -function SequenceCoordinator:resolve_explicit_same() - return self:resolve_explicit("explicit_same", "same_direction_request") -end - -function SequenceCoordinator:resolve_explicit_opposite() - return self:resolve_explicit( - "explicit_opposite", - "opposite_direction_request" - ) -end - -function SequenceCoordinator:primary(value) - local descriptor = self:validate_primary_descriptor(value) - local invocation = self:read_primary_invocation() - invocation.fold_state = self:inspect_fold_open_policy(invocation) - invocation.opened_folds = self:open_enclosing_folds( - invocation, - invocation.fold_state - ) - invocation.repeat_decision = self:decide_primary(invocation) - if invocation.repeat_decision == repeat_resolver_factory.Decision.ACQUIRE then - local acquired = self:resolve_acquisition(descriptor, invocation) - if domain.ActionOutcome.is(acquired) then - return acquired - end - return self:execute_primary_resolution(acquired) - end - local timeout = self:evaluate_repeat_timeout(invocation) - if timeout.decision == repeat_resolver_factory.Decision.ACQUIRE then - local record = coordinator_records[self] - if timeout.cleanup ~= nil then - if type(record.feedback.release_transition_cleanup) ~= "function" then - fail("FeedbackService must release reset cleanup", 2) - end - record.feedback:release_transition_cleanup(timeout.cleanup) - end - local acquired = self:resolve_acquisition(descriptor, invocation) - if domain.ActionOutcome.is(acquired) then - return acquired - end - return self:execute_primary_resolution(acquired) - end - return self:execute_primary_resolution( - self:stored_primary_resolution(invocation, descriptor, timeout) - ) -end - -function SequenceCoordinator:execute_explicit_resolution(resolution) - if type(resolution) ~= "table" - or not domain.ExplicitRepeatRequest.is(resolution.request) - then - fail("explicit execution requires a resolved repeat request", 2) - end - coordinator_records[self].last_explicit_resolution = resolution - if resolution.request.neutral then - return domain.ActionOutcome.empty(resolution.invocation.position) - end - return self:execute_resolved_motion(resolution) -end - -function SequenceCoordinator:repeat_same_direction() - return self:execute_explicit_resolution(self:resolve_explicit_same()) -end - -SequenceCoordinator.RepeatSameDirection = - SequenceCoordinator.repeat_same_direction - -function SequenceCoordinator:repeat_opposite_direction() - return self:execute_explicit_resolution(self:resolve_explicit_opposite()) -end - -SequenceCoordinator.RepeatOppositeDirection = - SequenceCoordinator.repeat_opposite_direction - -function M.new(options) - return SequenceCoordinator.new(options) -end - -setmetatable(M, { - __call = function(_, options) - return SequenceCoordinator.new(options) - end, -}) - -return M diff --git a/lua/clever_f/sequence_state.lua b/lua/clever_f/sequence_state.lua deleted file mode 100644 index e31752d..0000000 --- a/lua/clever_f/sequence_state.lua +++ /dev/null @@ -1,302 +0,0 @@ -local domain = require("clever_f.domain") - -local M = {} -local State = {} -M.State = State - -local MAP_FIELDS = { - "previous_descriptor", - "previous_landing", - "first_move", - "previous_target", -} - -local MAP_FIELD_SET = {} -for _, field in ipairs(MAP_FIELDS) do - MAP_FIELD_SET[field] = true -end - -local data = { - previous_descriptor = {}, - previous_landing = {}, - first_move = {}, - previous_target = {}, - known_contexts = {}, - last_input_context = nil, - moved_forward = false, - moved_forward_initialized = false, - migemo_cache = {}, - repeat_timestamp_ms = 0, - highlight_timer = nil, - target_overlays = {}, - temporary_overlays = {}, - finalizers = {}, -} - -local function fail(message, level) - error(message, (level or 1) + 1) -end - -local function normalize_context(context, name) - if domain.ModeContext.is(context) then - return domain.ModeContext.from_full_mode(context.full_mode) - end - if type(context) ~= "string" or context == "" then - fail((name or "context") .. " must be a ModeContext or full mode string", 2) - end - return domain.ModeContext.from_full_mode(context) -end - -local function copy_map(source) - local result = {} - for key, value in pairs(source) do - result[key] = value - end - return result -end - -local function copy_resource(resource) - local result = {} - for key, value in pairs(resource) do - result[key] = value - end - return result -end - -local function copy_resources(resources) - local result = {} - for index, resource in ipairs(resources) do - result[index] = copy_resource(resource) - end - return result -end - -local function sorted_contexts() - local result = {} - for context in pairs(data.known_contexts) do - result[#result + 1] = context - end - table.sort(result, function(left, right) - return left.key < right.key - end) - return result -end - -local function context_record(context) - return { - context = context, - previous_descriptor = data.previous_descriptor[context], - previous_landing = data.previous_landing[context], - first_move = data.first_move[context], - previous_target = data.previous_target[context], - } -end - -local function context_records() - local result = {} - for _, context in ipairs(sorted_contexts()) do - result[context.key] = context_record(context) - end - return result -end - -function State:get_previous_descriptor(context) - context = normalize_context(context) - return data.previous_descriptor[context] -end - -function State:get_previous_landing(context) - context = normalize_context(context) - return data.previous_landing[context] -end - -function State:get_first_move(context) - context = normalize_context(context) - return data.first_move[context] -end - -function State:get_previous_target(context) - context = normalize_context(context) - return data.previous_target[context] -end - -function State:get_context(context) - context = normalize_context(context) - return context_record(context) -end - -State.context = State.get_context - -function State:has_previous_landing(context) - return self:get_previous_landing(context) ~= nil -end - -function State:get_migemo(encoding) - if type(encoding) ~= "string" or encoding == "" then - fail("encoding must be a nonempty string", 2) - end - return data.migemo_cache[encoding] -end - -function State:target_overlay_identities() - local result = {} - for index, resource in ipairs(data.target_overlays) do - result[index] = resource.identity - end - return result -end - -function State:temporary_overlay_identities() - local result = {} - for index, resource in ipairs(data.temporary_overlays) do - result[index] = resource.identity - end - return result -end - -function State:finalizer_identities() - local result = {} - for index, resource in ipairs(data.finalizers) do - result[index] = resource.identity - end - return result -end - -function State:resources() - return { - highlight_timer = data.highlight_timer, - target_overlays = copy_resources(data.target_overlays), - temporary_overlays = copy_resources(data.temporary_overlays), - finalizers = copy_resources(data.finalizers), - } -end - -function State:snapshot() - local snapshot = { - contexts = context_records(), - last_input_context = data.last_input_context, - moved_forward = data.moved_forward, - moved_forward_initialized = data.moved_forward_initialized, - migemo_cache = copy_map(data.migemo_cache), - repeat_timestamp_ms = data.repeat_timestamp_ms, - highlight_timer = data.highlight_timer, - target_overlays = copy_resources(data.target_overlays), - temporary_overlays = copy_resources(data.temporary_overlays), - finalizers = copy_resources(data.finalizers), - } - for _, field in ipairs(MAP_FIELDS) do - snapshot[field] = copy_map(data[field]) - end - return snapshot -end - -local function target_to_table(target) - return target and target:to_table() or nil -end - -function State:to_table() - local contexts = {} - for _, context in ipairs(sorted_contexts()) do - local record = context_record(context) - contexts[context.key] = { - previous_descriptor = record.previous_descriptor - and record.previous_descriptor.value - or nil, - previous_landing = record.previous_landing - and record.previous_landing:to_table() - or nil, - first_move = record.first_move, - previous_target = target_to_table(record.previous_target), - } - end - - local cache_keys = {} - for encoding in pairs(data.migemo_cache) do - cache_keys[#cache_keys + 1] = encoding - end - table.sort(cache_keys) - - return { - contexts = contexts, - last_input_context = data.last_input_context and data.last_input_context.key or nil, - moved_forward = data.moved_forward, - moved_forward_initialized = data.moved_forward_initialized, - migemo_cache = cache_keys, - repeat_timestamp_ms = data.repeat_timestamp_ms, - highlight_timer = data.highlight_timer, - target_overlays = self:target_overlay_identities(), - temporary_overlays = self:temporary_overlay_identities(), - finalizers = self:finalizer_identities(), - } -end - -local state -local state_metatable = { - __index = function(_, key) - local method = State[key] - if method ~= nil then - return method - end - if MAP_FIELD_SET[key] then - return copy_map(data[key]) - end - if key == "contexts" then - return context_records() - end - if key == "last_input_context" - or key == "moved_forward" - or key == "moved_forward_initialized" - or key == "highlight_timer" - then - return data[key] - end - if key == "repeat_timestamp" or key == "repeat_timestamp_ms" then - return data.repeat_timestamp_ms - end - if key == "migemo_cache" then - return copy_map(data.migemo_cache) - end - if key == "target_overlays" - or key == "temporary_overlays" - or key == "finalizers" - then - return copy_resources(data[key]) - end - return nil - end, - __newindex = function() - fail("SequenceState is read-only; use StateTransitions", 2) - end, - __metatable = "clever_f.sequence_state.State", -} -state = setmetatable({}, state_metatable) - -function M.get() - return state -end - -function M.new() - return state -end - -function M.is(value) - return value == state -end - -M.global = state - -function M._mutate(target, mutation) - if target ~= state then - fail("StateTransitions must use the plugin-global SequenceState", 2) - end - if type(mutation) ~= "function" then - fail("state mutation must be a function", 2) - end - return mutation(data) -end - -function M._normalize_context(context) - return normalize_context(context) -end - -return M diff --git a/lua/clever_f/state_transitions.lua b/lua/clever_f/state_transitions.lua deleted file mode 100644 index 426bddb..0000000 --- a/lua/clever_f/state_transitions.lua +++ /dev/null @@ -1,493 +0,0 @@ -local domain = require("clever_f.domain") -local sequence_state = require("clever_f.sequence_state") - -local M = {} -local StateTransitions = {} -StateTransitions.__index = StateTransitions -M.StateTransitions = StateTransitions - -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 require_time(value, name) - if type(value) ~= "number" - or value ~= value - or value <= -math.huge - or value >= math.huge - then - fail((name or "time") .. " must be a finite number", 2) - end - return value -end - -local function require_identity(identity, name) - if identity == nil then - fail((name or "resource identity") .. " must be active", 2) - end - return identity -end - -local function require_location(location, name) - if location == nil then - fail((name or "resource location") .. " must identify its host location", 2) - end - return location -end - -local function require_target(target) - if not domain.TargetValue.is(target) then - fail("acquired target must be a TargetValue", 2) - end - return target -end - -local function require_position(position) - return domain.Position.coerce(position) -end - -local function moved_forward_value(direction) - if type(direction) == "boolean" then - return direction - end - if domain.Direction.is(direction) then - return direction == domain.Direction.FORWARD - end - if domain.Descriptor.is(direction) then - return direction.direction == domain.Direction.FORWARD - end - if direction == "forward" or direction == "backward" then - return direction == "forward" - end - fail("movement direction must be a Boolean, Direction, or Descriptor", 2) -end - -local function copy_resource(resource) - local result = {} - for key, value in pairs(resource) do - result[key] = value - end - return result -end - -local function resource_matches(resource, identity, location_field, location) - return resource.identity == identity - and (location == nil or resource[location_field] == location) -end - -local function add_unique_resource(resources, resource, location_field, field_name) - for _, active in ipairs(resources) do - if resource_matches( - active, - resource.identity, - location_field, - resource[location_field] - ) then - fail(field_name .. " resource is already active at this host location", 3) - end - end - resources[#resources + 1] = resource - return copy_resource(resource) -end - -local function remove_resources(data, field, predicate) - local removed = {} - local retained = {} - for _, resource in ipairs(data[field]) do - if predicate(resource) then - removed[#removed + 1] = copy_resource(resource) - else - retained[#retained + 1] = resource - end - end - data[field] = retained - return removed -end - -local function clear_target_overlays(data, window) - return remove_resources(data, "target_overlays", function(resource) - return window == nil or resource.window == window - end) -end - -local function clear_temporary_overlays(data, window) - return remove_resources(data, "temporary_overlays", function(resource) - return window == nil or resource.window == window - end) -end - -local function clear_finalizers(data, buffer) - return remove_resources(data, "finalizers", function(resource) - return buffer == nil or resource.buffer == buffer - end) -end - -local function clear_highlight_timer(data) - local identity = data.highlight_timer - data.highlight_timer = nil - return identity -end - -local function clear_all_landings_and_direction(data) - data.previous_landing = {} - data.moved_forward = false -end - -local function public_reset(data, current_window) - local cleanup = { - highlight_timer = clear_highlight_timer(data), - target_overlays = clear_target_overlays(data, current_window), - finalizers = {}, - temporary_overlays = {}, - } - data.previous_descriptor = {} - data.previous_landing = {} - data.first_move = {} - data.migemo_cache = {} - data.repeat_timestamp_ms = 0 - return cleanup -end - -function StateTransitions.new(state) - state = state or sequence_state.get() - if not sequence_state.is(state) then - fail("StateTransitions requires the plugin-global SequenceState", 2) - end - return setmetatable({ _state = state }, StateTransitions) -end - -function M.new(state) - return StateTransitions.new(state) -end - -setmetatable(M, { - __call = function(_, state) - return StateTransitions.new(state) - end, -}) - -function StateTransitions:state() - return self._state -end - -function StateTransitions:_mutate(mutation) - return sequence_state._mutate(self._state, mutation) -end - -function StateTransitions:BeginAcquisition(context, descriptor) - context = sequence_state._normalize_context(context) - descriptor = domain.Descriptor.from_string(descriptor) - return self:_mutate(function(state) - state.known_contexts[context] = true - state.previous_descriptor[context] = descriptor - state.first_move[context] = true - return self._state:get_context(context) - end) -end - -function StateTransitions:CommitAcquiredTarget(context, target, time_ms) - context = sequence_state._normalize_context(context) - target = require_target(target) - if time_ms ~= nil then - time_ms = require_time(time_ms, "acquisition time") - end - return self:_mutate(function(state) - state.known_contexts[context] = true - state.previous_target[context] = target - state.last_input_context = context - if time_ms ~= nil then - state.repeat_timestamp_ms = time_ms - end - return self._state:get_context(context) - end) -end - -function StateTransitions:CommitCommandSuccess(context, destination, direction) - context = sequence_state._normalize_context(context) - destination = require_position(destination) - local forward = moved_forward_value(direction) - return self:_mutate(function(state) - state.known_contexts[context] = true - state.moved_forward = forward - state.moved_forward_initialized = true - state.previous_landing[context] = destination - state.first_move[context] = false - return self._state:get_context(context) - end) -end - -function StateTransitions:CommitVisualSuccess(context, destination) - context = sequence_state._normalize_context(context) - destination = require_position(destination) - return self:_mutate(function(state) - state.known_contexts[context] = true - state.previous_landing[context] = destination - state.first_move[context] = false - return self._state:get_context(context) - end) -end - -function StateTransitions:ClearAllLandingsAndDirection() - return self:_mutate(function(state) - clear_all_landings_and_direction(state) - end) -end - -function StateTransitions:SetRepeatTimestamp(time_ms) - time_ms = require_time(time_ms, "repeat timestamp") - return self:_mutate(function(state) - local previous = state.repeat_timestamp_ms - state.repeat_timestamp_ms = time_ms - return previous - end) -end - -function StateTransitions:CacheMigemo(encoding, dictionary) - if type(encoding) ~= "string" or encoding == "" then - fail("Migemo cache encoding must be a nonempty string", 2) - end - if dictionary == nil then - fail("Migemo cache dictionary must be present", 2) - end - return self:_mutate(function(state) - local previous = state.migemo_cache[encoding] - state.migemo_cache[encoding] = dictionary - return previous - end) -end - -function StateTransitions:RemoveMigemo(encoding) - if type(encoding) ~= "string" or encoding == "" then - fail("Migemo cache encoding must be a nonempty string", 2) - end - return self:_mutate(function(state) - local previous = state.migemo_cache[encoding] - state.migemo_cache[encoding] = nil - return previous - end) -end - -function StateTransitions:ClearMigemoCache() - return self:_mutate(function(state) - local previous = state.migemo_cache - state.migemo_cache = {} - return previous - end) -end - -function StateTransitions:SetHighlightTimer(identity) - return self:_mutate(function(state) - local previous = state.highlight_timer - state.highlight_timer = identity - return previous - end) -end - -function StateTransitions:ClearHighlightTimer(expected_identity) - return self:_mutate(function(state) - local current = state.highlight_timer - if current == nil then - return nil, false - end - if expected_identity ~= nil and current ~= expected_identity then - return nil, false - end - state.highlight_timer = nil - return current, true - end) -end - -function StateTransitions:AddTargetOverlay(identity, window, anchor_line) - if type(identity) == "table" and window == nil and identity.identity ~= nil then - local resource = identity - identity = resource.identity - window = resource.window - anchor_line = resource.anchor_line - end - require_identity(identity, "target overlay identity") - require_location(window, "target overlay window") - if anchor_line ~= nil and (not is_integer(anchor_line) or anchor_line < 1) then - fail("target overlay anchor_line must be a positive integer", 2) - end - local resource = { - identity = identity, - window = window, - group = "CleverFChar", - anchor_line = anchor_line, - } - return self:_mutate(function(state) - return add_unique_resource( - state.target_overlays, - resource, - "window", - "target overlay" - ) - end) -end - -function StateTransitions:RemoveTargetOverlay(identity, window) - require_identity(identity, "target overlay identity") - return self:_mutate(function(state) - return remove_resources(state, "target_overlays", function(resource) - return resource_matches(resource, identity, "window", window) - end) - end) -end - -function StateTransitions:ClearTargetOverlays(window) - return self:_mutate(function(state) - return clear_target_overlays(state, window) - end) -end - -function StateTransitions:AddTemporaryOverlay(identity, window, group) - if type(identity) == "table" and window == nil and identity.identity ~= nil then - local resource = identity - identity = resource.identity - window = resource.window - group = resource.group - end - require_identity(identity, "temporary overlay identity") - require_location(window, "temporary overlay window") - group = group or "CleverFCursor" - if group ~= "CleverFCursor" and group ~= "CleverFDirect" then - fail("temporary overlay group must be CleverFCursor or CleverFDirect", 2) - end - local resource = { - identity = identity, - window = window, - group = group, - } - return self:_mutate(function(state) - return add_unique_resource( - state.temporary_overlays, - resource, - "window", - "temporary overlay" - ) - end) -end - -function StateTransitions:RemoveTemporaryOverlay(identity, window) - require_identity(identity, "temporary overlay identity") - return self:_mutate(function(state) - return remove_resources(state, "temporary_overlays", function(resource) - return resource_matches(resource, identity, "window", window) - end) - end) -end - -function StateTransitions:ClearTemporaryOverlays(window) - return self:_mutate(function(state) - return clear_temporary_overlays(state, window) - end) -end - -function StateTransitions:AddFinalizer(identity, buffer) - if type(identity) == "table" and buffer == nil and identity.identity ~= nil then - local resource = identity - identity = resource.identity - buffer = resource.buffer - end - require_identity(identity, "finalizer identity") - require_location(buffer, "finalizer buffer") - local resource = { - identity = identity, - buffer = buffer, - } - return self:_mutate(function(state) - return add_unique_resource(state.finalizers, resource, "buffer", "finalizer") - end) -end - -function StateTransitions:RemoveFinalizer(identity, buffer) - require_identity(identity, "finalizer identity") - return self:_mutate(function(state) - return remove_resources(state, "finalizers", function(resource) - return resource_matches(resource, identity, "buffer", buffer) - end) - end) -end - -function StateTransitions:ClearFinalizers(buffer) - return self:_mutate(function(state) - return clear_finalizers(state, buffer) - end) -end - -function StateTransitions:ClearTargetFeedback(current_window) - return self:_mutate(function(state) - return { - highlight_timer = clear_highlight_timer(state), - target_overlays = clear_target_overlays(state, current_window), - finalizers = {}, - temporary_overlays = {}, - } - end) -end - -function StateTransitions:FullFinalization(current_window) - return self:_mutate(function(state) - local cleanup = { - highlight_timer = clear_highlight_timer(state), - target_overlays = clear_target_overlays(state, current_window), - finalizers = clear_finalizers(state), - temporary_overlays = {}, - } - clear_all_landings_and_direction(state) - return cleanup - end) -end - -function StateTransitions:PublicReset(current_window) - return self:_mutate(function(state) - return public_reset(state, current_window) - end) -end - -function StateTransitions:DiagnosticFullReset(current_window) - return self:_mutate(function(state) - local cleanup = public_reset(state, current_window) - cleanup.finalizers = clear_finalizers(state) - state.previous_target = {} - state.last_input_context = nil - state.moved_forward = false - state.moved_forward_initialized = false - return cleanup - end) -end - -StateTransitions.begin_acquisition = StateTransitions.BeginAcquisition -StateTransitions.commit_acquired_target = StateTransitions.CommitAcquiredTarget -StateTransitions.commit_command_success = StateTransitions.CommitCommandSuccess -StateTransitions.commit_visual_success = StateTransitions.CommitVisualSuccess -StateTransitions.clear_all_landings_and_direction = - StateTransitions.ClearAllLandingsAndDirection -StateTransitions.set_repeat_timestamp = StateTransitions.SetRepeatTimestamp -StateTransitions.cache_migemo = StateTransitions.CacheMigemo -StateTransitions.remove_migemo = StateTransitions.RemoveMigemo -StateTransitions.clear_migemo_cache = StateTransitions.ClearMigemoCache -StateTransitions.set_highlight_timer = StateTransitions.SetHighlightTimer -StateTransitions.clear_highlight_timer = StateTransitions.ClearHighlightTimer -StateTransitions.add_target_overlay = StateTransitions.AddTargetOverlay -StateTransitions.remove_target_overlay = StateTransitions.RemoveTargetOverlay -StateTransitions.clear_target_overlays = StateTransitions.ClearTargetOverlays -StateTransitions.add_temporary_overlay = StateTransitions.AddTemporaryOverlay -StateTransitions.remove_temporary_overlay = StateTransitions.RemoveTemporaryOverlay -StateTransitions.clear_temporary_overlays = StateTransitions.ClearTemporaryOverlays -StateTransitions.add_finalizer = StateTransitions.AddFinalizer -StateTransitions.remove_finalizer = StateTransitions.RemoveFinalizer -StateTransitions.clear_finalizers = StateTransitions.ClearFinalizers -StateTransitions.clear_target_feedback = StateTransitions.ClearTargetFeedback -StateTransitions.full_finalization = StateTransitions.FullFinalization -StateTransitions.public_reset = StateTransitions.PublicReset -StateTransitions.diagnostic_full_reset = StateTransitions.DiagnosticFullReset - -return M 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 diff --git a/lua/clever_f/testing/memory_host.lua b/lua/clever_f/testing/memory_host.lua deleted file mode 100644 index 6147d2c..0000000 --- a/lua/clever_f/testing/memory_host.lua +++ /dev/null @@ -1,1075 +0,0 @@ -local capabilities = require("clever_f.capabilities") -local domain = require("clever_f.domain") -local text_topology = require("clever_f.text_topology") - -local M = {} -local MemoryHost = {} -MemoryHost.__index = MemoryHost -M.MemoryHost = MemoryHost -local unpack_values = table.unpack or unpack - -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(value, seen) - if type(value) ~= "table" or domain.type_of(value) ~= nil then - return value - end - seen = seen or {} - if seen[value] ~= nil then - return seen[value] - end - local result = {} - seen[value] = result - for key, item in pairs(value) do - result[copy(key, seen)] = copy(item, seen) - end - return result -end - -local function list_copy(values) - local result = {} - for index = 1, #values do - result[index] = values[index] - end - return result -end - -local function text_snapshot(value) - if domain.TextSnapshot.is(value) then - return value - end - if type(value) == "table" and value.lines ~= nil then - value = value.lines - end - return domain.TextSnapshot.new(value) -end - -local function selection_value(value) - if value == nil then - return domain.Selection.inactive() - end - return domain.Selection.new(value) -end - -local function macro_state(value) - if type(value) == "table" and not domain.MacroState.is(value) then - value = value.register - end - return domain.MacroState.new(value) -end - -local function fold_state(options) - if domain.FoldState.is(options.fold_state) then - return options.fold_state - end - return domain.FoldState.new( - options.fold_open_policy or {}, - options.closed_fold_levels or 0 - ) -end - -local function input_packet(value) - return domain.InputPacket.from_table(value) -end - -local function normalize_event_names(event_names) - if type(event_names) == "string" then - event_names = { event_names } - end - if type(event_names) ~= "table" or #event_names < 1 then - error("event names must be a nonempty list", 3) - end - local result = {} - local seen = {} - for index = 1, #event_names do - local name = event_names[index] - if type(name) ~= "string" or name == "" then - error("event name must be a nonempty string", 3) - end - if not seen[name] then - seen[name] = true - result[#result + 1] = name - end - end - return result, seen -end - -local function normalize_modes(modes) - if type(modes) == "string" then - modes = { modes } - end - if type(modes) ~= "table" or #modes < 1 then - error("mapping modes must be a nonempty list", 3) - end - local result = {} - for index = 1, #modes do - if type(modes[index]) ~= "string" or modes[index] == "" then - error("mapping mode must be a nonempty string", 3) - end - result[index] = modes[index] - end - return result -end - -function MemoryHost.new(options) - options = options or {} - if type(options) ~= "table" then - error("memory host options must be a table", 2) - end - - local cursor_presentation_support = options.cursor_presentation_support - if cursor_presentation_support == nil then - cursor_presentation_support = options.cmdline_cursor_support - end - if cursor_presentation_support == nil then - cursor_presentation_support = true - end - - local raw_mode = options.mode or "n" - if domain.ModeContext.is(raw_mode) then - raw_mode = raw_mode.full_mode - end - domain.ModeContext.from_full_mode(raw_mode) - - local self = setmetatable({ - _text = text_snapshot(options.text or options.buffer_lines or { "" }), - _buffer = options.buffer or "buffer-1", - _cursor = domain.Position.coerce(options.cursor or { line = 1, byte_column = 1 }), - _window = options.window or "window-1", - _mode = raw_mode, - _selection = selection_value(options.selection), - _count = domain.Count.new(options.count), - _configuration = copy(options.configuration or {}), - _encoding = options.encoding or options.effective_encoding or "utf-8", - _lowercase = options.lowercase or vim.fn.tolower, - _macro_state = macro_state(options.macro_state or options.macro_register), - _fold_state = fold_state(options), - _pending_operator = options.pending_operator, - _time_values = list_copy(options.time_values_ms or {}), - _time_index = 1, - _current_time = options.time_ms or 0, - _input_packets = {}, - _input_index = 1, - _timer_support = options.timer_support ~= false, - _cursor_presentation_support = cursor_presentation_support, - _cursor_presentation = copy(options.cursor_presentation or { - hidden = false, - }), - _emit_movement_events = options.emit_movement_events ~= false, - _operator_inclusive = false, - _operations = {}, - _prompts = {}, - _redraws = {}, - _diagnostics = {}, - _highlight_groups = copy(options.highlight_groups or {}), - _highlights = {}, - _timers = {}, - _event_registrations = {}, - _event_registration_order = {}, - _actions = {}, - _mappings = {}, - _cursor_leases = {}, - _dot_repeat = nil, - _identity_counters = {}, - }, MemoryHost) - - for index, packet in ipairs(options.input_packets or {}) do - self._input_packets[index] = input_packet(packet) - end - - self._event_queue = capabilities.EventQueue.new(function(name, payload) - self:_deliver_event_now(name, payload) - end) - - return capabilities.assert_implements(self) -end - -function M.new(options) - return MemoryHost.new(options) -end - -setmetatable(M, { - __call = function(_, options) - return MemoryHost.new(options) - end, -}) - -function MemoryHost:_next_identity(prefix) - local next_value = (self._identity_counters[prefix] or 0) + 1 - self._identity_counters[prefix] = next_value - return prefix .. "-" .. tostring(next_value) -end - -function MemoryHost:_record(operation, details) - local entry = { operation = operation } - for key, value in pairs(details or {}) do - entry[key] = copy(value) - end - self._operations[#self._operations + 1] = entry -end - -function MemoryHost:operations() - return copy(self._operations) -end - -function MemoryHost:clear_operations() - self._operations = {} -end - -function MemoryHost:read_text() - self:_record("read_text") - return self._text -end - -function MemoryHost:read_cursor() - self:_record("read_cursor") - return self._cursor -end - -function MemoryHost:read_buffer() - self:_record("read_buffer", { buffer = self._buffer }) - return self._buffer -end - -function MemoryHost:read_window() - self:_record("read_window", { window = self._window }) - return self._window -end - -function MemoryHost:read_mode() - self:_record("read_mode", { mode = self._mode }) - return self._mode -end - -function MemoryHost:read_mode_context() - return domain.ModeContext.from_full_mode(self:read_mode()) -end - -function MemoryHost:read_pending_operator() - self:_record("read_pending_operator", { operator = self._pending_operator }) - return self._pending_operator -end - -function MemoryHost:read_selection() - self:_record("read_selection") - return self._selection -end - -function MemoryHost:read_count() - self:_record("read_count", { count = self._count.value }) - return self._count -end - -function MemoryHost:configuration_present(name) - if type(name) ~= "string" or name == "" then - error("configuration name must be a nonempty string", 2) - end - local present = self._configuration[name] ~= nil - self:_record("configuration_present", { name = name, present = present }) - return present -end - -function MemoryHost:read_configuration(name) - if type(name) ~= "string" or name == "" then - error("configuration name must be a nonempty string", 2) - end - local value = copy(self._configuration[name]) - self:_record("read_configuration", { name = name, value = value }) - return value -end - -function MemoryHost:write_configuration(name, value) - if type(name) ~= "string" or name == "" then - error("configuration name must be a nonempty string", 2) - end - self._configuration[name] = copy(value) - self:_record("write_configuration", { name = name, value = value }) -end - -function MemoryHost:read_encoding() - self:_record("read_encoding", { encoding = self._encoding }) - return self._encoding -end - -function MemoryHost:lowercase(value) - if type(value) ~= "string" then - error("case conversion value must be a string", 2) - end - local result = self._lowercase(value) - if type(result) ~= "string" then - error("case converter must return a string", 2) - end - self:_record("lowercase", { value = value, result = result }) - return result -end - -function MemoryHost:read_macro_state() - self:_record("read_macro_state", { executing = self._macro_state.executing }) - return self._macro_state -end - -function MemoryHost:read_fold_state() - self:_record("read_fold_state", { closed_levels = self._fold_state.closed_levels }) - return self._fold_state -end - -function MemoryHost:read_time_ms() - local value = self._time_values[self._time_index] - if value ~= nil then - self._time_index = self._time_index + 1 - self._current_time = value - else - value = self._current_time - end - if type(value) ~= "number" then - error("time value must be a number", 2) - end - self:_record("read_time_ms", { value = value }) - return value -end - -function MemoryHost:set_text(value) - self._text = text_snapshot(value) -end - -function MemoryHost:set_cursor(position) - self._cursor = domain.Position.coerce(position) -end - -function MemoryHost:set_buffer(buffer) - if buffer == nil then - error("buffer identity must be present", 2) - end - self._buffer = buffer -end - -function MemoryHost:set_window(window) - if window == nil then - error("window identity must be present", 2) - end - self._window = window -end - -function MemoryHost:set_mode(full_mode) - if domain.ModeContext.is(full_mode) then - full_mode = full_mode.full_mode - end - domain.ModeContext.from_full_mode(full_mode) - self._mode = full_mode -end - -function MemoryHost:set_selection(selection) - self._selection = selection_value(selection) -end - -function MemoryHost:set_count(count) - self._count = domain.Count.new(count) -end - -function MemoryHost:set_configuration(name, value) - if type(name) ~= "string" or name == "" then - error("configuration name must be a nonempty string", 2) - end - self._configuration[name] = copy(value) -end - -function MemoryHost:unset_configuration(name) - self._configuration[name] = nil -end - -function MemoryHost:set_encoding(encoding) - if type(encoding) ~= "string" or encoding == "" then - error("encoding must be a nonempty string", 2) - end - self._encoding = encoding -end - -function MemoryHost:set_macro_state(state) - self._macro_state = macro_state(state) -end - -function MemoryHost:set_fold_state(state, closed_levels) - if domain.FoldState.is(state) then - self._fold_state = state - else - self._fold_state = domain.FoldState.new(state, closed_levels) - end -end - -function MemoryHost:set_pending_operator(operator) - self._pending_operator = operator -end - -function MemoryHost:push_time_ms(value) - if type(value) ~= "number" then - error("time value must be a number", 2) - end - self._time_values[#self._time_values + 1] = value -end - -function MemoryHost:push_input(packet) - self._input_packets[#self._input_packets + 1] = input_packet(packet) -end - -function MemoryHost:_emit_movement_event(previous) - if self._emit_movement_events and not domain.Position.equal(previous, self._cursor) then - self:deliver_event("CursorMoved", { - cursor = self._cursor, - }) - end -end - -local function motion_descriptor(motion) - if domain.Descriptor.is(motion) then - return motion - end - if type(motion) == "table" and motion.descriptor ~= nil then - return domain.Descriptor.from_string(motion.descriptor) - end - return nil -end - -local function character_boundary(view, position) - if view:line_is_empty(position.line) then - return 1 - end - return view:character_index_for_position(position) -end - -local function character_lines(snapshot) - local result = {} - for line_number, line in ipairs(snapshot:lines()) do - result[line_number] = text_topology.split_editor_characters(line) - end - return result -end - -local function joined_range(characters, first, last) - local result = {} - for index = first, last do - result[#result + 1] = characters[index] - end - return table.concat(result) -end - -local function delete_character_range( - snapshot, - start_line, - start_index, - finish_line, - finish_index -) - local source = character_lines(snapshot) - local lines = snapshot:lines() - local result = {} - - for line_number = 1, start_line - 1 do - result[#result + 1] = lines[line_number] - end - - local prefix = joined_range(source[start_line], 1, start_index - 1) - if start_line == finish_line then - result[#result + 1] = prefix - .. joined_range( - source[start_line], - finish_index, - #source[start_line] - ) - else - result[#result + 1] = prefix - .. joined_range( - source[finish_line], - finish_index, - #source[finish_line] - ) - end - - for line_number = finish_line + 1, #lines do - result[#result + 1] = lines[line_number] - end - return domain.TextSnapshot.new(result) -end - -local function normalized_cursor(snapshot, encoding, position) - local line_number = math.min(position.line, snapshot.line_count) - local view = text_topology.new(snapshot, encoding) - return view:normalize_endpoint(line_number, position.byte_column) -end - -function MemoryHost:_apply_pending_delete(origin, destination, descriptor) - if self._pending_operator ~= "delete" and self._pending_operator ~= "d" then - return false - end - - local view = text_topology.new(self._text, self._encoding) - local origin_index = character_boundary(view, origin) - local destination_index = character_boundary(view, destination) - local start_line - local start_index - local finish_line - local finish_index - local final_cursor - - if descriptor.direction == domain.Direction.FORWARD then - start_line = origin.line - start_index = origin_index - finish_line = destination.line - finish_index = destination_index + (self._operator_inclusive and 1 or 0) - final_cursor = origin - elseif descriptor.family == domain.Family.FIND then - start_line = destination.line - start_index = destination_index + 1 - finish_line = origin.line - finish_index = origin_index + 1 - final_cursor = destination - else - start_line = destination.line - start_index = destination_index - finish_line = origin.line - finish_index = origin_index - final_cursor = view:predecessor(destination) or destination - end - - self._text = delete_character_range( - self._text, - start_line, - start_index, - finish_line, - finish_index - ) - self._cursor = normalized_cursor(self._text, self._encoding, final_cursor) - self:_record("apply_operator", { - operator = self._pending_operator, - descriptor = descriptor.value, - origin = origin, - endpoint = destination, - position = self._cursor, - }) - return true -end - -function MemoryHost:apply_cursor(position, motion) - position = domain.Position.coerce(position) - local previous = self._cursor - local descriptor = motion_descriptor(motion) - self._cursor = position - self:_record("apply_cursor", { - position = position, - descriptor = descriptor and descriptor.value or nil, - }) - if descriptor ~= nil then - local origin = type(motion) == "table" and motion.origin or previous - self:_apply_pending_delete(domain.Position.coerce(origin), position, descriptor) - end - self:_emit_movement_event(previous) -end - -function MemoryHost:apply_selection(position, kind) - local previous = self._cursor - local next_selection - if domain.Selection.is(position) then - next_selection = position - position = next_selection.focus - else - position = domain.Position.coerce(position) - if kind == nil then - kind = self._selection.kind - end - kind = domain.SelectionKind.from_string(kind) - if kind == domain.SelectionKind.NONE then - error("selection movement requires a Visual selection kind", 2) - end - local anchor = self._selection.active and self._selection.anchor or previous - next_selection = domain.Selection.active( - kind, - anchor, - position, - self._selection.option - ) - end - - self._selection = next_selection - self._cursor = position - self:_record("apply_selection", { - position = position, - kind = next_selection.kind.value, - }) - self:_emit_movement_event(previous) -end - -function MemoryHost:set_operator_inclusive(enabled) - if type(enabled) ~= "boolean" then - error("operator inclusivity must be a Boolean", 2) - end - self._operator_inclusive = enabled - self:_record("set_operator_inclusive", { enabled = enabled }) -end - -function MemoryHost:operator_inclusive() - return self._operator_inclusive -end - -function MemoryHost:read_input() - local packet = self._input_packets[self._input_index] - if packet == nil then - error("in-memory input queue is empty", 2) - end - self._input_index = self._input_index + 1 - self:_record("read_input", { packet = packet:to_table() }) - if packet.kind == domain.InputPacketKind.ERROR then - error(packet.message, 0) - end - return packet -end - -function MemoryHost:open_fold(position) - position = position and domain.Position.coerce(position) or self._cursor - local closed_levels = self._fold_state.closed_levels - if closed_levels == 0 then - self:_record("open_fold", { position = position, opened = false }) - return false - end - self:_record("open_fold", { - position = position, - fold_level = closed_levels, - opened = true, - }) - self._fold_state = domain.FoldState.new( - self._fold_state:policies(), - closed_levels - 1 - ) - return true -end - -function MemoryHost:show_prompt(text) - if type(text) ~= "string" then - error("prompt must be a string", 2) - end - self._prompts[#self._prompts + 1] = text - self:_record("show_prompt", { text = text }) -end - -function MemoryHost:prompts() - return list_copy(self._prompts) -end - -function MemoryHost:redraw(kind) - if kind ~= "screen" and kind ~= "full" and kind ~= "suppressed" then - error("redraw kind must be screen, full, or suppressed", 2) - end - self._redraws[#self._redraws + 1] = kind - self:_record("redraw", { kind = kind }) -end - -function MemoryHost:redraws() - return list_copy(self._redraws) -end - -function MemoryHost:emit_diagnostic(level, text) - if level ~= "error" and level ~= "warning" and level ~= "info" then - error("diagnostic level must be error, warning, or info", 2) - end - if type(text) ~= "string" or text == "" then - error("diagnostic text must be a nonempty string", 2) - end - local diagnostic = { level = level, text = text } - self._diagnostics[#self._diagnostics + 1] = diagnostic - self:_record("emit_diagnostic", diagnostic) -end - -function MemoryHost:diagnostics() - return copy(self._diagnostics) -end - -function MemoryHost:read_highlight_group(name) - if type(name) ~= "string" or name == "" then - error("highlight group name must be a nonempty string", 2) - end - local definition = self._highlight_groups[name] - self:_record("read_highlight_group", { - name = name, - defined = definition ~= nil, - }) - return copy(definition) -end - -function MemoryHost:highlight_groups() - return copy(self._highlight_groups) -end - -function MemoryHost:define_highlight_group(name, definition, options) - if type(name) ~= "string" or name == "" then - error("highlight group name must be a nonempty string", 2) - end - if type(definition) ~= "table" then - error("highlight group definition must be a table", 2) - end - options = options or {} - if type(options) ~= "table" then - error("highlight group options must be a table", 2) - end - if options.default ~= nil and type(options.default) ~= "boolean" then - error("highlight group default option must be a Boolean", 2) - end - if options.force ~= nil and type(options.force) ~= "boolean" then - error("highlight group force option must be a Boolean", 2) - end - - local exists = self._highlight_groups[name] ~= nil - local applied = not (exists and options.default) - if applied then - self._highlight_groups[name] = copy(definition) - end - self:_record("define_highlight_group", { - name = name, - definition = definition, - options = options, - applied = applied, - }) - return applied -end - -function MemoryHost:create_highlight(specification) - if type(specification) ~= "table" then - error("highlight specification must be a table", 2) - end - if type(specification.group) ~= "string" or specification.group == "" then - error("highlight group must be a nonempty string", 2) - end - local identity = specification.identity or self:_next_identity("highlight") - if self._highlights[identity] ~= nil then - error("highlight identity is already active", 2) - end - local stored = copy(specification) - stored.identity = identity - self._highlights[identity] = stored - self:_record("create_highlight", stored) - return identity -end - -function MemoryHost:remove_highlight(identity) - if type(identity) ~= "string" or identity == "" then - error("highlight identity must be a nonempty string", 2) - end - local removed = self._highlights[identity] ~= nil - self._highlights[identity] = nil - self:_record("remove_highlight", { identity = identity, removed = removed }) - return removed -end - -function MemoryHost:highlights() - return copy(self._highlights) -end - -function MemoryHost:supports_cursor_presentation() - self:_record("supports_cursor_presentation", { - supported = self._cursor_presentation_support, - }) - return self._cursor_presentation_support -end - -function MemoryHost:suppress_cursor_presentation() - if not self._cursor_presentation_support then - self:_record("suppress_cursor_presentation", { supported = false }) - return nil - end - local identity = self:_next_identity("cursor-presentation") - self._cursor_leases[identity] = copy(self._cursor_presentation) - local suppressed = copy(self._cursor_presentation) - suppressed.hidden = true - self._cursor_presentation = suppressed - self:_record("suppress_cursor_presentation", { - identity = identity, - supported = true, - }) - return identity -end - -function MemoryHost:restore_cursor_presentation(identity) - if identity == nil then - self:_record("restore_cursor_presentation", { restored = false }) - return false - end - local saved = self._cursor_leases[identity] - if saved == nil then - error("cursor presentation lease is inactive", 2) - end - self._cursor_presentation = saved - self._cursor_leases[identity] = nil - self:_record("restore_cursor_presentation", { - identity = identity, - restored = true, - }) - return true -end - -function MemoryHost:cursor_presentation() - return copy(self._cursor_presentation) -end - -function MemoryHost:supports_timers() - self:_record("supports_timers", { supported = self._timer_support }) - return self._timer_support -end - -function MemoryHost:start_timer(delay_ms, callback) - if not is_integer(delay_ms) or delay_ms < 0 then - error("timer delay must be a nonnegative integer", 2) - end - if type(callback) ~= "function" then - error("timer callback must be a function", 2) - end - if not self._timer_support then - self:_record("start_timer", { delay_ms = delay_ms, supported = false }) - return nil - end - local identity = self:_next_identity("timer") - self._timers[identity] = { - identity = identity, - delay_ms = delay_ms, - callback = callback, - active = true, - } - self:_record("start_timer", { - identity = identity, - delay_ms = delay_ms, - supported = true, - }) - return identity -end - -function MemoryHost:stop_timer(identity) - if type(identity) ~= "string" or identity == "" then - error("timer identity must be a nonempty string", 2) - end - local timer = self._timers[identity] - local stopped = timer ~= nil and timer.active - if timer ~= nil then - timer.active = false - end - self:_record("stop_timer", { identity = identity, stopped = stopped }) - return stopped -end - -function MemoryHost:fire_timer(identity) - local timer = self._timers[identity] - if timer == nil then - error("timer identity is unknown", 2) - end - if not timer.active then - self:_record("ignore_timer", { identity = identity }) - return false - end - timer.active = false - self:_record("fire_timer", { identity = identity }) - timer.callback(identity) - return true -end - -function MemoryHost:timers() - local result = {} - for identity, timer in pairs(self._timers) do - result[identity] = { - identity = identity, - delay_ms = timer.delay_ms, - active = timer.active, - } - end - return result -end - -function MemoryHost:register_events(event_names, callback, options) - local names, name_set = normalize_event_names(event_names) - if type(callback) ~= "function" then - error("event callback must be a function", 2) - end - local identity = self:_next_identity("event-registration") - self._event_registrations[identity] = { - identity = identity, - names = names, - name_set = name_set, - callback = callback, - options = copy(options or {}), - active = true, - } - self._event_registration_order[#self._event_registration_order + 1] = identity - self:_record("register_events", { - identity = identity, - names = names, - options = options or {}, - }) - return identity -end - -function MemoryHost:remove_event_registration(identity) - local registration = self._event_registrations[identity] - local removed = registration ~= nil and registration.active - if registration ~= nil then - registration.active = false - end - self:_record("remove_event_registration", { - identity = identity, - removed = removed, - }) - return removed -end - -function MemoryHost:_deliver_event_now(name, payload) - self:_record("event", { name = name, payload = payload }) - local order = list_copy(self._event_registration_order) - local event_buffer = payload.buffer or self._buffer - for _, identity in ipairs(order) do - local registration = self._event_registrations[identity] - local registration_buffer = registration.options.buffer - if registration.active - and registration.name_set[name] - and (registration_buffer == nil or registration_buffer == event_buffer) - then - registration.callback(name, payload) - end - end -end - -function MemoryHost:deliver_event(name, payload) - if type(name) ~= "string" or name == "" then - error("event name must be a nonempty string", 2) - end - payload = copy(payload or {}) - local queued = self._event_queue:is_transition_active() - self:_record(queued and "queue_event" or "deliver_event", { - name = name, - payload = payload, - }) - return self._event_queue:emit(name, payload) -end - -function MemoryHost:begin_action_transition() - local token = self._event_queue:begin_transition() - self:_record("begin_action_transition", { identity = token }) - return token -end - -function MemoryHost:commit_action_transition(token) - self:_record("commit_action_transition", { identity = token }) - self._event_queue:commit_transition(token) -end - -function MemoryHost:pending_event_count() - return self._event_queue:pending_count() -end - -function MemoryHost:event_registrations() - local result = {} - for identity, registration in pairs(self._event_registrations) do - result[identity] = { - identity = identity, - names = list_copy(registration.names), - options = copy(registration.options), - active = registration.active, - } - end - return result -end - -function MemoryHost:register_action(name, callback) - if type(name) ~= "string" or name == "" then - error("action name must be a nonempty string", 2) - end - if type(callback) ~= "function" then - error("action callback must be a function", 2) - end - if self._actions[name] ~= nil then - error("action is already registered", 2) - end - self._actions[name] = callback - self:_record("register_action", { name = name }) - return name -end - -function MemoryHost:invoke_action(name, ...) - local callback = self._actions[name] - if callback == nil then - error("action is not registered", 2) - end - local arguments = { ... } - local argument_count = select("#", ...) - local token = self:begin_action_transition() - local results = { - pcall(function() - return callback(unpack_values(arguments, 1, argument_count)) - end), - } - self:commit_action_transition(token) - local succeeded = table.remove(results, 1) - if not succeeded then - error(results[1], 0) - end - return unpack_values(results) -end - -function MemoryHost:register_mapping(modes, lhs, action, options) - modes = normalize_modes(modes) - if type(lhs) ~= "string" or lhs == "" then - error("mapping lhs must be a nonempty string", 2) - end - if type(action) ~= "string" and type(action) ~= "function" then - error("mapping action must be an action name or function", 2) - end - local identity = self:_next_identity("mapping") - self._mappings[identity] = { - identity = identity, - modes = modes, - lhs = lhs, - action = action, - options = copy(options or {}), - } - self:_record("register_mapping", { - identity = identity, - modes = modes, - lhs = lhs, - action = type(action) == "string" and action or "<function>", - options = options or {}, - }) - return identity -end - -function MemoryHost:mappings() - return copy(self._mappings) -end - -function MemoryHost:register_dot_repeat(payload, callback) - if not domain.DotPayload.is(payload) then - error("dot-repeat payload must be a DotPayload", 2) - end - if callback ~= nil and type(callback) ~= "function" then - error("dot-repeat callback must be a function", 2) - end - self._dot_repeat = { - payload = payload, - callback = callback, - operator = self._pending_operator, - mode = self._mode, - } - self:_record("register_dot_repeat", { payload = payload:to_table() }) - return payload -end - -function MemoryHost:dot_repeat_payload() - return self._dot_repeat and self._dot_repeat.payload or nil -end - -function MemoryHost:replay_dot(count) - if self._dot_repeat == nil or self._dot_repeat.callback == nil then - error("dot repeat is not executable", 2) - end - self._pending_operator = self._dot_repeat.operator - self._mode = self._dot_repeat.mode - return self._dot_repeat.callback(self._dot_repeat.payload, domain.Count.new(count)) -end - -return M diff --git a/lua/clever_f/text_topology.lua b/lua/clever_f/text_topology.lua deleted file mode 100644 index db4ea3b..0000000 --- a/lua/clever_f/text_topology.lua +++ /dev/null @@ -1,1031 +0,0 @@ -local domain = require("clever_f.domain") - -local M = {} -local TextView = {} -local MatchStartBounds = {} -M.TextView = TextView -M.MatchStartBounds = MatchStartBounds - -local view_records = setmetatable({}, { __mode = "k" }) -local bounds_records = setmetatable({}, { __mode = "k" }) - -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 require_nonempty_string(value, name) - if type(value) ~= "string" or value == "" then - fail((name or "value") .. " must be a nonempty string", 2) - end - return value -end - -local function canonical_encoding(encoding) - encoding = require_nonempty_string(encoding, "effective encoding"):lower() - encoding = encoding:gsub("_", "-") - - local aliases = { - ["utf8"] = "utf-8", - ["cp-932"] = "cp932", - ["932"] = "cp932", - ["windows-31j"] = "cp932", - ["eucjp"] = "euc-jp", - ["ujis"] = "euc-jp", - ["unix-jis"] = "euc-jp", - } - return aliases[encoding] or encoding -end - -M.normalize_encoding = canonical_encoding - -local function utf8_character_length(text, offset) - local first = text:byte(offset) - if first == nil then - return nil - end - if first < 0x80 then - return 1 - end - - local length - local minimum - if first >= 0xc2 and first <= 0xdf then - length = 2 - minimum = 0x80 - elseif first >= 0xe0 and first <= 0xef then - length = 3 - minimum = 0x800 - elseif first >= 0xf0 and first <= 0xf4 then - length = 4 - minimum = 0x10000 - else - fail("text contains an invalid UTF-8 character", 3) - end - - if offset + length - 1 > #text then - fail("text contains an incomplete UTF-8 character", 3) - end - - local codepoint = first % (2 ^ (8 - length - 1)) - for index = offset + 1, offset + length - 1 do - local byte = text:byte(index) - if byte < 0x80 or byte > 0xbf then - fail("text contains an invalid UTF-8 character", 3) - end - codepoint = codepoint * 0x40 + (byte - 0x80) - end - - if codepoint < minimum - or codepoint > 0x10ffff - or (codepoint >= 0xd800 and codepoint <= 0xdfff) - then - fail("text contains an invalid UTF-8 character", 3) - end - return length -end - -local function split_utf8_codepoints(text) - local characters = {} - local offset = 1 - while offset <= #text do - local length = utf8_character_length(text, offset) - characters[#characters + 1] = text:sub(offset, offset + length - 1) - offset = offset + length - end - return characters -end - -local function nvim_split_segment(segment, result) - if segment == "" then - return - end - if segment:find("[\128-\255]") == nil then - for index = 1, #segment do - result[#result + 1] = segment:sub(index, index) - end - return - end - - local offset = 0 - while offset < #segment do - local match = vim.fn.matchstrpos(segment, "\\m.", offset) - local character = match[1] - local first = match[2] - local following = match[3] - if first ~= offset or following <= first or character == "" then - fail("Nvim could not index an editor character", 3) - end - result[#result + 1] = character - offset = following - end -end - -local function default_split_editor_characters(text) - local runtime = rawget(_G, "vim") - if type(runtime) ~= "table" - or type(runtime.fn) ~= "table" - or type(runtime.fn.strchars) ~= "function" - or type(runtime.fn.byteidx) ~= "function" - then - return split_utf8_codepoints(text) - end - - local result = {} - local offset = 1 - while offset <= #text do - local nul = text:find("\0", offset, true) - local last = nul and (nul - 1) or #text - nvim_split_segment(text:sub(offset, last), result) - if nul == nil then - break - end - result[#result + 1] = "\0" - offset = nul + 1 - end - return result -end - -local function default_encode(text, encoding) - if encoding == "utf-8" then - return text - end - - local runtime = rawget(_G, "vim") - if type(runtime) ~= "table" or type(runtime.iconv) ~= "function" then - fail("text encoding conversion requires Nvim or an encoder", 3) - end - - local ok, encoded = pcall(runtime.iconv, text, "utf-8", encoding) - if not ok or encoded == nil then - fail("text could not be converted to " .. encoding, 3) - end - return encoded -end - -local function require_character_list(characters) - 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 not is_integer(key) or key < 1 or key > #characters then - fail("editor character splitter must return a list", 3) - end - if type(character) ~= "string" or character == "" then - fail("editor character splitter must return nonempty strings", 3) - end - result[key] = character - item_count = item_count + 1 - end - if item_count ~= #characters then - fail("editor character splitter must return a list", 3) - end - return result -end - -function M.split_editor_characters(text) - if type(text) ~= "string" then - fail("text to split must be a string", 2) - end - return require_character_list(default_split_editor_characters(text)) -end - -local function snapshot_value(text) - if domain.TextSnapshot.is(text) then - return text - end - if type(text) == "table" and text.lines ~= nil then - text = text.lines - end - return domain.TextSnapshot.new(text) -end - -local function require_options(options) - if options == nil then - return {} - end - if type(options) == "function" then - return { encoder = options } - end - if type(options) ~= "table" then - fail("TextView options must be a table", 2) - end - return options -end - -local function selected_function(options, primary, alternate, fallback) - local value = options[primary] - if value == nil and alternate ~= nil then - value = options[alternate] - end - if value == nil then - return fallback - end - if type(value) ~= "function" then - fail("TextView " .. primary .. " must be a function", 3) - end - return value -end - -local function index_line(text, encoding, splitter, encoder) - if text:find("\n", 1, true) ~= nil then - fail("a text snapshot line must not contain a newline", 3) - end - - local characters = require_character_list(splitter(text)) - if table.concat(characters) ~= text then - fail("editor character splitter must preserve the complete line", 3) - end - - local entries = {} - local starts = {} - local by_start = {} - local encoded_parts = {} - local next_column = 1 - - for index, character in ipairs(characters) do - local encoded = encoder(character, encoding) - if type(encoded) ~= "string" or encoded == "" then - fail("TextView encoder must return a nonempty byte string", 3) - end - - local byte_length = #encoded - local entry = { - character = character, - encoded = encoded, - byte_start = next_column, - byte_end = next_column + byte_length - 1, - byte_length = byte_length, - } - entries[index] = entry - starts[index] = next_column - by_start[next_column] = index - encoded_parts[index] = encoded - next_column = next_column + byte_length - end - - return { - text = text, - encoded = table.concat(encoded_parts), - entries = entries, - starts = starts, - by_start = by_start, - byte_length = next_column - 1, - character_count = #entries, - } -end - -local text_view_metatable = { - __index = function(view, key) - local method = TextView[key] - if method ~= nil then - return method - end - - local record = view_records[view] - if key == "encoding" or key == "effective_encoding" then - return record.encoding - end - if key == "requested_encoding" then - return record.requested_encoding - end - if key == "line_count" then - return record.snapshot.line_count - end - return nil - end, - __newindex = function() - fail("TextView values are immutable", 2) - end, - __tostring = function(view) - local record = view_records[view] - return "text-view:" .. record.encoding .. ":" .. tostring(record.snapshot.line_count) - end, - __metatable = "clever_f.text_topology.TextView", -} - -function TextView.new(text, effective_encoding, options) - if TextView.is(text) and effective_encoding == nil and options == nil then - return text - end - - local snapshot = snapshot_value(text) - local requested_encoding = require_nonempty_string( - effective_encoding, - "effective encoding" - ) - local encoding = canonical_encoding(requested_encoding) - options = require_options(options) - local splitter = selected_function( - options, - "splitter", - "split_editor_characters", - default_split_editor_characters - ) - local encoder = selected_function(options, "encoder", "encode", default_encode) - - local view = setmetatable({}, text_view_metatable) - view_records[view] = { - snapshot = snapshot, - requested_encoding = requested_encoding, - encoding = encoding, - splitter = splitter, - encoder = encoder, - lines = {}, - } - return view -end - -function TextView.from_host(host, options) - if type(host) ~= "table" - or type(host.read_text) ~= "function" - or type(host.read_encoding) ~= "function" - then - fail("TextView host must provide read_text and read_encoding", 2) - end - local text = host:read_text() - local encoding = host:read_encoding() - return TextView.new(text, encoding, options) -end - -function TextView.is(value) - return type(value) == "table" and view_records[value] ~= nil -end - -local function view_record(view) - if not TextView.is(view) then - fail("value must be a TextView", 3) - end - return view_records[view] -end - -local function line_record(view, line_number) - local record = view_record(view) - if not is_integer(line_number) - or line_number < 1 - or line_number > record.snapshot.line_count - then - fail("line_number must identify a line in the TextView", 3) - end - local line = record.lines[line_number] - if line == nil then - line = index_line( - record.snapshot:line(line_number), - record.encoding, - record.splitter, - record.encoder - ) - record.lines[line_number] = line - end - return line -end - -local function require_character_index(line, character_index) - if not is_integer(character_index) - or character_index < 1 - or character_index > line.character_count - then - fail("character_index must identify an editor character", 3) - end - return character_index -end - -local function require_byte_column(byte_column) - if not is_integer(byte_column) or byte_column < 1 then - fail("byte_column must be a positive one-based integer", 3) - end - return byte_column -end - -local function position_arguments(position_or_line, byte_column, name) - if byte_column == nil then - local position = domain.Position.coerce(position_or_line) - return position.line, position.byte_column - end - if not is_integer(position_or_line) or position_or_line < 1 then - fail((name or "line_number") .. " must be a positive integer", 3) - end - return position_or_line, require_byte_column(byte_column) -end - -function TextView:text_snapshot() - return view_record(self).snapshot -end - -function TextView:line_text(line_number) - return line_record(self, line_number).text -end - -function TextView:line_encoded_text(line_number) - return line_record(self, line_number).encoded -end - -function TextView:text_suffix(position) - position = domain.Position.coerce(position) - local record = view_record(self) - local line = line_record(self, position.line) - local character_index = self:character_index_for_byte_column( - position.line, - position.byte_column - ) - local parts = {} - - for index = character_index, line.character_count do - parts[#parts + 1] = line.entries[index].character - end - for line_number = position.line + 1, record.snapshot.line_count do - parts[#parts + 1] = "\n" - parts[#parts + 1] = record.snapshot:line(line_number) - end - return table.concat(parts) -end - -function TextView:line_byte_length(line_number) - return line_record(self, line_number).byte_length -end - -function TextView:line_character_count(line_number) - return line_record(self, line_number).character_count -end - -function TextView:line_is_empty(line_number) - return self:line_character_count(line_number) == 0 -end - -function TextView:character_at_index(line_number, character_index) - local line = line_record(self, line_number) - require_character_index(line, character_index) - return line.entries[character_index].character -end - -function TextView:encoded_character_at_index(line_number, character_index) - local line = line_record(self, line_number) - require_character_index(line, character_index) - return line.entries[character_index].encoded -end - -function TextView:byte_column_for_character_index(line_number, character_index) - local line = line_record(self, line_number) - require_character_index(line, character_index) - return line.starts[character_index] -end - -function TextView:position_for_character_index(line_number, character_index) - return domain.Position.new( - line_number, - self:byte_column_for_character_index(line_number, character_index) - ) -end - -function TextView:try_character_index_for_byte_column(line_number, byte_column) - local line = line_record(self, line_number) - require_byte_column(byte_column) - return line.by_start[byte_column] -end - -function TextView:character_index_for_byte_column(line_number, byte_column) - local line = line_record(self, line_number) - require_byte_column(byte_column) - local character_index = line.by_start[byte_column] - if character_index == nil then - if byte_column <= line.byte_length then - fail("byte_column points inside an editor character", 2) - end - fail("byte_column does not identify an editor character", 2) - end - return character_index -end - -function TextView:character_index_for_position(position) - position = domain.Position.coerce(position) - return self:character_index_for_byte_column(position.line, position.byte_column) -end - -local function copy_span(line_number, character_index, entry) - local position = domain.Position.new(line_number, entry.byte_start) - return { - line = line_number, - character_index = character_index, - character = entry.character, - encoded = entry.encoded, - position = position, - byte_column = entry.byte_start, - byte_start = entry.byte_start, - byte_end = entry.byte_end, - start_byte_column = entry.byte_start, - end_byte_column = entry.byte_end, - byte_length = entry.byte_length, - } -end - -function TextView:byte_span_for_character_index(line_number, character_index) - local line = line_record(self, line_number) - require_character_index(line, character_index) - return copy_span(line_number, character_index, line.entries[character_index]) -end - -function TextView:byte_span_at(position_or_line, byte_column) - local line_number, column = position_arguments(position_or_line, byte_column) - local character_index = self:character_index_for_byte_column(line_number, column) - return self:byte_span_for_character_index(line_number, character_index) -end - -function TextView:character_at(position_or_line, byte_column) - local line_number, column = position_arguments(position_or_line, byte_column) - local character_index = self:character_index_for_byte_column(line_number, column) - return self:character_at_index(line_number, character_index) -end - -function TextView:is_character_start(position_or_line, byte_column) - local line_number, column = position_arguments(position_or_line, byte_column) - local record = view_record(self) - if line_number > record.snapshot.line_count then - return false - end - return line_record(self, line_number).by_start[column] ~= nil -end - -function TextView:is_valid_cursor_position(position_or_line, byte_column) - local line_number, column = position_arguments(position_or_line, byte_column) - local record = view_record(self) - if line_number > record.snapshot.line_count then - return false - end - local line = line_record(self, line_number) - if line.character_count == 0 then - return column == 1 - end - return line.by_start[column] ~= nil -end - -local function containing_character_index(line, byte_column) - for index = 1, line.character_count do - local entry = line.entries[index] - if byte_column >= entry.byte_start and byte_column <= entry.byte_end then - return index - end - end - return nil -end - -function TextView:normalize_endpoint(position_or_line, byte_column) - local line_number, column = position_arguments(position_or_line, byte_column) - local line = line_record(self, line_number) - if line.character_count == 0 then - return domain.Position.new(line_number, 1) - end - - if column > line.byte_length then - return self:position_for_character_index(line_number, line.character_count) - end - - local character_index = line.by_start[column] - or containing_character_index(line, column) - return self:position_for_character_index(line_number, character_index) -end - -local function first_cursor_position(view, line_number) - local line = line_record(view, line_number) - if line.character_count == 0 then - return domain.Position.new(line_number, 1) - end - return view:position_for_character_index(line_number, 1) -end - -local function last_cursor_position(view, line_number) - local line = line_record(view, line_number) - if line.character_count == 0 then - return domain.Position.new(line_number, 1) - end - return view:position_for_character_index(line_number, line.character_count) -end - -function TextView:first_cursor_position(line_number) - return first_cursor_position(self, line_number) -end - -function TextView:last_cursor_position(line_number) - return last_cursor_position(self, line_number) -end - -function TextView:predecessor(position) - position = domain.Position.coerce(position) - local line = line_record(self, position.line) - - if line.character_count > 0 then - local character_index = self:character_index_for_byte_column( - position.line, - position.byte_column - ) - if character_index > 1 then - return self:position_for_character_index(position.line, character_index - 1) - end - elseif position.byte_column ~= 1 then - fail("an empty line cursor position must use byte column one", 2) - end - - if position.line == 1 then - return nil - end - return last_cursor_position(self, position.line - 1) -end - -function TextView:successor(position) - position = domain.Position.coerce(position) - local record = view_record(self) - local line = line_record(self, position.line) - - if line.character_count > 0 then - local character_index = self:character_index_for_byte_column( - position.line, - position.byte_column - ) - if character_index < line.character_count then - return self:position_for_character_index(position.line, character_index + 1) - end - elseif position.byte_column ~= 1 then - fail("an empty line cursor position must use byte column one", 2) - end - - if position.line == record.snapshot.line_count then - return nil - end - return first_cursor_position(self, position.line + 1) -end - -local bounds_metatable = { - __index = function(bounds, key) - local method = MatchStartBounds[key] - if method ~= nil then - return method - end - return bounds_records[bounds][key] - end, - __newindex = function() - fail("MatchStartBounds values are immutable", 2) - end, - __tostring = function(bounds) - local record = bounds_records[bounds] - if record.empty then - return "match-start-bounds:empty" - end - return "match-start-bounds:" .. tostring(record.first) .. ":" .. tostring(record.last) - end, - __metatable = "clever_f.text_topology.MatchStartBounds", -} - -local function new_bounds(scope, first_line, last_line, first, last) - local bounds = setmetatable({}, bounds_metatable) - bounds_records[bounds] = { - scope = scope, - first_line = first_line, - last_line = last_line, - first = first, - last = last, - start = first, - finish = last, - empty = first == nil, - } - return bounds -end - -function MatchStartBounds.is(value) - return type(value) == "table" and bounds_records[value] ~= nil -end - -function MatchStartBounds:is_empty() - return bounds_records[self].empty -end - -function MatchStartBounds:contains(position) - position = domain.Position.coerce(position) - local record = bounds_records[self] - if record.empty then - return false - end - return domain.Position.compare(position, record.first) >= 0 - and domain.Position.compare(position, record.last) <= 0 -end - -function MatchStartBounds:to_table() - local record = bounds_records[self] - return { - scope = record.scope.value, - first_line = record.first_line, - last_line = record.last_line, - first = record.first and record.first:to_table() or nil, - last = record.last and record.last:to_table() or nil, - empty = record.empty, - } -end - -function TextView:line_match_start_bounds(line_number) - local line = line_record(self, line_number) - local first - local last - if line.character_count > 0 then - first = self:position_for_character_index(line_number, 1) - last = self:position_for_character_index(line_number, line.character_count) - end - return new_bounds( - domain.SearchScope.CURRENT_LINE, - line_number, - line_number, - first, - last - ) -end - -function TextView:buffer_match_start_bounds() - local record = view_record(self) - local first - local last - - for line_number = 1, record.snapshot.line_count do - local line = line_record(self, line_number) - if line.character_count > 0 then - first = self:position_for_character_index(line_number, 1) - break - end - end - - for line_number = record.snapshot.line_count, 1, -1 do - local line = line_record(self, line_number) - if line.character_count > 0 then - last = self:position_for_character_index(line_number, line.character_count) - break - end - end - - return new_bounds( - domain.SearchScope.BUFFER, - 1, - record.snapshot.line_count, - first, - last - ) -end - -local function scope_value(scope) - if scope == nil then - return domain.SearchScope.BUFFER - end - if scope == "line" then - return domain.SearchScope.CURRENT_LINE - end - return domain.SearchScope.from_string(scope) -end - -function TextView:match_start_bounds(scope, origin) - if domain.Position.is(scope) - or type(scope) == "number" - or (type(scope) == "table" and scope.line ~= nil) - then - scope, origin = origin, scope - end - - scope = scope_value(scope) - if scope == domain.SearchScope.BUFFER then - return self:buffer_match_start_bounds() - end - - if origin == nil then - fail("current-line match bounds require an origin line", 2) - end - local line_number = type(origin) == "number" - and origin - or domain.Position.coerce(origin).line - return self:line_match_start_bounds(line_number) -end - -local function empty_iterator() - return nil -end - -local function iteration_endpoint(view, position, name) - position = domain.Position.coerce(position) - if not view:is_character_start(position) then - fail((name or "iterator endpoint") .. " must start an editor character", 3) - end - return position -end - -local function step_character(view, position, direction) - local record = view_record(view) - local line = line_record(view, position.line) - local character_index = line.by_start[position.byte_column] - - if direction == domain.Direction.FORWARD then - if character_index < line.character_count then - return view:position_for_character_index(position.line, character_index + 1) - end - for line_number = position.line + 1, record.snapshot.line_count do - if line_record(view, line_number).character_count > 0 then - return view:position_for_character_index(line_number, 1) - end - end - return nil - end - - if character_index > 1 then - return view:position_for_character_index(position.line, character_index - 1) - end - for line_number = position.line - 1, 1, -1 do - local previous_line = line_record(view, line_number) - if previous_line.character_count > 0 then - return view:position_for_character_index( - line_number, - previous_line.character_count - ) - end - end - return nil -end - -local function position_iterator(view, direction, start_position, boundary) - if start_position == nil then - return empty_iterator - end - - start_position = iteration_endpoint(view, start_position, "iterator start") - boundary = iteration_endpoint(view, boundary, "iterator boundary") - local comparison = domain.Position.compare(start_position, boundary) - if direction == domain.Direction.FORWARD and comparison > 0 then - fail("a forward iterator start must not follow its boundary", 3) - end - if direction == domain.Direction.BACKWARD and comparison < 0 then - fail("a backward iterator start must not precede its boundary", 3) - end - - local current = start_position - local finished = false - return function() - if finished then - return nil - end - - local position = current - local character_index = view:character_index_for_position(position) - local character = view:character_at_index(position.line, character_index) - local span = view:byte_span_for_character_index(position.line, character_index) - - if position == boundary then - finished = true - else - current = step_character(view, position, direction) - if current == nil then - fail("iterator reached the text boundary before its selected boundary", 2) - end - end - return position, character, span - end -end - -local function iteration_arguments(view, direction, first, second) - if MatchStartBounds.is(first) then - local record = bounds_records[first] - if record.empty then - return nil, nil - end - if direction == domain.Direction.FORWARD then - return record.first, record.last - end - return record.last, record.first - end - - if type(first) == "number" and second == nil then - local bounds = view:line_match_start_bounds(first) - return iteration_arguments(view, direction, bounds) - end - - if first == nil then - local bounds = view:buffer_match_start_bounds() - return iteration_arguments(view, direction, bounds) - end - - first = domain.Position.coerce(first) - if second ~= nil then - return first, domain.Position.coerce(second) - end - - local bounds = view:buffer_match_start_bounds() - if bounds.empty then - return nil, nil - end - return first, direction == domain.Direction.FORWARD and bounds.last or bounds.first -end - -function TextView:iterate(direction, first, second) - direction = domain.Direction.from_string(direction) - local start_position, boundary = iteration_arguments(self, direction, first, second) - return position_iterator(self, direction, start_position, boundary) -end - -function TextView:iter_forward(first, boundary) - return self:iterate(domain.Direction.FORWARD, first, boundary) -end - -function TextView:iter_backward(first, boundary) - return self:iterate(domain.Direction.BACKWARD, first, boundary) -end - -function TextView:iter_line_forward(line_number) - return self:iter_forward(self:line_match_start_bounds(line_number)) -end - -function TextView:iter_line_backward(line_number) - return self:iter_backward(self:line_match_start_bounds(line_number)) -end - -function TextView:iter_buffer_forward() - return self:iter_forward(self:buffer_match_start_bounds()) -end - -function TextView:iter_buffer_backward() - return self:iter_backward(self:buffer_match_start_bounds()) -end - -local function strict_scope_bounds(view, origin, scope_or_bounds) - if MatchStartBounds.is(scope_or_bounds) then - return scope_or_bounds - end - local scope = scope_value(scope_or_bounds) - return view:match_start_bounds(scope, origin) -end - -function TextView:iter_strict(origin, direction, scope_or_bounds) - origin = domain.Position.coerce(origin) - line_record(self, origin.line) - if not self:is_valid_cursor_position(origin) then - fail("strict iterator origin must be a valid editor cursor position", 2) - end - - direction = domain.Direction.from_string(direction) - local bounds = strict_scope_bounds(self, origin, scope_or_bounds) - local candidates = self:iterate(direction, bounds) - - return function() - while true do - local position, character, span = candidates() - if position == nil then - return nil - end - local comparison = domain.Position.compare(position, origin) - if (direction == domain.Direction.FORWARD and comparison > 0) - or (direction == domain.Direction.BACKWARD and comparison < 0) - then - return position, character, span - end - end - end -end - -function TextView:iter_strict_forward(origin, scope_or_bounds) - return self:iter_strict(origin, domain.Direction.FORWARD, scope_or_bounds) -end - -function TextView:iter_strict_backward(origin, scope_or_bounds) - return self:iter_strict(origin, domain.Direction.BACKWARD, scope_or_bounds) -end - -TextView.character_index_to_byte_column = TextView.byte_column_for_character_index -TextView.byte_column_to_character_index = TextView.character_index_for_byte_column -TextView.character_count = TextView.line_character_count -TextView.byte_length = TextView.line_byte_length -TextView.text_from = TextView.text_suffix -TextView.suffix_from = TextView.text_suffix -TextView.character_span = TextView.byte_span_for_character_index -TextView.predecessor_endpoint = TextView.predecessor -TextView.successor_endpoint = TextView.successor -TextView.normalize_boundary_endpoint = TextView.normalize_endpoint -TextView.bounds_for_line = TextView.line_match_start_bounds -TextView.bounds_for_buffer = TextView.buffer_match_start_bounds -TextView.iterate_forward = TextView.iter_forward -TextView.iterate_backward = TextView.iter_backward -TextView.forward = TextView.iter_forward -TextView.backward = TextView.iter_backward -TextView.strict_forward = TextView.iter_strict_forward -TextView.strict_backward = TextView.iter_strict_backward - -function M.new(text, effective_encoding, options) - return TextView.new(text, effective_encoding, options) -end - -function M.from_host(host, options) - return TextView.from_host(host, options) -end - -M.build = M.new -M.build_from_host = M.from_host -M.is = TextView.is - -return M |
