Skip to content

Instantly share code, notes, and snippets.

@rodhash
Created May 31, 2026 12:51
Show Gist options
  • Select an option

  • Save rodhash/3cdfe74733c002ad0e8303909707e033 to your computer and use it in GitHub Desktop.

Select an option

Save rodhash/3cdfe74733c002ad0e8303909707e033 to your computer and use it in GitHub Desktop.
local utils = require("oklch-color-picker.utils")
local downloader = require("oklch-color-picker.downloader")
local ra = require("oklch-color-picker.reusable_array")
local ra_new, ra_push, ra_clear = ra.new, ra.push, ra.clear
local find, sub, format = string.find, string.sub, string.format
local insert = table.insert
local pow, min, max, floor = math.pow, math.min, math.max, math.floor
local rshift, band, lshift, bor = bit.rshift, bit.band, bit.lshift, bit.bor
local nvim_buf_clear_namespace, nvim_buf_del_extmark, nvim_buf_set_extmark, nvim_set_hl, nvim_buf_get_extmarks =
vim.api.nvim_buf_clear_namespace,
vim.api.nvim_buf_del_extmark,
vim.api.nvim_buf_set_extmark,
vim.api.nvim_set_hl,
vim.api.nvim_buf_get_extmarks
---@class oklch.highlight
local M = {}
--- Default parser for highlighting
---@type fun(color: string, format: string|nil): number|nil
M.parse = nil
---@type oklch.FinalPatternList[]
local patterns = nil
---@type oklch.highlight.Opts
local opts
---@type number
local ns
---@type number
local gr
---@type fun(client: string): boolean
local lsp_enabled
local hl_group = {}
---@type table<string, boolean>
local ignore_ft = {}
local ft_patterns_cache = {}
--- @param opts_ oklch.highlight.Opts
--- @param patterns_ oklch.FinalPatternList[]
--- @param auto_download boolean
function M.setup(opts_, patterns_, auto_download)
opts = opts_
patterns = patterns_
ft_patterns_cache = {}
if M.make_set_extmark() then
utils.log("Invalid config.highlight.style, highlighting disabled", vim.log.levels.ERROR)
opts.enabled = false
return
end
ignore_ft = {}
for _, ft in ipairs(opts.ignore_ft) do
ignore_ft[ft] = true
end
hl_group = {
bold = opts.bold,
italic = opts.italic,
}
if type(opts.enabled_lsps) == "boolean" then
local value = opts.enabled_lsps --[[@as boolean]]
lsp_enabled = function()
return value
end
elseif type(opts.enabled_lsps) == "table" then
local enabled_lsps = {}
for _, lsp in
ipairs(opts.enabled_lsps --[=[@as string[]]=])
do
enabled_lsps[lsp] = true
end
lsp_enabled = function(client)
return enabled_lsps[client] == true
end
end
local on_downloaded = function(err)
if err then
utils.log(err, vim.log.levels.ERROR)
return
end
local parser = require("oklch-color-picker.parser").get_parser()
if parser == nil then
utils.log("Couldn't load parser library", vim.log.levels.ERROR)
return
end
M.parse = parser.parse
ns = vim.api.nvim_create_namespace("OklchColorPickerNamespace")
gr = vim.api.nvim_create_augroup("OklchColorPicker", {})
if not opts.enabled then
return
end
-- set to false for enable to work
opts.enabled = false
M.enable()
end
if auto_download then
downloader.ensure_parser_downloaded(vim.schedule_wrap(on_downloaded))
else
on_downloaded(nil)
end
end
---@param bufnr number
function M.clear_buf_hl(bufnr)
if not vim.api.nvim_buf_is_loaded(bufnr) then
return
end
pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns, 0, -1)
local buf_data = M.bufs[bufnr]
if buf_data == nil then
return
end
for _, lsp_ns in ipairs(buf_data.lsp_namespaces_list) do
pcall(vim.api.nvim_buf_clear_namespace, bufnr, lsp_ns, 0, -1)
end
end
--- @param bufnr number
function M.delete_buf_data(bufnr)
local buf_data = M.bufs[bufnr]
if buf_data then
buf_data.pending_timer:stop()
buf_data.pending_timer:close()
buf_data.pending_timer_lsp:stop()
buf_data.pending_timer_lsp:close()
end
M.bufs[bufnr] = nil
if not vim.api.nvim_buf_is_loaded(bufnr) then
return
end
pcall(vim.api.nvim_clear_autocmds, { buffer = bufnr, group = gr })
end
function M.disable()
if not opts or not opts.enabled then
return
end
opts.enabled = false
vim.api.nvim_clear_autocmds({ group = gr })
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
M.delete_buf_data(bufnr)
M.clear_buf_hl(bufnr)
end
M.bufs = {}
end
local warned_document_color = false
function M.enable()
if not M.parse or not opts or opts.enabled then
return
end
opts.enabled = true
M.clear_hl_cache()
M.update_emphasis_values()
vim.api.nvim_create_autocmd({ "FileType", "BufEnter" }, {
group = gr,
callback = function(data)
M.init_buf(data.buf)
end,
})
vim.api.nvim_create_autocmd("LspAttach", {
group = gr,
callback = function(data)
if opts.disable_builtin_lsp_colors and vim.lsp.document_color then
local ok, err = pcall(vim.lsp.document_color.enable, false, { bufnr = data.buf })
if not ok and not warned_document_color then
warned_document_color = true
utils.log(
"Failed to disable builtin LSP colors: " .. tostring(err) .. ". Please update to the latest Neovim nightly.",
vim.log.levels.WARN
)
end
end
local buf_data = M.get_buf_data(data.buf)
if buf_data == nil then
return
end
M.update_lsp(data.buf, buf_data)
end,
})
vim.api.nvim_create_autocmd("ColorScheme", {
group = gr,
callback = function()
if not opts.enabled then
return
end
M.disable()
M.enable()
end,
})
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
M.init_buf(bufnr)
end
end
---@return boolean -- true if enabled, false if disabled
function M.toggle()
if opts.enabled then
M.disable()
else
M.enable()
end
return opts.enabled
end
---@return boolean -- true if enabled, false if disabled
function M.is_enabled()
return opts.enabled
end
---@class BufData
---@field pending_updates { from_line: integer, to_line: integer }|nil
---@field prev_view { top: integer, bottom: integer }
---@field lsp_colors table<string, LspColor[]>
---@field lsp_in_flight boolean|nil
---@field lsp_queued boolean|nil
---@field lsp_namespaces table<string, integer>
---@field lsp_namespaces_list table<integer, integer>
---@field pending_timer uv.uv_timer_t
---@field pending_timer_lsp uv.uv_timer_t
--- @type { [integer]: BufData }
M.bufs = {}
--- Unattaching is very annoying, so just make sure we never attach twice
--- @type { [integer]: boolean}
M.buf_attached = {}
--- @param bufnr number
M.init_buf = function(bufnr)
if not vim.api.nvim_buf_is_loaded(bufnr) then
return
end
local ft = vim.api.nvim_get_option_value("filetype", { buf = bufnr })
if ignore_ft[ft] then
if M.bufs[bufnr] then
M.clear_buf_hl(bufnr)
M.delete_buf_data(bufnr)
end
return
end
if not M.buf_attached[bufnr] then
local attached = vim.api.nvim_buf_attach(bufnr, false, {
on_bytes = function(_, _, _, start_row, _, _, old_end_row, _, _, new_end_row, _, _)
if new_end_row < old_end_row then
-- We deleted some lines.
-- It's possible that we uncovered new unhighlighted colors from the bottom
-- of the view, so update the rest of the view.
M.update_lines(bufnr, start_row, 1e9, false)
else
M.update_lines(bufnr, start_row, start_row + new_end_row + 1, false)
end
end,
on_reload = function()
M.update_view(bufnr, true)
end,
on_detach = function()
M.delete_buf_data(bufnr)
M.buf_attached[bufnr] = nil
end,
})
M.buf_attached[bufnr] = attached
end
if M.bufs[bufnr] then
M.update_view(bufnr, true)
return
end
M.bufs[bufnr] = {
prev_view = { top = 0, bottom = 0 },
lsp_colors = {},
lsp_namespaces = {},
lsp_namespaces_list = {},
pending_timer = assert(vim.uv.new_timer()),
pending_timer_lsp = assert(vim.uv.new_timer()),
}
M.update_view(bufnr, true)
vim.api.nvim_create_autocmd({ "WinScrolled", "VimResized" }, {
group = gr,
buffer = bufnr,
callback = function()
M.update_view(bufnr)
end,
})
end
--- @param bufnr integer
--- @return BufData|nil
function M.get_buf_data(bufnr)
if not vim.api.nvim_buf_is_loaded(bufnr) then
return nil
end
return M.bufs[bufnr]
end
--- @param bufnr integer
--- @param force? boolean
function M.update_view(bufnr, force)
local buf_data = M.get_buf_data(bufnr)
if buf_data == nil then
return
end
local top, bottom = M.get_view(bufnr)
if force then
M.update_lines(bufnr, 0, 1e9, true)
buf_data.prev_view.top = top
buf_data.prev_view.bottom = bottom
return
end
if top == buf_data.prev_view.top and bottom == buf_data.prev_view.bottom then
return
elseif top < buf_data.prev_view.top and bottom <= buf_data.prev_view.bottom then
-- scrolled up
M.update_lines(bufnr, 0, buf_data.prev_view.top + 1, true)
elseif bottom > buf_data.prev_view.bottom and top >= buf_data.prev_view.top then
-- scrolled down
M.update_lines(bufnr, buf_data.prev_view.bottom, 1e9, true)
else
-- large jump
M.update_lines(bufnr, 0, 1e9, true)
end
buf_data.prev_view.top = top
buf_data.prev_view.bottom = bottom
end
local function get_view()
return {
vim.fn.line("w0") - 1,
-- return one extra line because it doesn't count it if it's wrapped
vim.fn.line("w$") + 1,
}
end
--- @param bufnr integer
--- @return integer, integer
function M.get_view(bufnr)
local v = vim.api.nvim_buf_call(bufnr, get_view)
return v[1], v[2]
end
M.perf_logging = false
M.lsp_perf_logging = false
---@param enabled? boolean
function M.set_perf_logging(enabled)
if enabled == nil then
enabled = true
end
M.perf_logging = enabled
end
---@param enabled? boolean
function M.set_lsp_perf_logging(enabled)
if enabled == nil then
enabled = true
end
M.lsp_perf_logging = enabled
end
local in_perf_log = false
--- @param bufnr integer
--- @param from_line integer
--- @param to_line integer
--- @param scroll boolean
function M.update_lines(bufnr, from_line, to_line, scroll)
if in_perf_log then
return
end
local buf_data = M.get_buf_data(bufnr)
if buf_data == nil then
return
end
if vim.api.nvim_get_option_value("buftype", { buf = bufnr }) == "terminal" then
return
end
local top, bottom = M.get_view(bufnr)
if buf_data.pending_updates == nil then
buf_data.pending_updates = {
from_line = max(from_line, top),
to_line = min(to_line, bottom),
}
else
buf_data.pending_updates.from_line = max(min(buf_data.pending_updates.from_line, from_line), top)
buf_data.pending_updates.to_line = min(max(buf_data.pending_updates.to_line, to_line), bottom)
end
local delay = assert(scroll and opts.scroll_delay or opts.edit_delay)
buf_data.pending_timer:stop()
buf_data.pending_timer:start(
delay,
0,
vim.schedule_wrap(function()
M.process_update(bufnr)
end)
)
-- The whole buffer is updated, so no need to run when scrolling.
if not scroll then
M.update_lsp(bufnr, buf_data)
end
end
---@param bufnr integer
---@param buf_data BufData
M.update_lsp = function(bufnr, buf_data)
buf_data.pending_timer_lsp:stop()
if buf_data.lsp_in_flight then
buf_data.lsp_queued = true
return
end
buf_data.pending_timer_lsp:start(
opts.lsp_delay,
0,
vim.schedule_wrap(function()
buf_data.lsp_in_flight = true
M.process_update_lsp(bufnr, function()
local buf_data = M.get_buf_data(bufnr)
if buf_data == nil then
return
end
buf_data.lsp_in_flight = false
-- We got more update requests while we were waiting for LSPs, so update again.
if buf_data.lsp_queued then
buf_data.lsp_queued = false
M.update_lsp(bufnr, buf_data)
end
end)
end)
)
end
---@param bufnr integer
function M.process_update(bufnr)
local buf_data = M.get_buf_data(bufnr)
if buf_data == nil or buf_data.pending_updates == nil then
return
end
local t = vim.uv.hrtime()
local from_line = buf_data.pending_updates.from_line --[[@as integer]]
local to_line = buf_data.pending_updates.to_line --[[@as integer]]
buf_data.pending_updates = nil
local lines = vim.api.nvim_buf_get_lines(bufnr, from_line, to_line, false)
for i, line in ipairs(lines) do
if #line > 1000 then
lines[i] = sub(line, 1, 1000)
end
end
local ft = vim.api.nvim_get_option_value("filetype", { buf = bufnr })
M.highlight_lines(bufnr, lines, from_line, ft, buf_data)
if M.perf_logging then
in_perf_log = true
local ms = (vim.uv.hrtime() - t) / 1000000
print(format("color highlighting took: %.3f ms, lines %d to %d in buf %d", ms, from_line, to_line, bufnr))
in_perf_log = false
end
end
local function to_linear(c)
if c <= 0.04045 then
return c / 12.92
else
return pow((c + 0.055) / 1.055, 2.4)
end
end
local linear_lookup = {}
for i = 0, 255 do
linear_lookup[i] = to_linear(i / 255)
end
--- Splits a combined 0xRRGGBB color into { r, g, b } (0-255).
---@param rgb integer
---@return [integer, integer, integer]
function M.rgb_unpack(rgb)
local r = rshift(rgb, 16)
local g = band(rshift(rgb, 8), 0xff)
local b = band(rgb, 0xff)
return { r, g, b }
end
--- Combines r, g, b (0-255) integer values to a combined color 0xRRGGBB.
--- Passing floats or numbers outside of 0-255 can result in weird outputs.
---@param r integer
---@param g integer
---@param b integer
---@return integer
function M.rgb_pack(r, g, b)
return bor(lshift(r, 16), lshift(g, 8), b)
end
--- Based on W3C guidelines
--- https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
--- @param color [number, number, number]
local function lightness(color)
return 0.2126 * linear_lookup[color[1]] + 0.7152 * linear_lookup[color[2]] + 0.0722 * linear_lookup[color[3]]
end
--- @param color [number, number, number]
--- @return boolean
local function is_light(color)
return lightness(color) > 0.179
end
local function square(a)
return a * a
end
local function color_distance(a, b)
local r_ = 0.5 * (a[1] + b[1])
return math.sqrt(
(2 + r_ / 256) * square(b[1] - a[1]) + 4 * square(b[2] - a[2]) + (2 + (255 - r_) / 256) * square(b[3] - a[3])
)
end
local function float_to_int8(color)
return floor((min(max(color, 0), 1) * 255) + 0.5)
end
local function get_hl(hl_name)
local hl = vim.api.nvim_get_hl(0, { name = hl_name, create = false })
local i = 0
while hl and hl.link and i < 50 do
hl = vim.api.nvim_get_hl(0, { name = hl.link, create = false })
i = i + 1
end
return hl
end
---@type [number, number, number]
local bg_color = { 0, 0, 0 }
local bg_color_is_light = false
local emphasis_threshold = 1.
local light_emphasis = 0
local dark_emphasis = 0
function M.update_emphasis_values()
local hl = get_hl("Normal")
if hl.bg then
bg_color = M.rgb_unpack(hl.bg)
else
bg_color = vim.api.nvim_get_option_value("background", {}) == "light" and { 255, 255, 255 } or { 0, 0, 0 }
end
bg_color_is_light = is_light(bg_color)
emphasis_threshold = opts.emphasis and opts.emphasis.threshold[bg_color_is_light and 2 or 1] or 1
dark_emphasis = opts.emphasis and opts.emphasis.amount[1] or 0
light_emphasis = opts.emphasis and opts.emphasis.amount[2] or 0
end
local hex_color_groups = {}
local n_hex_color_groups = 0
function M.clear_hl_cache()
hex_color_groups = {}
n_hex_color_groups = 0
end
--- @param rgb number
--- @return string | nil
local function compute_color_group(rgb)
local cached_group_name = hex_color_groups[rgb]
if cached_group_name ~= nil then
return cached_group_name
end
-- Implements the same fix as https://github.com/neovim/neovim/pull/39133.
-- Vim limits hl groups to 19999, so limit our groups to half that and leave
-- the other half for other plugins/features.
if n_hex_color_groups >= 10000 then
return nil
end
local group_name = format("OCP_%06x", rgb)
if opts.style == "background" then
local opposite = is_light(M.rgb_unpack(rgb)) and 0x000000 or 0xFFFFFF
hl_group.fg = opposite
hl_group.bg = rgb
else
local color = M.rgb_unpack(rgb)
local bg = nil
if emphasis_threshold < 1. and color_distance(bg_color, color) < emphasis_threshold * 765 then
local emphasis = is_light(color) and light_emphasis or dark_emphasis
for i in ipairs(color) do
color[i] = min(max(color[i] + emphasis, 0), 255)
end
bg = M.rgb_pack(color[1], color[2], color[3])
end
hl_group.fg = rgb
hl_group.bg = bg
end
nvim_set_hl(0, group_name, hl_group)
hex_color_groups[rgb] = group_name
n_hex_color_groups = n_hex_color_groups + 1
return group_name
end
---@type fun(bufnr: integer, ns: integer, line_n: integer, start_col: integer, end_col: integer, group: string): integer
local set_extmark
---@return boolean|nil -- true if error
function M.make_set_extmark()
---@type vim.api.keyset.set_extmark
local reuse_mark = {
priority = opts.priority,
strict = false,
undo_restore = false,
invalidate = true,
}
if opts.style == "background" or opts.style == "foreground" then
set_extmark = function(bufnr, namespace, line_n, start_col, end_col, group)
reuse_mark.hl_group = group
reuse_mark.end_col = end_col
return nvim_buf_set_extmark(bufnr, namespace, line_n, start_col, reuse_mark)
end
elseif
opts.style == "virtual_left"
or opts.style == "virtual_eol"
or opts.style == "foreground+virtual_left"
or opts.style == "foreground+virtual_eol"
then
reuse_mark.virt_text = { { opts.virtual_text, "" } }
local virt_arr = reuse_mark.virt_text[1]
if opts.style:find("virtual_left") then
reuse_mark.virt_text_pos = "inline"
reuse_mark.right_gravity = false
reuse_mark.end_right_gravity = false
end
local foreground = opts.style:find("foreground")
set_extmark = function(bufnr, namespace, line_n, start_col, end_col, group)
virt_arr[2] = group
reuse_mark.end_col = end_col
if foreground then
reuse_mark.hl_group = group
end
return nvim_buf_set_extmark(bufnr, namespace, line_n, start_col, reuse_mark)
end
else
return true
end
end
---@param ft string
---@return oklch.FinalPatternList[]
local function get_ft_patterns(ft)
local ft_patterns = ft_patterns_cache[ft]
if ft_patterns then
return ft_patterns
end
ft_patterns = {}
for _, pattern_list in ipairs(patterns) do
if pattern_list.ft(ft) then
insert(ft_patterns, pattern_list)
end
end
ft_patterns_cache[ft] = ft_patterns
return ft_patterns
end
M.lsp_namespaces = {}
---@param client_name string
---@param buf_data BufData
local function get_lsp_namespace(client_name, buf_data)
local namespace = M.lsp_namespaces[client_name]
if namespace == nil then
namespace = vim.api.nvim_create_namespace("OklchColorPickerLsp_" .. client_name)
M.lsp_namespaces[client_name] = namespace
end
if buf_data.lsp_namespaces[client_name] == nil then
buf_data.lsp_namespaces[client_name] = namespace
table.insert(buf_data.lsp_namespaces_list, namespace)
end
return namespace
end
local mark_cache_end = {}
local mark_cache_text = {}
local mark_cache_priority = {}
local empty_opts = {}
-- Reusable range args for nvim_buf_get_extmarks
local range_start = { 0, 0 }
local range_end = { 0, -1 }
-- Per-line state (set in highlight_lines loop, read by should_create_extmark)
local new_extmark_starts = ra_new()
local new_extmark_ends = ra_new()
local cur_lsp_ns_list = {}
-- Current line's slice boundaries into the flat API result arrays.
-- Extmarks are returned sorted by (row, col), so each line is a contiguous slice.
local all_extmarks_arr = {}
local line_extmark_from = 0
local line_extmark_to = 0
local lsp_extmarks_arr = {} ---@type table<integer, table>
local lsp_line_from = {} ---@type table<integer, integer>
local lsp_line_to = {} ---@type table<integer, integer>
---@param match_start integer
---@param match_end integer
---@param text string
---@param priority integer
---@return boolean
local function should_create_extmark(match_start, match_end, text, priority)
for i = 1, new_extmark_starts.n do
if new_extmark_starts[i] <= match_end and new_extmark_ends[i] > match_start then
return false
end
end
for _, lsp_ns in ipairs(cur_lsp_ns_list) do
local arr = lsp_extmarks_arr[lsp_ns]
for idx = lsp_line_from[lsp_ns], lsp_line_to[lsp_ns] do
local extmark = arr[idx]
if extmark[3] <= match_end then
if mark_cache_end[extmark[1]] > match_start then
return false
end
end
end
end
for idx = line_extmark_from, line_extmark_to do
local extmark = all_extmarks_arr[idx]
if extmark[3] <= match_end then
if mark_cache_end[extmark[1]] > match_start then
if
extmark[3] == match_start
and mark_cache_end[extmark[1]] == match_end
and mark_cache_text[extmark[1]] == text
and mark_cache_priority[extmark[1]] == priority
then
-- The old extmark is the same as the new one, so reuse it.
-- Mark the extmark as used so we don't delete it.
extmark[2] = -1
return false
end
-- We are overlapping with a previous extmark, but it's not the same as the new one, so override it (if it has lower or the same priority).
return mark_cache_priority[extmark[1]] <= priority
end
end
end
return true
end
---@param bufnr integer
---@param lines string[]
---@param from_line integer
---@param ft string
---@param buf_data BufData
function M.highlight_lines(bufnr, lines, from_line, ft, buf_data)
local ft_patterns = get_ft_patterns(ft)
local parse = M.parse
cur_lsp_ns_list = buf_data.lsp_namespaces_list
local n_lines = #lines
range_start[1] = from_line
range_end[1] = from_line + n_lines - 1
all_extmarks_arr = nvim_buf_get_extmarks(bufnr, ns, range_start, range_end, empty_opts)
local extmark_cursor = 1
local extmark_count = #all_extmarks_arr
for _, lsp_ns in ipairs(cur_lsp_ns_list) do
lsp_extmarks_arr[lsp_ns] = nvim_buf_get_extmarks(bufnr, lsp_ns, range_start, range_end, empty_opts)
lsp_line_to[lsp_ns] = 0
end
for i, line in ipairs(lines) do
local line_n = from_line + i - 1
ra_clear(new_extmark_starts)
ra_clear(new_extmark_ends)
-- Advance cursor to find this line's extmark slice
line_extmark_from = extmark_cursor
while extmark_cursor <= extmark_count and all_extmarks_arr[extmark_cursor][2] == line_n do
extmark_cursor = extmark_cursor + 1
end
line_extmark_to = extmark_cursor - 1
for _, lsp_ns in ipairs(cur_lsp_ns_list) do
local arr = lsp_extmarks_arr[lsp_ns]
local cursor = lsp_line_to[lsp_ns] + 1
lsp_line_from[lsp_ns] = cursor
local count = #arr
while cursor <= count and arr[cursor][2] == line_n do
cursor = cursor + 1
end
lsp_line_to[lsp_ns] = cursor - 1
end
for pat_i, pattern_list in ipairs(ft_patterns) do
local priority = 1e6 - pat_i
for _, pattern in ipairs(pattern_list) do
local start = 1
local match_start, match_end = find(line, pattern.cheap, start)
while match_start ~= nil do
local replace_start, replace_end = match_start, match_end
if not pattern.simple_groups then
_, _, replace_start, replace_end = find(line, pattern.grouped, match_start)
replace_end = replace_end - 1
end
match_start = match_start - 1
local text = sub(line, replace_start --[[@as number]], replace_end)
if
should_create_extmark(match_start, match_end --[[@as number]], text, priority)
then
local rgb = pattern_list.custom_parse and pattern_list.custom_parse(text)
or parse(text, pattern_list.format)
if rgb then
local group = compute_color_group(rgb)
if group ~= nil then
local mark_id = set_extmark(bufnr, ns, line_n, match_start, match_end --[[@as integer]], group)
mark_cache_end[mark_id] = match_end --[[@as integer]]
mark_cache_text[mark_id] = text
mark_cache_priority[mark_id] = priority
ra_push(new_extmark_starts, match_start)
ra_push(new_extmark_ends, match_end)
end
end
end
start = match_end + 1
match_start, match_end = find(line, pattern.cheap, start)
end
end
end
for idx = line_extmark_from, line_extmark_to do
local extmark = all_extmarks_arr[idx]
if extmark[2] ~= -1 then
-- The extmark was not used, so delete it.
nvim_buf_del_extmark(bufnr, ns, extmark[1])
end
end
end
end
local color_method = "textDocument/documentColor"
---@alias LspColor lsp.ColorInformation|{ packed_color: integer }
local byteindex
if vim.fn.has("nvim-0.11") == 1 then
byteindex = function(line, col, offset_encoding)
return vim.str_byteindex(line, offset_encoding, col, false)
end
else
byteindex = function(line, col, offset_encoding)
return vim.lsp.util._str_byteindex_enc(line, col, offset_encoding)
end
end
---@param range lsp.Range
---@param bufnr integer
---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
local function convert_lsp_range_to_nvim(range, bufnr, offset_encoding)
local line = vim.api.nvim_buf_get_lines(bufnr, range.start.line, range.start.line + 1, false)[1] or ""
range.start.character = byteindex(line, range.start.character, offset_encoding)
if range["end"].line == range.start.line then
range["end"].character = byteindex(line, range["end"].character, offset_encoding)
else
range["end"].line = range.start.line
range["end"].character = #line
end
end
---@type fun(client: vim.lsp.Client, method: string, params: any, handler: any, bufnr: integer): boolean|nil
local client_request
if vim.fn.has("nvim-0.11") == 1 then
client_request = function(client, method, params, handler, bufnr)
return client:request(method, params, handler, bufnr)
end
else
client_request = function(client, method, params, handler, bufnr)
---@diagnostic disable-next-line: param-type-mismatch
return client.request(method, params, handler, bufnr)
end
end
---@param bufnr integer
---@param callback fun()
function M.process_update_lsp(bufnr, callback)
local buf_data = M.get_buf_data(bufnr)
if buf_data == nil then
return
end
local t = vim.uv.hrtime()
local params = { textDocument = vim.lsp.util.make_text_document_params(bufnr) }
local clients = vim.lsp.get_clients({ bufnr = bufnr, method = color_method })
local done = 0
local expected = 0
for _, client in ipairs(clients) do
if not lsp_enabled(client.name) then
goto continue
end
expected = expected + 1
local lsp_handler = function(err, results)
results = results --[[@as LspColor[]|nil]]
local buf_data = M.get_buf_data(bufnr)
if buf_data then
if not err and results then
local get_mark_start = {}
local get_mark_end = {}
local lsp_ns = get_lsp_namespace(client.name, buf_data)
nvim_buf_clear_namespace(bufnr, lsp_ns, 0, -1)
for _, result in ipairs(results) do
convert_lsp_range_to_nvim(result.range, bufnr, client.offset_encoding or "utf-16")
local line_n = result.range.start.line
get_mark_start[1] = line_n
get_mark_end[1] = line_n
get_mark_start[2] = result.range.start.character
get_mark_end[2] = result.range["end"].character
-- Override non-LSP marks
for _, m in ipairs(nvim_buf_get_extmarks(bufnr, ns, get_mark_start, get_mark_end, { overlap = true })) do
nvim_buf_del_extmark(bufnr, ns, m[1])
end
result.packed_color = M.rgb_pack(
float_to_int8(result.color.red),
float_to_int8(result.color.green),
float_to_int8(result.color.blue)
)
local group = compute_color_group(result.packed_color)
if group ~= nil then
local mark_id = set_extmark(bufnr, lsp_ns, line_n, get_mark_start[2], get_mark_end[2], group)
mark_cache_end[mark_id] = get_mark_end[2] --[[@as integer]]
end
end
end
buf_data.lsp_colors[client.name] = results or {}
end
if M.lsp_perf_logging then
in_perf_log = true
local ms = (vim.uv.hrtime() - t) / 1000000
print(format("lsp color highlighting (%s) took: %.3f ms", client.name, ms))
in_perf_log = false
end
done = done + 1
if done == expected then
callback()
end
end
local status = client_request(client, color_method, params, lsp_handler, bufnr)
if not status then
done = done + 1
buf_data.lsp_colors[client.name] = {}
utils.log(function()
return format("Failed LSP request with %s", client.name)
end, vim.log.levels.DEBUG)
end
::continue::
end
-- This only happens if all LSPs fail, otherwise the last LSP calls the callback.
if done == expected then
callback()
end
end
return M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment