diagnostic.lua 85 KB

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