diff options
| author | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 18:44:32 +0200 |
|---|---|---|
| committer | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 18:44:32 +0200 |
| commit | c7b14ffb6d14969c9c41864e827f33f8e80fc24e (patch) | |
| tree | c7c0ac23dc93a4a8b75449d2be4d0e05c4f05f9e /lua/clever_f/host_adapter.lua | |
| parent | 9013636a57144e1f57c9339e7888d588430aae6a (diff) | |
Rename plugin to clever-tee
Diffstat (limited to 'lua/clever_f/host_adapter.lua')
| -rw-r--r-- | lua/clever_f/host_adapter.lua | 1165 |
1 files changed, 0 insertions, 1165 deletions
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 |
