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 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 AcquisitionRequest = {} local AcquisitionService = {} local TemporaryResourceScope = {} AcquisitionService.__index = AcquisitionService M.AcquisitionRequest = AcquisitionRequest M.AcquisitionService = AcquisitionService M.TemporaryResourceScope = TemporaryResourceScope M.RepeatedDirection = { SAME = "same", } local request_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 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) 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, active = true, cursor_marker = nil, direct_marker = 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:release() local record = scope_records[self] if record == nil then fail("temporary resource scope is invalid", 2) end record.active = false 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" then fail("AcquisitionService transitions must begin acquisition", 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_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" then fail("AcquisitionService feedback must create cursor 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, 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 ), 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.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 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 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 acquisition = record.policy:sample_acquisition() if acquisition.mark_cursor then scope:set_cursor_marker(record.feedback:create_cursor_marker( request.position, current_window(record) )) if not request.macro_state.executing then record.host:redraw("screen") end end if acquisition.mark_direct and not request.macro_state.executing then local 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 )) record.host:redraw("screen") end return request 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