├── .git-blame-ignore-revs ├── .gitignore ├── README.md ├── lua └── neodim │ ├── Color.lua │ ├── TSOverride.lua │ ├── config.lua │ ├── filter.lua │ ├── init.lua │ ├── list.lua │ └── lsp.lua └── stylua.toml /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Format all codes by stylua 2 | 6dfa47cb7847ab1c8c612ac408c49f0d4c683d50 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lua/neodim/backup 2 | lua/.luarc.json 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neodim 2 | 3 | > *Neovim plugin for dimming the highlights of unused functions, variables, parameters, and more* 4 | 5 | This plugin takes heavy inspiration from https://github.com/NarutoXY/dim.lua. \ 6 | The implementation in NarutoXY/dim.lua was a bit inefficient and I saw room for various improvements, 7 | including making the dimming an actual LSP handler, rather than an autocmd. 8 | The result is a much more polished experience with greater efficiency. 9 | 10 | ## Setup 11 | 12 | ### Requirements 13 | 14 | - Neovim 0.10.0 or later 15 | - Language server that supports [diagnostic tags](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticTag) 16 | 17 | ### Installation 18 | 19 | You can install with builtin packages or your favorite package manager. 20 | 21 | The following is an example installation using Lazy.nvim: 22 | 23 | ```lua 24 | { 25 | "zbirenbaum/neodim", 26 | event = "LspAttach", 27 | config = function() 28 | require("neodim").setup() 29 | end, 30 | } 31 | ``` 32 | 33 | ## Options 34 | 35 | ```lua 36 | require("neodim").setup({ 37 | alpha = 0.75, 38 | blend_color = nil, 39 | hide = { 40 | underline = true, 41 | virtual_text = true, 42 | signs = true, 43 | }, 44 | regex = { 45 | "[uU]nused", 46 | "[nN]ever [rR]ead", 47 | "[nN]ot [rR]ead", 48 | }, 49 | priority = 128, 50 | disable = {}, 51 | }) 52 | ``` 53 | 54 | ### Dim Highlight Options 55 | 56 | #### alpha 57 | 58 | `alpha` controls how dim the highlight becomes. 59 | A value of 1 means that dimming will do nothing at all, while a value of 0 will make it identical to the color set in `blend_color`. 60 | Conceptually, if you were to place the text to be dimmed on a background of `blend_color`, 61 | and then set the opacity of the text to the value of alpha, you would have the resulting color that the plugin highlights with. 62 | 63 | 64 | ```lua 65 | require("neodim").setup({ 66 | alpha = 0.5 -- make the dimmed text even dimmer 67 | }) 68 | ``` 69 | 70 | #### blend_color 71 | 72 | `blend_color` controls the color which is used to dim your highlight. 73 | neodim sets this option automatically, so you don't need to set it if you want to set to the background color of the `Normal` highlight. 74 | 75 | Example: 76 | 77 | ```lua 78 | require("neodim").setup({ 79 | blend_color = "#10171f" 80 | }) 81 | ``` 82 | 83 | ### regex 84 | 85 | If the diagnostic message matches one of these, the code to which the diagnostic refers is dimmed. 86 | 87 | You can set up each filetype by entering in a table with the key as the filetype. 88 | 89 | Example: 90 | ```lua 91 | require("neodim").setup({ 92 | regex = { 93 | "[Uu]nused", 94 | cs = { 95 | "CS8019", 96 | }, 97 | -- disable `regex` option when filetype is "rust" 98 | rust = {}, 99 | } 100 | }) 101 | ``` 102 | 103 | ### Decoration Options 104 | 105 | All decorations can be hidden for diagnostics pertaining to unused tokens. \ 106 | By default, hiding all of them is enabled, but you can re-enable them by changing the config table passed to neodim. 107 | 108 | It is important to note that regardless of what you put in this configuration, 109 | neodim will always respect settings created with `vim.diagnostic.config`. 110 | For example, if all underline decorations are disabled by running `vim.diagnostic.config({ underline=false })`, 111 | neodim will ***not*** re-enable them for "unused" diagnostics. 112 | 113 | Example: 114 | 115 | ```lua 116 | -- re-enable only sign decorations for 'unused' diagnostics 117 | require("neodim").setup({ 118 | hide = { signs = false } 119 | }) 120 | ``` 121 | 122 | ```lua 123 | -- re-enable all decorations for 'unused' diagnostics 124 | require("neodim").setup({ 125 | hide = { 126 | virtual_text = false, 127 | signs = false, 128 | underline = false, 129 | } 130 | }) 131 | ``` 132 | -------------------------------------------------------------------------------- /lua/neodim/Color.lua: -------------------------------------------------------------------------------- 1 | ---@class neodim.Color 2 | ---@field r number 3 | ---@field g number 4 | ---@field b number 5 | local Color = {} 6 | ---@private 7 | Color.__index = Color 8 | 9 | ---@private 10 | ---@return string 11 | Color.__tostring = function(self) 12 | return ('#%02x%02x%02x'):format(self.r, self.g, self.b) 13 | end 14 | 15 | ---@param min number 16 | ---@param n number 17 | ---@param max number 18 | ---@return number 19 | local clamp = function(min, n, max) 20 | if n < min then 21 | return min 22 | elseif max < n then 23 | return max 24 | else 25 | return n 26 | end 27 | end 28 | 29 | ---@param r number 30 | ---@param g number 31 | ---@param b number 32 | ---@return self 33 | Color.new = function(r, g, b) 34 | return setmetatable({ 35 | r = clamp(0, r, 0xFF), 36 | g = clamp(0, g, 0xFF), 37 | b = clamp(0, b, 0xFF), 38 | }, Color) 39 | end 40 | 41 | if bit then 42 | ---@param int number 43 | ---@return self 44 | Color.from_int = function(int) 45 | int = clamp(0, int, 0xFFFFFF) 46 | return Color.new(bit.rshift(int, 16), bit.band(bit.rshift(int, 8), 0xFF), bit.band(int, 0xFF)) 47 | end 48 | else 49 | ---@param int number 50 | ---@return self 51 | Color.from_int = function(int) 52 | int = clamp(0, int, 0xFFFFFF) 53 | return Color.new(math.floor(int / 0x10000), math.floor(int / 0x100) % 0x100, int % 0x100) 54 | end 55 | end 56 | 57 | ---@param str string 58 | ---@return self 59 | Color.from_str = function(str) 60 | assert(#str == 7) 61 | return Color.new( 62 | assert(tonumber(str:sub(2, 3), 16)), 63 | assert(tonumber(str:sub(4, 5), 16)), 64 | assert(tonumber(str:sub(6, 7), 16)) 65 | ) 66 | end 67 | 68 | ---@param x number 69 | ---@param y number 70 | ---@param a number 71 | ---@return number 72 | local blend_channel = function(x, y, a) 73 | return x * a + y * (1 - a) 74 | end 75 | 76 | ---@param other neodim.Color 77 | ---@param alpha number 78 | ---@return self 79 | Color.blend = function(self, other, alpha) 80 | alpha = clamp(0, alpha, 1) 81 | return Color.new( 82 | blend_channel(self.r, other.r, alpha), 83 | blend_channel(self.g, other.g, alpha), 84 | blend_channel(self.b, other.b, alpha) 85 | ) 86 | end 87 | 88 | return Color 89 | -------------------------------------------------------------------------------- /lua/neodim/TSOverride.lua: -------------------------------------------------------------------------------- 1 | local TSHighlighter = vim.treesitter.highlighter 2 | local Range = vim.treesitter._range 3 | 4 | local Color = require 'neodim.Color' 5 | local config = require 'neodim.config' 6 | local list = require 'neodim.list' 7 | local lsp = require 'neodim.lsp' 8 | 9 | local NAMESPACE = vim.api.nvim_create_namespace 'treesitter/highlighter' 10 | 11 | ---@class neodim.ColumnRange 12 | ---@field start_col integer 13 | ---@field end_col integer 14 | 15 | ---@class neodim.TSOverride 16 | ---@field diagnostics_map table> 17 | ---@field highlight_cache table 18 | local TSOverride = {} 19 | ---@private 20 | TSOverride.__index = TSOverride 21 | 22 | ---@return self 23 | TSOverride.init = function() 24 | ---@type neodim.TSOverride 25 | local self = setmetatable({ 26 | diagnostics_map = {}, 27 | highlight_cache = {}, 28 | }, TSOverride) 29 | 30 | -- these are 'private' but technically accessible 31 | -- if that every changes, we will have to override the whole TSHighlighter 32 | vim.api.nvim_set_decoration_provider(NAMESPACE, { 33 | on_win = TSHighlighter._on_win, ---@diagnostic disable-line: invisible 34 | on_line = self:set_override(), 35 | _on_spell_nav = TSHighlighter._on_spell_nav, ---@diagnostic disable-line: invisible 36 | }) 37 | vim.api.nvim_create_autocmd('ColorScheme', { 38 | callback = function() 39 | for _, hl in pairs(self.highlight_cache) do 40 | vim.api.nvim_set_hl(0, hl, {}) 41 | end 42 | self.highlight_cache = {} 43 | end, 44 | }) 45 | 46 | return self 47 | end 48 | 49 | ---@return function 50 | TSOverride.set_override = function(self) 51 | ---@param buf integer 52 | ---@param line integer 53 | local function on_line(_, _, buf, line) 54 | local highlighter = TSHighlighter.active[buf] 55 | if not highlighter then 56 | return 57 | end 58 | 59 | self:on_line_impl(highlighter, buf, line) 60 | end 61 | 62 | return on_line 63 | end 64 | 65 | ---@param diagnostics vim.Diagnostic[] 66 | ---@param bufnr integer 67 | TSOverride.update_unused = function(self, diagnostics, bufnr) 68 | if not vim.api.nvim_buf_is_loaded(bufnr) then 69 | self.diagnostics_map[bufnr] = nil 70 | return 71 | end 72 | local ft = vim.api.nvim_get_option_value('filetype', { buf = bufnr }) 73 | if config.opts.disable[ft] then 74 | self.diagnostics_map[bufnr] = nil 75 | return 76 | end 77 | 78 | local map_buf = {} 79 | self.diagnostics_map[bufnr] = map_buf 80 | 81 | for _, diagnostic in ipairs(diagnostics) do 82 | local start_row, start_col = diagnostic.lnum, diagnostic.col 83 | local end_row = diagnostic.end_lnum or start_row 84 | local end_col = diagnostic.end_col or start_col 85 | 86 | for row = start_row, end_row do 87 | local range ---@type neodim.ColumnRange 88 | if start_row == end_row then 89 | range = { start_col = start_col, end_col = end_col } 90 | elseif row == start_row then 91 | range = { start_col = start_col, end_col = math.huge } 92 | elseif row == end_row then 93 | range = { start_col = 0, end_col = end_col } 94 | else 95 | range = { start_col = 0, end_col = math.huge } 96 | end 97 | 98 | local range_list = map_buf[row] 99 | if not range_list then 100 | range_list = list.new() 101 | map_buf[row] = range_list 102 | end 103 | list.insert(range_list, range) 104 | end 105 | end 106 | end 107 | 108 | ---@param row integer 109 | ---@param col integer 110 | ---@return boolean 111 | TSOverride.is_unused = function(self, bufnr, row, col) 112 | local map_buf = self.diagnostics_map[bufnr] 113 | if not map_buf then 114 | return false 115 | end 116 | local range_list = map_buf[row] 117 | if not range_list then 118 | return false 119 | end 120 | for _, range in list.iter(range_list) do 121 | if range.start_col <= col and col <= range.end_col then 122 | return true 123 | end 124 | end 125 | return false 126 | end 127 | 128 | ---@param hl vim.api.keyset.highlight 129 | ---@param hl_name string 130 | ---@return string 131 | TSOverride.get_dim_color = function(self, hl, hl_name) 132 | if not self.highlight_cache[hl_name] and hl and hl.fg then 133 | hl.fg = tostring(Color.from_int(hl.fg):blend(config.opts.blend_color, config.opts.alpha)) 134 | local unused_name = hl_name .. 'Unused' 135 | vim.api.nvim_set_hl(0, unused_name, hl) 136 | self.highlight_cache[hl_name] = unused_name 137 | end 138 | 139 | return self.highlight_cache[hl_name] 140 | end 141 | 142 | ---@param mark vim.api.keyset.set_extmark 143 | ---@param buf integer 144 | ---@param start_row integer 145 | ---@param start_col integer 146 | ---@return boolean 147 | TSOverride.override_mark_with_lsp = function(self, mark, buf, start_row, start_col) 148 | local sttoken_mark_data = lsp.get_sttoken_mark_data(buf, start_row, start_col) 149 | if sttoken_mark_data and self:is_unused(buf, start_row, start_col) then 150 | mark.hl_group = self:get_dim_color(sttoken_mark_data.hl_opts, sttoken_mark_data.hl_name) 151 | mark.priority = config.opts.priority 152 | return true 153 | end 154 | return false 155 | end 156 | 157 | ---@param mark vim.api.keyset.set_extmark 158 | ---@param buf integer 159 | ---@param start_row integer 160 | ---@param start_col integer 161 | ---@param hl_query vim.treesitter.highlighter.Query 162 | ---@param capture integer 163 | ---@param metadata vim.treesitter.query.TSMetadata 164 | ---@return boolean 165 | TSOverride.override_mark_with_ts = function(self, mark, buf, start_row, start_col, hl_query, capture, metadata) 166 | ---@diagnostic disable-next-line: invisible 167 | local hl = hl_query:get_hl_from_capture(capture) 168 | if not hl or hl == 0 then 169 | return false 170 | end 171 | ---@diagnostic disable-next-line: invisible 172 | local capture_name = hl_query:query().captures[capture] 173 | 174 | if self:is_unused(buf, start_row, start_col) then 175 | mark.hl_group = self:get_dim_color( 176 | vim.api.nvim_get_hl(0, { id = hl, link = false }) --[[@as vim.api.keyset.highlight]], 177 | '@' .. capture_name 178 | ) 179 | mark.priority = config.opts.priority 180 | else 181 | mark.hl_group = hl 182 | mark.priority = (tonumber(metadata.priority) or vim.highlight.priorities.treesitter) 183 | + (capture_name == 'nospell' and 1 or 0) 184 | end 185 | 186 | if capture_name == 'spell' then 187 | mark.spell = true 188 | elseif capture_name == 'nospell' then 189 | mark.spell = false 190 | end 191 | 192 | return true 193 | end 194 | 195 | ---@param highlighter vim.treesitter.highlighter 196 | ---@param buf integer 197 | ---@param line integer 198 | TSOverride.on_line_impl = function(self, highlighter, buf, line) 199 | ---@diagnostic disable-next-line: invisible 200 | highlighter:for_each_highlight_state(function(state) 201 | local root_node = state.tstree:root() 202 | 203 | local root_start_row, _, root_end_row, _ = root_node:range() 204 | if line < root_start_row or root_end_row < line then 205 | return 206 | end 207 | 208 | if state.iter == nil or state.next_row < line then 209 | ---@diagnostic disable-next-line: invisible 210 | state.iter = state.highlighter_query:query():iter_captures(root_node, highlighter.bufnr, line, root_end_row + 1) 211 | end 212 | 213 | while state.next_row <= line do 214 | local capture, node, metadata = state.iter() 215 | 216 | if capture == nil then 217 | break 218 | end 219 | 220 | local range = vim.treesitter.get_range(node, buf, metadata[capture]) 221 | ---@type integer, integer, integer, integer 222 | local start_row, start_col, end_row, end_col = Range.unpack4(range) 223 | 224 | if line <= end_row then 225 | ---@type vim.api.keyset.set_extmark 226 | local mark = { 227 | end_line = end_row, 228 | end_col = end_col, 229 | ephemeral = true, 230 | conceal = metadata.conceal, 231 | } 232 | if 233 | self:override_mark_with_lsp(mark, buf, start_row, start_col) 234 | or self:override_mark_with_ts(mark, buf, start_row, start_col, state.highlighter_query, capture, metadata) 235 | then 236 | vim.api.nvim_buf_set_extmark(buf, NAMESPACE, start_row, start_col, mark) 237 | end 238 | end 239 | 240 | if line < start_row then 241 | state.next_row = start_row 242 | end 243 | end 244 | end) 245 | end 246 | 247 | return TSOverride 248 | -------------------------------------------------------------------------------- /lua/neodim/config.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local Color = require 'neodim.Color' 4 | 5 | ---@return neodim.Color 6 | local get_bg = function() 7 | local normal = vim.api.nvim_get_hl(0, { name = 'Normal', link = false }) 8 | if normal and normal.bg then 9 | return Color.from_int(normal.bg) 10 | elseif vim.o.background == 'light' then 11 | return Color.new(0xFF, 0xFF, 0xFF) 12 | else 13 | return Color.new(0x00, 0x00, 0x00) 14 | end 15 | end 16 | 17 | ---@class neodim.Options 18 | ---@field alpha number 19 | ---@field blend_color neodim.Color 20 | ---@field hide { underline: boolean?, virtual_text: boolean?, signs: boolean? } 21 | ---@field priority integer 22 | ---@field disable table 23 | ---@field regex string[] | table 24 | 25 | ---@class neodim.SetupOptions 26 | ---@field alpha? number 27 | ---@field blend_color? string 28 | ---@field hide? { underline: boolean?, virtual_text: boolean?, signs: boolean? } 29 | ---@field priority? integer 30 | ---@field disable? string[] 31 | ---@field regex? string[] | table 32 | 33 | ---@type neodim.Options 34 | M.opts = { 35 | alpha = 0.75, 36 | blend_color = get_bg(), 37 | hide = { underline = true, virtual_text = true, signs = true }, 38 | priority = 128, 39 | disable = {}, 40 | regex = { 41 | '[uU]nused', 42 | '[nN]ever [rR]ead', 43 | '[nN]ot [rR]ead', 44 | }, 45 | } 46 | 47 | ---@generic T 48 | ---@param val T? 49 | ---@param default T 50 | ---@return T 51 | local function r(val, default) 52 | if val == nil then 53 | return default 54 | else 55 | return val 56 | end 57 | end 58 | 59 | local raw = {} ---@type neodim.SetupOptions 60 | 61 | ---@param opts neodim.SetupOptions? 62 | M.setup = function(opts) 63 | ---@type neodim.SetupOptions 64 | raw = vim.tbl_extend('force', raw, opts or {}) 65 | raw.hide = raw.hide or {} 66 | M.opts = { 67 | alpha = r(raw.alpha, M.opts.alpha), 68 | blend_color = r(raw.blend_color and Color.from_str(raw.blend_color), M.opts.blend_color), 69 | hide = { 70 | underline = r(raw.hide.underline, M.opts.hide.underline), 71 | virtual_text = r(raw.hide.virtual_text, M.opts.hide.virtual_text), 72 | signs = r(raw.hide.signs, M.opts.hide.signs), 73 | }, 74 | priority = r(raw.priority, M.opts.priority), 75 | disable = r(raw.disable, M.opts.disable), 76 | regex = r(raw.regex, M.opts.regex), 77 | } 78 | 79 | for _, lang in ipairs(M.opts.disable) do 80 | M.opts.disable[lang] = true 81 | end 82 | end 83 | 84 | vim.api.nvim_create_autocmd('ColorScheme', { 85 | callback = function() 86 | if not raw.blend_color then 87 | M.opts.blend_color = get_bg() 88 | end 89 | end, 90 | }) 91 | 92 | return M 93 | -------------------------------------------------------------------------------- /lua/neodim/filter.lua: -------------------------------------------------------------------------------- 1 | local filter = {} 2 | 3 | local config = require 'neodim.config' 4 | 5 | ---@param str string 6 | ---@return boolean 7 | local unused_string = function(str) 8 | local unused_regexes = config.opts.regex 9 | 10 | if not str then 11 | return false 12 | end 13 | 14 | str = tostring(str) 15 | 16 | local regexes 17 | 18 | local ft = vim.api.nvim_get_option_value('filetype', { buf = 0 }) 19 | if unused_regexes[ft] then 20 | ---@cast unused_regexes table 21 | -- don't merge global regexes because there is no way to modify global regexes 22 | regexes = unused_regexes[ft] 23 | else 24 | ---@cast unused_regexes string[] 25 | regexes = unused_regexes 26 | end 27 | 28 | for _, regex in ipairs(regexes) do 29 | if str:find(regex) ~= nil then 30 | return true 31 | end 32 | end 33 | 34 | return false 35 | end 36 | 37 | ---@param diagnostic vim.Diagnostic 38 | ---@return boolean 39 | local has_unused_tags = function(diagnostic) 40 | -- NOTE: `_tags` is available as of Neovim 0.10.0 41 | return diagnostic._tags and diagnostic._tags.unnecessary or false 42 | end 43 | 44 | ---@param diagnostic vim.Diagnostic 45 | ---@return boolean 46 | local has_unused_string = function(diagnostic) 47 | return unused_string(diagnostic.message) or diagnostic.code and unused_string(tostring(diagnostic.code)) or false 48 | end 49 | 50 | ---@param diagnostics vim.Diagnostic[] 51 | ---@return vim.Diagnostic[] 52 | filter.get_unused = function(diagnostics) 53 | local unused_filter = function(d) 54 | return has_unused_tags(d) or has_unused_string(d) 55 | end 56 | 57 | return vim.tbl_filter(unused_filter, diagnostics) 58 | end 59 | 60 | ---@param diagnostics vim.Diagnostic[] 61 | ---@return vim.Diagnostic[] 62 | filter.get_used = function(diagnostics) 63 | local used_filter = function(d) 64 | return not (has_unused_tags(d) or has_unused_string(d)) 65 | end 66 | 67 | return vim.tbl_filter(used_filter, diagnostics) 68 | end 69 | 70 | return filter 71 | -------------------------------------------------------------------------------- /lua/neodim/init.lua: -------------------------------------------------------------------------------- 1 | local dim = {} 2 | 3 | local TSOverride = require 'neodim.TSOverride' 4 | local filter = require 'neodim.filter' 5 | local config = require 'neodim.config' 6 | 7 | ---@param old_handler vim.diagnostic.Handler 8 | ---@param disable table 9 | ---@return vim.diagnostic.Handler 10 | local create_handler = function(old_handler, disable) 11 | return { 12 | show = function(namespace, bufnr, diagnostics, opts) 13 | local ft = vim.api.nvim_get_option_value('filetype', { buf = bufnr }) 14 | if not disable[ft] then 15 | diagnostics = filter.get_used(diagnostics) 16 | end 17 | old_handler.show(namespace, bufnr, diagnostics, opts) 18 | end, 19 | hide = old_handler.hide, 20 | } 21 | end 22 | 23 | ---@param opts neodim.SetupOptions 24 | dim.setup = function(opts) 25 | config.setup(opts) 26 | for d_handler, enable in pairs(config.opts.hide) do 27 | if enable then 28 | vim.diagnostic.handlers[d_handler] = create_handler(vim.diagnostic.handlers[d_handler], config.opts.disable) 29 | end 30 | end 31 | local ts_override = TSOverride.init() 32 | vim.diagnostic.handlers['dim/unused'] = { 33 | show = function(_, bufnr, diagnostics, _) 34 | ts_override:update_unused(filter.get_unused(diagnostics), bufnr) 35 | end, 36 | hide = function(_, bufnr) 37 | ts_override:update_unused({}, bufnr) 38 | end, 39 | } 40 | end 41 | 42 | return dim 43 | -------------------------------------------------------------------------------- /lua/neodim/list.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.new = function() 4 | return { [0] = 0 } 5 | end 6 | 7 | ---@generic T: table 8 | ---@param list T 9 | ---@return T 10 | M.from_raw = function(list) 11 | list[0] = #list 12 | return list 13 | end 14 | 15 | ---@param list table 16 | ---@return integer 17 | M.len = function(list) 18 | return list[0] 19 | end 20 | 21 | ---@generic T 22 | ---@param list T[] 23 | ---@param item T 24 | M.insert = function(list, item) 25 | local new_len = list[0] + 1 26 | list[0] = new_len 27 | list[new_len] = item 28 | end 29 | 30 | ---@generic T 31 | ---@param list T[] 32 | ---@param src T[] 33 | M.extend = function(list, src) 34 | local list_len = list[0] 35 | local src_len = src[0] 36 | list[0] = list_len + src_len 37 | for i = 1, src_len do 38 | list[list_len + i] = src[i] 39 | end 40 | end 41 | 42 | ---@generic T 43 | ---@param list T[] 44 | ---@return fun(): integer, T 45 | M.iter = function(list) 46 | local len = list[0] 47 | local i = 0 48 | return function() 49 | i = i + 1 50 | if i <= len then 51 | return i, list[i] 52 | end 53 | end 54 | end 55 | 56 | return M 57 | -------------------------------------------------------------------------------- /lua/neodim/lsp.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | 3 | local list = require 'neodim.list' 4 | 5 | local M = {} 6 | 7 | ---@alias extmark_data { priority: integer, hl_name: string, hl_opts: table }? 8 | 9 | ---@class extmark 10 | ---@field [1] integer mark ID 11 | ---@field [2] integer row 12 | ---@field [3] integer column 13 | ---@field [4] extmark_details 14 | 15 | ---@class extmark_details 16 | ---@field hl_group string 17 | ---@field priority integer 18 | ---@field end_col integer 19 | ---@field end_row integer 20 | 21 | ---@param buf buffer 22 | ---@param token_range STTokenRange 23 | ---@return extmark[] 24 | local function get_sttoken_extmarks(buf, token_range) 25 | -- NOTE: vim.lsp.get_active_clients() was renamed to get_clients() and deprecated on Neovim v0.10 26 | ---@diagnostic disable-next-line: deprecated 27 | local get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients 28 | ---@type table 29 | local client_ids = {} 30 | for _, client in pairs(get_clients { bufnr = buf }) do 31 | client_ids[client.id] = true 32 | end 33 | 34 | local start = { token_range.line, token_range.start_col } 35 | local end_ = { token_range.line, token_range.end_col } 36 | local opts = { type = 'highlight', details = true } 37 | ---@type extmark[] 38 | local extmarks = list.new() 39 | for name, ns_id in pairs(vim.api.nvim_get_namespaces()) do 40 | local client_id = name:sub(#'vim_lsp_semantic_tokens:') 41 | if client_ids[tonumber(client_id)] then 42 | list.extend(extmarks, list.from_raw(api.nvim_buf_get_extmarks(buf, ns_id, start, end_, opts))) 43 | end 44 | end 45 | return extmarks 46 | end 47 | 48 | ---@param extmarks extmark[] 49 | ---@return extmark_data 50 | local function get_max_pri_extmark(extmarks) 51 | local priority = 0 52 | local hl_name 53 | local hl_opts 54 | 55 | for _, extmark in ipairs(extmarks) do 56 | local details = extmark[4] 57 | if priority < details.priority then 58 | local _hl_opts = api.nvim_get_hl(0, { name = details.hl_group, link = false }) 59 | if next(_hl_opts) then 60 | hl_opts = _hl_opts 61 | priority = details.priority 62 | hl_name = details.hl_group 63 | end 64 | end 65 | end 66 | 67 | if hl_name then 68 | return { 69 | priority = priority, 70 | hl_name = hl_name, 71 | hl_opts = hl_opts, 72 | } 73 | end 74 | end 75 | 76 | ---@param buf buffer 77 | ---@param row integer 78 | ---@param col integer 79 | ---@return extmark_data? 80 | function M.get_sttoken_mark_data(buf, row, col) 81 | local max_priority = 0 82 | ---@type extmark_data? 83 | local mark_data 84 | 85 | ---@type STTokenRange[]? 86 | local token_ranges = vim.lsp.semantic_tokens.get_at_pos(buf, row, col) 87 | for _, token_range in ipairs(token_ranges or {}) do 88 | local extmarks = get_sttoken_extmarks(buf, token_range) 89 | local info = get_max_pri_extmark(extmarks) 90 | if info and info.priority > max_priority then 91 | mark_data = info 92 | end 93 | end 94 | 95 | return mark_data 96 | end 97 | 98 | return M 99 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 120 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferSingle" 6 | call_parentheses = "None" 7 | --------------------------------------------------------------------------------