summaryrefslogtreecommitdiff
path: root/lua/clever_f
diff options
context:
space:
mode:
Diffstat (limited to 'lua/clever_f')
-rw-r--r--lua/clever_f/acquisition_service.lua927
-rw-r--r--lua/clever_f/action_facade.lua134
-rw-r--r--lua/clever_f/capabilities.lua163
-rw-r--r--lua/clever_f/case_policy.lua190
-rw-r--r--lua/clever_f/composition_root.lua232
-rw-r--r--lua/clever_f/destination_engine.lua201
-rw-r--r--lua/clever_f/direct_preview_planner.lua250
-rw-r--r--lua/clever_f/domain.lua1390
-rw-r--r--lua/clever_f/feedback_service.lua1003
-rw-r--r--lua/clever_f/host_adapter.lua1165
-rw-r--r--lua/clever_f/init.lua118
-rw-r--r--lua/clever_f/migemo_catalog.lua527
-rw-r--r--lua/clever_f/motion_executor.lua436
-rw-r--r--lua/clever_f/motion_plan.lua188
-rw-r--r--lua/clever_f/policy.lua488
-rw-r--r--lua/clever_f/repeat_resolver.lua333
-rw-r--r--lua/clever_f/sequence_coordinator.lua675
-rw-r--r--lua/clever_f/sequence_state.lua302
-rw-r--r--lua/clever_f/state_transitions.lua493
-rw-r--r--lua/clever_f/target_plan.lua541
-rw-r--r--lua/clever_f/testing/memory_host.lua1075
-rw-r--r--lua/clever_f/text_topology.lua1031
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