aboutsummaryrefslogtreecommitdiff
path: root/lua/colorizer.lua
diff options
context:
space:
mode:
authorAkianonymus <anonymus.aki@gmail.com>2022-08-28 12:30:35 +0530
committerAkianonymus <anonymus.aki@gmail.com>2022-09-03 17:24:25 +0530
commit28b41de2f491ef598197823c04fc7e86ae76a625 (patch)
treeb480ea1c0f58e4802e92a6de9baf26f27b6e855d /lua/colorizer.lua
parentfeat: Incremental highlight loading (diff)
fragment | Implement better autocmd management | refactor
add a all_buffers option - colorizer will activate on all buffers, empty or not, still respect filetypes option handle errors when detach is called multiple times from the same buffer use bufdelete and bufdelete to remove the autocmds use a more efficient compile parse_fn function use custom ldoc template to generate vim help
Diffstat (limited to 'lua/colorizer.lua')
-rw-r--r--lua/colorizer.lua1060
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,