diff --git a/lua/neogit.lua b/lua/neogit.lua index 6a8baa30..5d4b8d75 100644 --- a/lua/neogit.lua +++ b/lua/neogit.lua @@ -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 } diff --git a/lua/neogit/lib/color.lua b/lua/neogit/lib/color.lua new file mode 100644 index 00000000..fd0440b5 --- /dev/null +++ b/lua/neogit/lib/color.lua @@ -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 diff --git a/lua/neogit/lib/hl.lua b/lua/neogit/lib/hl.lua new file mode 100644 index 00000000..7e1a56bf --- /dev/null +++ b/lua/neogit/lib/hl.lua @@ -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 diff --git a/lua/neogit/lib/util.lua b/lua/neogit/lib/util.lua index ee723c2a..1826fe1f 100644 --- a/lua/neogit/lib/util.lua +++ b/lua/neogit/lib/util.lua @@ -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, diff --git a/plugin/neogit.vim b/plugin/neogit.vim index dd30b8f6..78d23948 100644 --- a/plugin/neogit.vim +++ b/plugin/neogit.vim @@ -18,6 +18,7 @@ augroup Neogit au! au BufWritePost,BufEnter,FocusGained,ShellCmdPost,VimResume * call refresh(expand('')) 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()) diff --git a/selene/globals.toml b/selene/globals.toml index 85de278c..f1b9939c 100644 --- a/selene/globals.toml +++ b/selene/globals.toml @@ -4,3 +4,6 @@ name = "globals" [vim] any = true + +[bit] +any = true diff --git a/syntax/NeogitCommitView.vim b/syntax/NeogitCommitView.vim index 3604d7c2..596c1f7d 100644 --- a/syntax/NeogitCommitView.vim +++ b/syntax/NeogitCommitView.vim @@ -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 diff --git a/syntax/NeogitStatus.vim b/syntax/NeogitStatus.vim index 8077a5ad..b669ee81 100644 --- a/syntax/NeogitStatus.vim +++ b/syntax/NeogitStatus.vim @@ -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