From cf4f33f5ea34f9c4e9975299b094f1a2b8b34aa0 Mon Sep 17 00:00:00 2001 From: Akianonymus Date: Thu, 15 Sep 2022 10:00:00 +0530 Subject: Better structure and naming | Misc improvements --- lua/colorizer/buffer.lua | 266 ++++++++++++++++++++++++++ lua/colorizer/buffer_utils.lua | 254 ------------------------- lua/colorizer/color.lua | 405 +++++++++++++++++++++++++++++++++++++++ lua/colorizer/color_utils.lua | 411 ---------------------------------------- lua/colorizer/matcher.lua | 154 +++++++++++++++ lua/colorizer/matcher_utils.lua | 154 --------------- lua/colorizer/sass.lua | 27 +-- lua/colorizer/tailwind.lua | 53 +++--- lua/colorizer/utils.lua | 107 +++++------ 9 files changed, 915 insertions(+), 916 deletions(-) create mode 100644 lua/colorizer/buffer.lua delete mode 100644 lua/colorizer/buffer_utils.lua create mode 100644 lua/colorizer/color.lua delete mode 100644 lua/colorizer/color_utils.lua create mode 100644 lua/colorizer/matcher.lua delete mode 100644 lua/colorizer/matcher_utils.lua (limited to 'lua/colorizer') diff --git a/lua/colorizer/buffer.lua b/lua/colorizer/buffer.lua new file mode 100644 index 0000000..3b31daf --- /dev/null +++ b/lua/colorizer/buffer.lua @@ -0,0 +1,266 @@ +---Helper functions to highlight buffer smartly +--@module colorizer.buffer +local api = vim.api +local buf_set_virtual_text = api.nvim_buf_set_extmark +local buf_get_lines = api.nvim_buf_get_lines +local create_namespace = api.nvim_create_namespace +local clear_namespace = api.nvim_buf_clear_namespace +local set_highlight = api.nvim_set_hl + +local color = require "colorizer.color" +local color_is_bright = color.is_bright + +local make_matcher = require("colorizer.matcher").make + +local sass = require "colorizer.sass" +local sass_update_variables = sass.update_variables +local sass_cleanup = sass.cleanup + +local tailwind = require "colorizer.tailwind" +local tailwind_setup_lsp = tailwind.setup_lsp_colors +local tailwind_cleanup = tailwind.cleanup + +local buffer = {} + +local HIGHLIGHT_NAME_PREFIX = "colorizer" +local HIGHLIGHT_CACHE = {} + +--- Default namespace used in `highlight` and `colorizer.attach_to_buffer`. +-- @see highlight +-- @see colorizer.attach_to_buffer +buffer.default_namespace = create_namespace "colorizer" + +--- Highlight mode which will be use to render the colour +buffer.highlight_mode_names = { + background = "mb", + foreground = "mf", + virtualtext = "mv", +} + +--- Clean the highlight cache +function buffer.clear_hl_cache() + HIGHLIGHT_CACHE = {} +end + +--- Make a deterministic name for a highlight given these attributes +local function make_highlight_name(rgb, mode) + return table.concat({ HIGHLIGHT_NAME_PREFIX, buffer.highlight_mode_names[mode], rgb }, "_") +end + +local function create_highlight(rgb_hex, mode) + mode = mode or "background" + -- TODO validate rgb format? + rgb_hex = rgb_hex:lower() + local cache_key = table.concat({ buffer.highlight_mode_names[mode], rgb_hex }, "_") + local highlight_name = HIGHLIGHT_CACHE[cache_key] + + -- Look up in our cache. + if highlight_name then + return highlight_name + end + + -- convert from #fff to #ffffff + 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 + return highlight_name +end + +--- Create highlight and set highlights +---@param buf number +---@param ns number +---@param line_start number +---@param line_end number +---@param data table: table output of `parse_lines` +---@param options table: Passed in setup, mainly for `user_default_options` +function buffer.add_highlight(buf, ns, line_start, line_end, data, options) + clear_namespace(buf, ns, line_start, line_end) + + local mode = options.mode == "background" and "background" or "foreground" + if vim.tbl_contains({ "foreground", "background" }, options.mode) then + for linenr, hls in pairs(data) do + for _, hl in ipairs(hls) do + local hlname = create_highlight(hl.rgb_hex, mode) + api.nvim_buf_add_highlight(buf, ns, hlname, linenr, hl.range[1], hl.range[2]) + end + end + elseif options.mode == "virtualtext" then + for linenr, hls in pairs(data) do + for _, hl in ipairs(hls) do + local hlname = create_highlight(hl.rgb_hex, mode) + buf_set_virtual_text(0, ns, linenr, hl.range[2], { + end_col = hl.range[2], + virt_text = { { options.virtualtext or "■", hlname } }, + }) + end + end + 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`. +---@param buf number: buffer id +---@param ns number: The namespace id. Default is DEFAULT_NAMESPACE. Create it with `vim.api.nvim_create_namespace` +---@param line_start number: line_start should be 0-indexed +---@param line_end number: Last line to highlight +---@param options table: Configuration options as described in `setup` +---@param options_local table: Buffer local variables +---@return nil|boolean|number,table +function buffer.highlight(buf, ns, line_start, line_end, options, options_local) + local returns = { detach = { ns = {}, functions = {} } } + if buf == 0 or buf == nil then + buf = api.nvim_get_current_buf() + end + + local lines = buf_get_lines(buf, line_start, line_end, false) + + ns = ns or buffer.default_namespace + + -- only update sass varibles when text is changed + if options_local.__event ~= "WinScrolled" and options.sass and options.sass.enable then + table.insert(returns.detach.functions, sass_cleanup) + sass_update_variables(buf, 0, -1, nil, make_matcher(options.sass.parsers or { css = true }), options, options_local) + end + + local data = buffer.parse_lines(buf, lines, line_start, options) or {} + buffer.add_highlight(buf, ns, line_start, line_end, data, options) + + if options.tailwind == "lsp" or options.tailwind == "both" then + tailwind_setup_lsp(buf, options, options_local, buffer.add_highlight) + table.insert(returns.detach.functions, tailwind_cleanup) + end + + return true, returns +end + +--- Parse the given lines for colors and return a table containing +-- rgb_hex and range per line +---@param buf number +---@param lines table +---@param line_start number: This is the buffer line number, from where to start highlighting +---@param options table: Passed in `colorizer.setup`, Only uses `user_default_options` +---@return table|nil +function buffer.parse_lines(buf, lines, line_start, options) + local loop_parse_fn = make_matcher(options) + if not loop_parse_fn then + return + end + + local data = {} + for current_linenum, line in ipairs(lines) do + current_linenum = current_linenum - 1 + line_start + data[current_linenum] = data[current_linenum] or {} + + -- Upvalues are options and current_linenum + local i = 1 + while i < #line do + local length, rgb_hex = loop_parse_fn(line, i, buf) + if length and rgb_hex then + table.insert(data[current_linenum], { rgb_hex = rgb_hex, range = { i - 1, i + length - 1 } }) + i = i + length + else + i = i + 1 + end + end + end + + return data +end + +-- gets used in rehighlight function only +local BUFFER_LINES = {} +-- get the amount lines to highlight +local function getrow(buf) + if not BUFFER_LINES[buf] then + BUFFER_LINES[buf] = {} + end + + local a = api.nvim_buf_call(buf, function() + return { + vim.fn.line "w0", + vim.fn.line "w$", + } + end) + local min, max + local new_min, new_max = a[1] - 1, a[2] + local old_min, old_max = BUFFER_LINES[buf]["min"], BUFFER_LINES[buf]["max"] + + if old_min and old_max then + -- Triggered for TextChanged autocmds + -- TODO: Find a way to just apply highlight to changed text lines + if (old_max == new_max) or (old_min == new_min) then + min, max = new_min, new_max + -- Triggered for WinScrolled autocmd - Scroll Down + elseif old_max < new_max then + min = old_max + max = new_max + -- Triggered for WinScrolled autocmd - Scroll Up + elseif old_max > new_max then + min = new_min + max = new_min + (old_max - new_max) + end + -- just in case a long jump was made + if max - min > new_max - new_min then + min = new_min + max = new_max + end + end + min = min or new_min + max = max or new_max + -- store current window position to be used later to incremently highlight + BUFFER_LINES[buf]["max"] = new_max + BUFFER_LINES[buf]["min"] = new_min + return min, max +end + +--- Rehighlight the buffer if colorizer is active +---@param buf number: Buffer number +---@param options table: Buffer options +---@param options_local table|nil: Buffer local variables +---@param use_local_lines boolean|nil Whether to use lines num range from options_local +---@return nil|boolean|number,table +function buffer.rehighlight(buf, options, options_local, use_local_lines) + if buf == 0 or buf == nil then + buf = api.nvim_get_current_buf() + end + + local ns = buffer.default_namespace + + local min, max + if use_local_lines and options_local then + min, max = options_local.__startline or 0, options_local.__endline or -1 + else + min, max = getrow(buf) + end + + local bool, returns = buffer.highlight(buf, ns, min, max, options, options_local or {}) + table.insert(returns.detach.functions, function() + BUFFER_LINES[buf] = nil + end) + + return bool, returns +end + +return buffer diff --git a/lua/colorizer/buffer_utils.lua b/lua/colorizer/buffer_utils.lua deleted file mode 100644 index 1f389c2..0000000 --- a/lua/colorizer/buffer_utils.lua +++ /dev/null @@ -1,254 +0,0 @@ ----Helper functions to highlight buffer smartly ---@module colorizer.buffer_utils -local api = vim.api -local buf_set_virtual_text = api.nvim_buf_set_extmark -local buf_get_lines = api.nvim_buf_get_lines -local create_namespace = api.nvim_create_namespace -local clear_namespace = api.nvim_buf_clear_namespace -local set_highlight = api.nvim_set_hl - -local color_utils = require "colorizer.color_utils" -local color_is_bright = color_utils.color_is_bright - -local sass = require "colorizer.sass" -local sass_update_variables = sass.sass_update_variables -local sass_cleanup = sass.sass_cleanup - -local tailwind = require "colorizer.tailwind" -local tailwind_setup_lsp = tailwind.tailwind_setup_lsp_colors -local tailwind_cleanup = tailwind.tailwind_cleanup - -local make_matcher = require("colorizer.matcher_utils").make_matcher - -local highlight_buffer, rehighlight_buffer -local BUFFER_LINES = {} ---- Default namespace used in `highlight_buffer` and `colorizer.attach_to_buffer`. --- @see highlight_buffer --- @see colorizer.attach_to_buffer -local DEFAULT_NAMESPACE = create_namespace "colorizer" -local HIGHLIGHT_NAME_PREFIX = "colorizer" ---- Highlight mode which will be use to render the colour -local HIGHLIGHT_MODE_NAMES = { - background = "mb", - foreground = "mf", - virtualtext = "mv", -} -local HIGHLIGHT_CACHE = {} - -local function parse_lines(buf, lines, line_start, options) - local loop_parse_fn = make_matcher(options) - if not loop_parse_fn then - return false - end - - local data = {} - for current_linenum, line in ipairs(lines) do - current_linenum = current_linenum - 1 + line_start - data[current_linenum] = data[current_linenum] or {} - - -- Upvalues are options and current_linenum - local i = 1 - while i < #line do - local length, rgb_hex = loop_parse_fn(line, i, buf) - if length and rgb_hex then - table.insert(data[current_linenum], { rgb_hex = rgb_hex, range = { i - 1, i + length - 1 } }) - i = i + length - else - i = i + 1 - end - end - end - - return data -end - ---- Clean the highlight cache -local function clear_hl_cache() - HIGHLIGHT_CACHE = {} -end - ---- 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, mode) - mode = 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 highlight_name then - return highlight_name - end - - -- convert from #fff to #ffffff - 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 - return highlight_name -end - -local function add_highlight(options, buf, ns, data, line_start, line_end) - clear_namespace(buf, ns, line_start, line_end) - - local mode = options.mode == "background" and "background" or "foreground" - if vim.tbl_contains({ "foreground", "background" }, options.mode) then - for linenr, hls in pairs(data) do - for _, hl in ipairs(hls) do - local hlname = create_highlight(hl.rgb_hex, mode) - api.nvim_buf_add_highlight(buf, ns, hlname, linenr, hl.range[1], hl.range[2]) - end - end - elseif options.mode == "virtualtext" then - for linenr, hls in pairs(data) do - for _, hl in ipairs(hls) do - local hlname = create_highlight(hl.rgb_hex, mode) - buf_set_virtual_text(0, ns, linenr, hl.range[2], { - end_col = hl.range[2], - virt_text = { { options.virtualtext or "■", hlname } }, - }) - end - end - 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`. ----@param buf number: buffer id ----@param ns number: The namespace id. Default is DEFAULT_NAMESPACE. Create it with `vim.api.create_namespace` ----@param lines table: the lines to highlight from the buffer. ----@param line_start number: line_start should be 0-indexed ----@param line_end number: Last line to highlight ----@param options table: Configuration options as described in `setup` ----@param options_local table: Buffer local variables ----@return nil|boolean|number,table -function highlight_buffer(buf, ns, lines, line_start, line_end, options, options_local) - local returns = { detach = { ns = {}, functions = {} } } - if buf == 0 or buf == nil then - buf = api.nvim_get_current_buf() - end - - ns = ns or DEFAULT_NAMESPACE - - -- only update sass varibled when text is changed - if options_local.__event ~= "WinScrolled" and options.sass and options.sass.enable then - table.insert(returns.detach.functions, sass_cleanup) - sass_update_variables(buf, 0, -1, nil, make_matcher(options.sass.parsers or { css = true }), options, options_local) - end - - local data = parse_lines(buf, lines, line_start, options) - add_highlight(options, buf, ns, data, line_start, line_end) - - if options.tailwind == "lsp" or options.tailwind == "both" then - tailwind_setup_lsp(buf, options, options_local, add_highlight) - table.insert(returns.detach.functions, tailwind_cleanup) - end - - return true, returns -end - --- get the amount lines to highlight -local function getrow(buf) - if not BUFFER_LINES[buf] then - BUFFER_LINES[buf] = {} - end - - local a = api.nvim_buf_call(buf, function() - return { - vim.fn.line "w0", - vim.fn.line "w$", - } - end) - local min, max - local new_min, new_max = a[1] - 1, a[2] - local old_min, old_max = BUFFER_LINES[buf]["min"], BUFFER_LINES[buf]["max"] - - if old_min and old_max then - -- Triggered for TextChanged autocmds - -- TODO: Find a way to just apply highlight to changed text lines - if (old_max == new_max) or (old_min == new_min) then - min, max = new_min, new_max - -- Triggered for WinScrolled autocmd - Scroll Down - elseif old_max < new_max then - min = old_max - max = new_max - -- Triggered for WinScrolled autocmd - Scroll Up - elseif old_max > new_max then - min = new_min - max = new_min + (old_max - new_max) - end - -- just in case a long jump was made - if max - min > new_max - new_min then - min = new_min - max = new_max - end - end - min = min or new_min - max = max or new_max - -- store current window position to be used later to incremently highlight - BUFFER_LINES[buf]["max"] = new_max - BUFFER_LINES[buf]["min"] = new_min - return min, max -end - ---- Rehighlight the buffer if colorizer is active ----@param buf number: Buffer number ----@param options table: Buffer options ----@param options_local table|nil: Buffer local variables ----@param use_local_lines boolean|nil Whether to use lines num range from options_local ----@return nil|boolean|number,table -function rehighlight_buffer(buf, options, options_local, use_local_lines) - if buf == 0 or buf == nil then - buf = api.nvim_get_current_buf() - end - - local ns = DEFAULT_NAMESPACE - - local min, max - if use_local_lines and options_local then - min, max = options_local.__startline or 0, options_local.__endline or -1 - else - min, max = getrow(buf) - end - local lines = buf_get_lines(buf, min, max, false) - - local bool, returns = highlight_buffer(buf, ns, lines, min, max, options, options_local or {}) - table.insert(returns.detach.functions, function() - BUFFER_LINES[buf] = nil - end) - return bool, returns -end - ---- @export -return { - DEFAULT_NAMESPACE = DEFAULT_NAMESPACE, - HIGHLIGHT_MODE_NAMES = HIGHLIGHT_MODE_NAMES, - clear_hl_cache = clear_hl_cache, - rehighlight_buffer = rehighlight_buffer, - highlight_buffer = highlight_buffer, -} diff --git a/lua/colorizer/color.lua b/lua/colorizer/color.lua new file mode 100644 index 0000000..711c8e4 --- /dev/null +++ b/lua/colorizer/color.lua @@ -0,0 +1,405 @@ +---Helper functions to parse different colour formats +--@module colorizer.color +local api = vim.api + +local bit = require "bit" +local floor, min, max = math.floor, math.min, math.max +local band, rshift, lshift, tohex = bit.band, bit.rshift, bit.lshift, bit.tohex + +local Trie = require "colorizer.trie" + +local utils = require "colorizer.utils" +local byte_is_alphanumeric = utils.byte_is_alphanumeric +local byte_is_hex = utils.byte_is_hex +local parse_hex = utils.parse_hex +local percent_or_hex = utils.percent_or_hex + +local color = {} + +local ARGB_MINIMUM_LENGTH = #"0xAARRGGBB" - 1 +---parse for 0xaarrggbb and return rgb hex. +-- a format used in android apps +---@param line string: line to parse +---@param i number: index of line from where to start parsing +---@return number|nil: index of line where the hex value ended +---@return string|nil: rgb hex value +function color.argb_hex_parser(line, i) + if #line < i + ARGB_MINIMUM_LENGTH then + return + end + + local j = i + 2 + + 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 + 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) + local rgb_hex = string.format("%02x%02x%02x", r, g, b) + return length, rgb_hex +end + +--- Converts an HSL color value to RGB. +---@param h number: Hue +---@param s number: Saturation +---@param l number: Lightness +---@return number|nil,number|nil,number|nil +function color.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) + else + q = l + s - l * s + end + local p = 2 * l - q + return 255 * color.hue_to_rgb(p, q, h + 1 / 3), + 255 * color.hue_to_rgb(p, q, h), + 255 * color.hue_to_rgb(p, q, h - 1 / 3) +end + +local CSS_HSL_FN_MINIMUM_LENGTH = #"hsl(0,0%,0%)" - 1 +---Parse for hsl() css function and return rgb hex. +---@param line string: Line to parse +---@param i number: Index of line from where to start parsing +---@return number|nil: Index of line where the hsl function ended +---@return string|nil: rgb hex value +function color.hsl_function_parser(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 = color.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 + +local CSS_HSLA_FN_MINIMUM_LENGTH = #"hsla(0,0%,0%,0)" - 1 +---Parse for hsl() css function and return rgb hex. +---@param line string: Line to parse +---@param i number: Index of line from where to start parsing +---@return number|nil: Index of line where the hsla function ended +---@return string|nil: rgb hex value +function color.hsla_function_parser(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 = color.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 + +---Convert hsl colour values to rgb. +-- Source: https://gist.github.com/mjackson/5311256 +---@param p number +---@param q number +---@param t number +---@return number +function color.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 + +---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 +---@param r number: Red +---@param g number: Green +---@param b number: Blue +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 + +local COLOR_MAP +local COLOR_TRIE +local COLOR_NAME_MINLEN, COLOR_NAME_MAXLEN +local COLOR_NAME_SETTINGS = { lowercase = true, strip_digits = false } +local TAILWIND_ENABLED = false +--- Grab all the colour values from `vim.api.nvim_get_color_map` and create a lookup table. +-- COLOR_MAP is used to store the colour values +---@param line string: Line to parse +---@param i number: Index of line from where to start parsing +---@param opts table: Currently contains whether tailwind is enabled or not +function color.name_parser(line, i, opts) + --- Setup the COLOR_MAP and COLOR_TRIE + if not COLOR_TRIE or opts.tailwind ~= TAILWIND_ENABLED 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 + if opts and opts.tailwind then + if opts.tailwind == true or opts.tailwind == "normal" or opts.tailwind == "both" then + local tailwind = require "colorizer.tailwind_colors" + -- setup tailwind colors + for k, v in pairs(tailwind.colors) do + for _, pre in ipairs(tailwind.prefixes) do + local name = pre .. "-" .. k + COLOR_NAME_MINLEN = COLOR_NAME_MINLEN and min(#name, COLOR_NAME_MINLEN) or #name + COLOR_NAME_MAXLEN = COLOR_NAME_MAXLEN and max(#name, COLOR_NAME_MAXLEN) or #name + COLOR_MAP[name] = v + COLOR_TRIE:insert(name) + end + end + end + end + TAILWIND_ENABLED = opts.tailwind + end + + if #line < i + COLOR_NAME_MINLEN - 1 then + return + end + + if i > 1 and byte_is_alphanumeric(line:byte(i - 1)) 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 CSS_RGB_FN_MINIMUM_LENGTH = #"rgb(0,0,0)" - 1 +---Parse for rgb() css function and return rgb hex. +---@param line string: Line to parse +---@param i number: Index of line from where to start parsing +---@return number|nil: Index of line where the rgb function ended +---@return string|nil: rgb hex value +function color.rgb_function_parser(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 + +local CSS_RGBA_FN_MINIMUM_LENGTH = #"rgba(0,0,0,0)" - 1 +---Parse for rgba() css function and return rgb hex. +-- Todo consider removing the regexes here +-- Todo this might not be the best approach to alpha channel. +-- Things like pumblend might be useful here. +---@param line string: Line to parse +---@param i number: Index of line from where to start parsing +---@return number|nil: Index of line where the rgba function ended +---@return string|nil: rgb hex value +function color.rgba_function_parser(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 + +---parse for #rrggbbaa and return rgb hex. +-- a format used in android apps +---@param line string: line to parse +---@param i number: index of line from where to start parsing +---@param opts table: Containing minlen, maxlen, valid_lengths +---@return number|nil: index of line where the hex value ended +---@return string|nil: rgb hex value +function color.rgba_hex_parser(line, i, opts) + local minlen, maxlen, valid_lengths = opts.minlen, opts.maxlen, opts.valid_lengths + local j = i + 1 + if #line < j + minlen - 1 then + return + end + + if i > 1 and byte_is_alphanumeric(line:byte(i - 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) + local rgb_hex = string.format("%02x%02x%02x", r, g, b) + return 9, rgb_hex + end + return (valid_lengths[length - 1] and length), line:sub(i + 1, i + length - 1) +end + +return color diff --git a/lua/colorizer/color_utils.lua b/lua/colorizer/color_utils.lua deleted file mode 100644 index 2a7e7ab..0000000 --- a/lua/colorizer/color_utils.lua +++ /dev/null @@ -1,411 +0,0 @@ ----Helper functions to parse different colour formats ---@module colorizer.color_utils -local Trie = require "colorizer.trie" - -local utils = require "colorizer.utils" -local byte_is_alphanumeric = utils.byte_is_alphanumeric -local byte_is_hex = utils.byte_is_hex -local parse_hex = utils.parse_hex -local percent_or_hex = utils.percent_or_hex - -local bit = require "bit" -local floor, min, max = math.floor, math.min, math.max -local band, rshift, lshift, tohex = bit.band, bit.rshift, bit.lshift, bit.tohex - -local api = vim.api - ----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 ----@param r number: Red ----@param g number: Green ----@param b number: Blue -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 - ----Convert hsl colour values to rgb. --- Source: https://gist.github.com/mjackson/5311256 ----@param p number ----@param q number ----@param t number ----@return number -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 COLOR_MAP -local COLOR_TRIE -local COLOR_NAME_MINLEN, COLOR_NAME_MAXLEN -local COLOR_NAME_SETTINGS = { lowercase = true, strip_digits = false } -local TAILWIND_ENABLED = false ---- Grab all the colour values from `vim.api.nvim_get_color_map` and create a lookup table. --- COLOR_MAP is used to store the colour values ----@param line string: Line to parse ----@param i number: Index of line from where to start parsing ----@param opts table: Currently contains whether tailwind is enabled or not -local function color_name_parser(line, i, opts) - --- Setup the COLOR_MAP and COLOR_TRIE - if not COLOR_TRIE or opts.tailwind ~= TAILWIND_ENABLED 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 - if opts and opts.tailwind then - if opts.tailwind == true or opts.tailwind == "normal" or opts.tailwind == "both" then - local tailwind = require "colorizer.tailwind_colors" - -- setup tailwind colors - for k, v in pairs(tailwind.colors) do - for _, pre in ipairs(tailwind.prefixes) do - local name = pre .. "-" .. k - COLOR_NAME_MINLEN = COLOR_NAME_MINLEN and min(#name, COLOR_NAME_MINLEN) or #name - COLOR_NAME_MAXLEN = COLOR_NAME_MAXLEN and max(#name, COLOR_NAME_MAXLEN) or #name - COLOR_MAP[name] = v - COLOR_TRIE:insert(name) - end - end - end - end - TAILWIND_ENABLED = opts.tailwind - end - - if #line < i + COLOR_NAME_MINLEN - 1 then - return - end - - if i > 1 and byte_is_alphanumeric(line:byte(i - 1)) 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 - ---- Converts an HSL color value to RGB. ----@param h number: Hue ----@param s number: Saturation ----@param l number: Lightness ----@return number|nil,number|nil,number|nil -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) - else - q = l + s - l * s - 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) -end - -local CSS_RGB_FN_MINIMUM_LENGTH = #"rgb(0,0,0)" - 1 ----Parse for rgb() css function and return rgb hex. ----@param line string: Line to parse ----@param i number: Index of line from where to start parsing ----@return number|nil: Index of line where the rgb function ended ----@return string|nil: rgb hex value -local function rgb_function_parser(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 - -local CSS_RGBA_FN_MINIMUM_LENGTH = #"rgba(0,0,0,0)" - 1 ----Parse for rgba() css function and return rgb hex. --- Todo consider removing the regexes here --- Todo this might not be the best approach to alpha channel. --- Things like pumblend might be useful here. ----@param line string: Line to parse ----@param i number: Index of line from where to start parsing ----@return number|nil: Index of line where the rgba function ended ----@return string|nil: rgb hex value -local function rgba_function_parser(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 - -local CSS_HSL_FN_MINIMUM_LENGTH = #"hsl(0,0%,0%)" - 1 ----Parse for hsl() css function and return rgb hex. ----@param line string: Line to parse ----@param i number: Index of line from where to start parsing ----@return number|nil: Index of line where the hsl function ended ----@return string|nil: rgb hex value -local function hsl_function_parser(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 - -local CSS_HSLA_FN_MINIMUM_LENGTH = #"hsla(0,0%,0%,0)" - 1 ----Parse for hsl() css function and return rgb hex. ----@param line string: Line to parse ----@param i number: Index of line from where to start parsing ----@return number|nil: Index of line where the hsla function ended ----@return string|nil: rgb hex value -local function hsla_function_parser(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 - -local ARGB_MINIMUM_LENGTH = #"0xAARRGGBB" - 1 ----parse for 0xaarrggbb and return rgb hex. --- a format used in android apps ----@param line string: line to parse ----@param i number: index of line from where to start parsing ----@return number|nil: index of line where the hex value ended ----@return string|nil: rgb hex value -local function argb_hex_parser(line, i) - if #line < i + ARGB_MINIMUM_LENGTH then - return - end - - local j = i + 2 - - 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 - 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) - local rgb_hex = string.format("%02x%02x%02x", r, g, b) - return length, rgb_hex -end - ----parse for #rrggbbaa and return rgb hex. --- a format used in android apps ----@param line string: line to parse ----@param i number: index of line from where to start parsing ----@param opts table: Containing minlen, maxlen, valid_lengths ----@return number|nil: index of line where the hex value ended ----@return string|nil: rgb hex value -local function rgba_hex_parser(line, i, opts) - local minlen, maxlen, valid_lengths = opts.minlen, opts.maxlen, opts.valid_lengths - local j = i + 1 - if #line < j + minlen - 1 then - return - end - - if i > 1 and byte_is_alphanumeric(line:byte(i - 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) - local rgb_hex = string.format("%02x%02x%02x", r, g, b) - return 9, rgb_hex - end - return (valid_lengths[length - 1] and length), line:sub(i + 1, i + length - 1) -end - ---- @export -return { - color_is_bright = color_is_bright, - color_name_parser = color_name_parser, - rgba_hex_parser = rgba_hex_parser, - argb_hex_parser = argb_hex_parser, - rgb_function_parser = rgb_function_parser, - rgba_function_parser = rgba_function_parser, - hsl_function_parser = hsl_function_parser, - hsla_function_parser = hsla_function_parser, -} diff --git a/lua/colorizer/matcher.lua b/lua/colorizer/matcher.lua new file mode 100644 index 0000000..dba67c0 --- /dev/null +++ b/lua/colorizer/matcher.lua @@ -0,0 +1,154 @@ +---Helper functions for colorizer to enable required parsers +--@module colorizer.matcher +local Trie = require "colorizer.trie" +local min, max = math.min, math.max + +local color = require "colorizer.color" +local color_name_parser = color.name_parser +local rgba_hex_parser = color.rgba_hex_parser + +local sass = require "colorizer.sass" +local sass_name_parser = sass.name_parser + +local B_HASH, DOLLAR_HASH = ("#"):byte(), ("$"):byte() + +local parser = { + ["_0x"] = color.argb_hex_parser, + ["_rgb"] = color.rgb_function_parser, + ["_rgba"] = color.rgba_function_parser, + ["_hsl"] = color.hsl_function_parser, + ["_hsla"] = color.hsla_function_parser, +} + +local matcher = {} + +---Form a trie stuct with the given prefixes +---@param matchers table: List of prefixes, {"rgb", "hsl"} +---@param matchers_trie table: Table containing information regarding non-trie based parsers +---@return function: function which will just parse the line for enabled parsers +function matcher.compile(matchers, matchers_trie) + local trie = Trie(matchers_trie) + + local function parse_fn(line, i, buf) + -- prefix # + if matchers.rgba_hex_parser then + if line:byte(i) == B_HASH then + return rgba_hex_parser(line, i, matchers.rgba_hex_parser) + end + end + + -- prefix $, SASS Colour names + if matchers.sass_name_parser then + if line:byte(i) == DOLLAR_HASH then + return sass_name_parser(line, i, buf) + end + end + + -- Prefix 0x, rgba, rgb, hsla, hsl + local prefix = trie:longest_prefix(line, i) + if prefix then + local fn = "_" .. prefix + if parser[fn] then + return parser[fn](line, i, matchers[fn]) + end + end + + -- Colour names + if matchers.color_name_parser then + return color_name_parser(line, i, matchers.color_name_parser) + end + end + return parse_fn +end + +local MATCHER_CACHE = {} +---Parse the given options and return a function with enabled parsers. +--if no parsers enabled then return false +--Do not try make the function again if it is present in the cache +---@param options table: options created in `colorizer.setup` +---@return function|boolean: function which will just parse the line for enabled parsers +function matcher.make(options) + local enable_names = options.css or options.names + local enable_sass = options.sass and options.sass.enable + local enable_tailwind = options.tailwind + 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 = 0 + + (enable_names and 1 or 0) + + (enable_RGB and 1 or 1) + + (enable_RRGGBB and 1 or 2) + + (enable_RRGGBBAA and 1 or 3) + + (enable_AARRGGBB and 1 or 4) + + (enable_rgb and 1 or 5) + + (enable_hsl and 1 or 6) + + ((enable_tailwind == true or enable_tailwind == "normal") and 1 or 7) + + (enable_tailwind == "lsp" and 1 or 8) + + (enable_tailwind == "both" and 1 or 9) + + (enable_sass and 1 or 10) + + if matcher_key == 0 then + return false + end + + local loop_parse_fn = MATCHER_CACHE[matcher_key] + if loop_parse_fn then + return loop_parse_fn + end + + local matchers = {} + local matchers_prefix = {} + matchers.max_prefix_length = 0 + + if enable_names then + matchers.color_name_parser = { tailwind = options.tailwind } + end + + if enable_sass then + matchers.sass_name_parser = true + end + + 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 + matchers.rgba_hex_parser = {} + matchers.rgba_hex_parser.valid_lengths = valid_lengths + matchers.rgba_hex_parser.maxlen = maxlen + matchers.rgba_hex_parser.minlen = minlen + end + + if enable_AARRGGBB then + table.insert(matchers_prefix, "0x") + end + + if enable_rgb and enable_hsl then + table.insert(matchers_prefix, "hsla") + table.insert(matchers_prefix, "rgba") + table.insert(matchers_prefix, "rgb") + table.insert(matchers_prefix, "hsl") + elseif enable_rgb then + table.insert(matchers_prefix, "rgba") + table.insert(matchers_prefix, "rgb") + elseif enable_hsl then + table.insert(matchers_prefix, "hsla") + table.insert(matchers_prefix, "hsl") + end + + loop_parse_fn = matcher.compile(matchers, matchers_prefix) + MATCHER_CACHE[matcher_key] = loop_parse_fn + + return loop_parse_fn +end + +return matcher diff --git a/lua/colorizer/matcher_utils.lua b/lua/colorizer/matcher_utils.lua deleted file mode 100644 index 3f8047a..0000000 --- a/lua/colorizer/matcher_utils.lua +++ /dev/null @@ -1,154 +0,0 @@ ----Helper functions for colorizer to enable required parsers ---@module colorizer.matcher_utils -local Trie = require "colorizer.trie" -local min, max = math.min, math.max - -local color_utils = require "colorizer.color_utils" -local color_name_parser = color_utils.color_name_parser -local rgba_hex_parser = color_utils.rgba_hex_parser - -local sass = require "colorizer.sass" -local sass_name_parser = sass.sass_name_parser - -local parser = {} -parser["_0x"] = color_utils.argb_hex_parser -parser["_rgb"] = color_utils.rgb_function_parser -parser["_rgba"] = color_utils.rgba_function_parser -parser["_hsl"] = color_utils.hsl_function_parser -parser["_hsla"] = color_utils.hsla_function_parser -local b_hash, dollar_hash = ("#"):byte(), ("$"):byte() - ----Form a trie stuct with the given prefixes ----@param matchers table: List of prefixes, {"rgb", "hsl"} ----@param matchers_trie table: Table containing information regarding non-trie based parsers ----@return function: function which will just parse the line for enabled parsers -local function compile_matcher(matchers, matchers_trie) - local trie = Trie(matchers_trie) - - local function parse_fn(line, i, buf) - -- prefix # - if matchers.rgba_hex_parser then - if line:byte(i) == b_hash then - return rgba_hex_parser(line, i, matchers.rgba_hex_parser) - end - end - - -- prefix $, SASS Colour names - if matchers.sass_name_parser then - if line:byte(i) == dollar_hash then - return sass_name_parser(line, i, buf) - end - end - - -- Prefix 0x, rgba, rgb, hsla, hsl - local prefix = trie:longest_prefix(line, i) - if prefix then - local fn = "_" .. prefix - if parser[fn] then - return parser[fn](line, i, matchers[fn]) - end - end - - -- Colour names - if matchers.color_name_parser then - return color_name_parser(line, i, matchers.color_name_parser) - end - end - return parse_fn -end - -local MATCHER_CACHE = {} ----Parse the given options and return a function with enabled parsers. ---if no parsers enabled then return false ---Do not try make the function again if it is present in the cache ----@param options table: options created in `colorizer.setup` ----@return function|boolean: function which will just parse the line for enabled parsers -local function make_matcher(options) - local enable_names = options.css or options.names - local enable_sass = options.sass and options.sass.enable - local enable_tailwind = options.tailwind - 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 = 0 - + (enable_names and 1 or 0) - + (enable_RGB and 1 or 1) - + (enable_RRGGBB and 1 or 2) - + (enable_RRGGBBAA and 1 or 3) - + (enable_AARRGGBB and 1 or 4) - + (enable_rgb and 1 or 5) - + (enable_hsl and 1 or 6) - + ((enable_tailwind == true or enable_tailwind == "normal") and 1 or 7) - + (enable_tailwind == "lsp" and 1 or 8) - + (enable_tailwind == "both" and 1 or 9) - + (enable_sass and 1 or 10) - - if matcher_key == 0 then - return false - end - - local loop_parse_fn = MATCHER_CACHE[matcher_key] - if loop_parse_fn then - return loop_parse_fn - end - - local matchers = {} - local matchers_prefix = {} - matchers.max_prefix_length = 0 - - if enable_names then - matchers.color_name_parser = { tailwind = options.tailwind } - end - - if enable_sass then - matchers.sass_name_parser = true - end - - 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 - matchers.rgba_hex_parser = {} - matchers.rgba_hex_parser.valid_lengths = valid_lengths - matchers.rgba_hex_parser.maxlen = maxlen - matchers.rgba_hex_parser.minlen = minlen - end - - if enable_AARRGGBB then - table.insert(matchers_prefix, "0x") - end - - -- do not mess with the sequence, hsla before hsl, etc - if enable_rgb and enable_hsl then - table.insert(matchers_prefix, "hsla") - table.insert(matchers_prefix, "rgba") - table.insert(matchers_prefix, "rgb") - table.insert(matchers_prefix, "hsl") - elseif enable_rgb then - table.insert(matchers_prefix, "rgba") - table.insert(matchers_prefix, "rgb") - elseif enable_hsl then - table.insert(matchers_prefix, "hsla") - table.insert(matchers_prefix, "hsl") - end - - loop_parse_fn = compile_matcher(matchers, matchers_prefix) - MATCHER_CACHE[matcher_key] = loop_parse_fn - - return loop_parse_fn -end - ---- @export -return { - make_matcher = make_matcher, -} diff --git a/lua/colorizer/sass.lua b/lua/colorizer/sass.lua index a003624..429ce2c 100644 --- a/lua/colorizer/sass.lua +++ b/lua/colorizer/sass.lua @@ -7,6 +7,8 @@ local utils = require "colorizer.utils" local get_last_modified = utils.get_last_modified local watch_file = utils.watch_file +local sass = {} + local DOLLAR_HASH = ("$"):byte() local AT_HASH = ("@"):byte() local COLON_HASH = (";"):byte() @@ -29,7 +31,7 @@ end --- Cleanup sass variables and watch handlers ---@param buf number -local function sass_cleanup(buf) +function sass.cleanup(buf) remove_unused_imports(buf, api.nvim_buf_get_name(buf)) SASS[buf] = nil end @@ -40,8 +42,8 @@ end ---@param i number: Index of line from where to start parsing ---@param buf number ---@return number|nil, string|nil -local function sass_name_parser(line, i, buf) - local variable_name = line:sub(i):match "^%$([%w_-]+)" +function sass.name_parser(line, i, buf) + local variable_name = line:match("^%$([%w_-]+)", i) if variable_name then local rgb_hex = SASS[buf].DEFINITIONS_ALL[variable_name] if rgb_hex then @@ -194,6 +196,7 @@ local function sass_parse_lines(buf, line_start, content, name) local lastm = get_last_modified(v) if lastm then + SASS[buf].IMPORTS[name] = SASS[buf].IMPORTS[name] or {} SASS[buf].IMPORTS[name][v] = lastm local cc, inde = {}, 0 for l in io.lines(v) do @@ -204,12 +207,7 @@ local function sass_parse_lines(buf, line_start, content, name) cc = nil end - require("colorizer.buffer_utils").rehighlight_buffer( - buf, - SASS[buf].OPTIONS, - SASS[buf].LOCAL_OPTIONS, - true - ) + require("colorizer.buffer").rehighlight(buf, SASS[buf].OPTIONS, SASS[buf].LOCAL_OPTIONS, true) end SASS[buf].WATCH_IMPORTS[name][v] = watch_file(v, watch_callback) end @@ -234,7 +232,7 @@ local function sass_parse_lines(buf, line_start, content, name) end end -- sass_parse_lines end ---- Parse the given lines for sass variabled and add to SASS[buf].DEFINITIONS_ALL. +--- Parse the given lines for sass variabled and add to `SASS[buf].DEFINITIONS_ALL`. -- which is then used in |sass_name_parser| -- If lines are not given, then fetch the lines with line_start and line_end ---@param buf number @@ -244,7 +242,7 @@ end -- sass_parse_lines end ---@param color_parser function|boolean ---@param options table: Buffer options ---@param options_local table|nil: Buffer local variables -local function sass_update_variables(buf, line_start, line_end, lines, color_parser, options, options_local) +function sass.update_variables(buf, line_start, line_end, lines, color_parser, options, options_local) lines = lines or vim.api.nvim_buf_get_lines(buf, line_start, line_end, false) if not SASS[buf] then @@ -308,9 +306,4 @@ local function sass_update_variables(buf, line_start, line_end, lines, color_par SASS[buf].DEFINITIONS_RECURSIVE_CURRENT_ABSOLUTE = nil end ---- @export -return { - sass_cleanup = sass_cleanup, - sass_name_parser = sass_name_parser, - sass_update_variables = sass_update_variables, -} +return sass diff --git a/lua/colorizer/tailwind.lua b/lua/colorizer/tailwind.lua index 88094f5..839cd0c 100644 --- a/lua/colorizer/tailwind.lua +++ b/lua/colorizer/tailwind.lua @@ -2,6 +2,8 @@ --@module colorizer.tailwind local api = vim.api +local tailwind = {} + -- use a different namespace for tailwind as will be cleared if kept in Default namespace local DEFAULT_NAMESPACE_TAILWIND = api.nvim_create_namespace "colorizer_tailwind" @@ -9,23 +11,27 @@ local TAILWIND = {} --- Cleanup tailwind variables and autocmd ---@param buf number -local function tailwind_cleanup(buf) +function tailwind.cleanup(buf) pcall(api.nvim_del_autocmd, TAILWIND[buf] and TAILWIND[buf].AU_ID[1]) pcall(api.nvim_del_autocmd, TAILWIND[buf] and TAILWIND[buf].AU_ID[2]) api.nvim_buf_clear_namespace(buf, DEFAULT_NAMESPACE_TAILWIND, 0, -1) TAILWIND[buf] = nil end -local function highlight_buffer_tailwind(buf, ns, options, add_highlight) +local function highlight_tailwind(buf, ns, options, add_highlight) -- it can take some time to actually fetch the results -- on top of that, tailwindcss is quite slow in neovim vim.defer_fn(function() + if not TAILWIND[buf] or not TAILWIND[buf].CLIENT or not TAILWIND[buf].CLIENT.request then + return + end + local opts = { textDocument = vim.lsp.util.make_text_document_params() } --@local ---@diagnostic disable-next-line: param-type-mismatch TAILWIND[buf].CLIENT.request("textDocument/documentColor", opts, function(err, results, _, _) if err == nil and results ~= nil then - local datas, line_start, line_end = {}, nil, nil + local data, line_start, line_end = {}, nil, nil for _, color in pairs(results) do local cur_line = color.range.start.line if line_start then @@ -50,22 +56,22 @@ local function highlight_buffer_tailwind(buf, ns, options, add_highlight) local first_col = color.range.start.character local end_col = color.range["end"].character - datas[cur_line] = datas[cur_line] or {} - table.insert(datas[cur_line], { rgb_hex = rgb_hex, range = { first_col, end_col } }) + data[cur_line] = data[cur_line] or {} + table.insert(data[cur_line], { rgb_hex = rgb_hex, range = { first_col, end_col } }) end - add_highlight(options, buf, ns, datas, line_start or 0, line_end and (line_end + 2) or -1) + add_highlight(buf, ns, line_start or 0, line_end and (line_end + 2) or -1, data, options) end end) end, 10) end --- highlight buffer using values returned by tailwindcss --- To see these table information, see |colorizer.buffer_utils| +-- To see these table information, see |colorizer.buffer| ---@param buf number ---@param options table ---@param options_local table ---@param add_highlight function -local function tailwind_setup_lsp_colors(buf, options, options_local, add_highlight) +function tailwind.setup_lsp_colors(buf, options, options_local, add_highlight) TAILWIND[buf] = TAILWIND[buf] or {} TAILWIND[buf].AU_ID = TAILWIND[buf].AU_ID or {} @@ -73,7 +79,7 @@ local function tailwind_setup_lsp_colors(buf, options, options_local, add_highli if vim.version().minor >= 8 then -- create the autocmds so tailwind colours only activate when tailwindcss lsp is active if not TAILWIND[buf].AU_CREATED then - tailwind_cleanup(buf) + tailwind.cleanup(buf) TAILWIND[buf].AU_ID[1] = api.nvim_create_autocmd("LspAttach", { group = options_local.__augroup_id, buffer = buf, @@ -84,7 +90,7 @@ local function tailwind_setup_lsp_colors(buf, options, options_local, add_highli -- wait 100 ms for the first request TAILWIND[buf].CLIENT = client vim.defer_fn(function() - highlight_buffer_tailwind(buf, DEFAULT_NAMESPACE_TAILWIND, options, add_highlight) + highlight_tailwind(buf, DEFAULT_NAMESPACE_TAILWIND, options, add_highlight) end, 100) end end @@ -95,7 +101,7 @@ local function tailwind_setup_lsp_colors(buf, options, options_local, add_highli group = options_local.__augroup_id, buffer = buf, callback = function() - tailwind_cleanup(buf) + tailwind.cleanup(buf) end, }) TAILWIND[buf].AU_CREATED = true @@ -117,16 +123,19 @@ local function tailwind_setup_lsp_colors(buf, options, options_local, add_highli for _, cl in pairs(tailwind_client) do if cl["name"] == "tailwindcss" then tailwind_client = cl - ok = false + ok = true break end end if - vim.tbl_isempty(tailwind_client or {}) - or not tailwind_client - or not tailwind_client.supports_method - or not tailwind_client.supports_method "textDocument/documentColor" + not ok + and ( + vim.tbl_isempty(tailwind_client or {}) + or not tailwind_client + or not tailwind_client.supports_method + or not tailwind_client.supports_method "textDocument/documentColor" + ) then return true end @@ -135,20 +144,16 @@ local function tailwind_setup_lsp_colors(buf, options, options_local, add_highli -- wait 500 ms for the first request vim.defer_fn(function() - highlight_buffer_tailwind(buf, DEFAULT_NAMESPACE_TAILWIND, options, add_highlight) - end, 500) + highlight_tailwind(buf, DEFAULT_NAMESPACE_TAILWIND, options, add_highlight) + end, 1000) return true end -- only try to do tailwindcss highlight if lsp is attached if TAILWIND[buf].CLIENT then - highlight_buffer_tailwind(buf, DEFAULT_NAMESPACE_TAILWIND, options, add_highlight) + highlight_tailwind(buf, DEFAULT_NAMESPACE_TAILWIND, options, add_highlight) end end ----@export -return { - tailwind_cleanup = tailwind_cleanup, - tailwind_setup_lsp_colors = tailwind_setup_lsp_colors, -} +return tailwind diff --git a/lua/colorizer/utils.lua b/lua/colorizer/utils.lua index 9dad9d8..c83c32d 100644 --- a/lua/colorizer/utils.lua +++ b/lua/colorizer/utils.lua @@ -5,6 +5,8 @@ local band, bor, rshift, lshift = bit.band, bit.bor, bit.rshift, bit.lshift local uv = vim.loop +local utils = {} + -- -- 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 @@ -22,36 +24,38 @@ local CATEGORY_ALPHA = lshift(1, 1) local CATEGORY_HEX = lshift(1, 2) local CATEGORY_ALPHANUM = bor(CATEGORY_ALPHA, CATEGORY_DIGIT) --- do not run the loop multiple times -local b = string.byte -local byte_values = { ["0"] = b "0", ["9"] = b "9", ["a"] = b "a", ["f"] = b "f", ["z"] = b "z" } -local extra_char = { [b "-"] = true } - -for i = 0, 255 do - local v = 0 - local lowercase = bor(i, 0x20) - -- Digit is bit 1 - if i >= byte_values["0"] and i <= byte_values["9"] then - v = bor(v, lshift(1, 0)) - v = bor(v, lshift(1, 2)) - v = bor(v, lshift(i - byte_values["0"], 4)) - elseif lowercase >= byte_values["a"] and lowercase <= byte_values["z"] then - -- Alpha is bit 2 - v = bor(v, lshift(1, 1)) - if lowercase <= byte_values["f"] then +do + -- do not run the loop multiple times + local b = string.byte + local byte_values = { ["0"] = b "0", ["9"] = b "9", ["a"] = b "a", ["f"] = b "f", ["z"] = b "z" } + local extra_char = { [b "-"] = true } + + for i = 0, 255 do + local v = 0 + local lowercase = bor(i, 0x20) + -- Digit is bit 1 + if i >= byte_values["0"] and i <= byte_values["9"] then + v = bor(v, lshift(1, 0)) v = bor(v, lshift(1, 2)) - v = bor(v, lshift(lowercase - byte_values["a"] + 10, 4)) + v = bor(v, lshift(i - byte_values["0"], 4)) + elseif lowercase >= byte_values["a"] and lowercase <= byte_values["z"] then + -- Alpha is bit 2 + v = bor(v, lshift(1, 1)) + if lowercase <= byte_values["f"] then + v = bor(v, lshift(1, 2)) + v = bor(v, lshift(lowercase - byte_values["a"] + 10, 4)) + end + elseif extra_char[i] then + v = i end - elseif extra_char[i] then - v = i + BYTE_CATEGORY[i] = v end - BYTE_CATEGORY[i] = v end ---Obvious. ---@param byte number ---@return boolean -local function byte_is_alphanumeric(byte) +function utils.byte_is_alphanumeric(byte) local category = BYTE_CATEGORY[byte] return band(category, CATEGORY_ALPHANUM) ~= 0 end @@ -59,15 +63,33 @@ end ---Obvious. ---@param byte number ---@return boolean -local function byte_is_hex(byte) +function utils.byte_is_hex(byte) return band(BYTE_CATEGORY[byte], CATEGORY_HEX) ~= 0 end +--- Get last modified time of a file +---@param path string: file path +---@return number|nil: modified time +function utils.get_last_modified(path) + local fd = uv.fs_open(path, "r", 438) + if not fd then + return + end + + local stat = uv.fs_fstat(fd) + uv.fs_close(fd) + if stat then + return stat.mtime.nsec + else + return + end +end + ---Merge two tables. -- -- todo: Remove this and use `vim.tbl_deep_extend` ---@return table -local function merge(...) +function utils.merge(...) local res = {} for i = 1, select("#", ...) do local o = select(i, ...) @@ -84,7 +106,7 @@ end --- Obvious. ---@param byte number ---@return number -local function parse_hex(byte) +function utils.parse_hex(byte) return rshift(BYTE_CATEGORY[byte], 4) end @@ -92,7 +114,7 @@ local b_percent = string.byte "%" --- Obvious. ---@param v string ---@return number|nil -local function percent_or_hex(v) +function utils.percent_or_hex(v) if v:byte(-1) == b_percent then return tonumber(v:sub(1, -2)) / 100 * 255 end @@ -103,30 +125,12 @@ local function percent_or_hex(v) return x end ---- Get last modified time of a file ----@param path string: file path ----@return number|nil: modified time -local function get_last_modified(path) - local fd = uv.fs_open(path, "r", 438) - if not fd then - return - end - - local stat = uv.fs_fstat(fd) - uv.fs_close(fd) - if stat then - return stat.mtime.nsec - else - return - end -end - --- Watch a file for changes and execute callback ---@param path string: File path ---@param callback function: Callback to execute ---@param ... array: params for callback ---@return function|nil -local function watch_file(path, callback, ...) +function utils.watch_file(path, callback, ...) if not path or type(callback) ~= "function" then return end @@ -145,7 +149,7 @@ local function watch_file(path, callback, ...) callback(filename, unpack(args)) -- Debounce: stop/start. handle:stop() - if not err or not get_last_modified(filename) then + if not err or not utils.get_last_modified(filename) then start() end end @@ -165,13 +169,4 @@ local function watch_file(path, callback, ...) return handle end ---- @export -return { - byte_is_alphanumeric = byte_is_alphanumeric, - byte_is_hex = byte_is_hex, - merge = merge, - parse_hex = parse_hex, - percent_or_hex = percent_or_hex, - get_last_modified = get_last_modified, - watch_file = watch_file, -} +return utils -- cgit v1.2.3-70-g09d2