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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
|
local api, if_nil = vim.api, vim.F.if_nil
local shared = require('vim.diagnostic._shared')
local store = require('vim.diagnostic._store')
--- @class (private) vim.diagnostic._float
local M = {}
local severity = vim.diagnostic.severity
--- @type table<vim.diagnostic.Severity, string>
local floating_highlight_map = {
[severity.ERROR] = 'DiagnosticFloatingError',
[severity.WARN] = 'DiagnosticFloatingWarn',
[severity.INFO] = 'DiagnosticFloatingInfo',
[severity.HINT] = 'DiagnosticFloatingHint',
}
--- @param opts vim.diagnostic.Opts.Float
--- @param bufnr integer
--- @return vim.diagnostic.Opts.Float, vim.diagnostic.Opts
local function resolve_float_opts(opts, bufnr)
-- Resolve options with user settings from vim.diagnostic.config
-- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float`
-- does not have a dedicated table for configuration options; instead, the options are mixed in
-- with its `opts` table. We create a dedicated options table (`float_opts`) that inherits
-- missing keys from the global configuration (`global_diagnostic_options.float`), which can
-- be a table or a function.
local global_opts = assert(vim.diagnostic.config())
local float_opts = global_opts.float
local resolved_float_opts = type(float_opts) == 'table' and float_opts
or (type(float_opts) == 'function' and float_opts(opts.namespace, bufnr) or {})
return vim.tbl_extend('keep', opts, resolved_float_opts), global_opts
end
--- @param opts vim.diagnostic.Opts.Float?
--- @return integer? float_bufnr
--- @return integer? winid
function M.open(opts, ...)
-- Support old (bufnr, opts) signature
local bufnr --- @type integer?
if opts == nil or type(opts) == 'number' then
bufnr = opts
opts = ... --- @type vim.diagnostic.Opts.Float
else
vim.validate('opts', opts, 'table', true)
end
opts = opts or {}
bufnr = vim._resolve_bufnr(bufnr or opts.bufnr)
local global_opts --- @type vim.diagnostic.Opts
opts, global_opts = resolve_float_opts(opts, bufnr)
local scope = ({ l = 'line', c = 'cursor', b = 'buffer' })[opts.scope] or opts.scope or 'line'
local lnum, col --- @type integer, integer
local opts_pos = opts.pos
if scope == 'line' or scope == 'cursor' then
if not opts_pos then
local pos = api.nvim_win_get_cursor(0)
lnum = pos[1] - 1
col = pos[2]
elseif type(opts_pos) == 'number' then
lnum = opts_pos
elseif type(opts_pos) == 'table' then
lnum, col = opts_pos[1], opts_pos[2]
else
error("Invalid value for option 'pos'")
end
elseif scope ~= 'buffer' then
error("Invalid value for option 'scope'")
end
local diagnostics = store.get_diagnostics(bufnr, opts --[[@as vim.diagnostic.GetOpts]], true)
if scope == 'line' then
--- @param diagnostic vim.Diagnostic
local function line_filter(diagnostic)
local d_lnum, _, d_end_lnum, d_end_col = shared.get_logical_pos(diagnostic)
return lnum >= d_lnum
and lnum <= d_end_lnum
and (d_lnum == d_end_lnum or lnum ~= d_end_lnum or d_end_col ~= 0)
end
diagnostics = vim.tbl_filter(line_filter, diagnostics)
elseif scope == 'cursor' then
-- If `col` is past the end of the line, show if the cursor is on the last char in the line
local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
--- @param diagnostic vim.Diagnostic
local function cursor_filter(diagnostic)
local d_lnum, d_col, d_end_lnum, d_end_col = shared.get_logical_pos(diagnostic)
return lnum >= d_lnum
and lnum <= d_end_lnum
and (lnum ~= d_lnum or col >= math.min(d_col, line_length - 1))
and ((d_lnum == d_end_lnum and d_col == d_end_col) or lnum ~= d_end_lnum or col < d_end_col)
end
diagnostics = vim.tbl_filter(cursor_filter, diagnostics)
end
if vim.tbl_isempty(diagnostics) then
return
end
local severity_sort = if_nil(opts.severity_sort, global_opts.severity_sort)
if severity_sort then
if type(severity_sort) == 'table' and severity_sort.reverse then
table.sort(diagnostics, function(a, b)
return shared.diagnostic_cmp(a, b, 'severity', true)
end)
else
table.sort(diagnostics, function(a, b)
return shared.diagnostic_cmp(a, b, 'severity', false)
end)
end
end
local lines = {} --- @type string[]
local highlights = {} --- @type { hlname: string, prefix?: { length: integer, hlname: string? }, suffix?: { length: integer, hlname: string? } }[]
local header = if_nil(opts.header, 'Diagnostics:')
if header then
vim.validate('header', header, { 'string', 'table' }, "'string' or 'table'")
if type(header) == 'table' then
-- Don't insert any lines for an empty string
if #(header[1] or '') > 0 then
lines[#lines + 1] = header[1]
highlights[#highlights + 1] = { hlname = header[2] or 'Bold' }
end
elseif #header > 0 then
lines[#lines + 1] = header
highlights[#highlights + 1] = { hlname = 'Bold' }
end
end
if opts.format then
diagnostics = shared.reformat_diagnostics(opts.format, diagnostics)
end
if opts.source and (opts.source ~= 'if_many' or shared.count_sources(bufnr) > 1) then
diagnostics = shared.prefix_source(diagnostics)
end
local prefix_opt = opts.prefix
or (scope == 'cursor' and #diagnostics <= 1) and ''
or function(_, i)
return string.format('%d. ', i)
end
local prefix, prefix_hl_group --- @type string?, string?
if prefix_opt then
vim.validate(
'prefix',
prefix_opt,
{ 'string', 'table', 'function' },
"'string' or 'table' or 'function'"
)
if type(prefix_opt) == 'string' then
prefix, prefix_hl_group = prefix_opt, 'NormalFloat'
elseif type(prefix_opt) == 'table' then
prefix, prefix_hl_group = prefix_opt[1] or '', prefix_opt[2] or 'NormalFloat'
end
end
local suffix_opt = opts.suffix
or function(diagnostic)
return diagnostic.code and string.format(' [%s]', diagnostic.code) or ''
end
local suffix, suffix_hl_group --- @type string?, string?
if suffix_opt then
vim.validate(
'suffix',
suffix_opt,
{ 'string', 'table', 'function' },
"'string' or 'table' or 'function'"
)
if type(suffix_opt) == 'string' then
suffix, suffix_hl_group = suffix_opt, 'NormalFloat'
elseif type(suffix_opt) == 'table' then
suffix, suffix_hl_group = suffix_opt[1] or '', suffix_opt[2] or 'NormalFloat'
end
end
local related_info_locations = {} --- @type table<integer, lsp.Location>
for i, diagnostic in ipairs(diagnostics) do
if type(prefix_opt) == 'function' then
local prefix0, prefix_hl_group0 = prefix_opt(diagnostic, i, #diagnostics)
prefix, prefix_hl_group = prefix0 or '', prefix_hl_group0 or 'NormalFloat'
end
if type(suffix_opt) == 'function' then
local suffix0, suffix_hl_group0 = suffix_opt(diagnostic, i, #diagnostics)
suffix, suffix_hl_group = suffix0 or '', suffix_hl_group0 or 'NormalFloat'
end
local hiname = floating_highlight_map[diagnostic.severity]
local message_lines = vim.split(diagnostic.message, '\n')
local default_pre = string.rep(' ', #prefix)
for j = 1, #message_lines do
local pre = j == 1 and prefix or default_pre
local suf = j == #message_lines and suffix or ''
lines[#lines + 1] = pre .. message_lines[j] .. suf
highlights[#highlights + 1] = {
hlname = hiname,
prefix = {
length = j == 1 and #prefix or 0,
hlname = prefix_hl_group,
},
suffix = {
length = #suf,
hlname = suffix_hl_group,
},
}
end
--- @type lsp.DiagnosticRelatedInformation[]
local related_info = vim.tbl_get(diagnostic, 'user_data', 'lsp', 'relatedInformation') or {}
-- Below the diagnostic, show its LSP related information (if any) in the form of file name and
-- range, plus description.
for _, info in ipairs(related_info) do
local location = info.location
local file_name = vim.fs.basename(vim.uri_to_fname(location.uri))
local info_suffix = ': ' .. info.message
related_info_locations[#lines + 1] = location
lines[#lines + 1] = string.format(
'%s%s:%s:%s%s',
default_pre,
file_name,
location.range.start.line + 1,
location.range.start.character + 1,
info_suffix
)
highlights[#highlights + 1] = {
hlname = '@string.special.path',
prefix = {
length = #default_pre,
hlname = prefix_hl_group,
},
suffix = {
length = #info_suffix,
hlname = 'NormalFloat',
},
}
end
end
-- Used by open_floating_preview to allow the float to be focused
if not opts.focus_id then
opts.focus_id = scope
end
--- @diagnostic disable-next-line: param-type-mismatch
local float_bufnr, winnr = vim.lsp.util.open_floating_preview(lines, 'plaintext', opts)
vim.bo[float_bufnr].path = vim.bo[bufnr].path
-- TODO: Handle this generally (like vim.ui.open()), rather than overriding gf.
vim.keymap.set('n', 'gf', function()
local cursor_row = api.nvim_win_get_cursor(0)[1]
local location = related_info_locations[cursor_row]
if location then
-- Split the window before calling `show_document` so the window doesn't disappear.
vim.cmd.split()
vim.lsp.util.show_document(location, 'utf-16', { focus = true })
else
vim.cmd.normal({ 'gf', bang = true })
end
end, { buf = float_bufnr, remap = false })
--- @diagnostic disable-next-line: deprecated
local add_highlight = api.nvim_buf_add_highlight
for i, hl in ipairs(highlights) do
local line = lines[i]
local prefix_len = hl.prefix and hl.prefix.length or 0
local suffix_len = hl.suffix and hl.suffix.length or 0
if prefix_len > 0 then
add_highlight(float_bufnr, -1, hl.prefix.hlname, i - 1, 0, prefix_len)
end
add_highlight(float_bufnr, -1, hl.hlname, i - 1, prefix_len, #line - suffix_len)
if suffix_len > 0 then
add_highlight(float_bufnr, -1, hl.suffix.hlname, i - 1, #line - suffix_len, -1)
end
end
return float_bufnr, winnr
end
return M
|