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