Skip to content

Commit

Permalink
refactor!: use winhl instead of setting cc directly
Browse files Browse the repository at this point in the history
This is a major refactor, including:

* use `winhl` instead of setting `cc` directly, which alleviates the
  need recording/restoring/resetting `cc` options in each window, which
  is tricky and unreliable, leading to misaligned behavior with vim
  default settings.

* remove module `autocmds`, move necessary functions to main module

* use `nvim_get_hl()` instead of deprecated `nvim_get_hl_by_name()`
bekaboo committed Jan 12, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent b7cdc07 commit ce15b17
Showing 7 changed files with 293 additions and 512 deletions.
33 changes: 8 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -86,25 +86,23 @@ function to override the default options.
```lua
local opts = {
scope = 'line',
modes = { 'i', 'ic', 'ix', 'R', 'Rc', 'Rx', 'Rv', 'Rvc', 'Rvx' },
---@type string[]|fun(mode: string): boolean
modes = function(mode)
return mode:find('^[ictRss\x13]') ~= nil
end,
blending = {
threshold = 0.75,
colorcode = '#000000',
hlgroup = {
'Normal',
'background',
},
hlgroup = { 'Normal', 'bg' },
},
warning = {
alpha = 0.4,
offset = 0,
colorcode = '#FF0000',
hlgroup = {
'Error',
'background',
},
hlgroup = { 'Error', 'bg' },
},
extra = {
---@type string?
follow_tw = nil,
},
}
@@ -203,18 +201,6 @@ require('deadcolumn').setup(opts) -- Call the setup function

## FAQ

### Why `:echo &cc` or `lua =vim.wo.cc` is empty?

If you are using the default config, this is expected.

The default config makes colorcolumn visible only in insert mode and replace
mode, so it clears `cc` in normal mode and reset it to the original value when
you enter insert mode or replace mode. As long as the colorcolumn is displayed
correctly in insert mode and replace mode, you don't need to worry about this.

If you want to see colorcolumn in normal mode, you can change the `modes`
option, see [Options](#options).

### Why can't I see the colored column?

This can have several reasons:
@@ -225,10 +211,7 @@ This can have several reasons:
to show the colored column in normal mode.

2. Please make sure you have set `colorcolumn` to a value greater than 0 in
your config. Notice that the output of `:echo &cc` or `lua =vim.wo.cc` may
be empty even if you have set `colorcolumn` to a value greater than 0. This
is because this plugin clears `colorcolumn` when it is not needed to conceal
the colored column, see point 1.
your config.

3. If you set `colorcolumn` to a relative value (e.g. `'-10'`), make sure
`textwidth` is set to a value greater than 0.
16 changes: 7 additions & 9 deletions doc/deadcolumn.nvim.txt
Original file line number Diff line number Diff line change
@@ -42,25 +42,23 @@ the default options, the plugin should work out of the box.
>lua
local opts = {
scope = 'line',
modes = { 'i', 'ic', 'ix', 'R', 'Rc', 'Rx', 'Rv', 'Rvc', 'Rvx' },
---@type string[]|fun(mode: string): boolean
modes = function(mode)
return mode:find('^[ictRss\x13]') ~= nil
end,
blending = {
threshold = 0.75,
colorcode = '#000000',
hlgroup = {
'Normal',
'background',
},
hlgroup = { 'Normal', 'bg' },
},
warning = {
alpha = 0.4,
offset = 0,
colorcode = '#FF0000',
hlgroup = {
'Error',
'background',
},
hlgroup = { 'Error', 'bg' },
},
extra = {
---@type string?
follow_tw = nil,
},
}
191 changes: 183 additions & 8 deletions lua/deadcolumn.lua
Original file line number Diff line number Diff line change
@@ -1,22 +1,197 @@
local configs = require('deadcolumn.configs')
local autocmds = require('deadcolumn.autocmds')
local colors = require('deadcolumn.colors')
local utils = require('deadcolumn.utils')

local C_NORMAL, C_CC, C_ERROR

---Get background color in hex
---@param hlgroup_name string
---@param field string 'foreground' or 'background'
---@param fallback string|nil fallback color in hex, default to '#000000' if &bg is 'dark' and '#FFFFFF' if &bg is 'light'
---@return string hex color
local function get_hl_hex(hlgroup_name, field, fallback)
fallback = fallback or vim.opt.bg == 'dark' and '#000000' or '#FFFFFF'
if not vim.fn.hlexists(hlgroup_name) then
return fallback
end
local attr_val =
colors.get(0, { name = hlgroup_name, winhl_link = false })[field]
return attr_val and colors.dec2hex(attr_val) or fallback
end

---Update base colors: bg color of Normal & ColorColumn, and fg of Error
---@return nil
local function update_hl_hex()
C_NORMAL = get_hl_hex(
configs.opts.blending.hlgroup[1],
configs.opts.blending.hlgroup[2],
configs.opts.blending.colorcode
)
C_ERROR = get_hl_hex(
configs.opts.warning.hlgroup[1],
configs.opts.warning.hlgroup[2],
configs.opts.warning.colorcode
)
C_CC = get_hl_hex('ColorColumn', 'bg')
end

---Resolve the colorcolumn value
---@param cc string|nil
---@return integer|nil cc_number smallest integer >= 0 or nil
local function cc_resolve(cc)
if not cc or cc == '' then
return nil
end
local cc_tbl = vim.split(cc, ',')
local cc_min = nil
for _, cc_str in ipairs(cc_tbl) do
local cc_number = tonumber(cc_str)
if vim.startswith(cc_str, '+') or vim.startswith(cc_str, '-') then
cc_number = vim.bo.tw > 0 and vim.bo.tw + cc_number or nil
end
if cc_number and cc_number > 0 and (not cc_min or cc_number < cc_min) then
cc_min = cc_number
end
end
return cc_min
end

---Hide colorcolumn
---@param winid integer? window handler
local function cc_conceal(winid)
winid = winid or 0
local new_winhl = (
vim.wo[winid].winhl:gsub('ColorColumn:[^,]*', '') .. ',ColorColumn:'
):gsub(',*$', ''):gsub('^,*', ''):gsub(',+', ',')
if new_winhl ~= vim.wo[winid].winhl then
vim.wo[winid].winhl = new_winhl
end
end

---Show colorcolumn
---@param winid integer? window handler
local function cc_show(winid)
winid = winid or 0
local new_winhl = (
vim.wo[winid].winhl:gsub('ColorColumn:[^,]*', '')
.. ',ColorColumn:_ColorColumn'
):gsub(',*$', ''):gsub('^,*', ''):gsub(',+', ',')
if new_winhl ~= vim.wo[winid].winhl then
vim.wo[winid].winhl = new_winhl
end
end

---Check if the current mode is in the correct mode
---@param mode string? default to current mode
---@return boolean
local function is_in_correct_mode(mode)
mode = mode or vim.fn.mode()
if type(configs.opts.modes) == 'function' then
return configs.opts.modes(mode)
end
return type(configs.opts.modes) == 'table'
and vim.tbl_contains(configs.opts.modes --[=[@as string[]]=], mode)
or false
end

---Setup function
---@param opts ColorColumnOptions
---@param opts ColorColumnOptions?
local function setup(opts)
configs.set_options(opts)
if not vim.g.loaded_deadcolumn then
vim.g.loaded_deadcolumn = true
autocmds.init()
autocmds.make_autocmds()

---Conceal colorcolumn in each window
for _, win in ipairs(vim.api.nvim_list_wins()) do
cc_conceal(win)
end

---Create autocmds for concealing / showing colorcolumn
local id = vim.api.nvim_create_augroup('Deadcolumn', {})
vim.api.nvim_create_autocmd('WinLeave', {
desc = 'Conceal colorcolumn in other windows.',
group = id,
callback = function()
if vim.fn.win_gettype() == '' then
cc_conceal(0)
end
end,
})

vim.api.nvim_create_autocmd('ColorScheme', {
desc = 'Update base colors.',
group = id,
callback = update_hl_hex,
})

local cc_bg = nil

vim.api.nvim_create_autocmd({
'BufWinEnter',
'ColorScheme',
'CursorMoved',
'CursorMovedI',
'ModeChanged',
'TextChanged',
'TextChangedI',
'WinEnter',
'WinScrolled',
}, {
desc = 'Change colorcolumn color.',
group = id,
callback = function()
local cc = cc_resolve(vim.wo.cc)
if
not is_in_correct_mode(vim.fn.mode())
or vim.fn.win_gettype() ~= ''
or not cc
then
cc_conceal(0)
return
end

-- Fix 'E976: using Blob as a String' after select a snippet
-- entry from LSP server using omnifunc `<C-x><C-o>`
---@diagnostic disable-next-line: param-type-mismatch
local length = vim.fn.strdisplaywidth(vim.fn.getline('.'))
local thresh = configs.opts.blending.threshold
if 0 < thresh and thresh <= 1 then
thresh = math.floor(thresh * cc)
end
if length < thresh then
cc_conceal(0)
return
end

-- Show blended color when len < cc
if not C_CC or not C_NORMAL or not C_ERROR then
update_hl_hex()
end
local new_cc_color = length < cc
and colors.cblend(C_CC, C_NORMAL, (length - thresh) / (cc - thresh)).dec
or colors.cblend(C_ERROR, C_NORMAL, configs.opts.warning.alpha).dec
if new_cc_color ~= cc_bg then
cc_bg = new_cc_color
vim.api.nvim_set_hl(0, '_ColorColumn', {
bg = cc_bg,
})
end
cc_show(0)
end,
})

if configs.opts.extra.follow_tw then
vim.api.nvim_create_autocmd('OptionSet', {
pattern = 'textwidth',
desc = 'Set colorcolumn according to textwidth.',
callback = function()
if vim.v.option_new ~= 0 then
vim.opt_local.colorcolumn = configs.opts.extra.follow_tw
end
end,
})
end
end

return {
setup = setup,
configs = configs,
colors = colors,
utils = utils,
}
312 changes: 0 additions & 312 deletions lua/deadcolumn/autocmds.lua

This file was deleted.

150 changes: 68 additions & 82 deletions lua/deadcolumn/colors.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
local M = {}

-- stylua: ignore start
local todec = {
['0'] = 0,
@@ -10,86 +12,84 @@ local todec = {
['7'] = 7,
['8'] = 8,
['9'] = 9,
['a'] = 10,
['b'] = 11,
['c'] = 12,
['d'] = 13,
['e'] = 14,
['f'] = 15,
['A'] = 10,
['B'] = 11,
['C'] = 12,
['D'] = 13,
['E'] = 14,
['F'] = 15,
}

local tohex = {
[0] = '0',
[1] = '1',
[2] = '2',
[3] = '3',
[4] = '4',
[5] = '5',
[6] = '6',
[7] = '7',
[8] = '8',
[9] = '9',
[10] = 'A',
[11] = 'B',
[12] = 'C',
[13] = 'D',
[14] = 'E',
[15] = 'F',
}
-- stylua: ignore end

---Wrapper of nvim_get_hl(), but does not create a highlight group
---if it doesn't exist (default to opts.create = false), and add
---new option opts.winhl_link to get highlight attributes without
---being affected by winhl
---@param ns_id integer
---@param opts table{ name: string?, id: integer?, link: boolean? }
---@return vim.api.keyset.highlight: highlight attributes
function M.get(ns_id, opts)
local no_winhl_link = opts.winhl_link == false
opts.winhl_link = nil
opts.create = opts.create or false
local attr = vim.api.nvim_get_hl(ns_id, opts)
-- We want to get true highlight attribute not affected by winhl
if no_winhl_link then
while attr.link do
opts.name = attr.link
attr = vim.api.nvim_get_hl(ns_id, opts)
end
end
return attr
end

---Convert an integer from decimal to hexadecimal
---@param int integer
---@param n_digits integer? number of digits used for the hex code
---@return string hex
function M.dec2hex(int, n_digits)
return not n_digits and string.format('%x', int)
or string.format('%0' .. n_digits .. 'x', int)
end

---Convert an integer from hexadecimal to decimal
---@param hex string
---@return integer dec
local function hex2dec(hex)
function M.hex2dec(hex)
local digit = 1
local dec = 0

while digit <= #hex do
dec = dec + todec[string.sub(hex, digit, digit)] * 16 ^ (#hex - digit)
digit = digit + 1
end

return dec
end

---Convert an integer from decimal to hexadecimal
---@param int integer
---@return string hex
local function dec2hex(int)
local hex = ''

while int > 0 do
hex = tohex[int % 16] .. hex
int = math.floor(int / 16)
end

return hex
end

---Convert a hex color to rgb color
---@param hex string hex code of the color
---@return integer[] rgb
local function hex2rgb(hex)
local red = string.sub(hex, 1, 2)
local green = string.sub(hex, 3, 4)
local blue = string.sub(hex, 5, 6)

function M.hex2rgb(hex)
return {
hex2dec(red),
hex2dec(green),
hex2dec(blue),
M.hex2dec(string.sub(hex, 1, 2)),
M.hex2dec(string.sub(hex, 3, 4)),
M.hex2dec(string.sub(hex, 5, 6)),
}
end

---Convert an rgb color to hex color
---@param rgb integer[]
---@return string
local function rgb2hex(rgb)
function M.rgb2hex(rgb)
local hex = {
dec2hex(math.floor(rgb[1])),
dec2hex(math.floor(rgb[2])),
dec2hex(math.floor(rgb[3])),
M.dec2hex(math.floor(rgb[1])),
M.dec2hex(math.floor(rgb[2])),
M.dec2hex(math.floor(rgb[3])),
}
hex = {
string.rep('0', 2 - #hex[1]) .. hex[1],
@@ -99,44 +99,30 @@ local function rgb2hex(rgb)
return table.concat(hex, '')
end

---Blend two hex colors
---@param hex1 string the first color in hdex
---@param hex2 string the second color in hdex
---@param alpha number between 0~1, weight of the first color
---@return string hex_blended blended hex color
local function blend(hex1, hex2, alpha)
local rgb1 = hex2rgb(hex1:gsub('^#', '', 1))
local rgb2 = hex2rgb(hex2:gsub('^#', '', 1))

---Blend two colors
---@param c1 string|number|table the first color, in hex, dec, or rgb
---@param c2 string|number|table the second color, in hex, dec, or rgb
---@param alpha number? between 0~1, weight of the first color, default to 0.5
---@return { hex: string, dec: integer, r: integer, g: integer, b: integer }
function M.cblend(c1, c2, alpha)
alpha = alpha or 0.5
c1 = type(c1) == 'number' and M.dec2hex(c1, 6) or c1
c2 = type(c2) == 'number' and M.dec2hex(c2, 6) or c2
local rgb1 = type(c1) == 'string' and M.hex2rgb(c1:gsub('#', '', 1)) or c1
local rgb2 = type(c2) == 'string' and M.hex2rgb(c2:gsub('#', '', 1)) or c2
local rgb_blended = {
alpha * rgb1[1] + (1 - alpha) * rgb2[1],
alpha * rgb1[2] + (1 - alpha) * rgb2[2],
alpha * rgb1[3] + (1 - alpha) * rgb2[3],
}

return '#' .. rgb2hex(rgb_blended)
end

---Get background color in hex
---@param hlgroup_name string
---@param field string 'foreground' or 'background'
---@param fallback string|nil fallback color in hex, default to '#000000'
---@return string hex color
local function get_hl(hlgroup_name, field, fallback)
fallback = fallback or '#000000'
local has_hlgroup, hlgroup =
pcall(vim.api.nvim_get_hl_by_name, hlgroup_name, true)
if has_hlgroup and hlgroup[field] then
return '#' .. dec2hex(hlgroup[field])
end
return fallback
local hex = M.rgb2hex(rgb_blended)
return {
hex = '#' .. hex,
dec = M.hex2dec(hex),
r = math.floor(rgb_blended[1]),
g = math.floor(rgb_blended[2]),
b = math.floor(rgb_blended[3]),
}
end

return {
hex2dec = hex2dec,
dec2hex = dec2hex,
hex2rgb = hex2rgb,
rgb2hex = rgb2hex,
blend = blend,
get_hl = get_hl,
}
return M
37 changes: 27 additions & 10 deletions lua/deadcolumn/configs.lua
Original file line number Diff line number Diff line change
@@ -4,19 +4,23 @@ local M = {}
---@class ColorColumnOptions
M.opts = {
scope = 'line',
modes = { 'i', 'ic', 'ix', 'R', 'Rc', 'Rx', 'Rv', 'Rvc', 'Rvx' },
---@type string[]|fun(mode: string): boolean
modes = function(mode)
return mode:find('^[ictRss\x13]') ~= nil
end,
blending = {
threshold = 0.75,
colorcode = '#000000',
hlgroup = { 'Normal', 'background' },
hlgroup = { 'Normal', 'bg' },
},
warning = {
alpha = 0.4,
offset = 0,
colorcode = '#FF0000',
hlgroup = { 'Error', 'background' },
hlgroup = { 'Error', 'bg' },
},
extra = {
---@type string?
follow_tw = nil,
},
}
@@ -25,7 +29,9 @@ function M.set_options(user_opts)
M.opts = vim.tbl_deep_extend('force', M.opts, user_opts or {})
-- Sanity check
assert(
type(M.opts.modes) == 'function' or vim.tbl_islist(M.opts.modes),
type(M.opts.modes) == 'function'
or type(M.opts.modes) == 'table'
and vim.tbl_islist(M.opts.modes --[[@as table]]),
'modes must be a function or a list of strings'
)
assert(
@@ -43,10 +49,10 @@ function M.set_options(user_opts)
)
assert(
vim.tbl_contains(
{ 'foreground', 'background' },
{ 'foreground', 'background', 'fg', 'bg' },
M.opts.blending.hlgroup[2]
),
'blending.hlgroup[2] must be "foreground" or "background"'
'blending.hlgroup[2] must be "foreground"/"fg" or "background"/"bg"'
)
assert(M.opts.warning.alpha >= 0, 'warning.alpha must be >= 0')
assert(M.opts.warning.alpha <= 1, 'warning.alpha must be <= 1')
@@ -60,8 +66,11 @@ function M.set_options(user_opts)
'warning.colorcode must be a 6-digit hex color code'
)
assert(
vim.tbl_contains({ 'foreground', 'background' }, M.opts.warning.hlgroup[2]),
'warning.hlgroup[2] must be "foreground" or "background"'
vim.tbl_contains(
{ 'foreground', 'background', 'fg', 'bg' },
M.opts.warning.hlgroup[2]
),
'warning.hlgroup[2] must be "foreground"/"fg" or "background"/"bg"'
)
assert(
type(M.opts.extra.follow_tw) == 'nil'
@@ -70,8 +79,16 @@ function M.set_options(user_opts)
)

-- Preprocess
M.opts.blending.colorcode = M.opts.blending.colorcode:upper()
M.opts.warning.colorcode = M.opts.warning.colorcode:upper()
-- For compatibility, 'foreground'/'background' may be provided as field name
-- of hlgroup attributes. These field names come from the return value of
-- `nvim_get_hl_by_name()`, which is now deprecated. New api `nvim_get_hl`
-- returns hlgroup attributes with field names 'fg'/'bg' instead.
M.opts.blending.hlgroup[2] = M.opts.blending.hlgroup[2]
:gsub('^foreground$', 'fg')
:gsub('^background$', 'bg')
M.opts.warning.hlgroup[2] = M.opts.warning.hlgroup[2]
:gsub('^foreground$', 'fg')
:gsub('^background$', 'bg')
end

return M
66 changes: 0 additions & 66 deletions lua/deadcolumn/utils.lua

This file was deleted.

0 comments on commit ce15b17

Please sign in to comment.