diff options
Diffstat (limited to 'lua/colorizer.lua')
-rw-r--r-- | lua/colorizer.lua | 1060 |
1 files changed, 292 insertions, 768 deletions
diff --git a/lua/colorizer.lua b/lua/colorizer.lua index 0f80e83..87acad3 100644 --- a/lua/colorizer.lua +++ b/lua/colorizer.lua @@ -1,67 +1,98 @@ ---- Highlights terminal CSI ANSI color codes. +--- Requires Neovim >= 0.6.0 and `set termguicolors` +-- +--Highlights terminal CSI ANSI color codes. -- @module colorizer -local Trie = require "colorizer/trie" -local bit = require "bit" -local ffi = require "ffi" -local api = vim.api +-- @author Ashkan Kiani <from-nvim-colorizer.lua@kiani.io> +-- @usage Establish the autocmd to highlight all filetypes. +-- +-- `lua require 'colorizer'.setup()` +-- +-- Highlight using all css highlight modes in every filetype +-- +-- `lua require 'colorizer'.setup(user_default_options = { css = true; })` +-- +--============================================================================== +--USE WITH COMMANDS *colorizer-commands* +-- +-- *:ColorizerAttachToBuffer* +-- +-- Attach to the current buffer and start highlighting with the settings as +-- specified in setup (or the defaults). +-- +-- If the buffer was already attached(i.e. being highlighted), the +-- settings will be reloaded with the ones from setup. +-- This is useful for reloading settings for just one buffer. +-- +-- *:ColorizerDetachFromBuffer* +-- +-- Stop highlighting the current buffer (detach). +-- +-- *:ColorizerReloadAllBuffers* +-- +-- Reload all buffers that are being highlighted currently. +-- Shortcut for ColorizerAttachToBuffer on every buffer. +-- +-- *:ColorizerToggle* +-- Toggle highlighting of the current buffer. +-- +--USE WITH LUA +-- +-- All options that can be passed to user_default_options in `setup` +-- can be passed here. Can be empty too. +-- `0` is the buffer number here +-- +-- Attach to current buffer <pre> +-- require("colorizer").attach_to_buffer(0, { +-- mode = "background", +-- css = false, +-- }) +--</pre> +-- Detach from buffer <pre> +-- require("colorizer").detach_from_buffer(0, { +-- mode = "background", +-- css = false, +-- }) +--</pre> +-- @see colorizer.setup +-- @see colorizer.attach_to_buffer +-- @see colorizer.detach_from_buffer + +local buffer_utils = require "colorizer.buffer_utils" + +---Default namespace used in `colorizer.buffer_utils.highlight_buffer` and `attach_to_buffer`. +-- @see colorizer.buffer_utils.highlight_buffer +-- @see attach_to_buffer +local DEFAULT_NAMESPACE = buffer_utils.DEFAULT_NAMESPACE +local HIGHLIGHT_MODE_NAMES = buffer_utils.HIGHLIGHT_MODE_NAMES +local rehighlight_buffer = buffer_utils.rehighlight_buffer + +---Highlight the buffer region +---@function highlight_buffer +-- @see colorizer.buffer_utils.highlight_buffer +local highlight_buffer = buffer_utils.highlight_buffer + +local utils = require "colorizer.utils" +local merge = utils.merge + +local api = vim.api local augroup = api.nvim_create_augroup local autocmd = api.nvim_create_autocmd -local set_highlight = api.nvim_set_hl - -local buf_add_highlight = api.nvim_buf_add_highlight -local buf_clear_namespace = api.nvim_buf_clear_namespace -local get_current_buf = api.nvim_get_current_buf local buf_get_option = api.nvim_buf_get_option -local buf_get_lines = api.nvim_buf_get_lines -local buf_set_virtual_text = api.nvim_buf_set_virtual_text - -local band, lshift, bor, tohex = bit.band, bit.lshift, bit.bor, bit.tohex -local rshift = bit.rshift -local floor, min, max = math.floor, math.min, math.max - -local COLOR_MAP -local COLOR_TRIE -local COLOR_NAME_MINLEN, COLOR_NAME_MAXLEN -local COLOR_NAME_SETTINGS = { - lowercase = true, - strip_digits = false, -} +local clear_namespace = api.nvim_buf_clear_namespace +local current_buf = api.nvim_get_current_buf ---- Setup the COLOR_MAP and COLOR_TRIE -local function initialize_trie() - if not COLOR_TRIE then - COLOR_MAP = {} - COLOR_TRIE = Trie() - for k, v in pairs(api.nvim_get_color_map()) do - if not (COLOR_NAME_SETTINGS.strip_digits and k:match "%d+$") then - COLOR_NAME_MINLEN = COLOR_NAME_MINLEN and min(#k, COLOR_NAME_MINLEN) or #k - COLOR_NAME_MAXLEN = COLOR_NAME_MAXLEN and max(#k, COLOR_NAME_MAXLEN) or #k - local rgb_hex = tohex(v, 6) - COLOR_MAP[k] = rgb_hex - COLOR_TRIE:insert(k) - if COLOR_NAME_SETTINGS.lowercase then - local lowercase = k:lower() - COLOR_MAP[lowercase] = rgb_hex - COLOR_TRIE:insert(lowercase) - end - end - end - end -end - -local function merge(...) - local res = {} - for i = 1, select("#", ...) do - local o = select(i, ...) - for k, v in pairs(o) do - res[k] = v - end - end - return res -end +-- USER FACING FUNCTIONALITY -- +local AUGROUP_ID +local AUGROUP_NAME = "ColorizerSetup" +-- buffer specific options given in setup +local BUFFER_OPTIONS = {} +-- store boolean for buffer if it is initialzed +local BUFFER_INIT = {} +-- store buffer local autocmd(s) id +local BUFFER_AUTOCMDS = {} -local DEFAULT_OPTIONS = { +local USER_DEFAULT_OPTIONS = { RGB = true, -- #RGB hex codes RRGGBB = true, -- #RRGGBB hex codes names = true, -- "Name" codes like Blue or blue @@ -71,789 +102,282 @@ local DEFAULT_OPTIONS = { hsl_fn = false, -- CSS hsl() and hsla() functions css = false, -- Enable all CSS features: rgb_fn, hsl_fn, names, RGB, RRGGBB css_fn = false, -- Enable all CSS *functions*: rgb_fn, hsl_fn - -- Available modes: foreground, background, sign, virtualtext + -- Available modes: foreground, background, virtualtext mode = "background", -- Set the display mode. virtualtext = "■", } --- -- TODO use rgb as the return value from the matcher functions --- -- instead of the rgb_hex. Can be the highlight key as well --- -- when you shift it left 8 bits. Use the lower 8 bits for --- -- indicating which highlight mode to use. --- ffi.cdef [[ --- typedef struct { uint8_t r, g, b; } colorizer_rgb; --- ]] --- local rgb_t = ffi.typeof 'colorizer_rgb' - --- Create a lookup table where the bottom 4 bits are used to indicate the --- category and the top 4 bits are the hex value of the ASCII byte. -local BYTE_CATEGORY = ffi.new "uint8_t[256]" -local CATEGORY_DIGIT = lshift(1, 0) -local CATEGORY_ALPHA = lshift(1, 1) -local CATEGORY_HEX = lshift(1, 2) -local CATEGORY_ALPHANUM = bor(CATEGORY_ALPHA, CATEGORY_DIGIT) -do - local b = string.byte - for i = 0, 255 do - local v = 0 - -- Digit is bit 1 - if i >= b "0" and i <= b "9" then - v = bor(v, lshift(1, 0)) - v = bor(v, lshift(1, 2)) - v = bor(v, lshift(i - b "0", 4)) - end - local lowercase = bor(i, 0x20) - -- Alpha is bit 2 - if lowercase >= b "a" and lowercase <= b "z" then - v = bor(v, lshift(1, 1)) - if lowercase <= b "f" then - v = bor(v, lshift(1, 2)) - v = bor(v, lshift(lowercase - b "a" + 10, 4)) - end - end - BYTE_CATEGORY[i] = v - end -end - -local function byte_is_hex(byte) - return band(BYTE_CATEGORY[byte], CATEGORY_HEX) ~= 0 -end - -local function byte_is_alphanumeric(byte) - local category = BYTE_CATEGORY[byte] - return band(category, CATEGORY_ALPHANUM) ~= 0 -end - -local function parse_hex(b) - return rshift(BYTE_CATEGORY[b], 4) -end - -local function percent_or_hex(v) - if v:sub(-1, -1) == "%" then - return tonumber(v:sub(1, -2)) / 100 * 255 - end - local x = tonumber(v) - if x > 255 then - return - end - return x -end - ---- Determine whether to use black or white text --- Ref: https://stackoverflow.com/a/1855903/837964 --- https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color -local function color_is_bright(r, g, b) - -- Counting the perceptive luminance - human eye favors green color - local luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 - if luminance > 0.5 then - return true -- Bright colors, black font - else - return false -- Dark colors, white font - end -end - --- https://gist.github.com/mjackson/5311256 -local function hue_to_rgb(p, q, t) - if t < 0 then - t = t + 1 - end - if t > 1 then - t = t - 1 - end - if t < 1 / 6 then - return p + (q - p) * 6 * t - end - if t < 1 / 2 then - return q - end - if t < 2 / 3 then - return p + (q - p) * (2 / 3 - t) * 6 - end - return p -end +local OPTIONS = { buf = {}, file = {} } +local SETUP_SETTINGS = { + exclusions = { buf = {}, file = {} }, + all = { file = false, buf = false }, + default_options = USER_DEFAULT_OPTIONS, +} -local function hsl_to_rgb(h, s, l) - if h > 1 or s > 1 or l > 1 then - return - end - if s == 0 then - local r = l * 255 - return r, r, r - end - local q - if l < 0.5 then - q = l * (1 + s) +--- Make new buffer Configuration +---@param buf number: buffer number +---@param typ string|nil: "buf" or "file" - The type of buffer option +---@return table +local function new_buffer_options(buf, typ) + local value + if typ == "buf" then + value = buf_get_option(buf, "buftype") else - q = l + s - l * s + value = buf_get_option(buf, "filetype") end - local p = 2 * l - q - return 255 * hue_to_rgb(p, q, h + 1 / 3), 255 * hue_to_rgb(p, q, h), 255 * hue_to_rgb(p, q, h - 1 / 3) + return OPTIONS.file[value] or SETUP_SETTINGS.default_options end -local function color_name_parser(line, i) - if i > 1 and byte_is_alphanumeric(line:byte(i - 1)) then - return +--- Check if attached to a buffer. +---@param buf number|nil: A value of 0 implies the current buffer. +---@return number|nil: if attached to the buffer, false otherwise. +---@see highlight_buffer +local function is_buffer_attached(buf) + if buf == 0 or buf == nil then + buf = current_buf() end - if #line < i + COLOR_NAME_MINLEN - 1 then + local au = api.nvim_get_autocmds { + group = AUGROUP_ID, + event = { "WinScrolled", "TextChanged", "TextChangedI", "TextChangedP" }, + buffer = buf, + } + if not BUFFER_OPTIONS[buf] or vim.tbl_isempty(au) then return end - local prefix = COLOR_TRIE:longest_prefix(line, i) - if prefix then - -- Check if there is a letter here so as to disallow matching here. - -- Take the Blue out of Blueberry - -- Line end or non-letter. - local next_byte_index = i + #prefix - if #line >= next_byte_index and byte_is_alphanumeric(line:byte(next_byte_index)) then - return - end - return #prefix, COLOR_MAP[prefix] - end -end -local b_hash = ("#"):byte() -local function rgb_hex_parser(line, i, minlen, maxlen) - if i > 1 and byte_is_alphanumeric(line:byte(i - 1)) then - return - end - if line:byte(i) ~= b_hash then - return - end - local j = i + 1 - if #line < j + minlen - 1 then - return - end - local n = j + maxlen - local alpha - local v = 0 - while j <= min(n, #line) do - local b = line:byte(j) - if not byte_is_hex(b) then - break - end - if j - i >= 7 then - alpha = parse_hex(b) + lshift(alpha or 0, 4) - else - v = parse_hex(b) + lshift(v, 4) - end - j = j + 1 - end - if #line >= j and byte_is_alphanumeric(line:byte(j)) then - return - end - local length = j - i - if length ~= 4 and length ~= 7 and length ~= 9 then - return - end - if alpha then - alpha = tonumber(alpha) / 255 - local r = floor(band(rshift(v, 16), 0xFF) * alpha) - local g = floor(band(rshift(v, 8), 0xFF) * alpha) - local b = floor(band(v, 0xFF) * alpha) - v = bor(lshift(r, 16), lshift(g, 8), b) - return 9, tohex(v, 6) - end - return length, line:sub(i + 1, i + length - 1) + return buf end -local RGB_FUNCTION_TRIE = Trie { "0x" } -local function rgb_0x_parser(line, i) - local prefix = RGB_FUNCTION_TRIE:longest_prefix(line:sub(i)) - if not prefix then - return - end - - local j = i + 2 - if #line < 10 then - return - end - local n = j + 8 - local alpha - local v = 0 - while j <= min(n, #line) do - local b = line:byte(j) - if not byte_is_hex(b) then - break - end - if j - i <= 3 then - alpha = parse_hex(b) + lshift(alpha or 0, 4) - else - v = parse_hex(b) + lshift(v, 4) - end - j = j + 1 - end - if #line >= j and byte_is_alphanumeric(line:byte(j)) then - return - end - local length = j - i - if length ~= 10 then +--- Stop highlighting the current buffer. +---@param buf number|nil: buf A value of 0 or nil implies the current buffer. +---@param ns number|nil: ns the namespace id, if not given DEFAULT_NAMESPACE is used +local function detach_from_buffer(buf, ns) + buf = is_buffer_attached(buf) + if not buf then return end - alpha = tonumber(alpha) / 255 - local r = floor(band(rshift(v, 16), 0xFF) * alpha) - local g = floor(band(rshift(v, 8), 0xFF) * alpha) - local b = floor(band(v, 0xFF) * alpha) - v = bor(lshift(r, 16), lshift(g, 8), b) - return length, tohex(v, 6) -end --- TODO consider removing the regexes here --- TODO this might not be the best approach to alpha channel. --- Things like pumblend might be useful here. -local css_fn = {} -do - local CSS_RGB_FN_MINIMUM_LENGTH = #"rgb(0,0,0)" - 1 - local CSS_RGBA_FN_MINIMUM_LENGTH = #"rgba(0,0,0,0)" - 1 - local CSS_HSL_FN_MINIMUM_LENGTH = #"hsl(0,0%,0%)" - 1 - local CSS_HSLA_FN_MINIMUM_LENGTH = #"hsla(0,0%,0%,0)" - 1 - function css_fn.rgb(line, i) - if #line < i + CSS_RGB_FN_MINIMUM_LENGTH then - return - end - local r, g, b, match_end = line:sub(i):match "^rgb%(%s*(%d+%%?)%s*,%s*(%d+%%?)%s*,%s*(%d+%%?)%s*%)()" - if not match_end then - r, g, b, match_end = line:sub(i):match "^rgb%(%s*(%d+%%?)%s+(%d+%%?)%s+(%d+%%?)%s*%)()" - if not match_end then - return - end - end - r = percent_or_hex(r) - if not r then - return - end - g = percent_or_hex(g) - if not g then - return - end - b = percent_or_hex(b) - if not b then - return - end - local rgb_hex = string.format("%02x%02x%02x", r, g, b) - return match_end - 1, rgb_hex - end - - function css_fn.hsl(line, i) - if #line < i + CSS_HSL_FN_MINIMUM_LENGTH then - return - end - local h, s, l, match_end = line:sub(i):match "^hsl%(%s*(%d+)%s*,%s*(%d+)%%%s*,%s*(%d+)%%%s*%)()" - if not match_end then - h, s, l, match_end = line:sub(i):match "^hsl%(%s*(%d+)%s+(%d+)%%%s+(%d+)%%%s*%)()" - if not match_end then - return - end - end - h = tonumber(h) - if h > 360 then - return - end - s = tonumber(s) - if s > 100 then - return - end - l = tonumber(l) - if l > 100 then - return - end - local r, g, b = hsl_to_rgb(h / 360, s / 100, l / 100) - if r == nil or g == nil or b == nil then - return - end - local rgb_hex = string.format("%02x%02x%02x", r, g, b) - return match_end - 1, rgb_hex - end - - function css_fn.rgba(line, i) - if #line < i + CSS_RGBA_FN_MINIMUM_LENGTH then - return - end - local r, g, b, a, match_end = - line:sub(i):match "^rgba%(%s*(%d+%%?)%s*,%s*(%d+%%?)%s*,%s*(%d+%%?)%s*,%s*([.%d]+)%s*%)()" - if not match_end then - r, g, b, a, match_end = line:sub(i):match "^rgba%(%s*(%d+%%?)%s+(%d+%%?)%s+(%d+%%?)%s+([.%d]+)%s*%)()" - if not match_end then - return - end - end - a = tonumber(a) - if not a or a > 1 then - return - end - r = percent_or_hex(r) - if not r then - return - end - g = percent_or_hex(g) - if not g then - return - end - b = percent_or_hex(b) - if not b then - return - end - local rgb_hex = string.format("%02x%02x%02x", r * a, g * a, b * a) - return match_end - 1, rgb_hex - end - - function css_fn.hsla(line, i) - if #line < i + CSS_HSLA_FN_MINIMUM_LENGTH then - return - end - local h, s, l, a, match_end = line:sub(i):match "^hsla%(%s*(%d+)%s*,%s*(%d+)%%%s*,%s*(%d+)%%%s*,%s*([.%d]+)%s*%)()" - if not match_end then - h, s, l, a, match_end = line:sub(i):match "^hsla%(%s*(%d+)%s+(%d+)%%%s+(%d+)%%%s+([.%d]+)%s*%)()" - if not match_end then - return - end - end - a = tonumber(a) - if not a or a > 1 then - return - end - h = tonumber(h) - if h > 360 then - return - end - s = tonumber(s) - if s > 100 then - return - end - l = tonumber(l) - if l > 100 then - return - end - local r, g, b = hsl_to_rgb(h / 360, s / 100, l / 100) - if r == nil or g == nil or b == nil then - return - end - local rgb_hex = string.format("%02x%02x%02x", r * a, g * a, b * a) - return match_end - 1, rgb_hex - end -end - -local css_function_parser, rgb_function_parser, hsl_function_parser -do - local CSS_FUNCTION_TRIE = Trie { "rgb", "rgba", "hsl", "hsla" } - local RGB_FUNCTION_TRIE = Trie { "rgb", "rgba" } - local HSL_FUNCTION_TRIE = Trie { "hsl", "hsla" } - css_function_parser = function(line, i) - local prefix = CSS_FUNCTION_TRIE:longest_prefix(line:sub(i)) - if prefix then - return css_fn[prefix](line, i) - end - end - rgb_function_parser = function(line, i) - local prefix = RGB_FUNCTION_TRIE:longest_prefix(line:sub(i)) - if prefix then - return css_fn[prefix](line, i) - end - end - hsl_function_parser = function(line, i) - local prefix = HSL_FUNCTION_TRIE:longest_prefix(line:sub(i)) - if prefix then - return css_fn[prefix](line, i) - end - end -end - -local function compile_matcher(matchers) - local parse_fn = matchers[1] - for j = 2, #matchers do - local old_parse_fn = parse_fn - local new_parse_fn = matchers[j] - parse_fn = function(line, i) - local length, rgb_hex = new_parse_fn(line, i) - if length then - return length, rgb_hex - end - return old_parse_fn(line, i) - end - end - return parse_fn -end - ---- Default namespace used in `highlight_buffer` and `attach_to_buffer`. --- The name is "terminal_highlight" --- @see highlight_buffer --- @see attach_to_buffer -local DEFAULT_NAMESPACE = api.nvim_create_namespace "colorizer" -local HIGHLIGHT_NAME_PREFIX = "colorizer" -local HIGHLIGHT_MODE_NAMES = { - background = "mb", - foreground = "mf", - virtualtext = "mv", -} -local HIGHLIGHT_CACHE = {} - ---- Make a deterministic name for a highlight given these attributes -local function make_highlight_name(rgb, mode) - return table.concat({ HIGHLIGHT_NAME_PREFIX, HIGHLIGHT_MODE_NAMES[mode], rgb }, "_") -end - -local function create_highlight(rgb_hex, options) - local mode = options.mode or "background" - -- TODO validate rgb format? - rgb_hex = rgb_hex:lower() - local cache_key = table.concat({ HIGHLIGHT_MODE_NAMES[mode], rgb_hex }, "_") - local highlight_name = HIGHLIGHT_CACHE[cache_key] - -- Look up in our cache. - if not highlight_name then - if #rgb_hex == 3 then - rgb_hex = table.concat { - rgb_hex:sub(1, 1):rep(2), - rgb_hex:sub(2, 2):rep(2), - rgb_hex:sub(3, 3):rep(2), - } - end - -- Create the highlight - highlight_name = make_highlight_name(rgb_hex, mode) - if mode == "foreground" then - set_highlight(0, highlight_name, { fg = "#" .. rgb_hex }) - else - local rr, gg, bb = rgb_hex:sub(1, 2), rgb_hex:sub(3, 4), rgb_hex:sub(5, 6) - local r, g, b = tonumber(rr, 16), tonumber(gg, 16), tonumber(bb, 16) - local fg_color - if color_is_bright(r, g, b) then - fg_color = "Black" - else - fg_color = "White" - end - set_highlight(0, highlight_name, { fg = fg_color, bg = "#" .. rgb_hex }) - end - HIGHLIGHT_CACHE[cache_key] = highlight_name + clear_namespace(buf, ns or DEFAULT_NAMESPACE, 0, -1) + for _, id in ipairs(BUFFER_AUTOCMDS[buf] or {}) do + pcall(api.nvim_del_autocmd, id) end - return highlight_name + -- because now the buffer is not visible, so delete its information + BUFFER_OPTIONS[buf] = nil + BUFFER_AUTOCMDS[buf] = nil end -local MATCHER_CACHE = {} -local function make_matcher(options) - local enable_names = options.css or options.names - local enable_RGB = options.css or options.RGB - local enable_RRGGBB = options.css or options.RRGGBB - local enable_RRGGBBAA = options.css or options.RRGGBBAA - local enable_AARRGGBB = options.AARRGGBB - local enable_rgb = options.css or options.css_fns or options.rgb_fn - local enable_hsl = options.css or options.css_fns or options.hsl_fn - - local matcher_key = bor( - lshift(enable_names and 1 or 0, 0), - lshift(enable_RGB and 1 or 0, 1), - lshift(enable_RRGGBB and 1 or 0, 2), - lshift(enable_RRGGBBAA and 1 or 0, 3), - lshift(enable_rgb and 1 or 0, 4), - lshift(enable_hsl and 1 or 0, 5) - ) - - if matcher_key == 0 then - return - end - - local loop_parse_fn = MATCHER_CACHE[matcher_key] - if loop_parse_fn then - return loop_parse_fn - end - - local loop_matchers = {} - if enable_names then - table.insert(loop_matchers, color_name_parser) - end - if enable_AARRGGBB then - table.insert(loop_matchers, rgb_0x_parser) - end - do - local valid_lengths = { [3] = enable_RGB, [6] = enable_RRGGBB, [8] = enable_RRGGBBAA } - local minlen, maxlen - for k, v in pairs(valid_lengths) do - if v then - minlen = minlen and min(k, minlen) or k - maxlen = maxlen and max(k, maxlen) or k - end - end - if minlen then - table.insert(loop_matchers, function(line, i) - local length, rgb_hex = rgb_hex_parser(line, i, minlen, maxlen) - if length and valid_lengths[length - 1] then - return length, rgb_hex - end - end) - end - end - if enable_rgb and enable_hsl then - table.insert(loop_matchers, css_function_parser) - elseif enable_rgb then - table.insert(loop_matchers, rgb_function_parser) - elseif enable_hsl then - table.insert(loop_matchers, hsl_function_parser) +---Attach to a buffer and continuously highlight changes. +---@param buf integer: A value of 0 implies the current buffer. +---@param options table: Configuration options as described in `setup` +---@param typ string|nil: "buf" or "file" - The type of buffer option +local function attach_to_buffer(buf, options, typ) + if buf == 0 or buf == nil then + buf = current_buf() end - loop_parse_fn = compile_matcher(loop_matchers) - MATCHER_CACHE[matcher_key] = loop_parse_fn - return loop_parse_fn -end -local function add_highlight(options, buf, ns, data) - for linenr, hls in pairs(data) do - if vim.tbl_contains({ "foreground", "background" }, options.mode) then - for _, hl in ipairs(hls) do - buf_add_highlight(buf, ns, hl.name, linenr, hl.range[1], hl.range[2]) - end - elseif options.mode == "virtualtext" then - local chunks = {} - for _, hl in ipairs(hls) do - table.insert(chunks, { options.virtualtext, hl.name }) - end - buf_set_virtual_text(buf, ns, linenr, chunks, {}) - end + if not options then + options = new_buffer_options(buf, typ) end -end ---[[-- Highlight the buffer region. -Highlight starting from `line_start` (0-indexed) for each line described by `lines` in the -buffer `buf` and attach it to the namespace `ns`. - -@tparam integer buf buffer id. -@tparam[opt=DEFAULT_NAMESPACE] integer ns the namespace id. Create it with `vim.api.create_namespace` -@tparam {string,...} lines the lines to highlight from the buffer. -@tparam integer line_start should be 0-indexed -@param options Configuration options as described in `setup` -@see setup -]] -local function highlight_buffer(buf, ns, lines, line_start, options) - -- TODO do I have to put this here? - initialize_trie() - ns = ns or DEFAULT_NAMESPACE - local loop_parse_fn = make_matcher(options) - local data = {} - local mode = options.mode == "background" and { mode = "background" } or { mode = "foreground" } - for current_linenum, line in ipairs(lines) do - current_linenum = current_linenum - 1 + line_start - -- Upvalues are options and current_linenum - local i = 1 - while i < #line do - local length, rgb_hex = loop_parse_fn(line, i) - if length then - local name = create_highlight(rgb_hex, mode) - local d = data[current_linenum] or {} - table.insert(d, { name = name, range = { i - 1, i + length - 1 } }) - data[current_linenum] = d - i = i + length - else - i = i + 1 - end + if not HIGHLIGHT_MODE_NAMES[options.mode] then + if options.mode ~= nil then + local mode = options.mode + vim.defer_fn(function() + -- just notify the user once + vim.notify_once(string.format("Warning: Invalid mode given to colorizer setup [ %s ]", mode)) + end, 0) end + options.mode = "background" end - add_highlight(options, buf, ns, data) -end - ---- --- USER FACING FUNCTIONALITY ---- - -local SETUP_SETTINGS = { - exclusions = {}, - default_options = DEFAULT_OPTIONS, -} -local BUFFER_OPTIONS = {} -local FILETYPE_OPTIONS = {} - -local function rehighlight_buffer(buf, options) - local ns = DEFAULT_NAMESPACE - assert(options) - local a = vim.api.nvim_buf_call(buf, function() - return { - vim.fn.line "w0", - vim.fn.line "w$", - } - end) - local min_row = a[1] - 1 - local max_row = a[2] - buf_clear_namespace(buf, ns, min_row, max_row) - local lines = buf_get_lines(buf, min_row, max_row, false) - highlight_buffer(buf, ns, lines, min_row, options) -end - -local function new_buffer_options(buf) - local filetype = buf_get_option(buf, "filetype") - return FILETYPE_OPTIONS[filetype] or SETUP_SETTINGS.default_options -end - ---- Check if attached to a buffer. --- @tparam[opt=0|nil] integer buf A value of 0 implies the current buffer. --- @return true if attached to the buffer, false otherwise. -local function is_buffer_attached(buf) - if buf == 0 or buf == nil then - buf = get_current_buf() - end - return BUFFER_OPTIONS[buf] ~= nil -end ---- Stop highlighting the current buffer. --- @tparam[opt=0|nil] integer buf A value of 0 or nil implies the current buffer. --- @tparam[opt=DEFAULT_NAMESPACE] integer ns the namespace id. -local function detach_from_buffer(buf, ns) - if buf == 0 or buf == nil then - buf = get_current_buf() - end - buf_clear_namespace(buf, ns or DEFAULT_NAMESPACE, 0, -1) - for _, id in ipairs(BUFFER_OPTIONS["autocmds"][buf]) do - pcall(api.nvim_del_autocmd, id) - end - BUFFER_OPTIONS["autocmds"][buf] = nil - BUFFER_OPTIONS[buf] = nil -end - ---- Attach to a buffer and continuously highlight changes. --- @tparam[opt=0|nil] integer buf A value of 0 implies the current buffer. --- @param[opt] options Configuration options as described in `setup` --- @see setup -local function attach_to_buffer(buf, options) - if buf == 0 or buf == nil then - buf = get_current_buf() - end - local already_attached = BUFFER_OPTIONS[buf] ~= nil - if not options then - options = new_buffer_options(buf) - end BUFFER_OPTIONS[buf] = options rehighlight_buffer(buf, options) - if already_attached then + BUFFER_INIT[buf] = true + + if BUFFER_AUTOCMDS[buf] then return end local autocmds = {} - local au_group_id = augroup("ColorizerSetup", {}) + local au_group_id = AUGROUP_ID - autocmds[#autocmds + 1] = vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "TextChangedP" }, { + autocmds[#autocmds + 1] = autocmd({ "TextChanged", "TextChangedI", "TextChangedP" }, { + group = au_group_id, buffer = buf, callback = function() - -- only reload if it was not disabled using :HighlightColorsOff + -- only reload if it was not disabled using detach_from_buffer if BUFFER_OPTIONS[buf] then rehighlight_buffer(buf, options) end end, }) - autocmds[#autocmds + 1] = vim.api.nvim_create_autocmd({ "WinScrolled" }, { + autocmds[#autocmds + 1] = autocmd({ "WinScrolled" }, { group = au_group_id, buffer = buf, callback = function() - -- only reload if it was not disabled using :HighlightColorsOff + -- only reload if it was not disabled using detach_from_buffer if BUFFER_OPTIONS[buf] then rehighlight_buffer(buf, options) end end, }) - vim.api.nvim_create_autocmd({ "BufUnload", "BufDelete", "BufHidden" }, { + autocmd({ "BufUnload", "BufDelete" }, { group = au_group_id, buffer = buf, callback = function() if BUFFER_OPTIONS[buf] then detach_from_buffer(buf) end + BUFFER_INIT[buf] = nil end, }) - if not BUFFER_OPTIONS["autocmds"] then - BUFFER_OPTIONS["autocmds"] = {} - end - BUFFER_OPTIONS["autocmds"][buf] = autocmds + BUFFER_AUTOCMDS[buf] = autocmds end ---- Easy to use function if you want the full setup without fine grained control. --- Setup an autocmd which enables colorizing for the filetypes and options specified. --- --- By default highlights all FileTypes. --- --- Example config: --- ``` --- { 'scss', 'html', css = { rgb_fn = true; }, javascript = { no_names = true } } --- ``` +---Easy to use function if you want the full setup without fine grained control. +--Setup an autocmd which enables colorizing for the filetypes and options specified. -- --- You can combine an array and more specific options. --- Possible options: --- - `no_names`: Don't highlight names like Blue --- - `rgb_fn`: Highlight `rgb(...)` functions. --- - `mode`: Highlight mode. Valid options: `foreground`,`background` +--By default highlights all FileTypes. -- --- @param[opt={'*'}] filetypes A table/array of filetypes to selectively enable and/or customize. By default, enables all filetypes. --- @tparam[opt] {[string]=string} default_options Default options to apply for the filetypes enable. --- @usage require'colorizer'.setup() -local function setup(filetypes, user_default_options) +--Example config:~ +--<pre> +-- { filetypes = { "css", "html" }, user_default_options = { names = true } } +--</pre> +--Setup with all the default options:~ +--<pre> +-- require("colorizer").setup { +-- filetypes = { "*" }, +-- user_default_options = { +-- RGB = true, -- #RGB hex codes +-- RRGGBB = true, -- #RRGGBB hex codes +-- names = true, -- "Name" codes like Blue or blue +-- RRGGBBAA = false, -- #RRGGBBAA hex codes +-- AARRGGBB = false, -- 0xAARRGGBB hex codes +-- rgb_fn = false, -- CSS rgb() and rgba() functions +-- hsl_fn = false, -- CSS hsl() and hsla() functions +-- css = false, -- Enable all CSS features: rgb_fn, hsl_fn, names, RGB, RRGGBB +-- css_fn = false, -- Enable all CSS *functions*: rgb_fn, hsl_fn +-- -- Available modes for `mode`: foreground, background, virtualtext +-- mode = "background", -- Set the display mode. +-- virtualtext = "■", +-- }, +-- -- all the sub-options of filetypes apply to buftypes +-- buftypes = {}, +-- } +--</pre> +---@param config table: Config containing above parameters. +---@usage `require'colorizer'.setup()` +local function setup(config) if not vim.opt.termguicolors then - vim.notify("&termguicolors must be set", "ErrorMsg") + vim.schedule(function() + vim.notify("Colorizer: Error: &termguicolors must be set", "Error") + end) return end - FILETYPE_OPTIONS = {} + + local conf = vim.deepcopy(config) + + -- TODO: Remove conf[1] style + -- Mostly here to not break existing setups + local filetypes = conf.filetypes + local user_default_options = conf.user_default_options + local buftypes = conf.buftypes + -- if nothing given the enable for all filtypes + filetypes = filetypes or conf[1] or { "*" } + user_default_options = user_default_options or conf[2] or {} + buftypes = buftypes or conf[3] or nil + + OPTIONS = { buf = {}, file = {} } SETUP_SETTINGS = { - exclusions = {}, - default_options = merge(DEFAULT_OPTIONS, user_default_options or {}), + exclusions = { buf = {}, file = {} }, + all = { file = false, buf = false }, + default_options = merge(USER_DEFAULT_OPTIONS, user_default_options), } - -- Initialize this AFTER setting COLOR_NAME_SETTINGS - initialize_trie() - function COLORIZER_SETUP_HOOK() + + local function COLORIZER_SETUP_HOOK(typ) local filetype = vim.bo.filetype - if SETUP_SETTINGS.exclusions[filetype] then + local buftype = vim.bo.buftype + if SETUP_SETTINGS.exclusions.file[filetype] or SETUP_SETTINGS.exclusions.buf[buftype] then return end - local options = FILETYPE_OPTIONS[filetype] or SETUP_SETTINGS.default_options - attach_to_buffer(get_current_buf(), options) + + local fopts, bopts, options = OPTIONS[typ][filetype], OPTIONS[typ][buftype], nil + if typ == "file" then + options = fopts + -- if buffer and filetype options both are given, then prefer fileoptions + elseif fopts and bopts then + options = fopts + else + options = bopts + end + + if not options and not SETUP_SETTINGS.all[typ] then + return + end + + options = options or SETUP_SETTINGS.default_options + + -- this should ideally be triggered one time per buffer + -- but BufWinEnter also triggers for split formation + -- but we don't want that so add a check using local buffer variable + local buf = current_buf() + if not BUFFER_INIT[buf] then + attach_to_buffer(buf, options, typ) + end end - local au_group_id = augroup("ColorizerSetup", {}) - autocmd("FileType", { - group = au_group_id, - callback = function() - COLORIZER_SETUP_HOOK() - end, - }) + local au_group_id = augroup(AUGROUP_NAME, {}) + AUGROUP_ID = au_group_id - if not filetypes then - autocmd("FileType", { - group = au_group_id, - callback = function() - COLORIZER_SETUP_HOOK() - end, - }) - else - for k, v in pairs(filetypes) do - local filetype - local options = SETUP_SETTINGS.default_options - if type(k) == "string" then - filetype = k - if type(v) ~= "table" then - vim.notify("colorizer: Invalid option type for filetype " .. filetype, "ErrorMsg") + local aucmd = { buf = "BufWinEnter", file = "FileType" } + local function parse_opts(typ, tbl) + if type(tbl) == "table" then + local list = {} + + for k, v in pairs(tbl) do + local value + local options = SETUP_SETTINGS.default_options + if type(k) == "string" then + value = k + if type(v) ~= "table" then + vim.notify("colorizer: Invalid option type for " .. typ .. "type" .. value, "ErrorMsg") + else + options = merge(SETUP_SETTINGS.default_options, v) + end else - options = merge(SETUP_SETTINGS.default_options, v) - assert( - HIGHLIGHT_MODE_NAMES[options.mode or "background"], - "colorizer: Invalid mode: " .. tostring(options.mode) - ) + value = v + end + -- Exclude + if value:sub(1, 1) == "!" then + SETUP_SETTINGS.exclusions[typ][value:sub(2)] = true + else + OPTIONS[typ][value] = options + if value == "*" then + SETUP_SETTINGS.all[typ] = true + else + table.insert(list, value) + end end - else - filetype = v - end - -- Exclude - if filetype:sub(1, 1) == "!" then - SETUP_SETTINGS.exclusions[filetype:sub(2)] = true - else - FILETYPE_OPTIONS[filetype] = options - autocmd("BufWinEnter", { - group = au_group_id, - pattern = filetype, - callback = function() - -- this should ideally be triggered one time per buffer - -- but BufWinEnter also triggers for split formation - -- but we don't want that so add a check using local buffer variable - local buf = get_current_buf() - if BUFFER_OPTIONS[buf] then - COLORIZER_SETUP_HOOK() - end - end, - }) end + autocmd({ aucmd[typ] }, { + group = au_group_id, + pattern = typ == "file" and (SETUP_SETTINGS.all[typ] and "*" or list) or nil, + callback = function() + COLORIZER_SETUP_HOOK(typ) + end, + }) + elseif tbl then + vim.notify_once(string.format("colorizer: Invalid type for %stypes %s", typ, vim.inspect(tbl)), "ErrorMsg") end end + + parse_opts("file", filetypes) + parse_opts("buf", buftypes) + autocmd("ColorScheme", { group = au_group_id, callback = function() @@ -862,10 +386,19 @@ local function setup(filetypes, user_default_options) }) end +--- Return the currently active buffer options. +---@param buf number|nil: Buffer number +local function get_buffer_options(buf) + if buf == 0 or buf == nil then + buf = current_buf() + end + return merge({}, BUFFER_OPTIONS[buf]) +end + --- Reload all of the currently active highlighted buffers. local function reload_all_buffers() for buf, _ in pairs(BUFFER_OPTIONS) do - attach_to_buffer(buf) + attach_to_buffer(buf, get_buffer_options(buf)) end end @@ -875,15 +408,6 @@ local function clear_highlight_cache() vim.schedule(reload_all_buffers) end ---- Return the currently active buffer options. --- @tparam[opt=0|nil] integer buf A value of 0 or nil implies the current buffer. -local function get_buffer_options(buf) - if buf == 0 or buf == nil then - buf = get_current_buf() - end - return merge({}, BUFFER_OPTIONS[buf]) -end - --- @export return { DEFAULT_NAMESPACE = DEFAULT_NAMESPACE, |