diff options
80 files changed, 6350 insertions, 3268 deletions
@@ -189,8 +189,16 @@ contributed under the Vim license and (2) externally maintained libraries. The externally maintained libraries used by Neovim are: - Klib: a Generic Library in C. MIT/X11 license. - - libuv. Copyright Joyent, Inc. and other Node contributors. Node.js license. + - Lua: MIT license - LuaJIT: a Just-In-Time Compiler for Lua. Copyright Mike Pall. MIT license. + - Luv: Apache 2.0 license + - libmpack: MIT license + - libtermkey: MIT license + - libuv. Copyright Joyent, Inc. and other Node contributors. Node.js license. + - libvterm: MIT license + - lua-compat: MIT license + - tree-sitter: MIT license + - xdiff: LGPL license ==== diff --git a/MAINTAIN.md b/MAINTAIN.md index 58d977f247..73578a8c5d 100644 --- a/MAINTAIN.md +++ b/MAINTAIN.md @@ -10,14 +10,13 @@ General guidelines * Write down what was decided * Constraints are good * Use automation to solve problems -* Never break the API +* Never break the API... but sometimes break the UI Ticket triage ------------- -In practice we haven't found a meaningful way to forecast more precisely than -"next" and "after next". That means there are usually one or two (at most) -planned milestones: +In practice we haven't found a way to forecast more precisely than "next" and +"after next". So there are usually one or two (at most) planned milestones: - Next bugfix-release (1.0.x) - Next feature-release (1.x.0) @@ -25,16 +24,16 @@ planned milestones: The forecasting problem might be solved with an explicit priority system (like Bram's todo.txt). Meanwhile the Neovim priority system is defined by: -- PRs nearing completion (RDY). +- PRs nearing completion. - Issue labels. E.g. the `+plan` label increases the ticket's priority merely for having a plan written down: it is _closer to completion_ than tickets without a plan. - Comment activity or new information. -Anything that isn't in the next milestone, and doesn't have a RDY PR ... is +Anything that isn't in the next milestone, and doesn't have a finished PR—is just not something you care very much about, by construction. Post-release you can review open issues, but chances are your next milestone is already getting -full :) +full... :) Release policy -------------- diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim index 8bf95651b7..4f132b6121 100644 --- a/runtime/autoload/man.vim +++ b/runtime/autoload/man.vim @@ -58,6 +58,7 @@ function! man#open_page(count, mods, ...) abort else execute 'silent keepalt' a:mods 'stag' l:target endif + call s:set_options(v:false) finally call setbufvar(l:buf, '&tagfunc', l:save_tfu) endtry 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/eval.txt b/runtime/doc/eval.txt index f27d1e01a0..99e48e602b 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -14,8 +14,8 @@ Using expressions is introduced in chapter 41 of the user manual |usr_41.txt|. 1. Variables *variables* 1.1 Variable types ~ - *E712* -There are six types of variables: + *E712* *E896* *E897* *E899* +There are seven types of variables: *Number* *Integer* Number A 32 or 64 bit signed number. |expr-number| @@ -34,7 +34,7 @@ Funcref A reference to a function |Funcref|. like a Partial. Example: function("Callback", [arg], myDict) -List An ordered sequence of items |List|. +List An ordered sequence of items, see |List| for details. Example: [1, 2, ['a', 'b']] Dictionary An associative, unordered array: Each entry has a key and a @@ -43,6 +43,11 @@ Dictionary An associative, unordered array: Each entry has a key and a {'blue': "#0000ff", 'red': "#ff0000"} #{blue: "#0000ff", red: "#ff0000"} +Blob Binary Large Object. Stores any sequence of bytes. See |Blob| + for details. + Example: 0zFF00ED015DAF + 0z is an empty Blob. + The Number and String types are converted automatically, depending on how they are used. @@ -97,6 +102,7 @@ Note that " " and "0" are also non-empty strings, thus considered to be TRUE. A List, Dictionary or Float is not a Number or String, thus evaluate to FALSE. *E745* *E728* *E703* *E729* *E730* *E731* + *E974* *E975* *E976* |List|, |Dictionary|, |Funcref|, and |Blob| types are not automatically converted. @@ -366,8 +372,8 @@ Changing the order of items in a list: > For loop ~ -The |:for| loop executes commands for each item in a list. A variable is set -to each item in the list in sequence. Example: > +The |:for| loop executes commands for each item in a |List| or |Blob|. +A variable is set to each item in the sequence. Example with a List: > :for item in mylist : call Doit(item) :endfor @@ -400,6 +406,8 @@ It is also possible to put remaining items in a List variable: > : endif :endfor +For a Blob one byte at a time is used. + List functions ~ *E714* @@ -594,7 +602,137 @@ Functions that can be used with a Dictionary: > :call map(dict, '">> " . v:val') " prepend ">> " to each item -1.5 More about variables ~ +1.5 Blobs ~ + *blob* *Blob* *Blobs* *E978* +A Blob is a binary object. It can be used to read an image from a file and +send it over a channel, for example. + +A Blob mostly behaves like a |List| of numbers, where each number has the +value of an 8-bit byte, from 0 to 255. + + +Blob creation ~ + +A Blob can be created with a |blob-literal|: > + :let b = 0zFF00ED015DAF +Dots can be inserted between bytes (pair of hex characters) for readability, +they don't change the value: > + :let b = 0zFF00.ED01.5DAF + +A blob can be read from a file with |readfile()| passing the {type} argument +set to "B", for example: > + :let b = readfile('image.png', 'B') + + +Blob index ~ + *blob-index* *E979* +A byte in the Blob can be accessed by putting the index in square brackets +after the Blob. Indexes are zero-based, thus the first byte has index zero. > + :let myblob = 0z00112233 + :let byte = myblob[0] " get the first byte: 0x00 + :let byte = myblob[2] " get the third byte: 0x22 + +A negative index is counted from the end. Index -1 refers to the last byte in +the Blob, -2 to the last but one byte, etc. > + :let last = myblob[-1] " get the last byte: 0x33 + +To avoid an error for an invalid index use the |get()| function. When an item +is not available it returns -1 or the default value you specify: > + :echo get(myblob, idx) + :echo get(myblob, idx, 999) + + +Blob iteration ~ + +The |:for| loop executes commands for each byte of a Blob. The loop variable is +set to each byte in the Blob. Example: > + :for byte in 0z112233 + : call Doit(byte) + :endfor +This calls Doit() with 0x11, 0x22 and 0x33. + + +Blob concatenation ~ + +Two blobs can be concatenated with the "+" operator: > + :let longblob = myblob + 0z4455 + :let myblob += 0z6677 + +To change a blob in-place see |blob-modification| below. + + +Part of a blob ~ + +A part of the Blob can be obtained by specifying the first and last index, +separated by a colon in square brackets: > + :let myblob = 0z00112233 + :let shortblob = myblob[1:2] " get 0z1122 + :let shortblob = myblob[2:-1] " get 0z2233 + +Omitting the first index is similar to zero. Omitting the last index is +similar to -1. > + :let endblob = myblob[2:] " from item 2 to the end: 0z2233 + :let shortblob = myblob[2:2] " Blob with one byte: 0z22 + :let otherblob = myblob[:] " make a copy of the Blob + +If the first index is beyond the last byte of the Blob or the second byte is +before the first byte, the result is an empty Blob. There is no error +message. + +If the second index is equal to or greater than the length of the Blob the +length minus one is used: > + :echo myblob[2:8] " result: 0z2233 + + +Blob modification ~ + *blob-modification* +To change a specific byte of a blob use |:let| this way: > + :let blob[4] = 0x44 + +When the index is just one beyond the end of the Blob, it is appended. Any +higher index is an error. + +To change a sequence of bytes the [:] notation can be used: > + let blob[1:3] = 0z445566 +The length of the replaced bytes must be exactly the same as the value +provided. *E972* + +To change part of a blob you can specify the first and last byte to be +modified. The value must at least have the number of bytes in the range: > + :let blob[3:5] = [3, 4, 5] + +You can also use the functions |add()|, |remove()| and |insert()|. + + +Blob identity ~ + +Blobs can be compared for equality: > + if blob == 0z001122 +And for equal identity: > + if blob is otherblob +< *blob-identity* *E977* +When variable "aa" is a Blob and you assign it to another variable "bb", both +variables refer to the same Blob. Then the "is" operator returns true. + +When making a copy using [:] or |copy()| the values are the same, but the +identity is different: > + :let blob = 0z112233 + :let blob2 = blob + :echo blob == blob2 +< 1 > + :echo blob is blob2 +< 1 > + :let blob3 = blob[:] + :echo blob == blob3 +< 1 > + :echo blob is blob3 +< 0 + +Making a copy of a Blob is done with the |copy()| function. Using [:] also +works, as explained above. + + +1.6 More about variables ~ *more-variables* If you need to know the type of a variable or expression, use the |type()| function. @@ -645,8 +783,9 @@ Expression syntax summary, from least to most significant: etc. As above, append ? for ignoring case, # for matching case - expr5 is expr5 same |List| instance - expr5 isnot expr5 different |List| instance + expr5 is expr5 same |List|, |Dictionary| or |Blob| instance + expr5 isnot expr5 different |List|, |Dictionary| or |Blob| + instance |expr5| expr6 expr6 + expr6 ... number addition, list or blob concatenation @@ -817,12 +956,12 @@ Dictionary and arguments, use |get()| to get the function name: > if get(Part1, 'name') == get(Part2, 'name') " Part1 and Part2 refer to the same function -When using "is" or "isnot" with a |List| or a |Dictionary| this checks if the -expressions are referring to the same |List| or |Dictionary| instance. A copy -of a |List| is different from the original |List|. When using "is" without -a |List| or a |Dictionary| it is equivalent to using "equal", using "isnot" -equivalent to using "not equal". Except that a different type means the -values are different: > +Using "is" or "isnot" with a |List|, |Dictionary| or |Blob| checks whether +the expressions are referring to the same |List|, |Dictionary| or |Blob| +instance. A copy of a |List| is different from the original |List|. When +using "is" without a |List|, |Dictionary| or |Blob|, it is equivalent to +using "equal", using "isnot" is equivalent to using "not equal". Except that +a different type means the values are different: > echo 4 == '4' 1 echo 4 is '4' @@ -1012,6 +1151,12 @@ just above. Also see |sublist| below. Examples: > :let l = mylist[4:4] " List with one item :let l = mylist[:] " shallow copy of a List +If expr8 is a |Blob| this results in a new |Blob| with the bytes in the +indexes expr1a and expr1b, inclusive. Examples: > + :let b = 0zDEADBEEF + :let bs = b[1:2] " 0zADBE + :let bs = b[] " copy of 0zDEADBEEF + Using expr8[expr1] or expr8[expr1a : expr1b] on a |Funcref| results in an error. @@ -1180,6 +1325,14 @@ encodings. Use "\u00ff" to store character 255 correctly as UTF-8. Note that "\000" and "\x00" force the end of the string. +blob-literal *blob-literal* *E973* +------------ + +Hexadecimal starting with 0z or 0Z, with an arbitrary number of bytes. +The sequence must be an even number of hex characters. Example: > + :let b = 0zFF00ED015DAF + + literal-string *literal-string* *E115* --------------- 'string' string constant *expr-'* @@ -2007,19 +2160,21 @@ v:swapcommand Normal mode command to be executed after a file has been For ":edit +cmd file" the value is ":cmd\r". *v:t_TYPE* *v:t_bool* *t_bool-variable* -v:t_bool Value of Boolean type. Read-only. See: |type()| +v:t_bool Value of |Boolean| type. Read-only. See: |type()| *v:t_dict* *t_dict-variable* -v:t_dict Value of Dictionary type. Read-only. See: |type()| +v:t_dict Value of |Dictionary| type. Read-only. See: |type()| *v:t_float* *t_float-variable* -v:t_float Value of Float type. Read-only. See: |type()| +v:t_float Value of |Float| type. Read-only. See: |type()| *v:t_func* *t_func-variable* -v:t_func Value of Funcref type. Read-only. See: |type()| +v:t_func Value of |Funcref| type. Read-only. See: |type()| *v:t_list* *t_list-variable* -v:t_list Value of List type. Read-only. See: |type()| +v:t_list Value of |List| type. Read-only. See: |type()| *v:t_number* *t_number-variable* -v:t_number Value of Number type. Read-only. See: |type()| +v:t_number Value of |Number| type. Read-only. See: |type()| *v:t_string* *t_string-variable* -v:t_string Value of String type. Read-only. See: |type()| +v:t_string Value of |String| type. Read-only. See: |type()| + *v:t_blob* *t_blob-variable* +v:t_blob Value of |Blob| type. Read-only. See: |type()| *v:termresponse* *termresponse-variable* v:termresponse The escape sequence returned by the terminal for the DA @@ -2103,7 +2258,7 @@ USAGE RESULT DESCRIPTION ~ abs({expr}) Float or Number absolute value of {expr} acos({expr}) Float arc cosine of {expr} -add({list}, {item}) List append {item} to |List| {list} +add({object}, {item}) List/Blob append {item} to {object} and({expr}, {expr}) Number bitwise AND api_info() Dict api metadata append({lnum}, {string}) Number append {string} below line {lnum} @@ -2315,8 +2470,8 @@ hlID({name}) Number syntax ID of highlight group {name} hostname() String name of the machine Vim is running on iconv({expr}, {from}, {to}) String convert encoding of {expr} indent({lnum}) Number indent of line {lnum} -index({list}, {expr} [, {start} [, {ic}]]) - Number index in {list} where {expr} appears +index({object}, {expr} [, {start} [, {ic}]]) + Number index in {object} where {expr} appears input({prompt} [, {text} [, {completion}]]) String get input from the user inputlist({textlist}) Number let the user pick from a choice list @@ -2324,8 +2479,8 @@ inputrestore() Number restore typeahead inputsave() Number save and clear typeahead inputsecret({prompt} [, {text}]) String like input() but hiding the text -insert({list}, {item} [, {idx}]) - List insert {item} in {list} [before {idx}] +insert({object}, {item} [, {idx}]) + List insert {item} in {object} [before {idx}] interrupt() none interrupt script execution invert({expr}) Number bitwise invert isdirectory({directory}) Number |TRUE| if {directory} is a directory @@ -2384,8 +2539,8 @@ min({expr}) Number minimum value of items in {expr} mkdir({name} [, {path} [, {prot}]]) Number create directory {name} mode([expr]) String current editing mode -msgpackdump({list}) List dump a list of objects to msgpack -msgpackparse({list}) List parse msgpack to a list of objects +msgpackdump({list} [, {type}]) List/Blob dump objects to msgpack +msgpackparse({data}) List parse msgpack to a list of objects nextnonblank({lnum}) Number line nr of non-blank line >= {lnum} nr2char({expr}[, {utf8}]) String single char with ASCII/UTF8 value {expr} nvim_...({args}...) any call nvim |api| functions @@ -2407,7 +2562,7 @@ pyxeval({expr}) any evaluate |python_x| expression range({expr} [, {max} [, {stride}]]) List items from {expr} to {max} readdir({dir} [, {expr}]) List file names in {dir} selected by {expr} -readfile({fname} [, {binary} [, {max}]]) +readfile({fname} [, {type} [, {max}]]) List get list of lines from file {fname} reg_executing() String get the executing register name reg_recording() String get the recording register name @@ -2424,7 +2579,10 @@ remote_read({serverid} [, {timeout}]) remote_send({server}, {string} [, {idvar}]) String send key sequence remote_startserver({name}) none become server {name} -remove({list}, {idx} [, {end}]) any remove items {idx}-{end} from {list} +remove({list}, {idx} [, {end}]) any/List + remove items {idx}-{end} from {list} +remove({blob}, {idx} [, {end}]) Number/Blob + remove bytes {idx}-{end} from {blob} remove({dict}, {key}) any remove entry {key} from {dict} rename({from}, {to}) Number rename (move) file from {from} to {to} repeat({expr}, {count}) String repeat {expr} {count} times @@ -2612,8 +2770,8 @@ winrestview({dict}) none restore view of current window winsaveview() Dict save view of current window winwidth({nr}) Number width of window {nr} wordcount() Dict get byte/char/word statistics -writefile({list}, {fname} [, {flags}]) - Number write list of lines to file {fname} +writefile({object}, {fname} [, {flags}]) + Number write |Blob| or |List| of lines to file xor({expr}, {expr}) Number bitwise XOR @@ -2647,13 +2805,14 @@ acos({expr}) *acos()* Can also be used as a |method|: > Compute()->acos() -add({list}, {expr}) *add()* - Append the item {expr} to |List| {list}. Returns the - resulting |List|. Examples: > +add({object}, {expr}) *add()* + Append the item {expr} to |List| or |Blob| {object}. Returns + the resulting |List| or |Blob|. Examples: > :let alist = add([1, 2, 3], item) :call add(mylist, "woodstock") < Note that when {expr} is a |List| it is appended as a single item. Use |extend()| to concatenate |Lists|. + When {object} is a |Blob| then {expr} must be a number. Use |insert()| to add an item at another position. Can also be used as a |method|: > @@ -3066,8 +3225,8 @@ chansend({id}, {data}) *chansend()* written if the write succeeded, 0 otherwise. See |channel-bytes| for more information. - {data} may be a string, string convertible, or a list. If - {data} is a list, the items will be joined by newlines; any + {data} may be a string, string convertible, |Blob|, or a list. + If {data} is a list, the items will be joined by newlines; any newlines in an item will be sent as NUL. To send a final newline, include a final empty string. Example: > :call chansend(id, ["abc", "123\n456", ""]) @@ -3640,9 +3799,13 @@ diff_hlID({lnum}, {col}) *diff_hlID()* empty({expr}) *empty()* Return the Number 1 if {expr} is empty, zero otherwise. - A |List| or |Dictionary| is empty when it does not have any - items. A Number is empty when its value is zero. Special - variable is empty when it is |v:false| or |v:null|. + - A |List| or |Dictionary| is empty when it does not have any + items. + - A |String| is empty when its length is zero. + - A |Number| and |Float| are empty when their value is zero. + - |v:false| and |v:null| are empty, |v:true| is not. + - A |Blob| is empty when its length is zero. + Can also be used as a |method|: > mylist->empty() @@ -3665,8 +3828,8 @@ escape({string}, {chars}) *escape()* *eval()* eval({string}) Evaluate {string} and return the result. Especially useful to turn the result of |string()| back into the original value. - This works for Numbers, Floats, Strings and composites of - them. Also works for |Funcref|s that refer to existing + This works for Numbers, Floats, Strings, Blobs and composites + of them. Also works for |Funcref|s that refer to existing functions. Can also be used as a |method|: > @@ -4081,7 +4244,7 @@ filter({expr1}, {expr2}) *filter()* |Dictionary| to remain unmodified make a copy first: > :let l = filter(copy(mylist), 'v:val =~ "KEEP"') -< Returns {expr1}, the |List| , |Blob| or |Dictionary| that was +< Returns {expr1}, the |List|, |Blob| or |Dictionary| that was filtered. When an error is encountered while evaluating {expr2} no further items in {expr1} are processed. When {expr2} is a Funcref errors inside a function are ignored, @@ -4362,6 +4525,10 @@ get({list}, {idx} [, {default}]) *get()* omitted. Can also be used as a |method|: > mylist->get(idx) +get({blob}, {idx} [, {default}]) + Get byte {idx} from |Blob| {blob}. When this byte is not + available return {default}. Return -1 when {default} is + omitted. get({dict}, {key} [, {default}]) Get item with key {key} from |Dictionary| {dict}. When this item is not available return {default}. Return zero when @@ -5548,17 +5715,21 @@ indent({lnum}) The result is a Number, which is indent of line {lnum} in the When {lnum} is invalid -1 is returned. -index({list}, {expr} [, {start} [, {ic}]]) *index()* - Return the lowest index in |List| {list} where the item has a - value equal to {expr}. There is no automatic conversion, so - the String "4" is different from the Number 4. And the number - 4 is different from the Float 4.0. The value of 'ignorecase' - is not used here, case always matters. +index({object}, {expr} [, {start} [, {ic}]]) *index()* + If {object} is a |List| return the lowest index where the item + has a value equal to {expr}. There is no automatic + conversion, so the String "4" is different from the Number 4. + And the Number 4 is different from the Float 4.0. The value + of 'ignorecase' is not used here, case always matters. + + If {object} is a |Blob| return the lowest index where the byte + value is equal to {expr}. + If {start} is given then start looking at the item with index {start} (may be negative for an item relative to the end). When {ic} is given and it is |TRUE|, ignore case. Otherwise case must match. - -1 is returned when {expr} is not found in {list}. + -1 is returned when {expr} is not found in {object}. Example: > :let idx = index(words, "the") :if index(numbers, 123) >= 0 @@ -5717,13 +5888,16 @@ inputsecret({prompt} [, {text}]) *inputsecret()* typed on the command-line in response to the issued prompt. NOTE: Command-line completion is not supported. -insert({list}, {item} [, {idx}]) *insert()* - Insert {item} at the start of |List| {list}. +insert({object}, {item} [, {idx}]) *insert()* + When {object} is a |List| or a |Blob| insert {item} at the start + of it. + If {idx} is specified insert {item} before the item with index {idx}. If {idx} is zero it goes before the first item, just like omitting {idx}. A negative {idx} is also possible, see |list-index|. -1 inserts just before the last item. - Returns the resulting |List|. Examples: > + + Returns the resulting |List| or |Blob|. Examples: > :let mylist = insert([2, 3, 5], 1) :call insert(mylist, 4, -1) :call insert(mylist, 6, len(mylist)) @@ -5787,16 +5961,16 @@ islocked({expr}) *islocked()* *E786* id({expr}) *id()* Returns a |String| which is a unique identifier of the - container type (|List|, |Dict| and |Partial|). It is + container type (|List|, |Dict|, |Blob| and |Partial|). It is guaranteed that for the mentioned types `id(v1) ==# id(v2)` returns true iff `type(v1) == type(v2) && v1 is v2`. - Note that |v:_null_string|, |v:_null_list|, and |v:_null_dict| - have the same `id()` with different types because they are - internally represented as a NULL pointers. `id()` returns a - hexadecimal representanion of the pointers to the containers - (i.e. like `0x994a40`), same as `printf("%p", {expr})`, - but it is advised against counting on the exact format of - return value. + Note that |v:_null_string|, |v:_null_list|, |v:_null_dict| and + |v:_null_blob| have the same `id()` with different types + because they are internally represented as NULL pointers. + `id()` returns a hexadecimal representanion of the pointers to + the containers (i.e. like `0x994a40`), same as `printf("%p", + {expr})`, but it is advised against counting on the exact + format of the return value. It is not guaranteed that `id(no_longer_existing_container)` will not be equal to some other `id()`: new containers may @@ -5806,8 +5980,13 @@ items({dict}) *items()* Return a |List| with all the key-value pairs of {dict}. Each |List| item is a list with two items: the key of a {dict} entry and the value of this entry. The |List| is in arbitrary - order. - Can also be used as a |method|: > + order. Also see |keys()| and |values()|. + Example: > + for [key, value] in items(mydict) + echo key . ': ' . value + endfor + +< Can also be used as a |method|: > mydict->items() isnan({expr}) *isnan()* @@ -5974,10 +6153,12 @@ json_encode({expr}) *json_encode()* surrogate pairs (such strings are not valid UTF-8 strings). Non-printable characters are converted into "\u1234" escapes or special escapes like "\t", other are dumped as-is. + |Blob|s are converted to arrays of the individual bytes. keys({dict}) *keys()* Return a |List| with all the keys of {dict}. The |List| is in - arbitrary order. + arbitrary order. Also see |items()| and |values()|. + Can also be used as a |method|: > mydict->keys() @@ -6141,12 +6322,14 @@ luaeval({expr}[, {expr}]) to Vim data structures. See |lua-eval| for more details. map({expr1}, {expr2}) *map()* - {expr1} must be a |List| or a |Dictionary|. + {expr1} must be a |List|, |Blob| or |Dictionary|. Replace each item in {expr1} with the result of evaluating - {expr2}. {expr2} must be a |string| or |Funcref|. + {expr2}. For a |Blob| each byte is replaced. + + {expr2} must be a |string| or |Funcref|. If {expr2} is a |string|, inside {expr2} |v:val| has the value - of the current item. For a |Dictionary| |v:key| has the key + of the current item. For a |Dictionary| |v:key| has the key of the current item and for a |List| |v:key| has the index of the current item. For a |Blob| |v:key| has the index of the current byte. @@ -6179,11 +6362,11 @@ map({expr1}, {expr2}) *map()* |Dictionary| to remain unmodified make a copy first: > :let tlist = map(copy(mylist), ' v:val . "\t"') -< Returns {expr1}, the |List| or |Dictionary| that was filtered. - When an error is encountered while evaluating {expr2} no - further items in {expr1} are processed. When {expr2} is a - Funcref errors inside a function are ignored, unless it was - defined with the "abort" flag. +< Returns {expr1}, the |List|, |Blob| or |Dictionary| that was + filtered. When an error is encountered while evaluating + {expr2} no further items in {expr1} are processed. When + {expr2} is a Funcref errors inside a function are ignored, + unless it was defined with the "abort" flag. Can also be used as a |method|: > mylist->map(expr2) @@ -6651,11 +6834,15 @@ mode([expr]) Return a string that indicates the current mode. the leading character(s). Also see |visualmode()|. -msgpackdump({list}) *msgpackdump()* - Convert a list of VimL objects to msgpack. Returned value is - |readfile()|-style list. Example: > +msgpackdump({list} [, {type}]) *msgpackdump()* + Convert a list of VimL objects to msgpack. Returned value is a + |readfile()|-style list. When {type} contains "B", a |Blob| is + returned instead. Example: > call writefile(msgpackdump([{}]), 'fname.mpack', 'b') -< This will write the single 0x80 byte to `fname.mpack` file +< or, using a |Blob|: > + call writefile(msgpackdump([{}], 'B'), 'fname.mpack') +< + This will write the single 0x80 byte to a `fname.mpack` file (dictionary with zero items is represented by 0x80 byte in messagepack). @@ -6663,11 +6850,12 @@ msgpackdump({list}) *msgpackdump()* 1. |Funcref|s cannot be dumped. 2. Containers that reference themselves cannot be dumped. 3. Dictionary keys are always dumped as STR strings. - 4. Other strings are always dumped as BIN strings. + 4. Other strings and |Blob|s are always dumped as BIN strings. 5. Points 3. and 4. do not apply to |msgpack-special-dict|s. -msgpackparse({list}) *msgpackparse()* - Convert a |readfile()|-style list to a list of VimL objects. +msgpackparse({data}) *msgpackparse()* + Convert a |readfile()|-style list or a |Blob| to a list of + VimL objects. Example: > let fname = expand('~/.config/nvim/shada/main.shada') let mpack = readfile(fname, 'b') @@ -6677,7 +6865,7 @@ msgpackparse({list}) *msgpackparse()* Limitations: 1. Mapping ordering is not preserved unless messagepack - mapping is dumped using generic mapping + mapping is dumped using generic mapping (|msgpack-special-map|). 2. Since the parser aims to preserve all data untouched (except for 1.) some strings are parsed to @@ -6721,9 +6909,9 @@ msgpackparse({list}) *msgpackparse()* zero byte or if string is a mapping key and mapping is being represented as special dictionary for other reasons. - binary |readfile()|-style list of strings. This value will - appear in |msgpackparse()| output if binary string - contains zero byte. + binary |String|, or |Blob| if binary string contains zero + byte. This value cannot appear in |msgpackparse()| + output since blobs were introduced. array |List|. This value cannot appear in |msgpackparse()| output. *msgpack-special-map* @@ -7168,16 +7356,18 @@ readdir({directory} [, {expr}]) echo s:tree(".") < *readfile()* -readfile({fname} [, {binary} [, {max}]]) +readfile({fname} [, {type} [, {max}]]) Read file {fname} and return a |List|, each line of the file as an item. Lines are broken at NL characters. Macintosh files separated with CR will result in a single long line (unless a NL appears somewhere). All NUL characters are replaced with a NL character. - When {binary} contains "b" binary mode is used: + When {type} contains "b" binary mode is used: - When the last line ends in a NL an extra empty list item is added. - No CR characters are removed. + When {type} contains "B" a |Blob| is returned with the binary + data of the file unmodified. Otherwise: - CR characters that appear before a NL are removed. - Whether the last line ends in a NL or not does not matter. @@ -7357,6 +7547,17 @@ remove({list}, {idx} [, {end}]) *remove()* < Can also be used as a |method|: > mylist->remove(idx) +remove({blob}, {idx} [, {end}]) + Without {end}: Remove the byte at {idx} from |Blob| {blob} and + return the byte. + With {end}: Remove bytes from {idx} to {end} (inclusive) and + return a |Blob| with these bytes. When {idx} points to the same + byte as {end} a |Blob| with one byte is returned. When {end} + points to a byte before {idx} this is an error. + Example: > + :echo "last byte: " . remove(myblob, -1) + :call remove(mylist, 0, 9) + remove({dict}, {key}) Remove the entry from {dict} with key {key} and return it. Example: > @@ -7400,9 +7601,11 @@ resolve({filename}) *resolve()* *E655* path name) and also keeps a trailing path separator. *reverse()* -reverse({list}) Reverse the order of items in {list} in-place. Returns - {list}. - If you want a list to remain unmodified make a copy first: > +reverse({object}) + Reverse the order of items in {object} in-place. + {object} can be a |List| or a |Blob|. + Returns {object}. + If you want an object to remain unmodified make a copy first: > :let revlist = reverse(copy(mylist)) < Can also be used as a |method|: > mylist->reverse() @@ -8803,14 +9006,15 @@ stridx({haystack}, {needle} [, {start}]) *stridx()* *string()* string({expr}) Return {expr} converted to a String. If {expr} is a Number, - Float, String or a composition of them, then the result can be - parsed back with |eval()|. + Float, String, Blob or a composition of them, then the result + can be parsed back with |eval()|. {expr} type result ~ String 'string' Number 123 Float 123.123456 or 1.123456e8 or `str2float('inf')` Funcref `function('name')` + Blob 0z00112233.44556677.8899 List [item, item] Dictionary {key: value, key: value} Note that in String values the ' character is doubled. @@ -9139,11 +9343,23 @@ synstack({lnum}, {col}) *synstack()* valid positions. system({cmd} [, {input}]) *system()* *E677* - Get the output of {cmd} as a |string| (use |systemlist()| to - get a |List|). {cmd} is treated exactly as in |jobstart()|. - Not to be used for interactive commands. + Gets the output of {cmd} as a |string| (|systemlist()| returns + a |List|) and sets |v:shell_error| to the error code. + {cmd} is treated as in |jobstart()|: + If {cmd} is a List it runs directly (no 'shell'). + If {cmd} is a String it runs in the 'shell', like this: > + :call jobstart(split(&shell) + split(&shellcmdflag) + ['{cmd}']) - If {input} is a string it is written to a pipe and passed as +< Not to be used for interactive commands. + + Result is a String, filtered to avoid platform-specific quirks: + - <CR><NL> is replaced with <NL> + - NUL characters are replaced with SOH (0x01) + + Example: > + :echo system(['ls', expand('%:h')]) + +< If {input} is a string it is written to a pipe and passed as stdin to the command. The string is written as-is, line separators are not changed. If {input} is a |List| it is written to the pipe as @@ -9165,29 +9381,12 @@ system({cmd} [, {input}]) *system()* *E677* Note: Use |shellescape()| or |::S| with |expand()| or |fnamemodify()| to escape special characters in a command - argument. Newlines in {cmd} may cause the command to fail. - The characters in 'shellquote' and 'shellxquote' may also - cause trouble. - - Result is a String. Example: > - :let files = system("ls " . shellescape(expand('%:h'))) - :let files = system('ls ' . expand('%:h:S')) - -< To make the result more system-independent, the shell output - is filtered to replace <CR> with <NL> for Macintosh, and - <CR><NL> with <NL> for DOS-like systems. - To avoid the string being truncated at a NUL, all NUL - characters are replaced with SOH (0x01). + argument. 'shellquote' and 'shellxquote' must be properly + configured. Example: > + :echo system('ls '..shellescape(expand('%:h'))) + :echo system('ls '..expand('%:h:S')) - The command executed is constructed using several options when - {cmd} is a string: 'shell' 'shellcmdflag' {cmd} - - The resulting error code can be found in |v:shell_error|. - - Note that any wrong value in the options mentioned above may - make the function fail. It has also been reported to fail - when using a security agent application. - Unlike ":!cmd" there is no automatic check for changed files. +< Unlike ":!cmd" there is no automatic check for changed files. Use |:checktime| to force a check. Can also be used as a |method|: > @@ -9488,6 +9687,7 @@ type({expr}) *type()* Float: 5 (|v:t_float|) Boolean: 6 (|v:true| and |v:false|) Null: 7 (|v:null|) + Blob: 10 (|v:t_blob|) For backward compatibility, this method can be used: > :if type(myvar) == type(0) :if type(myvar) == type("") @@ -9572,7 +9772,7 @@ uniq({list} [, {func} [, {dict}]]) *uniq()* *E882* values({dict}) *values()* Return a |List| with all the values of {dict}. The |List| is - in arbitrary order. + in arbitrary order. Also see |items()| and |keys()|. Can also be used as a |method|: > mydict->values() @@ -9932,14 +10132,17 @@ wordcount() *wordcount()* *writefile()* -writefile({list}, {fname} [, {flags}]) - Write |List| {list} to file {fname}. Each list item is - separated with a NL. Each list item must be a String or - Number. +writefile({object}, {fname} [, {flags}]) + When {object} is a |List| write it to file {fname}. Each list + item is separated with a NL. Each list item must be a String + or Number. When {flags} contains "b" then binary mode is used: There will not be a NL after the last list item. An empty item at the end does cause the last line in the file to end in a NL. + When {object} is a |Blob| write the bytes to file {fname} + unmodified. + When {flags} contains "a" then append mode is used, lines are appended to the file: > :call writefile(["foo"], "event.log", "a") @@ -10455,7 +10658,10 @@ This does NOT work: > This cannot be used to set a byte in a String. You can do that like this: > :let var = var[0:2] . 'X' . var[4:] -< +< When {var-name} is a |Blob| then {idx} can be the + length of the blob, in which case one byte is + appended. + *E711* *E719* :let {var-name}[{idx1}:{idx2}] = {expr1} *E708* *E709* *E710* Set a sequence of items in a |List| to the result of @@ -10692,10 +10898,18 @@ text... :const x = 1 < is equivalent to: > :let x = 1 - :lockvar 1 x + :lockvar! x < This is useful if you want to make sure the variable - is not modified. - *E995* + is not modified. If the value is a List or Dictionary + literal then the items also cannot be changed: > + const ll = [1, 2, 3] + let ll[1] = 5 " Error! +< Nested references are not locked: > + let lvar = ['a'] + const lconst = [0, lvar] + let lconst[0] = 2 " Error! + let lconst[1][0] = 'b' " OK +< *E995* |:const| does not allow to for changing a variable. > :let x = 1 :const x = 2 " Error! @@ -10812,28 +11026,34 @@ text... NOTE: The ":append" and ":insert" commands don't work properly inside a ":while" and ":for" loop. -:for {var} in {list} *:for* *E690* *E732* +:for {var} in {object} *:for* *E690* *E732* :endfo[r] *:endfo* *:endfor* Repeat the commands between ":for" and ":endfor" for - each item in {list}. Variable {var} is set to the - value of each item. - When an error is detected for a command inside the - loop, execution continues after the "endfor". - Changing {list} inside the loop affects what items are - used. Make a copy if this is unwanted: > + each item in {object}. {object} can be a |List| or + a |Blob|. Variable {var} is set to the value of each + item. When an error is detected for a command inside + the loop, execution continues after the "endfor". + Changing {object} inside the loop affects what items + are used. Make a copy if this is unwanted: > :for item in copy(mylist) -< When not making a copy, Vim stores a reference to the - next item in the list, before executing the commands - with the current item. Thus the current item can be - removed without effect. Removing any later item means - it will not be found. Thus the following example - works (an inefficient way to make a list empty): > +< + When {object} is a |List| and not making a copy, Vim + stores a reference to the next item in the |List| + before executing the commands with the current item. + Thus the current item can be removed without effect. + Removing any later item means it will not be found. + Thus the following example works (an inefficient way + to make a |List| empty): > for item in mylist call remove(mylist, 0) endfor -< Note that reordering the list (e.g., with sort() or +< Note that reordering the |List| (e.g., with sort() or reverse()) may have unexpected effects. + When {object} is a |Blob|, Vim always makes a copy to + iterate over. Unlike with |List|, modifying the + |Blob| does not affect the iteration. + :for [{var1}, {var2}, ...] in {listlist} :endfo[r] Like ":for" above, but each item in {listlist} must be diff --git a/runtime/doc/if_perl.txt b/runtime/doc/if_perl.txt index ddcf220844..3787ca69ba 100644 --- a/runtime/doc/if_perl.txt +++ b/runtime/doc/if_perl.txt @@ -189,6 +189,9 @@ VIM::Eval({expr}) Evaluates {expr} and returns (success, value) in list A |List| is turned into a string by joining the items and inserting line breaks. + *perl-Blob* +VIM::Blob({expr}) Return Blob literal string 0zXXXX from scalar value. + ============================================================================== 3. VIM::Buffer objects *perl-buffer* 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. @@ -561,13 +446,6 @@ LspSignatureActiveParameter |vim.lsp.handlers.signature_help()|. ============================================================================== -AUTOCOMMANDS *lsp-autocommands* - - *LspDiagnosticsChanged* -LspDiagnosticsChanged After receiving publishDiagnostics server response - - -============================================================================== Lua module: vim.lsp *lsp-core* buf_attach_client({bufnr}, {client_id}) *vim.lsp.buf_attach_client()* @@ -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 - - 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 +get_namespace({client_id}) *vim.lsp.diagnostic.get_namespace()* + Get the diagnostic namespace associated with an LSP client + |vim.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/doc/lua.txt b/runtime/doc/lua.txt index 5731569947..53d68fa5e6 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1,15 +1,15 @@ *lua.txt* Nvim - NVIM REFERENCE MANUAL + NVIM REFERENCE MANUAL -Lua engine *lua* *Lua* +Lua engine *lua* *Lua* Type |gO| to see the table of contents. ============================================================================== -INTRODUCTION *lua-intro* +INTRODUCTION *lua-intro* The Lua 5.1 language is builtin and always available. Try this command to get an idea of what lurks beneath: > @@ -27,11 +27,12 @@ are on 'runtimepath': ~/.config/nvim/lua/foo.lua then `require('foo')` loads "~/.config/nvim/lua/foo.lua", and "runtime/lua/foo.lua" is not used. See |lua-require| to understand how Nvim -finds and loads Lua modules. The conventions are similar to VimL plugins, -with some extra features. See |lua-require-example| for a walkthrough. +finds and loads Lua modules. The conventions are similar to those of +Vimscript |plugin|s, with some extra features. See |lua-require-example| for +a walkthrough. ============================================================================== -IMPORTING LUA MODULES *lua-require* +IMPORTING LUA MODULES *lua-require* *lua-package-path* Nvim automatically adjusts `package.path` and `package.cpath` according to @@ -157,7 +158,7 @@ function without any parentheses. This is most often used to approximate ------------------------------------------------------------------------------ -LUA PLUGIN EXAMPLE *lua-require-example* +LUA PLUGIN EXAMPLE *lua-require-example* The following example plugin adds a command `:MakeCharBlob` which transforms current buffer into a long `unsigned char` array. Lua contains transformation @@ -234,7 +235,7 @@ lua/charblob.lua: > } ============================================================================== -COMMANDS *lua-commands* +COMMANDS *lua-commands* These commands execute a Lua chunk from either the command line (:lua, :luado) or a file (:luafile) on the given line [range]. As always in Lua, each chunk @@ -298,19 +299,20 @@ arguments separated by " " (space) instead of "\t" (tab). :luado if bp:match(line) then return "-->\t" .. line end < - *:luafile* + *:luafile* :[range]luafile {file} - Execute Lua script in {file}. - The whole argument is used as a single file name. + Execute Lua script in {file}. + The whole argument is used as the filename (like + |:edit|), spaces do not need to be escaped. + Alternatively you can |:source| Lua files. - Examples: - > + Examples: > :luafile script.lua :luafile % < ============================================================================== -luaeval() *lua-eval* *luaeval()* +luaeval() *lua-eval* *luaeval()* The (dual) equivalent of "vim.eval" for passing Lua values to Nvim is "luaeval". "luaeval" takes an expression string and an optional argument used @@ -324,8 +326,8 @@ semantically equivalent in Lua to: end Lua nils, numbers, strings, tables and booleans are converted to their -respective VimL types. An error is thrown if conversion of any other Lua types -is attempted. +respective Vimscript types. If a Lua string contains a NUL byte, it will be +converted to a |Blob|. Conversion of other Lua types is an error. The magic global "_A" contains the second argument to luaeval(). @@ -348,21 +350,21 @@ cases there is the following agreement: 3. Table with string keys, at least one of which contains NUL byte, is also considered to be a dictionary, but this time it is converted to a |msgpack-special-map|. - *lua-special-tbl* + *lua-special-tbl* 4. Table with `vim.type_idx` key may be a dictionary, a list or floating-point value: - - `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to - a floating-point 1.0. Note that by default integral Lua numbers are - converted to |Number|s, non-integral are converted to |Float|s. This + - `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to + a floating-point 1.0. Note that by default integral Lua numbers are + converted to |Number|s, non-integral are converted to |Float|s. This variant allows integral |Float|s. - - `{[vim.type_idx]=vim.types.dictionary}` is converted to an empty - dictionary, `{[vim.type_idx]=vim.types.dictionary, [42]=1, a=2}` is - converted to a dictionary `{'a': 42}`: non-string keys are ignored. - Without `vim.type_idx` key tables with keys not fitting in 1., 2. or 3. + - `{[vim.type_idx]=vim.types.dictionary}` is converted to an empty + dictionary, `{[vim.type_idx]=vim.types.dictionary, [42]=1, a=2}` is + converted to a dictionary `{'a': 42}`: non-string keys are ignored. + Without `vim.type_idx` key tables with keys not fitting in 1., 2. or 3. are errors. - - `{[vim.type_idx]=vim.types.list}` is converted to an empty list. As well - as `{[vim.type_idx]=vim.types.list, [42]=1}`: integral keys that do not - form a 1-step sequence from 1 to N are ignored, as well as all + - `{[vim.type_idx]=vim.types.list}` is converted to an empty list. As well + as `{[vim.type_idx]=vim.types.list, [42]=1}`: integral keys that do not + form a 1-step sequence from 1 to N are ignored, as well as all non-integral keys. Examples: > @@ -373,13 +375,13 @@ Examples: > : endfunction :echo Rand(1,10) -Note: second argument to `luaeval` undergoes VimL to Lua conversion -("marshalled"), so changes to Lua containers do not affect values in VimL. -Return value is also always converted. When converting, -|msgpack-special-dict|s are treated specially. +Note: second argument to `luaeval` is converted ("marshalled") from Vimscript +to Lua, so changes to Lua containers do not affect values in Vimscript. Return +value is also always converted. When converting, |msgpack-special-dict|s are +treated specially. ============================================================================== -Vimscript v:lua interface *v:lua-call* +Vimscript v:lua interface *v:lua-call* From Vimscript the special `v:lua` prefix can be used to call Lua functions which are global or accessible from global tables. The expression > @@ -419,7 +421,7 @@ Note: `v:lua` without a call is not allowed in a Vimscript expression: ============================================================================== -Lua standard modules *lua-stdlib* +Lua standard modules *lua-stdlib* The Nvim Lua "standard library" (stdlib) is the `vim` module, which exposes various functions and sub-modules. It is always loaded, thus require("vim") @@ -453,7 +455,7 @@ Note that underscore-prefixed functions (e.g. "_os_proc_children") are internal/private and must not be used by plugins. ------------------------------------------------------------------------------ -VIM.LOOP *lua-loop* *vim.loop* +VIM.LOOP *lua-loop* *vim.loop* `vim.loop` exposes all features of the Nvim event-loop. This is a low-level API that provides functionality for networking, filesystem, and process @@ -464,7 +466,7 @@ management. Try this command to see available functions: > Reference: https://github.com/luvit/luv/blob/master/docs.md Examples: https://github.com/luvit/luv/tree/master/examples - *E5560* *lua-loop-callbacks* + *E5560* *lua-loop-callbacks* It is an error to directly invoke `vim.api` functions (except |api-fast|) in `vim.loop` callbacks. For example, this is an error: > @@ -500,7 +502,7 @@ Example: repeating timer print('sleeping'); -Example: File-change detection *watch-file* +Example: File-change detection *watch-file* 1. Save this code to a file. 2. Execute it with ":luafile %". 3. Use ":Watch %" to watch any file. @@ -526,7 +528,7 @@ Example: File-change detection *watch-file* "command! -nargs=1 Watch call luaeval('watch_file(_A)', expand('<args>'))") -Example: TCP echo-server *tcp-server* +Example: TCP echo-server *tcp-server* 1. Save this code to a file. 2. Execute it with ":luafile %". 3. Note the port number. @@ -556,7 +558,7 @@ Example: TCP echo-server *tcp-server* print('TCP echo-server listening on port: '..server:getsockname().port) ------------------------------------------------------------------------------ -VIM.HIGHLIGHT *lua-highlight* +VIM.HIGHLIGHT *lua-highlight* Nvim includes a function for highlighting a selection on yank (see for example https://github.com/machakann/vim-highlightedyank). To enable it, add @@ -591,21 +593,19 @@ vim.highlight.range({bufnr}, {ns}, {higroup}, {start}, {finish}, {rtype}, {inclu range is inclusive (default false). ------------------------------------------------------------------------------ -VIM.REGEX *lua-regex* +VIM.REGEX *lua-regex* Vim regexes can be used directly from lua. Currently they only allow matching within a single line. -vim.regex({re}) *vim.regex()* +vim.regex({re}) *vim.regex()* + Parse the Vim regex {re} and return a regex object. Regexes are + "magic" and case-insensitive by default, regardless of 'magic' and + 'ignorecase'. The can be controlled with flags, see |/magic|. - Parse the regex {re} and return a regex object. 'magic' and - 'ignorecase' options are ignored, lua regexes always defaults to magic - and ignoring case. The behavior can be changed with flags in - the beginning of the string |/magic|. +Methods on the regex object: -Regex objects support the following methods: - -regex:match_str({str}) *regex:match_str()* +regex:match_str({str}) *regex:match_str()* Match the string against the regex. If the string should match the regex precisely, surround the regex with `^` and `$`. If the was a match, the byte indices for the beginning and end of @@ -613,7 +613,7 @@ regex:match_str({str}) *regex:match_str()* As any integer is truth-y, `regex:match()` can be directly used as a condition in an if-statement. -regex:match_line({bufnr}, {line_idx}[, {start}, {end}]) *regex:match_line()* +regex:match_line({bufnr}, {line_idx}[, {start}, {end}]) *regex:match_line()* Match line {line_idx} (zero-based) in buffer {bufnr}. If {start} and {end} are supplied, match only this byte index range. Otherwise see |regex:match_str()|. If {start} is used, then the returned byte @@ -692,67 +692,65 @@ VIM.MPACK *lua-mpack* The *vim.mpack* module provides packing and unpacking of lua objects to msgpack encoded strings. |vim.NIL| and |vim.empty_dict()| are supported. -vim.mpack.pack({obj}) *vim.mpack.pack* +vim.mpack.pack({obj}) *vim.mpack.pack* Packs a lua object {obj} and returns the msgpack representation as a string -vim.mpack.unpack({str}) *vim.mpack.unpack* +vim.mpack.unpack({str}) *vim.mpack.unpack* Unpacks the msgpack encoded {str} and returns a lua object ------------------------------------------------------------------------------ -VIM *lua-builtin* +VIM *lua-builtin* -vim.api.{func}({...}) *vim.api* +vim.api.{func}({...}) *vim.api* Invokes Nvim |API| function {func} with arguments {...}. Example: call the "nvim_get_current_line()" API function: > print(tostring(vim.api.nvim_get_current_line())) -vim.version() *vim.version* - Returns the version of the current neovim build. +vim.version() *vim.version* + Gets the version of the current Nvim build. -vim.in_fast_event() *vim.in_fast_event()* +vim.in_fast_event() *vim.in_fast_event()* Returns true if the code is executing as part of a "fast" event handler, where most of the API is disabled. These are low-level events (e.g. |lua-loop-callbacks|) which can be invoked whenever Nvim polls for input. When this is `false` most API functions are callable (but may be subject to other restrictions such as |textlock|). -vim.NIL *vim.NIL* - Special value used to represent NIL in msgpack-rpc and |v:null| in - vimL interaction, and similar cases. Lua `nil` cannot be used as - part of a lua table representing a Dictionary or Array, as it - is equivalent to a missing value: `{"foo", nil}` is the same as - `{"foo"}` +vim.NIL *vim.NIL* + Special value representing NIL in |RPC| and |v:null| in Vimscript + conversion, and similar cases. Lua `nil` cannot be used as part of + a Lua table representing a Dictionary or Array, because it is + treated as missing: `{"foo", nil}` is the same as `{"foo"}`. -vim.empty_dict() *vim.empty_dict()* - Creates a special table which will be converted to an empty - dictionary when converting lua values to vimL or API types. The - table is empty, and this property is marked using a metatable. An - empty table `{}` without this metatable will default to convert to - an array/list. +vim.empty_dict() *vim.empty_dict()* + Creates a special empty table (marked with a metatable), which Nvim + converts to an empty dictionary when translating Lua values to + Vimscript or API types. Nvim by default converts an empty table `{}` + without this metatable to an list/array. - Note: if numeric keys are added to the table, the metatable will be - ignored and the dict converted to a list/array anyway. + Note: if numeric keys are present in the table, Nvim ignores the + metatable marker and converts the dict to a list/array anyway. -vim.rpcnotify({channel}, {method}[, {args}...]) *vim.rpcnotify()* - Sends {event} to {channel} via |RPC| and returns immediately. - If {channel} is 0, the event is broadcast to all channels. +vim.rpcnotify({channel}, {method}[, {args}...]) *vim.rpcnotify()* + Sends {event} to {channel} via |RPC| and returns immediately. If + {channel} is 0, the event is broadcast to all channels. - This function also works in a fast callback |lua-loop-callbacks|. + This function also works in a fast callback |lua-loop-callbacks|. -vim.rpcrequest({channel}, {method}[, {args}...]) *vim.rpcrequest()* - Sends a request to {channel} to invoke {method} via - |RPC| and blocks until a response is received. +vim.rpcrequest({channel}, {method}[, {args}...]) *vim.rpcrequest()* + Sends a request to {channel} to invoke {method} via |RPC| and blocks + until a response is received. - Note: NIL values as part of the return value is represented as - |vim.NIL| special value + Note: NIL values as part of the return value is represented as + |vim.NIL| special value -vim.stricmp({a}, {b}) *vim.stricmp()* +vim.stricmp({a}, {b}) *vim.stricmp()* Compares strings case-insensitively. Returns 0, 1 or -1 if strings are equal, {a} is greater than {b} or {a} is lesser than {b}, respectively. -vim.str_utfindex({str}[, {index}]) *vim.str_utfindex()* +vim.str_utfindex({str}[, {index}]) *vim.str_utfindex()* Convert byte index to UTF-32 and UTF-16 indicies. If {index} is not supplied, the length of the string is used. All indicies are zero-based. Returns two values: the UTF-32 and UTF-16 indicies respectively. @@ -840,40 +838,40 @@ vim.wait({time} [, {callback}, {interval}, {fast_only}]) *vim.wait()* end < -vim.type_idx *vim.type_idx* - Type index for use in |lua-special-tbl|. Specifying one of the - values from |vim.types| allows typing the empty table (it is - unclear whether empty Lua table represents empty list or empty array) - and forcing integral numbers to be |Float|. See |lua-special-tbl| for - more details. +vim.type_idx *vim.type_idx* + Type index for use in |lua-special-tbl|. Specifying one of the values + from |vim.types| allows typing the empty table (it is unclear whether + empty Lua table represents empty list or empty array) and forcing + integral numbers to be |Float|. See |lua-special-tbl| for more + details. -vim.val_idx *vim.val_idx* - Value index for tables representing |Float|s. A table representing - floating-point value 1.0 looks like this: > +vim.val_idx *vim.val_idx* + Value index for tables representing |Float|s. A table representing + floating-point value 1.0 looks like this: > { [vim.type_idx] = vim.types.float, [vim.val_idx] = 1.0, } -< See also |vim.type_idx| and |lua-special-tbl|. - -vim.types *vim.types* - Table with possible values for |vim.type_idx|. Contains two sets - of key-value pairs: first maps possible values for |vim.type_idx| - to human-readable strings, second maps human-readable type names to - values for |vim.type_idx|. Currently contains pairs for `float`, - `array` and `dictionary` types. - - Note: one must expect that values corresponding to `vim.types.float`, - `vim.types.array` and `vim.types.dictionary` fall under only two - following assumptions: - 1. Value may serve both as a key and as a value in a table. Given the - properties of Lua tables this basically means “value is not `nil`”. - 2. For each value in `vim.types` table `vim.types[vim.types[value]]` - is the same as `value`. - No other restrictions are put on types, and it is not guaranteed that - values corresponding to `vim.types.float`, `vim.types.array` and - `vim.types.dictionary` will not change or that `vim.types` table will - only contain values for these three types. +< See also |vim.type_idx| and |lua-special-tbl|. + +vim.types *vim.types* + Table with possible values for |vim.type_idx|. Contains two sets of + key-value pairs: first maps possible values for |vim.type_idx| to + human-readable strings, second maps human-readable type names to + values for |vim.type_idx|. Currently contains pairs for `float`, + `array` and `dictionary` types. + + Note: one must expect that values corresponding to `vim.types.float`, + `vim.types.array` and `vim.types.dictionary` fall under only two + following assumptions: + 1. Value may serve both as a key and as a value in a table. Given the + properties of Lua tables this basically means “value is not `nil`”. + 2. For each value in `vim.types` table `vim.types[vim.types[value]]` + is the same as `value`. + No other restrictions are put on types, and it is not guaranteed that + values corresponding to `vim.types.float`, `vim.types.array` and + `vim.types.dictionary` will not change or that `vim.types` table will + only contain values for these three types. ------------------------------------------------------------------------------ LUA-VIMSCRIPT BRIDGE *lua-vimscript* @@ -966,8 +964,8 @@ vim.env *vim.env* *lua-vim-optlocal* *lua-vim-setlocal* -In vimL, there is a succint and simple way to set options. For more -information, see |set-option|. In Lua, the corresponding method is `vim.opt`. +In Vimscript, there is an way to set options |set-option|. In Lua, the +corresponding method is `vim.opt`. `vim.opt` provides several conveniences for setting and controlling options from within Lua. @@ -975,18 +973,18 @@ from within Lua. Examples: ~ To set a boolean toggle: - In vimL: + In Vimscript: `set number` In Lua: `vim.opt.number = true` To set an array of values: - In vimL: + In Vimscript: `set wildignore=*.o,*.a,__pycache__` In Lua, there are two ways you can do this now. One is very similar to - the vimL way: + the Vimscript form: `vim.opt.wildignore = '*.o,*.a,__pycache__'` However, vim.opt also supports a more elegent way of setting @@ -1019,7 +1017,7 @@ from within Lua. vim.opt.wildignore:remove { "node_modules" } < To set a map of values: - In vimL: + In Vimscript: `set listchars=space:_,tab:>~` In Lua: diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt index 6851cd1511..5fdd5fc3c0 100644 --- a/runtime/doc/repeat.txt +++ b/runtime/doc/repeat.txt @@ -172,15 +172,16 @@ Using Vim scripts *using-scripts* For writing a Vim script, see chapter 41 of the user manual |usr_41.txt|. *:so* *:source* *load-vim-script* -:so[urce] {file} Runs |Ex| commands or Lua code (".lua" files) read - from {file}. +:[range]so[urce] [file] Runs |Ex| commands or Lua code (".lua" files) from + [file], or from the current buffer if no [file] is + given. Triggers the |SourcePre| autocommand. *:source!* -:so[urce]! {file} Runs |Normal-mode| commands read from {file}. When - used after |:global|, |:argdo|, |:windo|, |:bufdo|, in +:[range]so[urce]! {file} + Runs |Normal-mode| commands from {file}. When used + after |:global|, |:argdo|, |:windo|, |:bufdo|, in a loop or when another command follows the display won't be updated while executing the commands. - Cannot be used in the |sandbox|. *:ru* *:runtime* :ru[ntime][!] [where] {file} .. diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 4dea053bc7..64824b2e3f 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -472,6 +472,7 @@ Compile-time features: X11 integration (see |x11-selection|) Eval: + Vim9script *js_encode()* *js_decode()* *v:none* (used by Vim to represent JavaScript "undefined"); use |v:null| instead. 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 <buffer=%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 <nomodeline> 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 <nomodeline> 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 0fdd43e210..90c5872f11 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -674,7 +674,7 @@ function lsp.start_client(config) ---@param method (string) LSP method name ---@param params (table) The parameters for that method. function dispatch.notification(method, params) - local _ = log.debug() and log.debug('notification', method, params) + local _ = log.trace() and log.trace('notification', method, params) local handler = resolve_handler(method) if handler then -- Method name is provided here for convenience. @@ -688,13 +688,13 @@ function lsp.start_client(config) ---@param method (string) LSP method name ---@param params (table) The parameters for that method function dispatch.server_request(method, params) - local _ = log.debug() and log.debug('server_request', method, params) + local _ = log.trace() and log.trace('server_request', method, params) local handler = resolve_handler(method) if handler then - local _ = log.debug() and log.debug("server_request: found handler for", method) + local _ = log.trace() and log.trace("server_request: found handler for", method) return handler(nil, params, {method=method, client_id=client_id}) end - local _ = log.debug() and log.debug("server_request: no handler found for", method) + local _ = log.warn() and log.warn("server_request: no handler found for", method) return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound) end @@ -826,7 +826,7 @@ function lsp.start_client(config) -- TODO(ashkan) handle errors here. pcall(config.before_init, initialize_params, config) end - local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params) + local _ = log.trace() and log.trace(log_prefix, "initialize_params", initialize_params) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) assert(result, "server sent empty result") @@ -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] -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 - - return rawget(t, bufnr) - end, - - __newindex = function(t, bufnr, v) - if bufnr == 0 or bufnr == nil then - bufnr = vim.api.nvim_get_current_buf() - 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 = {} - --- Disabled buffers and clients -local diagnostic_disabled = setmetatable({}, bufnr_and_client_cacher_mt) - -local _bufs_waiting_to_update = setmetatable({}, bufnr_and_client_cacher_mt) - ---- Store Diagnostic[] by line ---- ----@param diagnostics Diagnostic[] ----@return table<number, Diagnostic[]> -local _diagnostic_lines = function(diagnostics) - if not diagnostics then return end - - local diagnostics_by_line = {} - for _, diagnostic in ipairs(diagnostics) do - 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) +local function severity_vim_to_lsp(severity) + if type(severity) == 'string' then + severity = vim.diagnostic.severity[severity] end - return diagnostics_by_line + return severity end ---- Get the count of M by Severity ---- ----@param diagnostics Diagnostic[] ----@return table<DiagnosticSeverity, number> -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 +---@private +local function line_byte_from_position(lines, lnum, col, offset_encoding) + if offset_encoding == "utf-8" then + return col + end - counts[diagnostic.severity] = val + 1 - 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 - return counts + return col 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 +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 - diagnostic_cache[bufnr][client_id] = diagnostics - diagnostic_cache_lines[bufnr][client_id] = _diagnostic_lines(diagnostics) - diagnostic_cache_counts[bufnr][client_id] = _diagnostic_counts(diagnostics) + 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 +---@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 _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 ---@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) +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 - diagnostic_cache[bufnr][client_id] = nil - diagnostic_cache_lines[bufnr][client_id] = nil - diagnostic_cache_counts[bufnr][client_id] = nil +local _client_namespaces = {} + +--- Get the diagnostic namespace associated with an LSP client |vim.diagnostic|. +--- +---@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 _client_namespaces[client_id] end --- Save diagnostics to the current buffer. @@ -309,86 +137,146 @@ 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}, - } + local namespace = M.get_namespace(client_id) + vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) +end +-- }}} + +--- |lsp-handler| for the method "textDocument/publishDiagnostics" +--- +--- See |vim.diagnostic.config()| for configuration options. Handler-specific +--- configuration can be set using |vim.lsp.with()|: +--- <pre> +--- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( +--- vim.lsp.diagnostic.on_publish_diagnostics, { +--- -- Enable underline, use default values +--- underline = true, +--- -- Enable virtual text, override spacing to 4 +--- virtual_text = { +--- spacing = 4, +--- }, +--- -- Use a function to dynamically turn signs off +--- -- and on, using buffer local variables +--- signs = function(bufnr, client_id) +--- return vim.bo[bufnr].show_signs == false +--- end, +--- -- Disable a feature +--- update_in_insert = false, +--- } +--- ) +--- </pre> +--- +---@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 + local bufnr = vim.uri_to_bufnr(uri) - if not diagnostics then return end + if not bufnr then + return + end - bufnr = get_bufnr(bufnr) client_id = get_client_id(client_id) + local namespace = M.get_namespace(client_id) + local diagnostics = result.diagnostics - if not _diagnostic_cleanup[bufnr][client_id] then - _diagnostic_cleanup[bufnr][client_id] = true + 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 - -- 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 + 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 end - }) + end + + vim.diagnostic.config(config, namespace) end - set_diagnostic_cache(diagnostics, 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 <nomodeline> User LspDiagnosticsChanged") +end + +--- 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 --- }}} --- Diagnostic Retrieval {{{ +-- 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 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 + 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 diagnostics_by_bufnr + 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 = {} - for iter_client_id, _ in pairs(diagnostic_cache[bufnr]) do - local iter_diagnostics = M.get(bufnr, iter_client_id, predicate) - + 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 all_diagnostics + end) + return diagnostic_vim_to_lsp(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 + 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 @@ -398,216 +286,134 @@ end --- - 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] = {.... } } +--- Structured: { [1] = {...}, [5] = {.... } } +---@private 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 {} + 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 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)) + if client_id then + opts.namespace = M.get_namespace(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) + if not line_nr then + line_nr = vim.api.nvim_win_get_cursor(0)[1] - 1 end - table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end) + opts.lnum = line_nr - return line_diagnostics + return diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, opts)) end --- Get the counts for a particular severity --- ---- Useful for showing diagnostic counts in statusline. eg: ---- ---- <pre> ---- 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() ---- </pre> +---@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) - 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 + severity = severity_lsp_to_vim(severity) + local opts = { severity = severity } + if client_id ~= nil then + opts.namespace = M.get_namespace(client_id) 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 + 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) - 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) + 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) - return _diagnostic_pos( - opts, - 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 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) - return _diagnostic_move_pos( - "DiagnosticPrevious", - opts, - 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.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) - 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) + 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) - return _diagnostic_pos( - opts, - 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 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. @@ -626,17 +432,20 @@ end --- - {win_id}: (number, default 0) --- - Window ID function M.goto_next(opts) - return _diagnostic_move_pos( - "DiagnosticNext", - opts, - 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.goto_next(opts) end --- }}} --- Diagnostic Setters {{{ --- Set signs for given diagnostics --- +---@deprecated Prefer |vim.diagnostic._set_signs()| +--- --- Sign characters can be customized with the following commands: --- --- <pre> @@ -653,36 +462,13 @@ end --- - 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 - } - ) +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 @@ -690,6 +476,8 @@ end --- Set underline for given diagnostics --- +---@deprecated Prefer |vim.diagnostic._set_underline()| +--- --- Underline highlights can be customized by changing the following |:highlight| groups. --- --- <pre> @@ -706,35 +494,18 @@ end ---@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) - ) +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 --- Virtual Text {{{ --- Set virtual text given diagnostics --- +---@deprecated Prefer |vim.diagnostic._set_virtual_text()| +--- --- Virtual text highlights can be customized by changing the following |:highlight| groups. --- --- <pre> @@ -753,436 +524,75 @@ end --- - 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 +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, 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) +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 ---- 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. +--- Open a floating window with the diagnostics from {position} --- ---- 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 <buffer=%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 +---@deprecated Prefer |vim.diagnostic.show_position_diagnostics()| --- ---- 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) +---@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 - - -- 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 + return vim.diagnostic.show_position_diagnostics(opts, buf_nr, position) end --- }}} --- Diagnostic Display {{{ ---- |lsp-handler| for the method "textDocument/publishDiagnostics" +--- Open a floating window with the diagnostics from {line_nr} --- ----@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. ---- <pre> ---- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( ---- vim.lsp.diagnostic.on_publish_diagnostics, { ---- -- Enable underline, use default values ---- underline = true, ---- -- Enable virtual text, override spacing to 4 ---- virtual_text = { ---- spacing = 4, ---- }, ---- -- Use a function to dynamically turn signs off ---- -- and on, using buffer local variables ---- signs = function(bufnr, client_id) ---- return vim.bo[bufnr].show_signs == false ---- end, ---- -- Disable a feature ---- update_in_insert = false, ---- } ---- ) ---- </pre> +---@deprecated Prefer |vim.diagnostic.show_line_diagnostics()| --- ----@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) -function M.on_publish_diagnostics(_, result, ctx, config) - local client_id = ctx.client_id - local uri = result.uri - local bufnr = vim.uri_to_bufnr(uri) - - if not bufnr then - return - end - - 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 - 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(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 - 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.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged") - - if not diagnostics or vim.tbl_isempty(diagnostics) then - return - end - - local underline_opts = resolve_optional_value(config.underline) - if underline_opts then - M.set_underline(diagnostics, bufnr, client_id, nil, underline_opts) - 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) - end - - local signs_opts = resolve_optional_value(config.signs) - if signs_opts then - M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts) +---@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 - - -- cache extmarks - save_extmarks(bufnr, client_id) + 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: ---- <pre> ---- LspDiagnosticsFloatingError ---- LspDiagnosticsFloatingWarning ---- LspDiagnosticsFloatingInformation ---- LspDiagnosticsFloatingHint ---- </pre> ----@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 8fa6f6d024..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 @@ -428,7 +434,7 @@ M['window/logMessage'] = function(_, result, ctx, _) log.error(message) elseif message_type == protocol.MessageType.Warning then log.warn(message) - elseif message_type == protocol.MessageType.Info then + elseif message_type == protocol.MessageType.Info or message_type == protocol.MessageType.Log then log.info(message) else log.debug(message) @@ -458,7 +464,7 @@ end -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do M[k] = function(err, result, ctx, config) - local _ = log.debug() and log.debug('default_handler', ctx.method, { + local _ = log.trace() and log.trace('default_handler', ctx.method, { err = err, result = result, ctx=vim.inspect(ctx), config = config }) diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 5d2e396cc5..4597f1919a 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -14,7 +14,8 @@ log.levels = vim.deepcopy(vim.log.levels) -- Default log level is warn. local current_log_level = log.levels.WARN -local log_date_format = "%FT%H:%M:%S%z" +local log_date_format = "%F %H:%M:%S" +local format_func = function(arg) return vim.inspect(arg, {newline=''}) end do local path_sep = vim.loop.os_uname().version:match("Windows") and "\\" or "/" @@ -44,7 +45,7 @@ do end -- Start message for logging - logfile:write(string.format("[ START ] %s ] LSP logging initiated\n", os.date(log_date_format))) + logfile:write(string.format("[START][%s] LSP logging initiated\n", os.date(log_date_format))) for level, levelnr in pairs(log.levels) do -- Also export the log level on the root object. log[level] = levelnr @@ -67,14 +68,14 @@ do if levelnr < current_log_level then return false end if argc == 0 then return true end local info = debug.getinfo(2, "Sl") - local fileinfo = string.format("%s:%s", info.short_src, info.currentline) - local parts = { table.concat({"[", level, "]", os.date(log_date_format), "]", fileinfo, "]"}, " ") } + local header = string.format("[%s][%s] ...%s:%s", level, os.date(log_date_format), string.sub(info.short_src, #info.short_src - 15), info.currentline) + local parts = { header } for i = 1, argc do local arg = select(i, ...) if arg == nil then table.insert(parts, "nil") else - table.insert(parts, vim.inspect(arg, {newline=''})) + table.insert(parts, format_func(arg)) end end logfile:write(table.concat(parts, '\t'), "\n") @@ -104,6 +105,13 @@ function log.get_level() return current_log_level end +--- Sets formatting function used to format logs +---@param handle function function to apply to logging arguments, pass vim.inspect for multi-line formatting +function log.set_format_func(handle) + assert(handle == vim.inspect or type(handle) == 'function', "handle must be a function") + format_func = handle +end + --- Checks whether the level is sufficient for logging. ---@param level number log level ---@returns (bool) true if would log, false if not diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index eedb708118..7f31bbdf75 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -392,7 +392,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) ---@param payload (table) Converted into a JSON string, see |json_encode()| ---@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing. local function encode_and_send(payload) - local _ = log.debug() and log.debug("rpc.send.payload", payload) + local _ = log.debug() and log.debug("rpc.send", payload) if handle == nil or handle:is_closing() then return false end -- TODO(ashkan) remove this once we have a Lua json_encode schedule(function() @@ -493,7 +493,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) -- on_error(client_errors.INVALID_SERVER_JSON, err) return end - local _ = log.debug() and log.debug("decoded", decoded) + local _ = log.debug() and log.debug("rpc.receive", decoded) if type(decoded.method) == 'string' and decoded.id then -- Server Request 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 320c44e860..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', @@ -1130,7 +1147,7 @@ Doxyfile = textwrap.dedent(''' INPUT_FILTER = "{filter}" EXCLUDE = EXCLUDE_SYMLINKS = NO - EXCLUDE_PATTERNS = */private/* */health.lua + EXCLUDE_PATTERNS = */private/* */health.lua */_*.lua EXCLUDE_SYMBOLS = EXTENSION_MAPPING = lua=C EXTRACT_PRIVATE = NO diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 0ed5e6408b..ecbd4e13a3 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -444,6 +444,16 @@ void set_option_to(uint64_t channel_id, void *to, int type, #define TYPVAL_ENCODE_CONV_EXT_STRING(tv, str, len, type) \ TYPVAL_ENCODE_CONV_NIL(tv) +#define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \ + do { \ + const size_t len_ = (size_t)(len); \ + const blob_T *const blob_ = (blob); \ + kvi_push(edata->stack, STRING_OBJ(((String) { \ + .data = len_ != 0 ? xmemdup(blob_->bv_ga.ga_data, len_) : NULL, \ + .size = len_ \ + }))); \ + } while (0) + #define TYPVAL_ENCODE_CONV_FUNC_START(tv, fun) \ do { \ TYPVAL_ENCODE_CONV_NIL(tv); \ @@ -584,6 +594,7 @@ static inline void typval_encode_dict_end(EncodedData *const edata) #undef TYPVAL_ENCODE_CONV_STRING #undef TYPVAL_ENCODE_CONV_STR_STRING #undef TYPVAL_ENCODE_CONV_EXT_STRING +#undef TYPVAL_ENCODE_CONV_BLOB #undef TYPVAL_ENCODE_CONV_NUMBER #undef TYPVAL_ENCODE_CONV_FLOAT #undef TYPVAL_ENCODE_CONV_FUNC_START diff --git a/src/nvim/channel.c b/src/nvim/channel.c index 94db7fb3b9..54a59f6cc1 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -516,6 +516,7 @@ uint64_t channel_from_stdio(bool rpc, CallbackReader on_output, /// @param data will be consumed size_t channel_send(uint64_t id, char *data, size_t len, bool data_owned, const char **error) + FUNC_ATTR_NONNULL_ALL { Channel *chan = find_channel(id); size_t written = 0; diff --git a/src/nvim/charset.c b/src/nvim/charset.c index ab4e4ad4bd..e029973386 100644 --- a/src/nvim/charset.c +++ b/src/nvim/charset.c @@ -1150,15 +1150,29 @@ void getvcols(win_T *wp, pos_T *pos1, pos_T *pos2, colnr_T *left, /// skipwhite: skip over ' ' and '\t'. /// -/// @param[in] q String to skip in. +/// @param[in] p String to skip in. /// /// @return Pointer to character after the skipped whitespace. -char_u *skipwhite(const char_u *q) +char_u *skipwhite(const char_u *const p) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET { - const char_u *p = q; - while (ascii_iswhite(*p)) { + return skipwhite_len(p, STRLEN(p)); +} + +/// Like `skipwhite`, but skip up to `len` characters. +/// @see skipwhite +/// +/// @param[in] p String to skip in. +/// @param[in] len Max length to skip. +/// +/// @return Pointer to character after the skipped whitespace, or the `len`-th +/// character in the string. +char_u *skipwhite_len(const char_u *p, size_t len) + FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL + FUNC_ATTR_NONNULL_RET +{ + for (; len > 0 && ascii_iswhite(*p); len--) { p++; } return (char_u *)p; @@ -1304,6 +1318,18 @@ char_u* skiptowhite_esc(char_u *p) { return p; } +/// Skip over text until '\n' or NUL. +/// +/// @param[in] p Text to skip over. +/// +/// @return Pointer to the next '\n' or NUL character. +char_u *skip_to_newline(const char_u *const p) + FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL + FUNC_ATTR_NONNULL_RET +{ + return (char_u *)xstrchrnul((const char *)p, NL); +} + /// Gets a number from a string and skips over it, signalling overflow. /// /// @param[out] pp A pointer to a pointer to char_u. diff --git a/src/nvim/debugger.c b/src/nvim/debugger.c new file mode 100644 index 0000000000..73665009ff --- /dev/null +++ b/src/nvim/debugger.c @@ -0,0 +1,833 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + +/// @file debugger.c +/// +/// Vim script debugger functions + +#include "nvim/ascii.h" +#include "nvim/charset.h" +#include "nvim/debugger.h" +#include "nvim/eval.h" +#include "nvim/ex_docmd.h" +#include "nvim/ex_getln.h" +#include "nvim/fileio.h" +#include "nvim/getchar.h" +#include "nvim/globals.h" +#include "nvim/os/os.h" +#include "nvim/pos.h" +#include "nvim/regexp.h" +#include "nvim/screen.h" +#include "nvim/types.h" +#include "nvim/vim.h" + +/// batch mode debugging: don't save and restore typeahead. +static bool debug_greedy = false; + +static char *debug_oldval = NULL; // old and newval for debug expressions +static char *debug_newval = NULL; + +/// The list of breakpoints: dbg_breakp. +/// This is a grow-array of structs. +struct debuggy { + int dbg_nr; ///< breakpoint number + int dbg_type; ///< DBG_FUNC or DBG_FILE or DBG_EXPR + char_u *dbg_name; ///< function, expression or file name + regprog_T *dbg_prog; ///< regexp program + linenr_T dbg_lnum; ///< line number in function or file + int dbg_forceit; ///< ! used + typval_T *dbg_val; ///< last result of watchexpression + int dbg_level; ///< stored nested level for expr +}; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "debugger.c.generated.h" +#endif + +/// Debug mode. Repeatedly get Ex commands, until told to continue normal +/// execution. +void do_debug(char_u *cmd) +{ + int save_msg_scroll = msg_scroll; + int save_State = State; + int save_did_emsg = did_emsg; + const bool save_cmd_silent = cmd_silent; + int save_msg_silent = msg_silent; + int save_emsg_silent = emsg_silent; + bool save_redir_off = redir_off; + tasave_T typeaheadbuf; + bool typeahead_saved = false; + int save_ignore_script = 0; + int save_ex_normal_busy; + int n; + char_u *cmdline = NULL; + char_u *p; + char *tail = NULL; + static int last_cmd = 0; +#define CMD_CONT 1 +#define CMD_NEXT 2 +#define CMD_STEP 3 +#define CMD_FINISH 4 +#define CMD_QUIT 5 +#define CMD_INTERRUPT 6 +#define CMD_BACKTRACE 7 +#define CMD_FRAME 8 +#define CMD_UP 9 +#define CMD_DOWN 10 + + + RedrawingDisabled++; // don't redisplay the window + no_wait_return++; // don't wait for return + did_emsg = false; // don't use error from debugged stuff + cmd_silent = false; // display commands + msg_silent = false; // display messages + emsg_silent = false; // display error messages + redir_off = true; // don't redirect debug commands + + State = NORMAL; + debug_mode = true; + + if (!debug_did_msg) { + MSG(_("Entering Debug mode. Type \"cont\" to continue.")); + } + if (debug_oldval != NULL) { + smsg(_("Oldval = \"%s\""), debug_oldval); + xfree(debug_oldval); + debug_oldval = NULL; + } + if (debug_newval != NULL) { + smsg(_("Newval = \"%s\""), debug_newval); + xfree(debug_newval); + debug_newval = NULL; + } + if (sourcing_name != NULL) { + msg(sourcing_name); + } + if (sourcing_lnum != 0) { + smsg(_("line %" PRId64 ": %s"), (int64_t)sourcing_lnum, cmd); + } else { + smsg(_("cmd: %s"), cmd); + } + // Repeat getting a command and executing it. + for (;; ) { + msg_scroll = true; + need_wait_return = false; + // Save the current typeahead buffer and replace it with an empty one. + // This makes sure we get input from the user here and don't interfere + // with the commands being executed. Reset "ex_normal_busy" to avoid + // the side effects of using ":normal". Save the stuff buffer and make + // it empty. Set ignore_script to avoid reading from script input. + save_ex_normal_busy = ex_normal_busy; + ex_normal_busy = 0; + if (!debug_greedy) { + save_typeahead(&typeaheadbuf); + typeahead_saved = true; + save_ignore_script = ignore_script; + ignore_script = true; + } + + xfree(cmdline); + cmdline = (char_u *)getcmdline_prompt('>', NULL, 0, EXPAND_NOTHING, NULL, + CALLBACK_NONE); + + if (typeahead_saved) { + restore_typeahead(&typeaheadbuf); + ignore_script = save_ignore_script; + } + ex_normal_busy = save_ex_normal_busy; + + cmdline_row = msg_row; + msg_starthere(); + if (cmdline != NULL) { + // If this is a debug command, set "last_cmd". + // If not, reset "last_cmd". + // For a blank line use previous command. + p = skipwhite(cmdline); + if (*p != NUL) { + switch (*p) { + case 'c': last_cmd = CMD_CONT; + tail = "ont"; + break; + case 'n': last_cmd = CMD_NEXT; + tail = "ext"; + break; + case 's': last_cmd = CMD_STEP; + tail = "tep"; + break; + case 'f': + last_cmd = 0; + if (p[1] == 'r') { + last_cmd = CMD_FRAME; + tail = "rame"; + } else { + last_cmd = CMD_FINISH; + tail = "inish"; + } + break; + case 'q': last_cmd = CMD_QUIT; + tail = "uit"; + break; + case 'i': last_cmd = CMD_INTERRUPT; + tail = "nterrupt"; + break; + case 'b': + last_cmd = CMD_BACKTRACE; + if (p[1] == 't') { + tail = "t"; + } else { + tail = "acktrace"; + } + break; + case 'w': + last_cmd = CMD_BACKTRACE; + tail = "here"; + break; + case 'u': + last_cmd = CMD_UP; + tail = "p"; + break; + case 'd': + last_cmd = CMD_DOWN; + tail = "own"; + break; + default: last_cmd = 0; + } + if (last_cmd != 0) { + // Check that the tail matches. + p++; + while (*p != NUL && *p == *tail) { + p++; + tail++; + } + if (ASCII_ISALPHA(*p) && last_cmd != CMD_FRAME) { + last_cmd = 0; + } + } + } + + if (last_cmd != 0) { + // Execute debug command: decided where to break next and return. + switch (last_cmd) { + case CMD_CONT: + debug_break_level = -1; + break; + case CMD_NEXT: + debug_break_level = ex_nesting_level; + break; + case CMD_STEP: + debug_break_level = 9999; + break; + case CMD_FINISH: + debug_break_level = ex_nesting_level - 1; + break; + case CMD_QUIT: + got_int = true; + debug_break_level = -1; + break; + case CMD_INTERRUPT: + got_int = true; + debug_break_level = 9999; + // Do not repeat ">interrupt" cmd, continue stepping. + last_cmd = CMD_STEP; + break; + case CMD_BACKTRACE: + do_showbacktrace(cmd); + continue; + case CMD_FRAME: + if (*p == NUL) { + do_showbacktrace(cmd); + } else { + p = skipwhite(p); + do_setdebugtracelevel(p); + } + continue; + case CMD_UP: + debug_backtrace_level++; + do_checkbacktracelevel(); + continue; + case CMD_DOWN: + debug_backtrace_level--; + do_checkbacktracelevel(); + continue; + } + // Going out reset backtrace_level + debug_backtrace_level = 0; + break; + } + + // don't debug this command + n = debug_break_level; + debug_break_level = -1; + (void)do_cmdline(cmdline, getexline, NULL, + DOCMD_VERBOSE|DOCMD_EXCRESET); + debug_break_level = n; + } + lines_left = (int)(Rows - 1); + } + xfree(cmdline); + + RedrawingDisabled--; + no_wait_return--; + redraw_all_later(NOT_VALID); + need_wait_return = false; + msg_scroll = save_msg_scroll; + lines_left = (int)(Rows - 1); + State = save_State; + debug_mode = false; + did_emsg = save_did_emsg; + cmd_silent = save_cmd_silent; + msg_silent = save_msg_silent; + emsg_silent = save_emsg_silent; + redir_off = save_redir_off; + + // Only print the message again when typing a command before coming back here. + debug_did_msg = true; +} + +static int get_maxbacktrace_level(void) +{ + int maxbacktrace = 0; + + if (sourcing_name != NULL) { + char *p = (char *)sourcing_name; + char *q; + while ((q = strstr(p, "..")) != NULL) { + p = q + 2; + maxbacktrace++; + } + } + return maxbacktrace; +} + +static void do_setdebugtracelevel(char_u *arg) +{ + int level = atoi((char *)arg); + if (*arg == '+' || level < 0) { + debug_backtrace_level += level; + } else { + debug_backtrace_level = level; + } + + do_checkbacktracelevel(); +} + +static void do_checkbacktracelevel(void) +{ + if (debug_backtrace_level < 0) { + debug_backtrace_level = 0; + MSG(_("frame is zero")); + } else { + int max = get_maxbacktrace_level(); + if (debug_backtrace_level > max) { + debug_backtrace_level = max; + smsg(_("frame at highest level: %d"), max); + } + } +} + +static void do_showbacktrace(char_u *cmd) +{ + if (sourcing_name != NULL) { + int i = 0; + int max = get_maxbacktrace_level(); + char *cur = (char *)sourcing_name; + while (!got_int) { + char *next = strstr(cur, ".."); + if (next != NULL) { + *next = NUL; + } + if (i == max - debug_backtrace_level) { + smsg("->%d %s", max - i, cur); + } else { + smsg(" %d %s", max - i, cur); + } + i++; + if (next == NULL) { + break; + } + *next = '.'; + cur = next + 2; + } + } + if (sourcing_lnum != 0) { + smsg(_("line %" PRId64 ": %s"), (int64_t)sourcing_lnum, cmd); + } else { + smsg(_("cmd: %s"), cmd); + } +} + +/// ":debug". +void ex_debug(exarg_T *eap) +{ + int debug_break_level_save = debug_break_level; + + debug_break_level = 9999; + do_cmdline_cmd((char *)eap->arg); + debug_break_level = debug_break_level_save; +} + +static char_u *debug_breakpoint_name = NULL; +static linenr_T debug_breakpoint_lnum; + +/// When debugging or a breakpoint is set on a skipped command, no debug prompt +/// is shown by do_one_cmd(). This situation is indicated by debug_skipped, and +/// debug_skipped_name is then set to the source name in the breakpoint case. If +/// a skipped command decides itself that a debug prompt should be displayed, it +/// can do so by calling dbg_check_skipped(). +static int debug_skipped; +static char_u *debug_skipped_name; + +/// Go to debug mode when a breakpoint was encountered or "ex_nesting_level" is +/// at or below the break level. But only when the line is actually +/// executed. Return true and set breakpoint_name for skipped commands that +/// decide to execute something themselves. +/// Called from do_one_cmd() before executing a command. +void dbg_check_breakpoint(exarg_T *eap) +{ + char_u *p; + + debug_skipped = false; + if (debug_breakpoint_name != NULL) { + if (!eap->skip) { + // replace K_SNR with "<SNR>" + if (debug_breakpoint_name[0] == K_SPECIAL + && debug_breakpoint_name[1] == KS_EXTRA + && debug_breakpoint_name[2] == (int)KE_SNR) { + p = (char_u *)"<SNR>"; + } else { + p = (char_u *)""; + } + smsg(_("Breakpoint in \"%s%s\" line %" PRId64), + p, + debug_breakpoint_name + (*p == NUL ? 0 : 3), + (int64_t)debug_breakpoint_lnum); + debug_breakpoint_name = NULL; + do_debug(eap->cmd); + } else { + debug_skipped = true; + debug_skipped_name = debug_breakpoint_name; + debug_breakpoint_name = NULL; + } + } else if (ex_nesting_level <= debug_break_level) { + if (!eap->skip) { + do_debug(eap->cmd); + } else { + debug_skipped = true; + debug_skipped_name = NULL; + } + } +} + +/// Go to debug mode if skipped by dbg_check_breakpoint() because eap->skip was +/// set. +/// +/// @return true when the debug mode is entered this time. +bool dbg_check_skipped(exarg_T *eap) +{ + int prev_got_int; + + if (debug_skipped) { + // Save the value of got_int and reset it. We don't want a previous + // interruption cause flushing the input buffer. + prev_got_int = got_int; + got_int = false; + debug_breakpoint_name = debug_skipped_name; + // eap->skip is true + eap->skip = false; + dbg_check_breakpoint(eap); + eap->skip = true; + got_int |= prev_got_int; + return true; + } + return false; +} + +static garray_T dbg_breakp = { 0, 0, sizeof(struct debuggy), 4, NULL }; +#define BREAKP(idx) (((struct debuggy *)dbg_breakp.ga_data)[idx]) +#define DEBUGGY(gap, idx) (((struct debuggy *)gap->ga_data)[idx]) +static int last_breakp = 0; // nr of last defined breakpoint + +// Profiling uses file and func names similar to breakpoints. +static garray_T prof_ga = { 0, 0, sizeof(struct debuggy), 4, NULL }; +#define DBG_FUNC 1 +#define DBG_FILE 2 +#define DBG_EXPR 3 + +/// Evaluate the "bp->dbg_name" expression and return the result. +/// Disables error messages. +static typval_T *eval_expr_no_emsg(struct debuggy *const bp) + FUNC_ATTR_NONNULL_ALL +{ + // Disable error messages, a bad expression would make Vim unusable. + emsg_off++; + typval_T *const tv = eval_expr(bp->dbg_name); + emsg_off--; + return tv; +} + +/// Parse the arguments of ":profile", ":breakadd" or ":breakdel" and put them +/// in the entry just after the last one in dbg_breakp. Note that "dbg_name" +/// is allocated. +/// Returns FAIL for failure. +/// +/// @param arg +/// @param gap either &dbg_breakp or &prof_ga +static int dbg_parsearg(char_u *arg, garray_T *gap) +{ + char_u *p = arg; + char_u *q; + struct debuggy *bp; + bool here = false; + + ga_grow(gap, 1); + + bp = &DEBUGGY(gap, gap->ga_len); + + // Find "func" or "file". + if (STRNCMP(p, "func", 4) == 0) { + bp->dbg_type = DBG_FUNC; + } else if (STRNCMP(p, "file", 4) == 0) { + bp->dbg_type = DBG_FILE; + } else if (gap != &prof_ga && STRNCMP(p, "here", 4) == 0) { + if (curbuf->b_ffname == NULL) { + EMSG(_(e_noname)); + return FAIL; + } + bp->dbg_type = DBG_FILE; + here = true; + } else if (gap != &prof_ga && STRNCMP(p, "expr", 4) == 0) { + bp->dbg_type = DBG_EXPR; + } else { + EMSG2(_(e_invarg2), p); + return FAIL; + } + p = skipwhite(p + 4); + + // Find optional line number. + if (here) { + bp->dbg_lnum = curwin->w_cursor.lnum; + } else if (gap != &prof_ga && ascii_isdigit(*p)) { + bp->dbg_lnum = getdigits_long(&p, true, 0); + p = skipwhite(p); + } else { + bp->dbg_lnum = 0; + } + + // Find the function or file name. Don't accept a function name with (). + if ((!here && *p == NUL) + || (here && *p != NUL) + || (bp->dbg_type == DBG_FUNC && strstr((char *)p, "()") != NULL)) { + EMSG2(_(e_invarg2), arg); + return FAIL; + } + + if (bp->dbg_type == DBG_FUNC) { + bp->dbg_name = vim_strsave(p); + } else if (here) { + bp->dbg_name = vim_strsave(curbuf->b_ffname); + } else if (bp->dbg_type == DBG_EXPR) { + bp->dbg_name = vim_strsave(p); + bp->dbg_val = eval_expr_no_emsg(bp); + } else { + // Expand the file name in the same way as do_source(). This means + // doing it twice, so that $DIR/file gets expanded when $DIR is + // "~/dir". + q = expand_env_save(p); + if (q == NULL) { + return FAIL; + } + p = expand_env_save(q); + xfree(q); + if (p == NULL) { + return FAIL; + } + if (*p != '*') { + bp->dbg_name = (char_u *)fix_fname((char *)p); + xfree(p); + } else { + bp->dbg_name = p; + } + } + + if (bp->dbg_name == NULL) { + return FAIL; + } + return OK; +} + +/// ":breakadd". Also used for ":profile". +void ex_breakadd(exarg_T *eap) +{ + struct debuggy *bp; + garray_T *gap; + + gap = &dbg_breakp; + if (eap->cmdidx == CMD_profile) { + gap = &prof_ga; + } + + if (dbg_parsearg(eap->arg, gap) == OK) { + bp = &DEBUGGY(gap, gap->ga_len); + bp->dbg_forceit = eap->forceit; + + if (bp->dbg_type != DBG_EXPR) { + char_u *pat = file_pat_to_reg_pat(bp->dbg_name, NULL, NULL, false); + if (pat != NULL) { + bp->dbg_prog = vim_regcomp(pat, RE_MAGIC + RE_STRING); + xfree(pat); + } + if (pat == NULL || bp->dbg_prog == NULL) { + xfree(bp->dbg_name); + } else { + if (bp->dbg_lnum == 0) { // default line number is 1 + bp->dbg_lnum = 1; + } + if (eap->cmdidx != CMD_profile) { + DEBUGGY(gap, gap->ga_len).dbg_nr = ++last_breakp; + debug_tick++; + } + gap->ga_len++; + } + } else { + // DBG_EXPR + DEBUGGY(gap, gap->ga_len++).dbg_nr = ++last_breakp; + debug_tick++; + } + } +} + +/// ":debuggreedy". +void ex_debuggreedy(exarg_T *eap) +{ + if (eap->addr_count == 0 || eap->line2 != 0) { + debug_greedy = true; + } else { + debug_greedy = false; + } +} + +/// ":breakdel" and ":profdel". +void ex_breakdel(exarg_T *eap) +{ + struct debuggy *bp, *bpi; + int nr; + int todel = -1; + bool del_all = false; + linenr_T best_lnum = 0; + garray_T *gap; + + gap = &dbg_breakp; + if (eap->cmdidx == CMD_profdel) { + gap = &prof_ga; + } + + if (ascii_isdigit(*eap->arg)) { + // ":breakdel {nr}" + nr = atoi((char *)eap->arg); + for (int i = 0; i < gap->ga_len; i++) { + if (DEBUGGY(gap, i).dbg_nr == nr) { + todel = i; + break; + } + } + } else if (*eap->arg == '*') { + todel = 0; + del_all = true; + } else { + // ":breakdel {func|file|expr} [lnum] {name}" + if (dbg_parsearg(eap->arg, gap) == FAIL) { + return; + } + bp = &DEBUGGY(gap, gap->ga_len); + for (int i = 0; i < gap->ga_len; i++) { + bpi = &DEBUGGY(gap, i); + if (bp->dbg_type == bpi->dbg_type + && STRCMP(bp->dbg_name, bpi->dbg_name) == 0 + && (bp->dbg_lnum == bpi->dbg_lnum + || (bp->dbg_lnum == 0 + && (best_lnum == 0 + || bpi->dbg_lnum < best_lnum)))) { + todel = i; + best_lnum = bpi->dbg_lnum; + } + } + xfree(bp->dbg_name); + } + + if (todel < 0) { + EMSG2(_("E161: Breakpoint not found: %s"), eap->arg); + } else { + while (!GA_EMPTY(gap)) { + xfree(DEBUGGY(gap, todel).dbg_name); + if (DEBUGGY(gap, todel).dbg_type == DBG_EXPR + && DEBUGGY(gap, todel).dbg_val != NULL) { + tv_free(DEBUGGY(gap, todel).dbg_val); + } + vim_regfree(DEBUGGY(gap, todel).dbg_prog); + gap->ga_len--; + if (todel < gap->ga_len) { + memmove(&DEBUGGY(gap, todel), &DEBUGGY(gap, todel + 1), + (size_t)(gap->ga_len - todel) * sizeof(struct debuggy)); + } + if (eap->cmdidx == CMD_breakdel) { + debug_tick++; + } + if (!del_all) { + break; + } + } + + // If all breakpoints were removed clear the array. + if (GA_EMPTY(gap)) { + ga_clear(gap); + } + } +} + +/// ":breaklist". +void ex_breaklist(exarg_T *eap) +{ + struct debuggy *bp; + + if (GA_EMPTY(&dbg_breakp)) { + MSG(_("No breakpoints defined")); + } else { + for (int i = 0; i < dbg_breakp.ga_len; i++) { + bp = &BREAKP(i); + if (bp->dbg_type == DBG_FILE) { + home_replace(NULL, bp->dbg_name, NameBuff, MAXPATHL, true); + } + if (bp->dbg_type != DBG_EXPR) { + smsg(_("%3d %s %s line %" PRId64), + bp->dbg_nr, + bp->dbg_type == DBG_FUNC ? "func" : "file", + bp->dbg_type == DBG_FUNC ? bp->dbg_name : NameBuff, + (int64_t)bp->dbg_lnum); + } else { + smsg(_("%3d expr %s"), bp->dbg_nr, bp->dbg_name); + } + } + } +} + +/// Find a breakpoint for a function or sourced file. +/// Returns line number at which to break; zero when no matching breakpoint. +linenr_T +dbg_find_breakpoint( + bool file, // true for a file, false for a function + char_u *fname, // file or function name + linenr_T after // after this line number +) +{ + return debuggy_find(file, fname, after, &dbg_breakp, NULL); +} + +/// @param file true for a file, false for a function +/// @param fname file or function name +/// @param fp[out] forceit +/// +/// @returns true if profiling is on for a function or sourced file. +bool has_profiling(bool file, char_u *fname, bool *fp) +{ + return debuggy_find(file, fname, (linenr_T)0, &prof_ga, fp) + != (linenr_T)0; +} + +/// Common code for dbg_find_breakpoint() and has_profiling(). +static linenr_T +debuggy_find( + bool file, // true for a file, false for a function + char_u *fname, // file or function name + linenr_T after, // after this line number + garray_T *gap, // either &dbg_breakp or &prof_ga + bool *fp // if not NULL: return forceit +) +{ + struct debuggy *bp; + linenr_T lnum = 0; + char_u *name = fname; + int prev_got_int; + + // Return quickly when there are no breakpoints. + if (GA_EMPTY(gap)) { + return (linenr_T)0; + } + + // Replace K_SNR in function name with "<SNR>". + if (!file && fname[0] == K_SPECIAL) { + name = xmalloc(STRLEN(fname) + 3); + STRCPY(name, "<SNR>"); + STRCPY(name + 5, fname + 3); + } + + for (int i = 0; i < gap->ga_len; i++) { + // Skip entries that are not useful or are for a line that is beyond + // an already found breakpoint. + bp = &DEBUGGY(gap, i); + if ((bp->dbg_type == DBG_FILE) == file + && bp->dbg_type != DBG_EXPR + && (gap == &prof_ga + || (bp->dbg_lnum > after && (lnum == 0 || bp->dbg_lnum < lnum)))) { + // Save the value of got_int and reset it. We don't want a + // previous interruption cancel matching, only hitting CTRL-C + // while matching should abort it. + prev_got_int = got_int; + got_int = false; + if (vim_regexec_prog(&bp->dbg_prog, false, name, (colnr_T)0)) { + lnum = bp->dbg_lnum; + if (fp != NULL) { + *fp = bp->dbg_forceit; + } + } + got_int |= prev_got_int; + } else if (bp->dbg_type == DBG_EXPR) { + bool line = false; + + typval_T *const tv = eval_expr_no_emsg(bp); + if (tv != NULL) { + if (bp->dbg_val == NULL) { + debug_oldval = typval_tostring(NULL); + bp->dbg_val = tv; + debug_newval = typval_tostring(bp->dbg_val); + line = true; + } else { + if (typval_compare(tv, bp->dbg_val, EXPR_IS, false) == OK + && tv->vval.v_number == false) { + line = true; + debug_oldval = typval_tostring(bp->dbg_val); + // Need to evaluate again, typval_compare() overwrites "tv". + typval_T *const v = eval_expr_no_emsg(bp); + debug_newval = typval_tostring(v); + tv_free(bp->dbg_val); + bp->dbg_val = v; + } + tv_free(tv); + } + } else if (bp->dbg_val != NULL) { + debug_oldval = typval_tostring(bp->dbg_val); + debug_newval = typval_tostring(NULL); + tv_free(bp->dbg_val); + bp->dbg_val = NULL; + line = true; + } + + if (line) { + lnum = after > 0 ? after : 1; + break; + } + } + } + if (name != fname) { + xfree(name); + } + + return lnum; +} + +/// Called when a breakpoint was encountered. +void dbg_breakpoint(char_u *name, linenr_T lnum) +{ + // We need to check if this line is actually executed in do_one_cmd() + debug_breakpoint_name = name; + debug_breakpoint_lnum = lnum; +} diff --git a/src/nvim/debugger.h b/src/nvim/debugger.h new file mode 100644 index 0000000000..1f1139b3bc --- /dev/null +++ b/src/nvim/debugger.h @@ -0,0 +1,11 @@ +#ifndef NVIM_DEBUGGER_H +#define NVIM_DEBUGGER_H + +#include <stdbool.h> + +#include "nvim/ex_cmds_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "debugger.h.generated.h" +#endif +#endif // NVIM_DEBUGGER_H diff --git a/src/nvim/eval.c b/src/nvim/eval.c index dfbb187b5b..5607de3cc1 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -69,6 +69,7 @@ static char *e_nowhitespace = N_("E274: No white space allowed before parenthesis"); static char *e_invalwindow = N_("E957: Invalid window number"); static char *e_lock_unlock = N_("E940: Cannot lock or unlock variable %s"); +static char *e_write2 = N_("E80: Error while writing: %s"); // TODO(ZyX-I): move to eval/executor static char *e_letwrong = N_("E734: Wrong variable type for %s="); @@ -113,6 +114,8 @@ typedef struct { int fi_varcount; // nr of variables in the list listwatch_T fi_lw; // keep an eye on the item used. list_T *fi_list; // list being used + int fi_bi; // index of blob + blob_T *fi_blob; // blob being used } forinfo_T; // values for vv_flags: @@ -227,6 +230,7 @@ static struct vimvar { VV(VV_TYPE_DICT, "t_dict", VAR_NUMBER, VV_RO), VV(VV_TYPE_FLOAT, "t_float", VAR_NUMBER, VV_RO), VV(VV_TYPE_BOOL, "t_bool", VAR_NUMBER, VV_RO), + VV(VV_TYPE_BLOB, "t_blob", VAR_NUMBER, VV_RO), VV(VV_EVENT, "event", VAR_DICT, VV_RO), VV(VV_ECHOSPACE, "echospace", VAR_NUMBER, VV_RO), VV(VV_ARGV, "argv", VAR_LIST, VV_RO), @@ -238,6 +242,7 @@ static struct vimvar { VV(VV__NULL_STRING, "_null_string", VAR_STRING, VV_RO), VV(VV__NULL_LIST, "_null_list", VAR_LIST, VV_RO), VV(VV__NULL_DICT, "_null_dict", VAR_DICT, VV_RO), + VV(VV__NULL_BLOB, "_null_blob", VAR_BLOB, VV_RO), VV(VV_LUA, "lua", VAR_PARTIAL, VV_RO), }; #undef VV @@ -251,6 +256,7 @@ static struct vimvar { #define vv_str vv_di.di_tv.vval.v_string #define vv_list vv_di.di_tv.vval.v_list #define vv_dict vv_di.di_tv.vval.v_dict +#define vv_blob vv_di.di_tv.vval.v_blob #define vv_partial vv_di.di_tv.vval.v_partial #define vv_tv vv_di.di_tv @@ -393,6 +399,7 @@ void eval_init(void) set_vim_var_nr(VV_TYPE_DICT, VAR_TYPE_DICT); set_vim_var_nr(VV_TYPE_FLOAT, VAR_TYPE_FLOAT); set_vim_var_nr(VV_TYPE_BOOL, VAR_TYPE_BOOL); + set_vim_var_nr(VV_TYPE_BLOB, VAR_TYPE_BLOB); set_vim_var_bool(VV_FALSE, kBoolVarFalse); set_vim_var_bool(VV_TRUE, kBoolVarTrue); @@ -2059,18 +2066,17 @@ char_u *get_lval(char_u *const name, typval_T *const rettv, return NULL; } - /* - * Loop until no more [idx] or .key is following. - */ + // Loop until no more [idx] or .key is following. lp->ll_tv = &v->di_tv; var1.v_type = VAR_UNKNOWN; var2.v_type = VAR_UNKNOWN; while (*p == '[' || (*p == '.' && lp->ll_tv->v_type == VAR_DICT)) { if (!(lp->ll_tv->v_type == VAR_LIST && lp->ll_tv->vval.v_list != NULL) - && !(lp->ll_tv->v_type == VAR_DICT - && lp->ll_tv->vval.v_dict != NULL)) { - if (!quiet) - EMSG(_("E689: Can only index a List or Dictionary")); + && !(lp->ll_tv->v_type == VAR_DICT && lp->ll_tv->vval.v_dict != NULL) + && !(lp->ll_tv->v_type == VAR_BLOB && lp->ll_tv->vval.v_blob != NULL)) { + if (!quiet) { + EMSG(_("E689: Can only index a List, Dictionary or Blob")); + } return NULL; } if (lp->ll_range) { @@ -2119,10 +2125,11 @@ char_u *get_lval(char_u *const name, typval_T *const rettv, tv_clear(&var1); return NULL; } - if (rettv != NULL && (rettv->v_type != VAR_LIST - || rettv->vval.v_list == NULL)) { + if (rettv != NULL + && !(rettv->v_type == VAR_LIST && rettv->vval.v_list != NULL) + && !(rettv->v_type == VAR_BLOB && rettv->vval.v_blob != NULL)) { if (!quiet) { - EMSG(_("E709: [:] requires a List value")); + EMSG(_("E709: [:] requires a List or Blob value")); } tv_clear(&var1); return NULL; @@ -2236,6 +2243,38 @@ char_u *get_lval(char_u *const name, typval_T *const rettv, tv_clear(&var1); lp->ll_tv = &lp->ll_di->di_tv; + } else if (lp->ll_tv->v_type == VAR_BLOB) { + // Get the number and item for the only or first index of the List. + if (empty1) { + lp->ll_n1 = 0; + } else { + // Is number or string. + lp->ll_n1 = (long)tv_get_number(&var1); + } + tv_clear(&var1); + + const int bloblen = tv_blob_len(lp->ll_tv->vval.v_blob); + if (lp->ll_n1 < 0 || lp->ll_n1 > bloblen + || (lp->ll_range && lp->ll_n1 == bloblen)) { + if (!quiet) { + EMSGN(_(e_blobidx), lp->ll_n1); + } + tv_clear(&var2); + return NULL; + } + if (lp->ll_range && !lp->ll_empty2) { + lp->ll_n2 = (long)tv_get_number(&var2); + tv_clear(&var2); + if (lp->ll_n2 < 0 || lp->ll_n2 >= bloblen || lp->ll_n2 < lp->ll_n1) { + if (!quiet) { + EMSGN(_(e_blobidx), lp->ll_n2); + } + return NULL; + } + } + lp->ll_blob = lp->ll_tv->vval.v_blob; + lp->ll_tv = NULL; + break; } else { // Get the number and item for the only or first index of the List. if (empty1) { @@ -2329,7 +2368,50 @@ static void set_var_lval(lval_T *lp, char_u *endp, typval_T *rettv, if (lp->ll_tv == NULL) { cc = *endp; *endp = NUL; - if (op != NULL && *op != '=') { + if (lp->ll_blob != NULL) { + if (op != NULL && *op != '=') { + EMSG2(_(e_letwrong), op); + return; + } + if (var_check_lock(lp->ll_blob->bv_lock, lp->ll_name, TV_CSTRING)) { + return; + } + + if (lp->ll_range && rettv->v_type == VAR_BLOB) { + if (lp->ll_empty2) { + lp->ll_n2 = tv_blob_len(lp->ll_blob) - 1; + } + + if (lp->ll_n2 - lp->ll_n1 + 1 != tv_blob_len(rettv->vval.v_blob)) { + EMSG(_("E972: Blob value does not have the right number of bytes")); + return; + } + if (lp->ll_empty2) { + lp->ll_n2 = tv_blob_len(lp->ll_blob); + } + + for (int il = lp->ll_n1, ir = 0; il <= lp->ll_n2; il++) { + tv_blob_set(lp->ll_blob, il, tv_blob_get(rettv->vval.v_blob, ir++)); + } + } else { + bool error = false; + const char_u val = tv_get_number_chk(rettv, &error); + if (!error) { + garray_T *const gap = &lp->ll_blob->bv_ga; + + // Allow for appending a byte. Setting a byte beyond + // the end is an error otherwise. + if (lp->ll_n1 < gap->ga_len || lp->ll_n1 == gap->ga_len) { + ga_grow(&lp->ll_blob->bv_ga, 1); + tv_blob_set(lp->ll_blob, lp->ll_n1, val); + if (lp->ll_n1 == gap->ga_len) { + gap->ga_len++; + } + } + // error for invalid range was already given in get_lval() + } + } + } else if (op != NULL && *op != '=') { typval_T tv; if (is_const) { @@ -2508,19 +2590,32 @@ void *eval_for_line(const char_u *arg, bool *errp, char_u **nextcmdp, int skip) if (eval0(skipwhite(expr + 2), &tv, nextcmdp, !skip) == OK) { *errp = false; if (!skip) { - l = tv.vval.v_list; - if (tv.v_type != VAR_LIST) { - EMSG(_(e_listreq)); - tv_clear(&tv); - } else if (l == NULL) { - // a null list is like an empty list: do nothing + if (tv.v_type == VAR_LIST) { + l = tv.vval.v_list; + if (l == NULL) { + // a null list is like an empty list: do nothing + tv_clear(&tv); + } else { + // No need to increment the refcount, it's already set for + // the list being used in "tv". + fi->fi_list = l; + tv_list_watch_add(l, &fi->fi_lw); + fi->fi_lw.lw_item = tv_list_first(l); + } + } else if (tv.v_type == VAR_BLOB) { + fi->fi_bi = 0; + if (tv.vval.v_blob != NULL) { + typval_T btv; + + // Make a copy, so that the iteration still works when the + // blob is changed. + tv_blob_copy(&tv, &btv); + fi->fi_blob = btv.vval.v_blob; + } tv_clear(&tv); } else { - /* No need to increment the refcount, it's already set for the - * list being used in "tv". */ - fi->fi_list = l; - tv_list_watch_add(l, &fi->fi_lw); - fi->fi_lw.lw_item = tv_list_first(l); + EMSG(_(e_listblobreq)); + tv_clear(&tv); } } } @@ -2542,6 +2637,19 @@ bool next_for_item(void *fi_void, char_u *arg) { forinfo_T *fi = (forinfo_T *)fi_void; + if (fi->fi_blob != NULL) { + if (fi->fi_bi >= tv_blob_len(fi->fi_blob)) { + return false; + } + typval_T tv; + tv.v_type = VAR_NUMBER; + tv.v_lock = VAR_FIXED; + tv.vval.v_number = tv_blob_get(fi->fi_blob, fi->fi_bi); + fi->fi_bi++; + return ex_let_vars(arg, &tv, true, + fi->fi_semicolon, fi->fi_varcount, false, NULL) == OK; + } + listitem_T *item = fi->fi_lw.lw_item; if (item == NULL) { return false; @@ -2565,6 +2673,9 @@ void free_for_info(void *fi_void) tv_list_watch_remove(fi->fi_list, &fi->fi_lw); tv_list_unref(fi->fi_list); } + if (fi != NULL && fi->fi_blob != NULL) { + tv_blob_unref(fi->fi_blob); + } xfree(fi); } @@ -2974,7 +3085,7 @@ static int do_lock_var(lval_T *lp, char_u *name_end FUNC_ATTR_UNUSED, } else { di->di_flags &= ~DI_FLAGS_LOCK; } - tv_item_lock(&di->di_tv, deep, lock); + tv_item_lock(&di->di_tv, deep, lock, false); } } } else if (lp->ll_range) { @@ -2982,16 +3093,16 @@ static int do_lock_var(lval_T *lp, char_u *name_end FUNC_ATTR_UNUSED, // (un)lock a range of List items. while (li != NULL && (lp->ll_empty2 || lp->ll_n2 >= lp->ll_n1)) { - tv_item_lock(TV_LIST_ITEM_TV(li), deep, lock); + tv_item_lock(TV_LIST_ITEM_TV(li), deep, lock, false); li = TV_LIST_ITEM_NEXT(lp->ll_list, li); lp->ll_n1++; } } else if (lp->ll_list != NULL) { // (un)lock a List item. - tv_item_lock(TV_LIST_ITEM_TV(lp->ll_li), deep, lock); + tv_item_lock(TV_LIST_ITEM_TV(lp->ll_li), deep, lock, false); } else { // (un)lock a Dictionary item. - tv_item_lock(&lp->ll_di->di_tv, deep, lock); + tv_item_lock(&lp->ll_di->di_tv, deep, lock, false); } return ret; @@ -3607,7 +3718,7 @@ static int eval5(char_u **arg, typval_T *rettv, int evaluate) if (op != '+' && op != '-' && op != '.') break; - if ((op != '+' || rettv->v_type != VAR_LIST) + if ((op != '+' || (rettv->v_type != VAR_LIST && rettv->v_type != VAR_BLOB)) && (op == '.' || rettv->v_type != VAR_FLOAT)) { // For "list + ...", an illegal use of the first operand as // a number cannot be determined before evaluating the 2nd @@ -3653,6 +3764,21 @@ static int eval5(char_u **arg, typval_T *rettv, int evaluate) tv_clear(rettv); rettv->v_type = VAR_STRING; rettv->vval.v_string = p; + } else if (op == '+' && rettv->v_type == VAR_BLOB + && var2.v_type == VAR_BLOB) { + const blob_T *const b1 = rettv->vval.v_blob; + const blob_T *const b2 = var2.vval.v_blob; + blob_T *const b = tv_blob_alloc(); + + for (int i = 0; i < tv_blob_len(b1); i++) { + ga_append(&b->bv_ga, tv_blob_get(b1, i)); + } + for (int i = 0; i < tv_blob_len(b2); i++) { + ga_append(&b->bv_ga, tv_blob_get(b2, i)); + } + + tv_clear(rettv); + tv_blob_set_ret(rettv, b); } else if (op == '+' && rettv->v_type == VAR_LIST && var2.v_type == VAR_LIST) { // Concatenate Lists. @@ -3673,10 +3799,12 @@ static int eval5(char_u **arg, typval_T *rettv, int evaluate) } else { n1 = tv_get_number_chk(rettv, &error); if (error) { - /* This can only happen for "list + non-list". For - * "non-list + ..." or "something - ...", we returned - * before evaluating the 2nd operand. */ + // This can only happen for "list + non-list" or + // "blob + non-blob". For "non-list + ..." or + // "something - ...", we returned before evaluating the + // 2nd operand. tv_clear(rettv); + tv_clear(&var2); return FAIL; } if (var2.v_type == VAR_FLOAT) @@ -3850,6 +3978,7 @@ static int eval6(char_u **arg, typval_T *rettv, int evaluate, int want_string) // Handle sixth level expression: // number number constant +// 0zFFFFFFFF Blob constant // "string" string constant // 'string' literal string constant // &option-name option value @@ -3946,7 +4075,37 @@ static int eval7( rettv->v_type = VAR_FLOAT; rettv->vval.v_float = f; } + } else if (**arg == '0' && ((*arg)[1] == 'z' || (*arg)[1] == 'Z')) { + blob_T *blob = NULL; + // Blob constant: 0z0123456789abcdef + if (evaluate) { + blob = tv_blob_alloc(); + } + char_u *bp; + for (bp = *arg + 2; ascii_isxdigit(bp[0]); bp += 2) { + if (!ascii_isxdigit(bp[1])) { + if (blob != NULL) { + EMSG(_("E973: Blob literal should have an even number of hex " + "characters")); + ga_clear(&blob->bv_ga); + XFREE_CLEAR(blob); + } + ret = FAIL; + break; + } + if (blob != NULL) { + ga_append(&blob->bv_ga, (hex2nr(*bp) << 4) + hex2nr(*(bp + 1))); + } + if (bp[2] == '.' && ascii_isxdigit(bp[3])) { + bp++; + } + } + if (blob != NULL) { + tv_blob_set_ret(rettv, blob); + } + *arg = bp; } else { + // decimal, hex or octal number vim_str2nr(*arg, NULL, &len, STR2NR_ALL, &n, NULL, 0, true); if (len == 0) { EMSG2(_(e_invexpr2), *arg); @@ -4336,7 +4495,8 @@ eval_index( case VAR_STRING: case VAR_NUMBER: case VAR_LIST: - case VAR_DICT: { + case VAR_DICT: + case VAR_BLOB: { break; } } @@ -4462,6 +4622,53 @@ eval_index( rettv->vval.v_string = (char_u *)v; break; } + case VAR_BLOB: { + len = tv_blob_len(rettv->vval.v_blob); + if (range) { + // The resulting variable is a sub-blob. If the indexes + // are out of range the result is empty. + if (n1 < 0) { + n1 = len + n1; + if (n1 < 0) { + n1 = 0; + } + } + if (n2 < 0) { + n2 = len + n2; + } else if (n2 >= len) { + n2 = len - 1; + } + if (n1 >= len || n2 < 0 || n1 > n2) { + tv_clear(rettv); + rettv->v_type = VAR_BLOB; + rettv->vval.v_blob = NULL; + } else { + blob_T *const blob = tv_blob_alloc(); + ga_grow(&blob->bv_ga, n2 - n1 + 1); + blob->bv_ga.ga_len = n2 - n1 + 1; + for (long i = n1; i <= n2; i++) { + tv_blob_set(blob, i - n1, tv_blob_get(rettv->vval.v_blob, i)); + } + tv_clear(rettv); + tv_blob_set_ret(rettv, blob); + } + } else { + // The resulting variable is a byte value. + // If the index is too big or negative that is an error. + if (n1 < 0) { + n1 = len + n1; + } + if (n1 < len && n1 >= 0) { + const int v = (int)tv_blob_get(rettv->vval.v_blob, n1); + tv_clear(rettv); + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = v; + } else { + EMSGN(_(e_blobidx), n1); + } + } + break; + } case VAR_LIST: { len = tv_list_len(rettv->vval.v_list); if (n1 < 0) { @@ -5398,7 +5605,8 @@ bool set_ref_in_item(typval_T *tv, int copyID, ht_stack_T **ht_stack, case VAR_SPECIAL: case VAR_FLOAT: case VAR_NUMBER: - case VAR_STRING: { + case VAR_STRING: + case VAR_BLOB: { break; } } @@ -6161,6 +6369,7 @@ void filter_map(typval_T *argvars, typval_T *rettv, int map) dict_T *d = NULL; typval_T save_val; typval_T save_key; + blob_T *b = NULL; int rem = false; int todo; char_u *ermsg = (char_u *)(map ? "map()" : "filter()"); @@ -6170,7 +6379,12 @@ void filter_map(typval_T *argvars, typval_T *rettv, int map) int save_did_emsg; int idx = 0; - if (argvars[0].v_type == VAR_LIST) { + if (argvars[0].v_type == VAR_BLOB) { + tv_copy(&argvars[0], rettv); + if ((b = argvars[0].vval.v_blob) == NULL) { + return; + } + } else if (argvars[0].v_type == VAR_LIST) { tv_copy(&argvars[0], rettv); if ((l = argvars[0].vval.v_list) == NULL || (!map @@ -6184,7 +6398,7 @@ void filter_map(typval_T *argvars, typval_T *rettv, int map) return; } } else { - EMSG2(_(e_listdictarg), ermsg); + EMSG2(_(e_listdictblobarg), ermsg); return; } @@ -6234,6 +6448,34 @@ void filter_map(typval_T *argvars, typval_T *rettv, int map) } } hash_unlock(ht); + } else if (argvars[0].v_type == VAR_BLOB) { + vimvars[VV_KEY].vv_type = VAR_NUMBER; + + for (int i = 0; i < b->bv_ga.ga_len; i++) { + typval_T tv; + tv.v_type = VAR_NUMBER; + const varnumber_T val = tv_blob_get(b, i); + tv.vval.v_number = val; + vimvars[VV_KEY].vv_nr = idx; + if (filter_map_one(&tv, expr, map, &rem) == FAIL || did_emsg) { + break; + } + if (tv.v_type != VAR_NUMBER) { + EMSG(_(e_invalblob)); + return; + } + if (map) { + if (tv.vval.v_number != val) { + tv_blob_set(b, i, tv.vval.v_number); + } + } else if (rem) { + char_u *const p = (char_u *)argvars[0].vval.v_blob->bv_ga.ga_data; + memmove(p + i, p + i + 1, (size_t)b->bv_ga.ga_len - i - 1); + b->bv_ga.ga_len--; + i--; + } + idx++; + } } else { assert(argvars[0].v_type == VAR_LIST); vimvars[VV_KEY].vv_type = VAR_NUMBER; @@ -7728,10 +7970,61 @@ bool write_list(FileDescriptor *const fp, const list_T *const list, } return true; write_list_error: - emsgf(_("E80: Error while writing: %s"), os_strerror(error)); + emsgf(_(e_write2), os_strerror(error)); + return false; +} + +/// Write a blob to file with descriptor `fp`. +/// +/// @param[in] fp File to write to. +/// @param[in] blob Blob to write. +/// +/// @return true on success, or false on failure. +bool write_blob(FileDescriptor *const fp, const blob_T *const blob) + FUNC_ATTR_NONNULL_ARG(1) +{ + int error = 0; + const int len = tv_blob_len(blob); + if (len > 0) { + const ptrdiff_t written = file_write(fp, blob->bv_ga.ga_data, (size_t)len); + if (written < (ptrdiff_t)len) { + error = (int)written; + goto write_blob_error; + } + } + error = file_flush(fp); + if (error != 0) { + goto write_blob_error; + } + return true; +write_blob_error: + EMSG2(_(e_write2), os_strerror(error)); return false; } +/// Read a blob from a file `fd`. +/// +/// @param[in] fd File to read from. +/// @param[in,out] blob Blob to write to. +/// +/// @return true on success, or false on failure. +bool read_blob(FILE *const fd, blob_T *const blob) + FUNC_ATTR_NONNULL_ALL +{ + FileInfo file_info; + if (!os_fileinfo_fd(fileno(fd), &file_info)) { + return false; + } + const int size = (int)os_fileinfo_size(&file_info); + ga_grow(&blob->bv_ga, size); + blob->bv_ga.ga_len = size; + if (fread(blob->bv_ga.ga_data, 1, blob->bv_ga.ga_len, fd) + < (size_t)blob->bv_ga.ga_len) { + return false; + } + return true; +} + /// Saves a typval_T as a string. /// /// For lists or buffers, replaces NLs with NUL and separates items with NLs. @@ -9248,7 +9541,10 @@ static void set_var_const(const char *name, const size_t name_len, } if (is_const) { - tv_item_lock(&v->di_tv, 1, true); + // Like :lockvar! name: lock the value and what it contains, but only + // if the reference count is up to one. That locks only literal + // values. + tv_item_lock(&v->di_tv, DICT_MAXNEST, true, true); } } @@ -9456,6 +9752,9 @@ int var_item_copy(const vimconv_T *const conv, ret = FAIL; } break; + case VAR_BLOB: + tv_blob_copy(from, to); + break; case VAR_DICT: to->v_type = VAR_DICT; to->v_lock = VAR_UNLOCKED; @@ -10815,6 +11114,29 @@ int typval_compare( // For "is" a different type always means false, for "notis" // it means true. n1 = type == EXPR_ISNOT; + } else if (typ1->v_type == VAR_BLOB || typ2->v_type == VAR_BLOB) { + if (type_is) { + n1 = typ1->v_type == typ2->v_type + && typ1->vval.v_blob == typ2->vval.v_blob; + if (type == EXPR_ISNOT) { + n1 = !n1; + } + } else if (typ1->v_type != typ2->v_type + || (type != EXPR_EQUAL && type != EXPR_NEQUAL)) { + if (typ1->v_type != typ2->v_type) { + EMSG(_("E977: Can only compare Blob with Blob")); + } else { + EMSG(_(e_invalblob)); + } + tv_clear(typ1); + return FAIL; + } else { + // Compare two Blobs for being equal or unequal. + n1 = tv_blob_equal(typ1->vval.v_blob, typ2->vval.v_blob); + if (type == EXPR_NEQUAL) { + n1 = !n1; + } + } } else if (typ1->v_type == VAR_LIST || typ2->v_type == VAR_LIST) { if (type_is) { n1 = typ1->v_type == typ2->v_type diff --git a/src/nvim/eval.h b/src/nvim/eval.h index 41120b3c78..2452a0a8c8 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -63,6 +63,7 @@ typedef struct lval_S { dict_T *ll_dict; ///< The Dictionary or NULL. dictitem_T *ll_di; ///< The dictitem or NULL. char_u *ll_newkey; ///< New key for Dict in allocated memory or NULL. + blob_T *ll_blob; ///< The Blob or NULL. } lval_T; /// enum used by var_flavour() @@ -154,6 +155,7 @@ typedef enum { VV_TYPE_DICT, VV_TYPE_FLOAT, VV_TYPE_BOOL, + VV_TYPE_BLOB, VV_EVENT, VV_ECHOSPACE, VV_ARGV, @@ -165,6 +167,7 @@ typedef enum { VV__NULL_STRING, // String with NULL value. For test purposes only. VV__NULL_LIST, // List with NULL value. For test purposes only. VV__NULL_DICT, // Dictionary with NULL value. For test purposes only. + VV__NULL_BLOB, // Blob with NULL value. For test purposes only. VV_LUA, } VimVarIndex; diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index a7242ba73a..5f355abe3c 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -250,7 +250,7 @@ return { min={args=1, base=1}, mkdir={args={1, 3}}, mode={args={0, 1}}, - msgpackdump={args=1}, + msgpackdump={args={1, 2}}, msgpackparse={args=1}, nextnonblank={args=1}, nr2char={args={1, 2}}, diff --git a/src/nvim/eval/decode.c b/src/nvim/eval/decode.c index 89e1f04bfd..5c03f55621 100644 --- a/src/nvim/eval/decode.c +++ b/src/nvim/eval/decode.c @@ -246,14 +246,15 @@ list_T *decode_create_map_special_dict(typval_T *const ret_tv, /// Convert char* string to typval_T /// /// Depending on whether string has (no) NUL bytes, it may use a special -/// dictionary or decode string to VAR_STRING. +/// dictionary, VAR_BLOB, or decode string to VAR_STRING. /// /// @param[in] s String to decode. /// @param[in] len String length. /// @param[in] hasnul Whether string has NUL byte, not or it was not yet /// determined. -/// @param[in] binary If true, save special string type as kMPBinary, -/// otherwise kMPString. +/// @param[in] binary Determines decode type if string has NUL bytes. +/// If true convert string to VAR_BLOB, otherwise to the +/// kMPString special type. /// @param[in] s_allocated If true, then `s` was allocated and can be saved in /// a returned structure. If it is not saved there, it /// will be freed. @@ -269,21 +270,28 @@ typval_T decode_string(const char *const s, const size_t len, ? ((s != NULL) && (memchr(s, NUL, len) != NULL)) : (bool)hasnul); if (really_hasnul) { - list_T *const list = tv_list_alloc(kListLenMayKnow); - tv_list_ref(list); typval_T tv; - create_special_dict(&tv, binary ? kMPBinary : kMPString, ((typval_T) { - .v_type = VAR_LIST, - .v_lock = VAR_UNLOCKED, - .vval = { .v_list = list }, - })); - const int elw_ret = encode_list_write((void *)list, s, len); - if (s_allocated) { - xfree((void *)s); - } - if (elw_ret == -1) { - tv_clear(&tv); - return (typval_T) { .v_type = VAR_UNKNOWN, .v_lock = VAR_UNLOCKED }; + tv.v_lock = VAR_UNLOCKED; + if (binary) { + tv_blob_alloc_ret(&tv); + ga_concat_len(&tv.vval.v_blob->bv_ga, s, len); + } else { + list_T *const list = tv_list_alloc(kListLenMayKnow); + tv_list_ref(list); + create_special_dict(&tv, kMPString, + ((typval_T){ + .v_type = VAR_LIST, + .v_lock = VAR_UNLOCKED, + .vval = { .v_list = list }, + })); + const int elw_ret = encode_list_write((void *)list, s, len); + if (s_allocated) { + xfree((void *)s); + } + if (elw_ret == -1) { + tv_clear(&tv); + return (typval_T) { .v_type = VAR_UNKNOWN, .v_lock = VAR_UNLOCKED }; + } } return tv; } else { diff --git a/src/nvim/eval/encode.c b/src/nvim/eval/encode.c index a4d7af7971..b5e50e7ef5 100644 --- a/src/nvim/eval/encode.c +++ b/src/nvim/eval/encode.c @@ -47,6 +47,14 @@ const char *const encode_special_var_names[] = { # include "eval/encode.c.generated.h" #endif +/// Msgpack callback for writing to a Blob +int encode_blob_write(void *const data, const char *const buf, const size_t len) + FUNC_ATTR_NONNULL_ARG(1) +{ + ga_concat_len(&((blob_T *)data)->bv_ga, buf, len); + return (int)len; +} + /// Msgpack callback for writing to readfile()-style list int encode_list_write(void *const data, const char *const buf, const size_t len) FUNC_ATTR_NONNULL_ARG(1) @@ -319,6 +327,30 @@ int encode_read_from_list(ListReaderState *const state, char *const buf, #define TYPVAL_ENCODE_CONV_EXT_STRING(tv, buf, len, type) +#define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \ + do { \ + const blob_T *const blob_ = (blob); \ + const int len_ = (len); \ + if (len_ == 0) { \ + ga_concat(gap, "0z"); \ + } else { \ + /* Allocate space for "0z", the two hex chars per byte, and a */ \ + /* "." separator after every eight hex chars. */ \ + /* Example: "0z00112233.44556677.8899" */ \ + ga_grow(gap, 2 + 2 * len_ + (len_ - 1) / 4); \ + ga_concat(gap, "0z"); \ + char numbuf[NUMBUFLEN]; \ + for (int i_ = 0; i_ < len_; i_++) { \ + if (i_ > 0 && (i_ & 3) == 0) { \ + ga_append(gap, '.'); \ + } \ + vim_snprintf((char *)numbuf, ARRAY_SIZE(numbuf), "%02X", \ + (int)tv_blob_get(blob_, i_)); \ + ga_concat(gap, numbuf); \ + } \ + } \ + } while (0) + #define TYPVAL_ENCODE_CONV_NUMBER(tv, num) \ do { \ char numbuf[NUMBUFLEN]; \ @@ -705,6 +737,28 @@ static inline int convert_to_json_string(garray_T *const gap, return FAIL; \ } while (0) +#undef TYPVAL_ENCODE_CONV_BLOB +#define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \ + do { \ + const blob_T *const blob_ = (blob); \ + const int len_ = (len); \ + if (len_ == 0) { \ + ga_concat(gap, "[]"); \ + } else { \ + ga_append(gap, '['); \ + char numbuf[NUMBUFLEN]; \ + for (int i_ = 0; i_ < len_; i_++) { \ + if (i_ > 0) { \ + ga_concat(gap, ", "); \ + } \ + vim_snprintf((char *)numbuf, ARRAY_SIZE(numbuf), "%d", \ + (int)tv_blob_get(blob_, i_)); \ + ga_concat(gap, numbuf); \ + } \ + ga_append(gap, ']'); \ + } \ + } while (0) + #undef TYPVAL_ENCODE_CONV_FUNC_START #define TYPVAL_ENCODE_CONV_FUNC_START(tv, fun) \ return conv_error(_("E474: Error while dumping %s, %s: " \ @@ -770,6 +824,7 @@ bool encode_check_json_key(const typval_T *const tv) #undef TYPVAL_ENCODE_CONV_STRING #undef TYPVAL_ENCODE_CONV_STR_STRING #undef TYPVAL_ENCODE_CONV_EXT_STRING +#undef TYPVAL_ENCODE_CONV_BLOB #undef TYPVAL_ENCODE_CONV_NUMBER #undef TYPVAL_ENCODE_CONV_FLOAT #undef TYPVAL_ENCODE_CONV_FUNC_START @@ -904,6 +959,15 @@ char *encode_tv2json(typval_T *tv, size_t *len) } \ } while (0) +#define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \ + do { \ + const size_t len_ = (size_t)(len); \ + msgpack_pack_bin(packer, len_); \ + if (len_ > 0) { \ + msgpack_pack_bin_body(packer, (blob)->bv_ga.ga_data, len_); \ + } \ + } while (0) + #define TYPVAL_ENCODE_CONV_NUMBER(tv, num) \ msgpack_pack_int64(packer, (int64_t)(num)) @@ -982,6 +1046,7 @@ char *encode_tv2json(typval_T *tv, size_t *len) #undef TYPVAL_ENCODE_CONV_STRING #undef TYPVAL_ENCODE_CONV_STR_STRING #undef TYPVAL_ENCODE_CONV_EXT_STRING +#undef TYPVAL_ENCODE_CONV_BLOB #undef TYPVAL_ENCODE_CONV_NUMBER #undef TYPVAL_ENCODE_CONV_FLOAT #undef TYPVAL_ENCODE_CONV_FUNC_START diff --git a/src/nvim/eval/executor.c b/src/nvim/eval/executor.c index 8ac2c3b8eb..5ced2a0535 100644 --- a/src/nvim/eval/executor.c +++ b/src/nvim/eval/executor.c @@ -38,6 +38,20 @@ int eexe_mod_op(typval_T *const tv1, const typval_T *const tv2, case VAR_SPECIAL: { break; } + case VAR_BLOB: { + if (*op != '+' || tv2->v_type != VAR_BLOB) { + break; + } + // Blob += Blob + if (tv1->vval.v_blob != NULL && tv2->vval.v_blob != NULL) { + blob_T *const b1 = tv1->vval.v_blob; + blob_T *const b2 = tv2->vval.v_blob; + for (int i = 0; i < tv_blob_len(b2); i++) { + ga_append(&b1->bv_ga, (char)tv_blob_get(b2, i)); + } + } + return OK; + } case VAR_LIST: { if (*op != '+' || tv2->v_type != VAR_LIST) { break; diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 801b0f9d1c..bcbd2266d3 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -96,6 +96,7 @@ PRAGMA_DIAG_POP static char *e_listarg = N_("E686: Argument of %s must be a List"); +static char *e_listblobarg = N_("E899: Argument of %s must be a List or Blob"); static char *e_invalwindow = N_("E957: Invalid window number"); /// Dummy va_list for passing to vim_snprintf @@ -321,8 +322,20 @@ static void f_add(typval_T *argvars, typval_T *rettv, FunPtr fptr) tv_list_append_tv(l, &argvars[1]); tv_copy(&argvars[0], rettv); } + } else if (argvars[0].v_type == VAR_BLOB) { + blob_T *const b = argvars[0].vval.v_blob; + if (b != NULL + && !var_check_lock(b->bv_lock, N_("add() argument"), TV_TRANSLATE)) { + bool error = false; + const varnumber_T n = tv_get_number_chk(&argvars[1], &error); + + if (!error) { + ga_append(&b->bv_ga, (int)n); + tv_copy(&argvars[0], rettv); + } + } } else { - EMSG(_(e_listreq)); + EMSG(_(e_listblobreq)); } } @@ -959,7 +972,17 @@ static void f_chansend(typval_T *argvars, typval_T *rettv, FunPtr fptr) } ptrdiff_t input_len = 0; - char *input = save_tv_as_string(&argvars[1], &input_len, false); + char *input = NULL; + if (argvars[1].v_type == VAR_BLOB) { + const blob_T *const b = argvars[1].vval.v_blob; + input_len = tv_blob_len(b); + if (input_len > 0) { + input = xmemdup(b->bv_ga.ga_data, input_len); + } + } else { + input = save_tv_as_string(&argvars[1], &input_len, false); + } + if (!input) { // Either the error has been handled by save_tv_as_string(), // or there is no input to send. @@ -1874,6 +1897,10 @@ static void f_empty(typval_T *argvars, typval_T *rettv, FunPtr fptr) n = argvars[0].vval.v_special == kSpecialVarNull; break; } + case VAR_BLOB: { + n = (tv_blob_len(argvars[0].vval.v_blob) == 0); + break; + } case VAR_UNKNOWN: { internal_error("f_empty(UNKNOWN)"); break; @@ -2791,7 +2818,23 @@ static void f_get(typval_T *argvars, typval_T *rettv, FunPtr fptr) typval_T *tv = NULL; bool what_is_dict = false; - if (argvars[0].v_type == VAR_LIST) { + if (argvars[0].v_type == VAR_BLOB) { + bool error = false; + int idx = tv_get_number_chk(&argvars[1], &error); + + if (!error) { + rettv->v_type = VAR_NUMBER; + if (idx < 0) { + idx = tv_blob_len(argvars[0].vval.v_blob) + idx; + } + if (idx < 0 || idx >= tv_blob_len(argvars[0].vval.v_blob)) { + rettv->vval.v_number = -1; + } else { + rettv->vval.v_number = tv_blob_get(argvars[0].vval.v_blob, idx); + tv = rettv; + } + } + } else if (argvars[0].v_type == VAR_LIST) { if ((l = argvars[0].vval.v_list) != NULL) { bool error = false; @@ -2852,7 +2895,7 @@ static void f_get(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } } else { - EMSG2(_(e_listdictarg), "get()"); + EMSG2(_(e_listdictblobarg), "get()"); } if (tv == NULL) { @@ -4403,6 +4446,7 @@ static void f_has(typval_T *argvars, typval_T *rettv, FunPtr fptr) "user_commands", "vartabs", "vertsplit", + "vimscript-1", "virtualedit", "visual", "visualextra", @@ -4790,8 +4834,38 @@ static void f_index(typval_T *argvars, typval_T *rettv, FunPtr fptr) bool ic = false; rettv->vval.v_number = -1; - if (argvars[0].v_type != VAR_LIST) { - EMSG(_(e_listreq)); + if (argvars[0].v_type == VAR_BLOB) { + bool error = false; + int start = 0; + + if (argvars[2].v_type != VAR_UNKNOWN) { + start = tv_get_number_chk(&argvars[2], &error); + if (error) { + return; + } + } + blob_T *const b = argvars[0].vval.v_blob; + if (b == NULL) { + return; + } + if (start < 0) { + start = tv_blob_len(b) + start; + if (start < 0) { + start = 0; + } + } + for (idx = start; idx < tv_blob_len(b); idx++) { + typval_T tv; + tv.v_type = VAR_NUMBER; + tv.vval.v_number = tv_blob_get(b, idx); + if (tv_equal(&tv, &argvars[1], ic, false)) { + rettv->vval.v_number = idx; + return; + } + } + return; + } else if (argvars[0].v_type != VAR_LIST) { + EMSG(_(e_listblobreq)); return; } list_T *const l = argvars[0].vval.v_list; @@ -4920,8 +4994,46 @@ static void f_insert(typval_T *argvars, typval_T *rettv, FunPtr fptr) list_T *l; bool error = false; - if (argvars[0].v_type != VAR_LIST) { - EMSG2(_(e_listarg), "insert()"); + if (argvars[0].v_type == VAR_BLOB) { + blob_T *const b = argvars[0].vval.v_blob; + + if (b == NULL + || var_check_lock(b->bv_lock, N_("insert() argument"), + TV_TRANSLATE)) { + return; + } + + long before = 0; + const int len = tv_blob_len(b); + + if (argvars[2].v_type != VAR_UNKNOWN) { + before = (long)tv_get_number_chk(&argvars[2], &error); + if (error) { + return; // type error; errmsg already given + } + if (before < 0 || before > len) { + EMSG2(_(e_invarg2), tv_get_string(&argvars[2])); + return; + } + } + const int val = tv_get_number_chk(&argvars[1], &error); + if (error) { + return; + } + if (val < 0 || val > 255) { + EMSG2(_(e_invarg2), tv_get_string(&argvars[1])); + return; + } + + ga_grow(&b->bv_ga, 1); + char_u *const p = (char_u *)b->bv_ga.ga_data; + memmove(p + before + 1, p + before, (size_t)len - before); + *(p + before) = val; + b->bv_ga.ga_len++; + + tv_copy(&argvars[0], rettv); + } else if (argvars[0].v_type != VAR_LIST) { + EMSG2(_(e_listblobarg), "insert()"); } else if (!var_check_lock(tv_list_locked((l = argvars[0].vval.v_list)), N_("insert() argument"), TV_TRANSLATE)) { long before = 0; @@ -5581,6 +5693,10 @@ static void f_len(typval_T *argvars, typval_T *rettv, FunPtr fptr) tv_get_string(&argvars[0])); break; } + case VAR_BLOB: { + rettv->vval.v_number = tv_blob_len(argvars[0].vval.v_blob); + break; + } case VAR_LIST: { rettv->vval.v_number = tv_list_len(argvars[0].vval.v_list); break; @@ -6391,9 +6507,16 @@ static void f_msgpackdump(typval_T *argvars, typval_T *rettv, FunPtr fptr) EMSG2(_(e_listarg), "msgpackdump()"); return; } - list_T *const ret_list = tv_list_alloc_ret(rettv, kListLenMayKnow); list_T *const list = argvars[0].vval.v_list; - msgpack_packer *lpacker = msgpack_packer_new(ret_list, &encode_list_write); + msgpack_packer *packer; + if (argvars[1].v_type != VAR_UNKNOWN + && strequal(tv_get_string(&argvars[1]), "B")) { + tv_blob_alloc_ret(rettv); + packer = msgpack_packer_new(rettv->vval.v_blob, &encode_blob_write); + } else { + packer = msgpack_packer_new(tv_list_alloc_ret(rettv, kListLenMayKnow), + &encode_list_write); + } const char *const msg = _("msgpackdump() argument, index %i"); // Assume that translation will not take more then 4 times more space char msgbuf[sizeof("msgpackdump() argument, index ") * 4 + NUMBUFLEN]; @@ -6401,23 +6524,50 @@ static void f_msgpackdump(typval_T *argvars, typval_T *rettv, FunPtr fptr) TV_LIST_ITER(list, li, { vim_snprintf(msgbuf, sizeof(msgbuf), (char *)msg, idx); idx++; - if (encode_vim_to_msgpack(lpacker, TV_LIST_ITEM_TV(li), msgbuf) == FAIL) { + if (encode_vim_to_msgpack(packer, TV_LIST_ITEM_TV(li), msgbuf) == FAIL) { break; } }); - msgpack_packer_free(lpacker); + msgpack_packer_free(packer); } -/// "msgpackparse" function -static void f_msgpackparse(typval_T *argvars, typval_T *rettv, FunPtr fptr) +static int msgpackparse_convert_item(const msgpack_object data, + const msgpack_unpack_return result, + list_T *const ret_list, + const bool fail_if_incomplete) FUNC_ATTR_NONNULL_ALL { - if (argvars[0].v_type != VAR_LIST) { - EMSG2(_(e_listarg), "msgpackparse()"); - return; + switch (result) { + case MSGPACK_UNPACK_PARSE_ERROR: + EMSG2(_(e_invarg2), "Failed to parse msgpack string"); + return FAIL; + case MSGPACK_UNPACK_NOMEM_ERROR: + EMSG(_(e_outofmem)); + return FAIL; + case MSGPACK_UNPACK_CONTINUE: + if (fail_if_incomplete) { + EMSG2(_(e_invarg2), "Incomplete msgpack string"); + return FAIL; + } + return NOTDONE; + case MSGPACK_UNPACK_SUCCESS: { + typval_T tv = { .v_type = VAR_UNKNOWN }; + if (msgpack_to_vim(data, &tv) == FAIL) { + EMSG2(_(e_invarg2), "Failed to convert msgpack string"); + return FAIL; + } + tv_list_append_owned_tv(ret_list, tv); + return OK; + } + default: + abort(); } - list_T *const ret_list = tv_list_alloc_ret(rettv, kListLenMayKnow); - const list_T *const list = argvars[0].vval.v_list; +} + +static void msgpackparse_unpack_list(const list_T *const list, + list_T *const ret_list) + FUNC_ATTR_NONNULL_ARG(2) +{ if (tv_list_len(list) == 0) { return; } @@ -6436,43 +6586,28 @@ static void f_msgpackparse(typval_T *argvars, typval_T *rettv, FunPtr fptr) do { if (!msgpack_unpacker_reserve_buffer(unpacker, IOSIZE)) { EMSG(_(e_outofmem)); - goto f_msgpackparse_exit; + goto end; } size_t read_bytes; const int rlret = encode_read_from_list( &lrstate, msgpack_unpacker_buffer(unpacker), IOSIZE, &read_bytes); if (rlret == FAIL) { EMSG2(_(e_invarg2), "List item is not a string"); - goto f_msgpackparse_exit; + goto end; } msgpack_unpacker_buffer_consumed(unpacker, read_bytes); if (read_bytes == 0) { break; } while (unpacker->off < unpacker->used) { - const msgpack_unpack_return result = msgpack_unpacker_next(unpacker, - &unpacked); - if (result == MSGPACK_UNPACK_PARSE_ERROR) { - EMSG2(_(e_invarg2), "Failed to parse msgpack string"); - goto f_msgpackparse_exit; - } - if (result == MSGPACK_UNPACK_NOMEM_ERROR) { - EMSG(_(e_outofmem)); - goto f_msgpackparse_exit; - } - if (result == MSGPACK_UNPACK_SUCCESS) { - typval_T tv = { .v_type = VAR_UNKNOWN }; - if (msgpack_to_vim(unpacked.data, &tv) == FAIL) { - EMSG2(_(e_invarg2), "Failed to convert msgpack string"); - goto f_msgpackparse_exit; - } - tv_list_append_owned_tv(ret_list, tv); - } - if (result == MSGPACK_UNPACK_CONTINUE) { - if (rlret == OK) { - EMSG2(_(e_invarg2), "Incomplete msgpack string"); - } + const msgpack_unpack_return result + = msgpack_unpacker_next(unpacker, &unpacked); + const int conv_result = msgpackparse_convert_item(unpacked.data, result, + ret_list, rlret == OK); + if (conv_result == NOTDONE) { break; + } else if (conv_result == FAIL) { + goto end; } } if (rlret == OK) { @@ -6480,10 +6615,47 @@ static void f_msgpackparse(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } while (true); -f_msgpackparse_exit: - msgpack_unpacked_destroy(&unpacked); +end: msgpack_unpacker_free(unpacker); - return; + msgpack_unpacked_destroy(&unpacked); +} + +static void msgpackparse_unpack_blob(const blob_T *const blob, + list_T *const ret_list) + FUNC_ATTR_NONNULL_ARG(2) +{ + const int len = tv_blob_len(blob); + if (len == 0) { + return; + } + msgpack_unpacked unpacked; + msgpack_unpacked_init(&unpacked); + for (size_t offset = 0; offset < (size_t)len;) { + const msgpack_unpack_return result + = msgpack_unpack_next(&unpacked, blob->bv_ga.ga_data, len, &offset); + if (msgpackparse_convert_item(unpacked.data, result, ret_list, true) + != OK) { + break; + } + } + + msgpack_unpacked_destroy(&unpacked); +} + +/// "msgpackparse" function +static void f_msgpackparse(typval_T *argvars, typval_T *rettv, FunPtr fptr) + FUNC_ATTR_NONNULL_ALL +{ + if (argvars[0].v_type != VAR_LIST && argvars[0].v_type != VAR_BLOB) { + EMSG2(_(e_listblobarg), "msgpackparse()"); + return; + } + list_T *const ret_list = tv_list_alloc_ret(rettv, kListLenMayKnow); + if (argvars[0].v_type == VAR_LIST) { + msgpackparse_unpack_list(argvars[0].vval.v_list, ret_list); + } else { + msgpackparse_unpack_blob(argvars[0].vval.v_blob, ret_list); + } } /* @@ -6894,6 +7066,7 @@ static void f_readdir(typval_T *argvars, typval_T *rettv, FunPtr fptr) static void f_readfile(typval_T *argvars, typval_T *rettv, FunPtr fptr) { bool binary = false; + bool blob = false; FILE *fd; char_u buf[(IOSIZE/256) * 256]; // rounded to avoid odd + 1 int io_size = sizeof(buf); @@ -6906,22 +7079,41 @@ static void f_readfile(typval_T *argvars, typval_T *rettv, FunPtr fptr) if (argvars[1].v_type != VAR_UNKNOWN) { if (strcmp(tv_get_string(&argvars[1]), "b") == 0) { binary = true; + } else if (strcmp(tv_get_string(&argvars[1]), "B") == 0) { + blob = true; } if (argvars[2].v_type != VAR_UNKNOWN) { maxline = tv_get_number(&argvars[2]); } } - list_T *const l = tv_list_alloc_ret(rettv, kListLenUnknown); - // Always open the file in binary mode, library functions have a mind of // their own about CR-LF conversion. const char *const fname = tv_get_string(&argvars[0]); + + if (os_isdir((const char_u *)fname)) { + EMSG2(_(e_isadir2), fname); + return; + } if (*fname == NUL || (fd = os_fopen(fname, READBIN)) == NULL) { EMSG2(_(e_notopen), *fname == NUL ? _("<empty>") : fname); return; } + if (blob) { + tv_blob_alloc_ret(rettv); + if (!read_blob(fd, rettv->vval.v_blob)) { + EMSG2(_(e_notread), fname); + // An empty blob is returned on error. + tv_blob_free(rettv->vval.v_blob); + rettv->vval.v_blob = NULL; + } + fclose(fd); + return; + } + + list_T *const l = tv_list_alloc_ret(rettv, kListLenUnknown); + while (maxline < 0 || tv_list_len(l) < maxline) { readlen = (int)fread(buf, 1, io_size, fd); @@ -7190,8 +7382,64 @@ static void f_remove(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } } + } else if (argvars[0].v_type == VAR_BLOB) { + blob_T *const b = argvars[0].vval.v_blob; + + if (b != NULL && var_check_lock(b->bv_lock, arg_errmsg, TV_TRANSLATE)) { + return; + } + + bool error = false; + idx = (long)tv_get_number_chk(&argvars[1], &error); + + if (!error) { + const int len = tv_blob_len(b); + + if (idx < 0) { + // count from the end + idx = len + idx; + } + if (idx < 0 || idx >= len) { + EMSGN(_(e_blobidx), idx); + return; + } + if (argvars[2].v_type == VAR_UNKNOWN) { + // Remove one item, return its value. + char_u *const p = (char_u *)b->bv_ga.ga_data; + rettv->vval.v_number = (varnumber_T)(*(p + idx)); + memmove(p + idx, p + idx + 1, (size_t)len - idx - 1); + b->bv_ga.ga_len--; + } else { + // Remove range of items, return blob with values. + end = (long)tv_get_number_chk(&argvars[2], &error); + if (error) { + return; + } + if (end < 0) { + // count from the end + end = len + end; + } + if (end >= len || idx > end) { + EMSGN(_(e_blobidx), end); + return; + } + blob_T *const blob = tv_blob_alloc(); + blob->bv_ga.ga_len = end - idx + 1; + ga_grow(&blob->bv_ga, end - idx + 1); + + char_u *const p = (char_u *)b->bv_ga.ga_data; + memmove((char_u *)blob->bv_ga.ga_data, p + idx, + (size_t)(end - idx + 1)); + tv_blob_set_ret(rettv, blob); + + if (len - end - 1 > 0) { + memmove(p + idx, p + end + 1, (size_t)(len - end - 1)); + } + b->bv_ga.ga_len -= end - idx + 1; + } + } } else if (argvars[0].v_type != VAR_LIST) { - EMSG2(_(e_listdictarg), "remove()"); + EMSG2(_(e_listdictblobarg), "remove()"); } else if (!var_check_lock(tv_list_locked((l = argvars[0].vval.v_list)), arg_errmsg, TV_TRANSLATE)) { bool error = false; @@ -7465,13 +7713,25 @@ static void f_resolve(typval_T *argvars, typval_T *rettv, FunPtr fptr) */ static void f_reverse(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - list_T *l; - if (argvars[0].v_type != VAR_LIST) { - EMSG2(_(e_listarg), "reverse()"); - } else if (!var_check_lock(tv_list_locked((l = argvars[0].vval.v_list)), - N_("reverse() argument"), TV_TRANSLATE)) { - tv_list_reverse(l); - tv_list_set_ret(rettv, l); + if (argvars[0].v_type == VAR_BLOB) { + blob_T *const b = argvars[0].vval.v_blob; + const int len = tv_blob_len(b); + + for (int i = 0; i < len / 2; i++) { + const char_u tmp = tv_blob_get(b, i); + tv_blob_set(b, i, tv_blob_get(b, len - i - 1)); + tv_blob_set(b, len - i - 1, tmp); + } + tv_blob_set_ret(rettv, b); + } else if (argvars[0].v_type != VAR_LIST) { + EMSG2(_(e_listblobarg), "reverse()"); + } else { + list_T *const l = argvars[0].vval.v_list; + if (!var_check_lock(tv_list_locked(l), N_("reverse() argument"), + TV_TRANSLATE)) { + tv_list_reverse(l); + tv_list_set_ret(rettv, l); + } } } @@ -11291,15 +11551,16 @@ static void f_type(typval_T *argvars, typval_T *rettv, FunPtr fptr) int n = -1; switch (argvars[0].v_type) { - case VAR_NUMBER: n = VAR_TYPE_NUMBER; break; - case VAR_STRING: n = VAR_TYPE_STRING; break; + case VAR_NUMBER: n = VAR_TYPE_NUMBER; break; + case VAR_STRING: n = VAR_TYPE_STRING; break; case VAR_PARTIAL: - case VAR_FUNC: n = VAR_TYPE_FUNC; break; - case VAR_LIST: n = VAR_TYPE_LIST; break; - case VAR_DICT: n = VAR_TYPE_DICT; break; - case VAR_FLOAT: n = VAR_TYPE_FLOAT; break; - case VAR_BOOL: n = VAR_TYPE_BOOL; break; - case VAR_SPECIAL:n = VAR_TYPE_SPECIAL; break; + case VAR_FUNC: n = VAR_TYPE_FUNC; break; + case VAR_LIST: n = VAR_TYPE_LIST; break; + case VAR_DICT: n = VAR_TYPE_DICT; break; + case VAR_FLOAT: n = VAR_TYPE_FLOAT; break; + case VAR_BOOL: n = VAR_TYPE_BOOL; break; + case VAR_SPECIAL: n = VAR_TYPE_SPECIAL; break; + case VAR_BLOB: n = VAR_TYPE_BLOB; break; case VAR_UNKNOWN: { internal_error("f_type(UNKNOWN)"); break; @@ -11678,16 +11939,17 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, FunPtr fptr) return; } - if (argvars[0].v_type != VAR_LIST) { - EMSG2(_(e_listarg), "writefile()"); + if (argvars[0].v_type == VAR_LIST) { + TV_LIST_ITER_CONST(argvars[0].vval.v_list, li, { + if (!tv_check_str_or_nr(TV_LIST_ITEM_TV(li))) { + return; + } + }); + } else if (argvars[0].v_type != VAR_BLOB) { + EMSG2(_(e_invarg2), + _("writefile() first argument must be a List or a Blob")); return; } - const list_T *const list = argvars[0].vval.v_list; - TV_LIST_ITER_CONST(list, li, { - if (!tv_check_str_or_nr(TV_LIST_ITEM_TV(li))) { - return; - } - }); bool binary = false; bool append = false; @@ -11727,7 +11989,13 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, FunPtr fptr) emsgf(_("E482: Can't open file %s for writing: %s"), fname, os_strerror(error)); } else { - if (write_list(&fp, list, binary)) { + bool write_ok; + if (argvars[0].v_type == VAR_BLOB) { + write_ok = write_blob(&fp, argvars[0].vval.v_blob); + } else { + write_ok = write_list(&fp, argvars[0].vval.v_list, binary); + } + if (write_ok) { rettv->vval.v_number = 0; } if ((error = file_close(&fp, do_fsync)) != 0) { diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 22b3bf026b..381d70ea1b 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -2125,6 +2125,77 @@ void tv_dict_set_keys_readonly(dict_T *const dict) }); } +//{{{1 Blobs +//{{{2 Alloc/free + +/// Allocate an empty blob. +/// +/// Caller should take care of the reference count. +/// +/// @return [allocated] new blob. +blob_T *tv_blob_alloc(void) + FUNC_ATTR_NONNULL_RET +{ + blob_T *const blob = xcalloc(1, sizeof(blob_T)); + ga_init(&blob->bv_ga, 1, 100); + return blob; +} + +/// Free a blob. Ignores the reference count. +/// +/// @param[in,out] b Blob to free. +void tv_blob_free(blob_T *const b) + FUNC_ATTR_NONNULL_ALL +{ + ga_clear(&b->bv_ga); + xfree(b); +} + +/// Unreference a blob. +/// +/// Decrements the reference count and frees blob when it becomes zero. +/// +/// @param[in,out] b Blob to operate on. +void tv_blob_unref(blob_T *const b) +{ + if (b != NULL && --b->bv_refcount <= 0) { + tv_blob_free(b); + } +} + +//{{{2 Operations on the whole blob + +/// Check whether two blobs are equal. +/// +/// @param[in] b1 First blob. +/// @param[in] b2 Second blob. +/// +/// @return true if blobs are equal, false otherwise. +bool tv_blob_equal(const blob_T *const b1, const blob_T *const b2) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + const int len1 = tv_blob_len(b1); + const int len2 = tv_blob_len(b2); + + // empty and NULL are considered the same + if (len1 == 0 && len2 == 0) { + return true; + } + if (b1 == b2) { + return true; + } + if (len1 != len2) { + return false; + } + + for (int i = 0; i < b1->bv_ga.ga_len; i++) { + if (tv_blob_get(b1, i) != tv_blob_get(b2, i)) { + return false; + } + } + return true; +} + //{{{1 Generic typval operations //{{{2 Init/alloc/clear //{{{3 Alloc @@ -2169,6 +2240,44 @@ void tv_dict_alloc_ret(typval_T *const ret_tv) tv_dict_set_ret(ret_tv, d); } +/// Allocate an empty blob for a return value. +/// +/// Also sets reference count. +/// +/// @param[out] ret_tv Structure where blob is saved. +void tv_blob_alloc_ret(typval_T *const ret_tv) + FUNC_ATTR_NONNULL_ALL +{ + blob_T *const b = tv_blob_alloc(); + tv_blob_set_ret(ret_tv, b); +} + +/// Copy a blob typval to a different typval. +/// +/// @param[in] from Blob object to copy from. +/// @param[out] to Blob object to copy to. +void tv_blob_copy(typval_T *const from, typval_T *const to) + FUNC_ATTR_NONNULL_ALL +{ + assert(from->v_type == VAR_BLOB); + + to->v_type = VAR_BLOB; + to->v_lock = VAR_UNLOCKED; + if (from->vval.v_blob == NULL) { + to->vval.v_blob = NULL; + } else { + tv_blob_alloc_ret(to); + int len = from->vval.v_blob->bv_ga.ga_len; + + if (len > 0) { + to->vval.v_blob->bv_ga.ga_data + = xmemdup(from->vval.v_blob->bv_ga.ga_data, (size_t)len); + } + to->vval.v_blob->bv_ga.ga_len = len; + to->vval.v_blob->bv_ga.ga_maxlen = len; + } +} + //{{{3 Clear #define TYPVAL_ENCODE_ALLOW_SPECIALS false @@ -2210,6 +2319,13 @@ void tv_dict_alloc_ret(typval_T *const ret_tv) #define TYPVAL_ENCODE_CONV_EXT_STRING(tv, buf, len, type) +#define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \ + do { \ + tv_blob_unref(tv->vval.v_blob); \ + tv->vval.v_blob = NULL; \ + tv->v_lock = VAR_UNLOCKED; \ + } while (0) + static inline int _nothing_conv_func_start(typval_T *const tv, char_u *const fun) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ARG(1) @@ -2392,6 +2508,7 @@ static inline void _nothing_conv_dict_end(typval_T *const tv, #undef TYPVAL_ENCODE_CONV_STRING #undef TYPVAL_ENCODE_CONV_STR_STRING #undef TYPVAL_ENCODE_CONV_EXT_STRING +#undef TYPVAL_ENCODE_CONV_BLOB #undef TYPVAL_ENCODE_CONV_FUNC_START #undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS #undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_SELF @@ -2449,6 +2566,10 @@ void tv_free(typval_T *tv) xfree(tv->vval.v_string); break; } + case VAR_BLOB: { + tv_blob_unref(tv->vval.v_blob); + break; + } case VAR_LIST: { tv_list_unref(tv->vval.v_list); break; @@ -2509,6 +2630,12 @@ void tv_copy(const typval_T *const from, typval_T *const to) } break; } + case VAR_BLOB: { + if (from->vval.v_blob != NULL) { + to->vval.v_blob->bv_refcount++; + } + break; + } case VAR_LIST: { tv_list_ref(to->vval.v_list); break; @@ -2533,7 +2660,10 @@ void tv_copy(const typval_T *const from, typval_T *const to) /// @param[out] tv Item to (un)lock. /// @param[in] deep Levels to (un)lock, -1 to (un)lock everything. /// @param[in] lock True if it is needed to lock an item, false to unlock. -void tv_item_lock(typval_T *const tv, const int deep, const bool lock) +/// @param[in] check_refcount If true, do not lock a list or dict with a +/// reference count larger than 1. +void tv_item_lock(typval_T *const tv, const int deep, const bool lock, + const bool check_refcount) FUNC_ATTR_NONNULL_ALL { // TODO(ZyX-I): Make this not recursive @@ -2560,14 +2690,21 @@ void tv_item_lock(typval_T *const tv, const int deep, const bool lock) CHANGE_LOCK(lock, tv->v_lock); switch (tv->v_type) { + case VAR_BLOB: { + blob_T *const b = tv->vval.v_blob; + if (b != NULL && !(check_refcount && b->bv_refcount > 1)) { + CHANGE_LOCK(lock, b->bv_lock); + } + break; + } case VAR_LIST: { list_T *const l = tv->vval.v_list; - if (l != NULL) { + if (l != NULL && !(check_refcount && l->lv_refcount > 1)) { CHANGE_LOCK(lock, l->lv_lock); if (deep < 0 || deep > 1) { // Recursive: lock/unlock the items the List contains. TV_LIST_ITER(l, li, { - tv_item_lock(TV_LIST_ITEM_TV(li), deep - 1, lock); + tv_item_lock(TV_LIST_ITEM_TV(li), deep - 1, lock, check_refcount); }); } } @@ -2575,12 +2712,12 @@ void tv_item_lock(typval_T *const tv, const int deep, const bool lock) } case VAR_DICT: { dict_T *const d = tv->vval.v_dict; - if (d != NULL) { + if (d != NULL && !(check_refcount && d->dv_refcount > 1)) { CHANGE_LOCK(lock, d->dv_lock); if (deep < 0 || deep > 1) { // recursive: lock/unlock the items the List contains TV_DICT_ITER(d, di, { - tv_item_lock(&di->di_tv, deep - 1, lock); + tv_item_lock(&di->di_tv, deep - 1, lock, check_refcount); }); } } @@ -2646,10 +2783,11 @@ bool tv_check_lock(const typval_T *tv, const char *name, VarLockStatus lock = VAR_UNLOCKED; switch (tv->v_type) { - // case VAR_BLOB: - // if (tv->vval.v_blob != NULL) - // lock = tv->vval.v_blob->bv_lock; - // break; + case VAR_BLOB: + if (tv->vval.v_blob != NULL) { + lock = tv->vval.v_blob->bv_lock; + } + break; case VAR_LIST: if (tv->vval.v_list != NULL) { lock = tv->vval.v_list->lv_lock; @@ -2769,6 +2907,9 @@ bool tv_equal(typval_T *const tv1, typval_T *const tv2, const bool ic, recursive_cnt--; return r; } + case VAR_BLOB: { + return tv_blob_equal(tv1->vval.v_blob, tv2->vval.v_blob); + } case VAR_NUMBER: { return tv1->vval.v_number == tv2->vval.v_number; } @@ -2835,6 +2976,10 @@ bool tv_check_str_or_nr(const typval_T *const tv) EMSG(_("E728: Expected a Number or a String, Dictionary found")); return false; } + case VAR_BLOB: { + EMSG(_("E974: Expected a Number or a String, Blob found")); + return false; + } case VAR_BOOL: { EMSG(_("E5299: Expected a Number or a String, Boolean found")); return false; @@ -2860,6 +3005,7 @@ static const char *const num_errors[] = { [VAR_LIST]=N_("E745: Using a List as a Number"), [VAR_DICT]=N_("E728: Using a Dictionary as a Number"), [VAR_FLOAT]=N_("E805: Using a Float as a Number"), + [VAR_BLOB]=N_("E974: Using a Blob as a Number"), [VAR_UNKNOWN]=N_("E685: using an invalid value as a Number"), }; @@ -2888,6 +3034,7 @@ bool tv_check_num(const typval_T *const tv) case VAR_LIST: case VAR_DICT: case VAR_FLOAT: + case VAR_BLOB: case VAR_UNKNOWN: { EMSG(_(num_errors[tv->v_type])); return false; @@ -2905,6 +3052,7 @@ static const char *const str_errors[] = { [VAR_LIST]=N_("E730: using List as a String"), [VAR_DICT]=N_("E731: using Dictionary as a String"), [VAR_FLOAT]=((const char *)e_float_as_string), + [VAR_BLOB]=N_("E976: using Blob as a String"), [VAR_UNKNOWN]=N_("E908: using an invalid value as a String"), }; @@ -2933,6 +3081,7 @@ bool tv_check_str(const typval_T *const tv) case VAR_LIST: case VAR_DICT: case VAR_FLOAT: + case VAR_BLOB: case VAR_UNKNOWN: { EMSG(_(str_errors[tv->v_type])); return false; @@ -2980,6 +3129,7 @@ varnumber_T tv_get_number_chk(const typval_T *const tv, bool *const ret_error) case VAR_PARTIAL: case VAR_LIST: case VAR_DICT: + case VAR_BLOB: case VAR_FLOAT: { EMSG(_(num_errors[tv->v_type])); break; @@ -3075,6 +3225,10 @@ float_T tv_get_float(const typval_T *const tv) EMSG(_("E907: Using a special value as a Float")); break; } + case VAR_BLOB: { + EMSG(_("E975: Using a Blob as a Float")); + break; + } case VAR_UNKNOWN: { emsgf(_(e_intern2), "tv_get_float(UNKNOWN)"); break; @@ -3134,6 +3288,7 @@ const char *tv_get_string_buf_chk(const typval_T *const tv, char *const buf) case VAR_LIST: case VAR_DICT: case VAR_FLOAT: + case VAR_BLOB: case VAR_UNKNOWN: { EMSG(_(str_errors[tv->v_type])); return false; diff --git a/src/nvim/eval/typval.h b/src/nvim/eval/typval.h index ef49fa1de6..5aecaccee9 100644 --- a/src/nvim/eval/typval.h +++ b/src/nvim/eval/typval.h @@ -64,6 +64,7 @@ enum ListLenSpecials { typedef struct listvar_S list_T; typedef struct dictvar_S dict_T; typedef struct partial_S partial_T; +typedef struct blobvar_S blob_T; typedef struct ufunc ufunc_T; @@ -123,6 +124,7 @@ typedef enum { VAR_SPECIAL, ///< Special value (null), .v_special ///< is used. VAR_PARTIAL, ///< Partial, .v_partial is used. + VAR_BLOB, ///< Blob, .v_blob is used. } VarType; /// Structure that holds an internal variable value @@ -138,6 +140,7 @@ typedef struct { list_T *v_list; ///< List for VAR_LIST, can be NULL. dict_T *v_dict; ///< Dictionary for VAR_DICT, can be NULL. partial_T *v_partial; ///< Closure: function with args. + blob_T *v_blob; ///< Blob for VAR_BLOB, can be NULL. } vval; ///< Actual value. } typval_T; @@ -252,6 +255,13 @@ struct dictvar_S { LuaRef lua_table_ref; }; +/// Structure to hold info about a Blob +struct blobvar_S { + garray_T bv_ga; ///< Growarray with the data. + int bv_refcount; ///< Reference count. + VarLockStatus bv_lock; ///< VAR_UNLOCKED, VAR_LOCKED, VAR_FIXED. +}; + /// Type used for script ID typedef int scid_T; /// Format argument for scid_T @@ -711,6 +721,65 @@ static inline bool tv_dict_is_watched(const dict_T *const d) return d && !QUEUE_EMPTY(&d->watchers); } +static inline void tv_blob_set_ret(typval_T *const tv, blob_T *const b) + REAL_FATTR_ALWAYS_INLINE REAL_FATTR_NONNULL_ARG(1); + +/// Set a blob as the return value. +/// +/// Increments the reference count. +/// +/// @param[out] tv Object to receive the blob. +/// @param[in,out] b Blob to pass to the object. +static inline void tv_blob_set_ret(typval_T *const tv, blob_T *const b) +{ + tv->v_type = VAR_BLOB; + tv->vval.v_blob = b; + if (b != NULL) { + b->bv_refcount++; + } +} + +static inline int tv_blob_len(const blob_T *const b) + REAL_FATTR_PURE REAL_FATTR_WARN_UNUSED_RESULT; + +/// Get the length of the data in the blob, in bytes. +/// +/// @param[in] b Blob to check. +static inline int tv_blob_len(const blob_T *const b) +{ + if (b == NULL) { + return 0; + } + return b->bv_ga.ga_len; +} + +static inline char_u tv_blob_get(const blob_T *const b, int idx) + REAL_FATTR_ALWAYS_INLINE REAL_FATTR_NONNULL_ALL REAL_FATTR_WARN_UNUSED_RESULT; + +/// Get the byte at index `idx` in the blob. +/// +/// @param[in] b Blob to index. Cannot be NULL. +/// @param[in] idx Index in a blob. Must be valid. +/// +/// @return Byte value at the given index. +static inline char_u tv_blob_get(const blob_T *const b, int idx) +{ + return ((char_u *)b->bv_ga.ga_data)[idx]; +} + +static inline void tv_blob_set(blob_T *const b, int idx, char_u c) + REAL_FATTR_ALWAYS_INLINE REAL_FATTR_NONNULL_ALL; + +/// Store the byte `c` at index `idx` in the blob. +/// +/// @param[in] b Blob to index. Cannot be NULL. +/// @param[in] idx Index in a blob. Must be valid. +/// @param[in] c Value to store. +static inline void tv_blob_set(blob_T *const b, int idx, char_u c) +{ + ((char_u *)b->bv_ga.ga_data)[idx] = c; +} + /// Initialize VimL object /// /// Initializes to unlocked VAR_UNKNOWN object. diff --git a/src/nvim/eval/typval_encode.c.h b/src/nvim/eval/typval_encode.c.h index 91c948ce7e..cd1be1eecc 100644 --- a/src/nvim/eval/typval_encode.c.h +++ b/src/nvim/eval/typval_encode.c.h @@ -83,6 +83,13 @@ /// @param len String length. /// @param type EXT type. +/// @def TYPVAL_ENCODE_CONV_BLOB +/// @brief Macros used to convert a blob +/// +/// @param tv Pointer to typval where value is stored. May not be NULL. +/// @param blob Pointer to the blob to convert. +/// @param len Blob length. + /// @def TYPVAL_ENCODE_CONV_FUNC_START /// @brief Macros used when starting to convert a funcref or a partial /// @@ -330,6 +337,11 @@ static int _TYPVAL_ENCODE_CONVERT_ONE_VALUE( TYPVAL_ENCODE_CONV_FLOAT(tv, tv->vval.v_float); break; } + case VAR_BLOB: { + TYPVAL_ENCODE_CONV_BLOB(tv, tv->vval.v_blob, + tv_blob_len(tv->vval.v_blob)); + break; + } case VAR_FUNC: { TYPVAL_ENCODE_CONV_FUNC_START(tv, tv->vval.v_string); TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS(tv, 0); diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index 19535c2868..350c5ee1c2 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -5,6 +5,7 @@ #include "nvim/ascii.h" #include "nvim/charset.h" +#include "nvim/debugger.h" #include "nvim/edit.h" #include "nvim/eval.h" #include "nvim/eval/encode.h" diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index e06a62e0f6..6542ab41f5 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -20,14 +20,12 @@ #include "nvim/buffer.h" #include "nvim/change.h" #include "nvim/charset.h" +#include "nvim/debugger.h" #include "nvim/eval/userfunc.h" #include "nvim/ex_cmds.h" -#include "nvim/ex_docmd.h" #include "nvim/ex_eval.h" #include "nvim/ex_getln.h" #include "nvim/fileio.h" -#include "nvim/getchar.h" -#include "nvim/globals.h" #include "nvim/mark.h" #include "nvim/mbyte.h" #include "nvim/memline.h" @@ -43,13 +41,11 @@ #include "nvim/path.h" #include "nvim/quickfix.h" #include "nvim/regexp.h" -#include "nvim/screen.h" #include "nvim/strings.h" #include "nvim/undo.h" #include "nvim/version.h" #include "nvim/window.h" #include "nvim/profile.h" -#include "nvim/os/os.h" #include "nvim/os/shell.h" #include "nvim/os/fs_defs.h" #include "nvim/api/private/helpers.h" @@ -119,808 +115,6 @@ struct source_cookie { # include "ex_cmds2.c.generated.h" #endif -/// batch mode debugging: don't save and restore typeahead. -static bool debug_greedy = false; - -static char *debug_oldval = NULL; // old and newval for debug expressions -static char *debug_newval = NULL; - -/// Debug mode. Repeatedly get Ex commands, until told to continue normal -/// execution. -void do_debug(char_u *cmd) -{ - int save_msg_scroll = msg_scroll; - int save_State = State; - int save_did_emsg = did_emsg; - const bool save_cmd_silent = cmd_silent; - int save_msg_silent = msg_silent; - int save_emsg_silent = emsg_silent; - bool save_redir_off = redir_off; - tasave_T typeaheadbuf; - bool typeahead_saved = false; - int save_ignore_script = 0; - int save_ex_normal_busy; - int n; - char_u *cmdline = NULL; - char_u *p; - char *tail = NULL; - static int last_cmd = 0; -#define CMD_CONT 1 -#define CMD_NEXT 2 -#define CMD_STEP 3 -#define CMD_FINISH 4 -#define CMD_QUIT 5 -#define CMD_INTERRUPT 6 -#define CMD_BACKTRACE 7 -#define CMD_FRAME 8 -#define CMD_UP 9 -#define CMD_DOWN 10 - - - RedrawingDisabled++; // don't redisplay the window - no_wait_return++; // don't wait for return - did_emsg = false; // don't use error from debugged stuff - cmd_silent = false; // display commands - msg_silent = false; // display messages - emsg_silent = false; // display error messages - redir_off = true; // don't redirect debug commands - - State = NORMAL; - debug_mode = true; - - if (!debug_did_msg) { - MSG(_("Entering Debug mode. Type \"cont\" to continue.")); - } - if (debug_oldval != NULL) { - smsg(_("Oldval = \"%s\""), debug_oldval); - xfree(debug_oldval); - debug_oldval = NULL; - } - if (debug_newval != NULL) { - smsg(_("Newval = \"%s\""), debug_newval); - xfree(debug_newval); - debug_newval = NULL; - } - if (sourcing_name != NULL) { - msg(sourcing_name); - } - if (sourcing_lnum != 0) { - smsg(_("line %" PRId64 ": %s"), (int64_t)sourcing_lnum, cmd); - } else { - smsg(_("cmd: %s"), cmd); - } - // Repeat getting a command and executing it. - for (;; ) { - msg_scroll = true; - need_wait_return = false; - // Save the current typeahead buffer and replace it with an empty one. - // This makes sure we get input from the user here and don't interfere - // with the commands being executed. Reset "ex_normal_busy" to avoid - // the side effects of using ":normal". Save the stuff buffer and make - // it empty. Set ignore_script to avoid reading from script input. - save_ex_normal_busy = ex_normal_busy; - ex_normal_busy = 0; - if (!debug_greedy) { - save_typeahead(&typeaheadbuf); - typeahead_saved = true; - save_ignore_script = ignore_script; - ignore_script = true; - } - - xfree(cmdline); - cmdline = (char_u *)getcmdline_prompt('>', NULL, 0, EXPAND_NOTHING, NULL, - CALLBACK_NONE); - - if (typeahead_saved) { - restore_typeahead(&typeaheadbuf); - ignore_script = save_ignore_script; - } - ex_normal_busy = save_ex_normal_busy; - - cmdline_row = msg_row; - msg_starthere(); - if (cmdline != NULL) { - // If this is a debug command, set "last_cmd". - // If not, reset "last_cmd". - // For a blank line use previous command. - p = skipwhite(cmdline); - if (*p != NUL) { - switch (*p) { - case 'c': last_cmd = CMD_CONT; - tail = "ont"; - break; - case 'n': last_cmd = CMD_NEXT; - tail = "ext"; - break; - case 's': last_cmd = CMD_STEP; - tail = "tep"; - break; - case 'f': - last_cmd = 0; - if (p[1] == 'r') { - last_cmd = CMD_FRAME; - tail = "rame"; - } else { - last_cmd = CMD_FINISH; - tail = "inish"; - } - break; - case 'q': last_cmd = CMD_QUIT; - tail = "uit"; - break; - case 'i': last_cmd = CMD_INTERRUPT; - tail = "nterrupt"; - break; - case 'b': - last_cmd = CMD_BACKTRACE; - if (p[1] == 't') { - tail = "t"; - } else { - tail = "acktrace"; - } - break; - case 'w': - last_cmd = CMD_BACKTRACE; - tail = "here"; - break; - case 'u': - last_cmd = CMD_UP; - tail = "p"; - break; - case 'd': - last_cmd = CMD_DOWN; - tail = "own"; - break; - default: last_cmd = 0; - } - if (last_cmd != 0) { - // Check that the tail matches. - p++; - while (*p != NUL && *p == *tail) { - p++; - tail++; - } - if (ASCII_ISALPHA(*p) && last_cmd != CMD_FRAME) { - last_cmd = 0; - } - } - } - - if (last_cmd != 0) { - // Execute debug command: decided where to break next and return. - switch (last_cmd) { - case CMD_CONT: - debug_break_level = -1; - break; - case CMD_NEXT: - debug_break_level = ex_nesting_level; - break; - case CMD_STEP: - debug_break_level = 9999; - break; - case CMD_FINISH: - debug_break_level = ex_nesting_level - 1; - break; - case CMD_QUIT: - got_int = true; - debug_break_level = -1; - break; - case CMD_INTERRUPT: - got_int = true; - debug_break_level = 9999; - // Do not repeat ">interrupt" cmd, continue stepping. - last_cmd = CMD_STEP; - break; - case CMD_BACKTRACE: - do_showbacktrace(cmd); - continue; - case CMD_FRAME: - if (*p == NUL) { - do_showbacktrace(cmd); - } else { - p = skipwhite(p); - do_setdebugtracelevel(p); - } - continue; - case CMD_UP: - debug_backtrace_level++; - do_checkbacktracelevel(); - continue; - case CMD_DOWN: - debug_backtrace_level--; - do_checkbacktracelevel(); - continue; - } - // Going out reset backtrace_level - debug_backtrace_level = 0; - break; - } - - // don't debug this command - n = debug_break_level; - debug_break_level = -1; - (void)do_cmdline(cmdline, getexline, NULL, - DOCMD_VERBOSE|DOCMD_EXCRESET); - debug_break_level = n; - } - lines_left = (int)(Rows - 1); - } - xfree(cmdline); - - RedrawingDisabled--; - no_wait_return--; - redraw_all_later(NOT_VALID); - need_wait_return = false; - msg_scroll = save_msg_scroll; - lines_left = (int)(Rows - 1); - State = save_State; - debug_mode = false; - did_emsg = save_did_emsg; - cmd_silent = save_cmd_silent; - msg_silent = save_msg_silent; - emsg_silent = save_emsg_silent; - redir_off = save_redir_off; - - // Only print the message again when typing a command before coming back here. - debug_did_msg = true; -} - -static int get_maxbacktrace_level(void) -{ - int maxbacktrace = 0; - - if (sourcing_name != NULL) { - char *p = (char *)sourcing_name; - char *q; - while ((q = strstr(p, "..")) != NULL) { - p = q + 2; - maxbacktrace++; - } - } - return maxbacktrace; -} - -static void do_setdebugtracelevel(char_u *arg) -{ - int level = atoi((char *)arg); - if (*arg == '+' || level < 0) { - debug_backtrace_level += level; - } else { - debug_backtrace_level = level; - } - - do_checkbacktracelevel(); -} - -static void do_checkbacktracelevel(void) -{ - if (debug_backtrace_level < 0) { - debug_backtrace_level = 0; - MSG(_("frame is zero")); - } else { - int max = get_maxbacktrace_level(); - if (debug_backtrace_level > max) { - debug_backtrace_level = max; - smsg(_("frame at highest level: %d"), max); - } - } -} - -static void do_showbacktrace(char_u *cmd) -{ - if (sourcing_name != NULL) { - int i = 0; - int max = get_maxbacktrace_level(); - char *cur = (char *)sourcing_name; - while (!got_int) { - char *next = strstr(cur, ".."); - if (next != NULL) { - *next = NUL; - } - if (i == max - debug_backtrace_level) { - smsg("->%d %s", max - i, cur); - } else { - smsg(" %d %s", max - i, cur); - } - i++; - if (next == NULL) { - break; - } - *next = '.'; - cur = next + 2; - } - } - if (sourcing_lnum != 0) { - smsg(_("line %" PRId64 ": %s"), (int64_t)sourcing_lnum, cmd); - } else { - smsg(_("cmd: %s"), cmd); - } -} - - -/// ":debug". -void ex_debug(exarg_T *eap) -{ - int debug_break_level_save = debug_break_level; - - debug_break_level = 9999; - do_cmdline_cmd((char *)eap->arg); - debug_break_level = debug_break_level_save; -} - -static char_u *debug_breakpoint_name = NULL; -static linenr_T debug_breakpoint_lnum; - -/// When debugging or a breakpoint is set on a skipped command, no debug prompt -/// is shown by do_one_cmd(). This situation is indicated by debug_skipped, and -/// debug_skipped_name is then set to the source name in the breakpoint case. If -/// a skipped command decides itself that a debug prompt should be displayed, it -/// can do so by calling dbg_check_skipped(). -static int debug_skipped; -static char_u *debug_skipped_name; - -/// Go to debug mode when a breakpoint was encountered or "ex_nesting_level" is -/// at or below the break level. But only when the line is actually -/// executed. Return true and set breakpoint_name for skipped commands that -/// decide to execute something themselves. -/// Called from do_one_cmd() before executing a command. -void dbg_check_breakpoint(exarg_T *eap) -{ - char_u *p; - - debug_skipped = false; - if (debug_breakpoint_name != NULL) { - if (!eap->skip) { - // replace K_SNR with "<SNR>" - if (debug_breakpoint_name[0] == K_SPECIAL - && debug_breakpoint_name[1] == KS_EXTRA - && debug_breakpoint_name[2] == (int)KE_SNR) { - p = (char_u *)"<SNR>"; - } else { - p = (char_u *)""; - } - smsg(_("Breakpoint in \"%s%s\" line %" PRId64), - p, - debug_breakpoint_name + (*p == NUL ? 0 : 3), - (int64_t)debug_breakpoint_lnum); - debug_breakpoint_name = NULL; - do_debug(eap->cmd); - } else { - debug_skipped = true; - debug_skipped_name = debug_breakpoint_name; - debug_breakpoint_name = NULL; - } - } else if (ex_nesting_level <= debug_break_level) { - if (!eap->skip) { - do_debug(eap->cmd); - } else { - debug_skipped = true; - debug_skipped_name = NULL; - } - } -} - -/// Go to debug mode if skipped by dbg_check_breakpoint() because eap->skip was -/// set. -/// -/// @return true when the debug mode is entered this time. -bool dbg_check_skipped(exarg_T *eap) -{ - int prev_got_int; - - if (debug_skipped) { - // Save the value of got_int and reset it. We don't want a previous - // interruption cause flushing the input buffer. - prev_got_int = got_int; - got_int = false; - debug_breakpoint_name = debug_skipped_name; - // eap->skip is true - eap->skip = false; - dbg_check_breakpoint(eap); - eap->skip = true; - got_int |= prev_got_int; - return true; - } - return false; -} - -/// The list of breakpoints: dbg_breakp. -/// This is a grow-array of structs. -struct debuggy { - int dbg_nr; ///< breakpoint number - int dbg_type; ///< DBG_FUNC or DBG_FILE or DBG_EXPR - char_u *dbg_name; ///< function, expression or file name - regprog_T *dbg_prog; ///< regexp program - linenr_T dbg_lnum; ///< line number in function or file - int dbg_forceit; ///< ! used - typval_T *dbg_val; ///< last result of watchexpression - int dbg_level; ///< stored nested level for expr -}; - -static garray_T dbg_breakp = { 0, 0, sizeof(struct debuggy), 4, NULL }; -#define BREAKP(idx) (((struct debuggy *)dbg_breakp.ga_data)[idx]) -#define DEBUGGY(gap, idx) (((struct debuggy *)gap->ga_data)[idx]) -static int last_breakp = 0; // nr of last defined breakpoint - -// Profiling uses file and func names similar to breakpoints. -static garray_T prof_ga = { 0, 0, sizeof(struct debuggy), 4, NULL }; -#define DBG_FUNC 1 -#define DBG_FILE 2 -#define DBG_EXPR 3 - - -/// Parse the arguments of ":profile", ":breakadd" or ":breakdel" and put them -/// in the entry just after the last one in dbg_breakp. Note that "dbg_name" -/// is allocated. -/// Returns FAIL for failure. -/// -/// @param arg -/// @param gap either &dbg_breakp or &prof_ga -static int dbg_parsearg(char_u *arg, garray_T *gap) -{ - char_u *p = arg; - char_u *q; - struct debuggy *bp; - bool here = false; - - ga_grow(gap, 1); - - bp = &DEBUGGY(gap, gap->ga_len); - - // Find "func" or "file". - if (STRNCMP(p, "func", 4) == 0) { - bp->dbg_type = DBG_FUNC; - } else if (STRNCMP(p, "file", 4) == 0) { - bp->dbg_type = DBG_FILE; - } else if (gap != &prof_ga && STRNCMP(p, "here", 4) == 0) { - if (curbuf->b_ffname == NULL) { - EMSG(_(e_noname)); - return FAIL; - } - bp->dbg_type = DBG_FILE; - here = true; - } else if (gap != &prof_ga && STRNCMP(p, "expr", 4) == 0) { - bp->dbg_type = DBG_EXPR; - } else { - EMSG2(_(e_invarg2), p); - return FAIL; - } - p = skipwhite(p + 4); - - // Find optional line number. - if (here) { - bp->dbg_lnum = curwin->w_cursor.lnum; - } else if (gap != &prof_ga && ascii_isdigit(*p)) { - bp->dbg_lnum = getdigits_long(&p, true, 0); - p = skipwhite(p); - } else { - bp->dbg_lnum = 0; - } - - // Find the function or file name. Don't accept a function name with (). - if ((!here && *p == NUL) - || (here && *p != NUL) - || (bp->dbg_type == DBG_FUNC && strstr((char *)p, "()") != NULL)) { - EMSG2(_(e_invarg2), arg); - return FAIL; - } - - if (bp->dbg_type == DBG_FUNC) { - bp->dbg_name = vim_strsave(p); - } else if (here) { - bp->dbg_name = vim_strsave(curbuf->b_ffname); - } else if (bp->dbg_type == DBG_EXPR) { - bp->dbg_name = vim_strsave(p); - bp->dbg_val = eval_expr(bp->dbg_name); - } else { - // Expand the file name in the same way as do_source(). This means - // doing it twice, so that $DIR/file gets expanded when $DIR is - // "~/dir". - q = expand_env_save(p); - if (q == NULL) { - return FAIL; - } - p = expand_env_save(q); - xfree(q); - if (p == NULL) { - return FAIL; - } - if (*p != '*') { - bp->dbg_name = (char_u *)fix_fname((char *)p); - xfree(p); - } else { - bp->dbg_name = p; - } - } - - if (bp->dbg_name == NULL) { - return FAIL; - } - return OK; -} - -/// ":breakadd". Also used for ":profile". -void ex_breakadd(exarg_T *eap) -{ - struct debuggy *bp; - garray_T *gap; - - gap = &dbg_breakp; - if (eap->cmdidx == CMD_profile) { - gap = &prof_ga; - } - - if (dbg_parsearg(eap->arg, gap) == OK) { - bp = &DEBUGGY(gap, gap->ga_len); - bp->dbg_forceit = eap->forceit; - - if (bp->dbg_type != DBG_EXPR) { - char_u *pat = file_pat_to_reg_pat(bp->dbg_name, NULL, NULL, false); - if (pat != NULL) { - bp->dbg_prog = vim_regcomp(pat, RE_MAGIC + RE_STRING); - xfree(pat); - } - if (pat == NULL || bp->dbg_prog == NULL) { - xfree(bp->dbg_name); - } else { - if (bp->dbg_lnum == 0) { // default line number is 1 - bp->dbg_lnum = 1; - } - if (eap->cmdidx != CMD_profile) { - DEBUGGY(gap, gap->ga_len).dbg_nr = ++last_breakp; - debug_tick++; - } - gap->ga_len++; - } - } else { - // DBG_EXPR - DEBUGGY(gap, gap->ga_len++).dbg_nr = ++last_breakp; - debug_tick++; - } - } -} - -/// ":debuggreedy". -void ex_debuggreedy(exarg_T *eap) -{ - if (eap->addr_count == 0 || eap->line2 != 0) { - debug_greedy = true; - } else { - debug_greedy = false; - } -} - -/// ":breakdel" and ":profdel". -void ex_breakdel(exarg_T *eap) -{ - struct debuggy *bp, *bpi; - int nr; - int todel = -1; - bool del_all = false; - linenr_T best_lnum = 0; - garray_T *gap; - - gap = &dbg_breakp; - if (eap->cmdidx == CMD_profdel) { - gap = &prof_ga; - } - - if (ascii_isdigit(*eap->arg)) { - // ":breakdel {nr}" - nr = atoi((char *)eap->arg); - for (int i = 0; i < gap->ga_len; i++) { - if (DEBUGGY(gap, i).dbg_nr == nr) { - todel = i; - break; - } - } - } else if (*eap->arg == '*') { - todel = 0; - del_all = true; - } else { - // ":breakdel {func|file|expr} [lnum] {name}" - if (dbg_parsearg(eap->arg, gap) == FAIL) { - return; - } - bp = &DEBUGGY(gap, gap->ga_len); - for (int i = 0; i < gap->ga_len; i++) { - bpi = &DEBUGGY(gap, i); - if (bp->dbg_type == bpi->dbg_type - && STRCMP(bp->dbg_name, bpi->dbg_name) == 0 - && (bp->dbg_lnum == bpi->dbg_lnum - || (bp->dbg_lnum == 0 - && (best_lnum == 0 - || bpi->dbg_lnum < best_lnum)))) { - todel = i; - best_lnum = bpi->dbg_lnum; - } - } - xfree(bp->dbg_name); - } - - if (todel < 0) { - EMSG2(_("E161: Breakpoint not found: %s"), eap->arg); - } else { - while (!GA_EMPTY(gap)) { - xfree(DEBUGGY(gap, todel).dbg_name); - if (DEBUGGY(gap, todel).dbg_type == DBG_EXPR - && DEBUGGY(gap, todel).dbg_val != NULL) { - tv_free(DEBUGGY(gap, todel).dbg_val); - } - vim_regfree(DEBUGGY(gap, todel).dbg_prog); - gap->ga_len--; - if (todel < gap->ga_len) { - memmove(&DEBUGGY(gap, todel), &DEBUGGY(gap, todel + 1), - (size_t)(gap->ga_len - todel) * sizeof(struct debuggy)); - } - if (eap->cmdidx == CMD_breakdel) { - debug_tick++; - } - if (!del_all) { - break; - } - } - - // If all breakpoints were removed clear the array. - if (GA_EMPTY(gap)) { - ga_clear(gap); - } - } -} - -/// ":breaklist". -void ex_breaklist(exarg_T *eap) -{ - struct debuggy *bp; - - if (GA_EMPTY(&dbg_breakp)) { - MSG(_("No breakpoints defined")); - } else { - for (int i = 0; i < dbg_breakp.ga_len; i++) { - bp = &BREAKP(i); - if (bp->dbg_type == DBG_FILE) { - home_replace(NULL, bp->dbg_name, NameBuff, MAXPATHL, true); - } - if (bp->dbg_type != DBG_EXPR) { - smsg(_("%3d %s %s line %" PRId64), - bp->dbg_nr, - bp->dbg_type == DBG_FUNC ? "func" : "file", - bp->dbg_type == DBG_FUNC ? bp->dbg_name : NameBuff, - (int64_t)bp->dbg_lnum); - } else { - smsg(_("%3d expr %s"), bp->dbg_nr, bp->dbg_name); - } - } - } -} - -/// Find a breakpoint for a function or sourced file. -/// Returns line number at which to break; zero when no matching breakpoint. -linenr_T -dbg_find_breakpoint( - bool file, // true for a file, false for a function - char_u *fname, // file or function name - linenr_T after // after this line number -) -{ - return debuggy_find(file, fname, after, &dbg_breakp, NULL); -} - -/// @param file true for a file, false for a function -/// @param fname file or function name -/// @param fp[out] forceit -/// -/// @returns true if profiling is on for a function or sourced file. -bool has_profiling(bool file, char_u *fname, bool *fp) -{ - return debuggy_find(file, fname, (linenr_T)0, &prof_ga, fp) - != (linenr_T)0; -} - -/// Common code for dbg_find_breakpoint() and has_profiling(). -static linenr_T -debuggy_find( - bool file, // true for a file, false for a function - char_u *fname, // file or function name - linenr_T after, // after this line number - garray_T *gap, // either &dbg_breakp or &prof_ga - bool *fp // if not NULL: return forceit -) -{ - struct debuggy *bp; - linenr_T lnum = 0; - char_u *name = fname; - int prev_got_int; - - // Return quickly when there are no breakpoints. - if (GA_EMPTY(gap)) { - return (linenr_T)0; - } - - // Replace K_SNR in function name with "<SNR>". - if (!file && fname[0] == K_SPECIAL) { - name = xmalloc(STRLEN(fname) + 3); - STRCPY(name, "<SNR>"); - STRCPY(name + 5, fname + 3); - } - - for (int i = 0; i < gap->ga_len; i++) { - // Skip entries that are not useful or are for a line that is beyond - // an already found breakpoint. - bp = &DEBUGGY(gap, i); - if ((bp->dbg_type == DBG_FILE) == file - && bp->dbg_type != DBG_EXPR - && (gap == &prof_ga - || (bp->dbg_lnum > after && (lnum == 0 || bp->dbg_lnum < lnum)))) { - // Save the value of got_int and reset it. We don't want a - // previous interruption cancel matching, only hitting CTRL-C - // while matching should abort it. - prev_got_int = got_int; - got_int = false; - if (vim_regexec_prog(&bp->dbg_prog, false, name, (colnr_T)0)) { - lnum = bp->dbg_lnum; - if (fp != NULL) { - *fp = bp->dbg_forceit; - } - } - got_int |= prev_got_int; - } else if (bp->dbg_type == DBG_EXPR) { - bool line = false; - - prev_got_int = got_int; - got_int = false; - - typval_T *tv = eval_expr(bp->dbg_name); - if (tv != NULL) { - if (bp->dbg_val == NULL) { - debug_oldval = typval_tostring(NULL); - bp->dbg_val = tv; - debug_newval = typval_tostring(bp->dbg_val); - line = true; - } else { - if (typval_compare(tv, bp->dbg_val, EXPR_IS, false) == OK - && tv->vval.v_number == false) { - line = true; - debug_oldval = typval_tostring(bp->dbg_val); - // Need to evaluate again, typval_compare() overwrites "tv". - typval_T *v = eval_expr(bp->dbg_name); - debug_newval = typval_tostring(v); - tv_free(bp->dbg_val); - bp->dbg_val = v; - } - tv_free(tv); - } - } else if (bp->dbg_val != NULL) { - debug_oldval = typval_tostring(bp->dbg_val); - debug_newval = typval_tostring(NULL); - tv_free(bp->dbg_val); - bp->dbg_val = NULL; - line = true; - } - - if (line) { - lnum = after > 0 ? after : 1; - break; - } - - got_int |= prev_got_int; - } - } - if (name != fname) { - xfree(name); - } - - return lnum; -} - -/// Called when a breakpoint was encountered. -void dbg_breakpoint(char_u *name, linenr_T lnum) -{ - // We need to check if this line is actually executed in do_one_cmd() - debug_breakpoint_name = name; - debug_breakpoint_lnum = lnum; -} - static char_u *profile_fname = NULL; /// ":profile cmd args" @@ -2635,44 +1829,42 @@ static void cmd_source(char_u *fname, exarg_T *eap) } } -typedef struct { - linenr_T curr_lnum; - const linenr_T final_lnum; -} GetBufferLineCookie; - -/// Get one line from the current selection in the buffer. -/// Called by do_cmdline() when it's called from cmd_source_buffer(). +/// Concatenate VimL line if it starts with a line continuation into a growarray +/// (excluding the continuation chars and leading whitespace) /// -/// @return pointer to allocated line, or NULL for end-of-file or -/// some error. -static char_u *get_buffer_line(int c, void *cookie, int indent, bool do_concat) -{ - GetBufferLineCookie *p = cookie; - if (p->curr_lnum > p->final_lnum) { - return NULL; - } - char_u *curr_line = ml_get(p->curr_lnum); - p->curr_lnum++; - return (char_u *)xstrdup((const char *)curr_line); -} - -static void cmd_source_buffer(const exarg_T *eap) +/// @note Growsize of the growarray may be changed to speed up concatenations! +/// +/// @param ga the growarray to append to +/// @param init_growsize the starting growsize value of the growarray +/// @param p pointer to the beginning of the line to consider +/// @param len the length of this line +/// +/// @return true if this line did begin with a continuation (the next line +/// should also be considered, if it exists); false otherwise +static bool concat_continued_line(garray_T *const ga, const int init_growsize, + const char_u *const p, size_t len) FUNC_ATTR_NONNULL_ALL { - GetBufferLineCookie cookie = { - .curr_lnum = eap->line1, - .final_lnum = eap->line2, - }; - if (curbuf != NULL && curbuf->b_fname - && path_with_extension((const char *)curbuf->b_fname, "lua")) { - nlua_source_using_linegetter(get_buffer_line, (void *)&cookie, - ":source (no file)"); - } else { - source_using_linegetter((void *)&cookie, get_buffer_line, - ":source (no file)"); + const char_u *const line = skipwhite_len(p, len); + len -= (size_t)(line - p); + // Skip lines starting with '\" ', concat lines starting with '\' + if (len >= 3 && STRNCMP(line, "\"\\ ", 3) == 0) { + return true; + } else if (len == 0 || line[0] != '\\') { + return false; } + if (ga->ga_len > init_growsize) { + ga_set_growsize(ga, MAX(ga->ga_len, 8000)); + } + ga_concat_len(ga, (const char *)line + 1, len - 1); + return true; } +typedef struct { + linenr_T curr_lnum; + const linenr_T final_lnum; +} GetBufferLineCookie; + /// ":source" and associated commands. /// /// @return address holding the next breakpoint line for a source cookie @@ -2725,17 +1917,27 @@ typedef struct { static char_u *get_str_line(int c, void *cookie, int indent, bool do_concat) { GetStrLineCookie *p = cookie; - size_t i = p->offset; - if (strlen((char *)p->buf) <= p->offset) { + if (STRLEN(p->buf) <= p->offset) { return NULL; } - while (!(p->buf[i] == '\n' || p->buf[i] == '\0')) { - i++; + const char_u *line = p->buf + p->offset; + const char_u *eol = skip_to_newline(line); + garray_T ga; + ga_init(&ga, sizeof(char_u), 400); + ga_concat_len(&ga, (const char *)line, (size_t)(eol - line)); + if (do_concat && vim_strchr(p_cpo, CPO_CONCAT) == NULL) { + while (eol[0] != NUL) { + line = eol + 1; + const char_u *const next_eol = skip_to_newline(line); + if (!concat_continued_line(&ga, 400, line, (size_t)(next_eol - line))) { + break; + } + eol = next_eol; + } } - size_t line_length = i - p->offset; - char_u *buf = xmemdupz(p->buf + p->offset, line_length); - p->offset = i + 1; - return buf; + ga_append(&ga, NUL); + p->offset = (size_t)(eol - p->buf) + 1; + return ga.ga_data; } static int source_using_linegetter(void *cookie, @@ -2770,6 +1972,40 @@ static int source_using_linegetter(void *cookie, return retval; } +static void cmd_source_buffer(const exarg_T *const eap) + FUNC_ATTR_NONNULL_ALL +{ + if (curbuf == NULL) { + return; + } + garray_T ga; + ga_init(&ga, sizeof(char_u), 400); + const linenr_T final_lnum = eap->line2; + // Copy the contents to be executed. + for (linenr_T curr_lnum = eap->line1; curr_lnum <= final_lnum; curr_lnum++) { + // Adjust growsize to current length to speed up concatenating many lines. + if (ga.ga_len > 400) { + ga_set_growsize(&ga, MAX(ga.ga_len, 8000)); + } + ga_concat(&ga, ml_get(curr_lnum)); + ga_append(&ga, NL); + } + ((char_u *)ga.ga_data)[ga.ga_len - 1] = NUL; + const GetStrLineCookie cookie = { + .buf = ga.ga_data, + .offset = 0, + }; + if (curbuf->b_fname + && path_with_extension((const char *)curbuf->b_fname, "lua")) { + nlua_source_using_linegetter(get_str_line, (void *)&cookie, + ":source (no file)"); + } else { + source_using_linegetter((void *)&cookie, get_str_line, + ":source (no file)"); + } + ga_clear(&ga); +} + /// Executes lines in `src` as Ex commands. /// /// @see do_source() @@ -3227,26 +2463,11 @@ char_u *getsourceline(int c, void *cookie, int indent, bool do_concat) ga_init(&ga, (int)sizeof(char_u), 400); ga_concat(&ga, line); - if (*p == '\\') { - ga_concat(&ga, p + 1); - } - for (;; ) { + while (sp->nextline != NULL + && concat_continued_line(&ga, 400, sp->nextline, + STRLEN(sp->nextline))) { xfree(sp->nextline); sp->nextline = get_one_sourceline(sp); - if (sp->nextline == NULL) { - break; - } - p = skipwhite(sp->nextline); - if (*p == '\\') { - // Adjust the growsize to the current length to speed up - // concatenating many lines. - if (ga.ga_len > 400) { - ga_set_growsize(&ga, (ga.ga_len > 8000) ? 8000 : ga.ga_len); - } - ga_concat(&ga, p + 1); - } else if (p[0] != '"' || p[1] != '\\' || p[2] != ' ') { - break; - } } ga_append(&ga, NUL); xfree(line); diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 4280a6a2c6..2a0d365548 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -15,6 +15,7 @@ #include "nvim/change.h" #include "nvim/charset.h" #include "nvim/cursor.h" +#include "nvim/debugger.h" #include "nvim/diff.h" #include "nvim/digraph.h" #include "nvim/edit.h" diff --git a/src/nvim/ex_eval.c b/src/nvim/ex_eval.c index 1ceccac2bb..54776c35e7 100644 --- a/src/nvim/ex_eval.c +++ b/src/nvim/ex_eval.c @@ -13,6 +13,7 @@ #include "nvim/vim.h" #include "nvim/ascii.h" +#include "nvim/debugger.h" #include "nvim/ex_eval.h" #include "nvim/charset.h" #include "nvim/eval.h" diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 2a72dbcd09..4d54907a75 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -939,13 +939,18 @@ EXTERN char_u e_readonlyvar[] INIT(= N_( "E46: Cannot change read-only variable \"%.*s\"")); EXTERN char_u e_stringreq[] INIT(= N_("E928: String required")); EXTERN char_u e_dictreq[] INIT(= N_("E715: Dictionary required")); +EXTERN char_u e_blobidx[] INIT(= N_("E979: Blob index out of range: %" PRId64)); +EXTERN char_u e_invalblob[] INIT(= N_("E978: Invalid operation for Blob")); EXTERN char_u e_toomanyarg[] INIT(= N_( "E118: Too many arguments for function: %s")); EXTERN char_u e_dictkey[] INIT(= N_( "E716: Key not present in Dictionary: \"%s\"")); EXTERN char_u e_listreq[] INIT(= N_("E714: List required")); +EXTERN char_u e_listblobreq[] INIT(= N_("E897: List or Blob required")); EXTERN char_u e_listdictarg[] INIT(= N_( "E712: Argument of %s must be a List or Dictionary")); +EXTERN char_u e_listdictblobarg[] INIT(= N_( + "E896: Argument of %s must be a List, Dictionary or Blob")); EXTERN char_u e_readerrf[] INIT(= N_("E47: Error while reading errorfile")); EXTERN char_u e_sandbox[] INIT(= N_("E48: Not allowed in sandbox")); EXTERN char_u e_secure[] INIT(= N_("E523: Not allowed here")); diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c index 1a59cd94ae..0adbbdb953 100644 --- a/src/nvim/lua/converter.c +++ b/src/nvim/lua/converter.c @@ -481,6 +481,14 @@ static bool typval_conv_special = false; #define TYPVAL_ENCODE_CONV_EXT_STRING(tv, str, len, type) \ TYPVAL_ENCODE_CONV_NIL(tv) +#define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \ + do { \ + const blob_T *const blob_ = (blob); \ + lua_pushlstring(lstate, \ + blob_ != NULL ? (const char *)blob_->bv_ga.ga_data : "", \ + (size_t)(len)); \ + } while (0) + #define TYPVAL_ENCODE_CONV_FUNC_START(tv, fun) \ do { \ TYPVAL_ENCODE_CONV_NIL(tv); \ @@ -579,6 +587,7 @@ static bool typval_conv_special = false; #undef TYPVAL_ENCODE_CONV_STRING #undef TYPVAL_ENCODE_CONV_STR_STRING #undef TYPVAL_ENCODE_CONV_EXT_STRING +#undef TYPVAL_ENCODE_CONV_BLOB #undef TYPVAL_ENCODE_CONV_NUMBER #undef TYPVAL_ENCODE_CONV_FLOAT #undef TYPVAL_ENCODE_CONV_FUNC_START diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index d071203db1..ce5bfabd9f 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -90,7 +90,7 @@ static void nlua_error(lua_State *const lstate, const char *const msg) lua_pop(lstate, 1); } -/// Return version of current neovim build +/// Gets the version of the current Nvim build. /// /// @param lstate Lua interpreter state. static int nlua_nvim_version(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL 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/src/nvim/shada.c b/src/nvim/shada.c index c0e787380f..7d277fe5c8 100644 --- a/src/nvim/shada.c +++ b/src/nvim/shada.c @@ -1653,6 +1653,13 @@ static ShaDaWriteResult shada_pack_entry(msgpack_packer *const packer, break; } case kSDItemVariable: { + if (entry.data.global_var.value.v_type == VAR_TYPE_BLOB) { + // Strings and Blobs both pack as msgpack BINs; differentiate them by + // storing an additional VAR_TYPE_BLOB element alongside Blobs + list_T *const list = tv_list_alloc(1); + tv_list_append_number(list, VAR_TYPE_BLOB); + entry.data.global_var.additional_elements = list; + } const size_t arr_size = 2 + (size_t)( tv_list_len(entry.data.global_var.additional_elements)); msgpack_pack_array(spacker, arr_size); @@ -3937,15 +3944,38 @@ shada_read_next_item_start: entry->data.global_var.name = xmemdupz(unpacked.data.via.array.ptr[0].via.bin.ptr, unpacked.data.via.array.ptr[0].via.bin.size); - if (msgpack_to_vim(unpacked.data.via.array.ptr[1], - &(entry->data.global_var.value)) == FAIL) { + SET_ADDITIONAL_ELEMENTS(unpacked.data.via.array, 2, + entry->data.global_var.additional_elements, + "variable"); + bool is_blob = false; + // A msgpack BIN could be a String or Blob; an additional VAR_TYPE_BLOB + // element is stored with Blobs which can be used to differentiate them + if (unpacked.data.via.array.ptr[1].type == MSGPACK_OBJECT_BIN) { + const listitem_T *type_item + = tv_list_first(entry->data.global_var.additional_elements); + if (type_item != NULL) { + const typval_T *type_tv = TV_LIST_ITEM_TV(type_item); + if (type_tv->v_type != VAR_NUMBER + || type_tv->vval.v_number != VAR_TYPE_BLOB) { + emsgf(_(READERR("variable", "has wrong variable type")), + initial_fpos); + goto shada_read_next_item_error; + } + is_blob = true; + } + } + if (is_blob) { + const msgpack_object_bin *const bin + = &unpacked.data.via.array.ptr[1].via.bin; + blob_T *const blob = tv_blob_alloc(); + ga_concat_len(&blob->bv_ga, bin->ptr, (size_t)bin->size); + tv_blob_set_ret(&entry->data.global_var.value, blob); + } else if (msgpack_to_vim(unpacked.data.via.array.ptr[1], + &(entry->data.global_var.value)) == FAIL) { emsgf(_(READERR("variable", "has value that cannot " "be converted to the VimL value")), initial_fpos); goto shada_read_next_item_error; } - SET_ADDITIONAL_ELEMENTS(unpacked.data.via.array, 2, - entry->data.global_var.additional_elements, - "variable"); break; } case kSDItemSubString: { diff --git a/src/nvim/testdir/test_blob.vim b/src/nvim/testdir/test_blob.vim new file mode 100644 index 0000000000..20758b0c0a --- /dev/null +++ b/src/nvim/testdir/test_blob.vim @@ -0,0 +1,349 @@ +" Tests for the Blob types + +func TearDown() + " Run garbage collection after every test + call test_garbagecollect_now() +endfunc + +" Tests for Blob type + +" Blob creation from constant +func Test_blob_create() + let b = 0zDEADBEEF + call assert_equal(v:t_blob, type(b)) + call assert_equal(4, len(b)) + call assert_equal(0xDE, b[0]) + call assert_equal(0xAD, b[1]) + call assert_equal(0xBE, b[2]) + call assert_equal(0xEF, b[3]) + call assert_fails('let x = b[4]') + + call assert_equal(0xDE, get(b, 0)) + call assert_equal(0xEF, get(b, 3)) + + call assert_fails('let b = 0z1', 'E973:') + call assert_fails('let b = 0z1x', 'E973:') + call assert_fails('let b = 0z12345', 'E973:') + + call assert_equal(0z, v:_null_blob) + + let b = 0z001122.33445566.778899.aabbcc.dd + call assert_equal(0z00112233445566778899aabbccdd, b) + call assert_fails('let b = 0z1.1') + call assert_fails('let b = 0z.') + call assert_fails('let b = 0z001122.') + call assert_fails('call get("", 1)', 'E896:') + call assert_equal(0, len(v:_null_blob)) +endfunc + +" assignment to a blob +func Test_blob_assign() + let b = 0zDEADBEEF + let b2 = b[1:2] + call assert_equal(0zADBE, b2) + + let bcopy = b[:] + call assert_equal(b, bcopy) + call assert_false(b is bcopy) + + let b = 0zDEADBEEF + let b2 = b + call assert_true(b is b2) + let b[:] = 0z11223344 + call assert_equal(0z11223344, b) + call assert_equal(0z11223344, b2) + call assert_true(b is b2) + + let b = 0zDEADBEEF + let b[3:] = 0z66 + call assert_equal(0zDEADBE66, b) + let b[:1] = 0z8899 + call assert_equal(0z8899BE66, b) + + call assert_fails('let b[2:3] = 0z112233', 'E972:') + call assert_fails('let b[2:3] = 0z11', 'E972:') + call assert_fails('let b[3:2] = 0z', 'E979:') + + let b = 0zDEADBEEF + let b += 0z99 + call assert_equal(0zDEADBEEF99, b) + + call assert_fails('let b .= 0z33', 'E734:') + call assert_fails('let b .= "xx"', 'E734:') + call assert_fails('let b += "xx"', 'E734:') + call assert_fails('let b[1:1] .= 0z55', 'E734:') + + let l = [0z12] + let m = deepcopy(l) + let m[0] = 0z34 " E742 or E741 should not occur. +endfunc + +func Test_blob_get_range() + let b = 0z0011223344 + call assert_equal(0z2233, b[2:3]) + call assert_equal(0z223344, b[2:-1]) + call assert_equal(0z00, b[0:-5]) + call assert_equal(0z, b[0:-11]) + call assert_equal(0z44, b[-1:]) + call assert_equal(0z0011223344, b[:]) + call assert_equal(0z0011223344, b[:-1]) + call assert_equal(0z, b[5:6]) +endfunc + +func Test_blob_get() + let b = 0z0011223344 + call assert_equal(0x00, get(b, 0)) + call assert_equal(0x22, get(b, 2, 999)) + call assert_equal(0x44, get(b, 4)) + call assert_equal(0x44, get(b, -1)) + call assert_equal(-1, get(b, 5)) + call assert_equal(999, get(b, 5, 999)) + call assert_equal(-1, get(b, -8)) + call assert_equal(999, get(b, -8, 999)) + call assert_equal(10, get(v:_null_blob, 2, 10)) + + call assert_equal(0x00, b[0]) + call assert_equal(0x22, b[2]) + call assert_equal(0x44, b[4]) + call assert_equal(0x44, b[-1]) + call assert_fails('echo b[5]', 'E979:') + call assert_fails('echo b[-8]', 'E979:') +endfunc + +func Test_blob_to_string() + let b = 0z00112233445566778899aabbccdd + call assert_equal('0z00112233.44556677.8899AABB.CCDD', string(b)) + call assert_equal(b, eval(string(b))) + call remove(b, 4, -1) + call assert_equal('0z00112233', string(b)) + call remove(b, 0, 3) + call assert_equal('0z', string(b)) +endfunc + +func Test_blob_compare() + let b1 = 0z0011 + let b2 = 0z1100 + let b3 = 0z001122 + call assert_true(b1 == b1) + call assert_false(b1 == b2) + call assert_false(b1 == b3) + call assert_true(b1 != b2) + call assert_true(b1 != b3) + call assert_true(b1 == 0z0011) + call assert_fails('echo b1 == 9', 'E977:') + call assert_fails('echo b1 != 9', 'E977:') + + call assert_false(b1 is b2) + let b2 = b1 + call assert_true(b1 == b2) + call assert_true(b1 is b2) + let b2 = copy(b1) + call assert_true(b1 == b2) + call assert_false(b1 is b2) + let b2 = b1[:] + call assert_true(b1 == b2) + call assert_false(b1 is b2) + + call assert_fails('let x = b1 > b2') + call assert_fails('let x = b1 < b2') + call assert_fails('let x = b1 - b2') + call assert_fails('let x = b1 / b2') + call assert_fails('let x = b1 * b2') +endfunc + +" test for range assign +func Test_blob_range_assign() + let b = 0z00 + let b[1] = 0x11 + let b[2] = 0x22 + call assert_equal(0z001122, b) + call assert_fails('let b[4] = 0x33', 'E979:') +endfunc + +func Test_blob_for_loop() + let blob = 0z00010203 + let i = 0 + for byte in blob + call assert_equal(i, byte) + let i += 1 + endfor + call assert_equal(4, i) + + let blob = 0z00 + call remove(blob, 0) + call assert_equal(0, len(blob)) + for byte in blob + call assert_error('loop over empty blob') + endfor + + let blob = 0z0001020304 + let i = 0 + for byte in blob + call assert_equal(i, byte) + if i == 1 + call remove(blob, 0) + elseif i == 3 + call remove(blob, 3) + endif + let i += 1 + endfor + call assert_equal(5, i) +endfunc + +func Test_blob_concatenate() + let b = 0z0011 + let b += 0z2233 + call assert_equal(0z00112233, b) + + call assert_fails('let b += "a"') + call assert_fails('let b += 88') + + let b = 0zDEAD + 0zBEEF + call assert_equal(0zDEADBEEF, b) +endfunc + +func Test_blob_add() + let b = 0z0011 + call add(b, 0x22) + call assert_equal(0z001122, b) + call add(b, '51') + call assert_equal(0z00112233, b) + + call assert_fails('call add(b, [9])', 'E745:') + call assert_fails('call add("", 0x01)', 'E897:') +endfunc + +func Test_blob_empty() + call assert_false(empty(0z001122)) + call assert_true(empty(0z)) + call assert_true(empty(v:_null_blob)) +endfunc + +" Test removing items in blob +func Test_blob_func_remove() + " Test removing 1 element + let b = 0zDEADBEEF + call assert_equal(0xDE, remove(b, 0)) + call assert_equal(0zADBEEF, b) + + let b = 0zDEADBEEF + call assert_equal(0xEF, remove(b, -1)) + call assert_equal(0zDEADBE, b) + + let b = 0zDEADBEEF + call assert_equal(0xAD, remove(b, 1)) + call assert_equal(0zDEBEEF, b) + + " Test removing range of element(s) + let b = 0zDEADBEEF + call assert_equal(0zBE, remove(b, 2, 2)) + call assert_equal(0zDEADEF, b) + + let b = 0zDEADBEEF + call assert_equal(0zADBE, remove(b, 1, 2)) + call assert_equal(0zDEEF, b) + + " Test invalid cases + let b = 0zDEADBEEF + call assert_fails("call remove(b, 5)", 'E979:') + call assert_fails("call remove(b, 1, 5)", 'E979:') + call assert_fails("call remove(b, 3, 2)", 'E979:') + call assert_fails("call remove(1, 0)", 'E896:') + call assert_fails("call remove(b, b)", 'E974:') + call assert_fails("call remove(v:_null_blob, 1, 2)", 'E979:') + + " Translated from v8.2.3284 + let b = 0zDEADBEEF + lockvar b + call assert_fails('call remove(b, 0)', 'E741:') + unlockvar b +endfunc + +func Test_blob_read_write() + let b = 0zDEADBEEF + call writefile(b, 'Xblob') + let br = readfile('Xblob', 'B') + call assert_equal(b, br) + call delete('Xblob') + + " This was crashing when calling readfile() with a directory. + call assert_fails("call readfile('.', 'B')", 'E17: "." is a directory') +endfunc + +" filter() item in blob +func Test_blob_filter() + call assert_equal(0z, filter(0zDEADBEEF, '0')) + call assert_equal(0zADBEEF, filter(0zDEADBEEF, 'v:val != 0xDE')) + call assert_equal(0zDEADEF, filter(0zDEADBEEF, 'v:val != 0xBE')) + call assert_equal(0zDEADBE, filter(0zDEADBEEF, 'v:val != 0xEF')) + call assert_equal(0zDEADBEEF, filter(0zDEADBEEF, '1')) + call assert_equal(0z01030103, filter(0z010203010203, 'v:val != 0x02')) + call assert_equal(0zADEF, filter(0zDEADBEEF, 'v:key % 2')) +endfunc + +" map() item in blob +func Test_blob_map() + call assert_equal(0zDFAEBFF0, map(0zDEADBEEF, 'v:val + 1')) + call assert_equal(0z00010203, map(0zDEADBEEF, 'v:key')) + call assert_equal(0zDEAEC0F2, map(0zDEADBEEF, 'v:key + v:val')) + + call assert_fails("call map(0z00, '[9]')", 'E978:') +endfunc + +func Test_blob_index() + call assert_equal(2, index(0zDEADBEEF, 0xBE)) + call assert_equal(-1, index(0zDEADBEEF, 0)) + call assert_equal(2, index(0z11111111, 0x11, 2)) + call assert_equal(3, index(0z11110111, 0x11, 2)) + call assert_equal(2, index(0z11111111, 0x11, -2)) + call assert_equal(3, index(0z11110111, 0x11, -2)) + + call assert_fails('call index("asdf", 0)', 'E897:') +endfunc + +func Test_blob_insert() + let b = 0zDEADBEEF + call insert(b, 0x33) + call assert_equal(0z33DEADBEEF, b) + + let b = 0zDEADBEEF + call insert(b, 0x33, 2) + call assert_equal(0zDEAD33BEEF, b) + + call assert_fails('call insert(b, -1)', 'E475:') + call assert_fails('call insert(b, 257)', 'E475:') + call assert_fails('call insert(b, 0, [9])', 'E745:') + call assert_equal(0, insert(v:_null_blob, 0x33)) + + " Translated from v8.2.3284 + let b = 0zDEADBEEF + lockvar b + call assert_fails('call insert(b, 3)', 'E741:') + unlockvar b +endfunc + +func Test_blob_reverse() + call assert_equal(0zEFBEADDE, reverse(0zDEADBEEF)) + call assert_equal(0zBEADDE, reverse(0zDEADBE)) + call assert_equal(0zADDE, reverse(0zDEAD)) + call assert_equal(0zDE, reverse(0zDE)) + call assert_equal(0z, reverse(v:_null_blob)) +endfunc + +func Test_blob_lock() + let b = 0z112233 + lockvar b + call assert_fails('let b = 0z44', 'E741:') + unlockvar b + let b = 0z44 +endfunc + +func Test_blob_sort() + if has('float') + call assert_fails('call sort([1.0, 0z11], "f")', 'E975:') + else + call assert_fails('call sort(["abc", 0z11], "f")', 'E702:') + endif +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_const.vim b/src/nvim/testdir/test_const.vim index ea69c8cba4..0d064617a5 100644 --- a/src/nvim/testdir/test_const.vim +++ b/src/nvim/testdir/test_const.vim @@ -244,18 +244,33 @@ func Test_const_with_eval_name() call assert_fails('const {s2} = "bar"', 'E995:') endfunc -func Test_lock_depth_is_1() - const l = [1, 2, 3] - const d = {'foo': 10} - - " Modify list - setting item is OK, adding/removing items not - let l[0] = 42 +func Test_lock_depth_is_2() + " Modify list - error when changing item or adding/removing items + const l = [1, 2, [3, 4]] + call assert_fails('let l[0] = 42', 'E741:') + call assert_fails('let l[2][0] = 42', 'E741:') call assert_fails('call add(l, 4)', 'E741:') call assert_fails('unlet l[1]', 'E741:') - " Modify dict - changing item is OK, adding/removing items not - let d['foo'] = 'hello' - let d.foo = 44 + " Modify blob - error when changing + const b = 0z001122 + call assert_fails('let b[0] = 42', 'E741:') + + " Modify dict - error when changing item or adding/removing items + const d = {'foo': 10} + call assert_fails("let d['foo'] = 'hello'", 'E741:') + call assert_fails("let d.foo = 'hello'", 'E741:') call assert_fails("let d['bar'] = 'hello'", 'E741:') call assert_fails("unlet d['foo']", 'E741:') + + " Modifying list or dict item contents is OK. + let lvar = ['a', 'b'] + let bvar = 0z1122 + const l2 = [0, lvar, bvar] + let l2[1][0] = 'c' + let l2[2][1] = 0x33 + call assert_equal([0, ['c', 'b'], 0z1133], l2) + + const d2 = #{a: 0, b: lvar, c: 4} + let d2.b[1] = 'd' endfunc diff --git a/src/nvim/testdir/test_debugger.vim b/src/nvim/testdir/test_debugger.vim index d1464e9d3b..a396efc09e 100644 --- a/src/nvim/testdir/test_debugger.vim +++ b/src/nvim/testdir/test_debugger.vim @@ -267,9 +267,7 @@ func Test_Debugger() call RunDbgCmd(buf, 'breakd func a()', ['E475: Invalid argument: func a()']) call RunDbgCmd(buf, 'breakd func a', ['E161: Breakpoint not found: func a']) call RunDbgCmd(buf, 'breakd expr', ['E475: Invalid argument: expr']) - call RunDbgCmd(buf, 'breakd expr x', [ - \ 'E121: Undefined variable: x', - \ 'E161: Breakpoint not found: expr x']) + call RunDbgCmd(buf, 'breakd expr x', ['E161: Breakpoint not found: expr x']) " finish the current function call RunDbgCmd(buf, 'finish', [ @@ -314,9 +312,12 @@ func Test_Debugger() call RunDbgCmd(buf, 'enew! | only!') call StopVimInTerminal(buf) +endfunc +func Test_Debugger_breakadd() " Tests for :breakadd file and :breakadd here " Breakpoints should be set before sourcing the file + CheckRunVimInTerminal let lines =<< trim END let var1 = 10 @@ -337,6 +338,10 @@ func Test_Debugger() call StopVimInTerminal(buf) call delete('Xtest.vim') + %bw! + + call assert_fails('breakadd here', 'E32:') + call assert_fails('breakadd file Xtest.vim /\)/', 'E55:') endfunc func Test_Backtrace_Through_Source() diff --git a/src/nvim/testdir/test_eval_stuff.vim b/src/nvim/testdir/test_eval_stuff.vim index 084c856ba0..f7b6704610 100644 --- a/src/nvim/testdir/test_eval_stuff.vim +++ b/src/nvim/testdir/test_eval_stuff.vim @@ -12,6 +12,48 @@ func Test_catch_return_with_error() call assert_equal(1, s:foo()) endfunc +func Test_nocatch_restore_silent_emsg() + silent! try + throw 1 + catch + endtry + echoerr 'wrong' + let c1 = nr2char(screenchar(&lines, 1)) + let c2 = nr2char(screenchar(&lines, 2)) + let c3 = nr2char(screenchar(&lines, 3)) + let c4 = nr2char(screenchar(&lines, 4)) + let c5 = nr2char(screenchar(&lines, 5)) + call assert_equal('wrong', c1 . c2 . c3 . c4 . c5) +endfunc + +func Test_mkdir_p() + call mkdir('Xmkdir/nested', 'p') + call assert_true(isdirectory('Xmkdir/nested')) + try + " Trying to make existing directories doesn't error + call mkdir('Xmkdir', 'p') + call mkdir('Xmkdir/nested', 'p') + catch /E739:/ + call assert_report('mkdir(..., "p") failed for an existing directory') + endtry + " 'p' doesn't suppress real errors + call writefile([], 'Xfile') + call assert_fails('call mkdir("Xfile", "p")', 'E739') + call delete('Xfile') + call delete('Xmkdir', 'rf') +endfunc + +func Test_line_continuation() + let array = [5, + "\ ignore this + \ 6, + "\ more to ignore + "\ more moreto ignore + \ ] + "\ and some more + call assert_equal([5, 6], array) +endfunc + func Test_E963() " These commands used to cause an internal error prior to vim 8.1.0563 let v_e = v:errors @@ -23,9 +65,11 @@ func Test_E963() endfunc func Test_for_invalid() - call assert_fails("for x in 99", 'E714:') - call assert_fails("for x in function('winnr')", 'E714:') - call assert_fails("for x in {'a': 9}", 'E714:') + " Vim gives incorrect emsg here until v8.2.3284, but the exact emsg from that + " patch cannot be used until v8.2.2658 is ported (for loop over Strings) + call assert_fails("for x in 99", 'E897:') + call assert_fails("for x in function('winnr')", 'E897:') + call assert_fails("for x in {'a': 9}", 'E897:') if 0 /1/5/2/s/\n @@ -51,32 +95,11 @@ func Test_readfile_binary() call delete('XReadfile') endfunc -func Test_mkdir_p() - call mkdir('Xmkdir/nested', 'p') - call assert_true(isdirectory('Xmkdir/nested')) - try - " Trying to make existing directories doesn't error - call mkdir('Xmkdir', 'p') - call mkdir('Xmkdir/nested', 'p') - catch /E739:/ - call assert_report('mkdir(..., "p") failed for an existing directory') - endtry - " 'p' doesn't suppress real errors - call writefile([], 'Xfile') - call assert_fails('call mkdir("Xfile", "p")', 'E739') - call delete('Xfile') - call delete('Xmkdir', 'rf') -endfunc - -func Test_line_continuation() - let array = [5, - "\ ignore this - \ 6, - "\ more to ignore - "\ more moreto ignore - \ ] - "\ and some more - call assert_equal([5, 6], array) +func Test_let_errmsg() + call assert_fails('let v:errmsg = []', 'E730:') + let v:errmsg = '' + call assert_fails('let v:errmsg = []', 'E730:') + let v:errmsg = '' endfunc func Test_string_concatenation() @@ -117,25 +140,29 @@ func Test_skip_after_throw() endtry endfunc -func Test_nocatch_restore_silent_emsg() - silent! try - throw 1 - catch - endtry - echoerr 'wrong' - let c1 = nr2char(screenchar(&lines, 1)) - let c2 = nr2char(screenchar(&lines, 2)) - let c3 = nr2char(screenchar(&lines, 3)) - let c4 = nr2char(screenchar(&lines, 4)) - let c5 = nr2char(screenchar(&lines, 5)) - call assert_equal('wrong', c1 . c2 . c3 . c4 . c5) +" scriptversion 1 +func Test_string_concat_scriptversion1() + call assert_true(has('vimscript-1')) + let a = 'a' + let b = 'b' + + echo a . b + let a .= b + let vers = 1.2.3 + call assert_equal('123', vers) + + if has('float') + call assert_fails('let f = .5', 'E15:') + endif endfunc -func Test_let_errmsg() - call assert_fails('let v:errmsg = []', 'E730:') - let v:errmsg = '' - call assert_fails('let v:errmsg = []', 'E730:') - let v:errmsg = '' +" scriptversion 1 +func Test_vvar_scriptversion1() + call assert_equal(15, 017) + call assert_equal(15, 0o17) + call assert_equal(15, 0O17) + call assert_equal(18, 018) + call assert_equal(511, 0o777) endfunc func Test_number_max_min_size() diff --git a/src/nvim/testdir/test_filter_map.vim b/src/nvim/testdir/test_filter_map.vim index a15567bcf2..a52a66ac2f 100644 --- a/src/nvim/testdir/test_filter_map.vim +++ b/src/nvim/testdir/test_filter_map.vim @@ -81,7 +81,11 @@ func Test_filter_map_dict_expr_funcref() call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, map(copy(dict), function('s:filter4'))) endfunc -func Test_map_fails() +func Test_map_filter_fails() call assert_fails('call map([1], "42 +")', 'E15:') call assert_fails('call filter([1], "42 +")', 'E15:') + call assert_fails("let l = map('abc', '\"> \" . v:val')", 'E896:') + call assert_fails("let l = filter('abc', '\"> \" . v:val')", 'E896:') endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_fnamemodify.vim b/src/nvim/testdir/test_fnamemodify.vim index 116d23ba88..fe1df8fd4a 100644 --- a/src/nvim/testdir/test_fnamemodify.vim +++ b/src/nvim/testdir/test_fnamemodify.vim @@ -72,4 +72,8 @@ func Test_fnamemodify_er() " :e never includes the whole filename, so "a.b":e:e:e --> "b" call assert_equal('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e')) call assert_equal('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e:e')) + + call assert_equal('', fnamemodify(v:_null_string, v:_null_string)) endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_listdict.vim b/src/nvim/testdir/test_listdict.vim index 5152af8f58..ae035fa519 100644 --- a/src/nvim/testdir/test_listdict.vim +++ b/src/nvim/testdir/test_listdict.vim @@ -139,7 +139,7 @@ func Test_list_func_remove() call assert_fails("call remove(l, 5)", 'E684:') call assert_fails("call remove(l, 1, 5)", 'E684:') call assert_fails("call remove(l, 3, 2)", 'E16:') - call assert_fails("call remove(1, 0)", 'E712:') + call assert_fails("call remove(1, 0)", 'E896:') call assert_fails("call remove(l, l)", 'E745:') endfunc @@ -616,6 +616,8 @@ func Test_reverse_sort_uniq() call assert_equal(['bar', 'BAR', 'Bar', 'Foo', 'FOO', 'foo', 'FOOBAR', -1, 0, 0, 0.22, 1.0e-15, 12, 18, 22, 255, 7, 9, [], {}], sort(copy(l), 1)) call assert_equal(['bar', 'BAR', 'Bar', 'Foo', 'FOO', 'foo', 'FOOBAR', -1, 0, 0, 0.22, 1.0e-15, 12, 18, 22, 255, 7, 9, [], {}], sort(copy(l), 'i')) call assert_equal(['BAR', 'Bar', 'FOO', 'FOOBAR', 'Foo', 'bar', 'foo', -1, 0, 0, 0.22, 1.0e-15, 12, 18, 22, 255, 7, 9, [], {}], sort(copy(l))) + + call assert_fails('call reverse("")', 'E899:') endfunc " splitting a string to a List diff --git a/src/nvim/testdir/test_method.vim b/src/nvim/testdir/test_method.vim index 7a6e6aa19d..d34448e09e 100644 --- a/src/nvim/testdir/test_method.vim +++ b/src/nvim/testdir/test_method.vim @@ -46,11 +46,8 @@ func Test_dict_method() call assert_equal(#{one: 1, two: 2, three: 3, four: 4}, d->extend(#{four: 4})) call assert_equal(#{one: 1, two: 2, three: 3}, d->filter('v:val != 4')) call assert_equal(2, d->get('two')) - " Nvim doesn't support Blobs yet; expect a different emsg - " call assert_fails("let x = d->index(2)", 'E897:') - " call assert_fails("let x = d->insert(0)", 'E899:') - call assert_fails("let x = d->index(2)", 'E714:') - call assert_fails("let x = d->insert(0)", 'E686:') + call assert_fails("let x = d->index(2)", 'E897:') + call assert_fails("let x = d->insert(0)", 'E899:') call assert_true(d->has_key('two')) call assert_equal([['one', 1], ['two', 2], ['three', 3]], d->items()) call assert_fails("let x = d->join()", 'E714:') @@ -63,9 +60,7 @@ func Test_dict_method() call assert_equal(2, d->remove("two")) let d.two = 2 call assert_fails('let x = d->repeat(2)', 'E731:') - " Nvim doesn't support Blobs yet; expect a different emsg - " call assert_fails('let x = d->reverse()', 'E899:') - call assert_fails('let x = d->reverse()', 'E686:') + call assert_fails('let x = d->reverse()', 'E899:') call assert_fails('let x = d->sort()', 'E686:') call assert_equal("{'one': 1, 'two': 2, 'three': 3}", d->string()) call assert_equal(v:t_dict, d->type()) diff --git a/src/nvim/testdir/test_quickfix.vim b/src/nvim/testdir/test_quickfix.vim index 283e7bbafe..18587b9b2c 100644 --- a/src/nvim/testdir/test_quickfix.vim +++ b/src/nvim/testdir/test_quickfix.vim @@ -2221,6 +2221,10 @@ func Xproperty_tests(cchar) call g:Xsetlist([], 'a', {'context':246}) let d = g:Xgetlist({'context':1}) call assert_equal(246, d.context) + " set other Vim data types as context + call g:Xsetlist([], 'a', {'context' : v:_null_blob}) + call g:Xsetlist([], 'a', {'context' : ''}) + call test_garbagecollect_now() if a:cchar == 'l' " Test for copying context across two different location lists new | only diff --git a/src/nvim/testdir/test_rename.vim b/src/nvim/testdir/test_rename.vim index e4228188bd..2311caf790 100644 --- a/src/nvim/testdir/test_rename.vim +++ b/src/nvim/testdir/test_rename.vim @@ -95,7 +95,6 @@ func Test_rename_copy() endfunc func Test_rename_fails() - throw 'skipped: TODO: ' call writefile(['foo'], 'Xrenamefile') " Can't rename into a non-existing directory. diff --git a/src/nvim/testdir/test_swap.vim b/src/nvim/testdir/test_swap.vim index 02bc297de1..e3101d4e44 100644 --- a/src/nvim/testdir/test_swap.vim +++ b/src/nvim/testdir/test_swap.vim @@ -168,7 +168,6 @@ func Test_swapname() endfunc func Test_swapfile_delete() - throw 'skipped: need the "blob" feature for this test' autocmd! SwapExists function s:swap_exists() let v:swapchoice = s:swap_choice diff --git a/src/nvim/testdir/test_undo.vim b/src/nvim/testdir/test_undo.vim index 54caed3983..c7dcaa0f36 100644 --- a/src/nvim/testdir/test_undo.vim +++ b/src/nvim/testdir/test_undo.vim @@ -368,7 +368,6 @@ endfunc " Check that reading a truncted undo file doesn't hang. func Test_undofile_truncated() - throw 'skipped: TODO: ' new call setline(1, 'hello') set ul=100 diff --git a/src/nvim/testdir/test_vimscript.vim b/src/nvim/testdir/test_vimscript.vim index d5837e88c9..b18ce563d3 100644 --- a/src/nvim/testdir/test_vimscript.vim +++ b/src/nvim/testdir/test_vimscript.vim @@ -1152,6 +1152,10 @@ func Test_type() call assert_equal(v:t_float, type(0.0)) call assert_equal(v:t_bool, type(v:false)) call assert_equal(v:t_bool, type(v:true)) + call assert_equal(v:t_string, type(v:_null_string)) + call assert_equal(v:t_list, type(v:_null_list)) + call assert_equal(v:t_dict, type(v:_null_dict)) + call assert_equal(v:t_blob, type(v:_null_blob)) endfunc "------------------------------------------------------------------------------- diff --git a/src/nvim/testdir/test_writefile.vim b/src/nvim/testdir/test_writefile.vim index 6922e2185d..2504fcb14e 100644 --- a/src/nvim/testdir/test_writefile.vim +++ b/src/nvim/testdir/test_writefile.vim @@ -17,6 +17,8 @@ func Test_writefile() call assert_equal("morning", l[3]) call assert_equal("vimmers", l[4]) call delete(f) + + call assert_fails('call writefile("text", "Xfile")', 'E475: Invalid argument: writefile() first argument must be a List or a Blob') endfunc func Test_writefile_ignore_regexp_error() diff --git a/src/nvim/vim.h b/src/nvim/vim.h index c719c064e2..d84979f6fe 100644 --- a/src/nvim/vim.h +++ b/src/nvim/vim.h @@ -102,6 +102,7 @@ typedef enum { #define VAR_TYPE_FLOAT 5 #define VAR_TYPE_BOOL 6 #define VAR_TYPE_SPECIAL 7 +#define VAR_TYPE_BLOB 10 // values for xp_context when doing command line completion diff --git a/test/README.md b/test/README.md index 8669ab6f3e..37aa54c157 100644 --- a/test/README.md +++ b/test/README.md @@ -256,11 +256,15 @@ Number; !must be defined to function properly): - `VALGRIND_LOG` (F) (S): overrides valgrind log file name used for `VALGRIND`. +- `TEST_COLORS` (F) (U) (D): enable pretty colors in test runner. + - `TEST_SKIP_FRAGILE` (F) (D): makes test suite skip some fragile tests. - `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/busted/outputHandlers/nvim.lua b/test/busted/outputHandlers/nvim.lua index 191387e1b9..0e9801b94b 100644 --- a/test/busted/outputHandlers/nvim.lua +++ b/test/busted/outputHandlers/nvim.lua @@ -2,8 +2,8 @@ local pretty = require 'pl.pretty' local global_helpers = require('test.helpers') -- Colors are disabled by default. #15610 -local colors = setmetatable({}, {__index = function() return function(s) return s end end}) -if os.getenv "NVIM_COLORS" then +local colors = setmetatable({}, {__index = function() return function(s) return s == nil and '' or tostring(s) end end}) +if os.getenv "TEST_COLORS" then colors = require 'term.colors' end diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 9aad8a1319..b4b2e63fb0 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -117,6 +117,19 @@ describe('API', function() nvim('exec','autocmd BufAdd * :let x1 = "Hello"', false) nvim('command', 'new foo') eq('Hello', request('nvim_eval', 'g:x1')) + + -- Line continuations + nvim('exec', [[ + let abc = #{ + \ a: 1, + "\ b: 2, + \ c: 3 + \ }]], false) + eq({a = 1, c = 3}, request('nvim_eval', 'g:abc')) + + -- try no spaces before continuations to catch off-by-one error + nvim('exec', 'let ab = #{\n\\a: 98,\n"\\ b: 2\n\\}', false) + eq({a = 98}, request('nvim_eval', 'g:ab')) end) it('non-ASCII input', function() diff --git a/test/functional/core/channels_spec.lua b/test/functional/core/channels_spec.lua index 1ef34c7318..6efa4f9b80 100644 --- a/test/functional/core/channels_spec.lua +++ b/test/functional/core/channels_spec.lua @@ -89,6 +89,9 @@ describe('channels', function() command("call chansend(id, 'howdy')") eq({"notification", "stdout", {id, {"[1, ['howdy'], 'stdin']"}}}, next_msg()) + command("call chansend(id, 0z686f6c61)") + eq({"notification", "stdout", {id, {"[1, ['hola'], 'stdin']"}}}, next_msg()) + command("call chanclose(id, 'stdin')") expect_twostreams({{"notification", "stdout", {id, {"[1, [''], 'stdin']"}}}, {'notification', 'stdout', {id, {''}}}}, @@ -131,6 +134,8 @@ describe('channels', function() command("call chansend(id, 'TEXT\n')") expect_twoline(id, "stdout", "TEXT\r", "[1, ['TEXT', ''], 'stdin']") + command("call chansend(id, 0z426c6f6273210a)") + expect_twoline(id, "stdout", "Blobs!\r", "[1, ['Blobs!', ''], 'stdin']") command("call chansend(id, 'neovan')") eq({"notification", "stdout", {id, {"neovan"}}}, next_msg()) diff --git a/test/functional/eval/api_functions_spec.lua b/test/functional/eval/api_functions_spec.lua index 7d09a652ba..d07e74d40e 100644 --- a/test/functional/eval/api_functions_spec.lua +++ b/test/functional/eval/api_functions_spec.lua @@ -155,4 +155,13 @@ describe('eval-API', function() pcall_err(command, "sandbox call nvim_input('ievil')")) eq({''}, meths.buf_get_lines(0, 0, -1, true)) end) + + it('converts blobs to API strings', function() + command('let g:v1 = nvim__id(0z68656c6c6f)') + command('let g:v2 = nvim__id(v:_null_blob)') + eq(1, eval('type(g:v1)')) + eq(1, eval('type(g:v2)')) + eq('hello', eval('g:v1')) + eq('', eval('g:v2')) + end) end) diff --git a/test/functional/eval/execute_spec.lua b/test/functional/eval/execute_spec.lua index f52ac4e59b..fccf52935b 100644 --- a/test/functional/eval/execute_spec.lua +++ b/test/functional/eval/execute_spec.lua @@ -322,16 +322,16 @@ describe('execute()', function() eq('Vim(call):E731: using Dictionary as a String', ret) ret = exc_exec('call execute("echo add(1, 1)", "")') - eq('Vim(echo):E714: List required', ret) + eq('Vim(echo):E897: List or Blob required', ret) ret = exc_exec('call execute(["echon 42", "echo add(1, 1)"], "")') - eq('Vim(echo):E714: List required', ret) + eq('Vim(echo):E897: List or Blob required', ret) ret = exc_exec('call execute("echo add(1, 1)", "silent")') - eq('Vim(echo):E714: List required', ret) + eq('Vim(echo):E897: List or Blob required', ret) ret = exc_exec('call execute(["echon 42", "echo add(1, 1)"], "silent")') - eq('Vim(echo):E714: List required', ret) + eq('Vim(echo):E897: List or Blob required', ret) end) end) end) diff --git a/test/functional/eval/json_functions_spec.lua b/test/functional/eval/json_functions_spec.lua index 8dcaea806e..9b5e207c07 100644 --- a/test/functional/eval/json_functions_spec.lua +++ b/test/functional/eval/json_functions_spec.lua @@ -538,6 +538,11 @@ describe('json_encode() function', function() eq('"þÿþ"', funcs.json_encode('þÿþ')) end) + it('dumps blobs', function() + eq('[]', eval('json_encode(0z)')) + eq('[222, 173, 190, 239]', eval('json_encode(0zDEADBEEF)')) + end) + it('dumps numbers', function() eq('0', funcs.json_encode(0)) eq('10', funcs.json_encode(10)) @@ -769,6 +774,10 @@ describe('json_encode() function', function() eq('""', eval('json_encode($XXX_UNEXISTENT_VAR_XXX)')) end) + it('can dump NULL blob', function() + eq('[]', eval('json_encode(v:_null_blob)')) + end) + it('can dump NULL list', function() eq('[]', eval('json_encode(v:_null_list)')) end) diff --git a/test/functional/eval/msgpack_functions_spec.lua b/test/functional/eval/msgpack_functions_spec.lua index a8a413f68b..837b629858 100644 --- a/test/functional/eval/msgpack_functions_spec.lua +++ b/test/functional/eval/msgpack_functions_spec.lua @@ -13,6 +13,7 @@ describe('msgpack*() functions', function() it(msg, function() nvim('set_var', 'obj', obj) eq(obj, eval('msgpackparse(msgpackdump(g:obj))')) + eq(obj, eval('msgpackparse(msgpackdump(g:obj, "B"))')) end) end @@ -364,8 +365,7 @@ describe('msgpack*() functions', function() command('let dumped = ["\\xC4\\x01\\n"]') command('let parsed = msgpackparse(dumped)') command('let dumped2 = msgpackdump(parsed)') - eq({{_TYPE={}, _VAL={'\n'}}}, eval('parsed')) - eq(1, eval('parsed[0]._TYPE is v:msgpack_types.binary')) + eq({'\000'}, eval('parsed')) eq(1, eval('dumped ==# dumped2')) end) @@ -392,56 +392,61 @@ describe('msgpack*() functions', function() end) end) +local blobstr = function(list) + local l = {} + for i,v in ipairs(list) do + l[i] = v:gsub('\n', '\000') + end + return table.concat(l, '\n') +end + +-- Test msgpackparse() with a readfile()-style list and a blob argument +local parse_eq = function(expect, list_arg) + local blob_expr = '0z' .. blobstr(list_arg):gsub('(.)', function(c) + return ('%.2x'):format(c:byte()) + end) + eq(expect, funcs.msgpackparse(list_arg)) + command('let g:parsed = msgpackparse(' .. blob_expr .. ')') + eq(expect, eval('g:parsed')) +end + describe('msgpackparse() function', function() before_each(clear) it('restores nil as v:null', function() - command('let dumped = ["\\xC0"]') - command('let parsed = msgpackparse(dumped)') - eq('[v:null]', eval('string(parsed)')) + parse_eq(eval('[v:null]'), {'\192'}) end) it('restores boolean false as v:false', function() - command('let dumped = ["\\xC2"]') - command('let parsed = msgpackparse(dumped)') - eq({false}, eval('parsed')) + parse_eq({false}, {'\194'}) end) it('restores boolean true as v:true', function() - command('let dumped = ["\\xC3"]') - command('let parsed = msgpackparse(dumped)') - eq({true}, eval('parsed')) + parse_eq({true}, {'\195'}) end) it('restores FIXSTR as special dict', function() - command('let dumped = ["\\xa2ab"]') - command('let parsed = msgpackparse(dumped)') - eq({{_TYPE={}, _VAL={'ab'}}}, eval('parsed')) + parse_eq({{_TYPE={}, _VAL={'ab'}}}, {'\162ab'}) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.string')) end) it('restores BIN 8 as string', function() - command('let dumped = ["\\xC4\\x02ab"]') - eq({'ab'}, eval('msgpackparse(dumped)')) + parse_eq({'ab'}, {'\196\002ab'}) end) it('restores FIXEXT1 as special dictionary', function() - command('let dumped = ["\\xD4\\x10", ""]') - command('let parsed = msgpackparse(dumped)') - eq({{_TYPE={}, _VAL={0x10, {"", ""}}}}, eval('parsed')) + parse_eq({{_TYPE={}, _VAL={0x10, {"", ""}}}}, {'\212\016', ''}) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.ext')) end) it('restores MAP with BIN key as special dictionary', function() - command('let dumped = ["\\x81\\xC4\\x01a\\xC4\\n"]') - command('let parsed = msgpackparse(dumped)') - eq({{_TYPE={}, _VAL={{'a', ''}}}}, eval('parsed')) + parse_eq({{_TYPE={}, _VAL={{'a', ''}}}}, {'\129\196\001a\196\n'}) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) end) it('restores MAP with duplicate STR keys as special dictionary', function() command('let dumped = ["\\x82\\xA1a\\xC4\\n\\xA1a\\xC4\\n"]') - -- FIXME Internal error bug + -- FIXME Internal error bug, can't use parse_eq() here command('silent! let parsed = msgpackparse(dumped)') eq({{_TYPE={}, _VAL={ {{_TYPE={}, _VAL={'a'}}, ''}, {{_TYPE={}, _VAL={'a'}}, ''}}} }, eval('parsed')) @@ -451,9 +456,7 @@ describe('msgpackparse() function', function() end) it('restores MAP with MAP key as special dictionary', function() - command('let dumped = ["\\x81\\x80\\xC4\\n"]') - command('let parsed = msgpackparse(dumped)') - eq({{_TYPE={}, _VAL={{{}, ''}}}}, eval('parsed')) + parse_eq({{_TYPE={}, _VAL={{{}, ''}}}}, {'\129\128\196\n'}) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) end) @@ -478,43 +481,65 @@ describe('msgpackparse() function', function() end) it('fails to parse a string', function() - eq('Vim(call):E686: Argument of msgpackparse() must be a List', + eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob', exc_exec('call msgpackparse("abcdefghijklmnopqrstuvwxyz")')) end) it('fails to parse a number', function() - eq('Vim(call):E686: Argument of msgpackparse() must be a List', + eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob', exc_exec('call msgpackparse(127)')) end) it('fails to parse a dictionary', function() - eq('Vim(call):E686: Argument of msgpackparse() must be a List', + eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob', exc_exec('call msgpackparse({})')) end) it('fails to parse a funcref', function() - eq('Vim(call):E686: Argument of msgpackparse() must be a List', + eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob', exc_exec('call msgpackparse(function("tr"))')) end) it('fails to parse a partial', function() command('function T() dict\nendfunction') - eq('Vim(call):E686: Argument of msgpackparse() must be a List', + eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob', exc_exec('call msgpackparse(function("T", [1, 2], {}))')) end) it('fails to parse a float', function() - eq('Vim(call):E686: Argument of msgpackparse() must be a List', + eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob', exc_exec('call msgpackparse(0.0)')) end) + + it('fails on incomplete msgpack string', function() + local expected = 'Vim(call):E475: Invalid argument: Incomplete msgpack string' + eq(expected, exc_exec([[call msgpackparse(["\xc4"])]])) + eq(expected, exc_exec([[call msgpackparse(["\xca", "\x02\x03"])]])) + eq(expected, exc_exec('call msgpackparse(0zc4)')) + eq(expected, exc_exec('call msgpackparse(0zca0a0203)')) + end) + + it('fails when unable to parse msgpack string', function() + local expected = 'Vim(call):E475: Invalid argument: Failed to parse msgpack string' + eq(expected, exc_exec([[call msgpackparse(["\xc1"])]])) + eq(expected, exc_exec('call msgpackparse(0zc1)')) + end) end) describe('msgpackdump() function', function() before_each(clear) + local dump_eq = function(exp_list, arg_expr) + eq(exp_list, eval('msgpackdump(' .. arg_expr .. ')')) + eq(blobstr(exp_list), eval('msgpackdump(' .. arg_expr .. ', "B")')) + end + it('dumps string as BIN 8', function() - nvim('set_var', 'obj', {'Test'}) - eq({"\196\004Test"}, eval('msgpackdump(obj)')) + dump_eq({'\196\004Test'}, '["Test"]') + end) + + it('dumps blob as BIN 8', function() + dump_eq({'\196\005Bl\nb!'}, '[0z426c006221]') end) it('can dump generic mapping with generic mapping keys and values', function() @@ -522,56 +547,56 @@ describe('msgpackdump() function', function() command('let todumpv1 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') command('let todumpv2 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') command('call add(todump._VAL, [todumpv1, todumpv2])') - eq({'\129\128\128'}, eval('msgpackdump([todump])')) + dump_eq({'\129\128\128'}, '[todump]') end) it('can dump v:true', function() - eq({'\195'}, funcs.msgpackdump({true})) + dump_eq({'\195'}, '[v:true]') end) it('can dump v:false', function() - eq({'\194'}, funcs.msgpackdump({false})) + dump_eq({'\194'}, '[v:false]') end) - it('can v:null', function() - command('let todump = v:null') + it('can dump v:null', function() + dump_eq({'\192'}, '[v:null]') end) it('can dump special bool mapping (true)', function() command('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 1}') - eq({'\195'}, eval('msgpackdump([todump])')) + dump_eq({'\195'}, '[todump]') end) it('can dump special bool mapping (false)', function() command('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 0}') - eq({'\194'}, eval('msgpackdump([todump])')) + dump_eq({'\194'}, '[todump]') end) it('can dump special nil mapping', function() command('let todump = {"_TYPE": v:msgpack_types.nil, "_VAL": 0}') - eq({'\192'}, eval('msgpackdump([todump])')) + dump_eq({'\192'}, '[todump]') end) it('can dump special ext mapping', function() command('let todump = {"_TYPE": v:msgpack_types.ext, "_VAL": [5, ["",""]]}') - eq({'\212\005', ''}, eval('msgpackdump([todump])')) + dump_eq({'\212\005', ''}, '[todump]') end) it('can dump special array mapping', function() command('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": [5, [""]]}') - eq({'\146\005\145\196\n'}, eval('msgpackdump([todump])')) + dump_eq({'\146\005\145\196\n'}, '[todump]') end) it('can dump special UINT64_MAX mapping', function() command('let todump = {"_TYPE": v:msgpack_types.integer}') command('let todump._VAL = [1, 3, 0x7FFFFFFF, 0x7FFFFFFF]') - eq({'\207\255\255\255\255\255\255\255\255'}, eval('msgpackdump([todump])')) + dump_eq({'\207\255\255\255\255\255\255\255\255'}, '[todump]') end) it('can dump special INT64_MIN mapping', function() command('let todump = {"_TYPE": v:msgpack_types.integer}') command('let todump._VAL = [-1, 2, 0, 0]') - eq({'\211\128\n\n\n\n\n\n\n'}, eval('msgpackdump([todump])')) + dump_eq({'\211\128\n\n\n\n\n\n\n'}, '[todump]') end) it('fails to dump a function reference', function() @@ -610,13 +635,13 @@ describe('msgpackdump() function', function() it('can dump dict with two same dicts inside', function() command('let inter = {}') command('let todump = {"a": inter, "b": inter}') - eq({"\130\161a\128\161b\128"}, eval('msgpackdump([todump])')) + dump_eq({"\130\161a\128\161b\128"}, '[todump]') end) it('can dump list with two same lists inside', function() command('let inter = []') command('let todump = [inter, inter]') - eq({"\146\144\144"}, eval('msgpackdump([todump])')) + dump_eq({"\146\144\144"}, '[todump]') end) it('fails to dump a recursive list in a special dict', function() @@ -667,9 +692,9 @@ describe('msgpackdump() function', function() exc_exec('call msgpackdump()')) end) - it('fails when called with two arguments', function() + it('fails when called with three arguments', function() eq('Vim(call):E118: Too many arguments for function: msgpackdump', - exc_exec('call msgpackdump(["", ""], 1)')) + exc_exec('call msgpackdump(["", ""], 1, 2)')) end) it('fails to dump a string', function() @@ -711,9 +736,13 @@ describe('msgpackdump() function', function() end) it('can dump NULL string', function() - eq({'\196\n'}, eval('msgpackdump([$XXX_UNEXISTENT_VAR_XXX])')) - eq({'\196\n'}, eval('msgpackdump([{"_TYPE": v:msgpack_types.binary, "_VAL": [$XXX_UNEXISTENT_VAR_XXX]}])')) - eq({'\160'}, eval('msgpackdump([{"_TYPE": v:msgpack_types.string, "_VAL": [$XXX_UNEXISTENT_VAR_XXX]}])')) + dump_eq({'\196\n'}, '[$XXX_UNEXISTENT_VAR_XXX]') + dump_eq({'\196\n'}, '[{"_TYPE": v:msgpack_types.binary, "_VAL": [$XXX_UNEXISTENT_VAR_XXX]}]') + dump_eq({'\160'}, '[{"_TYPE": v:msgpack_types.string, "_VAL": [$XXX_UNEXISTENT_VAR_XXX]}]') + end) + + it('can dump NULL blob', function() + eq({'\196\n'}, eval('msgpackdump([v:_null_blob])')) end) it('can dump NULL list', function() diff --git a/test/functional/eval/null_spec.lua b/test/functional/eval/null_spec.lua index f866aca3ed..bc88e6c8b3 100644 --- a/test/functional/eval/null_spec.lua +++ b/test/functional/eval/null_spec.lua @@ -44,7 +44,7 @@ describe('NULL', function() -- Incorrect behaviour -- FIXME Should error out with different message null_test('makes :unlet act as if it is not a list', ':unlet L[0]', - 'Vim(unlet):E689: Can only index a List or Dictionary') + 'Vim(unlet):E689: Can only index a List, Dictionary or Blob') -- Subjectable behaviour diff --git a/test/functional/eval/writefile_spec.lua b/test/functional/eval/writefile_spec.lua index 356680ba7c..14be8c377c 100644 --- a/test/functional/eval/writefile_spec.lua +++ b/test/functional/eval/writefile_spec.lua @@ -119,7 +119,7 @@ describe('writefile()', function() eq('\nE118: Too many arguments for function: writefile', redir_exec(('call writefile([], "%s", "b", 1)'):format(fname))) for _, arg in ipairs({'0', '0.0', 'function("tr")', '{}', '"test"'}) do - eq('\nE686: Argument of writefile() must be a List', + eq('\nE475: Invalid argument: writefile() first argument must be a List or a Blob', redir_exec(('call writefile(%s, "%s", "b")'):format(arg, fname))) end for _, args in ipairs({'[], %s, "b"', '[], "' .. fname .. '", %s'}) do diff --git a/test/functional/ex_cmds/source_spec.lua b/test/functional/ex_cmds/source_spec.lua index 37c97f519a..bdf6ae76d1 100644 --- a/test/functional/ex_cmds/source_spec.lua +++ b/test/functional/ex_cmds/source_spec.lua @@ -8,6 +8,8 @@ local feed = helpers.feed local feed_command = helpers.feed_command local write_file = helpers.write_file local exec = helpers.exec +local exc_exec = helpers.exc_exec +local exec_lua = helpers.exec_lua local eval = helpers.eval local exec_capture = helpers.exec_capture local neq = helpers.neq @@ -18,16 +20,30 @@ describe(':source', function() end) it('current buffer', function() - insert('let a = 2') + insert([[ + let a = 2 + let b = #{ + \ k: "v" + "\ (o_o) + \ }]]) + command('source') eq('2', meths.exec('echo a', true)) + eq("{'k': 'v'}", meths.exec('echo b', true)) + + exec('set cpoptions+=C') + eq('Vim(let):E15: Invalid expression: #{', exc_exec('source')) end) it('selection in current buffer', function() - insert( - 'let a = 2\n'.. - 'let a = 3\n'.. - 'let a = 4\n') + insert([[ + let a = 2 + let a = 3 + let a = 4 + let b = #{ + "\ (>_<) + \ K: "V" + \ }]]) -- Source the 2nd line only feed('ggjV') @@ -38,13 +54,26 @@ describe(':source', function() feed('ggjVG') feed_command(':source') eq('4', meths.exec('echo a', true)) + eq("{'K': 'V'}", meths.exec('echo b', true)) + + exec('set cpoptions+=C') + eq('Vim(let):E15: Invalid expression: #{', exc_exec("'<,'>source")) + end) + + it('does not break if current buffer is modified while sourced', function() + insert [[ + bwipeout! + let a = 123 + ]] + command('source') + eq('123', meths.exec('echo a', true)) end) it('multiline heredoc command', function() - insert( - 'lua << EOF\n'.. - 'y = 4\n'.. - 'EOF\n') + insert([[ + lua << EOF + y = 4 + EOF]]) command('source') eq('4', meths.exec('echo luaeval("y")', true)) @@ -67,13 +96,21 @@ describe(':source', function() vim.g.b = 5 vim.g.b = 6 vim.g.b = 7 + a = [=[ + "\ a + \ b]=] ]]) command('edit '..test_file) + feed('ggjV') feed_command(':source') - eq(6, eval('g:b')) + + feed('GVkk') + feed_command(':source') + eq(' "\\ a\n \\ b', exec_lua('return _G.a')) + os.remove(test_file) end) @@ -84,12 +121,16 @@ describe(':source', function() vim.g.c = 10 vim.g.c = 11 vim.g.c = 12 + a = [=[ + \ 1 + "\ 2]=] ]]) command('edit '..test_file) feed_command(':source') eq(12, eval('g:c')) + eq(' \\ 1\n "\\ 2', exec_lua('return _G.a')) os.remove(test_file) end) diff --git a/test/functional/lua/api_spec.lua b/test/functional/lua/api_spec.lua index 896554f7a3..fdf79d55b2 100644 --- a/test/functional/lua/api_spec.lua +++ b/test/functional/lua/api_spec.lua @@ -15,7 +15,7 @@ describe('luaeval(vim.api.…)', function() describe('nvim_buf_get_lines', function() it('works', function() funcs.setline(1, {"abc", "def", "a\nb", "ttt"}) - eq({{_TYPE={}, _VAL={'a\nb'}}}, + eq({'a\000b'}, funcs.luaeval('vim.api.nvim_buf_get_lines(1, 2, 3, false)')) end) end) @@ -23,7 +23,7 @@ describe('luaeval(vim.api.…)', function() it('works', function() funcs.setline(1, {"abc", "def", "a\nb", "ttt"}) eq(NIL, funcs.luaeval('vim.api.nvim_buf_set_lines(1, 1, 2, false, {"b\\0a"})')) - eq({'abc', {_TYPE={}, _VAL={'b\na'}}, {_TYPE={}, _VAL={'a\nb'}}, 'ttt'}, + eq({'abc', 'b\000a', 'a\000b', 'ttt'}, funcs.luaeval('vim.api.nvim_buf_get_lines(1, 0, 4, false)')) end) end) @@ -64,15 +64,18 @@ describe('luaeval(vim.api.…)', function() it('correctly converts from API objects', function() eq(1, funcs.luaeval('vim.api.nvim_eval("1")')) eq('1', funcs.luaeval([[vim.api.nvim_eval('"1"')]])) + eq('Blobby', funcs.luaeval('vim.api.nvim_eval("0z426c6f626279")')) eq({}, funcs.luaeval('vim.api.nvim_eval("[]")')) eq({}, funcs.luaeval('vim.api.nvim_eval("{}")')) eq(1, funcs.luaeval('vim.api.nvim_eval("1.0")')) + eq('\000', funcs.luaeval('vim.api.nvim_eval("0z00")')) eq(true, funcs.luaeval('vim.api.nvim_eval("v:true")')) eq(false, funcs.luaeval('vim.api.nvim_eval("v:false")')) eq(NIL, funcs.luaeval('vim.api.nvim_eval("v:null")')) eq(0, eval([[type(luaeval('vim.api.nvim_eval("1")'))]])) eq(1, eval([[type(luaeval('vim.api.nvim_eval("''1''")'))]])) + eq(1, eval([[type(luaeval('vim.api.nvim_eval("0zbeef")'))]])) eq(3, eval([[type(luaeval('vim.api.nvim_eval("[]")'))]])) eq(4, eval([[type(luaeval('vim.api.nvim_eval("{}")'))]])) eq(5, eval([[type(luaeval('vim.api.nvim_eval("1.0")'))]])) 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", "<esc>") + 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", "<esc>") + 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", "<esc>") + 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", "<esc>") + 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", "<esc>") + 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", "<esc>") + 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. <msg> + 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. <msg> + 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", "<esc>") + + -- 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/lua/luaeval_spec.lua b/test/functional/lua/luaeval_spec.lua index 8ef77faa0f..255e99032f 100644 --- a/test/functional/lua/luaeval_spec.lua +++ b/test/functional/lua/luaeval_spec.lua @@ -63,11 +63,10 @@ describe('luaeval()', function() eq('\n[[...@0]]', funcs.execute('echo luaeval("l")')) end) end) - describe('strings', function() - it('are successfully converted to special dictionaries', function() + describe('strings with NULs', function() + it('are successfully converted to blobs', function() command([[let s = luaeval('"\0"')]]) - eq({_TYPE={}, _VAL={'\n'}}, meths.get_var('s')) - eq(1, funcs.eval('s._TYPE is v:msgpack_types.binary')) + eq('\000', meths.get_var('s')) end) it('are successfully converted to special dictionaries in table keys', function() @@ -76,13 +75,10 @@ describe('luaeval()', function() eq(1, funcs.eval('d._TYPE is v:msgpack_types.map')) eq(1, funcs.eval('d._VAL[0][0]._TYPE is v:msgpack_types.string')) end) - it('are successfully converted to special dictionaries from a list', + it('are successfully converted to blobs from a list', function() command([[let l = luaeval('{"abc", "a\0b", "c\0d", "def"}')]]) - eq({'abc', {_TYPE={}, _VAL={'a\nb'}}, {_TYPE={}, _VAL={'c\nd'}}, 'def'}, - meths.get_var('l')) - eq(1, funcs.eval('l[1]._TYPE is v:msgpack_types.binary')) - eq(1, funcs.eval('l[2]._TYPE is v:msgpack_types.binary')) + eq({'abc', 'a\000b', 'c\000d', 'def'}, meths.get_var('l')) end) end) @@ -100,9 +96,9 @@ describe('luaeval()', function() eq(1, eval('type(luaeval("\'test\'"))')) eq('', funcs.luaeval('""')) - eq({_TYPE={}, _VAL={'\n'}}, funcs.luaeval([['\0']])) - eq({_TYPE={}, _VAL={'\n', '\n'}}, funcs.luaeval([['\0\n\0']])) - eq(1, eval([[luaeval('"\0\n\0"')._TYPE is v:msgpack_types.binary]])) + eq('\000', funcs.luaeval([['\0']])) + eq('\000\n\000', funcs.luaeval([['\0\n\0']])) + eq(10, eval([[type(luaeval("'\\0\\n\\0'"))]])) eq(true, funcs.luaeval('true')) eq(false, funcs.luaeval('false')) @@ -122,12 +118,11 @@ describe('luaeval()', function() local level = 30 eq(nested_by_level[level].o, funcs.luaeval(nested_by_level[level].s)) - eq({_TYPE={}, _VAL={{{_TYPE={}, _VAL={'\n', '\n'}}, {_TYPE={}, _VAL={'\n', '\n\n'}}}}}, + eq({_TYPE={}, _VAL={{{_TYPE={}, _VAL={'\n', '\n'}}, '\000\n\000\000'}}}, funcs.luaeval([[{['\0\n\0']='\0\n\0\0'}]])) eq(1, eval([[luaeval('{["\0\n\0"]="\0\n\0\0"}')._TYPE is v:msgpack_types.map]])) eq(1, eval([[luaeval('{["\0\n\0"]="\0\n\0\0"}')._VAL[0][0]._TYPE is v:msgpack_types.string]])) - eq(1, eval([[luaeval('{["\0\n\0"]="\0\n\0\0"}')._VAL[0][1]._TYPE is v:msgpack_types.binary]])) - eq({nested={{_TYPE={}, _VAL={{{_TYPE={}, _VAL={'\n', '\n'}}, {_TYPE={}, _VAL={'\n', '\n\n'}}}}}}}, + eq({nested={{_TYPE={}, _VAL={{{_TYPE={}, _VAL={'\n', '\n'}}, '\000\n\000\000'}}}}}, funcs.luaeval([[{nested={{['\0\n\0']='\0\n\0\0'}}}]])) end) @@ -175,8 +170,8 @@ describe('luaeval()', function() end it('correctly passes special dictionaries', function() - eq({'binary', {'\n', '\n'}}, luaevalarg(sp('binary', '["\\n", "\\n"]'))) - eq({'binary', {'\n', '\n'}}, luaevalarg(sp('string', '["\\n", "\\n"]'))) + eq({0, '\000\n\000'}, luaevalarg(sp('binary', '["\\n", "\\n"]'))) + eq({0, '\000\n\000'}, luaevalarg(sp('string', '["\\n", "\\n"]'))) eq({0, true}, luaevalarg(sp('boolean', 1))) eq({0, false}, luaevalarg(sp('boolean', 0))) eq({0, NIL}, luaevalarg(sp('nil', 0))) @@ -458,6 +453,9 @@ describe('v:lua', function() function mymod.crashy() nonexistent() end + function mymod.whatis(value) + return type(value) .. ": " .. tostring(value) + end function mymod.omni(findstart, base) if findstart == 1 then return 5 @@ -476,6 +474,8 @@ describe('v:lua', function() eq(true, exec_lua([[return _G.val == vim.NIL]])) eq(NIL, eval('v:lua.mymod.noisy("eval")')) eq("hey eval", meths.get_current_line()) + eq("string: abc", eval('v:lua.mymod.whatis(0z616263)')) + eq("string: ", eval('v:lua.mymod.whatis(v:_null_blob)')) eq("Vim:E5108: Error executing lua [string \"<nvim>\"]:0: attempt to call global 'nonexistent' (a nil value)", pcall_err(eval, 'v:lua.mymod.crashy()')) diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 2bedbd1453..a066cfbc10 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -739,7 +739,7 @@ describe('lua stdlib', function() eq({NIL, NIL}, exec_lua([[return vim.fn.Nilly()]])) -- error handling - eq({false, 'Vim:E714: List required'}, exec_lua([[return {pcall(vim.fn.add, "aa", "bb")}]])) + eq({false, 'Vim:E897: List or Blob required'}, exec_lua([[return {pcall(vim.fn.add, "aa", "bb")}]])) end) it('vim.fn should error when calling API function', function() 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([[ diff --git a/test/functional/shada/errors_spec.lua b/test/functional/shada/errors_spec.lua index 77a41caec7..ebfd73cf85 100644 --- a/test/functional/shada/errors_spec.lua +++ b/test/functional/shada/errors_spec.lua @@ -342,6 +342,11 @@ describe('ShaDa error handling', function() eq('Vim(rshada):E575: Error while reading ShaDa file: variable entry at position 0 has wrong variable name type', exc_exec(sdrcmd())) end) + it('fails on variable item with BIN value and type value != VAR_TYPE_BLOB', function() + wshada('\006\000\007\147\196\001\065\196\000\000') + eq('Vim(rshada):E575: Error while reading ShaDa file: variable entry at position 0 has wrong variable type', exc_exec(sdrcmd())) + end) + it('fails on replacement item with NIL value', function() wshada('\003\000\001\192') eq('Vim(rshada):E575: Error while reading ShaDa file: sub string entry at position 0 is not an array', exc_exec(sdrcmd())) diff --git a/test/functional/shada/variables_spec.lua b/test/functional/shada/variables_spec.lua index cc0e7fa537..854add1363 100644 --- a/test/functional/shada/variables_spec.lua +++ b/test/functional/shada/variables_spec.lua @@ -1,7 +1,7 @@ -- ShaDa variables saving/reading support local helpers = require('test.functional.helpers')(after_each) -local meths, funcs, nvim_command, eq = - helpers.meths, helpers.funcs, helpers.command, helpers.eq +local meths, funcs, nvim_command, eq, eval = + helpers.meths, helpers.funcs, helpers.command, helpers.eq, helpers.eval local shada_helpers = require('test.functional.shada.helpers') local reset, clear = shada_helpers.reset, shada_helpers.clear @@ -30,10 +30,12 @@ describe('ShaDa support code', function() else meths.set_var(varname, varval) end + local vartype = eval('type(g:' .. varname .. ')') -- Exit during `reset` is not a regular exit: it does not write shada -- automatically nvim_command('qall') reset('set shada+=!') + eq(vartype, eval('type(g:' .. varname .. ')')) eq(varval, meths.get_var(varname)) end) end @@ -47,6 +49,8 @@ describe('ShaDa support code', function() autotest('false', 'FALSEVAR', false) autotest('null', 'NULLVAR', 'v:null', true) autotest('ext', 'EXTVAR', '{"_TYPE": v:msgpack_types.ext, "_VAL": [2, ["", ""]]}', true) + autotest('blob', 'BLOBVAR', '0z12ab34cd', true) + autotest('blob (with NULs)', 'BLOBVARNULS', '0z004e554c7300', true) it('does not read back variables without `!` in &shada', function() meths.set_var('STRVAR', 'foo') diff --git a/test/helpers.lua b/test/helpers.lua index 469aee53f0..499d68b0d6 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -70,7 +70,7 @@ local function dumplog(logfile, fn, ...) if status == false then logfile = logfile or os.getenv('NVIM_LOG_FILE') or '.nvimlog' local logtail = module.read_nvim_log(logfile) - error(string.format('%s\n%s', rv, logtail)) + error(string.format('%s\n%s', tostring(rv), logtail)) end end function module.eq(expected, actual, context, logfile) diff --git a/test/unit/eval/typval_spec.lua b/test/unit/eval/typval_spec.lua index d81e272877..e61b568f3a 100644 --- a/test/unit/eval/typval_spec.lua +++ b/test/unit/eval/typval_spec.lua @@ -2531,7 +2531,7 @@ describe('typval.c', function() value='tr', dict={}, }) - lib.tv_item_lock(p_tv, -1, true) + lib.tv_item_lock(p_tv, -1, true, false) eq(lib.VAR_UNLOCKED, p_tv.vval.v_partial.pt_dict.dv_lock) end) itp('does not change VAR_FIXED values', function() @@ -2542,14 +2542,14 @@ describe('typval.c', function() d_tv.vval.v_dict.dv_lock = lib.VAR_FIXED l_tv.v_lock = lib.VAR_FIXED l_tv.vval.v_list.lv_lock = lib.VAR_FIXED - lib.tv_item_lock(d_tv, 1, true) - lib.tv_item_lock(l_tv, 1, true) + lib.tv_item_lock(d_tv, 1, true, false) + lib.tv_item_lock(l_tv, 1, true, false) eq(lib.VAR_FIXED, d_tv.v_lock) eq(lib.VAR_FIXED, l_tv.v_lock) eq(lib.VAR_FIXED, d_tv.vval.v_dict.dv_lock) eq(lib.VAR_FIXED, l_tv.vval.v_list.lv_lock) - lib.tv_item_lock(d_tv, 1, false) - lib.tv_item_lock(l_tv, 1, false) + lib.tv_item_lock(d_tv, 1, false, false) + lib.tv_item_lock(l_tv, 1, false, false) eq(lib.VAR_FIXED, d_tv.v_lock) eq(lib.VAR_FIXED, l_tv.v_lock) eq(lib.VAR_FIXED, d_tv.vval.v_dict.dv_lock) @@ -2561,9 +2561,9 @@ describe('typval.c', function() local d_tv = lua2typvalt(null_dict) local s_tv = lua2typvalt(null_string) alloc_log:clear() - lib.tv_item_lock(l_tv, 1, true) - lib.tv_item_lock(d_tv, 1, true) - lib.tv_item_lock(s_tv, 1, true) + lib.tv_item_lock(l_tv, 1, true, false) + lib.tv_item_lock(d_tv, 1, true, false) + lib.tv_item_lock(s_tv, 1, true, false) eq(null_list, typvalt2lua(l_tv)) eq(null_dict, typvalt2lua(d_tv)) eq(null_string, typvalt2lua(s_tv)) |