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)), next_identity = 1, highlights = {}, } 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 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 escape_key(runtime) if type(runtime.keycode) == "function" then return runtime.keycode("") end if type(runtime.api.nvim_replace_termcodes) == "function" then return runtime.api.nvim_replace_termcodes("", 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