diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt index 861aed4884..d1f26c8c81 100644 --- a/runtime/doc/deprecated.txt +++ b/runtime/doc/deprecated.txt @@ -55,6 +55,50 @@ Functions ~ without stopping the job. Use chanclose(id) to close any socket. +LSP Diagnostics ~ + +For each of the functions below, use the corresponding function in +|vim.diagnostic| instead (unless otherwise noted). For example, use +|vim.diagnostic.get()| instead of |vim.lsp.diagnostic.get()|. + +*vim.lsp.diagnostic.clear()* Use |vim.diagnostic.hide()| instead. +*vim.lsp.diagnostic.disable()* +*vim.lsp.diagnostic.display()* Use |vim.diagnostic.show()| instead. +*vim.lsp.diagnostic.enable()* +*vim.lsp.diagnostic.get()* +*vim.lsp.diagnostic.get_all()* Use |vim.diagnostic.get()| instead. +*vim.lsp.diagnostic.get_count()* Use |vim.diagnostic.get()| instead. +*vim.lsp.diagnostic.get_line_diagnostics()* + Use |vim.diagnostic.get()| instead. +*vim.lsp.diagnostic.get_next()* +*vim.lsp.diagnostic.get_next_pos()* +*vim.lsp.diagnostic.get_prev()* +*vim.lsp.diagnostic.get_prev_pos()* +*vim.lsp.diagnostic.get_virtual_text_chunks_for_line()* + Use |vim.diagnostic.get_virt_text_chunks()| instead. +*vim.lsp.diagnostic.goto_next()* +*vim.lsp.diagnostic.goto_prev()* +*vim.lsp.diagnostic.redraw()* Use |vim.diagnostic.show()| instead. +*vim.lsp.diagnostic.reset()* +*vim.lsp.diagnostic.save()* Use |vim.diagnostic.set()| instead. +*vim.lsp.diagnostic.set_loclist()* Use |vim.diagnostic.setloclist()| instead. +*vim.lsp.diagnostic.set_qflist()* Use |vim.diagnostic.setqflist()| instead. +*vim.lsp.diagnostic.show_line_diagnostics()* +*vim.lsp.diagnostic.show_position_diagnostics()* + +The following are deprecated without replacement. These functions are moved +internally and are no longer exposed as part of the API. Instead, use +|vim.diagnostic.config()| and |vim.diagnostic.show()|. + +*vim.lsp.diagnostic.set_signs()* +*vim.lsp.diagnostic.set_underline()* +*vim.lsp.diagnostic.set_virtual_text()* + +LSP Utility Functions ~ + +*vim.lsp.util.set_qflist()* Use |setqflist()| instead. +*vim.lsp.util.set_loclist()* Use |setloclist()| instead. + Lua ~ *vim.register_keystroke_callback()* Use |vim.on_key()| instead. diff --git a/runtime/doc/diagnostic.txt b/runtime/doc/diagnostic.txt new file mode 100644 index 0000000000..f4975b187f --- /dev/null +++ b/runtime/doc/diagnostic.txt @@ -0,0 +1,483 @@ +*diagnostic.txt* Diagnostics + + + NVIM REFERENCE MANUAL + + +Diagnostic framework *vim.diagnostic* + +Nvim provides a framework for displaying errors or warnings from external +tools, otherwise known as "diagnostics". These diagnostics can come from a +variety of sources, such as linters or LSP servers. The diagnostic framework +is an extension to existing error handling functionality such as the +|quickfix| list. + + Type |gO| to see the table of contents. + +============================================================================== +QUICKSTART *diagnostic-quickstart* + +Anything that reports diagnostics is referred to below as a "diagnostic +producer". Diagnostic producers need only follow a few simple steps to +report diagnostics: + +1. Create a namespace |nvim_create_namespace()|. Note that the namespace must + have a name. Anonymous namespaces WILL NOT WORK. +2. (Optional) Configure options for the diagnostic namespace + |vim.diagnostic.config()|. +3. Generate diagnostics. +4. Set the diagnostics for the buffer |vim.diagnostic.set()|. +5. Repeat from step 3. + +Generally speaking, the API is split between functions meant to be used by +diagnostic producers and those meant for diagnostic consumers (i.e. end users +who want to read and view the diagnostics for a buffer). The APIs for +producers require a {namespace} as their first argument, while those for +consumers generally do not require a namespace (though often one may be +optionally supplied). A good rule of thumb is that if a method is meant to +modify the diagnostics for a buffer (e.g. |vim.diagnostic.set()|) then it +requires a namespace. + + *diagnostic-structure* +A diagnostic is a Lua table with the following keys: + + lnum: The starting line of the diagnostic + end_lnum: The final line of the diagnostic + col: The starting column of the diagnostic + end_col: The final column of the diagnostic + severity: The severity of the diagnostic |vim.diagnostic.severity| + message: The diagnostic text + +Diagnostics use the same indexing as the rest of the Nvim API (i.e. 0-based +rows and columns). |api-indexing| + + *vim.diagnostic.severity* *diagnostic-severity* +The "severity" key in a diagnostic is one of the values defined in +`vim.diagnostic.severity`: + + vim.diagnostic.severity.ERROR + vim.diagnostic.severity.WARN + vim.diagnostic.severity.INFO + vim.diagnostic.severity.HINT + +Functions that take a severity as an optional parameter (e.g. +|vim.diagnostic.get()|) accept one of two forms: + +1. A single |vim.diagnostic.severity| value: > + + vim.diagnostic.get(0, { severity = vim.diagnostic.severity.WARN }) + +2. A table with a "min" or "max" key (or both): > + + vim.diagnostic.get(0, { severity = {min=vim.diagnostic.severity.WARN}) + +The latter form allows users to specify a range of severities. + +============================================================================== +HIGHLIGHTS *diagnostic-highlights* + +All highlights defined for diagnostics begin with `Diagnostic` followed by +the type of highlight (e.g., `Sign`, `Underline`, etc.) and the severity (e.g. +`Error`, `Warn`, etc.) + +Sign, underline and virtual text highlights (by default) are linked to their +corresponding default highlight. + +For example, the default highlighting for |hl-DiagnosticSignError| is linked +to |hl-DiagnosticError|. To change the default (and therefore the linked +highlights), use the |:highlight| command: > + + highlight DiagnosticError guifg="BrightRed" +< + *hl-DiagnosticError* +DiagnosticError + Used as the base highlight group. + Other Diagnostic highlights link to this by default (except Underline) + + *hl-DiagnosticWarn* +DiagnosticWarn + Used as the base highlight group. + Other Diagnostic highlights link to this by default (except Underline) + + *hl-DiagnosticInfo* +DiagnosticInfo + Used as the base highlight group. + Other Diagnostic highlights link to this by default (except Underline) + + *hl-DiagnosticHint* +DiagnosticHint + Used as the base highlight group. + Other Diagnostic highlights link to this by default (except Underline) + + *hl-DiagnosticVirtualTextError* +DiagnosticVirtualTextError + Used for "Error" diagnostic virtual text. + + *hl-DiagnosticVirtualTextWarn* +DiagnosticVirtualTextWarn + Used for "Warn" diagnostic virtual text. + + *hl-DiagnosticVirtualTextInfo* +DiagnosticVirtualTextInfo + Used for "Info" diagnostic virtual text. + + *hl-DiagnosticVirtualTextHint* +DiagnosticVirtualTextHint + Used for "Hint" diagnostic virtual text. + + *hl-DiagnosticUnderlineError* +DiagnosticUnderlineError + Used to underline "Error" diagnostics. + + *hl-DiagnosticUnderlineWarn* +DiagnosticUnderlineWarn + Used to underline "Warn" diagnostics. + + *hl-DiagnosticUnderlineInfo* +DiagnosticUnderlineInfo + Used to underline "Info" diagnostics. + + *hl-DiagnosticUnderlineHint* +DiagnosticUnderlineHint + Used to underline "Hint" diagnostics. + + *hl-DiagnosticFloatingError* +DiagnosticFloatingError + Used to color "Error" diagnostic messages in diagnostics float. + See |vim.diagnostic.show_line_diagnostics()| + + *hl-DiagnosticFloatingWarn* +DiagnosticFloatingWarn + Used to color "Warn" diagnostic messages in diagnostics float. + + *hl-DiagnosticFloatingInfo* +DiagnosticFloatingInfo + Used to color "Info" diagnostic messages in diagnostics float. + + *hl-DiagnosticFloatingHint* +DiagnosticFloatingHint + Used to color "Hint" diagnostic messages in diagnostics float. + + *hl-DiagnosticSignError* +DiagnosticSignError + Used for "Error" signs in sign column. + + *hl-DiagnosticSignWarn* +DiagnosticSignWarn + Used for "Warn" signs in sign column. + + *hl-DiagnosticSignInfo* +DiagnosticSignInfo + Used for "Info" signs in sign column. + + *hl-DiagnosticSignHint* +DiagnosticSignHint + Used for "Hint" signs in sign column. + +============================================================================== +SIGNS *diagnostic-signs* + +Signs are defined for each diagnostic severity. The default text for each sign +is the first letter of the severity name (for example, "E" for ERROR). Signs +can be customized using the following: > + + sign define DiagnosticSignError text=E texthl=DiagnosticSignError linehl= numhl= + sign define DiagnosticSignWarning text=W texthl=DiagnosticSignWarning linehl= numhl= + sign define DiagnosticSignInformation text=I texthl=DiagnosticSignInformation linehl= numhl= + sign define DiagnosticSignHint text=H texthl=DiagnosticSignHint linehl= numhl= + +============================================================================== +EVENTS *diagnostic-events* + + *DiagnosticsChanged* +DiagnosticsChanged After diagnostics have changed. + +Example: > + autocmd User DiagnosticsChanged lua vim.diagnostic.setqflist({open = false }) +< + +============================================================================== +Lua module: vim.diagnostic *diagnostic-api* + +config({opts}, {namespace}) *vim.diagnostic.config()* + Configure diagnostic options globally or for a specific + diagnostic namespace. + + Note: + Each of the configuration options below accepts one of the + following: + • `false` : Disable this feature + • `true` : Enable this feature, use default settings. + • `table` : Enable this feature with overrides. + • `function` : Function with signature (namespace, bufnr) + that returns any of the above. + + Parameters: ~ + {opts} table Configuration table with the following + keys: + • underline: (default true) Use underline for + diagnostics + • virtual_text: (default true) Use virtual + text for diagnostics + • signs: (default true) Use signs for + diagnostics + • update_in_insert: (default false) Update + diagnostics in Insert mode (if false, + diagnostics are updated on InsertLeave) + • severity_sort: (default false) Sort + diagnostics by severity. This affects the + order in which signs and virtual text are + displayed + {namespace} number|nil Update the options for the given + namespace. When omitted, update the global + diagnostic options. + +disable({bufnr}, {namespace}) *vim.diagnostic.disable()* + Disable diagnostics in the given buffer. + + Parameters: ~ + {bufnr} number|nil Buffer number. Defaults to the + current buffer. + {namespace} number|nil Only disable diagnostics for the + given namespace. + +enable({bufnr}, {namespace}) *vim.diagnostic.enable()* + Enable diagnostics in the given buffer. + + Parameters: ~ + {bufnr} number|nil Buffer number. Defaults to the + current buffer. + {namespace} number|nil Only enable diagnostics for the + given namespace. + +get({bufnr}, {opts}) *vim.diagnostic.get()* + Get current diagnostics. + + Parameters: ~ + {bufnr} number|nil Buffer number to get diagnistics from. + Use 0 for current buffer or nil for all buffers. + {opts} table|nil A table with the following keys: + • namespace: (number) Limit diagnostics to the + given namespace. + • lnum: (number) Limit diagnostics to the given + line number. + • severity: See |diagnostic-severity|. + + Return: ~ + table A list of diagnostic items |diagnostic-structure|. + +get_next({opts}) *vim.diagnostic.get_next()* + Get the next diagnostic closest to the cursor position. + + Parameters: ~ + {opts} table See |vim.diagnostic.goto_next()| + + Return: ~ + table Next diagnostic + +get_next_pos({opts}) *vim.diagnostic.get_next_pos()* + Return the position of the next diagnostic in the current + buffer. + + Parameters: ~ + {opts} table See |vim.diagnostic.goto_next()| + + Return: ~ + table Next diagnostic position as a (row, col) tuple. + +get_prev({opts}) *vim.diagnostic.get_prev()* + Get the previous diagnostic closest to the cursor position. + + Parameters: ~ + {opts} table See |vim.diagnostic.goto_next()| + + Return: ~ + table Previous diagnostic + +get_prev_pos({opts}) *vim.diagnostic.get_prev_pos()* + Return the position of the previous diagnostic in the current + buffer. + + Parameters: ~ + {opts} table See |vim.diagnostic.goto_next()| + + Return: ~ + table Previous diagnostic position as a (row, col) tuple. + + *vim.diagnostic.get_virt_text_chunks()* +get_virt_text_chunks({line_diags}, {opts}) + Get virtual text chunks to display using + |nvim_buf_set_extmark()|. + + Parameters: ~ + {line_diags} table The diagnostics associated with the + line. + {opts} table|nil Configuration table with the + following keys: + • prefix: (string) Prefix to display before + virtual text on line. + • spacing: (number) Number of spaces to + insert before virtual text. + + Return: ~ + an array of [text, hl_group] arrays. This can be passed + directly to the {virt_text} option of + |nvim_buf_set_extmark()|. + +goto_next({opts}) *vim.diagnostic.goto_next()* + Move to the next diagnostic. + + Parameters: ~ + {opts} table|nil Configuration table with the following + keys: + • namespace: (number) Only consider diagnostics + from the given namespace. + • cursor_position: (cursor position) Cursor + position as a (row, col) tuple. See + |nvim_win_get_cursor()|. Defaults to the current + cursor position. + • wrap: (boolean, default true) Whether to loop + around file or not. Similar to 'wrapscan'. + • severity: See |diagnostic-severity|. + • enable_popup: (boolean, default true) Call + |vim.diagnostic.show_line_diagnostics()| on + jump. + • popup_opts: (table) Table to pass as {opts} + parameter to + |vim.diagnostic.show_line_diagnostics()| + • win_id: (number, default 0) Window ID + +goto_prev({opts}) *vim.diagnostic.goto_prev()* + Move to the previous diagnostic in the current buffer. + + Parameters: ~ + {opts} table See |vim.diagnostic.goto_next()| + +hide({namespace}, {bufnr}) *vim.diagnostic.hide()* + Hide currently displayed diagnostics. + + This only clears the decorations displayed in the buffer. + Diagnostics can be redisplayed with |vim.diagnostic.show()|. + To completely remove diagnostics, use + |vim.diagnostic.reset()|. + + To hide diagnostics and prevent them from re-displaying, use + |vim.diagnostic.disable()|. + + Parameters: ~ + {namespace} number The diagnostic namespace + {bufnr} number|nil Buffer number. Defaults to the + current buffer. + +reset({namespace}, {bufnr}) *vim.diagnostic.reset()* + Remove all diagnostics from the given namespace. + + Unlike |vim.diagnostic.hide()|, this function removes all + saved diagnostics. They cannot be redisplayed using + |vim.diagnostic.show()|. To simply remove diagnostic + decorations in a way that they can be re-displayed, use + |vim.diagnostic.hide()|. + + Parameters: ~ + {namespace} number + {bufnr} number|nil Remove diagnostics for the given + buffer. When omitted, diagnostics are removed + for all buffers. + +set({namespace}, {bufnr}, {diagnostics}, {opts}) *vim.diagnostic.set()* + Set diagnostics for the given namespace and buffer. + + Parameters: ~ + {namespace} number The diagnostic namespace + {bufnr} number Buffer number + {diagnostics} table A list of diagnostic items + |diagnostic-structure| + {opts} table|nil Display options to pass to + |vim.diagnostic.show()| + +setloclist({opts}) *vim.diagnostic.setloclist()* + Add buffer diagnostics to the location list. + + Parameters: ~ + {opts} table|nil Configuration table with the following + keys: + • namespace: (number) Only add diagnostics from + the given namespace. + • winnr: (number, default 0) Window number to set + location list for. + • open: (boolean, default true) Open the location + list after setting. + • title: (string) Title of the location list. + Defaults to "Diagnostics". + • severity: See |diagnostic-severity|. + +setqflist({opts}) *vim.diagnostic.setqflist()* + Add all diagnostics to the quickfix list. + + Parameters: ~ + {opts} table|nil Configuration table with the following + keys: + • namespace: (number) Only add diagnostics from + the given namespace. + • open: (boolean, default true) Open quickfix list + after setting. + • title: (string) Title of quickfix list. Defaults + to "Diagnostics". + • severity: See |diagnostic-severity|. + + *vim.diagnostic.show()* +show({namespace}, {bufnr}, {diagnostics}, {opts}) + Display diagnostics for the given namespace and buffer. + + Parameters: ~ + {namespace} number Diagnostic namespace + {bufnr} number|nil Buffer number. Defaults to the + current buffer. + {diagnostics} table|nil The diagnostics to display. When + omitted, use the saved diagnostics for the + given namespace and buffer. This can be + used to display a list of diagnostics + without saving them or to display only a + subset of diagnostics. + {opts} table|nil Display options. See + |vim.diagnostic.config()|. + + *vim.diagnostic.show_line_diagnostics()* +show_line_diagnostics({opts}, {bufnr}, {lnum}) + Open a floating window with the diagnostics from the given + line. + + Parameters: ~ + {opts} table Configuration table. See + |vim.diagnostic.show_position_diagnostics()|. + {bufnr} number|nil Buffer number. Defaults to the current + buffer. + {lnum} number|nil Line number. Defaults to line number + of cursor. + + Return: ~ + A ({popup_bufnr}, {win_id}) tuple + + *vim.diagnostic.show_position_diagnostics()* +show_position_diagnostics({opts}, {bufnr}, {position}) + Open a floating window with the diagnostics at the given + position. + + Parameters: ~ + {opts} table|nil Configuration table with the same + keys as |vim.lsp.util.open_floatin_preview()| + in addition to the following: + • namespace: (number) Limit diagnostics to the + given namespace + • severity: See |diagnostic-severity|. + • show_header: (boolean, default true) Show + "Diagnostics:" header + {bufnr} number|nil Buffer number. Defaults to the + current buffer. + {position} table|nil The (0,0)-indexed position. Defaults + to the current cursor position. + + Return: ~ + A ({popup_bufnr}, {win_id}) tuple + + vim:tw=78:ts=8:ft=help:norl: diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index e76e224596..48d65a22b6 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -424,121 +424,6 @@ LspReferenceRead used for highlighting "read" references LspReferenceWrite used for highlighting "write" references - *lsp-highlight-diagnostics* -All highlights defined for diagnostics begin with `LspDiagnostics` followed by -the type of highlight (e.g., `Sign`, `Underline`, etc.) and then the Severity -of the highlight (e.g. `Error`, `Warning`, etc.) - -Sign, underline and virtual text highlights (by default) are linked to their -corresponding LspDiagnosticsDefault highlight. - -For example, the default highlighting for |hl-LspDiagnosticsSignError| is -linked to |hl-LspDiagnosticsDefaultError|. To change the default (and -therefore the linked highlights), use the |:highlight| command: > - - highlight LspDiagnosticsDefaultError guifg="BrightRed" -< - - *hl-LspDiagnosticsDefaultError* -LspDiagnosticsDefaultError - Used as the base highlight group. - Other LspDiagnostic highlights link to this by default (except Underline) - - *hl-LspDiagnosticsDefaultWarning* -LspDiagnosticsDefaultWarning - Used as the base highlight group. - Other LspDiagnostic highlights link to this by default (except Underline) - - *hl-LspDiagnosticsDefaultInformation* -LspDiagnosticsDefaultInformation - Used as the base highlight group. - Other LspDiagnostic highlights link to this by default (except Underline) - - *hl-LspDiagnosticsDefaultHint* -LspDiagnosticsDefaultHint - Used as the base highlight group. - Other LspDiagnostic highlights link to this by default (except Underline) - - *hl-LspDiagnosticsVirtualTextError* -LspDiagnosticsVirtualTextError - Used for "Error" diagnostic virtual text. - See |vim.lsp.diagnostic.set_virtual_text()| - - *hl-LspDiagnosticsVirtualTextWarning* -LspDiagnosticsVirtualTextWarning - Used for "Warning" diagnostic virtual text. - See |vim.lsp.diagnostic.set_virtual_text()| - - *hl-LspDiagnosticsVirtualTextInformation* -LspDiagnosticsVirtualTextInformation - Used for "Information" diagnostic virtual text. - See |vim.lsp.diagnostic.set_virtual_text()| - - *hl-LspDiagnosticsVirtualTextHint* -LspDiagnosticsVirtualTextHint - Used for "Hint" diagnostic virtual text. - See |vim.lsp.diagnostic.set_virtual_text()| - - *hl-LspDiagnosticsUnderlineError* -LspDiagnosticsUnderlineError - Used to underline "Error" diagnostics. - See |vim.lsp.diagnostic.set_underline()| - - *hl-LspDiagnosticsUnderlineWarning* -LspDiagnosticsUnderlineWarning - Used to underline "Warning" diagnostics. - See |vim.lsp.diagnostic.set_underline()| - - *hl-LspDiagnosticsUnderlineInformation* -LspDiagnosticsUnderlineInformation - Used to underline "Information" diagnostics. - See |vim.lsp.diagnostic.set_underline()| - - *hl-LspDiagnosticsUnderlineHint* -LspDiagnosticsUnderlineHint - Used to underline "Hint" diagnostics. - See |vim.lsp.diagnostic.set_underline()| - - *hl-LspDiagnosticsFloatingError* -LspDiagnosticsFloatingError - Used to color "Error" diagnostic messages in diagnostics float. - See |vim.lsp.diagnostic.show_line_diagnostics()| - - *hl-LspDiagnosticsFloatingWarning* -LspDiagnosticsFloatingWarning - Used to color "Warning" diagnostic messages in diagnostics float. - See |vim.lsp.diagnostic.show_line_diagnostics()| - - *hl-LspDiagnosticsFloatingInformation* -LspDiagnosticsFloatingInformation - Used to color "Information" diagnostic messages in diagnostics float. - See |vim.lsp.diagnostic.show_line_diagnostics()| - - *hl-LspDiagnosticsFloatingHint* -LspDiagnosticsFloatingHint - Used to color "Hint" diagnostic messages in diagnostics float. - See |vim.lsp.diagnostic.show_line_diagnostics()| - - *hl-LspDiagnosticsSignError* -LspDiagnosticsSignError - Used for "Error" signs in sign column. - See |vim.lsp.diagnostic.set_signs()| - - *hl-LspDiagnosticsSignWarning* -LspDiagnosticsSignWarning - Used for "Warning" signs in sign column. - See |vim.lsp.diagnostic.set_signs()| - - *hl-LspDiagnosticsSignInformation* -LspDiagnosticsSignInformation - Used for "Information" signs in sign column. - See |vim.lsp.diagnostic.set_signs()| - - *hl-LspDiagnosticsSignHint* -LspDiagnosticsSignHint - Used for "Hint" signs in sign column. - See |vim.lsp.diagnostic.set_signs()| - *lsp-highlight-codelens* Highlight groups related to |lsp-codelens| functionality. @@ -560,13 +445,6 @@ LspSignatureActiveParameter Used to highlight the active parameter in the signature help. See |vim.lsp.handlers.signature_help()|. -============================================================================== -AUTOCOMMANDS *lsp-autocommands* - - *LspDiagnosticsChanged* -LspDiagnosticsChanged After receiving publishDiagnostics server response - - ============================================================================== Lua module: vim.lsp *lsp-core* @@ -1207,220 +1085,20 @@ workspace_symbol({query}) *vim.lsp.buf.workspace_symbol()* ============================================================================== Lua module: vim.lsp.diagnostic *lsp-diagnostic* - *vim.lsp.diagnostic.clear()* -clear({bufnr}, {client_id}, {diagnostic_ns}, {sign_ns}) - Clears the currently displayed diagnostics +get_namespace({client_id}) *vim.lsp.diagnostic.get_namespace()* + Get the diagnostic namespace associated with an LSP client + |vim.diagnostic|. Parameters: ~ - {bufnr} number The buffer number - {client_id} number the client id - {diagnostic_ns} number|nil Associated diagnostic - namespace - {sign_ns} number|nil Associated sign namespace - -disable({bufnr}, {client_id}) *vim.lsp.diagnostic.disable()* - Disable diagnostics for the given buffer and client - - Parameters: ~ - {bufnr} (optional, number): Buffer handle, defaults - to current - {client_id} (optional, number): Disable diagnostics for - the given client. The default is to disable - diagnostics for all attached clients. - -enable({bufnr}, {client_id}) *vim.lsp.diagnostic.enable()* - Enable diagnostics for the given buffer and client - - Parameters: ~ - {bufnr} (optional, number): Buffer handle, defaults - to current - {client_id} (optional, number): Enable diagnostics for - the given client. The default is to enable - diagnostics for all attached clients. - -get({bufnr}, {client_id}, {predicate}) *vim.lsp.diagnostic.get()* - Return associated diagnostics for bufnr - - Parameters: ~ - {bufnr} number - {client_id} number|nil If nil, then return all of the - diagnostics. Else, return just the - diagnostics associated with the client_id. - {predicate} function|nil Optional function for filtering - diagnostics - -get_all({client_id}) *vim.lsp.diagnostic.get_all()* - Get all diagnostics for clients - - Parameters: ~ - {client_id} number Restrict included diagnostics to the - client If nil, diagnostics of all clients are - included. - - Return: ~ - table with diagnostics grouped by bufnr (bufnr:Diagnostic[]) - - *vim.lsp.diagnostic.get_count()* -get_count({bufnr}, {severity}, {client_id}) - Get the counts for a particular severity - - Useful for showing diagnostic counts in statusline. eg: -> - - function! LspStatus() abort - let sl = '' - if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))') - let sl.='%#MyStatuslineLSP#E:' - let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Error]])")}' - let sl.='%#MyStatuslineLSP# W:' - let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Warning]])")}' - else - let sl.='%#MyStatuslineLSPErrors#off' - endif - return sl - endfunction - let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus() -< - - Parameters: ~ - {bufnr} number The buffer number - {severity} DiagnosticSeverity - {client_id} number the client id - - *vim.lsp.diagnostic.get_line_diagnostics()* -get_line_diagnostics({bufnr}, {line_nr}, {opts}, {client_id}) - Get the diagnostics by line - - Parameters: ~ - {bufnr} number|nil The buffer number - {line_nr} number|nil The line number - {opts} table|nil Configuration keys - • severity: (DiagnosticSeverity, default nil) - • Only return diagnostics with this - severity. Overrides severity_limit - - • severity_limit: (DiagnosticSeverity, default nil) - • Limit severity of diagnostics found. - E.g. "Warning" means { "Error", - "Warning" } will be valid. - {client_id|nil} number the client id - - Return: ~ - table Table with map of line number to list of - diagnostics. - -get_next({opts}) *vim.lsp.diagnostic.get_next()* - Get the next diagnostic closest to the cursor_position - - Parameters: ~ - {opts} table See |vim.lsp.diagnostic.goto_next()| - - Return: ~ - table Next diagnostic - -get_next_pos({opts}) *vim.lsp.diagnostic.get_next_pos()* - Return the pos, {row, col}, for the next diagnostic in the - current buffer. - - Parameters: ~ - {opts} table See |vim.lsp.diagnostic.goto_next()| - - Return: ~ - table Next diagnostic position - -get_prev({opts}) *vim.lsp.diagnostic.get_prev()* - Get the previous diagnostic closest to the cursor_position - - Parameters: ~ - {opts} table See |vim.lsp.diagnostic.goto_next()| - - Return: ~ - table Previous diagnostic - -get_prev_pos({opts}) *vim.lsp.diagnostic.get_prev_pos()* - Return the pos, {row, col}, for the prev diagnostic in the - current buffer. - - Parameters: ~ - {opts} table See |vim.lsp.diagnostic.goto_next()| - - Return: ~ - table Previous diagnostic position - - *vim.lsp.diagnostic.get_virtual_text_chunks_for_line()* -get_virtual_text_chunks_for_line({bufnr}, {line}, {line_diags}, {opts}) - Default function to get text chunks to display using - |nvim_buf_set_extmark()|. - - Parameters: ~ - {bufnr} number The buffer to display the virtual - text in - {line} number The line number to display the - virtual text on - {line_diags} Diagnostic [] The diagnostics associated with the line - {opts} table See {opts} from - |vim.lsp.diagnostic.set_virtual_text()| - - Return: ~ - an array of [text, hl_group] arrays. This can be passed - directly to the {virt_text} option of - |nvim_buf_set_extmark()|. - -goto_next({opts}) *vim.lsp.diagnostic.goto_next()* - Move to the next diagnostic - - Parameters: ~ - {opts} table|nil Configuration table. Keys: - • {client_id}: (number) - • If nil, will consider all clients attached to - buffer. - - • {cursor_position}: (Position, default current - position) - • See |nvim_win_get_cursor()| - - • {wrap}: (boolean, default true) - • Whether to loop around file or not. Similar to - 'wrapscan' - - • {severity}: (DiagnosticSeverity) - • Exclusive severity to consider. Overrides - {severity_limit} - - • {severity_limit}: (DiagnosticSeverity) - • Limit severity of diagnostics found. E.g. - "Warning" means { "Error", "Warning" } will be - valid. - - • {enable_popup}: (boolean, default true) - • Call - |vim.lsp.diagnostic.show_line_diagnostics()| - on jump - - • {popup_opts}: (table) - • Table to pass as {opts} parameter to - |vim.lsp.diagnostic.show_line_diagnostics()| - - • {win_id}: (number, default 0) - • Window ID - -goto_prev({opts}) *vim.lsp.diagnostic.goto_prev()* - Move to the previous diagnostic - - Parameters: ~ - {opts} table See |vim.lsp.diagnostic.goto_next()| + {client_id} number The id of the LSP client *vim.lsp.diagnostic.on_publish_diagnostics()* on_publish_diagnostics({_}, {result}, {ctx}, {config}) |lsp-handler| for the method "textDocument/publishDiagnostics" - Note: - Each of the configuration options accepts: - • `false` : Disable this feature - • `true` : Enable this feature, use default settings. - • `table` : Enable this feature, use overrides. - • `function`: Function with signature (bufnr, client_id) that - returns any of the above.> + See |vim.diagnostic.config()| for configuration options. + Handler-specific configuration can be set using + |vim.lsp.with()|: > vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( vim.lsp.diagnostic.on_publish_diagnostics, { @@ -1442,229 +1120,8 @@ on_publish_diagnostics({_}, {result}, {ctx}, {config}) < Parameters: ~ - {config} table Configuration table. - • underline: (default=true) - • Apply underlines to diagnostics. - • See |vim.lsp.diagnostic.set_underline()| - - • virtual_text: (default=true) - • Apply virtual text to line endings. - • See |vim.lsp.diagnostic.set_virtual_text()| - - • signs: (default=true) - • Apply signs for diagnostics. - • See |vim.lsp.diagnostic.set_signs()| - - • update_in_insert: (default=false) - • Update diagnostics in InsertMode or wait - until InsertLeave - - • severity_sort: (default=false) - • Sort diagnostics (and thus signs and virtual - text) - -redraw({bufnr}, {client_id}) *vim.lsp.diagnostic.redraw()* - Redraw diagnostics for the given buffer and client - - This calls the "textDocument/publishDiagnostics" handler - manually using the cached diagnostics already received from - the server. This can be useful for redrawing diagnostics after - making changes in diagnostics configuration. - |lsp-handler-configuration| - - Parameters: ~ - {bufnr} (optional, number): Buffer handle, defaults - to current - {client_id} (optional, number): Redraw diagnostics for - the given client. The default is to redraw - diagnostics for all attached clients. - -reset({client_id}, {buffer_client_map}) *vim.lsp.diagnostic.reset()* - Clear diagnotics and diagnostic cache - - Handles saving diagnostics from multiple clients in the same - buffer. - - Parameters: ~ - {client_id} number - {buffer_client_map} table map of buffers to active - clients - -save({diagnostics}, {bufnr}, {client_id}) *vim.lsp.diagnostic.save()* - Save diagnostics to the current buffer. - - Handles saving diagnostics from multiple clients in the same - buffer. - - Parameters: ~ - {diagnostics} Diagnostic [] - {bufnr} number - {client_id} number - -set_loclist({opts}) *vim.lsp.diagnostic.set_loclist()* - Sets the location list - - Parameters: ~ - {opts} table|nil Configuration table. Keys: - • {open}: (boolean, default true) - • Open loclist after set - - • {client_id}: (number) - • If nil, will consider all clients attached to - buffer. - - • {severity}: (DiagnosticSeverity) - • Exclusive severity to consider. Overrides - {severity_limit} - - • {severity_limit}: (DiagnosticSeverity) - • Limit severity of diagnostics found. E.g. - "Warning" means { "Error", "Warning" } will be - valid. - - • {workspace}: (boolean, default false) - • Set the list with workspace diagnostics - -set_qflist({opts}) *vim.lsp.diagnostic.set_qflist()* - Sets the quickfix list - - Parameters: ~ - {opts} table|nil Configuration table. Keys: - • {open}: (boolean, default true) - • Open quickfix list after set - - • {client_id}: (number) - • If nil, will consider all clients attached to - buffer. - - • {severity}: (DiagnosticSeverity) - • Exclusive severity to consider. Overrides - {severity_limit} - - • {severity_limit}: (DiagnosticSeverity) - • Limit severity of diagnostics found. E.g. - "Warning" means { "Error", "Warning" } will be - valid. - - • {workspace}: (boolean, default true) - • Set the list with workspace diagnostics - - *vim.lsp.diagnostic.set_signs()* -set_signs({diagnostics}, {bufnr}, {client_id}, {sign_ns}, {opts}) - Set signs for given diagnostics - - Sign characters can be customized with the following commands: -> - - sign define LspDiagnosticsSignError text=E texthl=LspDiagnosticsSignError linehl= numhl= - sign define LspDiagnosticsSignWarning text=W texthl=LspDiagnosticsSignWarning linehl= numhl= - sign define LspDiagnosticsSignInformation text=I texthl=LspDiagnosticsSignInformation linehl= numhl= - sign define LspDiagnosticsSignHint text=H texthl=LspDiagnosticsSignHint linehl= numhl= -< - - Parameters: ~ - {diagnostics} Diagnostic [] - {bufnr} number The buffer number - {client_id} number the client id - {sign_ns} number|nil - {opts} table Configuration for signs. Keys: - • priority: Set the priority of the signs. - • severity_limit (DiagnosticSeverity): - • Limit severity of diagnostics found. - E.g. "Warning" means { "Error", - "Warning" } will be valid. - - *vim.lsp.diagnostic.set_underline()* -set_underline({diagnostics}, {bufnr}, {client_id}, {diagnostic_ns}, {opts}) - Set underline for given diagnostics - - Underline highlights can be customized by changing the - following |:highlight| groups. -> - - LspDiagnosticsUnderlineError - LspDiagnosticsUnderlineWarning - LspDiagnosticsUnderlineInformation - LspDiagnosticsUnderlineHint -< - - Parameters: ~ - {diagnostics} Diagnostic [] - {bufnr} number: The buffer number - {client_id} number: The client id - {diagnostic_ns} number|nil: The namespace - {opts} table: Configuration table: - • severity_limit (DiagnosticSeverity): - • Limit severity of diagnostics found. - E.g. "Warning" means { "Error", - "Warning" } will be valid. - - *vim.lsp.diagnostic.set_virtual_text()* -set_virtual_text({diagnostics}, {bufnr}, {client_id}, {diagnostic_ns}, {opts}) - Set virtual text given diagnostics - - Virtual text highlights can be customized by changing the - following |:highlight| groups. -> - - LspDiagnosticsVirtualTextError - LspDiagnosticsVirtualTextWarning - LspDiagnosticsVirtualTextInformation - LspDiagnosticsVirtualTextHint -< - - Parameters: ~ - {diagnostics} Diagnostic [] - {bufnr} number - {client_id} number - {diagnostic_ns} number - {opts} table Options on how to display virtual - text. Keys: - • prefix (string): Prefix to display - before virtual text on line - • spacing (number): Number of spaces to - insert before virtual text - • severity_limit (DiagnosticSeverity): - • Limit severity of diagnostics found. - E.g. "Warning" means { "Error", - "Warning" } will be valid. - - *vim.lsp.diagnostic.show_line_diagnostics()* -show_line_diagnostics({opts}, {buf_nr}, {line_nr}, {client_id}) - Parameters: ~ - {opts} table Configuration table - • all opts for - |vim.lsp.diagnostic.get_line_diagnostics()| - and |show_diagnostics()| can be used here - {buf_nr} number|nil The buffer number - {line_nr} number|nil The line number - {client_id} number|nil the client id - - Return: ~ - table {popup_bufnr, win_id} - - *vim.lsp.diagnostic.show_position_diagnostics()* -show_position_diagnostics({opts}, {buf_nr}, {position}) - Open a floating window with the diagnostics from {position} - - Parameters: ~ - {opts} table|nil Configuration keys - • severity: (DiagnosticSeverity, default nil) - • Only return diagnostics with this - severity. Overrides severity_limit - - • severity_limit: (DiagnosticSeverity, default nil) - • Limit severity of diagnostics found. E.g. - "Warning" means { "Error", "Warning" } - will be valid. - - • all opts for |show_diagnostics()| can be - used here - {buf_nr} number|nil The buffer number - {position} table|nil The (0,0)-indexed position - - Return: ~ - table {popup_bufnr, win_id} + {config} table Configuration table (see + |vim.diagnostic.config()|). ============================================================================== @@ -1903,21 +1360,6 @@ create_file({change}) *vim.lsp.util.create_file()* delete_file({change}) *vim.lsp.util.delete_file()* TODO: Documentation - *vim.lsp.util.diagnostics_to_items()* -diagnostics_to_items({diagnostics_by_bufnr}, {predicate}) - Convert diagnostics grouped by bufnr to a list of items for - use in the quickfix or location list. - - Parameters: ~ - {diagnostics_by_bufnr} table bufnr -> Diagnostic [] - {predicate} an optional function to filter the - diagnostics. If present, only - diagnostic items matching will be - included. - - Return: ~ - table (A list of items) - *vim.lsp.util.extract_completion_items()* extract_completion_items({result}) Can be used to extract the completion items from a `textDocument/completion` request, which may return one of `CompletionItem[]` , `CompletionList` or null. @@ -1982,6 +1424,9 @@ locations_to_items({locations}) *vim.lsp.util.locations_to_items()* and in sorted order, for display in quickfix and location lists. + The result can be passed to the {list} argument of + |setqflist()| or |setloclist()|. + Parameters: ~ {locations} (table) list of `Location` s or `LocationLink` s @@ -2166,21 +1611,6 @@ set_lines({lines}, {A}, {B}, {new_lines}) *vim.lsp.util.set_lines()* Return: ~ (table) The modified {lines} object -set_loclist({items}, {win_id}) *vim.lsp.util.set_loclist()* - Fills target window's location list with given list of items. - Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. - Defaults to current window. - - Parameters: ~ - {items} (table) list of items - -set_qflist({items}) *vim.lsp.util.set_qflist()* - Fills quickfix list with given list of items. Can be obtained - with e.g. |vim.lsp.util.locations_to_items()|. - - Parameters: ~ - {items} (table) list of items - *vim.lsp.util.stylize_markdown()* stylize_markdown({bufnr}, {contents}, {opts}) Converts markdown into syntax highlighted regions by stripping diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua new file mode 100644 index 0000000000..33fa07ef4c --- /dev/null +++ b/runtime/lua/vim/diagnostic.lua @@ -0,0 +1,1150 @@ +local M = {} + +M.severity = { + ERROR = 1, + WARN = 2, + INFO = 3, + HINT = 4, +} + +vim.tbl_add_reverse_lookup(M.severity) + +local global_diagnostic_options = { + signs = true, + underline = true, + virtual_text = true, + update_in_insert = false, + severity_sort = false, +} + +-- Local functions {{{ + +---@private +local function to_severity(severity) + return type(severity) == 'string' and M.severity[string.upper(severity)] or severity +end + +---@private +local function filter_by_severity(severity, diagnostics) + if not severity then + return diagnostics + end + + if type(severity) ~= "table" then + severity = to_severity(severity) + return vim.tbl_filter(function(t) return t.severity == severity end, diagnostics) + end + + local min_severity = to_severity(severity.min) or M.severity.HINT + local max_severity = to_severity(severity.max) or M.severity.ERROR + + return vim.tbl_filter(function(t) return t.severity <= min_severity and t.severity >= max_severity end, diagnostics) +end + +---@private +local function resolve_optional_value(option, namespace, bufnr) + local enabled_val = {} + + if not option then + return false + elseif option == true then + return enabled_val + elseif type(option) == 'function' then + local val = option(namespace, bufnr) + if val == true then + return enabled_val + else + return val + end + elseif type(option) == 'table' then + return option + else + error("Unexpected option type: " .. vim.inspect(option)) + end +end + +local all_namespaces = {} + +---@private +local function get_namespace(ns) + if not all_namespaces[ns] then + local name + for k, v in pairs(vim.api.nvim_get_namespaces()) do + if ns == v then + name = k + break + end + end + + if not name then + return vim.notify("namespace does not exist or is anonymous", vim.log.levels.ERROR) + end + + all_namespaces[ns] = { + name = name, + sign_group = string.format("vim.diagnostic.%s", name), + opts = {} + } + end + return all_namespaces[ns] +end + +---@private +local function get_resolved_options(opts, namespace, bufnr) + local ns = get_namespace(namespace) + local resolved = vim.tbl_extend('keep', opts or {}, ns.opts, global_diagnostic_options) + for k in pairs(global_diagnostic_options) do + if resolved[k] ~= nil then + resolved[k] = resolve_optional_value(resolved[k], namespace, bufnr) + end + end + return resolved +end + +-- Default diagnostic highlights +local diagnostic_severities = { + [M.severity.ERROR] = { ctermfg = 1, guifg = "Red" }; + [M.severity.WARN] = { ctermfg = 3, guifg = "Orange" }; + [M.severity.INFO] = { ctermfg = 4, guifg = "LightBlue" }; + [M.severity.HINT] = { ctermfg = 7, guifg = "LightGrey" }; +} + +-- Make a map from DiagnosticSeverity -> Highlight Name +---@private +local function make_highlight_map(base_name) + local result = {} + for k in pairs(diagnostic_severities) do + local name = M.severity[k] + name = name:sub(1, 1) .. name:sub(2):lower() + result[k] = "Diagnostic" .. base_name .. name + end + + return result +end + +local virtual_text_highlight_map = make_highlight_map("VirtualText") +local underline_highlight_map = make_highlight_map("Underline") +local floating_highlight_map = make_highlight_map("Floating") +local sign_highlight_map = make_highlight_map("Sign") + +---@private +local define_default_signs = (function() + local signs_defined = false + return function() + if signs_defined then + return + end + + for severity, sign_hl_name in pairs(sign_highlight_map) do + local severity_name = M.severity[severity] + vim.fn.sign_define(sign_hl_name, { + text = (severity_name or 'U'):sub(1, 1), + texthl = sign_hl_name, + linehl = '', + numhl = '', + }) + end + + signs_defined = true + end +end)() + +---@private +local function get_bufnr(bufnr) + if not bufnr or bufnr == 0 then + return vim.api.nvim_get_current_buf() + end + return bufnr +end + +-- Metatable that automatically creates an empty table when assigning to a missing key +local bufnr_and_namespace_cacher_mt = { + __index = function(t, bufnr) + if not bufnr or bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + + if rawget(t, bufnr) == nil then + rawset(t, bufnr, {}) + end + + return rawget(t, bufnr) + end, + + __newindex = function(t, bufnr, v) + if not bufnr or bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + + rawset(t, bufnr, v) + end, +} + +local diagnostic_cleanup = setmetatable({}, bufnr_and_namespace_cacher_mt) +local diagnostic_cache = setmetatable({}, bufnr_and_namespace_cacher_mt) +local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt) +local diagnostic_attached_buffers = {} +local diagnostic_disabled = {} +local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt) + +---@private +local function is_disabled(namespace, bufnr) + if type(diagnostic_disabled[bufnr]) == "table" then + return diagnostic_disabled[bufnr][namespace] + end + return diagnostic_disabled[bufnr] +end + +---@private +local function diagnostic_lines(diagnostics) + if not diagnostics then + return + end + + local diagnostics_by_line = {} + for _, diagnostic in ipairs(diagnostics) do + local line_diagnostics = diagnostics_by_line[diagnostic.lnum] + if not line_diagnostics then + line_diagnostics = {} + diagnostics_by_line[diagnostic.lnum] = line_diagnostics + end + table.insert(line_diagnostics, diagnostic) + end + return diagnostics_by_line +end + +---@private +local function set_diagnostic_cache(namespace, diagnostics, bufnr) + local buf_line_count = vim.api.nvim_buf_line_count(bufnr) + for _, diagnostic in ipairs(diagnostics) do + if diagnostic.severity == nil then + diagnostic.severity = M.severity.ERROR + end + + diagnostic.namespace = namespace + diagnostic.bufnr = bufnr + + if buf_line_count > 0 then + diagnostic.lnum = math.max(math.min( + diagnostic.lnum, buf_line_count - 1 + ), 0) + diagnostic.end_lnum = math.max(math.min( + diagnostic.end_lnum, buf_line_count - 1 + ), 0) + end + end + + diagnostic_cache[bufnr][namespace] = diagnostics +end + +---@private +local function clear_diagnostic_cache(namespace, bufnr) + diagnostic_cache[bufnr][namespace] = nil +end + +---@private +local function restore_extmarks(bufnr, last) + for ns, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do + local extmarks_current = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true}) + local found = {} + for _, extmark in ipairs(extmarks_current) do + -- nvim_buf_set_lines will move any extmark to the line after the last + -- nvim_buf_set_text will move any extmark to the last line + if extmark[2] ~= last + 1 then + found[extmark[1]] = true + end + end + for _, extmark in ipairs(extmarks) do + if not found[extmark[1]] then + local opts = extmark[4] + opts.id = extmark[1] + -- HACK: end_row should be end_line + if opts.end_row then + opts.end_line = opts.end_row + opts.end_row = nil + end + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts) + end + end + end +end + +---@private +local function save_extmarks(namespace, bufnr) + bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr + if not diagnostic_attached_buffers[bufnr] then + vim.api.nvim_buf_attach(bufnr, false, { + on_lines = function(_, _, _, _, _, last) + restore_extmarks(bufnr, last - 1) + end, + on_detach = function() + diagnostic_cache_extmarks[bufnr] = nil + end}) + diagnostic_attached_buffers[bufnr] = true + end + diagnostic_cache_extmarks[bufnr][namespace] = vim.api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, {details = true}) +end + +local registered_autocmds = {} + +---@private +local function make_augroup_key(namespace, bufnr) + local ns = get_namespace(namespace) + return string.format("DiagnosticInsertLeave:%s:%s", bufnr, ns.name) +end + +--- Table of autocmd events to fire the update for displaying new diagnostic information +local insert_leave_auto_cmds = { "InsertLeave", "CursorHoldI" } + +---@private +local function schedule_display(namespace, bufnr, args) + bufs_waiting_to_update[bufnr][namespace] = args + + local key = make_augroup_key(namespace, bufnr) + if not registered_autocmds[key] then + vim.cmd(string.format("augroup %s", key)) + vim.cmd(" au!") + vim.cmd( + string.format( + [[autocmd %s lua vim.diagnostic._execute_scheduled_display(%s, %s)]], + table.concat(insert_leave_auto_cmds, ","), + bufnr, + namespace, + bufnr + ) + ) + vim.cmd("augroup END") + + registered_autocmds[key] = true + end +end + +---@private +local function clear_scheduled_display(namespace, bufnr) + local key = make_augroup_key(namespace, bufnr) + + if registered_autocmds[key] then + vim.cmd(string.format("augroup %s", key)) + vim.cmd(" au!") + vim.cmd("augroup END") + + registered_autocmds[key] = nil + end +end + +---@private +--- Open a floating window with the provided diagnostics +---@param opts table Configuration table +--- - show_header (boolean, default true): Show "Diagnostics:" header +--- - all opts for |vim.util.open_floating_preview()| can be used here +---@param diagnostics table: The diagnostics to display +---@return table {popup_bufnr, win_id} +local function show_diagnostics(opts, diagnostics) + if vim.tbl_isempty(diagnostics) then return end + local lines = {} + local highlights = {} + local show_header = vim.F.if_nil(opts.show_header, true) + if show_header then + table.insert(lines, "Diagnostics:") + table.insert(highlights, {0, "Bold"}) + end + + for i, diagnostic in ipairs(diagnostics) do + local prefix = string.format("%d. ", i) + local hiname = floating_highlight_map[diagnostic.severity] + assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity)) + + local message_lines = vim.split(diagnostic.message, '\n', true) + table.insert(lines, prefix..message_lines[1]) + table.insert(highlights, {#prefix, hiname}) + for j = 2, #message_lines do + table.insert(lines, string.rep(' ', #prefix) .. message_lines[j]) + table.insert(highlights, {0, hiname}) + end + end + + local popup_bufnr, winnr = require('vim.lsp.util').open_floating_preview(lines, 'plaintext', opts) + for i, hi in ipairs(highlights) do + local prefixlen, hiname = unpack(hi) + -- Start highlight after the prefix + vim.api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1) + end + + return popup_bufnr, winnr +end + +local errlist_type_map = { + [M.severity.ERROR] = 'E', + [M.severity.WARN] = 'W', + [M.severity.INFO] = 'I', + [M.severity.HINT] = 'I', +} + +---@private +local function diagnostics_to_list_items(diagnostics) + local items = {} + for _, d in pairs(diagnostics) do + table.insert(items, { + bufnr = d.bufnr, + lnum = d.lnum + 1, + col = d.col + 1, + text = d.message, + type = errlist_type_map[d.severity or M.severity.ERROR] or 'E' + }) + end + table.sort(items, function(a, b) + if a.bufnr == b.bufnr then + return a.lnum < b.lnum + else + return a.bufnr < b.bufnr + end + end) + return items +end + +---@private +local function set_list(loclist, opts) + opts = opts or {} + local open = vim.F.if_nil(opts.open, true) + local title = opts.title or "Diagnostics" + local winnr = opts.winnr or 0 + local diagnostics = M.get(loclist and vim.api.nvim_win_get_buf(winnr), opts) + local items = diagnostics_to_list_items(diagnostics) + if loclist then + vim.fn.setloclist(winnr, {}, ' ', { title = title, items = items }) + else + vim.fn.setqflist({}, ' ', { title = title, items = items }) + end + if open then + vim.api.nvim_command(loclist and "lopen" or "copen") + end +end + +-- }}} + +-- Public API {{{ + +--- Configure diagnostic options globally or for a specific diagnostic +--- namespace. +--- +---@note Each of the configuration options below accepts one of the following: +--- - `false`: Disable this feature +--- - `true`: Enable this feature, use default settings. +--- - `table`: Enable this feature with overrides. +--- - `function`: Function with signature (namespace, bufnr) that returns any of the above. +--- +---@param opts table Configuration table with the following keys: +--- - underline: (default true) Use underline for diagnostics +--- - virtual_text: (default true) Use virtual text for diagnostics +--- - signs: (default true) Use signs for diagnostics +--- - update_in_insert: (default false) Update diagnostics in Insert mode (if false, +--- diagnostics are updated on InsertLeave) +--- - severity_sort: (default false) Sort diagnostics by severity. This affects the order in +--- which signs and virtual text are displayed +---@param namespace number|nil Update the options for the given namespace. When omitted, update the +--- global diagnostic options. +function M.config(opts, namespace) + vim.validate { + opts = { opts, 't' }, + namespace = { namespace, 'n', true }, + } + + local t + if namespace then + local ns = get_namespace(namespace) + t = ns.opts + else + t = global_diagnostic_options + end + + for opt in pairs(global_diagnostic_options) do + if opts[opt] ~= nil then + t[opt] = opts[opt] + end + end + + if namespace then + for bufnr, v in pairs(diagnostic_cache) do + if v[namespace] then + M.show(namespace, bufnr) + end + end + else + for bufnr, v in pairs(diagnostic_cache) do + for ns in pairs(v) do + M.show(ns, bufnr) + end + end + end +end + +--- Set diagnostics for the given namespace and buffer. +--- +---@param namespace number The diagnostic namespace +---@param bufnr number Buffer number +---@param diagnostics table A list of diagnostic items |diagnostic-structure| +---@param opts table|nil Display options to pass to |vim.diagnostic.show()| +function M.set(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } + + if vim.tbl_isempty(diagnostics) then + return M.reset(namespace, bufnr) + end + + if not diagnostic_cleanup[bufnr][namespace] then + diagnostic_cleanup[bufnr][namespace] = true + + -- Clean up our data when the buffer unloads. + vim.api.nvim_buf_attach(bufnr, false, { + on_detach = function(_, b) + clear_diagnostic_cache(b, namespace) + diagnostic_cleanup[b][namespace] = nil + end + }) + end + + set_diagnostic_cache(namespace, diagnostics, bufnr) + + if opts then + M.config(opts, namespace) + end + + if vim.api.nvim_buf_is_loaded(bufnr) then + M.show(namespace, bufnr) + end + + vim.api.nvim_command("doautocmd User DiagnosticsChanged") +end + +--- Get current diagnostics. +--- +---@param bufnr number|nil Buffer number to get diagnistics from. Use 0 for +--- current buffer or nil for all buffers. +---@param opts table|nil A table with the following keys: +--- - namespace: (number) Limit diagnostics to the given namespace. +--- - lnum: (number) Limit diagnostics to the given line number. +--- - severity: See |diagnostic-severity|. +---@return table A list of diagnostic items |diagnostic-structure|. +function M.get(bufnr, opts) + vim.validate { + bufnr = { bufnr, 'n', true }, + opts = { opts, 't', true }, + } + + opts = opts or {} + + local namespace = opts.namespace + local diagnostics = {} + + ---@private + local function add(d) + if not opts.lnum or d.lnum == opts.lnum then + table.insert(diagnostics, d) + end + end + + if namespace == nil and bufnr == nil then + for _, t in pairs(diagnostic_cache) do + for _, v in pairs(t) do + for _, diagnostic in pairs(v) do + add(diagnostic) + end + end + end + elseif namespace == nil then + for iter_namespace in pairs(diagnostic_cache[bufnr]) do + for _, diagnostic in pairs(diagnostic_cache[bufnr][iter_namespace]) do + add(diagnostic) + end + end + elseif bufnr == nil then + for _, t in pairs(diagnostic_cache) do + for _, diagnostic in pairs(t[namespace] or {}) do + add(diagnostic) + end + end + else + for _, diagnostic in pairs(diagnostic_cache[bufnr][namespace] or {}) do + add(diagnostic) + end + end + + if opts.severity then + diagnostics = filter_by_severity(opts.severity, diagnostics) + end + + return diagnostics +end + +-- Diagnostic Movements {{{ + +local next_diagnostic = function(position, search_forward, bufnr, opts, namespace) + position[1] = position[1] - 1 + bufnr = bufnr or vim.api.nvim_get_current_buf() + local wrap = vim.F.if_nil(opts.wrap, true) + local line_count = vim.api.nvim_buf_line_count(bufnr) + opts.namespace = namespace + for i = 0, line_count do + local offset = i * (search_forward and 1 or -1) + local lnum = position[1] + offset + if lnum < 0 or lnum >= line_count then + if not wrap then + return + end + lnum = (lnum + line_count) % line_count + end + opts.lnum = lnum + local line_diagnostics = M.get(bufnr, opts) + if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then + local sort_diagnostics, is_next + if search_forward then + sort_diagnostics = function(a, b) return a.col < b.col end + is_next = function(diagnostic) return diagnostic.col > position[2] end + else + sort_diagnostics = function(a, b) return a.col > b.col end + is_next = function(diagnostic) return diagnostic.col < position[2] end + end + table.sort(line_diagnostics, sort_diagnostics) + if i == 0 then + for _, v in pairs(line_diagnostics) do + if is_next(v) then + return v + end + end + else + return line_diagnostics[1] + end + end + end +end + +---@private +local function diagnostic_move_pos(opts, pos) + opts = opts or {} + + local enable_popup = vim.F.if_nil(opts.enable_popup, true) + local win_id = opts.win_id or vim.api.nvim_get_current_win() + + if not pos then + vim.api.nvim_echo({"No more valid diagnostics to move to", "WarningMsg"}) + return + end + + vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]}) + + if enable_popup then + -- This is a bit weird... I'm surprised that we need to wait til the next tick to do this. + vim.schedule(function() + M.show_position_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id)) + end) + end +end + +--- Get the previous diagnostic closest to the cursor position. +--- +---@param opts table See |vim.diagnostic.goto_next()| +---@return table Previous diagnostic +function M.get_prev(opts) + opts = opts or {} + + local win_id = opts.win_id or vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(win_id) + local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id) + + return next_diagnostic(cursor_position, false, bufnr, opts, opts.namespace) +end + +--- Return the position of the previous diagnostic in the current buffer. +--- +---@param opts table See |vim.diagnostic.goto_next()| +---@return table Previous diagnostic position as a (row, col) tuple. +function M.get_prev_pos(opts) + local prev = M.get_prev(opts) + if not prev then + return false + end + + return {prev.lnum, prev.col} +end + +--- Move to the previous diagnostic in the current buffer. +---@param opts table See |vim.diagnostic.goto_next()| +function M.goto_prev(opts) + return diagnostic_move_pos( + opts, + M.get_prev_pos(opts) + ) +end + +--- Get the next diagnostic closest to the cursor position. +--- +---@param opts table See |vim.diagnostic.goto_next()| +---@return table Next diagnostic +function M.get_next(opts) + opts = opts or {} + + local win_id = opts.win_id or vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(win_id) + local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id) + + return next_diagnostic(cursor_position, true, bufnr, opts, opts.namespace) +end + +--- Return the position of the next diagnostic in the current buffer. +--- +---@param opts table See |vim.diagnostic.goto_next()| +---@return table Next diagnostic position as a (row, col) tuple. +function M.get_next_pos(opts) + local next = M.get_next(opts) + if not next then + return false + end + + return {next.lnum, next.col} +end + +--- Move to the next diagnostic. +--- +---@param opts table|nil Configuration table with the following keys: +--- - namespace: (number) Only consider diagnostics from the given namespace. +--- - cursor_position: (cursor position) Cursor position as a (row, col) tuple. See +--- |nvim_win_get_cursor()|. Defaults to the current cursor position. +--- - wrap: (boolean, default true) Whether to loop around file or not. Similar to 'wrapscan'. +--- - severity: See |diagnostic-severity|. +--- - enable_popup: (boolean, default true) Call |vim.diagnostic.show_line_diagnostics()| +--- on jump. +--- - popup_opts: (table) Table to pass as {opts} parameter to +--- |vim.diagnostic.show_line_diagnostics()| +--- - win_id: (number, default 0) Window ID +function M.goto_next(opts) + return diagnostic_move_pos( + opts, + M.get_next_pos(opts) + ) +end + +-- Diagnostic Setters {{{ + +--- Set signs for given diagnostics. +--- +---@param namespace number The diagnostic namespace +---@param bufnr number Buffer number +---@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the +--- current diagnostics in the given buffer are used. +---@param opts table Configuration table with the following keys: +--- - priority: Set the priority of the signs |sign-priority|. +---@private +function M._set_signs(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } + + bufnr = get_bufnr(bufnr) + opts = get_resolved_options({ signs = opts }, namespace, bufnr).signs + + if opts and opts.severity then + diagnostics = filter_by_severity(opts.severity, diagnostics) + end + + local ns = get_namespace(namespace) + + define_default_signs() + + for _, diagnostic in ipairs(diagnostics) do + vim.fn.sign_place( + 0, + ns.sign_group, + sign_highlight_map[diagnostic.severity], + bufnr, + { + priority = opts and opts.priority, + lnum = diagnostic.lnum + 1 + } + ) + end +end + +--- Set underline for given diagnostics. +--- +---@param namespace number The diagnostic namespace +---@param bufnr number Buffer number +---@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the +--- current diagnostics in the given buffer are used. +---@param opts table Configuration table. Currently unused. +---@private +function M._set_underline(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } + + bufnr = get_bufnr(bufnr) + opts = get_resolved_options({ underline = opts }, namespace, bufnr).underline + + if opts and opts.severity then + diagnostics = filter_by_severity(opts.severity, diagnostics) + end + + for _, diagnostic in ipairs(diagnostics) do + local higroup = underline_highlight_map[diagnostic.severity] + + if higroup == nil then + -- Default to error if we don't have a highlight associated + higroup = underline_highlight_map.Error + end + + vim.highlight.range( + bufnr, + namespace, + higroup, + { diagnostic.lnum, diagnostic.col }, + { diagnostic.end_lnum, diagnostic.end_col } + ) + end +end + +--- Set virtual text for given diagnostics. +--- +---@param namespace number The diagnostic namespace +---@param bufnr number Buffer number +---@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the +--- current diagnostics in the given buffer are used. +---@param opts table|nil Configuration table with the following keys: +--- - prefix: (string) Prefix to display before virtual text on line. +--- - spacing: (number) Number of spaces to insert before virtual text. +---@private +function M._set_virtual_text(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } + + bufnr = get_bufnr(bufnr) + opts = get_resolved_options({ virtual_text = opts }, namespace, bufnr).virtual_text + + local buffer_line_diagnostics = diagnostic_lines(diagnostics) + for line, line_diagnostics in pairs(buffer_line_diagnostics) do + if opts and opts.severity then + line_diagnostics = filter_by_severity(opts.severity, line_diagnostics) + end + local virt_texts = M.get_virt_text_chunks(line_diagnostics, opts) + + if virt_texts then + vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, { + hl_mode = "combine", + virt_text = virt_texts, + }) + end + end +end + +--- Get virtual text chunks to display using |nvim_buf_set_extmark()|. +--- +---@param line_diags table The diagnostics associated with the line. +---@param opts table|nil Configuration table with the following keys: +--- - prefix: (string) Prefix to display before virtual text on line. +--- - spacing: (number) Number of spaces to insert before virtual text. +---@return an array of [text, hl_group] arrays. This can be passed directly to +--- the {virt_text} option of |nvim_buf_set_extmark()|. +function M.get_virt_text_chunks(line_diags, opts) + if #line_diags == 0 then + return nil + end + + opts = opts or {} + local prefix = opts.prefix or "■" + local spacing = opts.spacing or 4 + + -- Create a little more space between virtual text and contents + local virt_texts = {{string.rep(" ", spacing)}} + + for i = 1, #line_diags - 1 do + table.insert(virt_texts, {prefix, virtual_text_highlight_map[line_diags[i].severity]}) + end + local last = line_diags[#line_diags] + + -- TODO(tjdevries): Allow different servers to be shown first somehow? + -- TODO(tjdevries): Display server name associated with these? + if last.message then + table.insert( + virt_texts, + { + string.format("%s %s", prefix, last.message:gsub("\r", ""):gsub("\n", " ")), + virtual_text_highlight_map[last.severity] + } + ) + + return virt_texts + end +end + +--- Callback scheduled when leaving Insert mode. +--- +--- This function must be exported publicly so that it is available to be +--- called from the Vimscript autocommand. +--- +--- See @ref schedule_display() +--- +---@private +function M._execute_scheduled_display(namespace, bufnr) + local args = bufs_waiting_to_update[bufnr][namespace] + if not args then + return + end + + -- Clear the args so we don't display unnecessarily. + bufs_waiting_to_update[bufnr][namespace] = nil + + M.show(namespace, bufnr, nil, args) +end + +--- Hide currently displayed diagnostics. +--- +--- This only clears the decorations displayed in the buffer. Diagnostics can +--- be redisplayed with |vim.diagnostic.show()|. To completely remove +--- diagnostics, use |vim.diagnostic.reset()|. +--- +--- To hide diagnostics and prevent them from re-displaying, use +--- |vim.diagnostic.disable()|. +--- +---@param namespace number The diagnostic namespace +---@param bufnr number|nil Buffer number. Defaults to the current buffer. +function M.hide(namespace, bufnr) + vim.validate { + namespace = { namespace, 'n' }, + bufnr = { bufnr, 'n', true }, + } + + bufnr = get_bufnr(bufnr) + diagnostic_cache_extmarks[bufnr][namespace] = {} + + local ns = get_namespace(namespace) + + -- clear sign group + vim.fn.sign_unplace(ns.sign_group, {buffer=bufnr}) + + -- clear virtual text namespace + vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) +end + + +--- Display diagnostics for the given namespace and buffer. +--- +---@param namespace number Diagnostic namespace +---@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param diagnostics table|nil The diagnostics to display. When omitted, use the +--- saved diagnostics for the given namespace and +--- buffer. This can be used to display a list of diagnostics +--- without saving them or to display only a subset of +--- diagnostics. +---@param opts table|nil Display options. See |vim.diagnostic.config()|. +function M.show(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = { namespace, 'n' }, + bufnr = { bufnr, 'n', true }, + diagnostics = { diagnostics, 't', true }, + opts = { opts, 't', true }, + } + + bufnr = get_bufnr(bufnr) + if is_disabled(namespace, bufnr) then + return + end + + M.hide(namespace, bufnr) + + diagnostics = diagnostics or M.get(bufnr, {namespace=namespace}) + + if not diagnostics or vim.tbl_isempty(diagnostics) then + return + end + + opts = get_resolved_options(opts, namespace, bufnr) + + if opts.update_in_insert then + clear_scheduled_display(namespace, bufnr) + else + local mode = vim.api.nvim_get_mode() + if string.sub(mode.mode, 1, 1) == 'i' then + schedule_display(namespace, bufnr, opts) + return + end + end + + if opts.underline then + M._set_underline(namespace, bufnr, diagnostics, opts.underline) + end + + if opts.virtual_text then + M._set_virtual_text(namespace, bufnr, diagnostics, opts.virtual_text) + end + + if opts.signs then + M._set_signs(namespace, bufnr, diagnostics, opts.signs) + end + + save_extmarks(namespace, bufnr) +end + +--- Open a floating window with the diagnostics at the given position. +--- +---@param opts table|nil Configuration table with the same keys as +--- |vim.lsp.util.open_floatin_preview()| in addition to the following: +--- - namespace: (number) Limit diagnostics to the given namespace +--- - severity: See |diagnostic-severity|. +--- - show_header: (boolean, default true) Show "Diagnostics:" header +---@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param position table|nil The (0,0)-indexed position. Defaults to the current cursor position. +---@return A ({popup_bufnr}, {win_id}) tuple +function M.show_position_diagnostics(opts, bufnr, position) + vim.validate { + opts = { opts, 't', true }, + bufnr = { bufnr, 'n', true }, + position = { position, 't', true }, + } + + opts = opts or {} + + opts.focus_id = "position_diagnostics" + bufnr = get_bufnr(bufnr) + if not position then + local curr_position = vim.api.nvim_win_get_cursor(0) + curr_position[1] = curr_position[1] - 1 + position = curr_position + end + local match_position_predicate = function(diag) + return position[1] == diag.lnum and + position[2] >= diag.col and + (position[2] <= diag.end_col or position[1] < diag.end_lnum) + end + local position_diagnostics = vim.tbl_filter(match_position_predicate, M.get(bufnr, opts)) + table.sort(position_diagnostics, function(a, b) return a.severity < b.severity end) + return show_diagnostics(opts, position_diagnostics) +end + +--- Open a floating window with the diagnostics from the given line. +--- +---@param opts table Configuration table. See |vim.diagnostic.show_position_diagnostics()|. +---@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param lnum number|nil Line number. Defaults to line number of cursor. +---@return A ({popup_bufnr}, {win_id}) tuple +function M.show_line_diagnostics(opts, bufnr, lnum) + vim.validate { + opts = { opts, 't', true }, + bufnr = { bufnr, 'n', true }, + lnum = { lnum, 'n', true }, + } + + opts = opts or {} + opts.focus_id = "line_diagnostics" + opts.lnum = lnum + local line_diagnostics = M.get(bufnr, opts) + return show_diagnostics(opts, line_diagnostics) +end + +--- Remove all diagnostics from the given namespace. +--- +--- Unlike |vim.diagnostic.hide()|, this function removes all saved +--- diagnostics. They cannot be redisplayed using |vim.diagnostic.show()|. To +--- simply remove diagnostic decorations in a way that they can be +--- re-displayed, use |vim.diagnostic.hide()|. +--- +---@param namespace number +---@param bufnr number|nil Remove diagnostics for the given buffer. When omitted, +--- diagnostics are removed for all buffers. +function M.reset(namespace, bufnr) + if bufnr == nil then + for iter_bufnr, namespaces in pairs(diagnostic_cache) do + if namespaces[namespace] then + M.reset(namespace, iter_bufnr) + end + end + else + clear_diagnostic_cache(namespace, bufnr) + M.hide(namespace, bufnr) + end + + vim.api.nvim_command("doautocmd User DiagnosticsChanged") +end + +--- Add all diagnostics to the quickfix list. +--- +---@param opts table|nil Configuration table with the following keys: +--- - namespace: (number) Only add diagnostics from the given namespace. +--- - open: (boolean, default true) Open quickfix list after setting. +--- - title: (string) Title of quickfix list. Defaults to "Diagnostics". +--- - severity: See |diagnostic-severity|. +function M.setqflist(opts) + set_list(false, opts) +end + +--- Add buffer diagnostics to the location list. +--- +---@param opts table|nil Configuration table with the following keys: +--- - namespace: (number) Only add diagnostics from the given namespace. +--- - winnr: (number, default 0) Window number to set location list for. +--- - open: (boolean, default true) Open the location list after setting. +--- - title: (string) Title of the location list. Defaults to "Diagnostics". +--- - severity: See |diagnostic-severity|. +function M.setloclist(opts) + set_list(true, opts) +end + +--- Disable diagnostics in the given buffer. +--- +---@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param namespace number|nil Only disable diagnostics for the given namespace. +function M.disable(bufnr, namespace) + vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} } + bufnr = get_bufnr(bufnr) + if namespace == nil then + diagnostic_disabled[bufnr] = true + for ns in pairs(diagnostic_cache[bufnr]) do + M.hide(ns, bufnr) + end + else + if type(diagnostic_disabled[bufnr]) ~= "table" then + diagnostic_disabled[bufnr] = {} + end + diagnostic_disabled[bufnr][namespace] = true + M.hide(namespace, bufnr) + end +end + +--- Enable diagnostics in the given buffer. +--- +---@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param namespace number|nil Only enable diagnostics for the given namespace. +function M.enable(bufnr, namespace) + vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} } + bufnr = get_bufnr(bufnr) + if namespace == nil then + diagnostic_disabled[bufnr] = nil + for ns in pairs(diagnostic_cache[bufnr]) do + M.show(ns, bufnr) + end + else + if type(diagnostic_disabled[bufnr]) ~= "table" then + return + end + diagnostic_disabled[bufnr][namespace] = nil + M.show(namespace, bufnr) + end +end + +-- }}} + + +return M diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 3276563439..90c5872f11 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1534,8 +1534,5 @@ function lsp._with_extend(name, options, user_config) return resulting_config end --- Define the LspDiagnostics signs if they're not defined already. -require('vim.lsp.diagnostic')._define_default_signs_and_highlights() - return lsp -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index ccd325b1ac..01c675ba77 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -1,48 +1,4 @@ -local api = vim.api -local validate = vim.validate - -local highlight = vim.highlight local log = require('vim.lsp.log') -local protocol = require('vim.lsp.protocol') -local util = require('vim.lsp.util') - -local if_nil = vim.F.if_nil - ----@class DiagnosticSeverity -local DiagnosticSeverity = protocol.DiagnosticSeverity - -local to_severity = function(severity) - if not severity then return nil end - return type(severity) == 'string' and DiagnosticSeverity[severity] or severity -end - -local filter_to_severity_limit = function(severity, diagnostics) - local filter_level = to_severity(severity) - if not filter_level then - return diagnostics - end - - return vim.tbl_filter(function(t) return t.severity == filter_level end, diagnostics) -end - -local filter_by_severity_limit = function(severity_limit, diagnostics) - local filter_level = to_severity(severity_limit) - if not filter_level then - return diagnostics - end - - return vim.tbl_filter(function(t) return t.severity <= filter_level end, diagnostics) -end - -local to_position = function(position, bufnr) - vim.validate { position = {position, 't'} } - - return { - position.line, - util._get_line_byte_from_position(bufnr, position) - } -end - ---@brief lsp-diagnostic --- @@ -57,70 +13,9 @@ end local M = {} --- Diagnostic Highlights {{{ - --- TODO(tjdevries): Determine how to generate documentation for these --- and how to configure them to be easy for users. --- --- For now, just use the following script. It should work pretty good. ---[[ -local levels = {"Error", "Warning", "Information", "Hint" } - -local all_info = { - { "Default", "Used as the base highlight group, other highlight groups link to", }, - { "VirtualText", 'Used for "%s" diagnostic virtual text.\n See |vim.lsp.diagnostic.set_virtual_text()|', }, - { "Underline", 'Used to underline "%s" diagnostics.\n See |vim.lsp.diagnostic.set_underline()|', }, - { "Floating", 'Used to color "%s" diagnostic messages in diagnostics float.\n See |vim.lsp.diagnostic.show_line_diagnostics()|', }, - { "Sign", 'Used for "%s" signs in sing column.\n See |vim.lsp.diagnostic.set_signs()|', }, -} - -local results = {} -for _, info in ipairs(all_info) do - for _, level in ipairs(levels) do - local name = info[1] - local description = info[2] - local fullname = string.format("Lsp%s%s", name, level) - table.insert(results, string.format( - "%78s", string.format("*hl-%s*", fullname)) - ) - - table.insert(results, fullname) - table.insert(results, string.format(" %s", description)) - table.insert(results, "") - end -end - --- print(table.concat(results, '\n')) -vim.fn.setreg("*", table.concat(results, '\n')) ---]] - -local diagnostic_severities = { - [DiagnosticSeverity.Error] = { guifg = "Red" }; - [DiagnosticSeverity.Warning] = { guifg = "Orange" }; - [DiagnosticSeverity.Information] = { guifg = "LightBlue" }; - [DiagnosticSeverity.Hint] = { guifg = "LightGrey" }; -} - --- Make a map from DiagnosticSeverity -> Highlight Name -local make_highlight_map = function(base_name) - local result = {} - for k, _ in pairs(diagnostic_severities) do - result[k] = "LspDiagnostics" .. base_name .. DiagnosticSeverity[k] - end - - return result -end - -local default_highlight_map = make_highlight_map("Default") -local virtual_text_highlight_map = make_highlight_map("VirtualText") -local underline_highlight_map = make_highlight_map("Underline") -local floating_highlight_map = make_highlight_map("Floating") -local sign_highlight_map = make_highlight_map("Sign") - --- }}} --- Diagnostic Namespaces {{{ local DEFAULT_CLIENT_ID = -1 -local get_client_id = function(client_id) +---@private +local function get_client_id(client_id) if client_id == nil then client_id = DEFAULT_CLIENT_ID end @@ -128,179 +23,112 @@ local get_client_id = function(client_id) return client_id end -local get_bufnr = function(bufnr) +---@private +local function get_bufnr(bufnr) if not bufnr then - return api.nvim_get_current_buf() + return vim.api.nvim_get_current_buf() elseif bufnr == 0 then - return api.nvim_get_current_buf() + return vim.api.nvim_get_current_buf() end return bufnr end - ---- Create a namespace table, used to track a client's buffer local items -local _make_namespace_table = function(namespace, api_namespace) - vim.validate { namespace = { namespace, 's' } } - - return setmetatable({ - [DEFAULT_CLIENT_ID] = api.nvim_create_namespace(namespace) - }, { - __index = function(t, client_id) - client_id = get_client_id(client_id) - - if rawget(t, client_id) == nil then - local value = string.format("%s:%s", namespace, client_id) - - if api_namespace then - value = api.nvim_create_namespace(value) - end - - rawset(t, client_id, value) - end - - return rawget(t, client_id) - end - }) -end - -local _diagnostic_namespaces = _make_namespace_table("vim_lsp_diagnostics", true) -local _sign_namespaces = _make_namespace_table("vim_lsp_signs", false) - ---@private -function M._get_diagnostic_namespace(client_id) - return _diagnostic_namespaces[client_id] +local function severity_lsp_to_vim(severity) + if type(severity) == 'string' then + severity = vim.lsp.protocol.DiagnosticSeverity[severity] + end + return severity end ---@private -function M._get_sign_namespace(client_id) - return _sign_namespaces[client_id] +local function severity_vim_to_lsp(severity) + if type(severity) == 'string' then + severity = vim.diagnostic.severity[severity] + end + return severity end --- }}} --- Diagnostic Buffer & Client metatables {{{ -local bufnr_and_client_cacher_mt = { - __index = function(t, bufnr) - if bufnr == 0 or bufnr == nil then - bufnr = vim.api.nvim_get_current_buf() - end - if rawget(t, bufnr) == nil then - rawset(t, bufnr, {}) - end +---@private +local function line_byte_from_position(lines, lnum, col, offset_encoding) + if offset_encoding == "utf-8" then + return col + end - return rawget(t, bufnr) - end, + local line = lines[lnum + 1] + local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == "utf-16") + if ok then + return result + end - __newindex = function(t, bufnr, v) - if bufnr == 0 or bufnr == nil then - bufnr = vim.api.nvim_get_current_buf() - end + return col +end - rawset(t, bufnr, v) - end, -} --- }}} --- Diagnostic Saving & Caching {{{ -local _diagnostic_cleanup = setmetatable({}, bufnr_and_client_cacher_mt) -local diagnostic_cache = setmetatable({}, bufnr_and_client_cacher_mt) -local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_client_cacher_mt) -local diagnostic_cache_lines = setmetatable({}, bufnr_and_client_cacher_mt) -local diagnostic_cache_counts = setmetatable({}, bufnr_and_client_cacher_mt) -local diagnostic_attached_buffers = {} +---@private +local function get_buf_lines(bufnr) + if vim.api.nvim_buf_is_loaded(bufnr) then + return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + end --- Disabled buffers and clients -local diagnostic_disabled = setmetatable({}, bufnr_and_client_cacher_mt) + local filename = vim.api.nvim_buf_get_name(bufnr) + local f = io.open(filename) + local lines = vim.split(f:read("*a"), "\n") + f:close() + return lines +end -local _bufs_waiting_to_update = setmetatable({}, bufnr_and_client_cacher_mt) - ---- Store Diagnostic[] by line ---- ----@param diagnostics Diagnostic[] ----@return table -local _diagnostic_lines = function(diagnostics) - if not diagnostics then return end - - local diagnostics_by_line = {} - for _, diagnostic in ipairs(diagnostics) do +---@private +local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id) + local buf_lines = get_buf_lines(bufnr) + local client = vim.lsp.get_client_by_id(client_id) + local offset_encoding = client and client.offset_encoding or "utf-16" + return vim.tbl_map(function(diagnostic) local start = diagnostic.range.start - local line_diagnostics = diagnostics_by_line[start.line] - if not line_diagnostics then - line_diagnostics = {} - diagnostics_by_line[start.line] = line_diagnostics - end - table.insert(line_diagnostics, diagnostic) - end - return diagnostics_by_line + local _end = diagnostic.range["end"] + return { + lnum = start.line, + col = line_byte_from_position(buf_lines, start.line, start.character, offset_encoding), + end_lnum = _end.line, + end_col = line_byte_from_position(buf_lines, _end.line, _end.character, offset_encoding), + severity = severity_lsp_to_vim(diagnostic.severity), + message = diagnostic.message + } + end, diagnostics) end ---- Get the count of M by Severity +---@private +local function diagnostic_vim_to_lsp(diagnostics) + return vim.tbl_map(function(diagnostic) + return { + range = { + start = { + line = diagnostic.lnum, + character = diagnostic.col, + }, + ["end"] = { + line = diagnostic.end_lnum, + character = diagnostic.end_col, + }, + }, + severity = severity_vim_to_lsp(diagnostic.severity), + message = diagnostic.message, + } + end, diagnostics) +end + +local _client_namespaces = {} + +--- Get the diagnostic namespace associated with an LSP client |vim.diagnostic|. --- ----@param diagnostics Diagnostic[] ----@return table -local _diagnostic_counts = function(diagnostics) - if not diagnostics then return end - - local counts = {} - for _, diagnostic in pairs(diagnostics) do - if diagnostic.severity then - local val = counts[diagnostic.severity] - if val == nil then - val = 0 - end - - counts[diagnostic.severity] = val + 1 - end +---@param client_id number The id of the LSP client +function M.get_namespace(client_id) + vim.validate { client_id = { client_id, 'n' } } + if not _client_namespaces[client_id] then + local name = string.format("vim.lsp.client-%d", client_id) + _client_namespaces[client_id] = vim.api.nvim_create_namespace(name) end - - return counts -end - ----@private ---- Set the different diagnostic cache after `textDocument/publishDiagnostics` ----@param diagnostics Diagnostic[] ----@param bufnr number ----@param client_id number ----@return nil -local function set_diagnostic_cache(diagnostics, bufnr, client_id) - client_id = get_client_id(client_id) - - -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic - -- - -- The diagnostic's severity. Can be omitted. If omitted it is up to the - -- client to interpret diagnostics as error, warning, info or hint. - -- TODO: Replace this with server-specific heuristics to infer severity. - local buf_line_count = vim.api.nvim_buf_line_count(bufnr) - for _, diagnostic in ipairs(diagnostics) do - if diagnostic.severity == nil then - diagnostic.severity = DiagnosticSeverity.Error - end - -- Account for servers that place diagnostics on terminating newline - if buf_line_count > 0 then - diagnostic.range.start.line = math.max(math.min( - diagnostic.range.start.line, buf_line_count - 1 - ), 0) - diagnostic.range["end"].line = math.max(math.min( - diagnostic.range["end"].line, buf_line_count - 1 - ), 0) - end - end - - diagnostic_cache[bufnr][client_id] = diagnostics - diagnostic_cache_lines[bufnr][client_id] = _diagnostic_lines(diagnostics) - diagnostic_cache_counts[bufnr][client_id] = _diagnostic_counts(diagnostics) -end - - ----@private ---- Clear the cached diagnostics ----@param bufnr number ----@param client_id number -local function clear_diagnostic_cache(bufnr, client_id) - client_id = get_client_id(client_id) - - diagnostic_cache[bufnr][client_id] = nil - diagnostic_cache_lines[bufnr][client_id] = nil - diagnostic_cache_counts[bufnr][client_id] = nil + return _client_namespaces[client_id] end --- Save diagnostics to the current buffer. @@ -309,683 +137,17 @@ end ---@param diagnostics Diagnostic[] ---@param bufnr number ---@param client_id number +---@private function M.save(diagnostics, bufnr, client_id) - validate { - diagnostics = {diagnostics, 't'}, - bufnr = {bufnr, 'n'}, - client_id = {client_id, 'n', true}, - } - - if not diagnostics then return end - - bufnr = get_bufnr(bufnr) - client_id = get_client_id(client_id) - - if not _diagnostic_cleanup[bufnr][client_id] then - _diagnostic_cleanup[bufnr][client_id] = true - - -- Clean up our data when the buffer unloads. - api.nvim_buf_attach(bufnr, false, { - on_detach = function(_, b) - clear_diagnostic_cache(b, client_id) - _diagnostic_cleanup[b][client_id] = nil - end - }) - end - - set_diagnostic_cache(diagnostics, bufnr, client_id) + local namespace = M.get_namespace(client_id) + vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) end -- }}} --- Diagnostic Retrieval {{{ - - ---- Get all diagnostics for clients ---- ----@param client_id number Restrict included diagnostics to the client ---- If nil, diagnostics of all clients are included. ----@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[]) -function M.get_all(client_id) - local diagnostics_by_bufnr = {} - for bufnr, buf_diagnostics in pairs(diagnostic_cache) do - diagnostics_by_bufnr[bufnr] = {} - for cid, client_diagnostics in pairs(buf_diagnostics) do - if client_id == nil or cid == client_id then - vim.list_extend(diagnostics_by_bufnr[bufnr], client_diagnostics) - end - end - end - return diagnostics_by_bufnr -end - ---- Return associated diagnostics for bufnr ---- ----@param bufnr number ----@param client_id number|nil If nil, then return all of the diagnostics. ---- Else, return just the diagnostics associated with the client_id. ----@param predicate function|nil Optional function for filtering diagnostics -function M.get(bufnr, client_id, predicate) - if client_id == nil then - local all_diagnostics = {} - for iter_client_id, _ in pairs(diagnostic_cache[bufnr]) do - local iter_diagnostics = M.get(bufnr, iter_client_id, predicate) - - for _, diagnostic in ipairs(iter_diagnostics) do - table.insert(all_diagnostics, diagnostic) - end - end - - return all_diagnostics - end - - predicate = predicate or function(_) return true end - local client_diagnostics = {} - for _, diagnostic in ipairs(diagnostic_cache[bufnr][client_id] or {}) do - if predicate(diagnostic) then - table.insert(client_diagnostics, diagnostic) - end - end - return client_diagnostics -end - ---- Get the diagnostics by line ---- ----@param bufnr number|nil The buffer number ----@param line_nr number|nil The line number ----@param opts table|nil Configuration keys ---- - severity: (DiagnosticSeverity, default nil) ---- - Only return diagnostics with this severity. Overrides severity_limit ---- - severity_limit: (DiagnosticSeverity, default nil) ---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. ----@param client_id|nil number the client id ----@return table Table with map of line number to list of diagnostics. --- Structured: { [1] = {...}, [5] = {.... } } -function M.get_line_diagnostics(bufnr, line_nr, opts, client_id) - opts = opts or {} - - bufnr = bufnr or vim.api.nvim_get_current_buf() - line_nr = line_nr or vim.api.nvim_win_get_cursor(0)[1] - 1 - - local client_get_diags = function(iter_client_id) - return (diagnostic_cache_lines[bufnr][iter_client_id] or {})[line_nr] or {} - end - - local line_diagnostics - if client_id == nil then - line_diagnostics = {} - for iter_client_id, _ in pairs(diagnostic_cache_lines[bufnr]) do - for _, diagnostic in ipairs(client_get_diags(iter_client_id)) do - table.insert(line_diagnostics, diagnostic) - end - end - else - line_diagnostics = vim.deepcopy(client_get_diags(client_id)) - end - - if opts.severity then - line_diagnostics = filter_to_severity_limit(opts.severity, line_diagnostics) - elseif opts.severity_limit then - line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics) - end - - table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end) - - return line_diagnostics -end - ---- Get the counts for a particular severity ---- ---- Useful for showing diagnostic counts in statusline. eg: ---- ----
---- function! LspStatus() abort
----   let sl = ''
----   if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))')
----     let sl.='%#MyStatuslineLSP#E:'
----     let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Error]])")}'
----     let sl.='%#MyStatuslineLSP# W:'
----     let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Warning]])")}'
----   else
----       let sl.='%#MyStatuslineLSPErrors#off'
----   endif
----   return sl
---- endfunction
---- autocmd BufWinEnter * let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
---- 
---- ----@param bufnr number The buffer number ----@param severity DiagnosticSeverity ----@param client_id number the client id -function M.get_count(bufnr, severity, client_id) - if client_id == nil then - local total = 0 - for iter_client_id, _ in pairs(diagnostic_cache_counts[bufnr]) do - total = total + M.get_count(bufnr, severity, iter_client_id) - end - - return total - end - - return (diagnostic_cache_counts[bufnr][client_id] or {})[DiagnosticSeverity[severity]] or 0 -end - - --- }}} --- Diagnostic Movements {{{ - ---- Helper function to find the next diagnostic relative to a position ----@return table the next diagnostic if found -local _next_diagnostic = function(position, search_forward, bufnr, opts, client_id) - position[1] = position[1] - 1 - bufnr = bufnr or vim.api.nvim_get_current_buf() - local wrap = if_nil(opts.wrap, true) - local line_count = vim.api.nvim_buf_line_count(bufnr) - for i = 0, line_count do - local offset = i * (search_forward and 1 or -1) - local line_nr = position[1] + offset - if line_nr < 0 or line_nr >= line_count then - if not wrap then - return - end - line_nr = (line_nr + line_count) % line_count - end - local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id) - if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then - local sort_diagnostics, is_next - if search_forward then - sort_diagnostics = function(a, b) return a.range.start.character < b.range.start.character end - is_next = function(diagnostic) return diagnostic.range.start.character > position[2] end - else - sort_diagnostics = function(a, b) return a.range.start.character > b.range.start.character end - is_next = function(diagnostic) return diagnostic.range.start.character < position[2] end - end - table.sort(line_diagnostics, sort_diagnostics) - if i == 0 then - for _, v in pairs(line_diagnostics) do - if is_next(v) then - return v - end - end - else - return line_diagnostics[1] - end - end - end -end - ----@private ---- Helper function to return a position from a diagnostic ---- ----@return table {row, col} -local function _diagnostic_pos(opts, diagnostic) - opts = opts or {} - - local win_id = opts.win_id or vim.api.nvim_get_current_win() - local bufnr = vim.api.nvim_win_get_buf(win_id) - - if not diagnostic then return false end - - return to_position(diagnostic.range.start, bufnr) -end - ----@private --- Move to the diagnostic position -local function _diagnostic_move_pos(name, opts, pos) - opts = opts or {} - - local enable_popup = if_nil(opts.enable_popup, true) - local win_id = opts.win_id or vim.api.nvim_get_current_win() - - if not pos then - print(string.format("%s: No more valid diagnostics to move to.", name)) - return - end - - vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]}) - - if enable_popup then - -- This is a bit weird... I'm surprised that we need to wait til the next tick to do this. - vim.schedule(function() - M.show_position_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id)) - end) - end -end - ---- Get the previous diagnostic closest to the cursor_position ---- ----@param opts table See |vim.lsp.diagnostic.goto_next()| ----@return table Previous diagnostic -function M.get_prev(opts) - opts = opts or {} - - local win_id = opts.win_id or vim.api.nvim_get_current_win() - local bufnr = vim.api.nvim_win_get_buf(win_id) - local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id) - - return _next_diagnostic(cursor_position, false, bufnr, opts, opts.client_id) -end - ---- Return the pos, {row, col}, for the prev diagnostic in the current buffer. ----@param opts table See |vim.lsp.diagnostic.goto_next()| ----@return table Previous diagnostic position -function M.get_prev_pos(opts) - return _diagnostic_pos( - opts, - M.get_prev(opts) - ) -end - ---- Move to the previous diagnostic ----@param opts table See |vim.lsp.diagnostic.goto_next()| -function M.goto_prev(opts) - return _diagnostic_move_pos( - "DiagnosticPrevious", - opts, - M.get_prev_pos(opts) - ) -end - ---- Get the next diagnostic closest to the cursor_position ----@param opts table See |vim.lsp.diagnostic.goto_next()| ----@return table Next diagnostic -function M.get_next(opts) - opts = opts or {} - - local win_id = opts.win_id or vim.api.nvim_get_current_win() - local bufnr = vim.api.nvim_win_get_buf(win_id) - local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id) - - return _next_diagnostic(cursor_position, true, bufnr, opts, opts.client_id) -end - ---- Return the pos, {row, col}, for the next diagnostic in the current buffer. ----@param opts table See |vim.lsp.diagnostic.goto_next()| ----@return table Next diagnostic position -function M.get_next_pos(opts) - return _diagnostic_pos( - opts, - M.get_next(opts) - ) -end - ---- Move to the next diagnostic ----@param opts table|nil Configuration table. Keys: ---- - {client_id}: (number) ---- - If nil, will consider all clients attached to buffer. ---- - {cursor_position}: (Position, default current position) ---- - See |nvim_win_get_cursor()| ---- - {wrap}: (boolean, default true) ---- - Whether to loop around file or not. Similar to 'wrapscan' ---- - {severity}: (DiagnosticSeverity) ---- - Exclusive severity to consider. Overrides {severity_limit} ---- - {severity_limit}: (DiagnosticSeverity) ---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. ---- - {enable_popup}: (boolean, default true) ---- - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump ---- - {popup_opts}: (table) ---- - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()| ---- - {win_id}: (number, default 0) ---- - Window ID -function M.goto_next(opts) - return _diagnostic_move_pos( - "DiagnosticNext", - opts, - M.get_next_pos(opts) - ) -end --- }}} --- Diagnostic Setters {{{ - ---- Set signs for given diagnostics ---- ---- Sign characters can be customized with the following commands: ---- ----
---- sign define LspDiagnosticsSignError text=E texthl=LspDiagnosticsSignError linehl= numhl=
---- sign define LspDiagnosticsSignWarning text=W texthl=LspDiagnosticsSignWarning linehl= numhl=
---- sign define LspDiagnosticsSignInformation text=I texthl=LspDiagnosticsSignInformation linehl= numhl=
---- sign define LspDiagnosticsSignHint text=H texthl=LspDiagnosticsSignHint linehl= numhl=
---- 
----@param diagnostics Diagnostic[] ----@param bufnr number The buffer number ----@param client_id number the client id ----@param sign_ns number|nil ----@param opts table Configuration for signs. Keys: ---- - priority: Set the priority of the signs. ---- - severity_limit (DiagnosticSeverity): ---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. -function M.set_signs(diagnostics, bufnr, client_id, sign_ns, opts) - opts = opts or {} - sign_ns = sign_ns or M._get_sign_namespace(client_id) - - if not diagnostics then - diagnostics = diagnostic_cache[bufnr][client_id] - end - - if not diagnostics then - return - end - - bufnr = get_bufnr(bufnr) - diagnostics = filter_by_severity_limit(opts.severity_limit, diagnostics) - - local ok = true - for _, diagnostic in ipairs(diagnostics) do - - ok = ok and pcall(vim.fn.sign_place, - 0, - sign_ns, - sign_highlight_map[diagnostic.severity], - bufnr, - { - priority = opts.priority, - lnum = diagnostic.range.start.line + 1 - } - ) - end - - if not ok then - log.debug("Failed to place signs:", diagnostics) - end -end - ---- Set underline for given diagnostics ---- ---- Underline highlights can be customized by changing the following |:highlight| groups. ---- ----
---- LspDiagnosticsUnderlineError
---- LspDiagnosticsUnderlineWarning
---- LspDiagnosticsUnderlineInformation
---- LspDiagnosticsUnderlineHint
---- 
---- ----@param diagnostics Diagnostic[] ----@param bufnr number: The buffer number ----@param client_id number: The client id ----@param diagnostic_ns number|nil: The namespace ----@param opts table: Configuration table: ---- - severity_limit (DiagnosticSeverity): ---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. -function M.set_underline(diagnostics, bufnr, client_id, diagnostic_ns, opts) - opts = opts or {} - - diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id) - diagnostics = filter_by_severity_limit(opts.severity_limit, diagnostics) - - for _, diagnostic in ipairs(diagnostics) do - local start = diagnostic.range["start"] - local finish = diagnostic.range["end"] - local higroup = underline_highlight_map[diagnostic.severity] - - if higroup == nil then - -- Default to error if we don't have a highlight associated - higroup = underline_highlight_map[DiagnosticSeverity.Error] - end - - highlight.range( - bufnr, - diagnostic_ns, - higroup, - to_position(start, bufnr), - to_position(finish, bufnr) - ) - end -end - --- Virtual Text {{{ ---- Set virtual text given diagnostics ---- ---- Virtual text highlights can be customized by changing the following |:highlight| groups. ---- ----
---- LspDiagnosticsVirtualTextError
---- LspDiagnosticsVirtualTextWarning
---- LspDiagnosticsVirtualTextInformation
---- LspDiagnosticsVirtualTextHint
---- 
---- ----@param diagnostics Diagnostic[] ----@param bufnr number ----@param client_id number ----@param diagnostic_ns number ----@param opts table Options on how to display virtual text. Keys: ---- - prefix (string): Prefix to display before virtual text on line ---- - spacing (number): Number of spaces to insert before virtual text ---- - severity_limit (DiagnosticSeverity): ---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. -function M.set_virtual_text(diagnostics, bufnr, client_id, diagnostic_ns, opts) - opts = opts or {} - - client_id = get_client_id(client_id) - diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id) - - local buffer_line_diagnostics - if diagnostics then - buffer_line_diagnostics = _diagnostic_lines(diagnostics) - else - buffer_line_diagnostics = diagnostic_cache_lines[bufnr][client_id] - end - - if not buffer_line_diagnostics then - return nil - end - - for line, line_diagnostics in pairs(buffer_line_diagnostics) do - line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics) - local virt_texts = M.get_virtual_text_chunks_for_line(bufnr, line, line_diagnostics, opts) - - if virt_texts then - api.nvim_buf_set_extmark(bufnr, diagnostic_ns, line, 0, { - virt_text = virt_texts, - }) - end - end -end - ---- Default function to get text chunks to display using |nvim_buf_set_extmark()|. ----@param bufnr number The buffer to display the virtual text in ----@param line number The line number to display the virtual text on ----@param line_diags Diagnostic[] The diagnostics associated with the line ----@param opts table See {opts} from |vim.lsp.diagnostic.set_virtual_text()| ----@return an array of [text, hl_group] arrays. This can be passed directly to ---- the {virt_text} option of |nvim_buf_set_extmark()|. -function M.get_virtual_text_chunks_for_line(bufnr, line, line_diags, opts) - assert(bufnr or line) - - if #line_diags == 0 then - return nil - end - - opts = opts or {} - local prefix = opts.prefix or "■" - local spacing = opts.spacing or 4 - - -- Create a little more space between virtual text and contents - local virt_texts = {{string.rep(" ", spacing)}} - - for i = 1, #line_diags - 1 do - table.insert(virt_texts, {prefix, virtual_text_highlight_map[line_diags[i].severity]}) - end - local last = line_diags[#line_diags] - - -- TODO(tjdevries): Allow different servers to be shown first somehow? - -- TODO(tjdevries): Display server name associated with these? - if last.message then - table.insert( - virt_texts, - { - string.format("%s %s", prefix, last.message:gsub("\r", ""):gsub("\n", " ")), - virtual_text_highlight_map[last.severity] - } - ) - - return virt_texts - end -end --- }}} --- }}} --- Diagnostic Clear {{{ ---- Clears the currently displayed diagnostics ----@param bufnr number The buffer number ----@param client_id number the client id ----@param diagnostic_ns number|nil Associated diagnostic namespace ----@param sign_ns number|nil Associated sign namespace -function M.clear(bufnr, client_id, diagnostic_ns, sign_ns) - bufnr = get_bufnr(bufnr) - if client_id == nil then - return vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _) - return M.clear(bufnr, iter_client_id) - end) - end - - diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id) - sign_ns = sign_ns or M._get_sign_namespace(client_id) - diagnostic_cache_extmarks[bufnr][client_id] = {} - - assert(bufnr, "bufnr is required") - assert(diagnostic_ns, "Need diagnostic_ns, got nil") - assert(sign_ns, string.format("Need sign_ns, got nil %s", sign_ns)) - - -- clear sign group - vim.fn.sign_unplace(sign_ns, {buffer=bufnr}) - - -- clear virtual text namespace - api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) -end --- }}} --- Diagnostic Insert Leave Handler {{{ - ---- Callback scheduled for after leaving insert mode ---- ---- Used to handle ----@private -function M._execute_scheduled_display(bufnr, client_id) - local args = _bufs_waiting_to_update[bufnr][client_id] - if not args then - return - end - - -- Clear the args so we don't display unnecessarily. - _bufs_waiting_to_update[bufnr][client_id] = nil - - M.display(nil, bufnr, client_id, args) -end - -local registered = {} - -local make_augroup_key = function(bufnr, client_id) - return string.format("LspDiagnosticInsertLeave:%s:%s", bufnr, client_id) -end - ---- Table of autocmd events to fire the update for displaying new diagnostic information -M.insert_leave_auto_cmds = { "InsertLeave", "CursorHoldI" } - ---- Used to schedule diagnostic updates upon leaving insert mode. ---- ---- For parameter description, see |M.display()| -function M._schedule_display(bufnr, client_id, args) - _bufs_waiting_to_update[bufnr][client_id] = args - - local key = make_augroup_key(bufnr, client_id) - if not registered[key] then - vim.cmd(string.format("augroup %s", key)) - vim.cmd(" au!") - vim.cmd( - string.format( - [[autocmd %s :lua vim.lsp.diagnostic._execute_scheduled_display(%s, %s)]], - table.concat(M.insert_leave_auto_cmds, ","), - bufnr, - bufnr, - client_id - ) - ) - vim.cmd("augroup END") - - registered[key] = true - end -end - - ---- Used in tandem with ---- ---- For parameter description, see |M.display()| -function M._clear_scheduled_display(bufnr, client_id) - local key = make_augroup_key(bufnr, client_id) - - if registered[key] then - vim.cmd(string.format("augroup %s", key)) - vim.cmd(" au!") - vim.cmd("augroup END") - - registered[key] = nil - end -end --- }}} - --- Diagnostic Private Highlight Utilies {{{ ---- Get the severity highlight name ----@private -function M._get_severity_highlight_name(severity) - return virtual_text_highlight_map[severity] -end - ---- Get floating severity highlight name ----@private -function M._get_floating_severity_highlight_name(severity) - return floating_highlight_map[severity] -end - ---- This should be called to update the highlights for the LSP client. -function M._define_default_signs_and_highlights() - ---@private - local function define_default_sign(name, properties) - if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then - vim.fn.sign_define(name, properties) - end - end - - -- Initialize default diagnostic highlights - for severity, hi_info in pairs(diagnostic_severities) do - local default_highlight_name = default_highlight_map[severity] - highlight.create(default_highlight_name, hi_info, true) - - -- Default link all corresponding highlights to the default highlight - highlight.link(virtual_text_highlight_map[severity], default_highlight_name, false) - highlight.link(floating_highlight_map[severity], default_highlight_name, false) - highlight.link(sign_highlight_map[severity], default_highlight_name, false) - end - - -- Create all signs - for severity, sign_hl_name in pairs(sign_highlight_map) do - local severity_name = DiagnosticSeverity[severity] - - define_default_sign(sign_hl_name, { - text = (severity_name or 'U'):sub(1, 1), - texthl = sign_hl_name, - linehl = '', - numhl = '', - }) - end - - -- Initialize Underline highlights - for severity, underline_highlight_name in pairs(underline_highlight_map) do - highlight.create(underline_highlight_name, { - cterm = 'underline', - gui = 'underline', - guisp = diagnostic_severities[severity].guifg - }, true) - end -end --- }}} --- Diagnostic Display {{{ --- |lsp-handler| for the method "textDocument/publishDiagnostics" --- ----@note Each of the configuration options accepts: ---- - `false`: Disable this feature ---- - `true`: Enable this feature, use default settings. ---- - `table`: Enable this feature, use overrides. ---- - `function`: Function with signature (bufnr, client_id) that returns any of the above. +--- See |vim.diagnostic.config()| for configuration options. Handler-specific +--- configuration can be set using |vim.lsp.with()|: ---
 --- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
 ---   vim.lsp.diagnostic.on_publish_diagnostics, {
@@ -1006,20 +168,7 @@ end
 --- )
 --- 
--- ----@param config table Configuration table. ---- - underline: (default=true) ---- - Apply underlines to diagnostics. ---- - See |vim.lsp.diagnostic.set_underline()| ---- - virtual_text: (default=true) ---- - Apply virtual text to line endings. ---- - See |vim.lsp.diagnostic.set_virtual_text()| ---- - signs: (default=true) ---- - Apply signs for diagnostics. ---- - See |vim.lsp.diagnostic.set_signs()| ---- - update_in_insert: (default=false) ---- - Update diagnostics in InsertMode or wait until InsertLeave ---- - severity_sort: (default=false) ---- - Sort diagnostics (and thus signs and virtual text) +---@param config table Configuration table (see |vim.diagnostic.config()|). function M.on_publish_diagnostics(_, result, ctx, config) local client_id = ctx.client_id local uri = result.uri @@ -1029,160 +178,421 @@ function M.on_publish_diagnostics(_, result, ctx, config) return end + client_id = get_client_id(client_id) + local namespace = M.get_namespace(client_id) local diagnostics = result.diagnostics - if config and if_nil(config.severity_sort, false) then - table.sort(diagnostics, function(a, b) return a.severity > b.severity end) - end - - -- Always save the diagnostics, even if the buf is not loaded. - -- Language servers may report compile or build errors via diagnostics - -- Users should be able to find these, even if they're in files which - -- are not loaded. - M.save(diagnostics, bufnr, client_id) - - -- Unloaded buffers should not handle diagnostics. - -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen. - -- This should trigger another publish of the diagnostics. - -- - -- In particular, this stops a ton of spam when first starting a server for current - -- unloaded buffers. - if not api.nvim_buf_is_loaded(bufnr) then - return - end - - M.display(diagnostics, bufnr, client_id, config) -end - --- restores the extmarks set by M.display ----@param last number last line that was changed ----@private -local function restore_extmarks(bufnr, last) - for client_id, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do - local ns = M._get_diagnostic_namespace(client_id) - local extmarks_current = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true}) - local found = {} - for _, extmark in ipairs(extmarks_current) do - -- nvim_buf_set_lines will move any extmark to the line after the last - -- nvim_buf_set_text will move any extmark to the last line - if extmark[2] ~= last + 1 then - found[extmark[1]] = true - end + if config then + if vim.F.if_nil(config.severity_sort, false) then + table.sort(diagnostics, function(a, b) return a.severity > b.severity end) end - for _, extmark in ipairs(extmarks) do - if not found[extmark[1]] then - local opts = extmark[4] - opts.id = extmark[1] - -- HACK: end_row should be end_line - if opts.end_row then - opts.end_line = opts.end_row - opts.end_row = nil + + for _, opt in pairs(config) do + if type(opt) == 'table' then + if not opt.severity and opt.severity_limit then + opt.severity = {min=severity_lsp_to_vim(opt.severity_limit)} end - pcall(api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts) end end - end -end --- caches the extmarks set by M.display ----@private -local function save_extmarks(bufnr, client_id) - bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr - if not diagnostic_attached_buffers[bufnr] then - api.nvim_buf_attach(bufnr, false, { - on_lines = function(_, _, _, _, _, last) - restore_extmarks(bufnr, last - 1) - end, - on_detach = function() - diagnostic_cache_extmarks[bufnr] = nil - end}) - diagnostic_attached_buffers[bufnr] = true - end - local ns = M._get_diagnostic_namespace(client_id) - diagnostic_cache_extmarks[bufnr][client_id] = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true}) -end - ----@private ---- Display diagnostics for the buffer, given a configuration. -function M.display(diagnostics, bufnr, client_id, config) - if diagnostic_disabled[bufnr][client_id] then - return + vim.diagnostic.config(config, namespace) end - config = vim.lsp._with_extend('vim.lsp.diagnostic.on_publish_diagnostics', { - signs = true, - underline = true, - virtual_text = true, - update_in_insert = false, - severity_sort = false, - }, config) - - -- TODO(tjdevries): Consider how we can make this a "standardized" kind of thing for |lsp-handlers|. - -- It seems like we would probably want to do this more often as we expose more of them. - -- It provides a very nice functional interface for people to override configuration. - local resolve_optional_value = function(option) - local enabled_val = {} - - if not option then - return false - elseif option == true then - return enabled_val - elseif type(option) == 'function' then - local val = option(bufnr, client_id) - if val == true then - return enabled_val - else - return val - end - elseif type(option) == 'table' then - return option - else - error("Unexpected option type: " .. vim.inspect(option)) - end - end - - if resolve_optional_value(config.update_in_insert) then - M._clear_scheduled_display(bufnr, client_id) - else - local mode = vim.api.nvim_get_mode() - - if string.sub(mode.mode, 1, 1) == 'i' then - M._schedule_display(bufnr, client_id, config) - return - end - end - - M.clear(bufnr, client_id) - - diagnostics = diagnostics or M.get(bufnr, client_id) + vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) + -- Keep old autocmd for back compat. This should eventually be removed. vim.api.nvim_command("doautocmd User LspDiagnosticsChanged") +end - if not diagnostics or vim.tbl_isempty(diagnostics) then - return +--- Clear diagnotics and diagnostic cache. +--- +--- Diagnostic producers should prefer |vim.diagnostic.reset()|. However, +--- this method signature is still used internally in some parts of the LSP +--- implementation so it's simply marked @private rather than @deprecated. +--- +---@param client_id number +---@param buffer_client_map table map of buffers to active clients +---@private +function M.reset(client_id, buffer_client_map) + buffer_client_map = vim.deepcopy(buffer_client_map) + vim.schedule(function() + for bufnr, client_ids in pairs(buffer_client_map) do + if client_ids[client_id] then + local namespace = M.get_namespace(client_id) + vim.diagnostic.reset(namespace, bufnr) + end + end + end) +end + +-- Deprecated Functions {{{ + +--- Get all diagnostics for clients +--- +---@deprecated Prefer |vim.diagnostic.get()| +--- +---@param client_id number Restrict included diagnostics to the client +--- If nil, diagnostics of all clients are included. +---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[]) +function M.get_all(client_id) + local result = {} + local namespace + if client_id then + namespace = M.get_namespace(client_id) + end + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + local diagnostics = diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, {namespace = namespace})) + result[bufnr] = diagnostics + end + return result +end + +--- Return associated diagnostics for bufnr +--- +---@deprecated Prefer |vim.diagnostic.get()| +--- +---@param bufnr number +---@param client_id number|nil If nil, then return all of the diagnostics. +--- Else, return just the diagnostics associated with the client_id. +---@param predicate function|nil Optional function for filtering diagnostics +function M.get(bufnr, client_id, predicate) + predicate = predicate or function() return true end + if client_id == nil then + local all_diagnostics = {} + vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _) + local iter_diagnostics = vim.tbl_filter(predicate, M.get(bufnr, iter_client_id)) + for _, diagnostic in ipairs(iter_diagnostics) do + table.insert(all_diagnostics, diagnostic) + end + end) + return diagnostic_vim_to_lsp(all_diagnostics) end - local underline_opts = resolve_optional_value(config.underline) - if underline_opts then - M.set_underline(diagnostics, bufnr, client_id, nil, underline_opts) + local namespace = M.get_namespace(client_id) + return diagnostic_vim_to_lsp(vim.tbl_filter(predicate, vim.diagnostic.get(bufnr, {namespace=namespace}))) +end + +--- Get the diagnostics by line +--- +--- Marked private as this is used internally by the LSP subsystem, but +--- most users should instead prefer |vim.diagnostic.get()|. +--- +---@param bufnr number|nil The buffer number +---@param line_nr number|nil The line number +---@param opts table|nil Configuration keys +--- - severity: (DiagnosticSeverity, default nil) +--- - Only return diagnostics with this severity. Overrides severity_limit +--- - severity_limit: (DiagnosticSeverity, default nil) +--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. +---@param client_id|nil number the client id +---@return table Table with map of line number to list of diagnostics. +--- Structured: { [1] = {...}, [5] = {.... } } +---@private +function M.get_line_diagnostics(bufnr, line_nr, opts, client_id) + opts = opts or {} + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} end - local virtual_text_opts = resolve_optional_value(config.virtual_text) - if virtual_text_opts then - M.set_virtual_text(diagnostics, bufnr, client_id, nil, virtual_text_opts) + if client_id then + opts.namespace = M.get_namespace(client_id) end - local signs_opts = resolve_optional_value(config.signs) - if signs_opts then - M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts) + if not line_nr then + line_nr = vim.api.nvim_win_get_cursor(0)[1] - 1 end - -- cache extmarks - save_extmarks(bufnr, client_id) + opts.lnum = line_nr + + return diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, opts)) +end + +--- Get the counts for a particular severity +--- +---@deprecated Prefer |vim.diagnostic.get_count()| +--- +---@param bufnr number The buffer number +---@param severity DiagnosticSeverity +---@param client_id number the client id +function M.get_count(bufnr, severity, client_id) + severity = severity_lsp_to_vim(severity) + local opts = { severity = severity } + if client_id ~= nil then + opts.namespace = M.get_namespace(client_id) + end + + return #vim.diagnostic.get(bufnr, opts) +end + +--- Get the previous diagnostic closest to the cursor_position +--- +---@deprecated Prefer |vim.diagnostic.get_prev()| +--- +---@param opts table See |vim.lsp.diagnostic.goto_next()| +---@return table Previous diagnostic +function M.get_prev(opts) + if opts then + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + end + return diagnostic_vim_to_lsp({vim.diagnostic.get_prev(opts)})[1] +end + +--- Return the pos, {row, col}, for the prev diagnostic in the current buffer. +--- +---@deprecated Prefer |vim.diagnostic.get_prev_pos()| +--- +---@param opts table See |vim.lsp.diagnostic.goto_next()| +---@return table Previous diagnostic position +function M.get_prev_pos(opts) + if opts then + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + end + return vim.diagnostic.get_prev_pos(opts) +end + +--- Move to the previous diagnostic +--- +---@deprecated Prefer |vim.diagnostic.goto_prev()| +--- +---@param opts table See |vim.lsp.diagnostic.goto_next()| +function M.goto_prev(opts) + if opts then + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + end + return vim.diagnostic.goto_prev(opts) +end + +--- Get the next diagnostic closest to the cursor_position +--- +---@deprecated Prefer |vim.diagnostic.get_next()| +--- +---@param opts table See |vim.lsp.diagnostic.goto_next()| +---@return table Next diagnostic +function M.get_next(opts) + if opts then + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + end + return diagnostic_vim_to_lsp({vim.diagnostic.get_next(opts)})[1] +end + +--- Return the pos, {row, col}, for the next diagnostic in the current buffer. +--- +---@deprecated Prefer |vim.diagnostic.get_next_pos()| +--- +---@param opts table See |vim.lsp.diagnostic.goto_next()| +---@return table Next diagnostic position +function M.get_next_pos(opts) + if opts then + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + end + return vim.diagnostic.get_next_pos(opts) +end + +--- Move to the next diagnostic +--- +---@deprecated Prefer |vim.diagnostic.goto_next()| +--- +---@param opts table|nil Configuration table. Keys: +--- - {client_id}: (number) +--- - If nil, will consider all clients attached to buffer. +--- - {cursor_position}: (Position, default current position) +--- - See |nvim_win_get_cursor()| +--- - {wrap}: (boolean, default true) +--- - Whether to loop around file or not. Similar to 'wrapscan' +--- - {severity}: (DiagnosticSeverity) +--- - Exclusive severity to consider. Overrides {severity_limit} +--- - {severity_limit}: (DiagnosticSeverity) +--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. +--- - {enable_popup}: (boolean, default true) +--- - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump +--- - {popup_opts}: (table) +--- - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()| +--- - {win_id}: (number, default 0) +--- - Window ID +function M.goto_next(opts) + if opts then + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + end + return vim.diagnostic.goto_next(opts) +end + +--- Set signs for given diagnostics +--- +---@deprecated Prefer |vim.diagnostic._set_signs()| +--- +--- Sign characters can be customized with the following commands: +--- +---
+--- sign define LspDiagnosticsSignError text=E texthl=LspDiagnosticsSignError linehl= numhl=
+--- sign define LspDiagnosticsSignWarning text=W texthl=LspDiagnosticsSignWarning linehl= numhl=
+--- sign define LspDiagnosticsSignInformation text=I texthl=LspDiagnosticsSignInformation linehl= numhl=
+--- sign define LspDiagnosticsSignHint text=H texthl=LspDiagnosticsSignHint linehl= numhl=
+--- 
+---@param diagnostics Diagnostic[] +---@param bufnr number The buffer number +---@param client_id number the client id +---@param sign_ns number|nil +---@param opts table Configuration for signs. Keys: +--- - priority: Set the priority of the signs. +--- - severity_limit (DiagnosticSeverity): +--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. +function M.set_signs(diagnostics, bufnr, client_id, _, opts) + local namespace = M.get_namespace(client_id) + if opts and not opts.severity and opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + + local ok = vim.diagnostic._set_signs(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts) + if not ok then + log.debug("Failed to place signs:", diagnostics) + end +end + +--- Set underline for given diagnostics +--- +---@deprecated Prefer |vim.diagnostic._set_underline()| +--- +--- Underline highlights can be customized by changing the following |:highlight| groups. +--- +---
+--- LspDiagnosticsUnderlineError
+--- LspDiagnosticsUnderlineWarning
+--- LspDiagnosticsUnderlineInformation
+--- LspDiagnosticsUnderlineHint
+--- 
+--- +---@param diagnostics Diagnostic[] +---@param bufnr number: The buffer number +---@param client_id number: The client id +---@param diagnostic_ns number|nil: The namespace +---@param opts table: Configuration table: +--- - severity_limit (DiagnosticSeverity): +--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. +function M.set_underline(diagnostics, bufnr, client_id, _, opts) + local namespace = M.get_namespace(client_id) + if opts and not opts.severity and opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + return vim.diagnostic._set_underline(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts) +end + +--- Set virtual text given diagnostics +--- +---@deprecated Prefer |vim.diagnostic._set_virtual_text()| +--- +--- Virtual text highlights can be customized by changing the following |:highlight| groups. +--- +---
+--- LspDiagnosticsVirtualTextError
+--- LspDiagnosticsVirtualTextWarning
+--- LspDiagnosticsVirtualTextInformation
+--- LspDiagnosticsVirtualTextHint
+--- 
+--- +---@param diagnostics Diagnostic[] +---@param bufnr number +---@param client_id number +---@param diagnostic_ns number +---@param opts table Options on how to display virtual text. Keys: +--- - prefix (string): Prefix to display before virtual text on line +--- - spacing (number): Number of spaces to insert before virtual text +--- - severity_limit (DiagnosticSeverity): +--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. +function M.set_virtual_text(diagnostics, bufnr, client_id, _, opts) + local namespace = M.get_namespace(client_id) + if opts and not opts.severity and opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + return vim.diagnostic._set_virtual_text(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts) +end + +--- Default function to get text chunks to display using |nvim_buf_set_extmark()|. +--- +---@deprecated Prefer |vim.diagnostic.get_virt_text_chunks()| +--- +---@param bufnr number The buffer to display the virtual text in +---@param line number The line number to display the virtual text on +---@param line_diags Diagnostic[] The diagnostics associated with the line +---@param opts table See {opts} from |vim.lsp.diagnostic.set_virtual_text()| +---@return an array of [text, hl_group] arrays. This can be passed directly to +--- the {virt_text} option of |nvim_buf_set_extmark()|. +function M.get_virtual_text_chunks_for_line(bufnr, _, line_diags, opts) + return vim.diagnostic.get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts) +end + +--- Open a floating window with the diagnostics from {position} +--- +---@deprecated Prefer |vim.diagnostic.show_position_diagnostics()| +--- +---@param opts table|nil Configuration keys +--- - severity: (DiagnosticSeverity, default nil) +--- - Only return diagnostics with this severity. Overrides severity_limit +--- - severity_limit: (DiagnosticSeverity, default nil) +--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. +--- - all opts for |show_diagnostics()| can be used here +---@param buf_nr number|nil The buffer number +---@param position table|nil The (0,0)-indexed position +---@return table {popup_bufnr, win_id} +function M.show_position_diagnostics(opts, buf_nr, position) + if opts then + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + end + return vim.diagnostic.show_position_diagnostics(opts, buf_nr, position) +end + +--- Open a floating window with the diagnostics from {line_nr} +--- +---@deprecated Prefer |vim.diagnostic.show_line_diagnostics()| +--- +---@param opts table Configuration table +--- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and +--- |show_diagnostics()| can be used here +---@param buf_nr number|nil The buffer number +---@param line_nr number|nil The line number +---@param client_id number|nil the client id +---@return table {popup_bufnr, win_id} +function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id) + if client_id then + opts = opts or {} + opts.namespace = M.get_namespace(client_id) + end + return vim.diagnostic.show_line_diagnostics(opts, buf_nr, line_nr) end --- Redraw diagnostics for the given buffer and client --- +---@deprecated Prefer |vim.diagnostic.redraw()| +--- --- This calls the "textDocument/publishDiagnostics" handler manually using --- the cached diagnostics already received from the server. This can be useful --- for redrawing diagnostics after making changes in diagnostics @@ -1200,183 +610,14 @@ function M.redraw(bufnr, client_id) end) end - -- We need to invoke the publishDiagnostics handler directly instead of just - -- calling M.display so that we can preserve any custom configuration options - -- the user may have set with vim.lsp.with. - vim.lsp.handlers["textDocument/publishDiagnostics"]( - nil, - { - uri = vim.uri_from_bufnr(bufnr), - diagnostics = M.get(bufnr, client_id), - }, - { - method = "textDocument/publishDiagnostics", - client_id = client_id, - bufnr = bufnr, - } - ) - end - - ----@private ---- Open a floating window with the provided diagnostics ---- ---- The floating window can be customized with the following highlight groups: ----
---- LspDiagnosticsFloatingError
---- LspDiagnosticsFloatingWarning
---- LspDiagnosticsFloatingInformation
---- LspDiagnosticsFloatingHint
---- 
----@param opts table Configuration table ---- - show_header (boolean, default true): Show "Diagnostics:" header ---- - all opts for |vim.lsp.util.open_floating_preview()| can be used here ----@param diagnostics table: The diagnostics to display ----@return table {popup_bufnr, win_id} -local function show_diagnostics(opts, diagnostics) - if vim.tbl_isempty(diagnostics) then return end - local lines = {} - local highlights = {} - local show_header = if_nil(opts.show_header, true) - if show_header then - table.insert(lines, "Diagnostics:") - table.insert(highlights, {0, "Bold"}) - end - - for i, diagnostic in ipairs(diagnostics) do - local prefix = string.format("%d. ", i) - local hiname = M._get_floating_severity_highlight_name(diagnostic.severity) - assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity)) - - local message_lines = vim.split(diagnostic.message, '\n', true) - table.insert(lines, prefix..message_lines[1]) - table.insert(highlights, {#prefix, hiname}) - for j = 2, #message_lines do - table.insert(lines, string.rep(' ', #prefix) .. message_lines[j]) - table.insert(highlights, {0, hiname}) - end - end - - local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts) - for i, hi in ipairs(highlights) do - local prefixlen, hiname = unpack(hi) - -- Start highlight after the prefix - api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1) - end - - return popup_bufnr, winnr -end - - --- }}} --- Diagnostic User Functions {{{ - ---- Open a floating window with the diagnostics from {position} ----@param opts table|nil Configuration keys ---- - severity: (DiagnosticSeverity, default nil) ---- - Only return diagnostics with this severity. Overrides severity_limit ---- - severity_limit: (DiagnosticSeverity, default nil) ---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. ---- - all opts for |show_diagnostics()| can be used here ----@param buf_nr number|nil The buffer number ----@param position table|nil The (0,0)-indexed position ----@return table {popup_bufnr, win_id} -function M.show_position_diagnostics(opts, buf_nr, position) - opts = opts or {} - opts.focus_id = "position_diagnostics" - buf_nr = buf_nr or vim.api.nvim_get_current_buf() - if not position then - local curr_position = vim.api.nvim_win_get_cursor(0) - curr_position[1] = curr_position[1] - 1 - position = curr_position - end - local match_position_predicate = function(diag) - return position[1] == diag.range['start'].line and - position[2] >= diag.range['start'].character and - (position[2] <= diag.range['end'].character or position[1] < diag.range['end'].line) - end - local position_diagnostics = M.get(buf_nr, nil, match_position_predicate) - if opts.severity then - position_diagnostics = filter_to_severity_limit(opts.severity, position_diagnostics) - elseif opts.severity_limit then - position_diagnostics = filter_by_severity_limit(opts.severity_limit, position_diagnostics) - end - table.sort(position_diagnostics, function(a, b) return a.severity < b.severity end) - return show_diagnostics(opts, position_diagnostics) -end - ---- Open a floating window with the diagnostics from {line_nr} - ----@param opts table Configuration table ---- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and ---- |show_diagnostics()| can be used here ----@param buf_nr number|nil The buffer number ----@param line_nr number|nil The line number ----@param client_id number|nil the client id ----@return table {popup_bufnr, win_id} -function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id) - opts = opts or {} - opts.focus_id = "line_diagnostics" - line_nr = line_nr or (vim.api.nvim_win_get_cursor(0)[1] - 1) - local line_diagnostics = M.get_line_diagnostics(buf_nr, line_nr, opts, client_id) - return show_diagnostics(opts, line_diagnostics) -end - ---- Clear diagnotics and diagnostic cache ---- ---- Handles saving diagnostics from multiple clients in the same buffer. ----@param client_id number ----@param buffer_client_map table map of buffers to active clients -function M.reset(client_id, buffer_client_map) - buffer_client_map = vim.deepcopy(buffer_client_map) - vim.schedule(function() - for bufnr, client_ids in pairs(buffer_client_map) do - if client_ids[client_id] then - clear_diagnostic_cache(bufnr, client_id) - M.clear(bufnr, client_id) - end - end - end) -end - ----@private ---- Gets diagnostics, converts them to quickfix/location list items, and applies the item_handler callback to the items. ----@param item_handler function Callback to apply to the diagnostic items ----@param command string|nil Command to execute after applying the item_handler ----@param opts table|nil Configuration table. Keys: ---- - {client_id}: (number) ---- - If nil, will consider all clients attached to buffer. ---- - {severity}: (DiagnosticSeverity) ---- - Exclusive severity to consider. Overrides {severity_limit} ---- - {severity_limit}: (DiagnosticSeverity) ---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. ---- - {workspace}: (boolean, default false) ---- - Set the list with workspace diagnostics -local function apply_to_diagnostic_items(item_handler, command, opts) - opts = opts or {} - local current_bufnr = api.nvim_get_current_buf() - local diags = opts.workspace and M.get_all(opts.client_id) or { - [current_bufnr] = M.get(current_bufnr, opts.client_id) - } - local predicate = function(d) - local severity = to_severity(opts.severity) - if severity then - return d.severity == severity - end - local severity_limit = to_severity(opts.severity_limit) - if severity_limit then - return d.severity <= severity_limit - end - return true - end - local items = util.diagnostics_to_items(diags, predicate) - item_handler(items) - if command then - vim.cmd(command) - end + local namespace = M.get_namespace(client_id) + return vim.diagnostic.show(namespace, bufnr) end --- Sets the quickfix list +--- +---@deprecated Prefer |vim.diagnostic.setqflist()| +--- ---@param opts table|nil Configuration table. Keys: --- - {open}: (boolean, default true) --- - Open quickfix list after set @@ -1390,13 +631,24 @@ end --- - Set the list with workspace diagnostics function M.set_qflist(opts) opts = opts or {} - opts.workspace = if_nil(opts.workspace, true) - local open_qflist = if_nil(opts.open, true) - local command = open_qflist and [[copen]] or nil - apply_to_diagnostic_items(util.set_qflist, command, opts) + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + if opts.client_id then + opts.client_id = nil + opts.namespace = M.get_namespace(opts.client_id) + end + local workspace = vim.F.if_nil(opts.workspace, true) + opts.bufnr = not workspace and 0 + return vim.diagnostic.setqflist(opts) end --- Sets the location list +--- +---@deprecated Prefer |vim.diagnostic.setloclist()| +--- ---@param opts table|nil Configuration table. Keys: --- - {open}: (boolean, default true) --- - Open loclist after set @@ -1410,12 +662,24 @@ end --- - Set the list with workspace diagnostics function M.set_loclist(opts) opts = opts or {} - local open_loclist = if_nil(opts.open, true) - local command = open_loclist and [[lopen]] or nil - apply_to_diagnostic_items(util.set_loclist, command, opts) + if opts.severity then + opts.severity = severity_lsp_to_vim(opts.severity) + elseif opts.severity_limit then + opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + end + if opts.client_id then + opts.client_id = nil + opts.namespace = M.get_namespace(opts.client_id) + end + local workspace = vim.F.if_nil(opts.workspace, false) + opts.bufnr = not workspace and 0 + return vim.diagnostic.setloclist(opts) end --- Disable diagnostics for the given buffer and client +--- +---@deprecated Prefer |vim.diagnostic.disable()| +--- ---@param bufnr (optional, number): Buffer handle, defaults to current ---@param client_id (optional, number): Disable diagnostics for the given --- client. The default is to disable diagnostics for all attached @@ -1430,11 +694,15 @@ function M.disable(bufnr, client_id) end) end - diagnostic_disabled[bufnr][client_id] = true - M.clear(bufnr, client_id) + bufnr = get_bufnr(bufnr) + local namespace = M.get_namespace(client_id) + return vim.diagnostic.disable(bufnr, namespace) end --- Enable diagnostics for the given buffer and client +--- +---@deprecated Prefer |vim.diagnostic.enable()| +--- ---@param bufnr (optional, number): Buffer handle, defaults to current ---@param client_id (optional, number): Enable diagnostics for the given --- client. The default is to enable diagnostics for all attached @@ -1446,14 +714,13 @@ function M.enable(bufnr, client_id) end) end - if not diagnostic_disabled[bufnr][client_id] then - return - end - - diagnostic_disabled[bufnr][client_id] = nil - - M.redraw(bufnr, client_id) + bufnr = get_bufnr(bufnr) + local namespace = M.get_namespace(client_id) + return vim.diagnostic.enable(bufnr, namespace) end + -- }}} return M + +-- vim: fdm=marker diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index dd766496f4..918666ab27 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -210,10 +210,16 @@ local function response_to_list(map_result, entity) else config = config or {} if config.loclist then - util.set_loclist(map_result(result, ctx.bufnr)) + vim.fn.setloclist(0, {}, ' ', { + title = 'Language Server'; + items = map_result(result, ctx.bufnr); + }) api.nvim_command("lopen") else - util.set_qflist(map_result(result, ctx.bufnr)) + vim.fn.setqflist({}, ' ', { + title = 'Language Server'; + items = map_result(result, ctx.bufnr); + }) api.nvim_command("copen") end end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 5a22a311e0..9a3ce185a0 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -31,16 +31,6 @@ local default_border = { {" ", "NormalFloat"}, } - -local DiagnosticSeverity = protocol.DiagnosticSeverity -local loclist_type_map = { - [DiagnosticSeverity.Error] = 'E', - [DiagnosticSeverity.Warning] = 'W', - [DiagnosticSeverity.Information] = 'I', - [DiagnosticSeverity.Hint] = 'I', -} - - ---@private --- Check the border given by opts or the default border for the additional --- size it adds to a float. @@ -1543,6 +1533,9 @@ end --- Returns the items with the byte position calculated correctly and in sorted --- order, for display in quickfix and location lists. --- +--- The result can be passed to the {list} argument of |setqflist()| or +--- |setloclist()|. +--- ---@param locations (table) list of `Location`s or `LocationLink`s ---@returns (table) list of items function M.locations_to_items(locations) @@ -1601,6 +1594,8 @@ end --- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. --- Defaults to current window. --- +---@deprecated Use |setloclist()| +--- ---@param items (table) list of items function M.set_loclist(items, win_id) vim.fn.setloclist(win_id or 0, {}, ' ', { @@ -1612,6 +1607,8 @@ end --- Fills quickfix list with given list of items. --- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. --- +---@deprecated Use |setqflist()| +--- ---@param items (table) list of items function M.set_qflist(items) vim.fn.setqflist({}, ' ', { @@ -1869,40 +1866,6 @@ function M.lookup_section(settings, section) return settings end - ---- Convert diagnostics grouped by bufnr to a list of items for use in the ---- quickfix or location list. ---- ----@param diagnostics_by_bufnr table bufnr -> Diagnostic[] ----@param predicate an optional function to filter the diagnostics. ---- If present, only diagnostic items matching will be included. ----@return table (A list of items) -function M.diagnostics_to_items(diagnostics_by_bufnr, predicate) - local items = {} - for bufnr, diagnostics in pairs(diagnostics_by_bufnr or {}) do - for _, d in pairs(diagnostics) do - if not predicate or predicate(d) then - table.insert(items, { - bufnr = bufnr, - lnum = d.range.start.line + 1, - col = d.range.start.character + 1, - text = d.message, - type = loclist_type_map[d.severity or DiagnosticSeverity.Error] or 'E' - }) - end - end - end - table.sort(items, function(a, b) - if a.bufnr == b.bufnr then - return a.lnum < b.lnum - else - return a.bufnr < b.bufnr - end - end) - return items -end - - M._get_line_byte_from_position = get_line_byte_from_position M._warn_once = warn_once diff --git a/runtime/plugin/diagnostic.vim b/runtime/plugin/diagnostic.vim new file mode 100644 index 0000000000..569c63bdba --- /dev/null +++ b/runtime/plugin/diagnostic.vim @@ -0,0 +1,48 @@ +" :help vim.diagnostic + +hi DiagnosticError ctermfg=1 guifg=Red +hi DiagnosticWarn ctermfg=3 guifg=Orange +hi DiagnosticInfo ctermfg=4 guifg=LightBlue +hi DiagnosticHint ctermfg=7 guifg=LightGrey + +hi DiagnosticUnderlineError cterm=underline gui=underline guisp=Red +hi DiagnosticUnderlineWarn cterm=underline gui=underline guisp=Orange +hi DiagnosticUnderlineInfo cterm=underline gui=underline guisp=LightBlue +hi DiagnosticUnderlineHint cterm=underline gui=underline guisp=LightGrey + +hi link DiagnosticVirtualTextError DiagnosticError +hi link DiagnosticVirtualTextWarn DiagnosticWarn +hi link DiagnosticVirtualTextInfo DiagnosticInfo +hi link DiagnosticVirtualTextHint DiagnosticHint + +hi link DiagnosticFloatingError DiagnosticError +hi link DiagnosticFloatingWarn DiagnosticWarn +hi link DiagnosticFloatingInfo DiagnosticInfo +hi link DiagnosticFloatingHint DiagnosticHint + +hi link DiagnosticSignError DiagnosticError +hi link DiagnosticSignWarn DiagnosticWarn +hi link DiagnosticSignInfo DiagnosticInfo +hi link DiagnosticSignHint DiagnosticHint + +" Link LspDiagnostics for backward compatibility +hi link LspDiagnosticsDefaultHint DiagnosticHint +hi link LspDiagnosticsVirtualTextHint DiagnosticVirtualTextHint +hi link LspDiagnosticsFloatingHint DiagnosticFloatingHint +hi link LspDiagnosticsSignHint DiagnosticSignHint +hi link LspDiagnosticsDefaultError DiagnosticError +hi link LspDiagnosticsVirtualTextError DiagnosticVirtualTextError +hi link LspDiagnosticsFloatingError DiagnosticFloatingError +hi link LspDiagnosticsSignError DiagnosticSignError +hi link LspDiagnosticsDefaultWarning DiagnosticWarn +hi link LspDiagnosticsVirtualTextWarning DiagnosticVirtualTextWarn +hi link LspDiagnosticsFloatingWarning DiagnosticFloatingWarn +hi link LspDiagnosticsSignWarning DiagnosticSignWarn +hi link LspDiagnosticsDefaultInformation DiagnosticInfo +hi link LspDiagnosticsVirtualTextInformation DiagnosticVirtualTextInfo +hi link LspDiagnosticsFloatingInformation DiagnosticFloatingInfo +hi link LspDiagnosticsSignInformation DiagnosticSignInfo +hi link LspDiagnosticsUnderlineError DiagnosticUnderlineError +hi link LspDiagnosticsUnderlineWarning DiagnosticUnderlineWarn +hi link LspDiagnosticsUnderlineInformation DiagnosticUnderlineInfo +hi link LspDiagnosticsUnderlineHint DiagnosticUnderlineHint diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py index c8eb71985c..64ed8d61f6 100755 --- a/scripts/gen_vimdoc.py +++ b/scripts/gen_vimdoc.py @@ -187,6 +187,23 @@ CONFIG = { 'module_override': {}, 'append_only': [], }, + 'diagnostic': { + 'mode': 'lua', + 'filename': 'diagnostic.txt', + 'section_start_token': '*diagnostic-api*', + 'section_order': [ + 'diagnostic.lua', + ], + 'files': os.path.join(base_dir, 'runtime/lua/vim/diagnostic.lua'), + 'file_patterns': '*.lua', + 'fn_name_prefix': '', + 'section_name': {'diagnostic.lua': 'diagnostic'}, + 'section_fmt': lambda _: 'Lua module: vim.diagnostic', + 'helptag_fmt': lambda _: '*diagnostic-api*', + 'fn_helptag_fmt': lambda fstem, name: f'*vim.{fstem}.{name}()*', + 'module_override': {}, + 'append_only': [], + }, 'treesitter': { 'mode': 'lua', 'filename': 'treesitter.txt', diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index c6bbdee7ad..ba124c41ad 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -105,6 +105,9 @@ setmetatable(vim, { elseif key == 'highlight' then t.highlight = require('vim.highlight') return t.highlight + elseif key == 'diagnostic' then + t.diagnostic = require('vim.diagnostic') + return t.diagnostic end end }) diff --git a/test/README.md b/test/README.md index 7a93c45f1c..37aa54c157 100644 --- a/test/README.md +++ b/test/README.md @@ -263,6 +263,8 @@ Number; !must be defined to function properly): - `TEST_TIMEOUT` (FU) (I): specifies maximum time, in seconds, before the test suite run is killed +- `NVIM_LUA_NOTRACK` (F) (D): disable reference counting of Lua objects + - `NVIM_PROG`, `NVIM_PRG` (F) (S): override path to Neovim executable (default to `build/bin/nvim`). diff --git a/test/functional/lua/diagnostic_spec.lua b/test/functional/lua/diagnostic_spec.lua new file mode 100644 index 0000000000..8da33173a2 --- /dev/null +++ b/test/functional/lua/diagnostic_spec.lua @@ -0,0 +1,831 @@ +local helpers = require('test.functional.helpers')(after_each) + +local command = helpers.command +local clear = helpers.clear +local exec_lua = helpers.exec_lua +local eq = helpers.eq +local nvim = helpers.nvim + +describe('vim.diagnostic', function() + before_each(function() + clear() + + exec_lua [[ + require('vim.diagnostic') + + function make_error(msg, x1, y1, x2, y2) + return { + lnum = x1, + col = y1, + end_lnum = x2, + end_col = y2, + message = msg, + severity = vim.diagnostic.severity.ERROR, + } + end + + function make_warning(msg, x1, y1, x2, y2) + return { + lnum = x1, + col = y1, + end_lnum = x2, + end_col = y2, + message = msg, + severity = vim.diagnostic.severity.WARN, + } + end + + function make_information(msg, x1, y1, x2, y2) + return { + lnum = x1, + col = y1, + end_lnum = x2, + end_col = y2, + message = msg, + severity = vim.diagnostic.severity.INFO, + } + end + + function make_hint(msg, x1, y1, x2, y2) + return { + lnum = x1, + col = y1, + end_lnum = x2, + end_col = y2, + message = msg, + severity = vim.diagnostic.severity.HINT, + } + end + + function count_diagnostics(bufnr, severity, namespace) + return #vim.diagnostic.get(bufnr, {severity = severity, namespace = namespace}) + end + + function count_extmarks(bufnr, namespace) + return #vim.api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, {}) + end + ]] + + exec_lua([[ + diagnostic_ns = vim.api.nvim_create_namespace("diagnostic_spec") + other_ns = vim.api.nvim_create_namespace("other_namespace") + diagnostic_bufnr = vim.api.nvim_create_buf(true, false) + local lines = {"1st line of text", "2nd line of text", "wow", "cool", "more", "lines"} + vim.fn.bufload(diagnostic_bufnr) + vim.api.nvim_buf_set_lines(diagnostic_bufnr, 0, 1, false, lines) + return diagnostic_bufnr + ]]) + end) + + after_each(function() + clear() + end) + + it('creates highlight groups', function() + command('runtime plugin/diagnostic.vim') + eq({ + 'DiagnosticError', + 'DiagnosticFloatingError', + 'DiagnosticFloatingHint', + 'DiagnosticFloatingInfo', + 'DiagnosticFloatingWarn', + 'DiagnosticHint', + 'DiagnosticInfo', + 'DiagnosticSignError', + 'DiagnosticSignHint', + 'DiagnosticSignInfo', + 'DiagnosticSignWarn', + 'DiagnosticUnderlineError', + 'DiagnosticUnderlineHint', + 'DiagnosticUnderlineInfo', + 'DiagnosticUnderlineWarn', + 'DiagnosticVirtualTextError', + 'DiagnosticVirtualTextHint', + 'DiagnosticVirtualTextInfo', + 'DiagnosticVirtualTextWarn', + 'DiagnosticWarn', + }, exec_lua([[return vim.fn.getcompletion('Diagnostic', 'highlight')]])) + end) + + it('retrieves diagnostics from all buffers and namespaces', function() + local result = exec_lua [[ + vim.diagnostic.set(diagnostic_ns, 1, { + make_error('Diagnostic #1', 1, 1, 1, 1), + make_error('Diagnostic #2', 2, 1, 2, 1), + }) + vim.diagnostic.set(other_ns, 2, { + make_error('Diagnostic #3', 3, 1, 3, 1), + }) + return vim.diagnostic.get() + ]] + eq(3, #result) + eq(2, exec_lua([[return #vim.tbl_filter(function(d) return d.bufnr == 1 end, ...)]], result)) + eq('Diagnostic #1', result[1].message) + end) + + it('saves and count a single error', function() + eq(1, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #1', 1, 1, 1, 1), + }) + return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns) + ]]) + end) + + it('saves and count multiple errors', function() + eq(2, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #1', 1, 1, 1, 1), + make_error('Diagnostic #2', 2, 1, 2, 1), + }) + return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns) + ]]) + end) + + it('saves and count from multiple namespaces', function() + eq({1, 1, 2}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic From Server 1', 1, 1, 1, 1), + }) + vim.diagnostic.set(other_ns, diagnostic_bufnr, { + make_error('Diagnostic From Server 2', 1, 1, 1, 1), + }) + return { + -- First namespace + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns), + -- Second namespace + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, other_ns), + -- All namespaces + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR), + } + ]]) + end) + + it('saves and count from multiple namespaces with respect to severity', function() + eq({3, 0, 3}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic From Server 1:1', 1, 1, 1, 1), + make_error('Diagnostic From Server 1:2', 2, 2, 2, 2), + make_error('Diagnostic From Server 1:3', 2, 3, 3, 2), + }) + vim.diagnostic.set(other_ns, diagnostic_bufnr, { + make_warning('Warning From Server 2', 3, 3, 3, 3), + }) + return { + -- Namespace 1 + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns), + -- Namespace 2 + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, other_ns), + -- All namespaces + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR), + } + ]]) + end) + + it('handles one namespace clearing highlights while the other still has highlights', function() + -- 1 Error (1) + -- 1 Warning (2) + -- 1 Warning (2) + 1 Warning (1) + -- 2 highlights and 2 underlines (since error) + -- 1 highlight + 1 underline + local all_highlights = {1, 1, 2, 4, 2} + eq(all_highlights, exec_lua [[ + local ns_1_diags = { + make_error("Error 1", 1, 1, 1, 5), + make_warning("Warning on Server 1", 2, 1, 2, 5), + } + local ns_2_diags = { + make_warning("Warning 1", 2, 1, 2, 5), + } + + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, ns_1_diags) + vim.diagnostic.set(other_ns, diagnostic_bufnr, ns_2_diags) + + return { + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN, other_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN), + count_extmarks(diagnostic_bufnr, diagnostic_ns), + count_extmarks(diagnostic_bufnr, other_ns), + } + ]]) + + -- Clear diagnostics from namespace 1, and make sure we have the right amount of stuff for namespace 2 + eq({1, 1, 2, 0, 2}, exec_lua [[ + vim.diagnostic.disable(diagnostic_bufnr, diagnostic_ns) + return { + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN, other_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN), + count_extmarks(diagnostic_bufnr, diagnostic_ns), + count_extmarks(diagnostic_bufnr, other_ns), + } + ]]) + + -- Show diagnostics from namespace 1 again + eq(all_highlights, exec_lua([[ + vim.diagnostic.enable(diagnostic_bufnr, diagnostic_ns) + return { + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN, other_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN), + count_extmarks(diagnostic_bufnr, diagnostic_ns), + count_extmarks(diagnostic_bufnr, other_ns), + } + ]])) + end) + + it('does not display diagnostics when disabled', function() + eq({0, 2}, exec_lua [[ + local ns_1_diags = { + make_error("Error 1", 1, 1, 1, 5), + make_warning("Warning on Server 1", 2, 1, 2, 5), + } + local ns_2_diags = { + make_warning("Warning 1", 2, 1, 2, 5), + } + + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, ns_1_diags) + vim.diagnostic.set(other_ns, diagnostic_bufnr, ns_2_diags) + + vim.diagnostic.disable(diagnostic_bufnr, diagnostic_ns) + + return { + count_extmarks(diagnostic_bufnr, diagnostic_ns), + count_extmarks(diagnostic_bufnr, other_ns), + } + ]]) + + eq({4, 0}, exec_lua [[ + vim.diagnostic.enable(diagnostic_bufnr, diagnostic_ns) + vim.diagnostic.disable(diagnostic_bufnr, other_ns) + + return { + count_extmarks(diagnostic_bufnr, diagnostic_ns), + count_extmarks(diagnostic_bufnr, other_ns), + } + ]]) + end) + + describe('reset()', function() + it('diagnostic count is 0 and displayed diagnostics are 0 after call', function() + -- 1 Error (1) + -- 1 Warning (2) + -- 1 Warning (2) + 1 Warning (1) + -- 2 highlights and 2 underlines (since error) + -- 1 highlight + 1 underline + local all_highlights = {1, 1, 2, 4, 2} + eq(all_highlights, exec_lua [[ + local ns_1_diags = { + make_error("Error 1", 1, 1, 1, 5), + make_warning("Warning on Server 1", 2, 1, 2, 5), + } + local ns_2_diags = { + make_warning("Warning 1", 2, 1, 2, 5), + } + + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, ns_1_diags) + vim.diagnostic.set(other_ns, diagnostic_bufnr, ns_2_diags) + + return { + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN, other_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN), + count_extmarks(diagnostic_bufnr, diagnostic_ns), + count_extmarks(diagnostic_bufnr, other_ns), + } + ]]) + + -- Reset diagnostics from namespace 1 + exec_lua([[ vim.diagnostic.reset(diagnostic_ns) ]]) + + -- Make sure we have the right diagnostic count + eq({0, 1, 1, 0, 2} , exec_lua [[ + local diagnostic_count = {} + vim.wait(100, function () diagnostic_count = { + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN, other_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN), + count_extmarks(diagnostic_bufnr, diagnostic_ns), + count_extmarks(diagnostic_bufnr, other_ns), + } end ) + return diagnostic_count + ]]) + + -- Reset diagnostics from namespace 2 + exec_lua([[ vim.diagnostic.reset(other_ns) ]]) + + -- Make sure we have the right diagnostic count + eq({0, 0, 0, 0, 0}, exec_lua [[ + local diagnostic_count = {} + vim.wait(100, function () diagnostic_count = { + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN, other_ns), + count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.WARN), + count_extmarks(diagnostic_bufnr, diagnostic_ns), + count_extmarks(diagnostic_bufnr, other_ns), + } end ) + return diagnostic_count + ]]) + + end) + end) + + describe('get_next_pos()', function() + it('can find the next pos with only one namespace', function() + eq({1, 1}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #1', 1, 1, 1, 1), + }) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + return vim.diagnostic.get_next_pos() + ]]) + end) + + it('can find next pos with two errors', function() + eq({4, 4}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #1', 1, 1, 1, 1), + make_error('Diagnostic #2', 4, 4, 4, 4), + }) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.diagnostic.get_next_pos { namespace = diagnostic_ns } + ]]) + end) + + it('can cycle when position is past error', function() + eq({1, 1}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #1', 1, 1, 1, 1), + }) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.diagnostic.get_next_pos { namespace = diagnostic_ns } + ]]) + end) + + it('will not cycle when wrap is off', function() + eq(false, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #1', 1, 1, 1, 1), + }) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.diagnostic.get_next_pos { namespace = diagnostic_ns, wrap = false } + ]]) + end) + + it('can cycle even from the last line', function() + eq({4, 4}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #2', 4, 4, 4, 4), + }) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {vim.api.nvim_buf_line_count(0), 1}) + return vim.diagnostic.get_prev_pos { namespace = diagnostic_ns } + ]]) + end) + end) + + describe('get_prev_pos()', function() + it('can find the prev pos with only one namespace', function() + eq({1, 1}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #1', 1, 1, 1, 1), + }) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.diagnostic.get_prev_pos() + ]]) + end) + + it('can find prev pos with two errors', function() + eq({1, 1}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #1', 1, 1, 1, 1), + make_error('Diagnostic #2', 4, 4, 4, 4), + }) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.diagnostic.get_prev_pos { namespace = diagnostic_ns } + ]]) + end) + + it('can cycle when position is past error', function() + eq({4, 4}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #2', 4, 4, 4, 4), + }) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.diagnostic.get_prev_pos { namespace = diagnostic_ns } + ]]) + end) + + it('respects wrap parameter', function() + eq(false, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic #2', 4, 4, 4, 4), + }) + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.api.nvim_win_set_cursor(0, {3, 1}) + return vim.diagnostic.get_prev_pos { namespace = diagnostic_ns, wrap = false} + ]]) + end) + end) + + describe('get()', function() + it('returns an empty table when no diagnostics are present', function() + eq({}, exec_lua [[return vim.diagnostic.get(diagnostic_bufnr, {namespace=diagnostic_ns})]]) + end) + + it('returns all diagnostics when no severity is supplied', function() + eq(2, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error("Error 1", 1, 1, 1, 5), + make_warning("Warning on Server 1", 1, 1, 2, 5), + }) + + return #vim.diagnostic.get(diagnostic_bufnr) + ]]) + end) + + it('returns only requested diagnostics when severity is supplied', function() + eq({2, 3, 2}, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error("Error 1", 1, 1, 1, 5), + make_warning("Warning on Server 1", 1, 1, 2, 5), + make_information("Ignored information", 1, 1, 2, 5), + make_hint("Here's a hint", 1, 1, 2, 5), + }) + + return { + #vim.diagnostic.get(diagnostic_bufnr, { severity = {min=vim.diagnostic.severity.WARN} }), + #vim.diagnostic.get(diagnostic_bufnr, { severity = {max=vim.diagnostic.severity.WARN} }), + #vim.diagnostic.get(diagnostic_bufnr, { + severity = { + min=vim.diagnostic.severity.INFO, + max=vim.diagnostic.severity.WARN, + } + }), + } + ]]) + end) + + it('allows filtering by line', function() + eq(1, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error("Error 1", 1, 1, 1, 5), + make_warning("Warning on Server 1", 1, 1, 2, 5), + make_information("Ignored information", 1, 1, 2, 5), + make_error("Error On Other Line", 2, 1, 1, 5), + }) + + return #vim.diagnostic.get(diagnostic_bufnr, {lnum = 2}) + ]]) + end) + end) + + describe('config()', function() + it('can use functions for config values', function() + exec_lua [[ + vim.diagnostic.config({ + virtual_text = function() return true end, + }, diagnostic_ns) + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + }) + ]] + + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(2, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + + -- Now, don't enable virtual text. + -- We should have one less extmark displayed. + exec_lua [[ + vim.diagnostic.config({ + virtual_text = function() return false end, + }, diagnostic_ns) + ]] + + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(1, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + end) + + it('allows filtering by severity', function() + local get_extmark_count_with_severity = function(min_severity) + return exec_lua([[ + vim.diagnostic.config({ + underline = false, + virtual_text = { + severity = {min=...}, + }, + }) + + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_warning('Delayed Diagnostic', 4, 4, 4, 4), + }) + + return count_extmarks(diagnostic_bufnr, diagnostic_ns) + ]], min_severity) + end + + -- No messages with Error or higher + eq(0, get_extmark_count_with_severity("ERROR")) + + -- But now we don't filter it + eq(1, get_extmark_count_with_severity("WARN")) + eq(1, get_extmark_count_with_severity("HINT")) + end) + end) + + describe('set()', function() + it('can perform updates after insert_leave', function() + exec_lua [[vim.api.nvim_set_current_buf(diagnostic_bufnr)]] + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + -- Save the diagnostics + exec_lua [[ + vim.diagnostic.config({ + update_in_insert = false, + }) + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + }) + ]] + + -- No diagnostics displayed yet. + eq({mode='i', blocking=false}, nvim("get_mode")) + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(0, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + + nvim("input", "") + eq({mode='n', blocking=false}, nvim("get_mode")) + + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(2, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + end) + + it('does not perform updates when not needed', function() + exec_lua [[vim.api.nvim_set_current_buf(diagnostic_bufnr)]] + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + -- Save the diagnostics + exec_lua [[ + vim.diagnostic.config({ + update_in_insert = false, + virtual_text = true, + }) + + -- Count how many times we call display. + SetVirtualTextOriginal = vim.diagnostic._set_virtual_text + + DisplayCount = 0 + vim.diagnostic._set_virtual_text = function(...) + DisplayCount = DisplayCount + 1 + return SetVirtualTextOriginal(...) + end + + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + }) + ]] + + -- No diagnostics displayed yet. + eq({mode='i', blocking=false}, nvim("get_mode")) + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(0, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + eq(0, exec_lua [[return DisplayCount]]) + + nvim("input", "") + eq({mode='n', blocking=false}, nvim("get_mode")) + + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(2, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + eq(1, exec_lua [[return DisplayCount]]) + + -- Go in and out of insert mode one more time. + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + nvim("input", "") + eq({mode='n', blocking=false}, nvim("get_mode")) + + -- Should not have set the virtual text again. + eq(1, exec_lua [[return DisplayCount]]) + end) + + it('never sets virtual text, in combination with insert leave', function() + exec_lua [[vim.api.nvim_set_current_buf(diagnostic_bufnr)]] + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + -- Save the diagnostics + exec_lua [[ + vim.diagnostic.config({ + update_in_insert = false, + virtual_text = false, + }) + + -- Count how many times we call display. + SetVirtualTextOriginal = vim.diagnostic._set_virtual_text + + DisplayCount = 0 + vim.diagnostic._set_virtual_text = function(...) + DisplayCount = DisplayCount + 1 + return SetVirtualTextOriginal(...) + end + + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + }) + ]] + + -- No diagnostics displayed yet. + eq({mode='i', blocking=false}, nvim("get_mode")) + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(0, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + eq(0, exec_lua [[return DisplayCount]]) + + nvim("input", "") + eq({mode='n', blocking=false}, nvim("get_mode")) + + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(1, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + eq(0, exec_lua [[return DisplayCount]]) + + -- Go in and out of insert mode one more time. + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + nvim("input", "") + eq({mode='n', blocking=false}, nvim("get_mode")) + + -- Should not have set the virtual text still. + eq(0, exec_lua [[return DisplayCount]]) + end) + + it('can perform updates while in insert mode, if desired', function() + exec_lua [[vim.api.nvim_set_current_buf(diagnostic_bufnr)]] + nvim("input", "o") + eq({mode='i', blocking=false}, nvim("get_mode")) + + -- Save the diagnostics + exec_lua [[ + vim.diagnostic.config({ + update_in_insert = true, + }) + + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Delayed Diagnostic', 4, 4, 4, 4), + }) + ]] + + -- Diagnostics are displayed, because the user wanted them that way! + eq({mode='i', blocking=false}, nvim("get_mode")) + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(2, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + + nvim("input", "") + eq({mode='n', blocking=false}, nvim("get_mode")) + + eq(1, exec_lua [[return count_diagnostics(diagnostic_bufnr, vim.diagnostic.severity.ERROR, diagnostic_ns)]]) + eq(2, exec_lua [[return count_extmarks(diagnostic_bufnr, diagnostic_ns)]]) + end) + + it('can set diagnostics without displaying them', function() + eq(0, exec_lua [[ + vim.diagnostic.disable(diagnostic_bufnr, diagnostic_ns) + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic From Server 1:1', 1, 1, 1, 1), + }) + return count_extmarks(diagnostic_bufnr, diagnostic_ns) + ]]) + + eq(2, exec_lua [[ + vim.diagnostic.enable(diagnostic_bufnr, diagnostic_ns) + return count_extmarks(diagnostic_bufnr, diagnostic_ns) + ]]) + end) + + it('can set display options', function() + eq(0, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic From Server 1:1', 1, 1, 1, 1), + }, { virtual_text = false, underline = false }) + return count_extmarks(diagnostic_bufnr, diagnostic_ns) + ]]) + + eq(1, exec_lua [[ + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Diagnostic From Server 1:1', 1, 1, 1, 1), + }, { virtual_text = true, underline = false }) + return count_extmarks(diagnostic_bufnr, diagnostic_ns) + ]]) + end) + end) + + describe('show_line_diagnostics()', function() + it('creates floating window and returns popup bufnr and winnr if current line contains diagnostics', function() + -- Two lines: + -- Diagnostic: + -- 1. + eq(2, exec_lua [[ + local diagnostics = { + make_error("Syntax error", 0, 1, 0, 3), + } + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, diagnostics) + local popup_bufnr, winnr = vim.diagnostic.show_line_diagnostics() + return #vim.api.nvim_buf_get_lines(popup_bufnr, 0, -1, false) + ]]) + end) + + it('creates floating window and returns popup bufnr and winnr without header, if requested', function() + -- One line (since no header): + -- 1. + eq(1, exec_lua [[ + local diagnostics = { + make_error("Syntax error", 0, 1, 0, 3), + } + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, diagnostics) + local popup_bufnr, winnr = vim.diagnostic.show_line_diagnostics {show_header = false} + return #vim.api.nvim_buf_get_lines(popup_bufnr, 0, -1, false) + ]]) + end) + end) + + describe('set_signs()', function() + -- TODO(tjdevries): Find out why signs are not displayed when set from Lua...?? + pending('sets signs by default', function() + exec_lua [[ + vim.diagnostic.config({ + update_in_insert = true, + signs = true, + }) + + local diagnostics = { + make_error('Delayed Diagnostic', 1, 1, 1, 2), + make_error('Delayed Diagnostic', 3, 3, 3, 3), + } + + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, diagnostics) + + vim.diagnostic._set_signs(diagnostic_ns, diagnostic_bufnr, diagnostics) + -- return vim.fn.sign_getplaced() + ]] + + nvim("input", "o") + nvim("input", "") + + -- TODO(tjdevries): Find a way to get the signs to display in the test... + eq(nil, exec_lua [[ + return im.fn.sign_getplaced()[1].signs + ]]) + end) + end) + + describe('setloclist()', function() + it('sets diagnostics in lnum order', function() + local loc_list = exec_lua [[ + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Farther Diagnostic', 4, 4, 4, 4), + make_error('Lower Diagnostic', 1, 1, 1, 1), + }) + + vim.diagnostic.setloclist() + + return vim.fn.getloclist(0) + ]] + + assert(loc_list[1].lnum < loc_list[2].lnum) + end) + + it('sets diagnostics in lnum order, regardless of namespace', function() + local loc_list = exec_lua [[ + vim.api.nvim_win_set_buf(0, diagnostic_bufnr) + + vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, { + make_error('Lower Diagnostic', 1, 1, 1, 1), + }) + + vim.diagnostic.set(other_ns, diagnostic_bufnr, { + make_warning('Farther Diagnostic', 4, 4, 4, 4), + }) + + vim.diagnostic.setloclist() + + return vim.fn.getloclist(0) + ]] + + assert(loc_list[1].lnum < loc_list[2].lnum) + end) + end) +end) diff --git a/test/functional/plugin/lsp/diagnostic_spec.lua b/test/functional/plugin/lsp/diagnostic_spec.lua index e4fe1c1992..2a6d7de634 100644 --- a/test/functional/plugin/lsp/diagnostic_spec.lua +++ b/test/functional/plugin/lsp/diagnostic_spec.lua @@ -1,5 +1,6 @@ local helpers = require('test.functional.helpers')(after_each) +local command = helpers.command local clear = helpers.clear local exec_lua = helpers.exec_lua local eq = helpers.eq @@ -9,7 +10,10 @@ describe('vim.lsp.diagnostic', function() local fake_uri before_each(function() - clear() + clear {env={ + NVIM_LUA_NOTRACK="1"; + VIMRUNTIME=os.getenv"VIMRUNTIME"; + }} exec_lua [[ require('vim.lsp') @@ -44,7 +48,7 @@ describe('vim.lsp.diagnostic', function() count_of_extmarks_for_client = function(bufnr, client_id) return #vim.api.nvim_buf_get_extmarks( - bufnr, vim.lsp.diagnostic._get_diagnostic_namespace(client_id), 0, -1, {} + bufnr, vim.lsp.diagnostic.get_namespace(client_id), 0, -1, {} ) end ]] @@ -86,39 +90,6 @@ describe('vim.lsp.diagnostic', function() eq(2, #result[1]) eq('Diagnostic #1', result[1][1].message) end) - it('Can convert diagnostic to quickfix items format', function() - local bufnr = exec_lua([[ - local fake_uri = ... - return vim.uri_to_bufnr(fake_uri) - ]], fake_uri) - local result = exec_lua([[ - local bufnr = ... - vim.lsp.diagnostic.save( - { - make_error('Diagnostic #1', 1, 1, 1, 1), - make_error('Diagnostic #2', 2, 1, 2, 1), - }, bufnr, 1 - ) - return vim.lsp.util.diagnostics_to_items(vim.lsp.diagnostic.get_all()) - ]], bufnr) - local expected = { - { - bufnr = bufnr, - col = 2, - lnum = 2, - text = 'Diagnostic #1', - type = 'E' - }, - { - bufnr = bufnr, - col = 2, - lnum = 3, - text = 'Diagnostic #2', - type = 'E' - }, - } - eq(expected, result) - end) it('should be able to save and count a single client error', function() eq(1, exec_lua [[ vim.lsp.diagnostic.save( @@ -218,7 +189,7 @@ describe('vim.lsp.diagnostic', function() -- Clear diagnostics from server 1, and make sure we have the right amount of stuff for client 2 eq({1, 1, 2, 0, 2}, exec_lua [[ - vim.lsp.diagnostic.clear(diagnostic_bufnr, 1) + vim.lsp.diagnostic.disable(diagnostic_bufnr, 1) return { vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1), vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", 2), @@ -230,7 +201,7 @@ describe('vim.lsp.diagnostic', function() -- Show diagnostics from server 1 again eq(all_highlights, exec_lua([[ - vim.lsp.diagnostic.display(nil, diagnostic_bufnr, 1) + vim.lsp.diagnostic.enable(diagnostic_bufnr, 1) return { vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1), vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", 2), @@ -575,10 +546,10 @@ describe('vim.lsp.diagnostic', function() }) -- Count how many times we call display. - SetVirtualTextOriginal = vim.lsp.diagnostic.set_virtual_text + SetVirtualTextOriginal = vim.diagnostic._set_virtual_text DisplayCount = 0 - vim.lsp.diagnostic.set_virtual_text = function(...) + vim.diagnostic._set_virtual_text = function(...) DisplayCount = DisplayCount + 1 return SetVirtualTextOriginal(...) end @@ -719,7 +690,7 @@ describe('vim.lsp.diagnostic', function() return vim.api.nvim_buf_get_extmarks( diagnostic_bufnr, - vim.lsp.diagnostic._get_diagnostic_namespace(1), + vim.lsp.diagnostic.get_namespace(1), 0, -1, { details = true } @@ -756,7 +727,7 @@ describe('vim.lsp.diagnostic', function() return vim.api.nvim_buf_get_extmarks( diagnostic_bufnr, - vim.lsp.diagnostic._get_diagnostic_namespace(1), + vim.lsp.diagnostic.get_namespace(1), 0, -1, { details = true } @@ -798,6 +769,40 @@ describe('vim.lsp.diagnostic', function() eq(1, get_extmark_count_with_severity("Warning")) eq(1, get_extmark_count_with_severity("Hint")) end) + + it('correctly handles UTF-16 offsets', function() + local line = "All 💼 and no 🎉 makes Jack a dull 👦" + local result = exec_lua([[ + local line = ... + local client_id = vim.lsp.start_client { + cmd_env = { + NVIM_LUA_NOTRACK = "1"; + }; + cmd = { + vim.v.progpath, '-es', '-u', 'NONE', '--headless' + }; + offset_encoding = "utf-16"; + } + + vim.api.nvim_buf_set_lines(diagnostic_bufnr, 0, -1, false, {line}) + + vim.lsp.diagnostic.on_publish_diagnostics(nil, { + uri = fake_uri, + diagnostics = { + make_error('UTF-16 Diagnostic', 0, 7, 0, 8), + } + }, {client_id=client_id} + ) + + local diags = vim.diagnostic.get(diagnostic_bufnr) + vim.lsp.stop_client(client_id) + vim.lsp._vim_exit_handler() + return diags + ]], line) + eq(1, #result) + eq(exec_lua([[return vim.str_byteindex(..., 7, true)]], line), result[1].col) + eq(exec_lua([[return vim.str_byteindex(..., 8, true)]], line), result[1].end_col) + end) end) describe('lsp.util.show_line_diagnostics', function() @@ -940,4 +945,31 @@ describe('vim.lsp.diagnostic', function() assert(loc_list[1].lnum < loc_list[2].lnum) end) end) + + it('highlight groups', function() + command('runtime plugin/diagnostic.vim') + eq({ + 'LspDiagnosticsDefaultError', + 'LspDiagnosticsDefaultHint', + 'LspDiagnosticsDefaultInformation', + 'LspDiagnosticsDefaultWarning', + 'LspDiagnosticsFloatingError', + 'LspDiagnosticsFloatingHint', + 'LspDiagnosticsFloatingInformation', + 'LspDiagnosticsFloatingWarning', + 'LspDiagnosticsSignError', + 'LspDiagnosticsSignHint', + 'LspDiagnosticsSignInformation', + 'LspDiagnosticsSignWarning', + 'LspDiagnosticsUnderlineError', + 'LspDiagnosticsUnderlineHint', + 'LspDiagnosticsUnderlineInformation', + 'LspDiagnosticsUnderlineWarning', + 'LspDiagnosticsVirtualTextError', + 'LspDiagnosticsVirtualTextHint', + 'LspDiagnosticsVirtualTextInformation', + 'LspDiagnosticsVirtualTextWarning', + }, exec_lua([[return vim.fn.getcompletion('Lsp', 'highlight')]])) + end) + end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 7df5eb049c..ef78c8db4d 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -1017,31 +1017,6 @@ describe('LSP', function() } end - it('highlight groups', function() - eq({ - 'LspDiagnosticsDefaultError', - 'LspDiagnosticsDefaultHint', - 'LspDiagnosticsDefaultInformation', - 'LspDiagnosticsDefaultWarning', - 'LspDiagnosticsFloatingError', - 'LspDiagnosticsFloatingHint', - 'LspDiagnosticsFloatingInformation', - 'LspDiagnosticsFloatingWarning', - 'LspDiagnosticsSignError', - 'LspDiagnosticsSignHint', - 'LspDiagnosticsSignInformation', - 'LspDiagnosticsSignWarning', - 'LspDiagnosticsUnderlineError', - 'LspDiagnosticsUnderlineHint', - 'LspDiagnosticsUnderlineInformation', - 'LspDiagnosticsUnderlineWarning', - 'LspDiagnosticsVirtualTextError', - 'LspDiagnosticsVirtualTextHint', - 'LspDiagnosticsVirtualTextInformation', - 'LspDiagnosticsVirtualTextWarning', - }, exec_lua([[require'vim.lsp'; return vim.fn.getcompletion('Lsp', 'highlight')]])) - end) - describe('apply_text_edits', function() before_each(function() insert(dedent([[