From 7406476fa093b9af888e82d3cf5c1cc3b320a06f Mon Sep 17 00:00:00 2001 From: Jackson Moore Date: Fri, 4 Sep 2026 12:14:32 +0200 Subject: Finalize acquisition resource scope --- lua/clever_f/acquisition_service.lua | 56 +++++++++++++++++++++++++++++------- tests/run.lua | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/lua/clever_f/acquisition_service.lua b/lua/clever_f/acquisition_service.lua index a50e614..a19f8f8 100644 --- a/lua/clever_f/acquisition_service.lua +++ b/lua/clever_f/acquisition_service.lua @@ -695,10 +695,7 @@ local function direct_preview_settings(policy_service) 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 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( @@ -711,11 +708,11 @@ function AcquisitionService:acquire(descriptor, context, position, count, macro_ request.position, current_window(record) )) - if not request.macro_state.executing then + if interactive then record.host:redraw("screen") end end - if acquisition.mark_direct and not request.macro_state.executing then + 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, @@ -733,7 +730,7 @@ function AcquisitionService:acquire(descriptor, context, position, count, macro_ )) record.host:redraw("screen") end - if acquisition.show_prompt and not request.macro_state.executing then + if acquisition.show_prompt and interactive then record.host:show_prompt(M.PROMPT) scope:mark_prompt_shown() end @@ -742,7 +739,6 @@ function AcquisitionService:acquire(descriptor, context, position, count, macro_ scope:mark_input_completed() if M.is_escape(packet) then local outcome = scope:set_outcome(domain.ActionOutcome.escape(request.position)) - scope:release() return AcquisitionResult.new(request, { outcome = outcome }) end local target = scope:set_acquired_target(M.normalize_input_packet(packet)) @@ -765,7 +761,6 @@ function AcquisitionService:acquire(descriptor, context, position, count, macro_ local outcome = scope:set_outcome( domain.ActionOutcome.empty(request.position) ) - scope:release() return AcquisitionResult.new(request, { outcome = outcome, previous_input_trigger = trigger, @@ -836,7 +831,48 @@ function AcquisitionService:acquire(descriptor, context, position, count, macro_ missing_previous_input = scope.missing_previous_input, }) scope:mark_acquisition_completed() - scope:release() + 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 diff --git a/tests/run.lua b/tests/run.lua index 1df3d0e..4139544 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -5563,6 +5563,59 @@ test("Acquisition releases direct cursor and presentation resources", function() truthy(restore_index > removals[2].index) end) +test("Acquisition cleanup runs from a finally block", function() + local state = fresh_sequence_state() + local prior = { + hidden = false, + guicursor = "n:block", + } + local host = MemoryHost.new({ + buffer_lines = { "abc" }, + cursor_presentation = prior, + configuration = { + mark_cursor = true, + mark_direct = true, + mark_char = false, + show_prompt = true, + }, + input_packets = { { kind = "error", message = "input failed" } }, + }) + local service = acquisition_service.new(host) + + local result = service:acquire( + "f", + "n", + domain.Position.new(1, 1), + nil, + nil + ) + same(domain.ActionKind.ERROR, result.outcome.kind) + same("input failed", result.outcome.diagnostic) + falsy(service:last_temporary_scope().active) + same(0, #state:temporary_overlay_identities()) + same(0, map_size(host:highlights())) + same(prior.hidden, host:cursor_presentation().hidden) + same(prior.guicursor, host:cursor_presentation().guicursor) + same(1, #host:diagnostics()) + same("input failed", host:diagnostics()[1].text) + + local read_index + local remove_index + local restore_index + for index, operation in ipairs(host:operations()) do + if operation.operation == "read_input" then + read_index = index + elseif operation.operation == "remove_highlight" then + remove_index = index + elseif operation.operation == "restore_cursor_presentation" then + restore_index = index + end + end + truthy(remove_index > read_index) + truthy(restore_index > remove_index) + same(2, #host:redraws()) +end) + for _, item in ipairs(tests) do local ok, failure = xpcall(item.body, debug.traceback) if not ok then -- cgit v1.2.3