diagnostic.lua 12 KB

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