diagnostic.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. local protocol = require('vim.lsp.protocol')
  2. local ms = protocol.Methods
  3. local api = vim.api
  4. local M = {}
  5. local augroup = api.nvim_create_augroup('nvim.lsp.diagnostic', {})
  6. local DEFAULT_CLIENT_ID = -1
  7. ---@param severity lsp.DiagnosticSeverity
  8. local function severity_lsp_to_vim(severity)
  9. if type(severity) == 'string' then
  10. severity = protocol.DiagnosticSeverity[severity] --- @type integer
  11. end
  12. return severity
  13. end
  14. ---@return lsp.DiagnosticSeverity
  15. local function severity_vim_to_lsp(severity)
  16. if type(severity) == 'string' then
  17. severity = vim.diagnostic.severity[severity] --- @type integer
  18. end
  19. return severity
  20. end
  21. ---@param bufnr integer
  22. ---@return string[]?
  23. local function get_buf_lines(bufnr)
  24. if vim.api.nvim_buf_is_loaded(bufnr) then
  25. return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
  26. end
  27. local filename = vim.api.nvim_buf_get_name(bufnr)
  28. local f = io.open(filename)
  29. if not f then
  30. return
  31. end
  32. local content = f:read('*a')
  33. if not content then
  34. -- Some LSP servers report diagnostics at a directory level, in which case
  35. -- io.read() returns nil
  36. f:close()
  37. return
  38. end
  39. local lines = vim.split(content, '\n')
  40. f:close()
  41. return lines
  42. end
  43. --- @param diagnostic lsp.Diagnostic
  44. --- @param client_id integer
  45. --- @return table?
  46. local function tags_lsp_to_vim(diagnostic, client_id)
  47. local tags ---@type table?
  48. for _, tag in ipairs(diagnostic.tags or {}) do
  49. if tag == protocol.DiagnosticTag.Unnecessary then
  50. tags = tags or {}
  51. tags.unnecessary = true
  52. elseif tag == protocol.DiagnosticTag.Deprecated then
  53. tags = tags or {}
  54. tags.deprecated = true
  55. else
  56. vim.lsp.log.info(string.format('Unknown DiagnosticTag %d from LSP client %d', tag, client_id))
  57. end
  58. end
  59. return tags
  60. end
  61. ---@param diagnostics lsp.Diagnostic[]
  62. ---@param bufnr integer
  63. ---@param client_id integer
  64. ---@return vim.Diagnostic[]
  65. local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
  66. local buf_lines = get_buf_lines(bufnr)
  67. local client = vim.lsp.get_client_by_id(client_id)
  68. local position_encoding = client and client.offset_encoding or 'utf-16'
  69. --- @param diagnostic lsp.Diagnostic
  70. --- @return vim.Diagnostic
  71. return vim.tbl_map(function(diagnostic)
  72. local start = diagnostic.range.start
  73. local _end = diagnostic.range['end']
  74. local message = diagnostic.message
  75. if type(message) ~= 'string' then
  76. vim.notify_once(
  77. string.format('Unsupported Markup message from LSP client %d', client_id),
  78. vim.lsp.log_levels.ERROR
  79. )
  80. --- @diagnostic disable-next-line: undefined-field,no-unknown
  81. message = diagnostic.message.value
  82. end
  83. local line = buf_lines and buf_lines[start.line + 1] or ''
  84. --- @type vim.Diagnostic
  85. return {
  86. lnum = start.line,
  87. col = vim.str_byteindex(line, position_encoding, start.character, false),
  88. end_lnum = _end.line,
  89. end_col = vim.str_byteindex(line, position_encoding, _end.character, false),
  90. severity = severity_lsp_to_vim(diagnostic.severity),
  91. message = message,
  92. source = diagnostic.source,
  93. code = diagnostic.code,
  94. _tags = tags_lsp_to_vim(diagnostic, client_id),
  95. user_data = {
  96. lsp = diagnostic,
  97. },
  98. }
  99. end, diagnostics)
  100. end
  101. --- @param diagnostic vim.Diagnostic
  102. --- @return lsp.DiagnosticTag[]?
  103. local function tags_vim_to_lsp(diagnostic)
  104. if not diagnostic._tags then
  105. return
  106. end
  107. local tags = {} --- @type lsp.DiagnosticTag[]
  108. if diagnostic._tags.unnecessary then
  109. tags[#tags + 1] = protocol.DiagnosticTag.Unnecessary
  110. end
  111. if diagnostic._tags.deprecated then
  112. tags[#tags + 1] = protocol.DiagnosticTag.Deprecated
  113. end
  114. return tags
  115. end
  116. --- Converts the input `vim.Diagnostic`s to LSP diagnostics.
  117. --- @param diagnostics vim.Diagnostic[]
  118. --- @return lsp.Diagnostic[]
  119. function M.from(diagnostics)
  120. ---@param diagnostic vim.Diagnostic
  121. ---@return lsp.Diagnostic
  122. return vim.tbl_map(function(diagnostic)
  123. local user_data = diagnostic.user_data or {}
  124. if user_data.lsp then
  125. return user_data.lsp
  126. end
  127. return {
  128. range = {
  129. start = {
  130. line = diagnostic.lnum,
  131. character = diagnostic.col,
  132. },
  133. ['end'] = {
  134. line = diagnostic.end_lnum,
  135. character = diagnostic.end_col,
  136. },
  137. },
  138. severity = severity_vim_to_lsp(diagnostic.severity),
  139. message = diagnostic.message,
  140. source = diagnostic.source,
  141. code = diagnostic.code,
  142. tags = tags_vim_to_lsp(diagnostic),
  143. }
  144. end, diagnostics)
  145. end
  146. ---@type table<integer, integer>
  147. local _client_push_namespaces = {}
  148. ---@type table<string, integer>
  149. local _client_pull_namespaces = {}
  150. --- Get the diagnostic namespace associated with an LSP client |vim.diagnostic| for diagnostics
  151. ---
  152. ---@param client_id integer The id of the LSP client
  153. ---@param is_pull boolean? Whether the namespace is for a pull or push client. Defaults to push
  154. function M.get_namespace(client_id, is_pull)
  155. vim.validate('client_id', client_id, 'number')
  156. local client = vim.lsp.get_client_by_id(client_id)
  157. if is_pull then
  158. local server_id =
  159. vim.tbl_get((client or {}).server_capabilities, 'diagnosticProvider', 'identifier')
  160. local key = string.format('%d:%s', client_id, server_id or 'nil')
  161. local name = string.format(
  162. 'vim.lsp.%s.%d.%s',
  163. client and client.name or 'unknown',
  164. client_id,
  165. server_id or 'nil'
  166. )
  167. local ns = _client_pull_namespaces[key]
  168. if not ns then
  169. ns = api.nvim_create_namespace(name)
  170. _client_pull_namespaces[key] = ns
  171. end
  172. return ns
  173. else
  174. local name = string.format('vim.lsp.%s.%d', client and client.name or 'unknown', client_id)
  175. local ns = _client_push_namespaces[client_id]
  176. if not ns then
  177. ns = api.nvim_create_namespace(name)
  178. _client_push_namespaces[client_id] = ns
  179. end
  180. return ns
  181. end
  182. end
  183. local function convert_severity(opt)
  184. if type(opt) == 'table' and not opt.severity and opt.severity_limit then
  185. vim.deprecate('severity_limit', '{min = severity} See vim.diagnostic.severity', '0.11')
  186. opt.severity = { min = severity_lsp_to_vim(opt.severity_limit) }
  187. end
  188. end
  189. --- @param uri string
  190. --- @param client_id? integer
  191. --- @param diagnostics lsp.Diagnostic[]
  192. --- @param is_pull boolean
  193. local function handle_diagnostics(uri, client_id, diagnostics, is_pull)
  194. local fname = vim.uri_to_fname(uri)
  195. if #diagnostics == 0 and vim.fn.bufexists(fname) == 0 then
  196. return
  197. end
  198. local bufnr = vim.fn.bufadd(fname)
  199. if not bufnr then
  200. return
  201. end
  202. if client_id == nil then
  203. client_id = DEFAULT_CLIENT_ID
  204. end
  205. local namespace = M.get_namespace(client_id, is_pull)
  206. vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
  207. end
  208. --- |lsp-handler| for the method "textDocument/publishDiagnostics"
  209. ---
  210. --- See |vim.diagnostic.config()| for configuration options.
  211. ---
  212. ---@param _ lsp.ResponseError?
  213. ---@param result lsp.PublishDiagnosticsParams
  214. ---@param ctx lsp.HandlerContext
  215. function M.on_publish_diagnostics(_, result, ctx)
  216. handle_diagnostics(result.uri, ctx.client_id, result.diagnostics, false)
  217. end
  218. --- |lsp-handler| for the method "textDocument/diagnostic"
  219. ---
  220. --- See |vim.diagnostic.config()| for configuration options.
  221. ---
  222. ---@param error lsp.ResponseError?
  223. ---@param result lsp.DocumentDiagnosticReport
  224. ---@param ctx lsp.HandlerContext
  225. function M.on_diagnostic(error, result, ctx)
  226. if error ~= nil and error.code == protocol.ErrorCodes.ServerCancelled then
  227. if error.data == nil or error.data.retriggerRequest ~= false then
  228. local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
  229. client:request(ctx.method, ctx.params)
  230. end
  231. return
  232. end
  233. if result == nil or result.kind == 'unchanged' then
  234. return
  235. end
  236. handle_diagnostics(ctx.params.textDocument.uri, ctx.client_id, result.items, true)
  237. end
  238. --- Clear push diagnostics and diagnostic cache.
  239. ---
  240. --- Diagnostic producers should prefer |vim.diagnostic.reset()|. However,
  241. --- this method signature is still used internally in some parts of the LSP
  242. --- implementation so it's simply marked @private rather than @deprecated.
  243. ---
  244. ---@param client_id integer
  245. ---@param buffer_client_map table<integer, table<integer, table>> map of buffers to active clients
  246. ---@private
  247. function M.reset(client_id, buffer_client_map)
  248. buffer_client_map = vim.deepcopy(buffer_client_map)
  249. vim.schedule(function()
  250. for bufnr, client_ids in pairs(buffer_client_map) do
  251. if client_ids[client_id] then
  252. local namespace = M.get_namespace(client_id, false)
  253. vim.diagnostic.reset(namespace, bufnr)
  254. end
  255. end
  256. end)
  257. end
  258. --- Get the diagnostics by line
  259. ---
  260. --- Marked private as this is used internally by the LSP subsystem, but
  261. --- most users should instead prefer |vim.diagnostic.get()|.
  262. ---
  263. ---@param bufnr integer|nil The buffer number
  264. ---@param line_nr integer|nil The line number
  265. ---@param opts {severity?:lsp.DiagnosticSeverity}?
  266. --- - severity: (lsp.DiagnosticSeverity)
  267. --- - Only return diagnostics with this severity.
  268. ---@param client_id integer|nil the client id
  269. ---@return table Table with map of line number to list of diagnostics.
  270. --- Structured: { [1] = {...}, [5] = {.... } }
  271. ---@private
  272. function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
  273. vim.deprecate('vim.lsp.diagnostic.get_line_diagnostics', 'vim.diagnostic.get', '0.12')
  274. convert_severity(opts)
  275. local diag_opts = {} --- @type vim.diagnostic.GetOpts
  276. if opts and opts.severity then
  277. diag_opts.severity = severity_lsp_to_vim(opts.severity)
  278. end
  279. if client_id then
  280. diag_opts.namespace = M.get_namespace(client_id, false)
  281. end
  282. diag_opts.lnum = line_nr or (api.nvim_win_get_cursor(0)[1] - 1)
  283. return M.from(vim.diagnostic.get(bufnr, diag_opts))
  284. end
  285. --- Clear diagnostics from pull based clients
  286. --- @private
  287. local function clear(bufnr)
  288. for _, namespace in pairs(_client_pull_namespaces) do
  289. vim.diagnostic.reset(namespace, bufnr)
  290. end
  291. end
  292. ---@class (private) lsp.diagnostic.bufstate
  293. ---@field enabled boolean Whether inlay hints are enabled for this buffer
  294. ---@type table<integer, lsp.diagnostic.bufstate>
  295. local bufstates = {}
  296. --- Disable pull diagnostics for a buffer
  297. --- @param bufnr integer
  298. --- @private
  299. local function disable(bufnr)
  300. local bufstate = bufstates[bufnr]
  301. if bufstate then
  302. bufstate.enabled = false
  303. end
  304. clear(bufnr)
  305. end
  306. --- Refresh diagnostics, only if we have attached clients that support it
  307. ---@param bufnr (integer) buffer number
  308. ---@param opts? table Additional options to pass to util._refresh
  309. ---@private
  310. local function _refresh(bufnr, opts)
  311. opts = opts or {}
  312. opts['bufnr'] = bufnr
  313. vim.lsp.util._refresh(ms.textDocument_diagnostic, opts)
  314. end
  315. --- Enable pull diagnostics for a buffer
  316. ---@param bufnr (integer) Buffer handle, or 0 for current
  317. ---@private
  318. function M._enable(bufnr)
  319. bufnr = vim._resolve_bufnr(bufnr)
  320. if not bufstates[bufnr] then
  321. bufstates[bufnr] = { enabled = true }
  322. api.nvim_create_autocmd('LspNotify', {
  323. buffer = bufnr,
  324. callback = function(opts)
  325. if
  326. opts.data.method ~= ms.textDocument_didChange
  327. and opts.data.method ~= ms.textDocument_didOpen
  328. then
  329. return
  330. end
  331. if bufstates[bufnr] and bufstates[bufnr].enabled then
  332. local client_id = opts.data.client_id --- @type integer?
  333. _refresh(bufnr, { only_visible = true, client_id = client_id })
  334. end
  335. end,
  336. group = augroup,
  337. })
  338. api.nvim_buf_attach(bufnr, false, {
  339. on_reload = function()
  340. if bufstates[bufnr] and bufstates[bufnr].enabled then
  341. _refresh(bufnr)
  342. end
  343. end,
  344. on_detach = function()
  345. disable(bufnr)
  346. end,
  347. })
  348. api.nvim_create_autocmd('LspDetach', {
  349. buffer = bufnr,
  350. callback = function(args)
  351. local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_diagnostic })
  352. if
  353. not vim.iter(clients):any(function(c)
  354. return c.id ~= args.data.client_id
  355. end)
  356. then
  357. disable(bufnr)
  358. end
  359. end,
  360. group = augroup,
  361. })
  362. else
  363. bufstates[bufnr].enabled = true
  364. end
  365. end
  366. return M