aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE10
-rw-r--r--MAINTAIN.md13
-rw-r--r--runtime/autoload/man.vim1
-rw-r--r--runtime/doc/deprecated.txt44
-rw-r--r--runtime/doc/diagnostic.txt483
-rw-r--r--runtime/doc/eval.txt498
-rw-r--r--runtime/doc/if_perl.txt3
-rw-r--r--runtime/doc/lsp.txt594
-rw-r--r--runtime/doc/lua.txt228
-rw-r--r--runtime/doc/repeat.txt11
-rw-r--r--runtime/doc/vim_diff.txt1
-rw-r--r--runtime/lua/vim/diagnostic.lua1150
-rw-r--r--runtime/lua/vim/lsp.lua13
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua1477
-rw-r--r--runtime/lua/vim/lsp/handlers.lua14
-rw-r--r--runtime/lua/vim/lsp/log.lua18
-rw-r--r--runtime/lua/vim/lsp/rpc.lua4
-rw-r--r--runtime/lua/vim/lsp/util.lua51
-rw-r--r--runtime/plugin/diagnostic.vim48
-rwxr-xr-xscripts/gen_vimdoc.py19
-rw-r--r--src/nvim/api/private/helpers.c11
-rw-r--r--src/nvim/channel.c1
-rw-r--r--src/nvim/charset.c34
-rw-r--r--src/nvim/debugger.c833
-rw-r--r--src/nvim/debugger.h11
-rw-r--r--src/nvim/eval.c394
-rw-r--r--src/nvim/eval.h3
-rw-r--r--src/nvim/eval.lua2
-rw-r--r--src/nvim/eval/decode.c42
-rw-r--r--src/nvim/eval/encode.c65
-rw-r--r--src/nvim/eval/executor.c14
-rw-r--r--src/nvim/eval/funcs.c414
-rw-r--r--src/nvim/eval/typval.c173
-rw-r--r--src/nvim/eval/typval.h69
-rw-r--r--src/nvim/eval/typval_encode.c.h12
-rw-r--r--src/nvim/eval/userfunc.c1
-rw-r--r--src/nvim/ex_cmds2.c951
-rw-r--r--src/nvim/ex_docmd.c1
-rw-r--r--src/nvim/ex_eval.c1
-rw-r--r--src/nvim/globals.h5
-rw-r--r--src/nvim/lua/converter.c9
-rw-r--r--src/nvim/lua/executor.c2
-rw-r--r--src/nvim/lua/vim.lua3
-rw-r--r--src/nvim/shada.c40
-rw-r--r--src/nvim/testdir/test_blob.vim349
-rw-r--r--src/nvim/testdir/test_const.vim33
-rw-r--r--src/nvim/testdir/test_debugger.vim11
-rw-r--r--src/nvim/testdir/test_eval_stuff.vim119
-rw-r--r--src/nvim/testdir/test_filter_map.vim6
-rw-r--r--src/nvim/testdir/test_fnamemodify.vim4
-rw-r--r--src/nvim/testdir/test_listdict.vim4
-rw-r--r--src/nvim/testdir/test_method.vim11
-rw-r--r--src/nvim/testdir/test_quickfix.vim4
-rw-r--r--src/nvim/testdir/test_rename.vim1
-rw-r--r--src/nvim/testdir/test_swap.vim1
-rw-r--r--src/nvim/testdir/test_undo.vim1
-rw-r--r--src/nvim/testdir/test_vimscript.vim4
-rw-r--r--src/nvim/testdir/test_writefile.vim2
-rw-r--r--src/nvim/vim.h1
-rw-r--r--test/README.md4
-rw-r--r--test/busted/outputHandlers/nvim.lua4
-rw-r--r--test/functional/api/vim_spec.lua13
-rw-r--r--test/functional/core/channels_spec.lua5
-rw-r--r--test/functional/eval/api_functions_spec.lua9
-rw-r--r--test/functional/eval/execute_spec.lua8
-rw-r--r--test/functional/eval/json_functions_spec.lua9
-rw-r--r--test/functional/eval/msgpack_functions_spec.lua135
-rw-r--r--test/functional/eval/null_spec.lua2
-rw-r--r--test/functional/eval/writefile_spec.lua2
-rw-r--r--test/functional/ex_cmds/source_spec.lua61
-rw-r--r--test/functional/lua/api_spec.lua7
-rw-r--r--test/functional/lua/diagnostic_spec.lua831
-rw-r--r--test/functional/lua/luaeval_spec.lua34
-rw-r--r--test/functional/lua/vim_spec.lua2
-rw-r--r--test/functional/plugin/lsp/diagnostic_spec.lua114
-rw-r--r--test/functional/plugin/lsp_spec.lua25
-rw-r--r--test/functional/shada/errors_spec.lua5
-rw-r--r--test/functional/shada/variables_spec.lua8
-rw-r--r--test/helpers.lua2
-rw-r--r--test/unit/eval/typval_spec.lua16
80 files changed, 6350 insertions, 3268 deletions
diff --git a/LICENSE b/LICENSE
index 9e241ef0c3..eabb27f5cd 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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))