diff options
| author | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 14:24:35 +0200 |
|---|---|---|
| committer | Jackson Moore <jacksonmoore@tuta.io> | 2026-09-04 14:24:35 +0200 |
| commit | 4cb3fb0f1532de3465962903a1697339ad3d6159 (patch) | |
| tree | 952c43a26dd8b114dfacf17ecaa09b2f9b31afbe | |
| parent | 970db02504aeaf5c392a1bcad2f148734ba6ea48 (diff) | |
Translate window-local highlights
| -rw-r--r-- | lua/clever_f/host_adapter.lua | 160 | ||||
| -rw-r--r-- | tests/run.lua | 90 |
2 files changed, 250 insertions, 0 deletions
diff --git a/lua/clever_f/host_adapter.lua b/lua/clever_f/host_adapter.lua index a0eec66..747d9b8 100644 --- a/lua/clever_f/host_adapter.lua +++ b/lua/clever_f/host_adapter.lua @@ -41,6 +41,8 @@ function HostAdapter.new(options) local adapter = setmetatable({}, HostAdapter) adapter_records[adapter] = { runtime = require_runtime(current_runtime(options)), + next_identity = 1, + highlights = {}, } return adapter end @@ -53,6 +55,164 @@ 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("<Esc>") diff --git a/tests/run.lua b/tests/run.lua index 8c3c771..64c12d6 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -361,6 +361,96 @@ test("ActionOutcome and DotPayload retain resolved result data", function() same("problem", domain.ActionOutcome.error(destination, "problem").diagnostic) end) +test("Host adapter materializes window-local highlight resources", function() + local groups = { Existing = { fg = 7 } } + local added = {} + local deleted = {} + local next_match = 1000 + local runtime = { + api = { + nvim_get_hl = function(_, options) + return vim.deepcopy(groups[options.name] or {}) + end, + nvim_set_hl = function(_, name, definition) + groups[name] = vim.deepcopy(definition) + end, + }, + fn = { + hlexists = function(name) + return groups[name] ~= nil and 1 or 0 + end, + matchaddpos = function(group, positions, priority, id, options) + local match_id = next_match + next_match = next_match + 1 + added[#added + 1] = { + group = group, + positions = positions, + priority = priority, + id = id, + window = options.window, + match_id = match_id, + } + return match_id + end, + matchdelete = function(match_id, window) + deleted[#deleted + 1] = { match_id = match_id, window = window } + return 0 + end, + }, + } + local adapter = host_adapter.new({ runtime = runtime }) + + same(7, adapter:read_highlight_group("Existing").fg) + same(nil, adapter:read_highlight_group("Missing")) + falsy(adapter:define_highlight_group( + "Existing", + { guifg = "red" }, + { default = true } + )) + truthy(adapter:define_highlight_group("CleverFDefaultLabel", { + guifg = "red", + guibg = "NONE", + gui = { bold = true, underline = true }, + ctermfg = "red", + ctermbg = "NONE", + cterm = { bold = true, underline = true }, + }, { default = true })) + same("red", groups.CleverFDefaultLabel.fg) + same("NONE", groups.CleverFDefaultLabel.bg) + truthy(groups.CleverFDefaultLabel.bold) + truthy(groups.CleverFDefaultLabel.underline) + truthy(groups.CleverFDefaultLabel.default) + + local cursor = adapter:create_highlight({ + group = "CleverFCursor", + window = 42, + position = domain.Position.new(3, 5), + priority = "high", + }) + local character = adapter:create_highlight({ + group = "CleverFChar", + window = 51, + positions = {}, + priority = "ordinary", + }) + same(2, #added) + list_same({ 3, 5 }, added[1].positions[1]) + same(100, added[1].priority) + same(42, added[1].window) + list_same({ 0 }, added[2].positions[1]) + same(10, added[2].priority) + same(51, added[2].window) + + truthy(adapter:remove_highlight(cursor)) + truthy(adapter:remove_highlight(character)) + falsy(adapter:remove_highlight(character)) + same(2, #deleted) + same(1000, deleted[1].match_id) + same(42, deleted[1].window) + same(1001, deleted[2].match_id) + same(51, deleted[2].window) +end) + test("Host adapter translates typed action outcomes to Nvim effects", function() local effects = {} local runtime = { |
