hl.lua (6337B)
1 local api = vim.api 2 3 local M = {} 4 5 --- Table with default priorities used for highlighting: 6 --- - `syntax`: `50`, used for standard syntax highlighting 7 --- - `treesitter`: `100`, used for treesitter-based highlighting 8 --- - `semantic_tokens`: `125`, used for LSP semantic token highlighting 9 --- - `diagnostics`: `150`, used for code analysis such as diagnostics 10 --- - `user`: `200`, used for user-triggered highlights such as LSP document 11 --- symbols or `on_yank` autocommands 12 M.priorities = { 13 syntax = 50, 14 treesitter = 100, 15 semantic_tokens = 125, 16 diagnostics = 150, 17 user = 200, 18 } 19 20 --- @class vim.hl.range.Opts 21 --- @inlinedoc 22 --- 23 --- Type of range. See [getregtype()] 24 --- (default: `'v'` i.e. charwise) 25 --- @field regtype? string 26 --- 27 --- Indicates whether the range is end-inclusive 28 --- (default: `false`) 29 --- @field inclusive? boolean 30 --- 31 --- Highlight priority 32 --- (default: `vim.hl.priorities.user`) 33 --- @field priority? integer 34 --- 35 --- Time in ms before highlight is cleared 36 --- (default: -1 no timeout) 37 --- @field timeout? integer 38 39 --- Apply highlight group to range of text. 40 --- 41 ---@param bufnr integer Buffer number to apply highlighting to 42 ---@param ns integer Namespace to add highlight to 43 ---@param higroup string Highlight group to use for highlighting 44 ---@param start [integer,integer]|string Start of region as a (line, column) tuple or string accepted by |getpos()| 45 ---@param finish [integer,integer]|string End of region as a (line, column) tuple or string accepted by |getpos()| 46 ---@param opts? vim.hl.range.Opts 47 --- @return uv.uv_timer_t? range_timer A timer which manages how much time the 48 --- highlight has left 49 --- @return fun()? range_clear A function which allows clearing the highlight manually. 50 --- nil is returned if timeout is not specified 51 function M.range(bufnr, ns, higroup, start, finish, opts) 52 opts = opts or {} 53 local regtype = opts.regtype or 'v' 54 local inclusive = opts.inclusive or false 55 local priority = opts.priority or M.priorities.user 56 local timeout = opts.timeout or -1 57 58 local v_maxcol = vim.v.maxcol 59 60 local pos1 = type(start) == 'string' and vim.fn.getpos(start) 61 or { 62 bufnr, 63 start[1] + 1, 64 start[2] ~= -1 and start[2] ~= v_maxcol and start[2] + 1 or v_maxcol, 65 0, 66 } 67 local pos2 = type(finish) == 'string' and vim.fn.getpos(finish) 68 or { 69 bufnr, 70 finish[1] + 1, 71 finish[2] ~= -1 and start[2] ~= v_maxcol and finish[2] + 1 or v_maxcol, 72 0, 73 } 74 75 local buf_line_count = api.nvim_buf_line_count(bufnr) 76 pos1[2] = math.min(pos1[2], buf_line_count) 77 pos2[2] = math.min(pos2[2], buf_line_count) 78 79 if pos1[2] <= 0 or pos1[3] <= 0 or pos2[2] <= 0 or pos2[3] <= 0 then 80 return 81 end 82 83 vim._with({ buf = bufnr }, function() 84 if pos1[3] ~= v_maxcol then 85 local max_col1 = vim.fn.col({ pos1[2], '$' }) 86 pos1[3] = math.min(pos1[3], max_col1) 87 end 88 if pos2[3] ~= v_maxcol then 89 local max_col2 = vim.fn.col({ pos2[2], '$' }) 90 pos2[3] = math.min(pos2[3], max_col2) 91 end 92 end) 93 94 local region = vim.fn.getregionpos(pos1, pos2, { 95 type = regtype, 96 exclusive = not inclusive, 97 eol = true, 98 }) 99 -- For non-blockwise selection, use a single extmark. 100 if regtype == 'v' or regtype == 'V' then 101 --- @type [ [integer, integer, integer, integer], [integer, integer, integer, integer]][] 102 region = { { assert(region[1])[1], assert(region[#region])[2] } } 103 local region1 = assert(region[1]) 104 if 105 regtype == 'V' 106 or region1[2][2] == pos1[2] and pos1[3] == v_maxcol 107 or region1[2][2] == pos2[2] and pos2[3] == v_maxcol 108 then 109 region1[2][2] = region1[2][2] + 1 110 region1[2][3] = 0 111 end 112 end 113 114 local extmarks = {} --- @type integer[] 115 for _, res in ipairs(region) do 116 local start_row = res[1][2] - 1 117 local start_col = res[1][3] - 1 118 local end_row = res[2][2] - 1 119 local end_col = res[2][3] 120 table.insert( 121 extmarks, 122 api.nvim_buf_set_extmark(bufnr, ns, start_row, start_col, { 123 hl_group = higroup, 124 end_row = end_row, 125 end_col = end_col, 126 priority = priority, 127 strict = false, 128 }) 129 ) 130 end 131 132 local range_hl_clear = function() 133 if not api.nvim_buf_is_valid(bufnr) then 134 return 135 end 136 for _, mark in ipairs(extmarks) do 137 api.nvim_buf_del_extmark(bufnr, ns, mark) 138 end 139 end 140 141 if timeout ~= -1 then 142 local range_timer = vim.defer_fn(range_hl_clear, timeout) 143 return range_timer, range_hl_clear 144 end 145 end 146 147 local yank_timer --- @type uv.uv_timer_t? 148 local yank_hl_clear --- @type fun()? 149 local yank_ns = api.nvim_create_namespace('nvim.hlyank') 150 151 --- Highlight the yanked text during a |TextYankPost| event. 152 --- 153 --- Add the following to your `init.vim`: 154 --- 155 --- ```vim 156 --- autocmd TextYankPost * silent! lua vim.hl.on_yank {higroup='Visual', timeout=300} 157 --- ``` 158 --- 159 --- @param opts table|nil Optional parameters 160 --- - higroup highlight group for yanked region (default "IncSearch") 161 --- - timeout time in ms before highlight is cleared (default 150) 162 --- - on_macro highlight when executing macro (default false) 163 --- - on_visual highlight when yanking visual selection (default true) 164 --- - event event structure (default vim.v.event) 165 --- - priority integer priority (default |vim.hl.priorities|`.user`) 166 function M.on_yank(opts) 167 vim.validate('opts', opts, 'table', true) 168 opts = opts or {} 169 local event = opts.event or vim.v.event 170 local on_macro = opts.on_macro or false 171 local on_visual = (opts.on_visual ~= false) 172 173 if not on_macro and vim.fn.reg_executing() ~= '' then 174 return 175 end 176 if event.operator ~= 'y' or event.regtype == '' then 177 return 178 end 179 if not on_visual and event.visual then 180 return 181 end 182 183 local higroup = opts.higroup or 'IncSearch' 184 185 local bufnr = api.nvim_get_current_buf() 186 local winid = api.nvim_get_current_win() 187 188 if yank_timer and not yank_timer:is_closing() then 189 yank_timer:close() 190 assert(yank_hl_clear) 191 yank_hl_clear() 192 end 193 194 api.nvim__ns_set(yank_ns, { wins = { winid } }) 195 yank_timer, yank_hl_clear = M.range(bufnr, yank_ns, higroup, "'[", "']", { 196 regtype = event.regtype, 197 inclusive = true, 198 priority = opts.priority or M.priorities.user, 199 timeout = opts.timeout or 150, 200 }) 201 end 202 203 return M