diagnostic.lua 73 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303
  1. local api, if_nil = vim.api, vim.F.if_nil
  2. local M = {}
  3. local _qf_id = nil
  4. --- [diagnostic-structure]()
  5. ---
  6. --- Diagnostics use the same indexing as the rest of the Nvim API (i.e. 0-based
  7. --- rows and columns). |api-indexing|
  8. --- @class vim.Diagnostic
  9. ---
  10. --- Buffer number
  11. --- @field bufnr? integer
  12. ---
  13. --- The starting line of the diagnostic (0-indexed)
  14. --- @field lnum integer
  15. ---
  16. --- The final line of the diagnostic (0-indexed)
  17. --- @field end_lnum? integer
  18. ---
  19. --- The starting column of the diagnostic (0-indexed)
  20. --- @field col integer
  21. ---
  22. --- The final column of the diagnostic (0-indexed)
  23. --- @field end_col? integer
  24. ---
  25. --- The severity of the diagnostic |vim.diagnostic.severity|
  26. --- @field severity? vim.diagnostic.Severity
  27. ---
  28. --- The diagnostic text
  29. --- @field message string
  30. ---
  31. --- The source of the diagnostic
  32. --- @field source? string
  33. ---
  34. --- The diagnostic code
  35. --- @field code? string|integer
  36. ---
  37. --- @field _tags? { deprecated: boolean, unnecessary: boolean}
  38. ---
  39. --- Arbitrary data plugins or users can add
  40. --- @field user_data? any arbitrary data plugins can add
  41. ---
  42. --- @field namespace? integer
  43. --- Many of the configuration options below accept one of the following:
  44. --- - `false`: Disable this feature
  45. --- - `true`: Enable this feature, use default settings.
  46. --- - `table`: Enable this feature with overrides. Use an empty table to use default values.
  47. --- - `function`: Function with signature (namespace, bufnr) that returns any of the above.
  48. --- @class vim.diagnostic.Opts
  49. ---
  50. --- Use underline for diagnostics.
  51. --- (default: `true`)
  52. --- @field underline? boolean|vim.diagnostic.Opts.Underline|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.Underline
  53. ---
  54. --- Use virtual text for diagnostics. If multiple diagnostics are set for a
  55. --- namespace, one prefix per diagnostic + the last diagnostic message are
  56. --- shown.
  57. --- (default: `true`)
  58. --- @field virtual_text? boolean|vim.diagnostic.Opts.VirtualText|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.VirtualText
  59. ---
  60. --- Use signs for diagnostics |diagnostic-signs|.
  61. --- (default: `true`)
  62. --- @field signs? boolean|vim.diagnostic.Opts.Signs|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.Signs
  63. ---
  64. --- Options for floating windows. See |vim.diagnostic.Opts.Float|.
  65. --- @field float? boolean|vim.diagnostic.Opts.Float|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.Float
  66. ---
  67. --- Update diagnostics in Insert mode
  68. --- (if `false`, diagnostics are updated on |InsertLeave|)
  69. --- (default: `false`)
  70. --- @field update_in_insert? boolean
  71. ---
  72. --- Sort diagnostics by severity. This affects the order in which signs,
  73. --- virtual text, and highlights are displayed. When true, higher severities are
  74. --- displayed before lower severities (e.g. ERROR is displayed before WARN).
  75. --- Options:
  76. --- - {reverse}? (boolean) Reverse sort order
  77. --- (default: `false`)
  78. --- @field severity_sort? boolean|{reverse?:boolean}
  79. ---
  80. --- Default values for |vim.diagnostic.jump()|. See |vim.diagnostic.Opts.Jump|.
  81. --- @field jump? vim.diagnostic.Opts.Jump
  82. --- @class (private) vim.diagnostic.OptsResolved
  83. --- @field float vim.diagnostic.Opts.Float
  84. --- @field update_in_insert boolean
  85. --- @field underline vim.diagnostic.Opts.Underline
  86. --- @field virtual_text vim.diagnostic.Opts.VirtualText
  87. --- @field signs vim.diagnostic.Opts.Signs
  88. --- @field severity_sort {reverse?:boolean}
  89. --- @class vim.diagnostic.Opts.Float
  90. ---
  91. --- Buffer number to show diagnostics from.
  92. --- (default: current buffer)
  93. --- @field bufnr? integer
  94. ---
  95. --- Limit diagnostics to the given namespace
  96. --- @field namespace? integer
  97. ---
  98. --- Show diagnostics from the whole buffer (`buffer"`, the current cursor line
  99. --- (`line`), or the current cursor position (`cursor`). Shorthand versions
  100. --- are also accepted (`c` for `cursor`, `l` for `line`, `b` for `buffer`).
  101. --- (default: `line`)
  102. --- @field scope? 'line'|'buffer'|'cursor'|'c'|'l'|'b'
  103. ---
  104. --- If {scope} is "line" or "cursor", use this position rather than the cursor
  105. --- position. If a number, interpreted as a line number; otherwise, a
  106. --- (row, col) tuple.
  107. --- @field pos? integer|[integer,integer]
  108. ---
  109. --- Sort diagnostics by severity.
  110. --- Overrides the setting from |vim.diagnostic.config()|.
  111. --- (default: `false`)
  112. --- @field severity_sort? boolean|{reverse?:boolean}
  113. ---
  114. --- See |diagnostic-severity|.
  115. --- Overrides the setting from |vim.diagnostic.config()|.
  116. --- @field severity? vim.diagnostic.SeverityFilter
  117. ---
  118. --- String to use as the header for the floating window. If a table, it is
  119. --- interpreted as a `[text, hl_group]` tuple.
  120. --- Overrides the setting from |vim.diagnostic.config()|.
  121. --- @field header? string|[string,any]
  122. ---
  123. --- Include the diagnostic source in the message.
  124. --- Use "if_many" to only show sources if there is more than one source of
  125. --- diagnostics in the buffer. Otherwise, any truthy value means to always show
  126. --- the diagnostic source.
  127. --- Overrides the setting from |vim.diagnostic.config()|.
  128. --- @field source? boolean|'if_many'
  129. ---
  130. --- A function that takes a diagnostic as input and returns a string.
  131. --- The return value is the text used to display the diagnostic.
  132. --- Overrides the setting from |vim.diagnostic.config()|.
  133. --- @field format? fun(diagnostic:vim.Diagnostic): string
  134. ---
  135. --- Prefix each diagnostic in the floating window:
  136. --- - If a `function`, {i} is the index of the diagnostic being evaluated and
  137. --- {total} is the total number of diagnostics displayed in the window. The
  138. --- function should return a `string` which is prepended to each diagnostic
  139. --- in the window as well as an (optional) highlight group which will be
  140. --- used to highlight the prefix.
  141. --- - If a `table`, it is interpreted as a `[text, hl_group]` tuple as
  142. --- in |nvim_echo()|
  143. --- - If a `string`, it is prepended to each diagnostic in the window with no
  144. --- highlight.
  145. --- Overrides the setting from |vim.diagnostic.config()|.
  146. --- @field prefix? string|table|(fun(diagnostic:vim.Diagnostic,i:integer,total:integer): string, string)
  147. ---
  148. --- Same as {prefix}, but appends the text to the diagnostic instead of
  149. --- prepending it.
  150. --- Overrides the setting from |vim.diagnostic.config()|.
  151. --- @field suffix? string|table|(fun(diagnostic:vim.Diagnostic,i:integer,total:integer): string, string)
  152. ---
  153. --- @field focus_id? string
  154. ---
  155. --- @field border? string see |nvim_open_win()|.
  156. --- @class vim.diagnostic.Opts.Underline
  157. ---
  158. --- Only underline diagnostics matching the given
  159. --- severity |diagnostic-severity|.
  160. --- @field severity? vim.diagnostic.SeverityFilter
  161. --- @class vim.diagnostic.Opts.VirtualText
  162. ---
  163. --- Only show virtual text for diagnostics matching the given
  164. --- severity |diagnostic-severity|
  165. --- @field severity? vim.diagnostic.SeverityFilter
  166. ---
  167. --- Include the diagnostic source in virtual text. Use `'if_many'` to only
  168. --- show sources if there is more than one diagnostic source in the buffer.
  169. --- Otherwise, any truthy value means to always show the diagnostic source.
  170. --- @field source? boolean|"if_many"
  171. ---
  172. --- Amount of empty spaces inserted at the beginning of the virtual text.
  173. --- @field spacing? integer
  174. ---
  175. --- Prepend diagnostic message with prefix. If a `function`, {i} is the index
  176. --- of the diagnostic being evaluated, and {total} is the total number of
  177. --- diagnostics for the line. This can be used to render diagnostic symbols
  178. --- or error codes.
  179. --- @field prefix? string|(fun(diagnostic:vim.Diagnostic,i:integer,total:integer): string)
  180. ---
  181. --- Append diagnostic message with suffix.
  182. --- This can be used to render an LSP diagnostic error code.
  183. --- @field suffix? string|(fun(diagnostic:vim.Diagnostic): string)
  184. ---
  185. --- The return value is the text used to display the diagnostic. Example:
  186. --- ```lua
  187. --- function(diagnostic)
  188. --- if diagnostic.severity == vim.diagnostic.severity.ERROR then
  189. --- return string.format("E: %s", diagnostic.message)
  190. --- end
  191. --- return diagnostic.message
  192. --- end
  193. --- ```
  194. --- @field format? fun(diagnostic:vim.Diagnostic): string
  195. ---
  196. --- See |nvim_buf_set_extmark()|.
  197. --- @field hl_mode? 'replace'|'combine'|'blend'
  198. ---
  199. --- See |nvim_buf_set_extmark()|.
  200. --- @field virt_text? [string,any][]
  201. ---
  202. --- See |nvim_buf_set_extmark()|.
  203. --- @field virt_text_pos? 'eol'|'overlay'|'right_align'|'inline'
  204. ---
  205. --- See |nvim_buf_set_extmark()|.
  206. --- @field virt_text_win_col? integer
  207. ---
  208. --- See |nvim_buf_set_extmark()|.
  209. --- @field virt_text_hide? boolean
  210. --- @class vim.diagnostic.Opts.Signs
  211. ---
  212. --- Only show virtual text for diagnostics matching the given
  213. --- severity |diagnostic-severity|
  214. --- @field severity? vim.diagnostic.SeverityFilter
  215. ---
  216. --- Base priority to use for signs. When {severity_sort} is used, the priority
  217. --- of a sign is adjusted based on its severity.
  218. --- Otherwise, all signs use the same priority.
  219. --- (default: `10`)
  220. --- @field priority? integer
  221. ---
  222. --- A table mapping |diagnostic-severity| to the sign text to display in the
  223. --- sign column. The default is to use `"E"`, `"W"`, `"I"`, and `"H"` for errors,
  224. --- warnings, information, and hints, respectively. Example:
  225. --- ```lua
  226. --- vim.diagnostic.config({
  227. --- signs = { text = { [vim.diagnostic.severity.ERROR] = 'E', ... } }
  228. --- })
  229. --- ```
  230. --- @field text? table<vim.diagnostic.Severity,string>
  231. ---
  232. --- A table mapping |diagnostic-severity| to the highlight group used for the
  233. --- line number where the sign is placed.
  234. --- @field numhl? table<vim.diagnostic.Severity,string>
  235. ---
  236. --- A table mapping |diagnostic-severity| to the highlight group used for the
  237. --- whole line the sign is placed in.
  238. --- @field linehl? table<vim.diagnostic.Severity,string>
  239. --- @class vim.diagnostic.Opts.Jump
  240. ---
  241. --- Default value of the {float} parameter of |vim.diagnostic.jump()|.
  242. --- (default: false)
  243. --- @field float? boolean|vim.diagnostic.Opts.Float
  244. ---
  245. --- Default value of the {wrap} parameter of |vim.diagnostic.jump()|.
  246. --- (default: true)
  247. --- @field wrap? boolean
  248. ---
  249. --- Default value of the {severity} parameter of |vim.diagnostic.jump()|.
  250. --- @field severity? vim.diagnostic.SeverityFilter
  251. ---
  252. --- Default value of the {_highest} parameter of |vim.diagnostic.jump()|.
  253. --- @field package _highest? boolean
  254. -- TODO: inherit from `vim.diagnostic.Opts`, implement its fields.
  255. --- Optional filters |kwargs|, or `nil` for all.
  256. --- @class vim.diagnostic.Filter
  257. --- @inlinedoc
  258. ---
  259. --- Diagnostic namespace, or `nil` for all.
  260. --- @field ns_id? integer
  261. ---
  262. --- Buffer number, or 0 for current buffer, or `nil` for all buffers.
  263. --- @field bufnr? integer
  264. --- @nodoc
  265. --- @enum vim.diagnostic.Severity
  266. M.severity = {
  267. ERROR = 1,
  268. WARN = 2,
  269. INFO = 3,
  270. HINT = 4,
  271. [1] = 'ERROR',
  272. [2] = 'WARN',
  273. [3] = 'INFO',
  274. [4] = 'HINT',
  275. --- Mappings from qflist/loclist error types to severities
  276. E = 1,
  277. W = 2,
  278. I = 3,
  279. N = 4,
  280. }
  281. --- @alias vim.diagnostic.SeverityInt 1|2|3|4
  282. --- See |diagnostic-severity| and |vim.diagnostic.get()|
  283. --- @alias vim.diagnostic.SeverityFilter vim.diagnostic.Severity|vim.diagnostic.Severity[]|{min:vim.diagnostic.Severity,max:vim.diagnostic.Severity}
  284. --- @type vim.diagnostic.Opts
  285. local global_diagnostic_options = {
  286. signs = true,
  287. underline = true,
  288. virtual_text = true,
  289. float = true,
  290. update_in_insert = false,
  291. severity_sort = false,
  292. jump = {
  293. -- Do not show floating window
  294. float = false,
  295. -- Wrap around buffer
  296. wrap = true,
  297. },
  298. }
  299. --- @class (private) vim.diagnostic.Handler
  300. --- @field show? fun(namespace: integer, bufnr: integer, diagnostics: vim.Diagnostic[], opts?: vim.diagnostic.OptsResolved)
  301. --- @field hide? fun(namespace:integer, bufnr:integer)
  302. --- @nodoc
  303. --- @type table<string,vim.diagnostic.Handler>
  304. M.handlers = setmetatable({}, {
  305. __newindex = function(t, name, handler)
  306. vim.validate('handler', handler, 'table')
  307. rawset(t, name, handler)
  308. if global_diagnostic_options[name] == nil then
  309. global_diagnostic_options[name] = true
  310. end
  311. end,
  312. })
  313. -- Metatable that automatically creates an empty table when assigning to a missing key
  314. local bufnr_and_namespace_cacher_mt = {
  315. --- @param t table<integer,table>
  316. --- @param bufnr integer
  317. --- @return table
  318. __index = function(t, bufnr)
  319. assert(bufnr > 0, 'Invalid buffer number')
  320. t[bufnr] = {}
  321. return t[bufnr]
  322. end,
  323. }
  324. -- bufnr -> ns -> Diagnostic[]
  325. local diagnostic_cache = {} --- @type table<integer,table<integer,vim.Diagnostic[]>>
  326. do
  327. local group = api.nvim_create_augroup('DiagnosticBufWipeout', {})
  328. setmetatable(diagnostic_cache, {
  329. --- @param t table<integer,vim.Diagnostic[]>
  330. --- @param bufnr integer
  331. __index = function(t, bufnr)
  332. assert(bufnr > 0, 'Invalid buffer number')
  333. api.nvim_create_autocmd('BufWipeout', {
  334. group = group,
  335. buffer = bufnr,
  336. callback = function()
  337. rawset(t, bufnr, nil)
  338. end,
  339. })
  340. t[bufnr] = {}
  341. return t[bufnr]
  342. end,
  343. })
  344. end
  345. --- @class (private) vim.diagnostic._extmark
  346. --- @field [1] integer id
  347. --- @field [2] integer start
  348. --- @field [3] integer end
  349. --- @field [4] table details
  350. --- @type table<integer,table<integer,vim.diagnostic._extmark[]>>
  351. local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt)
  352. --- @type table<integer,true>
  353. local diagnostic_attached_buffers = {}
  354. --- @type table<integer,true|table<integer,true>>
  355. local diagnostic_disabled = {}
  356. --- @type table<integer,table<integer,table>>
  357. local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt)
  358. --- @class vim.diagnostic.NS
  359. --- @field name string
  360. --- @field opts vim.diagnostic.Opts
  361. --- @field user_data table
  362. --- @field disabled? boolean
  363. --- @type table<integer,vim.diagnostic.NS>
  364. local all_namespaces = {}
  365. ---@param severity string|vim.diagnostic.Severity
  366. ---@return vim.diagnostic.Severity?
  367. local function to_severity(severity)
  368. if type(severity) == 'string' then
  369. assert(M.severity[string.upper(severity)], string.format('Invalid severity: %s', severity))
  370. return M.severity[string.upper(severity)]
  371. end
  372. return severity
  373. end
  374. --- @param severity vim.diagnostic.SeverityFilter
  375. --- @return fun(vim.Diagnostic):boolean
  376. local function severity_predicate(severity)
  377. if type(severity) ~= 'table' then
  378. severity = assert(to_severity(severity))
  379. ---@param d vim.Diagnostic
  380. return function(d)
  381. return d.severity == severity
  382. end
  383. end
  384. if severity.min or severity.max then
  385. --- @cast severity {min:vim.diagnostic.Severity,max:vim.diagnostic.Severity}
  386. local min_severity = to_severity(severity.min) or M.severity.HINT
  387. local max_severity = to_severity(severity.max) or M.severity.ERROR
  388. --- @param d vim.Diagnostic
  389. return function(d)
  390. return d.severity <= min_severity and d.severity >= max_severity
  391. end
  392. end
  393. --- @cast severity vim.diagnostic.Severity[]
  394. local severities = {} --- @type table<vim.diagnostic.Severity,true>
  395. for _, s in ipairs(severity) do
  396. severities[assert(to_severity(s))] = true
  397. end
  398. --- @param d vim.Diagnostic
  399. return function(d)
  400. return severities[d.severity]
  401. end
  402. end
  403. --- @param severity vim.diagnostic.SeverityFilter
  404. --- @param diagnostics vim.Diagnostic[]
  405. --- @return vim.Diagnostic[]
  406. local function filter_by_severity(severity, diagnostics)
  407. if not severity then
  408. return diagnostics
  409. end
  410. return vim.tbl_filter(severity_predicate(severity), diagnostics)
  411. end
  412. --- @param bufnr integer
  413. --- @return integer
  414. local function count_sources(bufnr)
  415. local seen = {} --- @type table<string,true>
  416. local count = 0
  417. for _, namespace_diagnostics in pairs(diagnostic_cache[bufnr]) do
  418. for _, diagnostic in ipairs(namespace_diagnostics) do
  419. local source = diagnostic.source
  420. if source and not seen[source] then
  421. seen[source] = true
  422. count = count + 1
  423. end
  424. end
  425. end
  426. return count
  427. end
  428. --- @param diagnostics vim.Diagnostic[]
  429. --- @return vim.Diagnostic[]
  430. local function prefix_source(diagnostics)
  431. --- @param d vim.Diagnostic
  432. return vim.tbl_map(function(d)
  433. if not d.source then
  434. return d
  435. end
  436. local t = vim.deepcopy(d, true)
  437. t.message = string.format('%s: %s', d.source, d.message)
  438. return t
  439. end, diagnostics)
  440. end
  441. --- @param diagnostics vim.Diagnostic[]
  442. --- @return vim.Diagnostic[]
  443. local function reformat_diagnostics(format, diagnostics)
  444. vim.validate('format', format, 'function')
  445. vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
  446. local formatted = vim.deepcopy(diagnostics, true)
  447. for _, diagnostic in ipairs(formatted) do
  448. diagnostic.message = format(diagnostic)
  449. end
  450. return formatted
  451. end
  452. --- @param option string
  453. --- @param namespace integer?
  454. --- @return table
  455. local function enabled_value(option, namespace)
  456. local ns = namespace and M.get_namespace(namespace) or {}
  457. if ns.opts and type(ns.opts[option]) == 'table' then
  458. return ns.opts[option]
  459. end
  460. local global_opt = global_diagnostic_options[option]
  461. if type(global_opt) == 'table' then
  462. return global_opt
  463. end
  464. return {}
  465. end
  466. --- @param option string
  467. --- @param value any?
  468. --- @param namespace integer?
  469. --- @param bufnr integer
  470. --- @return any
  471. local function resolve_optional_value(option, value, namespace, bufnr)
  472. if not value then
  473. return false
  474. elseif value == true then
  475. return enabled_value(option, namespace)
  476. elseif type(value) == 'function' then
  477. local val = value(namespace, bufnr) --- @type any
  478. if val == true then
  479. return enabled_value(option, namespace)
  480. else
  481. return val
  482. end
  483. elseif type(value) == 'table' then
  484. return value
  485. end
  486. error('Unexpected option type: ' .. vim.inspect(value))
  487. end
  488. --- @param opts vim.diagnostic.Opts?
  489. --- @param namespace integer?
  490. --- @param bufnr integer
  491. --- @return vim.diagnostic.OptsResolved
  492. local function get_resolved_options(opts, namespace, bufnr)
  493. local ns = namespace and M.get_namespace(namespace) or {}
  494. -- Do not use tbl_deep_extend so that an empty table can be used to reset to default values
  495. local resolved = vim.tbl_extend('keep', opts or {}, ns.opts or {}, global_diagnostic_options) --- @type table<string,any>
  496. for k in pairs(global_diagnostic_options) do
  497. if resolved[k] ~= nil then
  498. resolved[k] = resolve_optional_value(k, resolved[k], namespace, bufnr)
  499. end
  500. end
  501. return resolved
  502. end
  503. -- Default diagnostic highlights
  504. local diagnostic_severities = {
  505. [M.severity.ERROR] = { ctermfg = 1, guifg = 'Red' },
  506. [M.severity.WARN] = { ctermfg = 3, guifg = 'Orange' },
  507. [M.severity.INFO] = { ctermfg = 4, guifg = 'LightBlue' },
  508. [M.severity.HINT] = { ctermfg = 7, guifg = 'LightGrey' },
  509. }
  510. --- Make a map from vim.diagnostic.Severity -> Highlight Name
  511. --- @param base_name string
  512. --- @return table<vim.diagnostic.SeverityInt,string>
  513. local function make_highlight_map(base_name)
  514. local result = {} --- @type table<vim.diagnostic.SeverityInt,string>
  515. for k in pairs(diagnostic_severities) do
  516. local name = M.severity[k]
  517. name = name:sub(1, 1) .. name:sub(2):lower()
  518. result[k] = 'Diagnostic' .. base_name .. name
  519. end
  520. return result
  521. end
  522. -- TODO(lewis6991): these highlight maps can only be indexed with an integer, however there usage
  523. -- implies they can be indexed with any vim.diagnostic.Severity
  524. local virtual_text_highlight_map = make_highlight_map('VirtualText')
  525. local underline_highlight_map = make_highlight_map('Underline')
  526. local floating_highlight_map = make_highlight_map('Floating')
  527. local sign_highlight_map = make_highlight_map('Sign')
  528. --- @param diagnostics vim.Diagnostic[]
  529. --- @return table<integer,vim.Diagnostic[]>
  530. local function diagnostic_lines(diagnostics)
  531. if not diagnostics then
  532. return {}
  533. end
  534. local diagnostics_by_line = {} --- @type table<integer,vim.Diagnostic[]>
  535. for _, diagnostic in ipairs(diagnostics) do
  536. local line_diagnostics = diagnostics_by_line[diagnostic.lnum]
  537. if not line_diagnostics then
  538. line_diagnostics = {}
  539. diagnostics_by_line[diagnostic.lnum] = line_diagnostics
  540. end
  541. table.insert(line_diagnostics, diagnostic)
  542. end
  543. return diagnostics_by_line
  544. end
  545. --- @param namespace integer
  546. --- @param bufnr integer
  547. --- @param diagnostics vim.Diagnostic[]
  548. local function set_diagnostic_cache(namespace, bufnr, diagnostics)
  549. for _, diagnostic in ipairs(diagnostics) do
  550. assert(diagnostic.lnum, 'Diagnostic line number is required')
  551. assert(diagnostic.col, 'Diagnostic column is required')
  552. diagnostic.severity = diagnostic.severity and to_severity(diagnostic.severity)
  553. or M.severity.ERROR
  554. diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum
  555. diagnostic.end_col = diagnostic.end_col or diagnostic.col
  556. diagnostic.namespace = namespace
  557. diagnostic.bufnr = bufnr
  558. end
  559. diagnostic_cache[bufnr][namespace] = diagnostics
  560. end
  561. --- @param bufnr integer
  562. --- @param last integer
  563. local function restore_extmarks(bufnr, last)
  564. for ns, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do
  565. local extmarks_current = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
  566. local found = {} --- @type table<integer,true>
  567. for _, extmark in ipairs(extmarks_current) do
  568. -- nvim_buf_set_lines will move any extmark to the line after the last
  569. -- nvim_buf_set_text will move any extmark to the last line
  570. if extmark[2] ~= last + 1 then
  571. found[extmark[1]] = true
  572. end
  573. end
  574. for _, extmark in ipairs(extmarks) do
  575. if not found[extmark[1]] then
  576. local opts = extmark[4]
  577. opts.id = extmark[1]
  578. pcall(api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
  579. end
  580. end
  581. end
  582. end
  583. --- @param namespace integer
  584. --- @param bufnr? integer
  585. local function save_extmarks(namespace, bufnr)
  586. bufnr = vim._resolve_bufnr(bufnr)
  587. if not diagnostic_attached_buffers[bufnr] then
  588. api.nvim_buf_attach(bufnr, false, {
  589. on_lines = function(_, _, _, _, _, last)
  590. restore_extmarks(bufnr, last - 1)
  591. end,
  592. on_detach = function()
  593. diagnostic_cache_extmarks[bufnr] = nil
  594. end,
  595. })
  596. diagnostic_attached_buffers[bufnr] = true
  597. end
  598. diagnostic_cache_extmarks[bufnr][namespace] =
  599. api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, { details = true })
  600. end
  601. --- Create a function that converts a diagnostic severity to an extmark priority.
  602. --- @param priority integer Base priority
  603. --- @param opts vim.diagnostic.OptsResolved
  604. --- @return fun(severity: vim.diagnostic.Severity): integer
  605. local function severity_to_extmark_priority(priority, opts)
  606. if opts.severity_sort then
  607. if type(opts.severity_sort) == 'table' and opts.severity_sort.reverse then
  608. return function(severity)
  609. return priority + (severity - vim.diagnostic.severity.ERROR)
  610. end
  611. end
  612. return function(severity)
  613. return priority + (vim.diagnostic.severity.HINT - severity)
  614. end
  615. end
  616. return function()
  617. return priority
  618. end
  619. end
  620. --- @type table<string,true>
  621. local registered_autocmds = {}
  622. local function make_augroup_key(namespace, bufnr)
  623. local ns = M.get_namespace(namespace)
  624. return string.format('DiagnosticInsertLeave:%s:%s', bufnr, ns.name)
  625. end
  626. --- @param namespace integer
  627. --- @param bufnr integer
  628. local function execute_scheduled_display(namespace, bufnr)
  629. local args = bufs_waiting_to_update[bufnr][namespace]
  630. if not args then
  631. return
  632. end
  633. -- Clear the args so we don't display unnecessarily.
  634. bufs_waiting_to_update[bufnr][namespace] = nil
  635. M.show(namespace, bufnr, nil, args)
  636. end
  637. --- Table of autocmd events to fire the update for displaying new diagnostic information
  638. local insert_leave_auto_cmds = { 'InsertLeave', 'CursorHoldI' }
  639. --- @param namespace integer
  640. --- @param bufnr integer
  641. --- @param args any[]
  642. local function schedule_display(namespace, bufnr, args)
  643. bufs_waiting_to_update[bufnr][namespace] = args
  644. local key = make_augroup_key(namespace, bufnr)
  645. if not registered_autocmds[key] then
  646. local group = api.nvim_create_augroup(key, { clear = true })
  647. api.nvim_create_autocmd(insert_leave_auto_cmds, {
  648. group = group,
  649. buffer = bufnr,
  650. callback = function()
  651. execute_scheduled_display(namespace, bufnr)
  652. end,
  653. desc = 'vim.diagnostic: display diagnostics',
  654. })
  655. registered_autocmds[key] = true
  656. end
  657. end
  658. --- @param namespace integer
  659. --- @param bufnr integer
  660. local function clear_scheduled_display(namespace, bufnr)
  661. local key = make_augroup_key(namespace, bufnr)
  662. if registered_autocmds[key] then
  663. api.nvim_del_augroup_by_name(key)
  664. registered_autocmds[key] = nil
  665. end
  666. end
  667. --- @param bufnr integer?
  668. --- @param opts vim.diagnostic.GetOpts?
  669. --- @param clamp boolean
  670. --- @return vim.Diagnostic[]
  671. local function get_diagnostics(bufnr, opts, clamp)
  672. opts = opts or {}
  673. local namespace = opts.namespace
  674. if type(namespace) == 'number' then
  675. namespace = { namespace }
  676. end
  677. ---@cast namespace integer[]
  678. local diagnostics = {}
  679. -- Memoized results of buf_line_count per bufnr
  680. --- @type table<integer,integer>
  681. local buf_line_count = setmetatable({}, {
  682. --- @param t table<integer,integer>
  683. --- @param k integer
  684. --- @return integer
  685. __index = function(t, k)
  686. t[k] = api.nvim_buf_line_count(k)
  687. return rawget(t, k)
  688. end,
  689. })
  690. local match_severity = opts.severity and severity_predicate(opts.severity)
  691. or function(_)
  692. return true
  693. end
  694. ---@param b integer
  695. ---@param d vim.Diagnostic
  696. local function add(b, d)
  697. if
  698. match_severity(d)
  699. and (not opts.lnum or (opts.lnum >= d.lnum and opts.lnum <= (d.end_lnum or d.lnum)))
  700. then
  701. if clamp and api.nvim_buf_is_loaded(b) then
  702. local line_count = buf_line_count[b] - 1
  703. if
  704. d.lnum > line_count
  705. or d.end_lnum > line_count
  706. or d.lnum < 0
  707. or d.end_lnum < 0
  708. or d.col < 0
  709. or d.end_col < 0
  710. then
  711. d = vim.deepcopy(d, true)
  712. d.lnum = math.max(math.min(d.lnum, line_count), 0)
  713. d.end_lnum = math.max(math.min(assert(d.end_lnum), line_count), 0)
  714. d.col = math.max(d.col, 0)
  715. d.end_col = math.max(d.end_col, 0)
  716. end
  717. end
  718. table.insert(diagnostics, d)
  719. end
  720. end
  721. --- @param buf integer
  722. --- @param diags vim.Diagnostic[]
  723. local function add_all_diags(buf, diags)
  724. for _, diagnostic in pairs(diags) do
  725. add(buf, diagnostic)
  726. end
  727. end
  728. if namespace == nil and bufnr == nil then
  729. for b, t in pairs(diagnostic_cache) do
  730. for _, v in pairs(t) do
  731. add_all_diags(b, v)
  732. end
  733. end
  734. elseif namespace == nil then
  735. bufnr = vim._resolve_bufnr(bufnr)
  736. for iter_namespace in pairs(diagnostic_cache[bufnr]) do
  737. add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace])
  738. end
  739. elseif bufnr == nil then
  740. for b, t in pairs(diagnostic_cache) do
  741. for _, iter_namespace in ipairs(namespace) do
  742. add_all_diags(b, t[iter_namespace] or {})
  743. end
  744. end
  745. else
  746. bufnr = vim._resolve_bufnr(bufnr)
  747. for _, iter_namespace in ipairs(namespace) do
  748. add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace] or {})
  749. end
  750. end
  751. return diagnostics
  752. end
  753. --- @param loclist boolean
  754. --- @param opts vim.diagnostic.setqflist.Opts|vim.diagnostic.setloclist.Opts?
  755. local function set_list(loclist, opts)
  756. opts = opts or {}
  757. local open = if_nil(opts.open, true)
  758. local title = opts.title or 'Diagnostics'
  759. local winnr = opts.winnr or 0
  760. local bufnr --- @type integer?
  761. if loclist then
  762. bufnr = api.nvim_win_get_buf(winnr)
  763. end
  764. -- Don't clamp line numbers since the quickfix list can already handle line
  765. -- numbers beyond the end of the buffer
  766. local diagnostics = get_diagnostics(bufnr, opts --[[@as vim.diagnostic.GetOpts]], false)
  767. local items = M.toqflist(diagnostics)
  768. if loclist then
  769. vim.fn.setloclist(winnr, {}, 'u', { title = title, items = items })
  770. else
  771. -- Check if the diagnostics quickfix list no longer exists.
  772. if _qf_id and vim.fn.getqflist({ id = _qf_id }).id == 0 then
  773. _qf_id = nil
  774. end
  775. -- If we already have a diagnostics quickfix, update it rather than creating a new one.
  776. -- This avoids polluting the finite set of quickfix lists, and preserves the currently selected
  777. -- entry.
  778. vim.fn.setqflist({}, _qf_id and 'u' or ' ', {
  779. title = title,
  780. items = items,
  781. id = _qf_id,
  782. })
  783. -- Get the id of the newly created quickfix list.
  784. if _qf_id == nil then
  785. _qf_id = vim.fn.getqflist({ id = 0 }).id
  786. end
  787. end
  788. if open then
  789. api.nvim_command(loclist and 'lwindow' or 'botright cwindow')
  790. end
  791. end
  792. --- Jump to the diagnostic with the highest severity. First sort the
  793. --- diagnostics by severity. The first diagnostic then contains the highest severity, and we can
  794. --- discard all diagnostics with a lower severity.
  795. --- @param diagnostics vim.Diagnostic[]
  796. local function filter_highest(diagnostics)
  797. table.sort(diagnostics, function(a, b)
  798. return a.severity < b.severity
  799. end)
  800. -- Find the first diagnostic where the severity does not match the highest severity, and remove
  801. -- that element and all subsequent elements from the array
  802. local worst = (diagnostics[1] or {}).severity
  803. local len = #diagnostics
  804. for i = 2, len do
  805. if diagnostics[i].severity ~= worst then
  806. for j = i, len do
  807. diagnostics[j] = nil
  808. end
  809. break
  810. end
  811. end
  812. end
  813. --- @param search_forward boolean
  814. --- @param opts vim.diagnostic.JumpOpts?
  815. --- @return vim.Diagnostic?
  816. local function next_diagnostic(search_forward, opts)
  817. opts = opts or {}
  818. -- Support deprecated win_id alias
  819. if opts.win_id then
  820. vim.deprecate('opts.win_id', 'opts.winid', '0.13')
  821. opts.winid = opts.win_id
  822. opts.win_id = nil --- @diagnostic disable-line
  823. end
  824. -- Support deprecated cursor_position alias
  825. if opts.cursor_position then
  826. vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
  827. opts.pos = opts.cursor_position
  828. opts.cursor_position = nil --- @diagnostic disable-line
  829. end
  830. local winid = opts.winid or api.nvim_get_current_win()
  831. local bufnr = api.nvim_win_get_buf(winid)
  832. local position = opts.pos or api.nvim_win_get_cursor(winid)
  833. -- Adjust row to be 0-indexed
  834. position[1] = position[1] - 1
  835. local wrap = if_nil(opts.wrap, true)
  836. local diagnostics = get_diagnostics(bufnr, opts, true)
  837. if opts._highest then
  838. filter_highest(diagnostics)
  839. end
  840. local line_diagnostics = diagnostic_lines(diagnostics)
  841. local line_count = api.nvim_buf_line_count(bufnr)
  842. for i = 0, line_count do
  843. local offset = i * (search_forward and 1 or -1)
  844. local lnum = position[1] + offset
  845. if lnum < 0 or lnum >= line_count then
  846. if not wrap then
  847. return
  848. end
  849. lnum = (lnum + line_count) % line_count
  850. end
  851. if line_diagnostics[lnum] and not vim.tbl_isempty(line_diagnostics[lnum]) then
  852. local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
  853. --- @type function, function
  854. local sort_diagnostics, is_next
  855. if search_forward then
  856. sort_diagnostics = function(a, b)
  857. return a.col < b.col
  858. end
  859. is_next = function(d)
  860. return math.min(d.col, math.max(line_length - 1, 0)) > position[2]
  861. end
  862. else
  863. sort_diagnostics = function(a, b)
  864. return a.col > b.col
  865. end
  866. is_next = function(d)
  867. return math.min(d.col, math.max(line_length - 1, 0)) < position[2]
  868. end
  869. end
  870. table.sort(line_diagnostics[lnum], sort_diagnostics)
  871. if i == 0 then
  872. for _, v in
  873. pairs(line_diagnostics[lnum] --[[@as table<string,any>]])
  874. do
  875. if is_next(v) then
  876. return v
  877. end
  878. end
  879. else
  880. return line_diagnostics[lnum][1]
  881. end
  882. end
  883. end
  884. end
  885. --- Move the cursor to the given diagnostic.
  886. ---
  887. --- @param diagnostic vim.Diagnostic?
  888. --- @param opts vim.diagnostic.JumpOpts?
  889. local function goto_diagnostic(diagnostic, opts)
  890. if not diagnostic then
  891. api.nvim_echo({ { 'No more valid diagnostics to move to', 'WarningMsg' } }, true, {})
  892. return
  893. end
  894. opts = opts or {}
  895. -- Support deprecated win_id alias
  896. if opts.win_id then
  897. vim.deprecate('opts.win_id', 'opts.winid', '0.13')
  898. opts.winid = opts.win_id
  899. opts.win_id = nil --- @diagnostic disable-line
  900. end
  901. local winid = opts.winid or api.nvim_get_current_win()
  902. vim._with({ win = winid }, function()
  903. -- Save position in the window's jumplist
  904. vim.cmd("normal! m'")
  905. api.nvim_win_set_cursor(winid, { diagnostic.lnum + 1, diagnostic.col })
  906. -- Open folds under the cursor
  907. vim.cmd('normal! zv')
  908. end)
  909. local float_opts = opts.float
  910. if float_opts then
  911. float_opts = type(float_opts) == 'table' and float_opts or {}
  912. vim.schedule(function()
  913. M.open_float(vim.tbl_extend('keep', float_opts, {
  914. bufnr = api.nvim_win_get_buf(winid),
  915. scope = 'cursor',
  916. focus = false,
  917. }))
  918. end)
  919. end
  920. end
  921. --- Configure diagnostic options globally or for a specific diagnostic
  922. --- namespace.
  923. ---
  924. --- Configuration can be specified globally, per-namespace, or ephemerally
  925. --- (i.e. only for a single call to |vim.diagnostic.set()| or
  926. --- |vim.diagnostic.show()|). Ephemeral configuration has highest priority,
  927. --- followed by namespace configuration, and finally global configuration.
  928. ---
  929. --- For example, if a user enables virtual text globally with
  930. ---
  931. --- ```lua
  932. --- vim.diagnostic.config({ virtual_text = true })
  933. --- ```
  934. ---
  935. --- and a diagnostic producer sets diagnostics with
  936. ---
  937. --- ```lua
  938. --- vim.diagnostic.set(ns, 0, diagnostics, { virtual_text = false })
  939. --- ```
  940. ---
  941. --- then virtual text will not be enabled for those diagnostics.
  942. ---
  943. ---@param opts vim.diagnostic.Opts? When omitted or `nil`, retrieve the current
  944. --- configuration. Otherwise, a configuration table (see |vim.diagnostic.Opts|).
  945. ---@param namespace integer? Update the options for the given namespace.
  946. --- When omitted, update the global diagnostic options.
  947. ---@return vim.diagnostic.Opts? : Current diagnostic config if {opts} is omitted.
  948. function M.config(opts, namespace)
  949. vim.validate('opts', opts, 'table', true)
  950. vim.validate('namespace', namespace, 'number', true)
  951. local t --- @type vim.diagnostic.Opts
  952. if namespace then
  953. local ns = M.get_namespace(namespace)
  954. t = ns.opts
  955. else
  956. t = global_diagnostic_options
  957. end
  958. if not opts then
  959. -- Return current config
  960. return vim.deepcopy(t, true)
  961. end
  962. for k, v in
  963. pairs(opts --[[@as table<any,any>]])
  964. do
  965. t[k] = v
  966. end
  967. if namespace then
  968. for bufnr, v in pairs(diagnostic_cache) do
  969. if v[namespace] then
  970. M.show(namespace, bufnr)
  971. end
  972. end
  973. else
  974. for bufnr, v in pairs(diagnostic_cache) do
  975. for ns in pairs(v) do
  976. M.show(ns, bufnr)
  977. end
  978. end
  979. end
  980. end
  981. --- Set diagnostics for the given namespace and buffer.
  982. ---
  983. ---@param namespace integer The diagnostic namespace
  984. ---@param bufnr integer Buffer number
  985. ---@param diagnostics vim.Diagnostic[]
  986. ---@param opts? vim.diagnostic.Opts Display options to pass to |vim.diagnostic.show()|
  987. function M.set(namespace, bufnr, diagnostics, opts)
  988. vim.validate('namespace', namespace, 'number')
  989. vim.validate('bufnr', bufnr, 'number')
  990. vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
  991. vim.validate('opts', opts, 'table', true)
  992. bufnr = vim._resolve_bufnr(bufnr)
  993. if vim.tbl_isempty(diagnostics) then
  994. diagnostic_cache[bufnr][namespace] = nil
  995. else
  996. set_diagnostic_cache(namespace, bufnr, diagnostics)
  997. end
  998. M.show(namespace, bufnr, nil, opts)
  999. api.nvim_exec_autocmds('DiagnosticChanged', {
  1000. modeline = false,
  1001. buffer = bufnr,
  1002. -- TODO(lewis6991): should this be deepcopy()'d like they are in vim.diagnostic.get()
  1003. data = { diagnostics = diagnostics },
  1004. })
  1005. end
  1006. --- Get namespace metadata.
  1007. ---
  1008. ---@param namespace integer Diagnostic namespace
  1009. ---@return vim.diagnostic.NS : Namespace metadata
  1010. function M.get_namespace(namespace)
  1011. vim.validate('namespace', namespace, 'number')
  1012. if not all_namespaces[namespace] then
  1013. local name --- @type string?
  1014. for k, v in pairs(api.nvim_get_namespaces()) do
  1015. if namespace == v then
  1016. name = k
  1017. break
  1018. end
  1019. end
  1020. assert(name, 'namespace does not exist or is anonymous')
  1021. all_namespaces[namespace] = {
  1022. name = name,
  1023. opts = {},
  1024. user_data = {},
  1025. }
  1026. end
  1027. return all_namespaces[namespace]
  1028. end
  1029. --- Get current diagnostic namespaces.
  1030. ---
  1031. ---@return table<integer,vim.diagnostic.NS> : List of active diagnostic namespaces |vim.diagnostic|.
  1032. function M.get_namespaces()
  1033. return vim.deepcopy(all_namespaces, true)
  1034. end
  1035. --- Get current diagnostics.
  1036. ---
  1037. --- Modifying diagnostics in the returned table has no effect.
  1038. --- To set diagnostics in a buffer, use |vim.diagnostic.set()|.
  1039. ---
  1040. ---@param bufnr integer? Buffer number to get diagnostics from. Use 0 for
  1041. --- current buffer or nil for all buffers.
  1042. ---@param opts? vim.diagnostic.GetOpts
  1043. ---@return vim.Diagnostic[] : Fields `bufnr`, `end_lnum`, `end_col`, and `severity`
  1044. --- are guaranteed to be present.
  1045. function M.get(bufnr, opts)
  1046. vim.validate('bufnr', bufnr, 'number', true)
  1047. vim.validate('opts', opts, 'table', true)
  1048. return vim.deepcopy(get_diagnostics(bufnr, opts, false), true)
  1049. end
  1050. --- Get current diagnostics count.
  1051. ---
  1052. ---@param bufnr? integer Buffer number to get diagnostics from. Use 0 for
  1053. --- current buffer or nil for all buffers.
  1054. ---@param opts? vim.diagnostic.GetOpts
  1055. ---@return table : Table with actually present severity values as keys
  1056. --- (see |diagnostic-severity|) and integer counts as values.
  1057. function M.count(bufnr, opts)
  1058. vim.validate('bufnr', bufnr, 'number', true)
  1059. vim.validate('opts', opts, 'table', true)
  1060. local diagnostics = get_diagnostics(bufnr, opts, false)
  1061. local count = {} --- @type table<integer,integer>
  1062. for i = 1, #diagnostics do
  1063. local severity = diagnostics[i].severity --[[@as integer]]
  1064. count[severity] = (count[severity] or 0) + 1
  1065. end
  1066. return count
  1067. end
  1068. --- Get the previous diagnostic closest to the cursor position.
  1069. ---
  1070. ---@param opts? vim.diagnostic.JumpOpts
  1071. ---@return vim.Diagnostic? : Previous diagnostic
  1072. function M.get_prev(opts)
  1073. return next_diagnostic(false, opts)
  1074. end
  1075. --- Return the position of the previous diagnostic in the current buffer.
  1076. ---
  1077. ---@param opts? vim.diagnostic.JumpOpts
  1078. ---@return table|false: Previous diagnostic position as a `(row, col)` tuple
  1079. --- or `false` if there is no prior diagnostic.
  1080. ---@deprecated
  1081. function M.get_prev_pos(opts)
  1082. vim.deprecate(
  1083. 'vim.diagnostic.get_prev_pos()',
  1084. 'access the lnum and col fields from get_prev() instead',
  1085. '0.13'
  1086. )
  1087. local prev = M.get_prev(opts)
  1088. if not prev then
  1089. return false
  1090. end
  1091. return { prev.lnum, prev.col }
  1092. end
  1093. --- Move to the previous diagnostic in the current buffer.
  1094. ---@param opts? vim.diagnostic.JumpOpts
  1095. ---@deprecated
  1096. function M.goto_prev(opts)
  1097. vim.deprecate('vim.diagnostic.goto_prev()', 'vim.diagnostic.jump()', '0.13')
  1098. opts = opts or {}
  1099. opts.float = if_nil(opts.float, true)
  1100. goto_diagnostic(M.get_prev(opts), opts)
  1101. end
  1102. --- Get the next diagnostic closest to the cursor position.
  1103. ---
  1104. ---@param opts? vim.diagnostic.JumpOpts
  1105. ---@return vim.Diagnostic? : Next diagnostic
  1106. function M.get_next(opts)
  1107. return next_diagnostic(true, opts)
  1108. end
  1109. --- Return the position of the next diagnostic in the current buffer.
  1110. ---
  1111. ---@param opts? vim.diagnostic.JumpOpts
  1112. ---@return table|false : Next diagnostic position as a `(row, col)` tuple or false if no next
  1113. --- diagnostic.
  1114. ---@deprecated
  1115. function M.get_next_pos(opts)
  1116. vim.deprecate(
  1117. 'vim.diagnostic.get_next_pos()',
  1118. 'access the lnum and col fields from get_next() instead',
  1119. '0.13'
  1120. )
  1121. local next = M.get_next(opts)
  1122. if not next then
  1123. return false
  1124. end
  1125. return { next.lnum, next.col }
  1126. end
  1127. --- A table with the following keys:
  1128. --- @class vim.diagnostic.GetOpts
  1129. ---
  1130. --- Limit diagnostics to one or more namespaces.
  1131. --- @field namespace? integer[]|integer
  1132. ---
  1133. --- Limit diagnostics to those spanning the specified line number.
  1134. --- @field lnum? integer
  1135. ---
  1136. --- See |diagnostic-severity|.
  1137. --- @field severity? vim.diagnostic.SeverityFilter
  1138. --- Configuration table with the keys listed below. Some parameters can have their default values
  1139. --- changed with |vim.diagnostic.config()|.
  1140. --- @class vim.diagnostic.JumpOpts : vim.diagnostic.GetOpts
  1141. ---
  1142. --- The diagnostic to jump to. Mutually exclusive with {count}, {namespace},
  1143. --- and {severity}.
  1144. --- @field diagnostic? vim.Diagnostic
  1145. ---
  1146. --- The number of diagnostics to move by, starting from {pos}. A positive
  1147. --- integer moves forward by {count} diagnostics, while a negative integer moves
  1148. --- backward by {count} diagnostics. Mutually exclusive with {diagnostic}.
  1149. --- @field count? integer
  1150. ---
  1151. --- Cursor position as a `(row, col)` tuple. See |nvim_win_get_cursor()|. Used
  1152. --- to find the nearest diagnostic when {count} is used. Only used when {count}
  1153. --- is non-nil. Default is the current cursor position.
  1154. --- @field pos? [integer,integer]
  1155. ---
  1156. --- Whether to loop around file or not. Similar to 'wrapscan'.
  1157. --- (default: `true`)
  1158. --- @field wrap? boolean
  1159. ---
  1160. --- See |diagnostic-severity|.
  1161. --- @field severity? vim.diagnostic.SeverityFilter
  1162. ---
  1163. --- Go to the diagnostic with the highest severity.
  1164. --- (default: `false`)
  1165. --- @field package _highest? boolean
  1166. ---
  1167. --- If `true`, call |vim.diagnostic.open_float()| after moving.
  1168. --- If a table, pass the table as the {opts} parameter to |vim.diagnostic.open_float()|.
  1169. --- Unless overridden, the float will show diagnostics at the new cursor
  1170. --- position (as if "cursor" were passed to the "scope" option).
  1171. --- (default: `false`)
  1172. --- @field float? boolean|vim.diagnostic.Opts.Float
  1173. ---
  1174. --- Window ID
  1175. --- (default: `0`)
  1176. --- @field winid? integer
  1177. --- Move to a diagnostic.
  1178. ---
  1179. --- @param opts vim.diagnostic.JumpOpts
  1180. --- @return vim.Diagnostic? # The diagnostic that was moved to.
  1181. function M.jump(opts)
  1182. vim.validate('opts', opts, 'table')
  1183. -- One of "diagnostic" or "count" must be provided
  1184. assert(
  1185. opts.diagnostic or opts.count,
  1186. 'One of "diagnostic" or "count" must be specified in the options to vim.diagnostic.jump()'
  1187. )
  1188. -- Apply configuration options from vim.diagnostic.config()
  1189. opts = vim.tbl_deep_extend('keep', opts, global_diagnostic_options.jump)
  1190. if opts.diagnostic then
  1191. goto_diagnostic(opts.diagnostic, opts)
  1192. return opts.diagnostic
  1193. end
  1194. local count = opts.count
  1195. if count == 0 then
  1196. return nil
  1197. end
  1198. -- Support deprecated cursor_position alias
  1199. if opts.cursor_position then
  1200. vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
  1201. opts.pos = opts.cursor_position
  1202. opts.cursor_position = nil --- @diagnostic disable-line
  1203. end
  1204. local diag = nil
  1205. while count ~= 0 do
  1206. local next = next_diagnostic(count > 0, opts)
  1207. if not next then
  1208. break
  1209. end
  1210. -- Update cursor position
  1211. opts.pos = { next.lnum + 1, next.col }
  1212. if count > 0 then
  1213. count = count - 1
  1214. else
  1215. count = count + 1
  1216. end
  1217. diag = next
  1218. end
  1219. goto_diagnostic(diag, opts)
  1220. return diag
  1221. end
  1222. --- Move to the next diagnostic.
  1223. ---
  1224. ---@param opts? vim.diagnostic.JumpOpts
  1225. ---@deprecated
  1226. function M.goto_next(opts)
  1227. vim.deprecate('vim.diagnostic.goto_next()', 'vim.diagnostic.jump()', '0.13')
  1228. opts = opts or {}
  1229. opts.float = if_nil(opts.float, true)
  1230. goto_diagnostic(M.get_next(opts), opts)
  1231. end
  1232. M.handlers.signs = {
  1233. show = function(namespace, bufnr, diagnostics, opts)
  1234. vim.validate('namespace', namespace, 'number')
  1235. vim.validate('bufnr', bufnr, 'number')
  1236. vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
  1237. vim.validate('opts', opts, 'table', true)
  1238. bufnr = vim._resolve_bufnr(bufnr)
  1239. opts = opts or {}
  1240. if not api.nvim_buf_is_loaded(bufnr) then
  1241. return
  1242. end
  1243. if opts.signs and opts.signs.severity then
  1244. diagnostics = filter_by_severity(opts.signs.severity, diagnostics)
  1245. end
  1246. -- 10 is the default sign priority when none is explicitly specified
  1247. local priority = opts.signs and opts.signs.priority or 10
  1248. local get_priority = severity_to_extmark_priority(priority, opts)
  1249. local ns = M.get_namespace(namespace)
  1250. if not ns.user_data.sign_ns then
  1251. ns.user_data.sign_ns =
  1252. api.nvim_create_namespace(string.format('%s/diagnostic/signs', ns.name))
  1253. end
  1254. -- Handle legacy diagnostic sign definitions
  1255. -- These were deprecated in 0.10 and will be removed in 0.12
  1256. if opts.signs and not opts.signs.text and not opts.signs.numhl then
  1257. for _, v in ipairs({ 'Error', 'Warn', 'Info', 'Hint' }) do
  1258. local name = string.format('DiagnosticSign%s', v)
  1259. local sign = vim.fn.sign_getdefined(name)[1]
  1260. if sign then
  1261. local severity = M.severity[v:upper()]
  1262. vim.deprecate(
  1263. 'Defining diagnostic signs with :sign-define or sign_define()',
  1264. 'vim.diagnostic.config()',
  1265. '0.12'
  1266. )
  1267. if not opts.signs.text then
  1268. opts.signs.text = {}
  1269. end
  1270. if not opts.signs.numhl then
  1271. opts.signs.numhl = {}
  1272. end
  1273. if not opts.signs.linehl then
  1274. opts.signs.linehl = {}
  1275. end
  1276. if opts.signs.text[severity] == nil then
  1277. opts.signs.text[severity] = sign.text or ''
  1278. end
  1279. if opts.signs.numhl[severity] == nil then
  1280. opts.signs.numhl[severity] = sign.numhl
  1281. end
  1282. if opts.signs.linehl[severity] == nil then
  1283. opts.signs.linehl[severity] = sign.linehl
  1284. end
  1285. end
  1286. end
  1287. end
  1288. local text = {} ---@type table<vim.diagnostic.Severity|string, string>
  1289. for k in pairs(M.severity) do
  1290. if opts.signs.text and opts.signs.text[k] then
  1291. text[k] = opts.signs.text[k]
  1292. elseif type(k) == 'string' and not text[k] then
  1293. text[k] = string.sub(k, 1, 1):upper()
  1294. end
  1295. end
  1296. local numhl = opts.signs.numhl or {}
  1297. local linehl = opts.signs.linehl or {}
  1298. local line_count = api.nvim_buf_line_count(bufnr)
  1299. for _, diagnostic in ipairs(diagnostics) do
  1300. if diagnostic.lnum <= line_count then
  1301. api.nvim_buf_set_extmark(bufnr, ns.user_data.sign_ns, diagnostic.lnum, 0, {
  1302. sign_text = text[diagnostic.severity] or text[M.severity[diagnostic.severity]] or 'U',
  1303. sign_hl_group = sign_highlight_map[diagnostic.severity],
  1304. number_hl_group = numhl[diagnostic.severity],
  1305. line_hl_group = linehl[diagnostic.severity],
  1306. priority = get_priority(diagnostic.severity),
  1307. })
  1308. end
  1309. end
  1310. end,
  1311. --- @param namespace integer
  1312. --- @param bufnr integer
  1313. hide = function(namespace, bufnr)
  1314. local ns = M.get_namespace(namespace)
  1315. if ns.user_data.sign_ns and api.nvim_buf_is_valid(bufnr) then
  1316. api.nvim_buf_clear_namespace(bufnr, ns.user_data.sign_ns, 0, -1)
  1317. end
  1318. end,
  1319. }
  1320. M.handlers.underline = {
  1321. show = function(namespace, bufnr, diagnostics, opts)
  1322. vim.validate('namespace', namespace, 'number')
  1323. vim.validate('bufnr', bufnr, 'number')
  1324. vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
  1325. vim.validate('opts', opts, 'table', true)
  1326. bufnr = vim._resolve_bufnr(bufnr)
  1327. opts = opts or {}
  1328. if not vim.api.nvim_buf_is_loaded(bufnr) then
  1329. return
  1330. end
  1331. if opts.underline and opts.underline.severity then
  1332. diagnostics = filter_by_severity(opts.underline.severity, diagnostics)
  1333. end
  1334. local ns = M.get_namespace(namespace)
  1335. if not ns.user_data.underline_ns then
  1336. ns.user_data.underline_ns =
  1337. api.nvim_create_namespace(string.format('%s/diagnostic/underline', ns.name))
  1338. end
  1339. local underline_ns = ns.user_data.underline_ns
  1340. local get_priority = severity_to_extmark_priority(vim.hl.priorities.diagnostics, opts)
  1341. for _, diagnostic in ipairs(diagnostics) do
  1342. -- Default to error if we don't have a highlight associated
  1343. local higroup = underline_highlight_map[assert(diagnostic.severity)]
  1344. or underline_highlight_map[vim.diagnostic.severity.ERROR]
  1345. if diagnostic._tags then
  1346. -- TODO(lewis6991): we should be able to stack these.
  1347. if diagnostic._tags.unnecessary then
  1348. higroup = 'DiagnosticUnnecessary'
  1349. end
  1350. if diagnostic._tags.deprecated then
  1351. higroup = 'DiagnosticDeprecated'
  1352. end
  1353. end
  1354. vim.hl.range(
  1355. bufnr,
  1356. underline_ns,
  1357. higroup,
  1358. { diagnostic.lnum, diagnostic.col },
  1359. { diagnostic.end_lnum, diagnostic.end_col },
  1360. { priority = get_priority(diagnostic.severity) }
  1361. )
  1362. end
  1363. save_extmarks(underline_ns, bufnr)
  1364. end,
  1365. hide = function(namespace, bufnr)
  1366. local ns = M.get_namespace(namespace)
  1367. if ns.user_data.underline_ns then
  1368. diagnostic_cache_extmarks[bufnr][ns.user_data.underline_ns] = {}
  1369. if api.nvim_buf_is_valid(bufnr) then
  1370. api.nvim_buf_clear_namespace(bufnr, ns.user_data.underline_ns, 0, -1)
  1371. end
  1372. end
  1373. end,
  1374. }
  1375. M.handlers.virtual_text = {
  1376. show = function(namespace, bufnr, diagnostics, opts)
  1377. vim.validate('namespace', namespace, 'number')
  1378. vim.validate('bufnr', bufnr, 'number')
  1379. vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
  1380. vim.validate('opts', opts, 'table', true)
  1381. bufnr = vim._resolve_bufnr(bufnr)
  1382. opts = opts or {}
  1383. if not vim.api.nvim_buf_is_loaded(bufnr) then
  1384. return
  1385. end
  1386. local severity --- @type vim.diagnostic.SeverityFilter?
  1387. if opts.virtual_text then
  1388. if opts.virtual_text.format then
  1389. diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics)
  1390. end
  1391. if
  1392. opts.virtual_text.source
  1393. and (opts.virtual_text.source ~= 'if_many' or count_sources(bufnr) > 1)
  1394. then
  1395. diagnostics = prefix_source(diagnostics)
  1396. end
  1397. if opts.virtual_text.severity then
  1398. severity = opts.virtual_text.severity
  1399. end
  1400. end
  1401. local ns = M.get_namespace(namespace)
  1402. if not ns.user_data.virt_text_ns then
  1403. ns.user_data.virt_text_ns =
  1404. api.nvim_create_namespace(string.format('%s/diagnostic/virtual_text', ns.name))
  1405. end
  1406. local virt_text_ns = ns.user_data.virt_text_ns
  1407. local buffer_line_diagnostics = diagnostic_lines(diagnostics)
  1408. for line, line_diagnostics in pairs(buffer_line_diagnostics) do
  1409. if severity then
  1410. line_diagnostics = filter_by_severity(severity, line_diagnostics)
  1411. end
  1412. local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts.virtual_text)
  1413. if virt_texts then
  1414. api.nvim_buf_set_extmark(bufnr, virt_text_ns, line, 0, {
  1415. hl_mode = opts.virtual_text.hl_mode or 'combine',
  1416. virt_text = virt_texts,
  1417. virt_text_pos = opts.virtual_text.virt_text_pos,
  1418. virt_text_hide = opts.virtual_text.virt_text_hide,
  1419. virt_text_win_col = opts.virtual_text.virt_text_win_col,
  1420. })
  1421. end
  1422. end
  1423. save_extmarks(virt_text_ns, bufnr)
  1424. end,
  1425. hide = function(namespace, bufnr)
  1426. local ns = M.get_namespace(namespace)
  1427. if ns.user_data.virt_text_ns then
  1428. diagnostic_cache_extmarks[bufnr][ns.user_data.virt_text_ns] = {}
  1429. if api.nvim_buf_is_valid(bufnr) then
  1430. api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_text_ns, 0, -1)
  1431. end
  1432. end
  1433. end,
  1434. }
  1435. --- Get virtual text chunks to display using |nvim_buf_set_extmark()|.
  1436. ---
  1437. --- Exported for backward compatibility with
  1438. --- vim.lsp.diagnostic.get_virtual_text_chunks_for_line(). When that function is eventually removed,
  1439. --- this can be made local.
  1440. --- @private
  1441. --- @param line_diags table<integer,vim.Diagnostic>
  1442. --- @param opts vim.diagnostic.Opts.VirtualText
  1443. function M._get_virt_text_chunks(line_diags, opts)
  1444. if #line_diags == 0 then
  1445. return nil
  1446. end
  1447. opts = opts or {}
  1448. local prefix = opts.prefix or '■'
  1449. local suffix = opts.suffix or ''
  1450. local spacing = opts.spacing or 4
  1451. -- Create a little more space between virtual text and contents
  1452. local virt_texts = { { string.rep(' ', spacing) } }
  1453. for i = 1, #line_diags do
  1454. local resolved_prefix = prefix
  1455. if type(prefix) == 'function' then
  1456. resolved_prefix = prefix(line_diags[i], i, #line_diags) or ''
  1457. end
  1458. table.insert(
  1459. virt_texts,
  1460. { resolved_prefix, virtual_text_highlight_map[line_diags[i].severity] }
  1461. )
  1462. end
  1463. local last = line_diags[#line_diags]
  1464. -- TODO(tjdevries): Allow different servers to be shown first somehow?
  1465. -- TODO(tjdevries): Display server name associated with these?
  1466. if last.message then
  1467. if type(suffix) == 'function' then
  1468. suffix = suffix(last) or ''
  1469. end
  1470. table.insert(virt_texts, {
  1471. string.format(' %s%s', last.message:gsub('\r', ''):gsub('\n', ' '), suffix),
  1472. virtual_text_highlight_map[last.severity],
  1473. })
  1474. return virt_texts
  1475. end
  1476. end
  1477. --- Hide currently displayed diagnostics.
  1478. ---
  1479. --- This only clears the decorations displayed in the buffer. Diagnostics can
  1480. --- be redisplayed with |vim.diagnostic.show()|. To completely remove
  1481. --- diagnostics, use |vim.diagnostic.reset()|.
  1482. ---
  1483. --- To hide diagnostics and prevent them from re-displaying, use
  1484. --- |vim.diagnostic.enable()|.
  1485. ---
  1486. ---@param namespace integer? Diagnostic namespace. When omitted, hide
  1487. --- diagnostics from all namespaces.
  1488. ---@param bufnr integer? Buffer number, or 0 for current buffer. When
  1489. --- omitted, hide diagnostics in all buffers.
  1490. function M.hide(namespace, bufnr)
  1491. vim.validate('namespace', namespace, 'number', true)
  1492. vim.validate('bufnr', bufnr, 'number', true)
  1493. local buffers = bufnr and { vim._resolve_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache)
  1494. for _, iter_bufnr in ipairs(buffers) do
  1495. local namespaces = namespace and { namespace } or vim.tbl_keys(diagnostic_cache[iter_bufnr])
  1496. for _, iter_namespace in ipairs(namespaces) do
  1497. for _, handler in pairs(M.handlers) do
  1498. if handler.hide then
  1499. handler.hide(iter_namespace, iter_bufnr)
  1500. end
  1501. end
  1502. end
  1503. end
  1504. end
  1505. --- Check whether diagnostics are enabled.
  1506. ---
  1507. --- @param filter vim.diagnostic.Filter?
  1508. --- @return boolean
  1509. --- @since 12
  1510. function M.is_enabled(filter)
  1511. filter = filter or {}
  1512. if filter.ns_id and M.get_namespace(filter.ns_id).disabled then
  1513. return false
  1514. elseif filter.bufnr == nil then
  1515. -- See enable() logic.
  1516. return vim.tbl_isempty(diagnostic_disabled) and not diagnostic_disabled[1]
  1517. end
  1518. local bufnr = vim._resolve_bufnr(filter.bufnr)
  1519. if type(diagnostic_disabled[bufnr]) == 'table' then
  1520. return not diagnostic_disabled[bufnr][filter.ns_id]
  1521. end
  1522. return diagnostic_disabled[bufnr] == nil
  1523. end
  1524. --- @deprecated use `vim.diagnostic.is_enabled()`
  1525. function M.is_disabled(bufnr, namespace)
  1526. vim.deprecate('vim.diagnostic.is_disabled()', 'vim.diagnostic.is_enabled()', '0.12')
  1527. return not M.is_enabled { bufnr = bufnr or 0, ns_id = namespace }
  1528. end
  1529. --- Display diagnostics for the given namespace and buffer.
  1530. ---
  1531. ---@param namespace integer? Diagnostic namespace. When omitted, show
  1532. --- diagnostics from all namespaces.
  1533. ---@param bufnr integer? Buffer number, or 0 for current buffer. When omitted, show
  1534. --- diagnostics in all buffers.
  1535. ---@param diagnostics vim.Diagnostic[]? The diagnostics to display. When omitted, use the
  1536. --- saved diagnostics for the given namespace and
  1537. --- buffer. This can be used to display a list of diagnostics
  1538. --- without saving them or to display only a subset of
  1539. --- diagnostics. May not be used when {namespace}
  1540. --- or {bufnr} is nil.
  1541. ---@param opts? vim.diagnostic.Opts Display options.
  1542. function M.show(namespace, bufnr, diagnostics, opts)
  1543. vim.validate('namespace', namespace, 'number', true)
  1544. vim.validate('bufnr', bufnr, 'number', true)
  1545. vim.validate('diagnostics', diagnostics, vim.islist, true, 'a list of diagnostics')
  1546. vim.validate('opts', opts, 'table', true)
  1547. if not bufnr or not namespace then
  1548. assert(not diagnostics, 'Cannot show diagnostics without a buffer and namespace')
  1549. if not bufnr then
  1550. for iter_bufnr in pairs(diagnostic_cache) do
  1551. M.show(namespace, iter_bufnr, nil, opts)
  1552. end
  1553. else
  1554. -- namespace is nil
  1555. bufnr = vim._resolve_bufnr(bufnr)
  1556. for iter_namespace in pairs(diagnostic_cache[bufnr]) do
  1557. M.show(iter_namespace, bufnr, nil, opts)
  1558. end
  1559. end
  1560. return
  1561. end
  1562. if not M.is_enabled { bufnr = bufnr or 0, ns_id = namespace } then
  1563. return
  1564. end
  1565. M.hide(namespace, bufnr)
  1566. diagnostics = diagnostics or get_diagnostics(bufnr, { namespace = namespace }, true)
  1567. if vim.tbl_isempty(diagnostics) then
  1568. return
  1569. end
  1570. local opts_res = get_resolved_options(opts, namespace, bufnr)
  1571. if opts_res.update_in_insert then
  1572. clear_scheduled_display(namespace, bufnr)
  1573. else
  1574. local mode = api.nvim_get_mode()
  1575. if string.sub(mode.mode, 1, 1) == 'i' then
  1576. schedule_display(namespace, bufnr, opts_res)
  1577. return
  1578. end
  1579. end
  1580. if if_nil(opts_res.severity_sort, false) then
  1581. if type(opts_res.severity_sort) == 'table' and opts_res.severity_sort.reverse then
  1582. table.sort(diagnostics, function(a, b)
  1583. return a.severity < b.severity
  1584. end)
  1585. else
  1586. table.sort(diagnostics, function(a, b)
  1587. return a.severity > b.severity
  1588. end)
  1589. end
  1590. end
  1591. for handler_name, handler in pairs(M.handlers) do
  1592. if handler.show and opts_res[handler_name] then
  1593. handler.show(namespace, bufnr, diagnostics, opts_res)
  1594. end
  1595. end
  1596. end
  1597. --- Show diagnostics in a floating window.
  1598. ---
  1599. ---@param opts vim.diagnostic.Opts.Float?
  1600. ---@return integer? float_bufnr
  1601. ---@return integer? winid
  1602. function M.open_float(opts, ...)
  1603. -- Support old (bufnr, opts) signature
  1604. local bufnr --- @type integer?
  1605. if opts == nil or type(opts) == 'number' then
  1606. bufnr = opts
  1607. opts = ... --- @type vim.diagnostic.Opts.Float
  1608. else
  1609. vim.validate('opts', opts, 'table', true)
  1610. end
  1611. opts = opts or {}
  1612. bufnr = vim._resolve_bufnr(bufnr or opts.bufnr)
  1613. do
  1614. -- Resolve options with user settings from vim.diagnostic.config
  1615. -- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float`
  1616. -- does not have a dedicated table for configuration options; instead, the options are mixed in
  1617. -- with its `opts` table which also includes "keyword" parameters. So we create a dedicated
  1618. -- options table that inherits missing keys from the global configuration before resolving.
  1619. local t = global_diagnostic_options.float
  1620. local float_opts = vim.tbl_extend('keep', opts, type(t) == 'table' and t or {})
  1621. opts = get_resolved_options({ float = float_opts }, nil, bufnr).float
  1622. end
  1623. local scope = ({ l = 'line', c = 'cursor', b = 'buffer' })[opts.scope] or opts.scope or 'line'
  1624. local lnum, col --- @type integer, integer
  1625. local opts_pos = opts.pos
  1626. if scope == 'line' or scope == 'cursor' then
  1627. if not opts_pos then
  1628. local pos = api.nvim_win_get_cursor(0)
  1629. lnum = pos[1] - 1
  1630. col = pos[2]
  1631. elseif type(opts_pos) == 'number' then
  1632. lnum = opts_pos
  1633. elseif type(opts_pos) == 'table' then
  1634. lnum, col = opts_pos[1], opts_pos[2]
  1635. else
  1636. error("Invalid value for option 'pos'")
  1637. end
  1638. elseif scope ~= 'buffer' then
  1639. error("Invalid value for option 'scope'")
  1640. end
  1641. local diagnostics = get_diagnostics(bufnr, opts --[[@as vim.diagnostic.GetOpts]], true)
  1642. if scope == 'line' then
  1643. --- @param d vim.Diagnostic
  1644. diagnostics = vim.tbl_filter(function(d)
  1645. return lnum >= d.lnum
  1646. and lnum <= d.end_lnum
  1647. and (d.lnum == d.end_lnum or lnum ~= d.end_lnum or d.end_col ~= 0)
  1648. end, diagnostics)
  1649. elseif scope == 'cursor' then
  1650. -- If `col` is past the end of the line, show if the cursor is on the last char in the line
  1651. local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
  1652. --- @param d vim.Diagnostic
  1653. diagnostics = vim.tbl_filter(function(d)
  1654. return lnum >= d.lnum
  1655. and lnum <= d.end_lnum
  1656. and (lnum ~= d.lnum or col >= math.min(d.col, line_length - 1))
  1657. and ((d.lnum == d.end_lnum and d.col == d.end_col) or lnum ~= d.end_lnum or col < d.end_col)
  1658. end, diagnostics)
  1659. end
  1660. if vim.tbl_isempty(diagnostics) then
  1661. return
  1662. end
  1663. local severity_sort = if_nil(opts.severity_sort, global_diagnostic_options.severity_sort)
  1664. if severity_sort then
  1665. if type(severity_sort) == 'table' and severity_sort.reverse then
  1666. table.sort(diagnostics, function(a, b)
  1667. return a.severity > b.severity
  1668. end)
  1669. else
  1670. table.sort(diagnostics, function(a, b)
  1671. return a.severity < b.severity
  1672. end)
  1673. end
  1674. end
  1675. local lines = {} --- @type string[]
  1676. local highlights = {} --- @type table[]
  1677. local header = if_nil(opts.header, 'Diagnostics:')
  1678. if header then
  1679. vim.validate('header', header, { 'string', 'table' }, "'string' or 'table'")
  1680. if type(header) == 'table' then
  1681. -- Don't insert any lines for an empty string
  1682. if string.len(if_nil(header[1], '')) > 0 then
  1683. table.insert(lines, header[1])
  1684. table.insert(highlights, { hlname = header[2] or 'Bold' })
  1685. end
  1686. elseif #header > 0 then
  1687. table.insert(lines, header)
  1688. table.insert(highlights, { hlname = 'Bold' })
  1689. end
  1690. end
  1691. if opts.format then
  1692. diagnostics = reformat_diagnostics(opts.format, diagnostics)
  1693. end
  1694. if opts.source and (opts.source ~= 'if_many' or count_sources(bufnr) > 1) then
  1695. diagnostics = prefix_source(diagnostics)
  1696. end
  1697. local prefix_opt =
  1698. if_nil(opts.prefix, (scope == 'cursor' and #diagnostics <= 1) and '' or function(_, i)
  1699. return string.format('%d. ', i)
  1700. end)
  1701. local prefix, prefix_hl_group --- @type string?, string?
  1702. if prefix_opt then
  1703. vim.validate(
  1704. 'prefix',
  1705. prefix_opt,
  1706. { 'string', 'table', 'function' },
  1707. "'string' or 'table' or 'function'"
  1708. )
  1709. if type(prefix_opt) == 'string' then
  1710. prefix, prefix_hl_group = prefix_opt, 'NormalFloat'
  1711. elseif type(prefix_opt) == 'table' then
  1712. prefix, prefix_hl_group = prefix_opt[1] or '', prefix_opt[2] or 'NormalFloat'
  1713. end
  1714. end
  1715. local suffix_opt = if_nil(opts.suffix, function(diagnostic)
  1716. return diagnostic.code and string.format(' [%s]', diagnostic.code) or ''
  1717. end)
  1718. local suffix, suffix_hl_group --- @type string?, string?
  1719. if suffix_opt then
  1720. vim.validate(
  1721. 'suffix',
  1722. suffix_opt,
  1723. { 'string', 'table', 'function' },
  1724. "'string' or 'table' or 'function'"
  1725. )
  1726. if type(suffix_opt) == 'string' then
  1727. suffix, suffix_hl_group = suffix_opt, 'NormalFloat'
  1728. elseif type(suffix_opt) == 'table' then
  1729. suffix, suffix_hl_group = suffix_opt[1] or '', suffix_opt[2] or 'NormalFloat'
  1730. end
  1731. end
  1732. for i, diagnostic in ipairs(diagnostics) do
  1733. if type(prefix_opt) == 'function' then
  1734. --- @cast prefix_opt fun(...): string?, string?
  1735. local prefix0, prefix_hl_group0 = prefix_opt(diagnostic, i, #diagnostics)
  1736. prefix, prefix_hl_group = prefix0 or '', prefix_hl_group0 or 'NormalFloat'
  1737. end
  1738. if type(suffix_opt) == 'function' then
  1739. --- @cast suffix_opt fun(...): string?, string?
  1740. local suffix0, suffix_hl_group0 = suffix_opt(diagnostic, i, #diagnostics)
  1741. suffix, suffix_hl_group = suffix0 or '', suffix_hl_group0 or 'NormalFloat'
  1742. end
  1743. --- @type string?
  1744. local hiname = floating_highlight_map[assert(diagnostic.severity)]
  1745. local message_lines = vim.split(diagnostic.message, '\n')
  1746. for j = 1, #message_lines do
  1747. local pre = j == 1 and prefix or string.rep(' ', #prefix)
  1748. local suf = j == #message_lines and suffix or ''
  1749. table.insert(lines, pre .. message_lines[j] .. suf)
  1750. table.insert(highlights, {
  1751. hlname = hiname,
  1752. prefix = {
  1753. length = j == 1 and #prefix or 0,
  1754. hlname = prefix_hl_group,
  1755. },
  1756. suffix = {
  1757. length = j == #message_lines and #suffix or 0,
  1758. hlname = suffix_hl_group,
  1759. },
  1760. })
  1761. end
  1762. end
  1763. -- Used by open_floating_preview to allow the float to be focused
  1764. if not opts.focus_id then
  1765. opts.focus_id = scope
  1766. end
  1767. local float_bufnr, winnr = vim.lsp.util.open_floating_preview(lines, 'plaintext', opts)
  1768. vim.bo[float_bufnr].path = vim.bo[bufnr].path
  1769. for i, hl in ipairs(highlights) do
  1770. local line = lines[i]
  1771. local prefix_len = hl.prefix and hl.prefix.length or 0
  1772. local suffix_len = hl.suffix and hl.suffix.length or 0
  1773. if prefix_len > 0 then
  1774. api.nvim_buf_add_highlight(float_bufnr, -1, hl.prefix.hlname, i - 1, 0, prefix_len)
  1775. end
  1776. api.nvim_buf_add_highlight(float_bufnr, -1, hl.hlname, i - 1, prefix_len, #line - suffix_len)
  1777. if suffix_len > 0 then
  1778. api.nvim_buf_add_highlight(float_bufnr, -1, hl.suffix.hlname, i - 1, #line - suffix_len, -1)
  1779. end
  1780. end
  1781. return float_bufnr, winnr
  1782. end
  1783. --- Remove all diagnostics from the given namespace.
  1784. ---
  1785. --- Unlike |vim.diagnostic.hide()|, this function removes all saved
  1786. --- diagnostics. They cannot be redisplayed using |vim.diagnostic.show()|. To
  1787. --- simply remove diagnostic decorations in a way that they can be
  1788. --- re-displayed, use |vim.diagnostic.hide()|.
  1789. ---
  1790. ---@param namespace integer? Diagnostic namespace. When omitted, remove
  1791. --- diagnostics from all namespaces.
  1792. ---@param bufnr integer? Remove diagnostics for the given buffer. When omitted,
  1793. --- diagnostics are removed for all buffers.
  1794. function M.reset(namespace, bufnr)
  1795. vim.validate('namespace', namespace, 'number', true)
  1796. vim.validate('bufnr', bufnr, 'number', true)
  1797. local buffers = bufnr and { vim._resolve_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache)
  1798. for _, iter_bufnr in ipairs(buffers) do
  1799. local namespaces = namespace and { namespace } or vim.tbl_keys(diagnostic_cache[iter_bufnr])
  1800. for _, iter_namespace in ipairs(namespaces) do
  1801. diagnostic_cache[iter_bufnr][iter_namespace] = nil
  1802. M.hide(iter_namespace, iter_bufnr)
  1803. end
  1804. if api.nvim_buf_is_valid(iter_bufnr) then
  1805. api.nvim_exec_autocmds('DiagnosticChanged', {
  1806. modeline = false,
  1807. buffer = iter_bufnr,
  1808. data = { diagnostics = {} },
  1809. })
  1810. else
  1811. diagnostic_cache[iter_bufnr] = nil
  1812. end
  1813. end
  1814. end
  1815. --- Configuration table with the following keys:
  1816. --- @class vim.diagnostic.setqflist.Opts
  1817. --- @inlinedoc
  1818. ---
  1819. --- Only add diagnostics from the given namespace.
  1820. --- @field namespace? integer
  1821. ---
  1822. --- Open quickfix list after setting.
  1823. --- (default: `true`)
  1824. --- @field open? boolean
  1825. ---
  1826. --- Title of quickfix list. Defaults to "Diagnostics".
  1827. --- @field title? string
  1828. ---
  1829. --- See |diagnostic-severity|.
  1830. --- @field severity? vim.diagnostic.SeverityFilter
  1831. --- Add all diagnostics to the quickfix list.
  1832. ---
  1833. ---@param opts? vim.diagnostic.setqflist.Opts
  1834. function M.setqflist(opts)
  1835. set_list(false, opts)
  1836. end
  1837. ---Configuration table with the following keys:
  1838. --- @class vim.diagnostic.setloclist.Opts
  1839. --- @inlinedoc
  1840. ---
  1841. --- Only add diagnostics from the given namespace.
  1842. --- @field namespace? integer
  1843. ---
  1844. --- Window number to set location list for.
  1845. --- (default: `0`)
  1846. --- @field winnr? integer
  1847. ---
  1848. --- Open the location list after setting.
  1849. --- (default: `true`)
  1850. --- @field open? boolean
  1851. ---
  1852. --- Title of the location list. Defaults to "Diagnostics".
  1853. --- @field title? string
  1854. ---
  1855. --- See |diagnostic-severity|.
  1856. --- @field severity? vim.diagnostic.SeverityFilter
  1857. --- Add buffer diagnostics to the location list.
  1858. ---
  1859. ---@param opts? vim.diagnostic.setloclist.Opts
  1860. function M.setloclist(opts)
  1861. set_list(true, opts)
  1862. end
  1863. --- @deprecated use `vim.diagnostic.enable(false, …)`
  1864. function M.disable(bufnr, namespace)
  1865. vim.deprecate('vim.diagnostic.disable()', 'vim.diagnostic.enable(false, …)', '0.12')
  1866. M.enable(false, { bufnr = bufnr, ns_id = namespace })
  1867. end
  1868. --- Enables or disables diagnostics.
  1869. ---
  1870. --- To "toggle", pass the inverse of `is_enabled()`:
  1871. ---
  1872. --- ```lua
  1873. --- vim.diagnostic.enable(not vim.diagnostic.is_enabled())
  1874. --- ```
  1875. ---
  1876. --- @param enable (boolean|nil) true/nil to enable, false to disable
  1877. --- @param filter vim.diagnostic.Filter?
  1878. function M.enable(enable, filter)
  1879. -- Deprecated signature. Drop this in 0.12
  1880. local legacy = (enable or filter)
  1881. and vim.tbl_contains({ 'number', 'nil' }, type(enable))
  1882. and vim.tbl_contains({ 'number', 'nil' }, type(filter))
  1883. if legacy then
  1884. vim.deprecate(
  1885. 'vim.diagnostic.enable(buf:number, namespace:number)',
  1886. 'vim.diagnostic.enable(enable:boolean, filter:table)',
  1887. '0.12'
  1888. )
  1889. vim.validate('enable', enable, 'number', true) -- Legacy `bufnr` arg.
  1890. vim.validate('filter', filter, 'number', true) -- Legacy `namespace` arg.
  1891. local ns_id = type(filter) == 'number' and filter or nil
  1892. filter = {}
  1893. filter.ns_id = ns_id
  1894. filter.bufnr = type(enable) == 'number' and enable or nil
  1895. enable = true
  1896. else
  1897. filter = filter or {}
  1898. vim.validate('enable', enable, 'boolean', true)
  1899. vim.validate('filter', filter, 'table', true)
  1900. end
  1901. enable = enable == nil and true or enable
  1902. local bufnr = filter.bufnr
  1903. local ns_id = filter.ns_id
  1904. if not bufnr then
  1905. if not ns_id then
  1906. diagnostic_disabled = (
  1907. enable
  1908. -- Enable everything by setting diagnostic_disabled to an empty table.
  1909. and {}
  1910. -- Disable everything (including as yet non-existing buffers and namespaces) by setting
  1911. -- diagnostic_disabled to an empty table and set its metatable to always return true.
  1912. or setmetatable({}, {
  1913. __index = function()
  1914. return true
  1915. end,
  1916. })
  1917. )
  1918. else
  1919. local ns = M.get_namespace(ns_id)
  1920. ns.disabled = not enable
  1921. end
  1922. else
  1923. bufnr = vim._resolve_bufnr(bufnr)
  1924. if not ns_id then
  1925. diagnostic_disabled[bufnr] = (not enable) and true or nil
  1926. else
  1927. if type(diagnostic_disabled[bufnr]) ~= 'table' then
  1928. if enable then
  1929. return
  1930. else
  1931. diagnostic_disabled[bufnr] = {}
  1932. end
  1933. end
  1934. diagnostic_disabled[bufnr][ns_id] = (not enable) and true or nil
  1935. end
  1936. end
  1937. if enable then
  1938. M.show(ns_id, bufnr)
  1939. else
  1940. M.hide(ns_id, bufnr)
  1941. end
  1942. end
  1943. --- Parse a diagnostic from a string.
  1944. ---
  1945. --- For example, consider a line of output from a linter:
  1946. ---
  1947. --- ```
  1948. --- WARNING filename:27:3: Variable 'foo' does not exist
  1949. --- ```
  1950. ---
  1951. --- This can be parsed into |vim.Diagnostic| structure with:
  1952. ---
  1953. --- ```lua
  1954. --- local s = "WARNING filename:27:3: Variable 'foo' does not exist"
  1955. --- local pattern = "^(%w+) %w+:(%d+):(%d+): (.+)$"
  1956. --- local groups = { "severity", "lnum", "col", "message" }
  1957. --- vim.diagnostic.match(s, pattern, groups, { WARNING = vim.diagnostic.WARN })
  1958. --- ```
  1959. ---
  1960. ---@param str string String to parse diagnostics from.
  1961. ---@param pat string Lua pattern with capture groups.
  1962. ---@param groups string[] List of fields in a |vim.Diagnostic| structure to
  1963. --- associate with captures from {pat}.
  1964. ---@param severity_map table A table mapping the severity field from {groups}
  1965. --- with an item from |vim.diagnostic.severity|.
  1966. ---@param defaults table? Table of default values for any fields not listed in {groups}.
  1967. --- When omitted, numeric values default to 0 and "severity" defaults to
  1968. --- ERROR.
  1969. ---@return vim.Diagnostic?: |vim.Diagnostic| structure or `nil` if {pat} fails to match {str}.
  1970. function M.match(str, pat, groups, severity_map, defaults)
  1971. vim.validate('str', str, 'string')
  1972. vim.validate('pat', pat, 'string')
  1973. vim.validate('groups', groups, 'table')
  1974. vim.validate('severity_map', severity_map, 'table', true)
  1975. vim.validate('defaults', defaults, 'table', true)
  1976. --- @type table<string,vim.diagnostic.Severity>
  1977. severity_map = severity_map or M.severity
  1978. local matches = { str:match(pat) } --- @type any[]
  1979. if vim.tbl_isempty(matches) then
  1980. return
  1981. end
  1982. local diagnostic = {} --- @type type<string,any>
  1983. for i, match in ipairs(matches) do
  1984. local field = groups[i]
  1985. if field == 'severity' then
  1986. match = severity_map[match]
  1987. elseif field == 'lnum' or field == 'end_lnum' or field == 'col' or field == 'end_col' then
  1988. match = assert(tonumber(match)) - 1
  1989. end
  1990. diagnostic[field] = match --- @type any
  1991. end
  1992. diagnostic = vim.tbl_extend('keep', diagnostic, defaults or {}) --- @type vim.Diagnostic
  1993. diagnostic.severity = diagnostic.severity or M.severity.ERROR
  1994. diagnostic.col = diagnostic.col or 0
  1995. diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum
  1996. diagnostic.end_col = diagnostic.end_col or diagnostic.col
  1997. return diagnostic
  1998. end
  1999. local errlist_type_map = {
  2000. [M.severity.ERROR] = 'E',
  2001. [M.severity.WARN] = 'W',
  2002. [M.severity.INFO] = 'I',
  2003. [M.severity.HINT] = 'N',
  2004. }
  2005. --- Convert a list of diagnostics to a list of quickfix items that can be
  2006. --- passed to |setqflist()| or |setloclist()|.
  2007. ---
  2008. ---@param diagnostics vim.Diagnostic[]
  2009. ---@return table[] : Quickfix list items |setqflist-what|
  2010. function M.toqflist(diagnostics)
  2011. vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
  2012. local list = {} --- @type table[]
  2013. for _, v in ipairs(diagnostics) do
  2014. local item = {
  2015. bufnr = v.bufnr,
  2016. lnum = v.lnum + 1,
  2017. col = v.col and (v.col + 1) or nil,
  2018. end_lnum = v.end_lnum and (v.end_lnum + 1) or nil,
  2019. end_col = v.end_col and (v.end_col + 1) or nil,
  2020. text = v.message,
  2021. type = errlist_type_map[v.severity] or 'E',
  2022. }
  2023. table.insert(list, item)
  2024. end
  2025. table.sort(list, function(a, b)
  2026. if a.bufnr == b.bufnr then
  2027. if a.lnum == b.lnum then
  2028. return a.col < b.col
  2029. else
  2030. return a.lnum < b.lnum
  2031. end
  2032. else
  2033. return a.bufnr < b.bufnr
  2034. end
  2035. end)
  2036. return list
  2037. end
  2038. --- Convert a list of quickfix items to a list of diagnostics.
  2039. ---
  2040. ---@param list table[] List of quickfix items from |getqflist()| or |getloclist()|.
  2041. ---@return vim.Diagnostic[]
  2042. function M.fromqflist(list)
  2043. vim.validate('list', list, 'table')
  2044. local diagnostics = {} --- @type vim.Diagnostic[]
  2045. for _, item in ipairs(list) do
  2046. if item.valid == 1 then
  2047. local lnum = math.max(0, item.lnum - 1)
  2048. local col = math.max(0, item.col - 1)
  2049. local end_lnum = item.end_lnum > 0 and (item.end_lnum - 1) or lnum
  2050. local end_col = item.end_col > 0 and (item.end_col - 1) or col
  2051. local severity = item.type ~= '' and M.severity[item.type] or M.severity.ERROR
  2052. diagnostics[#diagnostics + 1] = {
  2053. bufnr = item.bufnr,
  2054. lnum = lnum,
  2055. col = col,
  2056. end_lnum = end_lnum,
  2057. end_col = end_col,
  2058. severity = severity,
  2059. message = item.text,
  2060. }
  2061. end
  2062. end
  2063. return diagnostics
  2064. end
  2065. return M