diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-06-12 16:58:39 +0200 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-06-12 22:57:30 +0200 |
| commit | 2e42bf1ef8c27173ed3a540135eada4c24abbaaf (patch) | |
| tree | a7e04f70f47b82e44a0e4721505a315867aac854 /.config/nvim | |
| parent | 9236e1f700b028da61302be8371401fe0fd86f0c (diff) | |
| download | dotfiles-2e42bf1ef8c27173ed3a540135eada4c24abbaaf.tar.gz dotfiles-2e42bf1ef8c27173ed3a540135eada4c24abbaaf.zip | |
refactor(nvim): rewrite the highlight and extended marks plugins
Diffstat (limited to '.config/nvim')
| -rw-r--r-- | .config/nvim/init.lua | 7 | ||||
| -rw-r--r-- | .config/nvim/plugin/10-treesitter.lua | 17 | ||||
| -rw-r--r-- | .config/nvim/plugin/40-colors.lua | 163 | ||||
| -rw-r--r-- | .config/nvim/plugin/50-extmarks.lua | 225 | ||||
| -rw-r--r-- | .config/nvim/plugin/50-highlight.lua | 70 |
5 files changed, 210 insertions, 272 deletions
diff --git a/.config/nvim/init.lua b/.config/nvim/init.lua index 5bdaaf5..a99e2c6 100644 --- a/.config/nvim/init.lua +++ b/.config/nvim/init.lua @@ -9,6 +9,12 @@ vim.g.dotfiles = { textwidth = { sh = 80, lua = 120, markdown = 120, gitcommit = 72, python = 88 }, } +-- Sets the window bar content and style (custom hl-groups WinBarCwd and WinBarFilePath) +-- Note that this will usually be cleared when loading a colorscheme (which do the equivalent of :hi clear) +vim.api.nvim_set_hl(0, "WinBarCwd", { bold = true }) +vim.api.nvim_set_hl(0, "WinBarFilePath", { link = "Normal" }) +vim.opt.winbar = "%#WinBarCwd#%{fnamemodify(getcwd(),':~')}%* | %#WinBarFilePath#%f%*" + ------------------------------------------------------------------------------------------------------------------------ -- Local functions ------------------------------------------------------------------------------------------------------------------------ @@ -204,7 +210,6 @@ vim.opt.ignorecase = true -- Search ignores case by default vim.opt.smartcase = true -- Search is case-sensitive if searching for uppercase characters vim.opt.conceallevel = 2 -- Hide text with the "conceal" syntax attribute vim.opt.showtabline = 2 -- Always show tabline -vim.opt.winbar = "%#WinBarCwd#%{fnamemodify(getcwd(),':~')}%* | %#WinBarFile#%f%*" vim.opt.winborder = "rounded" -- Rounded outline for floating windows vim.opt.updatetime = 1000 -- Swap file save frequency; also how often GitGutter signs update in ms vim.opt.list = true -- Display <Tab> and other non-printables diff --git a/.config/nvim/plugin/10-treesitter.lua b/.config/nvim/plugin/10-treesitter.lua index c0879b6..941614c 100644 --- a/.config/nvim/plugin/10-treesitter.lua +++ b/.config/nvim/plugin/10-treesitter.lua @@ -2,9 +2,7 @@ -- 10-treesitter.lua -- -- * Installs treesitter parsers. --- * Sets up an autocommand to parse the tree synchronously on 'FileType'. --- This file should sort early (alphabetically) in plugin/ so that this autcommand triggers before other autcommands --- that use the tree. +-- * Creates an autocommand to start treesitter automatically. -- require("nvim-treesitter").install({ @@ -21,19 +19,14 @@ require("nvim-treesitter").install({ "vimdoc", }) --- Parse the tree synchronously as early as possible (which is as soon as we know the filetype) --- This can be useful for example so that the tree is ready before any other code calls vim.treesitter.get_node(), which --- returns nil when the tree is not parsed. --- TODO actually test it vim.api.nvim_create_autocmd("FileType", { desc = "Start treesitter", group = vim.g.dotfiles.augroup, - callback = function() - local parser = vim.treesitter.get_parser(0) - if parser == nil then + callback = function(ev) + local parser = vim.treesitter.get_parser(ev.buf) + if not parser then return end - parser:parse() - vim.treesitter.start() + vim.treesitter.start(ev.buf) end, }) diff --git a/.config/nvim/plugin/40-colors.lua b/.config/nvim/plugin/40-colors.lua deleted file mode 100644 index df2e48c..0000000 --- a/.config/nvim/plugin/40-colors.lua +++ /dev/null @@ -1,163 +0,0 @@ --- --- Color scheme, custom highlights & per-window highlight namespaces --- - -local solarized_colors = { - base03 = "#002b36", - base02 = "#073642", - base01 = "#586e75", - base00 = "#657b83", - base0 = "#839496", - base1 = "#93a1a1", - base2 = "#eee8d5", - base3 = "#fdf6e3", - yellow = "#b58900", - orange = "#cb4b16", - red = "#dc322f", - magenta = "#d33682", - violet = "#6c71c4", - blue = "#268bd2", - cyan = "#2aa198", - green = "#859900", - -- solarized.nvim "mix" tints: subtle highlight backgrounds, theme-specific - mix_base1 = "#2c4e56", -- dark (active cursor line) - mix_blue_light = "#e7ebe1", -- light (active cursor line) - mix_green = "#274c25", -- dark (inactive cursor line: muted green) - mix_magenta_light = "#f8e2d9", -- light (inactive cursor line: muted pink) -} - --- Per-window highlight namespaces: one for the focused window, one for the --- unfocused windows. A window's active namespace decides its CursorLine color, --- which is how the focused window is made obvious. Both namespaces must define --- `dotfiles.Todo`: an extmark's hl_group is resolved against the window's --- active namespace and does NOT fall back to the global namespace, so the TODO --- highlight (placed in 50-extmarks.lua) only renders if the group exists in --- whichever namespace the window currently uses. -local ns_active = vim.api.nvim_create_namespace("extmarks") -local ns_inactive = vim.api.nvim_create_namespace("extmarks.inactive") -local todo_hl = { bg = "Yellow", fg = "Black", bold = true } -vim.api.nvim_set_hl(ns_active, "dotfiles.Todo", todo_hl) -vim.api.nvim_set_hl(ns_inactive, "dotfiles.Todo", todo_hl) - --- Publish the extmark storage namespace and TODO group name for 50-extmarks.lua. --- This file is named 40- (not 50-) so it is sourced before 50-extmarks.lua and --- the contract below exists when that file reads it. Preserve the existing --- `dotfiles` table (augroup). -local dotfiles = vim.g.dotfiles -dotfiles.todo_ns = ns_active -dotfiles.todo_hl = "dotfiles.Todo" -vim.g.dotfiles = dotfiles - --- Custom highlights in the global namespace, independent of the per-window ones -local function adjust_highlight() - if vim.g.colors_name == "solarized" then - vim.api.nvim_set_hl(0, "EndOfBuffer", { fg = solarized_colors.base0, update = true }) - vim.api.nvim_set_hl(0, "MatchParen", { link = "CurSearch" }) - vim.api.nvim_set_hl(0, "WinBarCwd", { fg = solarized_colors.blue, italic = true }) - vim.api.nvim_set_hl(0, "WinBarFile", { italic = true }) - -- Floating windows (LSP hover, etc.) follow the editor background; - -- keep the border's line color but match its bg to the float interior. - vim.api.nvim_set_hl(0, "NormalFloat", { link = "Normal" }) - local normal_bg = vim.api.nvim_get_hl(0, { name = "Normal", link = false }).bg - local border_fg = vim.api.nvim_get_hl(0, { name = "FloatBorder", link = false }).fg - vim.api.nvim_set_hl(0, "FloatBorder", { fg = border_fg, bg = normal_bg }) - -- Popup menu (completion, etc.): match the editor background like floats do. - vim.api.nvim_set_hl(0, "Pmenu", { fg = solarized_colors.base0, bg = normal_bg }) - vim.api.nvim_set_hl(0, "PmenuSbar", { bg = normal_bg }) - vim.api.nvim_set_hl(0, "TabLine", { fg = solarized_colors.yellow, bold = true }) - if vim.o.background == "light" then - vim.api.nvim_set_hl(0, "TabLineSel", { bg = "#edffd5", fg = solarized_colors.yellow, bold = true }) - elseif vim.o.background == "dark" then - vim.api.nvim_set_hl(0, "TabLineSel", { bg = "#043624" }) - end - end -end - --- Active and inactive cursorline highlight -local function set_cursorline_hl() - local accent = vim.o.background == "light" and solarized_colors.mix_blue_light or solarized_colors.mix_base1 - vim.api.nvim_set_hl(ns_active, "CursorLine", { bg = accent }) - vim.api.nvim_set_hl(ns_active, "CursorLineNr", { bg = accent }) - vim.api.nvim_set_hl(ns_inactive, "CursorLine", { bg = accent, underline = true }) - vim.api.nvim_set_hl(ns_inactive, "CursorLineNr", { bg = accent, underline = true }) -end - -local function apply_highlights() - adjust_highlight() - set_cursorline_hl() -end - -local function is_normal_win(win) - return vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_config(win).relative == "" -end - -local function update_focus_ns() - local cur = vim.api.nvim_get_current_win() - -- Focus in a floating window (completion, hover, ...) must not recolor the split underneath; leave every window as - -- it is. - if not is_normal_win(cur) then - return - end - for _, win in ipairs(vim.api.nvim_list_wins()) do - if is_normal_win(win) then - vim.api.nvim_win_set_hl_ns(win, (win == cur) and ns_active or ns_inactive) - end - end -end - -local function next_colorscheme() - local colorschemes = vim.fn.getcompletion("", "color") - vim.g.colors_name = vim.g.colors_name or "default" - for i = 1, #colorschemes do - if colorschemes[i] == vim.g.colors_name then - vim.cmd.colorscheme(colorschemes[(i % #colorschemes) + 1]) - vim.notify("Color scheme set to " .. vim.g.colors_name) - return - end - end -end - -local function random_colorscheme() - local colorschemes = vim.fn.getcompletion("", "color") - vim.cmd.colorscheme(colorschemes[math.random(#colorschemes)]) - vim.notify("Color scheme set to " .. vim.g.colors_name) -end - -local function toggle_theme() - vim.opt.background = vim.o.background == "light" and "dark" or "light" - vim.notify("Color theme set to " .. vim.o.background) -end - --- Reapply custom highlights after the colorscheme loads and on theme change -vim.api.nvim_create_autocmd("ColorScheme", { - desc = "Reapply custom highlights", - group = vim.g.dotfiles.augroup, - callback = apply_highlights, -}) -vim.api.nvim_create_autocmd("OptionSet", { - desc = "Reapply custom highlights on theme change", - pattern = "background", - group = vim.g.dotfiles.augroup, - callback = apply_highlights, -}) - --- Keep the focused/unfocused cursor line correct as windows and focus change -vim.api.nvim_create_autocmd( - { "VimEnter", "WinEnter", "WinNew", "WinClosed", "TabEnter", "SessionLoadPost", "FileType" }, - { - desc = "Track the focused window for the cursor line", - group = vim.g.dotfiles.augroup, - callback = function() - -- Defer so the window list and current window have settled. - vim.schedule(update_focus_ns) - end, - } -) - -vim.cmd.colorscheme("solarized") -- Default colorscheme -vim.opt.background = "dark" -- Default theme -apply_highlights() - -vim.api.nvim_create_user_command("ColorsNext", next_colorscheme, { desc = "Load next color scheme" }) -vim.api.nvim_create_user_command("ColorsRandom", random_colorscheme, { desc = "Load random color scheme" }) -vim.api.nvim_create_user_command("ColorsToggle", toggle_theme, { desc = "Toggle light/dark theme" }) diff --git a/.config/nvim/plugin/50-extmarks.lua b/.config/nvim/plugin/50-extmarks.lua index 1324da0..50ba81d 100644 --- a/.config/nvim/plugin/50-extmarks.lua +++ b/.config/nvim/plugin/50-extmarks.lua @@ -1,129 +1,162 @@ -- --- TODO highlighting (treesitter-aware extmarks) +-- 50-extmarks.lua +-- +-- * Creates a global highlight namespace for extended marks. +-- * Keeps track of the current global highlight namespace in `vim.g.hl_ns`. +-- * Creates an autocommand that initializes and updates extended marks. +-- For now, only "TODO" strings are marked but the system can be easily extended by adding items +-- in `marks`. +-- Doesn't support multi-line patterns. +-- +-- Global variables: +-- `vim.g.hl_ns`: global highlight namespace +-- +-- User commands: +-- `ExtmarkToggle`: toggle extended marks highlight namespace -- --- The `dotfiles.Todo` highlight group and the storage namespace are owned by --- 40-colors.lua, because the same per-window namespaces also drive the --- focused/unfocused cursor line. That file is numbered 40- so it is sourced --- before this one and `vim.g.dotfiles.todo_ns` / `.todo_hl` exist below. Here --- we only place the extmarks. --- Returns an iterator over the ascendants of `node`, starting at the root node -local function node_ascendants(node) - local root = node:tree():root() - local current = root - return function() - if current:equal(node) then - return nil - end - current = assert(current:child_with_descendant(node)) - return current +vim.g.hl_ns = 0 + +local extmarks_hl_ns = vim.api.nvim_create_namespace("dotfiles_extmarks") + +local hl_group_todo = "dotfiles.Todo" +vim.api.nvim_set_hl(extmarks_hl_ns, hl_group_todo, { link = "Todo" }) + +-- Wrapper around `nvim_set_hl_ns()`; sets the global highlight namespace to `hl_ns` and save it to +-- `vim.g.hl_ns`. Useful because the global highlight namespace is not saved anywhere by default. +local function set_global_hl_ns(hl_ns) + vim.g.hl_ns = hl_ns + vim.api.nvim_set_hl_ns(hl_ns) +end + +-- Toggles extended marks highlight namespace +local function toggle_marks() + local new_hl_ns = vim.g.hl_ns == 0 and extmarks_hl_ns or 0 + set_global_hl_ns(new_hl_ns) +end + +-- Parses the treesitter tree synchronously, including injections, in the 0-based, end-exclusive +-- line range `first`:`last` of buffer `buf`. +--- @return boolean parser_exists +local function parse_tree(buf, first, last) + local parser = vim.treesitter.get_parser(buf) + if parser then + parser:parse({ first, last }) end + return parser ~= nil end --- Returns true if `node` or any of its parent has the type `type` -local function node_within_type(node, type) - local next_ascendant = node_ascendants(node) - local next = next_ascendant() - while next do - if next:type() == type then +-- Returns a predicate telling whether a "TODO" string should be marked at a given position. +-- In markdown buffers, positions inside code are invalid, otherwise positions outside comments are +-- invalid. All positions are valid if there is no parser for the buffer filetype. +-- Only the line range `first`:`last` is parsed; the predicate must not be called outside it. +--- @param buf number +--- @return fun(row: number, col: number): boolean predicate taking a 0-based position +local function todo_filter(buf, first, last) + local has_parser = parse_tree(buf, first, last) + return function(row, col) + assert(first <= row and row < last, "Position outside the parsed line range") + if not has_parser then return true end - next = next_ascendant() + local block = vim.treesitter.get_node({ + bufnr = buf, + pos = { row, col }, + }) + assert(block, "Expected node to be truthy, is the tree parsed?") + if vim.bo[buf].filetype == "markdown" then + if string.match(block:type(), "code") then + return false + end + local inline = vim.treesitter.get_node({ + bufnr = buf, + ignore_injections = false, + pos = { row, col }, + }) + assert(inline, "Expected node to be truthy, is the tree parsed?") + return inline:type() ~= "code_span" + else + return string.match(block:type(), "comment") ~= nil + end end - return false end --- Returns an iterator over the positions (`{ col1, col2, row }`) of matches of pattern in lines -local function pmatches(lines, pattern) - local col1, col2, row = nil, nil, 1 +-- Returns an iterator over the positions of valid "TODO" strings in buffer `buf` in the line range +-- `first`:`last`. Each item is a table with 0-based fields: row `y`, start column `x1` (inclusive) +-- and end column `x2` (exclusive). See `todo_filter()` for what makes a position valid. +local function find_todo(buf, first, last) + local lines = vim.api.nvim_buf_get_lines(buf, first, last, false) + local should_mark = todo_filter(buf, first, first + #lines) + local x1, x2, y = nil, nil, 1 return function() - while row <= #lines do - col1, col2 = string.find(lines[row], pattern, (col2 or 0) + 1) - if col1 then - return { col1 = col1, col2 = col2, row = row } + while y <= #lines do + x1, x2 = string.find(lines[y], "TODO", (x2 or 0) + 1) + if x1 and x2 then + local row = y + first - 1 + if should_mark(row, x1 - 1) then + return { y = row, x1 = x1 - 1, x2 = x2 } + end else - col1, col2, row = 0, 0, row + 1 + x1, x2, y = 0, 0, y + 1 end end return nil end end --- Set extmarks are the positions (`{ col1, col2, row }`) given by `next_pos()` --- `opts`: { --- ns: highlight namespace --- predicate: is passed row and col, if true then set the mark --- hl_group: highlight group --- } -local function set_extmarks(buf, next_pos, opts) - for pos in next_pos do - local marks = vim.api.nvim_buf_get_extmarks( - buf, - opts.ns, - { pos.row - 1, pos.col1 - 1 }, - { pos.row - 1, pos.col2 - 1 }, - {} - ) - if #marks == 0 and opts.predicate(buf, pos.row - 1, pos.col1 - 1) then +-- List of extended marks to apply with `mark_all()` +-- +-- Each item is a table with two fields: `hl_group`, the name of the highlight group to use +-- for the marks, and `find`, which must return an iterator over the positions to mark (see +-- `find_todo()` for the format). +local marks = { + { hl_group = hl_group_todo, find = find_todo }, +} + +-- Updates extended marks in buffer `buf` in range `first`:`last`. +-- For each item in `marks`, sets extended marks on positions returned by `item.find` to the +-- highlight group `item.hl_group`. +local function mark_all(buf, first, last) + vim.api.nvim_buf_clear_namespace(buf, extmarks_hl_ns, first, last) + for _, marks_opts in ipairs(marks) do + for p in marks_opts.find(buf, first, last) do vim.api.nvim_buf_set_extmark( buf, - opts.ns, - pos.row - 1, - pos.col1 - 1, - { end_col = pos.col2, hl_group = opts.hl_group } + extmarks_hl_ns, + p.y, + p.x1, + { end_col = p.x2, hl_group = marks_opts.hl_group } ) end end end --- TODO a better way to do this: --- For code, only check in "comment" node ranges --- For non-code, do it per type. For Markdown, only check "paragraph" nodes -local function hl_todo_predicate(buf, row, col) - if vim.bo[buf].filetype == "markdown" or vim.treesitter.get_parser(buf) == nil then - return true +-- Create extended marks in buffer `ev.buf` and updates them when the buffer changes. +local function watch_extmarks(ev) + mark_all(ev.buf, 0, -1) + if vim.b[ev.buf].dotfiles_todo_attached then + return end - local node = assert(vim.treesitter.get_node({ bufnr = buf, pos = { row, col } })) - return node_within_type(node, "comment") + vim.b[ev.buf].dotfiles_todo_attached = true + vim.api.nvim_buf_attach(ev.buf, false, { + on_lines = function(_, b, _, first, _, last_new) + vim.schedule(function() + if vim.api.nvim_buf_is_valid(b) then + mark_all(b, first, last_new) + end + end) + end, + }) end -local patterns = { todo = "TODO" } -local extmarks_todo_opts = { - ns = vim.g.dotfiles.todo_ns, - hl_group = vim.g.dotfiles.todo_hl, - predicate = hl_todo_predicate, -} - -local function scan(buf, first, last) - local lines = vim.api.nvim_buf_get_lines(buf, first, last, false) - local iter = pmatches(lines, patterns.todo) - set_extmarks(buf, function() - local pos = iter() - if pos then - pos.row = pos.row + first - end - return pos - end, extmarks_todo_opts) -end +---------------------------------------------------------------------------------------------------- vim.api.nvim_create_autocmd("FileType", { - desc = "Initialize and watch TODO extmarks", + desc = "Set extended marks watcher", group = vim.g.dotfiles.augroup, - callback = function(args) - local buf = args.buf - scan(buf, 0, -1) - if vim.b[buf].dotfiles_todo_attached then - return - end - vim.b[buf].dotfiles_todo_attached = true - vim.api.nvim_buf_attach(buf, false, { - on_lines = function(_, b, _, first, _, last_new) - vim.schedule(function() - if vim.api.nvim_buf_is_valid(b) then - scan(b, first, last_new) - end - end) - end, - }) - end, + callback = watch_extmarks, }) + +vim.api.nvim_create_user_command("ExtmarkToggle", toggle_marks, { desc = "Toggle extended marks" }) + +set_global_hl_ns(extmarks_hl_ns) diff --git a/.config/nvim/plugin/50-highlight.lua b/.config/nvim/plugin/50-highlight.lua new file mode 100644 index 0000000..1daba82 --- /dev/null +++ b/.config/nvim/plugin/50-highlight.lua @@ -0,0 +1,70 @@ +-- +-- 50-highlight.lua +-- +-- * Creates a function to adjust highlight groups for the solarized colorscheme. It also sets popup +-- menu width and border style. +-- * Creates an autocommand to trigger this function when the background or color scheme changes. +-- * Sets the default colorscheme (solarized) and background (dark). +-- +-- User commands: +-- `ColorsBgToggle`: toggle light/dark background +-- + +-- Adjust highlights (for the solarized theme) to fit my personal tastes. +local function adjust_solarized_highlights() + local colors = { blue = "#268bd2", yellow = "#b58900" } + -- Window bar style + vim.api.nvim_set_hl(0, "WinBarCwd", { fg = colors.blue, italic = true }) + vim.api.nvim_set_hl(0, "WinBarFilePath", { italic = true }) + -- Matching delimiter + vim.api.nvim_set_hl(0, "MatchParen", { link = "CurSearch" }) + -- Floating windows and popup menu windows follow the editor background, their borders too so + -- that the popup menu border draws a clean separation. + vim.opt.pumborder = "rounded" + vim.opt.pumwidth = 25 -- Minimum popup menu width + vim.api.nvim_set_hl(0, "NormalFloat", { link = "Normal" }) + local normal_bg = vim.api.nvim_get_hl(0, { name = "Normal", link = false }).bg + vim.api.nvim_set_hl(0, "FloatBorder", { bg = normal_bg, update = true }) + vim.api.nvim_set_hl(0, "Pmenu", { bg = normal_bg, update = true }) + vim.api.nvim_set_hl(0, "PmenuSbar", { bg = normal_bg, update = true }) + -- Currently selected tabpage + vim.api.nvim_set_hl(0, "TabLineSel", { fg = colors.yellow, bold = true, update = true }) + -- Todo hl group + vim.api.nvim_set_hl(0, "Todo", { bg = "Yellow", fg = "Black", bold = true }) +end + +-- Adjust highlights to fit my personal tastes. +local function adjust_highlights() + if vim.g.colors_name == "solarized" then + adjust_solarized_highlights() + end +end + +local function toggle_theme() + vim.opt.background = vim.o.background == "light" and "dark" or "light" + vim.notify("Color theme set to " .. vim.o.background) +end + +---------------------------------------------------------------------------------------------------- + +vim.api.nvim_create_autocmd("ColorScheme", { + desc = "Adjust custom highlights", + group = vim.g.dotfiles.augroup, + callback = adjust_highlights, +}) + +vim.api.nvim_create_autocmd("OptionSet", { + desc = "Adjust custom highlights on theme change", + pattern = "background", + group = vim.g.dotfiles.augroup, + callback = adjust_highlights, +}) + +vim.api.nvim_create_user_command( + "ColorsBgToggle", + toggle_theme, + { desc = "Toggle light/dark theme" } +) + +vim.cmd.colorscheme("solarized") +vim.opt.background = "dark" |
