feat: Dynamically generate highlight groups based on color scheme.

When Neogit highlight groups are not defined by the active color
scheme, they'll be generated. The color generation will take properties
such as 'lightness' into account, such that the resulting highlight
groups will work well for both light and dark color schemes.

* chore(color): Correct some code documentation.

* chore: Please linter.
This commit is contained in:
Sindre T. Strøm 2021-10-26 22:26:11 +02:00
parent c94549f3c3
commit b688a2c61f
8 changed files with 629 additions and 11 deletions

View file

@ -1,6 +1,7 @@
local config = require("neogit.config")
local lib = require("neogit.lib")
local signs = require("neogit.lib.signs")
local hl = require("neogit.lib.hl")
local status = require("neogit.status")
local neogit = {
@ -47,6 +48,7 @@ local neogit = {
if not config.values.disable_signs then
signs.setup()
end
hl.setup()
end
}

423
lua/neogit/lib/color.lua Normal file
View file

@ -0,0 +1,423 @@
local util = require("neogit.lib.util")
local M = {}
--#region TYPES
---@class RGBA
---@field red number Float [0,1]
---@field green number Float [0,1]
---@field blue number Float [0,1]
---@field alpha number Float [0,1]
---@class HSV
---@field hue number Float [0,360)
---@field saturation number Float [0,1]
---@field value number Float [0,1]
---@class HSL
---@field hue number Float [0,360)
---@field saturation number Float [0,1]
---@field lightness number Float [0,1]
--#endregion
---@class Color
---@field red number
---@field green number
---@field blue number
---@field alpha number
---@field hue number
---@field saturation number
---@field value number
---@field lightness number
local Color = setmetatable({}, {})
function Color:init(r, g, b, a)
self:set_red(r)
self:set_green(g)
self:set_blue(b)
self:set_alpha(a)
end
---@param c Color
---@return Color
function Color.from_color(c)
return Color(c.red, c.green, c.blue, c.alpha)
end
---@param h number Hue. Float [0,360)
---@param s number Saturation. Float [0,1]
---@param v number Value. Float [0,1]
---@param a number (Optional) Alpha. Float [0,1]
---@return Color
function Color.from_hsv(h, s, v, a)
h = h % 360
s = util.clamp(s, 0, 1)
v = util.clamp(v, 0, 1)
a = util.clamp(a or 1, 0, 1)
local function f(n)
local k = (n + h / 60) % 6
return v - v * s * math.max(math.min(k, 4 - k, 1), 0)
end
return Color(f(5), f(3), f(1), a)
end
---@param h number Hue. Float [0,360)
---@param s number Saturation. Float [0,1]
---@param l number Lightness. Float [0,1]
---@param a number (Optional) Alpha. Float [0,1]
---@return Color
function Color.from_hsl(h, s, l, a)
h = h % 360
s = util.clamp(s, 0, 1)
l = util.clamp(l, 0, 1)
a = util.clamp(a or 1, 0, 1)
local _a = s * math.min(l, 1 - l)
local function f(n)
local k = (n + h / 30) % 12
return l - _a * math.max(math.min(k - 3, 9 - k, 1), -1)
end
return Color(f(0), f(8), f(4), a)
end
---Create a color from a hex number.
---@param c number|string Either a literal number or a css-style hex string (`#RRGGBB[AA]`).
---@return Color
function Color.from_hex(c)
local n = c
if type(c) == "string" then
local s = c:lower():match("#?([a-f0-9]+)")
n = tonumber(s, 16)
if #s <= 6 then
n = bit.lshift(n, 8) + 0xff
end
end
return Color(
bit.rshift(n, 24) / 0xff,
bit.band(bit.rshift(n, 16), 0xff) / 0xff,
bit.band(bit.rshift(n, 8), 0xff) / 0xff,
bit.band(n, 0xff) / 0xff
)
end
---@return Color
function Color:clone()
return Color(self.red, self.green, self.blue, self.alpha)
end
---Returns a new shaded color.
---@param f number Amount. Float [-1,1].
---@return Color
function Color:shade(f)
local t = f < 0 and 0 or 1.0
local p = f < 0 and f * -1.0 or f
return Color(
(t - self.red) * p + self.red,
(t - self.green) * p + self.green,
(t - self.blue) * p + self.blue,
self.alpha
)
end
---Returns a new color that's a linear blend between two colors.
---@param other Color
---@param f number Amount. Float [0,1].
function Color:blend(other, f)
return Color(
(other.red - self.red) * f + self.red,
(other.green - self.green) * f + self.green,
(other.blue - self.blue) * f + self.blue,
self.alpha
)
end
function Color.test_shade()
print("-- SHADE TEST -- ")
local c = Color.from_hex("#98c379")
for i = 0, 10 do
local f = (1 / 5) * i - 1
print(string.format("%-8.1f%s", f, c:shade(f):to_css()))
end
end
function Color.test_blend()
print("-- BLEND TEST -- ")
local c0 = Color.from_hex("#98c379")
local c1 = Color.from_hex("#61afef")
for i = 0, 10 do
local f = (1 / 10) * i
print(string.format("%-8.1f%s", f, c0:blend(c1, f):to_css()))
end
end
---Return the RGBA values in a new table.
---@return RGBA
function Color:to_rgba()
return { red = self.red, green = self.green, blue = self.blue, alpha = self.alpha }
end
---Convert the color to HSV.
---@return HSV
function Color:to_hsv()
local r = self.red
local g = self.green
local b = self.blue
local max = math.max(r, g, b)
local min = math.min(r, g, b)
local delta = max - min
local h = 0
local s = 0
local v = max
if max == min then
h = 0
elseif max == r then
h = 60 * ((g - b) / delta)
elseif max == g then
h = 60 * ((b - r) / delta + 2)
elseif max == b then
h = 60 * ((r - g) / delta + 4)
end
if h < 0 then
h = h + 360
end
if max ~= 0 then
s = (max - min) / max
end
return { hue = h, saturation = s, value = v }
end
---Convert the color to HSL.
---@return HSL
function Color:to_hsl()
local r = self.red
local g = self.green
local b = self.blue
local max = math.max(r, g, b)
local min = math.min(r, g, b)
local delta = max - min
local h = 0
local s = 0
local l = (max + min) / 2
if max == min then
h = 0
elseif max == r then
h = 60 * ((g - b) / delta)
elseif max == g then
h = 60 * ((b - r) / delta + 2)
elseif max == b then
h = 60 * ((r - g) / delta + 4)
end
if h < 0 then
h = h + 360
end
if max ~= 0 and min ~= 1 then
s = (max - l) / math.min(l, 1 - l)
end
return { hue = h, saturation = s, lightness = l }
end
---Convert the color to a hex number representation (`0xRRGGBB[AA]`).
---@param with_alpha boolean Include the alpha component.
---@return integer
function Color:to_hex(with_alpha)
local n = bit.bor(
bit.bor((self.blue * 0xff), bit.lshift((self.green * 0xff), 8)),
bit.lshift((self.red * 0xff), 16)
)
return with_alpha and bit.lshift(n, 8) + (self.alpha * 0xff) or n
end
---Convert the color to a css hex color (`#RRGGBB[AA]`).
---@param with_alpha boolean Include the alpha component.
---@return string
function Color:to_css(with_alpha)
local n = self:to_hex(with_alpha)
local l = with_alpha and 8 or 6
return string.format("#%0" .. l .. "x", n)
end
---@param v number Float [0,1].
function Color:set_red(v)
self._red = util.clamp(v or 1.0, 0, 1)
end
---@param v number Float [0,1].
function Color:set_green(v)
self._green = util.clamp(v or 1.0, 0, 1)
end
---@param v number Float [0,1].
function Color:set_blue(v)
self._blue = util.clamp(v or 1.0, 0, 1)
end
---@param v number Float [0,1].
function Color:set_alpha(v)
self._alpha = util.clamp(v or 1.0, 0, 1)
end
---@param v number Hue. Float [0,360).
function Color:set_hue(v)
local hsv = self:to_hsv()
hsv.hue = v % 360
local c = Color.from_hsv(hsv.hue, hsv.saturation, hsv.value)
self._red = c.red
self._green = c.green
self._blue = c.blue
end
---@param v number Float [0,1].
function Color:set_saturation(v)
local hsv = self:to_hsv()
hsv.saturation = util.clamp(v, 0, 1)
local c = Color.from_hsv(hsv.hue, hsv.saturation, hsv.value)
self._red = c.red
self._green = c.green
self._blue = c.blue
end
---@param v number Float [0,1].
function Color:set_value(v)
local hsv = self:to_hsv()
hsv.value = util.clamp(v, 0, 1)
local c = Color.from_hsv(hsv.hue, hsv.saturation, hsv.value)
self._red = c.red
self._green = c.green
self._blue = c.blue
end
---@param v number Float [0,1].
function Color:set_lightness(v)
local hsl = self:to_hsl()
hsl.lightness = util.clamp(v, 0, 1)
local c = Color.from_hsl(hsl.hue, hsl.saturation, hsl.lightness)
self._red = c.red
self._green = c.green
self._blue = c.blue
end
---Copy the values from another color.
---@param c Color
function Color:set_from_color(c)
self._red = c.red
self._green = c.green
self._blue = c.blue
self._alpha = c.alpha
end
---@param x RGBA|number[]|number Either an RGBA struct, or a vector, or the value for red.
---@param g number (Optional) Green. Float [0,1].
---@param b number (Optional) Blue. Float [0,1].
---@param a number (Optional) Alpha. Float [0,1].
function Color:set_from_rgba(x, g, b, a)
if type(x) == "number" then
self.red = x
self.green = g
self.blue = b
self.alpha = a or self.alpha
elseif type(x.red) == "number" then
self.red = x.red
self.green = x.green
self.blue = x.blue
self.alpha = x.alpha
else
self.red = x[1]
self.green = x[2]
self.blue = x[3]
self.alpha = x[4] or self.alpha
end
end
---@param x HSV|number[]|number Either an HSV struct, or a vector, or the value for hue.
---@param s number (Optional) Saturation. Float [0,1].
---@param v number (Optional) Value Float [0,1].
function Color:set_from_hsv(x, s, v)
local c
if type(x) == "number" then
c = Color.from_hsv(x, s, v, self.alpha)
elseif type(x.hue) == "number" then
c = Color.from_hsv(x.hue, x.saturation, x.value, self.alpha)
else
c = Color.from_hsv(x[1], x[2], x[3], self.alpha)
end
self:set_from_color(c)
end
---@param x HSL|number[]|number Either an HSL struct, or a vector, or the value for hue.
---@param s number (Optional) Saturation. Float [0,1].
---@param l number (Optional) Lightness. Float [0,1].
function Color:set_from_hsl(x, s, l)
local c
if type(x) == "number" then
c = Color.from_hsl(x, s, l, self.alpha)
elseif type(x.hue) == "number" then
c = Color.from_hsl(x.hue, x.saturation, x.lightness, self.alpha)
else
c = Color.from_hsl(x[1], x[2], x[3], self.alpha)
end
self:set_from_color(c)
end
do
-- stylua: ignore start
local getters = {
red = function(self) return self._red end,
green = function(self) return self._green end,
blue = function(self) return self._blue end,
alpha = function(self) return self._alpha end,
hue = function (self) return self:to_hsv().hue end,
saturation = function (self) return self:to_hsv().saturation end,
value = function (self) return self:to_hsv().value end,
lightness = function (self) return self:to_hsl().lightness end,
}
local setters = {
red = function(self, v) self:set_red(v) end,
green = function(self, v) self:set_green(v) end,
blue = function(self, v) self:set_blue(v) end,
alpha = function(self, v) self:set_alpha(v) end,
hue = function(self, v) self:set_hue(v) end,
saturation = function(self, v) self:set_saturation(v) end,
value = function(self, v) self:set_value(v) end,
lightness = function(self, v) self:set_lightness(v) end,
}
-- stylua: ignore end
function Color.__index(self, k)
if getters[k] then
return getters[k](self)
end
return Color[k]
end
function Color.__newindex(self, k, v)
if setters[k] then
setters[k](self, v)
else
rawset(self, k, v)
end
end
local mt = getmetatable(Color)
function mt.__call(_, ...)
local this = setmetatable({}, Color)
this:init(...)
return this
end
end
M.Color = Color
return M

186
lua/neogit/lib/hl.lua Normal file
View file

@ -0,0 +1,186 @@
--#region TYPES
---@class HiSpec
---@field fg string
---@field bg string
---@field gui string
---@field sp string
---@field blend integer
---@field default boolean
---@class HiLinkSpec
---@field force boolean
---@field default boolean
--#endregion
local Color = require("neogit.lib.color").Color
local api = vim.api
local hl_store
local M = {}
---@param group string Syntax group name.
---@param opt HiSpec
function M.hi(group, opt)
vim.cmd(string.format(
"hi %s %s guifg=%s guibg=%s gui=%s guisp=%s blend=%s",
opt.default and "default" or "",
group,
opt.fg or "NONE",
opt.bg or "NONE",
opt.gui or "NONE",
opt.sp or "NONE",
opt.blend or "NONE"
))
end
---@param from string Syntax group name.
---@param to string Syntax group name.
---@param opt HiLinkSpec
function M.hi_link(from, to, opt)
vim.cmd(string.format(
"hi%s %s link %s %s",
opt.force and "!" or "",
opt.default and "default" or "",
from,
to or ""
))
end
---@param name string Syntax group name.
---@param attr string Attribute name.
---@param trans boolean Translate the syntax group (follows links).
function M.get_hl_attr(name, attr, trans)
local id = api.nvim_get_hl_id_by_name(name)
if id and trans then
id = vim.fn.synIDtrans(id)
end
if not id then
return
end
local value = vim.fn.synIDattr(id, attr)
if not value or value == "" then
return
end
return value
end
---@param group_name string Syntax group name.
---@param trans boolean Translate the syntax group (follows links). True by default.
function M.get_fg(group_name, trans)
if type(trans) ~= "boolean" then
trans = true
end
return M.get_hl_attr(group_name, "fg", trans)
end
---@param group_name string Syntax group name.
---@param trans boolean Translate the syntax group (follows links). True by default.
function M.get_bg(group_name, trans)
if type(trans) ~= "boolean" then
trans = true
end
return M.get_hl_attr(group_name, "bg", trans)
end
---@param group_name string Syntax group name.
---@param trans boolean Translate the syntax group (follows links). True by default.
function M.get_gui(group_name, trans)
if type(trans) ~= "boolean" then
trans = true
end
local hls = {}
local attributes = {
"bold",
"italic",
"reverse",
"standout",
"underline",
"undercurl",
"strikethrough"
}
for _, attr in ipairs(attributes) do
if M.get_hl_attr(group_name, attr, trans) == "1" then
table.insert(hls, attr)
end
end
if #hls > 0 then
return table.concat(hls, ",")
end
end
local function get_cur_hl()
return {
NeogitHunkHeader = { bg = M.get_bg("NeogitHunkHeader", false) },
NeogitHunkHeaderHighlight = { bg = M.get_bg("NeogitHunkHeaderHighlight", false) },
NeogitDiffContextHighlight = { bg = M.get_bg("NeogitDiffContextHighlight", false) },
NeogitDiffAddHighlight = {
bg = M.get_bg("NeogitDiffAddHighlight", false),
fg = M.get_fg("NeogitDiffAddHighlight", false),
gui = M.get_gui("NeogitDiffAddHighlight", false),
},
NeogitDiffDeleteHighlight = {
bg = M.get_bg("NeogitDiffDeleteHighlight", false),
fg = M.get_fg("NeogitDiffDeleteHighlight", false),
gui = M.get_gui("NeogitDiffDeleteHighlight", false),
},
}
end
local function is_hl_cleared(hl_map)
local keys = { "fg", "bg", "gui", "sp", "blend" }
for _, hl in pairs(hl_map) do
for _, k in ipairs(keys) do
if hl[k] then
return false
end
end
end
return true
end
function M.setup()
local cur_hl = get_cur_hl()
if hl_store and not is_hl_cleared(cur_hl) and not vim.deep_equal(hl_store, cur_hl) then
-- Highlights have been modified somewhere else. Return.
hl_store = cur_hl
return
end
local hl_fg_normal = M.get_fg("Normal")
local hl_bg_normal = M.get_bg("Normal")
-- Generate highlights by lightening for dark color schemes, and darkening
-- for light color schemes.
local bg_normal = Color.from_hex(hl_bg_normal)
local sign = bg_normal.lightness >= 0.5 and -1 or 1
local bg_hunk_header_hl = bg_normal:shade(0.15 * sign)
local bg_diff_context_hl = bg_normal:shade(0.075 * sign)
hl_store = {
NeogitHunkHeader = { bg = bg_diff_context_hl:to_css() },
NeogitHunkHeaderHighlight = { bg = bg_hunk_header_hl:to_css() },
NeogitDiffContextHighlight = { bg = bg_diff_context_hl:to_css() },
NeogitDiffAddHighlight = {
bg = M.get_bg("DiffAdd", false) or bg_diff_context_hl:to_css(),
fg = M.get_fg("DiffAdd", false) or M.get_fg("diffAdded") or hl_fg_normal,
gui = M.get_gui("DiffAdd", false),
},
NeogitDiffDeleteHighlight = {
bg = M.get_bg("DiffDelete", false) or bg_diff_context_hl:to_css(),
fg = M.get_fg("DiffDelete", false) or M.get_fg("diffRemoved") or hl_fg_normal,
gui = M.get_gui("DiffDelete", false),
},
}
for group, hl in pairs(hl_store) do
M.hi(group, hl)
end
end
return M

View file

@ -8,6 +8,19 @@ local function map(tbl, f)
return t
end
---@param value number
---@param min number
---@param max number
---@return number
local function clamp(value, min, max)
if value < min then
return min
elseif value > max then
return max
end
return value
end
local function trim(s)
return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end
@ -147,6 +160,7 @@ end
return {
time = time,
time_async = time_async,
clamp = clamp,
slice = slice,
map = map,
range = range,

View file

@ -18,6 +18,7 @@ augroup Neogit
au!
au BufWritePost,BufEnter,FocusGained,ShellCmdPost,VimResume * call <SID>refresh(expand('<afile>'))
au DirChanged * lua vim.defer_fn(function() require 'neogit.status'.dispatch_reset() end, 0)
au ColorScheme * lua require'neogit.lib.hl'.setup()
augroup END
command! -nargs=* Neogit lua require'neogit'.open(require'neogit.lib.util'.parse_command_args(<f-args>))<CR>

View file

@ -4,3 +4,6 @@ name = "globals"
[vim]
any = true
[bit]
any = true

View file

@ -8,11 +8,6 @@ syn match NeogitDiffDelete /.*/ contained
hi def link NeogitDiffAdd DiffAdd
hi def link NeogitDiffDelete DiffDelete
hi def NeogitDiffAddHighlight guibg=#404040 guifg=#859900
hi def NeogitDiffDeleteHighlight guibg=#404040 guifg=#dc322f
hi def NeogitDiffContextHighlight guibg=#333333 guifg=#b2b2b2
hi def NeogitHunkHeader guifg=#cccccc guibg=#404040
hi def NeogitHunkHeaderHighlight guifg=#cccccc guibg=#4d4d4d
hi def NeogitFilePath guifg=#798bf2
hi def NeogitCommitViewHeader guifg=#ffffff guibg=#94bbd1

View file

@ -40,12 +40,6 @@ hi def link NeogitUnpulledFrom Function
hi def link NeogitStash Comment
hi def NeogitDiffAddHighlight guibg=#404040 guifg=#859900
hi def NeogitDiffDeleteHighlight guibg=#404040 guifg=#dc322f
hi def NeogitDiffContextHighlight guibg=#333333 guifg=#b2b2b2
hi def NeogitHunkHeader guifg=#cccccc guibg=#404040
hi def NeogitHunkHeaderHighlight guifg=#cccccc guibg=#4d4d4d
hi def NeogitFold guifg=None guibg=None
sign define NeogitDiffContextHighlight linehl=NeogitDiffContextHighlight