diff options
| author | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 14:23:03 +0200 |
|---|---|---|
| committer | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 14:23:03 +0200 |
| commit | 970db02504aeaf5c392a1bcad2f148734ba6ea48 (patch) | |
| tree | b384b7e5e28e45d658112a6a8e1dcc71c3dbe374 | |
| parent | 5199ca2e6527ce83b80898c57a09e984beb1fb67 (diff) | |
Translate action outcomes
| -rw-r--r-- | lua/clever_f/host_adapter.lua | 108 | ||||
| -rw-r--r-- | tests/run.lua | 71 |
2 files changed, 179 insertions, 0 deletions
diff --git a/lua/clever_f/host_adapter.lua b/lua/clever_f/host_adapter.lua new file mode 100644 index 0000000..a0eec66 --- /dev/null +++ b/lua/clever_f/host_adapter.lua @@ -0,0 +1,108 @@ +local domain = require("clever_f.domain") + +local M = {} +local HostAdapter = {} +HostAdapter.__index = HostAdapter +M.HostAdapter = HostAdapter + +M.ActionEffect = { + NONE = "none", + ESCAPE = "escape", + ERROR = "error", +} + +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" and options.runtime ~= nil then + return options.runtime + 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) + adapter_records[adapter] = { + runtime = require_runtime(current_runtime(options)), + } + 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 + +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) + local runtime = self: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 nil + runtime.notify(text, levels and levels.ERROR or nil, { title = "clever-f" }) +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 + self:emit_action_error(outcome.diagnostic) + return M.ActionEffect.ERROR + end + return M.ActionEffect.NONE +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/tests/run.lua b/tests/run.lua index 87b6e73..8c3c771 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -14,6 +14,7 @@ local composition_root = require("clever_f.composition_root") local destination_engine = require("clever_f.destination_engine") local direct_preview_planner = require("clever_f.direct_preview_planner") local feedback_service = require("clever_f.feedback_service") +local host_adapter = require("clever_f.host_adapter") local case_policy = require("clever_f.case_policy") local policy = require("clever_f.policy") local repeat_resolver = require("clever_f.repeat_resolver") @@ -360,6 +361,76 @@ test("ActionOutcome and DotPayload retain resolved result data", function() same("problem", domain.ActionOutcome.error(destination, "problem").diagnostic) end) +test("Host adapter translates typed action outcomes to Nvim effects", function() + local effects = {} + local runtime = { + api = { + nvim_feedkeys = function(keys, mode, escape_csi) + effects[#effects + 1] = { + kind = "feedkeys", + keys = keys, + mode = mode, + escape_csi = escape_csi, + } + end, + }, + keycode = function(key) + same("<Esc>", key) + return string.char(27) + end, + log = { levels = { ERROR = 4 } }, + notify = function(text, level, options) + effects[#effects + 1] = { + kind = "diagnostic", + text = text, + level = level, + title = options.title, + } + end, + } + local adapter = host_adapter.new({ runtime = runtime }) + local position = domain.Position.new(1, 1) + local complete = domain.SearchOutcome.complete( + domain.Position.new(1, 2), + 1 + ) + local failed = domain.SearchOutcome.boundary_before_any(position) + local no_effect = { + domain.ActionOutcome.neutral(position), + domain.ActionOutcome.empty(position), + domain.ActionOutcome.from_search(complete, "f"), + domain.ActionOutcome.from_search(failed, "f"), + } + + for _, outcome in ipairs(no_effect) do + same( + host_adapter.ActionEffect.NONE, + adapter:translate_action_outcome(outcome) + ) + end + same(0, #effects) + + same( + host_adapter.ActionEffect.ESCAPE, + adapter:translate_action_outcome(domain.ActionOutcome.escape(position)) + ) + same("feedkeys", effects[1].kind) + same(string.char(27), effects[1].keys) + same("n", effects[1].mode) + falsy(effects[1].escape_csi) + + same( + host_adapter.ActionEffect.ERROR, + adapter:translate_action_outcome( + domain.ActionOutcome.error(position, "adapter error") + ) + ) + same("diagnostic", effects[2].kind) + same("adapter error", effects[2].text) + same(4, effects[2].level) + same("clever-f", effects[2].title) +end) + test("Capability contract reports every semantic method", function() local host = MemoryHost.new() same(host, capabilities.assert_implements(host)) |
