norcalli.nvim-colorizer.lua/lua/colorizer.lua
Akianonymus 28b41de2f4 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
2022-09-03 17:24:25 +05:30

423 lines
13 KiB
Lua

--- Requires Neovim >= 0.6.0 and `set termguicolors`
--
--Highlights terminal CSI ANSI color codes.
-- @module colorizer
-- @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 buf_get_option = api.nvim_buf_get_option
local clear_namespace = api.nvim_buf_clear_namespace
local current_buf = api.nvim_get_current_buf
-- 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 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: foreground, background, virtualtext
mode = "background", -- Set the display mode.
virtualtext = "",
}
local OPTIONS = { buf = {}, file = {} }
local SETUP_SETTINGS = {
exclusions = { buf = {}, file = {} },
all = { file = false, buf = false },
default_options = USER_DEFAULT_OPTIONS,
}
--- 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
value = buf_get_option(buf, "filetype")
end
return OPTIONS.file[value] or SETUP_SETTINGS.default_options
end
--- 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
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
return buf
end
--- 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
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
-- because now the buffer is not visible, so delete its information
BUFFER_OPTIONS[buf] = nil
BUFFER_AUTOCMDS[buf] = nil
end
---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
if not options then
options = new_buffer_options(buf, typ)
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
BUFFER_OPTIONS[buf] = options
rehighlight_buffer(buf, options)
BUFFER_INIT[buf] = true
if BUFFER_AUTOCMDS[buf] then
return
end
local autocmds = {}
local au_group_id = AUGROUP_ID
autocmds[#autocmds + 1] = autocmd({ "TextChanged", "TextChangedI", "TextChangedP" }, {
group = au_group_id,
buffer = buf,
callback = function()
-- 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] = autocmd({ "WinScrolled" }, {
group = au_group_id,
buffer = buf,
callback = function()
-- only reload if it was not disabled using detach_from_buffer
if BUFFER_OPTIONS[buf] then
rehighlight_buffer(buf, options)
end
end,
})
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,
})
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:~
--<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.schedule(function()
vim.notify("Colorizer: Error: &termguicolors must be set", "Error")
end)
return
end
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 = { buf = {}, file = {} },
all = { file = false, buf = false },
default_options = merge(USER_DEFAULT_OPTIONS, user_default_options),
}
local function COLORIZER_SETUP_HOOK(typ)
local filetype = vim.bo.filetype
local buftype = vim.bo.buftype
if SETUP_SETTINGS.exclusions.file[filetype] or SETUP_SETTINGS.exclusions.buf[buftype] then
return
end
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(AUGROUP_NAME, {})
AUGROUP_ID = au_group_id
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
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
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()
require("colorizer").clear_highlight_cache()
end,
})
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, get_buffer_options(buf))
end
end
--- Clear the highlight cache and reload all buffers.
local function clear_highlight_cache()
HIGHLIGHT_CACHE = {}
vim.schedule(reload_all_buffers)
end
--- @export
return {
DEFAULT_NAMESPACE = DEFAULT_NAMESPACE,
setup = setup,
is_buffer_attached = is_buffer_attached,
attach_to_buffer = attach_to_buffer,
detach_from_buffer = detach_from_buffer,
highlight_buffer = highlight_buffer,
reload_all_buffers = reload_all_buffers,
get_buffer_options = get_buffer_options,
clear_highlight_cache = clear_highlight_cache,
}