diff options
Diffstat (limited to 'lua/clever_tee/acquisition_service.lua')
| -rw-r--r-- | lua/clever_tee/acquisition_service.lua | 927 |
1 files changed, 927 insertions, 0 deletions
diff --git a/lua/clever_tee/acquisition_service.lua b/lua/clever_tee/acquisition_service.lua new file mode 100644 index 0000000..6e91159 --- /dev/null +++ b/lua/clever_tee/acquisition_service.lua @@ -0,0 +1,927 @@ +local domain = require("clever_tee.domain") +local direct_preview_planner = require("clever_tee.direct_preview_planner") +local feedback_service = require("clever_tee.feedback_service") +local motion_plan_factory = require("clever_tee.motion_plan") +local policy = require("clever_tee.policy") +local sequence_state = require("clever_tee.sequence_state") +local state_transitions = require("clever_tee.state_transitions") +local target_plan_factory = require("clever_tee.target_plan") +local text_topology = require("clever_tee.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-tee: " +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_tee.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_tee.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_tee.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-tee: 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 |
