mirror of
https://github.com/norcalli/nvim-colorizer.lua.git
synced 2026-09-10 07:06:14 -04:00
Initial commit.
This commit is contained in:
commit
f0c8b75cc4
247
lua/colorizer.lua
Normal file
247
lua/colorizer.lua
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
--- Highlights terminal CSI ANSI color codes.
|
||||
-- @module terminal
|
||||
local nvim = require 'nvim'
|
||||
local Trie = require 'trie'
|
||||
|
||||
--- 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 = nvim.create_namespace 'colorizer'
|
||||
|
||||
local COLOR_MAP
|
||||
local COLOR_TRIE
|
||||
|
||||
local function initialize_trie()
|
||||
if not COLOR_TRIE then
|
||||
COLOR_MAP = nvim.get_color_map()
|
||||
COLOR_TRIE = Trie()
|
||||
|
||||
for k in pairs(COLOR_MAP) do
|
||||
COLOR_TRIE:insert(k)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local b_a = string.byte('a')
|
||||
local b_z = string.byte('z')
|
||||
local b_A = string.byte('A')
|
||||
local b_Z = string.byte('Z')
|
||||
local b_0 = string.byte('0')
|
||||
local b_9 = string.byte('9')
|
||||
local b_f = string.byte('f')
|
||||
local b_hash = string.byte('#')
|
||||
local bnot = bit.bnot
|
||||
local band, bor, bxor = bit.band, bit.bor, bit.bxor
|
||||
local lshift, rshift, rol = bit.lshift, bit.rshift, bit.rol
|
||||
|
||||
-- TODO use lookup table?
|
||||
local function byte_is_hex(byte)
|
||||
byte = bor(byte, 0x20)
|
||||
return (byte >= b_0 and byte <= b_9) or (byte >= b_a and byte <= b_f)
|
||||
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
|
||||
|
||||
local HIGHLIGHT_NAME_PREFIX = "colorizer_"
|
||||
--- Make a deterministic name for a highlight given these attributes
|
||||
local function make_highlight_name(rgb)
|
||||
return table.concat {HIGHLIGHT_NAME_PREFIX, rgb}
|
||||
end
|
||||
|
||||
local highlight_cache = {}
|
||||
|
||||
-- Ref: https://stackoverflow.com/questions/1252539/most-efficient-way-to-determine-if-a-lua-table-is-empty-contains-no-entries
|
||||
local function table_is_empty(t)
|
||||
return next(t) == nil
|
||||
end
|
||||
|
||||
local function create_highlight(rgb_hex, options)
|
||||
-- TODO validate rgb format?
|
||||
local highlight_name = highlight_cache[rgb_hex]
|
||||
-- Look up in our cache.
|
||||
if not highlight_name then
|
||||
-- Create the highlight
|
||||
highlight_name = make_highlight_name(rgb_hex)
|
||||
if options.mode == 'foreground' then
|
||||
nvim.ex.highlight(highlight_name, "guifg=#"..rgb_hex)
|
||||
else
|
||||
local r, g, b = rgb_hex:sub(1,2), rgb_hex:sub(3,4), rgb_hex:sub(5,6)
|
||||
r, g, b = tonumber(r,16), tonumber(g,16), tonumber(b,16)
|
||||
local fg_color
|
||||
if color_is_bright(r,g,b) then
|
||||
fg_color = "Black"
|
||||
else
|
||||
fg_color = "White"
|
||||
end
|
||||
nvim.ex.highlight(highlight_name, "guifg="..fg_color, "guibg=#"..rgb_hex)
|
||||
end
|
||||
highlight_cache[rgb_hex] = highlight_name
|
||||
end
|
||||
return highlight_name
|
||||
end
|
||||
|
||||
--- Highlight a region in a buffer from the attributes specified
|
||||
local function highlight_region(buf, ns, highlight_name,
|
||||
region_line_start, region_byte_start,
|
||||
region_line_end, region_byte_end)
|
||||
-- TODO should I bother with highlighting normal regions?
|
||||
if region_line_start == region_line_end then
|
||||
nvim.buf_add_highlight(buf, ns, highlight_name, region_line_start, region_byte_start, region_byte_end)
|
||||
else
|
||||
nvim.buf_add_highlight(buf, ns, highlight_name, region_line_start, region_byte_start, -1)
|
||||
for linenum = region_line_start + 1, region_line_end - 1 do
|
||||
nvim.buf_add_highlight(buf, ns, highlight_name, linenum, 0, -1)
|
||||
end
|
||||
nvim.buf_add_highlight(buf, ns, highlight_name, region_line_end, 0, region_byte_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`.
|
||||
|
||||
@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
|
||||
]]
|
||||
local function highlight_buffer(buf, ns, lines, line_start, options)
|
||||
options = options or {}
|
||||
-- TODO do I have to put this here?
|
||||
initialize_trie()
|
||||
ns = ns or DEFAULT_NAMESPACE
|
||||
for current_linenum, line in ipairs(lines) do
|
||||
-- @todo it's possible to skip processing the new code if the attributes hasn't changed.
|
||||
current_linenum = current_linenum - 1 + line_start
|
||||
local i = 1
|
||||
while i < #line do
|
||||
local byte = line:byte(i)
|
||||
if byte == b_hash then
|
||||
i = i + 1
|
||||
if #line >= i + 5 then
|
||||
local invalid = false
|
||||
for n = i, i+5 do
|
||||
byte = line:byte(n)
|
||||
if not byte_is_hex(byte) then
|
||||
invalid = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not invalid then
|
||||
local rgb_hex = line:sub(i, i+5)
|
||||
-- TODO figure out proper background/foreground
|
||||
local highlight_name = create_highlight(rgb_hex, options)
|
||||
-- Subtract one because 0-indexed, subtract another 1 for the '#'
|
||||
nvim.buf_add_highlight(buf, ns, highlight_name, current_linenum, i-1-1, i+6-1)
|
||||
i = i + 5
|
||||
end
|
||||
end
|
||||
else
|
||||
-- TODO skip if the remaining length is less than the shortest length
|
||||
-- of an entry in our trie.
|
||||
local prefix = COLOR_TRIE:longest_prefix(line:sub(i))
|
||||
if prefix then
|
||||
local rgb = COLOR_MAP[prefix]
|
||||
local rgb_hex = bit.tohex(rgb):sub(-6)
|
||||
-- TODO figure out proper background/foreground
|
||||
local highlight_name = create_highlight(rgb_hex, options)
|
||||
nvim.buf_add_highlight(buf, ns, highlight_name, current_linenum, i-1, i+#prefix-1)
|
||||
i = i + #prefix
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Attach to a buffer and continuously highlight changes.
|
||||
-- @tparam[opt=0] integer buf A value of 0 implies the current buffer.
|
||||
-- @see highlight_buffer
|
||||
local function attach_to_buffer(buf, options)
|
||||
local ns = DEFAULT_NAMESPACE
|
||||
if buf == 0 or buf == nil then
|
||||
buf = nvim.get_current_buf()
|
||||
end
|
||||
-- Already attached.
|
||||
if pcall(vim.api.nvim_buf_get_var, buf, "colorizer_attached") then
|
||||
return
|
||||
end
|
||||
nvim.buf_set_var(buf, "colorizer_attached", true)
|
||||
do
|
||||
nvim.buf_clear_namespace(buf, ns, 0, -1)
|
||||
local lines = nvim.buf_get_lines(buf, 0, -1, true)
|
||||
highlight_buffer(buf, ns, lines, 0, options)
|
||||
end
|
||||
-- send_buffer: true doesn't actually do anything in Lua (yet)
|
||||
nvim.buf_attach(buf, false, {
|
||||
on_lines = function(event_type, buf, changed_tick, firstline, lastline, new_lastline)
|
||||
nvim.buf_clear_namespace(buf, ns, firstline, new_lastline)
|
||||
local lines = nvim.buf_get_lines(buf, firstline, new_lastline, true)
|
||||
highlight_buffer(buf, ns, lines, firstline, options)
|
||||
end;
|
||||
})
|
||||
end
|
||||
|
||||
--- Easy to use function if you want the full setup without fine grained control.
|
||||
-- Establishes an autocmd for `FileType terminal`
|
||||
-- @usage require'colorizer'.setup()
|
||||
local function auto_setup(filetypes, default_options)
|
||||
if not nvim.o.termguicolors then
|
||||
nvim.err_writeln("&termguicolors must be set")
|
||||
return
|
||||
end
|
||||
initialize_trie()
|
||||
local filetype_options = {}
|
||||
function COLORIZER_SETUP_HOOK()
|
||||
local filetype = nvim.bo.filetype
|
||||
local options = filetype_options[filetype] or default_options
|
||||
attach_to_buffer(nvim.get_current_buf(), options)
|
||||
end
|
||||
nvim.ex.augroup("ColorizerSetup")
|
||||
nvim.ex.autocmd_()
|
||||
if not filetypes then
|
||||
nvim.ex.autocmd("FileType * lua COLORIZER_SETUP_HOOK()")
|
||||
else
|
||||
for k, v in pairs(filetypes) do
|
||||
local filetype
|
||||
local options = default_options or {}
|
||||
if type(k) == 'string' then
|
||||
filetype = k
|
||||
if type(v) ~= 'table' then
|
||||
nvim.err_writeln("colorizer: Invalid option type for filetype "..filetype)
|
||||
else
|
||||
options = vim.tbl_extend("keep", v, default_options)
|
||||
end
|
||||
else
|
||||
filetype = v
|
||||
end
|
||||
filetype_options[filetype] = options
|
||||
-- TODO What's the right mode for this? BufEnter?
|
||||
nvim.ex.autocmd("FileType", filetype, "lua COLORIZER_SETUP_HOOK(%s)")
|
||||
end
|
||||
end
|
||||
nvim.ex.augroup("END")
|
||||
end
|
||||
|
||||
--- @export
|
||||
return {
|
||||
DEFAULT_NAMESPACE = DEFAULT_NAMESPACE;
|
||||
setup = auto_setup;
|
||||
attach_to_buffer = attach_to_buffer;
|
||||
highlight_buffer = highlight_buffer;
|
||||
initialize = initialize_trie;
|
||||
}
|
||||
|
||||
306
lua/trie.lua
Normal file
306
lua/trie.lua
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
-- Trie implementation in luajit
|
||||
-- Copyright © 2019 Ashkan Kiani
|
||||
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details.
|
||||
|
||||
-- You should have received a copy of the GNU General Public License
|
||||
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
local ffi = require 'ffi'
|
||||
|
||||
local bnot = bit.bnot
|
||||
local band, bor, bxor = bit.band, bit.bor, bit.bxor
|
||||
local lshift, rshift, rol = bit.lshift, bit.rshift, bit.rol
|
||||
|
||||
ffi.cdef [[
|
||||
struct Trie {
|
||||
bool is_leaf;
|
||||
struct Trie* character[62];
|
||||
};
|
||||
void *malloc(size_t size);
|
||||
void free(void *ptr);
|
||||
]]
|
||||
|
||||
local Trie_t = ffi.typeof('struct Trie')
|
||||
local Trie_ptr_t = ffi.typeof('$ *', Trie_t)
|
||||
|
||||
local function byte_to_index(b)
|
||||
-- 0-9 starts at string.byte('0') == 0x30 == 48 == 0b0011_0000
|
||||
-- A-Z starts at string.byte('A') == 0x41 == 65 == 0b0100_0001
|
||||
-- a-z starts at string.byte('a') == 0x61 == 97 == 0b0110_0001
|
||||
|
||||
-- This works for mapping characters to
|
||||
-- 0-9 A-Z a-z in that order
|
||||
-- Letters have bit 0x40 set, so we use that as an indicator for
|
||||
-- an additional offset from the space of the digits, and then
|
||||
-- add the 10 allocated for the range of digits.
|
||||
-- Then, within that indicator for letters, we subtract another
|
||||
-- (65 - 97) which is the difference between lower and upper case
|
||||
-- and add back another 26 to allocate for the range of uppercase
|
||||
-- letters.
|
||||
-- return b - 0x30
|
||||
-- + rshift(b, 6) * (
|
||||
-- 0x30 - 0x41
|
||||
-- + 10
|
||||
-- + band(1, rshift(b, 5)) * (
|
||||
-- 0x61 - 0x41
|
||||
-- + 26
|
||||
-- ))
|
||||
return b - 0x30 - rshift(b, 6) * (7 + band(1, rshift(b, 5)) * 6)
|
||||
end
|
||||
|
||||
local function insensitive_byte_to_index(b)
|
||||
-- return b - 0x30
|
||||
-- + rshift(b, 6) * (
|
||||
-- 0x30 - 0x61
|
||||
-- + 10
|
||||
-- )
|
||||
b = bor(b, 0x20)
|
||||
return b - 0x30 - rshift(b, 6) * 39
|
||||
end
|
||||
|
||||
local function verify_byte_to_index()
|
||||
local chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
for i = 1, #chars do
|
||||
local c = chars:sub(i,i)
|
||||
local index = byte_to_index(string.byte(c))
|
||||
assert((i-1) == index, vim.inspect{index=index,c=c})
|
||||
end
|
||||
end
|
||||
|
||||
local Trie_size = ffi.sizeof(Trie_t)
|
||||
|
||||
local function new_trie()
|
||||
local ptr = ffi.C.malloc(Trie_size)
|
||||
ffi.fill(ptr, Trie_size)
|
||||
return ffi.cast(Trie_ptr_t, ptr)
|
||||
end
|
||||
|
||||
local INDEX_LOOKUP_TABLE = ffi.new('uint8_t[256]')
|
||||
local b_a = string.byte('a')
|
||||
local b_z = string.byte('z')
|
||||
local b_A = string.byte('A')
|
||||
local b_Z = string.byte('Z')
|
||||
local b_0 = string.byte('0')
|
||||
local b_9 = string.byte('9')
|
||||
for i = 0, 255 do
|
||||
if i >= b_0 and i <= b_9 then
|
||||
INDEX_LOOKUP_TABLE[i] = i - b_0
|
||||
elseif i >= b_A and i <= b_Z then
|
||||
INDEX_LOOKUP_TABLE[i] = i - b_A + 10
|
||||
elseif i >= b_a and i <= b_z then
|
||||
INDEX_LOOKUP_TABLE[i] = i - b_a + 10 + 26
|
||||
else
|
||||
INDEX_LOOKUP_TABLE[i] = 255
|
||||
end
|
||||
end
|
||||
|
||||
local function insert(trie, value)
|
||||
if trie == nil then return false end
|
||||
local node = trie
|
||||
for i = 1, #value do
|
||||
local index = INDEX_LOOKUP_TABLE[value:byte(i)]
|
||||
if index == 255 then
|
||||
return false
|
||||
end
|
||||
if node.character[index] == nil then
|
||||
node.character[index] = new_trie()
|
||||
end
|
||||
node = node.character[index]
|
||||
end
|
||||
node.is_leaf = true
|
||||
return node, trie
|
||||
end
|
||||
|
||||
local function search(trie, value)
|
||||
if trie == nil then return false end
|
||||
local node = trie
|
||||
for i = 1, #value do
|
||||
local index = INDEX_LOOKUP_TABLE[value:byte(i)]
|
||||
if index == 255 then
|
||||
return
|
||||
end
|
||||
local child = node.character[index]
|
||||
if child == nil then
|
||||
return false
|
||||
end
|
||||
node = child
|
||||
end
|
||||
return node.is_leaf
|
||||
end
|
||||
|
||||
local function longest_prefix(trie, value)
|
||||
if trie == nil then return false end
|
||||
local node = trie
|
||||
local last_i = nil
|
||||
for i = 1, #value do
|
||||
local index = INDEX_LOOKUP_TABLE[value:byte(i)]
|
||||
if index == 255 then
|
||||
break
|
||||
end
|
||||
local child = node.character[index]
|
||||
if child == nil then
|
||||
break
|
||||
end
|
||||
if child.is_leaf then
|
||||
last_i = i
|
||||
end
|
||||
node = child
|
||||
end
|
||||
if last_i then
|
||||
return value:sub(1, last_i)
|
||||
end
|
||||
end
|
||||
|
||||
--- Printing utilities
|
||||
|
||||
local function index_to_char(index)
|
||||
if index < 10 then
|
||||
return string.char(index + b_0)
|
||||
elseif index < 36 then
|
||||
return string.char(index - 10 + b_A)
|
||||
else
|
||||
return string.char(index - 26 - 10 + b_a)
|
||||
end
|
||||
end
|
||||
|
||||
local function trie_structure(trie)
|
||||
if trie == nil then
|
||||
return nil
|
||||
end
|
||||
local children = {}
|
||||
for i = 0, 61 do
|
||||
local child = trie.character[i]
|
||||
if child ~= nil then
|
||||
local child_table = trie_structure(child)
|
||||
child_table.c = index_to_char(i)
|
||||
table.insert(children, child_table)
|
||||
end
|
||||
end
|
||||
return {
|
||||
is_leaf = trie.is_leaf;
|
||||
children = children;
|
||||
}
|
||||
end
|
||||
|
||||
local function print_structure(s)
|
||||
local mark
|
||||
if not s then
|
||||
return nil
|
||||
end
|
||||
if s.c then
|
||||
if s.is_leaf then
|
||||
mark = s.c.."*"
|
||||
else
|
||||
mark = s.c.."─"
|
||||
end
|
||||
else
|
||||
mark = "├─"
|
||||
end
|
||||
if #s.children == 0 then
|
||||
return {mark}
|
||||
end
|
||||
local lines = {}
|
||||
for _, child in ipairs(s.children) do
|
||||
local child_lines = print_structure(child)
|
||||
for _, child_line in ipairs(child_lines) do
|
||||
table.insert(lines, child_line)
|
||||
end
|
||||
end
|
||||
for i, v in ipairs(lines) do
|
||||
if v:match("^[%w%d]") then
|
||||
if i == 1 then
|
||||
lines[i] = mark.."─"..v
|
||||
elseif i == #lines then
|
||||
lines[i] = "└──"..v
|
||||
else
|
||||
lines[i] = "├──"..v
|
||||
end
|
||||
else
|
||||
if i == 1 then
|
||||
lines[i] = mark.."─"..v
|
||||
elseif #s.children > 1 then
|
||||
lines[i] = "│ "..v
|
||||
else
|
||||
lines[i] = " "..v
|
||||
end
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
local function free_trie(trie)
|
||||
for i = 0, 61 do
|
||||
local child = trie.character[i]
|
||||
if child ~= nil then
|
||||
free_trie(child)
|
||||
end
|
||||
end
|
||||
ffi.C.free(trie)
|
||||
end
|
||||
|
||||
local Trie_mt = {
|
||||
__index = {
|
||||
insert = insert;
|
||||
search = search;
|
||||
longest_prefix = longest_prefix;
|
||||
};
|
||||
__tostring = function(trie)
|
||||
local structure = trie_structure(trie)
|
||||
if structure then
|
||||
return table.concat(print_structure(structure), '\n')
|
||||
else
|
||||
return 'nil'
|
||||
end
|
||||
end;
|
||||
__gc = free_trie;
|
||||
}
|
||||
local Trie = ffi.metatype(Trie_t, Trie_mt)
|
||||
|
||||
return Trie
|
||||
|
||||
-- local tests = {
|
||||
-- "cat";
|
||||
-- "car";
|
||||
-- "celtic";
|
||||
-- "carb";
|
||||
-- "carb0";
|
||||
-- "CART0";
|
||||
-- "CaRT0";
|
||||
-- "Cart0";
|
||||
-- "931";
|
||||
-- "191";
|
||||
-- "121";
|
||||
-- "cardio";
|
||||
-- "call";
|
||||
-- "calcium";
|
||||
-- "calciur";
|
||||
-- "carry";
|
||||
-- "dog";
|
||||
-- "catdog";
|
||||
-- }
|
||||
-- local trie = Trie()
|
||||
-- for i, v in ipairs(tests) do
|
||||
-- trie:insert(v)
|
||||
-- end
|
||||
|
||||
-- print(trie)
|
||||
-- print(trie.character[0])
|
||||
-- print("catdo", trie:longest_prefix("catdo"))
|
||||
-- print("catastrophic", trie:longest_prefix("catastrophic"))
|
||||
|
||||
-- local COLOR_MAP = vim.api.nvim_get_color_map()
|
||||
-- local start = os.clock()
|
||||
-- for k, v in pairs(COLOR_MAP) do
|
||||
-- insert(trie, k)
|
||||
-- end
|
||||
-- print(os.clock() - start)
|
||||
|
||||
-- print(table.concat(print_structure(trie_structure(trie)), '\n'))
|
||||
Loading…
Reference in a new issue