aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt27
-rw-r--r--cmake.deps/CMakeLists.txt19
-rw-r--r--cmake.deps/deps.txt5
-rw-r--r--cmake/Deps.cmake26
-rw-r--r--runtime/doc/deprecated.txt19
-rw-r--r--runtime/doc/diagnostic.txt109
-rw-r--r--runtime/doc/lsp.txt26
-rw-r--r--runtime/doc/lua.txt24
-rw-r--r--runtime/doc/news-0.10.txt2
-rw-r--r--runtime/doc/news.txt21
-rw-r--r--runtime/doc/tagsrch.txt16
-rw-r--r--runtime/doc/vim_diff.txt2
-rw-r--r--runtime/lua/vim/_defaults.lua16
-rw-r--r--runtime/lua/vim/_editor.lua3
-rw-r--r--runtime/lua/vim/diagnostic.lua224
-rw-r--r--runtime/lua/vim/lsp.lua5
-rw-r--r--runtime/lua/vim/lsp/_completion.lua276
-rw-r--r--runtime/lua/vim/lsp/buf.lua2
-rw-r--r--runtime/lua/vim/lsp/client.lua23
-rw-r--r--runtime/lua/vim/lsp/completion.lua734
-rw-r--r--runtime/lua/vim/lsp/handlers.lua2
-rw-r--r--runtime/lua/vim/lsp/protocol.lua10
-rw-r--r--runtime/lua/vim/shared.lua2
-rw-r--r--runtime/lua/vim/snippet.lua63
-rwxr-xr-xscripts/gen_vimdoc.lua1
-rw-r--r--src/nvim/ex_getln.c4
-rw-r--r--src/nvim/main.c14
-rw-r--r--src/nvim/os/fileio.c11
-rw-r--r--test/CMakeLists.txt6
-rw-r--r--test/functional/lua/diagnostic_spec.lua303
-rw-r--r--test/functional/lua/snippet_spec.lua27
-rw-r--r--test/functional/lua/ui_event_spec.lua53
-rw-r--r--test/functional/plugin/lsp/completion_spec.lua235
33 files changed, 1714 insertions, 596 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7c0f8483ea..1b070fef21 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -40,6 +40,12 @@ include(PreventInTreeBuilds)
include(Util)
#-------------------------------------------------------------------------------
+# User settings
+#-------------------------------------------------------------------------------
+
+set(DEPS_IGNORE_SHA FALSE)
+
+#-------------------------------------------------------------------------------
# Variables
#-------------------------------------------------------------------------------
set(FUNCS_DATA ${PROJECT_BINARY_DIR}/funcs_data.mpack)
@@ -47,9 +53,6 @@ set(TOUCHES_DIR ${PROJECT_BINARY_DIR}/touches)
file(GLOB DOCFILES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/runtime/doc/*.txt)
-set_directory_properties(PROPERTIES
- EP_PREFIX "${DEPS_BUILD_DIR}")
-
if(NOT CI_BUILD)
set(CMAKE_INSTALL_MESSAGE NEVER)
endif()
@@ -241,7 +244,7 @@ add_glob_target(
GLOB_DIRS runtime scripts src test
GLOB_PAT *.lua
TOUCH_STRATEGY PER_DIR)
-add_dependencies(lintlua-luacheck lua-dev-deps)
+add_dependencies(lintlua-luacheck lua_dev_deps)
add_glob_target(
TARGET lintlua-stylua
@@ -300,26 +303,24 @@ if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR})
add_subdirectory(cmake.packaging)
endif()
+get_externalproject_options(uncrustify ${DEPS_IGNORE_SHA})
ExternalProject_Add(uncrustify
- URL https://github.com/uncrustify/uncrustify/archive/uncrustify-0.79.0.tar.gz
- URL_HASH SHA256=e7afaeabf636b7f0ce4e3e9747b95f7bd939613a8db49579755dddf44fedca5f
DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/uncrustify
CMAKE_ARGS ${DEPS_CMAKE_ARGS}
EXCLUDE_FROM_ALL TRUE
- DOWNLOAD_NO_PROGRESS TRUE)
+ ${EXTERNALPROJECT_OPTIONS})
option(USE_BUNDLED_BUSTED "Use bundled busted" ON)
if(USE_BUNDLED_BUSTED)
- ExternalProject_Add(lua-dev-deps
- URL https://github.com/neovim/deps/raw/5a1f71cceb24990a0b15fd9a472a5f549f019248/opt/lua-dev-deps.tar.gz
- URL_HASH SHA256=27db2495f5eddc7fc191701ec9b291486853530c6125609d3197d03481e8d5a2
- DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/lua-dev-deps
+ get_externalproject_options(lua_dev_deps ${DEPS_IGNORE_SHA})
+ ExternalProject_Add(lua_dev_deps
+ DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/lua_dev_deps
SOURCE_DIR ${DEPS_SHARE_DIR}
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
EXCLUDE_FROM_ALL TRUE
- DOWNLOAD_NO_PROGRESS TRUE)
+ ${EXTERNALPROJECT_OPTIONS})
else()
- add_custom_target(lua-dev-deps)
+ add_custom_target(lua_dev_deps)
endif()
diff --git a/cmake.deps/CMakeLists.txt b/cmake.deps/CMakeLists.txt
index 4853a1ab14..5b1f149dd7 100644
--- a/cmake.deps/CMakeLists.txt
+++ b/cmake.deps/CMakeLists.txt
@@ -74,25 +74,6 @@ if(APPLE)
message(STATUS "Using deployment target ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()
-set_directory_properties(PROPERTIES
- EP_PREFIX "${DEPS_BUILD_DIR}"
- CMAKE_CONFIGURE_DEPENDS deps.txt)
-
-file(READ deps.txt DEPENDENCIES)
-STRING(REGEX REPLACE "\n" ";" DEPENDENCIES "${DEPENDENCIES}")
-foreach(dep ${DEPENDENCIES})
- STRING(REGEX REPLACE " " ";" dep "${dep}")
- list(GET dep 0 name)
- list(GET dep 1 value)
- if(NOT ${name})
- # _URL variables must NOT be set when USE_EXISTING_SRC_DIR is set,
- # otherwise ExternalProject will try to re-download the sources.
- if(NOT USE_EXISTING_SRC_DIR)
- set(${name} ${value})
- endif()
- endif()
-endforeach()
-
if(USE_BUNDLED_LUAJIT)
set(LUA_ENGINE LuaJit)
elseif(USE_BUNDLED_LUA)
diff --git a/cmake.deps/deps.txt b/cmake.deps/deps.txt
index f76a6a1b70..f2b152640e 100644
--- a/cmake.deps/deps.txt
+++ b/cmake.deps/deps.txt
@@ -59,3 +59,8 @@ TREESITTER_MARKDOWN_URL https://github.com/MDeiml/tree-sitter-markdown/archive/v
TREESITTER_MARKDOWN_SHA256 4909d6023643f1afc3ab219585d4035b7403f3a17849782ab803c5f73c8a31d5
TREESITTER_URL https://github.com/tree-sitter/tree-sitter/archive/v0.22.6.tar.gz
TREESITTER_SHA256 e2b687f74358ab6404730b7fb1a1ced7ddb3780202d37595ecd7b20a8f41861f
+
+UNCRUSTIFY_URL https://github.com/uncrustify/uncrustify/archive/uncrustify-0.79.0.tar.gz
+UNCRUSTIFY_SHA256 e7afaeabf636b7f0ce4e3e9747b95f7bd939613a8db49579755dddf44fedca5f
+LUA_DEV_DEPS_URL https://github.com/neovim/deps/raw/5a1f71cceb24990a0b15fd9a472a5f549f019248/opt/lua-dev-deps.tar.gz
+LUA_DEV_DEPS_SHA256 27db2495f5eddc7fc191701ec9b291486853530c6125609d3197d03481e8d5a2
diff --git a/cmake/Deps.cmake b/cmake/Deps.cmake
index 413e3a08a9..398d9564cd 100644
--- a/cmake/Deps.cmake
+++ b/cmake/Deps.cmake
@@ -58,6 +58,32 @@ if(CMAKE_OSX_SYSROOT)
set(DEPS_C_COMPILER "${DEPS_C_COMPILER} -isysroot${CMAKE_OSX_SYSROOT}")
endif()
+get_filename_component(rootdir ${PROJECT_SOURCE_DIR} NAME)
+if(${rootdir} MATCHES "cmake.deps")
+ set(depsfile ${PROJECT_SOURCE_DIR}/deps.txt)
+else()
+ set(depsfile ${PROJECT_SOURCE_DIR}/cmake.deps/deps.txt)
+endif()
+
+set_directory_properties(PROPERTIES
+ EP_PREFIX "${DEPS_BUILD_DIR}"
+ CMAKE_CONFIGURE_DEPENDS ${depsfile})
+
+file(READ ${depsfile} DEPENDENCIES)
+STRING(REGEX REPLACE "\n" ";" DEPENDENCIES "${DEPENDENCIES}")
+foreach(dep ${DEPENDENCIES})
+ STRING(REGEX REPLACE " " ";" dep "${dep}")
+ list(GET dep 0 name)
+ list(GET dep 1 value)
+ if(NOT ${name})
+ # _URL variables must NOT be set when USE_EXISTING_SRC_DIR is set,
+ # otherwise ExternalProject will try to re-download the sources.
+ if(NOT USE_EXISTING_SRC_DIR)
+ set(${name} ${value})
+ endif()
+ endif()
+endforeach()
+
function(get_externalproject_options name DEPS_IGNORE_SHA)
string(TOUPPER ${name} name_allcaps)
set(url ${${name_allcaps}_URL})
diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt
index 646ba72bd8..6c6585d76e 100644
--- a/runtime/doc/deprecated.txt
+++ b/runtime/doc/deprecated.txt
@@ -19,6 +19,22 @@ API
- nvim_subscribe() Plugins must maintain their own "multicast" channels list.
- nvim_unsubscribe() Plugins must maintain their own "multicast" channels list.
+LUA
+- vim.region() Use |getregionpos()| instead.
+
+DIAGNOSTICS
+- *vim.diagnostic.goto_next()* Use |vim.diagnostic.jump()| with `{count = 1}` instead.
+- *vim.diagnostic.goto_prev()* Use |vim.diagnostic.jump()| with `{count = -1}` instead.
+- *vim.diagnostic.get_next_pos()*
+ Use the "lnum" and "col" fields from the return value of
+ |vim.diagnostic.get_next()| instead.
+- *vim.diagnostic.get_prev_pos()*
+ Use the "lnum" and "col" fields from the return value of
+ |vim.diagnostic.get_prev()| instead.
+- The "win_id" parameter used by various functions is deprecated in favor of
+ "winid" |winid|
+- The "cursor_position" parameter of |vim.diagnostic.JumpOpts| is renamed to
+ "pos"
------------------------------------------------------------------------------
DEPRECATED IN 0.10 *deprecated-0.10*
@@ -202,9 +218,6 @@ internally and are no longer exposed as part of the API. Instead, use
- *vim.lsp.diagnostic.set_underline()*
- *vim.lsp.diagnostic.set_virtual_text()*
-Configuring |diagnostic-signs| with |:sign-define| or |sign_define()| is no
-longer supported. Use the "signs" key of |vim.diagnostic.config()| instead.
-
LSP FUNCTIONS
- *vim.lsp.buf.server_ready()*
Use |LspAttach| instead, depending on your use-case. "Server ready" is not
diff --git a/runtime/doc/diagnostic.txt b/runtime/doc/diagnostic.txt
index 36616b9a0d..2438c48154 100644
--- a/runtime/doc/diagnostic.txt
+++ b/runtime/doc/diagnostic.txt
@@ -378,28 +378,38 @@ Lua module: vim.diagnostic *diagnostic-api*
• {severity}? (`vim.diagnostic.SeverityFilter`) See
|diagnostic-severity|.
-*vim.diagnostic.GotoOpts*
+*vim.diagnostic.JumpOpts*
Extends: |vim.diagnostic.GetOpts|
- Configuration table with the following keys:
+ Configuration table with the keys listed below. Some parameters can have
+ their default values changed with |vim.diagnostic.config()|.
Fields: ~
- • {cursor_position}? (`{[1]:integer,[2]:integer}`, default: current cursor position)
- Cursor position as a `(row, col)` tuple. See
- |nvim_win_get_cursor()|.
- • {wrap}? (`boolean`, default: `true`) Whether to loop
- around file or not. Similar to 'wrapscan'.
- • {severity}? (`vim.diagnostic.SeverityFilter`) See
- |diagnostic-severity|.
- • {float}? (`boolean|vim.diagnostic.Opts.Float`, default:
- `true`) If `true`, call
- |vim.diagnostic.open_float()| after moving. If a
- table, pass the table as the {opts} parameter to
- |vim.diagnostic.open_float()|. Unless overridden,
- the float will show diagnostics at the new cursor
- position (as if "cursor" were passed to the
- "scope" option).
- • {win_id}? (`integer`, default: `0`) Window ID
+ • {diagnostic}? (`vim.Diagnostic`) The diagnostic to jump to. Mutually
+ exclusive with {count}, {namespace}, and {severity}.
+ See |vim.Diagnostic|.
+ • {count}? (`integer`) The number of diagnostics to move by,
+ starting from {pos}. A positive integer moves forward
+ by {count} diagnostics, while a negative integer moves
+ backward by {count} diagnostics. Mutually exclusive
+ with {diagnostic}.
+ • {pos}? (`{[1]:integer,[2]:integer}`) Cursor position as a
+ `(row, col)` tuple. See |nvim_win_get_cursor()|. Used
+ to find the nearest diagnostic when {count} is used.
+ Only used when {count} is non-nil. Default is the
+ current cursor position.
+ • {wrap}? (`boolean`, default: `true`) Whether to loop around
+ file or not. Similar to 'wrapscan'.
+ • {severity}? (`vim.diagnostic.SeverityFilter`) See
+ |diagnostic-severity|.
+ • {float}? (`boolean|vim.diagnostic.Opts.Float`, default: `true`)
+ If `true`, call |vim.diagnostic.open_float()| after
+ moving. If a table, pass the table as the {opts}
+ parameter to |vim.diagnostic.open_float()|. Unless
+ overridden, the float will show diagnostics at the new
+ cursor position (as if "cursor" were passed to the
+ "scope" option).
+ • {winid}? (`integer`, default: `0`) Window ID
*vim.diagnostic.NS*
@@ -410,7 +420,7 @@ Lua module: vim.diagnostic *diagnostic-api*
• {disabled}? (`boolean`)
*vim.diagnostic.Opts*
- Each of the configuration options below accepts one of the following:
+ Many of the configuration options below accept one of the following:
• `false`: Disable this feature
• `true`: Enable this feature, use default settings.
• `table`: Enable this feature with overrides. Use an empty table to use
@@ -441,6 +451,9 @@ Lua module: vim.diagnostic *diagnostic-api*
displayed before lower severities (e.g. ERROR is
displayed before WARN). Options:
• {reverse}? (boolean) Reverse sort order
+ • {jump}? (`vim.diagnostic.Opts.Jump`) Default values for
+ |vim.diagnostic.jump()|. See
+ |vim.diagnostic.Opts.Jump|.
*vim.diagnostic.Opts.Float*
@@ -500,6 +513,16 @@ Lua module: vim.diagnostic *diagnostic-api*
• {focus_id}? (`string`)
• {border}? (`string`) see |nvim_open_win()|.
+*vim.diagnostic.Opts.Jump*
+
+ Fields: ~
+ • {float}? (`boolean|vim.diagnostic.Opts.Float`) Default value of
+ the {float} parameter of |vim.diagnostic.jump()|.
+ • {wrap}? (`boolean`) Default value of the {wrap} parameter of
+ |vim.diagnostic.jump()|.
+ • {severity}? (`vim.diagnostic.SeverityFilter`) Default value of the
+ {severity} parameter of |vim.diagnostic.jump()|.
+
*vim.diagnostic.Opts.Signs*
Fields: ~
@@ -678,52 +701,20 @@ get_next({opts}) *vim.diagnostic.get_next()*
Get the next diagnostic closest to the cursor position.
Parameters: ~
- • {opts} (`vim.diagnostic.GotoOpts?`) See |vim.diagnostic.GotoOpts|.
+ • {opts} (`vim.diagnostic.JumpOpts?`) See |vim.diagnostic.JumpOpts|.
Return: ~
(`vim.Diagnostic?`) Next diagnostic. See |vim.Diagnostic|.
-get_next_pos({opts}) *vim.diagnostic.get_next_pos()*
- Return the position of the next diagnostic in the current buffer.
-
- Parameters: ~
- • {opts} (`vim.diagnostic.GotoOpts?`) See |vim.diagnostic.GotoOpts|.
-
- Return: ~
- (`table|false`) Next diagnostic position as a `(row, col)` tuple or
- false if no next diagnostic.
-
get_prev({opts}) *vim.diagnostic.get_prev()*
Get the previous diagnostic closest to the cursor position.
Parameters: ~
- • {opts} (`vim.diagnostic.GotoOpts?`) See |vim.diagnostic.GotoOpts|.
+ • {opts} (`vim.diagnostic.JumpOpts?`) See |vim.diagnostic.JumpOpts|.
Return: ~
(`vim.Diagnostic?`) Previous diagnostic. See |vim.Diagnostic|.
-get_prev_pos({opts}) *vim.diagnostic.get_prev_pos()*
- Return the position of the previous diagnostic in the current buffer.
-
- Parameters: ~
- • {opts} (`vim.diagnostic.GotoOpts?`) See |vim.diagnostic.GotoOpts|.
-
- Return: ~
- (`table|false`) Previous diagnostic position as a `(row, col)` tuple
- or `false` if there is no prior diagnostic.
-
-goto_next({opts}) *vim.diagnostic.goto_next()*
- Move to the next diagnostic.
-
- Parameters: ~
- • {opts} (`vim.diagnostic.GotoOpts?`) See |vim.diagnostic.GotoOpts|.
-
-goto_prev({opts}) *vim.diagnostic.goto_prev()*
- Move to the previous diagnostic in the current buffer.
-
- Parameters: ~
- • {opts} (`vim.diagnostic.GotoOpts?`) See |vim.diagnostic.GotoOpts|.
-
hide({namespace}, {bufnr}) *vim.diagnostic.hide()*
Hide currently displayed diagnostics.
@@ -753,6 +744,16 @@ is_enabled({filter}) *vim.diagnostic.is_enabled()*
Return: ~
(`boolean`)
+jump({opts}) *vim.diagnostic.jump()*
+ Move to a diagnostic.
+
+ Parameters: ~
+ • {opts} (`vim.diagnostic.JumpOpts`) See |vim.diagnostic.JumpOpts|.
+
+ Return: ~
+ (`vim.Diagnostic?`) The diagnostic that was moved to. See
+ |vim.Diagnostic|.
+
*vim.diagnostic.match()*
match({str}, {pat}, {groups}, {severity_map}, {defaults})
Parse a diagnostic from a string.
@@ -792,7 +793,7 @@ open_float({opts}) *vim.diagnostic.open_float()*
Return (multiple): ~
(`integer?`) float_bufnr
- (`integer?`) win_id
+ (`integer?`) winid
reset({namespace}, {bufnr}) *vim.diagnostic.reset()*
Remove all diagnostics from the given namespace.
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
index a66b577b11..a78a16968f 100644
--- a/runtime/doc/lsp.txt
+++ b/runtime/doc/lsp.txt
@@ -1605,6 +1605,32 @@ save({lenses}, {bufnr}, {client_id}) *vim.lsp.codelens.save()*
==============================================================================
+Lua module: vim.lsp.completion *lsp-completion*
+
+*vim.lsp.completion.BufferOpts*
+
+ Fields: ~
+ • {autotrigger}? (`boolean`) Whether to trigger completion
+ automatically. Default: false
+
+
+ *vim.lsp.completion.enable()*
+enable({enable}, {client_id}, {bufnr}, {opts})
+ Enables or disables completions from the given language client in the
+ given buffer.
+
+ Parameters: ~
+ • {enable} (`boolean`) True to enable, false to disable
+ • {client_id} (`integer`) Client ID
+ • {bufnr} (`integer`) Buffer handle, or 0 for the current buffer
+ • {opts} (`vim.lsp.completion.BufferOpts?`) See
+ |vim.lsp.completion.BufferOpts|.
+
+trigger() *vim.lsp.completion.trigger()*
+ Trigger LSP completion in the current buffer.
+
+
+==============================================================================
Lua module: vim.lsp.inlay_hint *lsp-inlay_hint*
enable({enable}, {filter}) *vim.lsp.inlay_hint.enable()*
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index fd0cd3252f..36d6e0d41e 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -1708,30 +1708,6 @@ vim.print({...}) *vim.print()*
• |vim.inspect()|
• |:=|
- *vim.region()*
-vim.region({bufnr}, {pos1}, {pos2}, {regtype}, {inclusive})
- Gets a dict of line segment ("chunk") positions for the region from `pos1`
- to `pos2`.
-
- Input and output positions are byte positions, (0,0)-indexed. "End of
- line" column position (for example, |linewise| visual selection) is
- returned as |v:maxcol| (big number).
-
- Parameters: ~
- • {bufnr} (`integer`) Buffer number, or 0 for current buffer
- • {pos1} (`integer[]|string`) Start of region as a (line, column)
- tuple or |getpos()|-compatible string
- • {pos2} (`integer[]|string`) End of region as a (line, column)
- tuple or |getpos()|-compatible string
- • {regtype} (`string`) |setreg()|-style selection type
- • {inclusive} (`boolean`) Controls whether the ending column is
- inclusive (see also 'selection').
-
- Return: ~
- (`table`) region Dict of the form `{linenr = {startcol,endcol}}`.
- `endcol` is exclusive, and whole lines are returned as
- `{startcol,endcol} = {0,-1}`.
-
vim.schedule_wrap({fn}) *vim.schedule_wrap()*
Returns a function which calls {fn} via |vim.schedule()|.
diff --git a/runtime/doc/news-0.10.txt b/runtime/doc/news-0.10.txt
index 8a0e7e92e7..d611eee242 100644
--- a/runtime/doc/news-0.10.txt
+++ b/runtime/doc/news-0.10.txt
@@ -307,7 +307,7 @@ The following new features were added.
a predicate function that is checked for each value. (Use
|vim.list_contains()| for checking list-like tables (integer keys without
gaps) for literal values.)
- • |vim.region()| can use a string accepted by |getpos()| as position.
+ • vim.region() can use a string accepted by |getpos()| as position.
• Options:
• 'winfixbuf' keeps a window focused onto a specific buffer
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 2d4017e3c4..13e4b23ea1 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -34,11 +34,19 @@ API
DEFAULTS
-• TODO
+• |]d-default| and |[d-default| accept a count.
+• |[D-default| and |]D-default| jump to the first and last diagnostic in the
+ current buffer, respectively.
+
+DIAGNOSTICS
+
+• |vim.diagnostic.config()| accepts a "jump" table to specify defaults for
+ |vim.diagnostic.jump()|.
EDITOR
-• The order in which signs are placed was changed. Higher priority signs will now appear left of lower priority signs.
+• The order in which signs are placed was changed. Higher priority signs will
+ now appear left of lower priority signs.
EVENTS
@@ -85,6 +93,12 @@ DEFAULTS
- |gra| in Normal and Visual mode maps to |vim.lsp.buf.code_action()|
- CTRL-S in Insert mode maps to |vim.lsp.buf.signature_help()|
+• Snippet:
+ - `<Tab>` in Insert and Select mode maps to |vim.snippet.jump({ direction = 1 })|
+ when a snippet is active and jumpable forwards.
+ - `<S-Tab>` in Insert and Select mode maps to |vim.snippet.jump({ direction = -1 })|
+ when a snippet is active and jumpable backwards.
+
EDITOR
* On Windows, filename arguments on the command-line prefixed with "~\" or
@@ -97,7 +111,8 @@ EVENTS
LSP
-• TODO
+• Completion side effects (including snippet expansion, execution of commands
+ and application of additional text edits) is now built-in.
LUA
diff --git a/runtime/doc/tagsrch.txt b/runtime/doc/tagsrch.txt
index ef1654d365..8b2813785f 100644
--- a/runtime/doc/tagsrch.txt
+++ b/runtime/doc/tagsrch.txt
@@ -781,15 +781,15 @@ CTRL-W i Open a new window, with the cursor on the first line
count'th matching line is displayed.
*[d-default*
- Mapped to |vim.diagnostic.goto_prev()| by default.
- |default-mappings|
+ Jumps to the previous diagnostic in the current buffer
+ by default. |vim.diagnostic.jump()| |default-mappings|
*]d*
]d like "[d", but start at the current cursor position.
*]d-default*
- Mapped to |vim.diagnostic.goto_next()| by default.
- |default-mappings|
+ Jumps to the next diagnostic in the current buffer by
+ default. |vim.diagnostic.jump()| |default-mappings|
*:ds* *:dsearch*
:[range]ds[earch][!] [count] [/]string[/]
@@ -803,9 +803,17 @@ CTRL-W i Open a new window, with the cursor on the first line
displayed for the found lines. The search starts
from the beginning of the file.
+ *[D-default*
+ Jumps to the first diagnostic in the current buffer by
+ default. |vim.diagnostic.jump()| |default-mappings|
+
*]D*
]D like "[D", but start at the current cursor position.
+ *]D-default*
+ Jumps to the last diagnostic in the current buffer by
+ default. |vim.diagnostic.jump()| |default-mappings|
+
*:dli* *:dlist*
:[range]dli[st][!] [/]string[/]
Like `[D` and `]D`, but search in [range] lines
diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt
index 5d894bb5e1..ac20948f14 100644
--- a/runtime/doc/vim_diff.txt
+++ b/runtime/doc/vim_diff.txt
@@ -145,6 +145,8 @@ of these in your config by simply removing the mapping, e.g. ":unmap Y".
- <C-S> |i_CTRL-S|
- ]d |]d-default|
- [d |[d-default|
+- [D |[D-default|
+- ]D |]D-default|
- <C-W>d |CTRL-W_d-default|
- Nvim LSP client defaults |lsp-defaults|
- K |K-lsp-default|
diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua
index 5b964b84a0..79fe5a8513 100644
--- a/runtime/lua/vim/_defaults.lua
+++ b/runtime/lua/vim/_defaults.lua
@@ -180,12 +180,20 @@ do
--- See |[d-default|, |]d-default|, and |CTRL-W_d-default|.
do
vim.keymap.set('n', ']d', function()
- vim.diagnostic.goto_next({ float = false })
- end, { desc = 'Jump to the next diagnostic' })
+ vim.diagnostic.jump({ count = vim.v.count1 })
+ end, { desc = 'Jump to the next diagnostic in the current buffer' })
vim.keymap.set('n', '[d', function()
- vim.diagnostic.goto_prev({ float = false })
- end, { desc = 'Jump to the previous diagnostic' })
+ vim.diagnostic.jump({ count = -vim.v.count1 })
+ end, { desc = 'Jump to the previous diagnostic in the current buffer' })
+
+ vim.keymap.set('n', ']D', function()
+ vim.diagnostic.jump({ count = math.huge, wrap = false })
+ end, { desc = 'Jump to the last diagnostic in the current buffer' })
+
+ vim.keymap.set('n', '[D', function()
+ vim.diagnostic.jump({ count = -math.huge, wrap = false })
+ end, { desc = 'Jump to the first diagnostic in the current buffer' })
vim.keymap.set('n', '<C-W>d', function()
vim.diagnostic.open_float()
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index 5e9be509c8..9f952db4fc 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -494,6 +494,7 @@ do
vim.t = make_dict_accessor('t')
end
+--- @deprecated
--- Gets a dict of line segment ("chunk") positions for the region from `pos1` to `pos2`.
---
--- Input and output positions are byte positions, (0,0)-indexed. "End of line" column
@@ -507,6 +508,8 @@ end
---@return table region Dict of the form `{linenr = {startcol,endcol}}`. `endcol` is exclusive, and
---whole lines are returned as `{startcol,endcol} = {0,-1}`.
function vim.region(bufnr, pos1, pos2, regtype, inclusive)
+ vim.deprecate('vim.region', 'vim.fn.getregionpos()', '0.13')
+
if not vim.api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
end
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index 348204abb7..dca7698356 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -42,7 +42,7 @@ local M = {}
---
--- @field namespace? integer
---- Each of the configuration options below accepts one of the following:
+--- Many of the configuration options below accept one of the following:
--- - `false`: Disable this feature
--- - `true`: Enable this feature, use default settings.
--- - `table`: Enable this feature with overrides. Use an empty table to use default values.
@@ -78,6 +78,9 @@ local M = {}
--- - {reverse}? (boolean) Reverse sort order
--- (default: `false`)
--- @field severity_sort? boolean|{reverse?:boolean}
+---
+--- Default values for |vim.diagnostic.jump()|. See |vim.diagnostic.Opts.Jump|.
+--- @field jump? vim.diagnostic.Opts.Jump
--- @class (private) vim.diagnostic.OptsResolved
--- @field float vim.diagnostic.Opts.Float
@@ -241,6 +244,20 @@ local M = {}
--- whole line the sign is placed in.
--- @field linehl? table<vim.diagnostic.Severity,string>
+--- @class vim.diagnostic.Opts.Jump
+---
+--- Default value of the {float} parameter of |vim.diagnostic.jump()|.
+--- @field float? boolean|vim.diagnostic.Opts.Float
+---
+--- Default value of the {wrap} parameter of |vim.diagnostic.jump()|.
+--- @field wrap? boolean
+---
+--- Default value of the {severity} parameter of |vim.diagnostic.jump()|.
+--- @field severity? vim.diagnostic.SeverityFilter
+---
+--- Default value of the {_highest} parameter of |vim.diagnostic.jump()|.
+--- @field package _highest? boolean
+
-- TODO: inherit from `vim.diagnostic.Opts`, implement its fields.
--- Optional filters |kwargs|, or `nil` for all.
--- @class vim.diagnostic.Filter
@@ -284,6 +301,13 @@ local global_diagnostic_options = {
float = true,
update_in_insert = false,
severity_sort = false,
+ jump = {
+ -- Do not show floating window
+ float = false,
+
+ -- Wrap around buffer
+ wrap = true,
+ },
}
--- @class (private) vim.diagnostic.Handler
@@ -835,21 +859,36 @@ local function filter_highest(diagnostics)
end
end
---- @param position {[1]: integer, [2]: integer}
--- @param search_forward boolean
---- @param bufnr integer
---- @param opts vim.diagnostic.GotoOpts
---- @param namespace integer[]|integer
+--- @param opts vim.diagnostic.JumpOpts?
--- @return vim.Diagnostic?
-local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
+local function next_diagnostic(search_forward, opts)
+ opts = opts or {}
+
+ -- Support deprecated win_id alias
+ if opts.win_id then
+ vim.deprecate('opts.win_id', 'opts.winid', '0.13')
+ opts.winid = opts.win_id
+ opts.win_id = nil
+ end
+
+ -- Support deprecated cursor_position alias
+ if opts.cursor_position then
+ vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
+ opts.pos = opts.cursor_position
+ opts.cursor_position = nil
+ end
+
+ local winid = opts.winid or api.nvim_get_current_win()
+ local bufnr = api.nvim_win_get_buf(winid)
+ local position = opts.pos or api.nvim_win_get_cursor(winid)
+
+ -- Adjust row to be 0-indexed
position[1] = position[1] - 1
- bufnr = get_bufnr(bufnr)
- local wrap = if_nil(opts.wrap, true)
- local get_opts = vim.deepcopy(opts)
- get_opts.namespace = get_opts.namespace or namespace
+ local wrap = if_nil(opts.wrap, true)
- local diagnostics = get_diagnostics(bufnr, get_opts, true)
+ local diagnostics = get_diagnostics(bufnr, opts, true)
if opts._highest then
filter_highest(diagnostics)
@@ -902,32 +941,41 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
end
end
---- @param opts vim.diagnostic.GotoOpts?
---- @param pos {[1]:integer,[2]:integer}|false
-local function diagnostic_move_pos(opts, pos)
- opts = opts or {}
-
- local float = if_nil(opts.float, true)
- local win_id = opts.win_id or api.nvim_get_current_win()
-
- if not pos then
+--- Move the cursor to the given diagnostic.
+---
+--- @param diagnostic vim.Diagnostic?
+--- @param opts vim.diagnostic.JumpOpts?
+local function goto_diagnostic(diagnostic, opts)
+ if not diagnostic then
api.nvim_echo({ { 'No more valid diagnostics to move to', 'WarningMsg' } }, true, {})
return
end
- api.nvim_win_call(win_id, function()
+ opts = opts or {}
+
+ -- Support deprecated win_id alias
+ if opts.win_id then
+ vim.deprecate('opts.win_id', 'opts.winid', '0.13')
+ opts.winid = opts.win_id
+ opts.win_id = nil
+ end
+
+ local winid = opts.winid or api.nvim_get_current_win()
+
+ api.nvim_win_call(winid, function()
-- Save position in the window's jumplist
vim.cmd("normal! m'")
- api.nvim_win_set_cursor(win_id, { pos[1] + 1, pos[2] })
+ api.nvim_win_set_cursor(winid, { diagnostic.lnum + 1, diagnostic.col })
-- Open folds under the cursor
vim.cmd('normal! zv')
end)
+ local float = if_nil(opts.float, true)
if float then
local float_opts = type(float) == 'table' and float or {}
vim.schedule(function()
M.open_float(vim.tbl_extend('keep', float_opts, {
- bufnr = api.nvim_win_get_buf(win_id),
+ bufnr = api.nvim_win_get_buf(winid),
scope = 'cursor',
focus = false,
}))
@@ -1114,24 +1162,24 @@ end
--- Get the previous diagnostic closest to the cursor position.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
---@return vim.Diagnostic? : Previous diagnostic
function M.get_prev(opts)
- opts = opts or {}
-
- local win_id = opts.win_id or api.nvim_get_current_win()
- local bufnr = api.nvim_win_get_buf(win_id)
- local cursor_position = opts.cursor_position or api.nvim_win_get_cursor(win_id)
-
- return next_diagnostic(cursor_position, false, bufnr, opts, opts.namespace)
+ return next_diagnostic(false, opts)
end
--- Return the position of the previous diagnostic in the current buffer.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
---@return table|false: Previous diagnostic position as a `(row, col)` tuple
--- or `false` if there is no prior diagnostic.
+---@deprecated
function M.get_prev_pos(opts)
+ vim.deprecate(
+ 'vim.diagnostic.get_prev_pos()',
+ 'access the lnum and col fields from get_prev() instead',
+ '0.13'
+ )
local prev = M.get_prev(opts)
if not prev then
return false
@@ -1141,31 +1189,33 @@ function M.get_prev_pos(opts)
end
--- Move to the previous diagnostic in the current buffer.
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
+---@deprecated
function M.goto_prev(opts)
- return diagnostic_move_pos(opts, M.get_prev_pos(opts))
+ vim.deprecate('vim.diagnostic.goto_prev()', 'vim.diagnostic.jump()', '0.13')
+ goto_diagnostic(M.get_prev(opts), opts)
end
--- Get the next diagnostic closest to the cursor position.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
---@return vim.Diagnostic? : Next diagnostic
function M.get_next(opts)
- opts = opts or {}
-
- local win_id = opts.win_id or api.nvim_get_current_win()
- local bufnr = api.nvim_win_get_buf(win_id)
- local cursor_position = opts.cursor_position or api.nvim_win_get_cursor(win_id)
-
- return next_diagnostic(cursor_position, true, bufnr, opts, opts.namespace)
+ return next_diagnostic(true, opts)
end
--- Return the position of the next diagnostic in the current buffer.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
---@return table|false : Next diagnostic position as a `(row, col)` tuple or false if no next
--- diagnostic.
+---@deprecated
function M.get_next_pos(opts)
+ vim.deprecate(
+ 'vim.diagnostic.get_next_pos()',
+ 'access the lnum and col fields from get_next() instead',
+ '0.13'
+ )
local next = M.get_next(opts)
if not next then
return false
@@ -1186,13 +1236,23 @@ end
--- See |diagnostic-severity|.
--- @field severity? vim.diagnostic.SeverityFilter
---- Configuration table with the following keys:
---- @class vim.diagnostic.GotoOpts : vim.diagnostic.GetOpts
+--- Configuration table with the keys listed below. Some parameters can have their default values
+--- changed with |vim.diagnostic.config()|.
+--- @class vim.diagnostic.JumpOpts : vim.diagnostic.GetOpts
---
---- Cursor position as a `(row, col)` tuple.
---- See |nvim_win_get_cursor()|.
---- (default: current cursor position)
---- @field cursor_position? {[1]:integer,[2]:integer}
+--- The diagnostic to jump to. Mutually exclusive with {count}, {namespace},
+--- and {severity}.
+--- @field diagnostic? vim.Diagnostic
+---
+--- The number of diagnostics to move by, starting from {pos}. A positive
+--- integer moves forward by {count} diagnostics, while a negative integer moves
+--- backward by {count} diagnostics. Mutually exclusive with {diagnostic}.
+--- @field count? integer
+---
+--- Cursor position as a `(row, col)` tuple. See |nvim_win_get_cursor()|. Used
+--- to find the nearest diagnostic when {count} is used. Only used when {count}
+--- is non-nil. Default is the current cursor position.
+--- @field pos? {[1]:integer,[2]:integer}
---
--- Whether to loop around file or not. Similar to 'wrapscan'.
--- (default: `true`)
@@ -1214,13 +1274,71 @@ end
---
--- Window ID
--- (default: `0`)
---- @field win_id? integer
+--- @field winid? integer
+
+--- Move to a diagnostic.
+---
+--- @param opts vim.diagnostic.JumpOpts
+--- @return vim.Diagnostic? # The diagnostic that was moved to.
+function M.jump(opts)
+ vim.validate('opts', opts, 'table')
+
+ -- One of "diagnostic" or "count" must be provided
+ assert(
+ opts.diagnostic or opts.count,
+ 'One of "diagnostic" or "count" must be specified in the options to vim.diagnostic.jump()'
+ )
+
+ -- Apply configuration options from vim.diagnostic.config()
+ opts = vim.tbl_deep_extend('keep', opts, global_diagnostic_options.jump)
+
+ if opts.diagnostic then
+ goto_diagnostic(opts.diagnostic, opts)
+ return opts.diagnostic
+ end
+
+ local count = opts.count
+ if count == 0 then
+ return nil
+ end
+
+ -- Support deprecated cursor_position alias
+ if opts.cursor_position then
+ vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
+ opts.pos = opts.cursor_position
+ opts.cursor_position = nil
+ end
+
+ local diag = nil
+ while count ~= 0 do
+ local next = next_diagnostic(count > 0, opts)
+ if not next then
+ break
+ end
+
+ -- Update cursor position
+ opts.pos = { next.lnum + 1, next.col }
+
+ if count > 0 then
+ count = count - 1
+ else
+ count = count + 1
+ end
+ diag = next
+ end
+
+ goto_diagnostic(diag, opts)
+
+ return diag
+end
--- Move to the next diagnostic.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
+---@deprecated
function M.goto_next(opts)
- diagnostic_move_pos(opts, M.get_next_pos(opts))
+ vim.deprecate('vim.diagnostic.goto_next()', 'vim.diagnostic.jump()', '0.13')
+ goto_diagnostic(M.get_next(opts), opts)
end
M.handlers.signs = {
@@ -1688,7 +1806,7 @@ end
---
---@param opts vim.diagnostic.Opts.Float?
---@return integer? float_bufnr
----@return integer? win_id
+---@return integer? winid
function M.open_float(opts, ...)
-- Support old (bufnr, opts) signature
local bufnr --- @type integer?
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 60b3f3e502..94c31359da 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -3,7 +3,6 @@ local validate = vim.validate
local lsp = vim._defer_require('vim.lsp', {
_changetracking = ..., --- @module 'vim.lsp._changetracking'
- _completion = ..., --- @module 'vim.lsp._completion'
_dynamic = ..., --- @module 'vim.lsp._dynamic'
_snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar'
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
@@ -11,6 +10,7 @@ local lsp = vim._defer_require('vim.lsp', {
buf = ..., --- @module 'vim.lsp.buf'
client = ..., --- @module 'vim.lsp.client'
codelens = ..., --- @module 'vim.lsp.codelens'
+ completion = ..., --- @module 'vim.lsp.completion'
diagnostic = ..., --- @module 'vim.lsp.diagnostic'
handlers = ..., --- @module 'vim.lsp.handlers'
inlay_hint = ..., --- @module 'vim.lsp.inlay_hint'
@@ -1003,8 +1003,7 @@ end
--- - findstart=0: column where the completion starts, or -2 or -3
--- - findstart=1: list of matches (actually just calls |complete()|)
function lsp.omnifunc(findstart, base)
- log.debug('omnifunc.findstart', { findstart = findstart, base = base })
- return vim.lsp._completion.omnifunc(findstart, base)
+ return vim.lsp.completion._omnifunc(findstart, base)
end
--- @class vim.lsp.formatexpr.Opts
diff --git a/runtime/lua/vim/lsp/_completion.lua b/runtime/lua/vim/lsp/_completion.lua
deleted file mode 100644
index a169f96565..0000000000
--- a/runtime/lua/vim/lsp/_completion.lua
+++ /dev/null
@@ -1,276 +0,0 @@
-local M = {}
-local api = vim.api
-local lsp = vim.lsp
-local protocol = lsp.protocol
-local ms = protocol.Methods
-
---- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[]
-
--- TODO(mariasolos): Remove this declaration once we figure out a better way to handle
--- literal/anonymous types (see https://github.com/neovim/neovim/pull/27542/files#r1495259331).
---- @class lsp.ItemDefaults
---- @field editRange lsp.Range | { insert: lsp.Range, replace: lsp.Range } | nil
---- @field insertTextFormat lsp.InsertTextFormat?
---- @field insertTextMode lsp.InsertTextMode?
---- @field data any
-
----@param input string unparsed snippet
----@return string parsed snippet
-local function parse_snippet(input)
- local ok, parsed = pcall(function()
- return vim.lsp._snippet_grammar.parse(input)
- end)
- return ok and tostring(parsed) or input
-end
-
---- Returns text that should be inserted when selecting completion item. The
---- precedence is as follows: textEdit.newText > insertText > label
----
---- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
----
----@param item lsp.CompletionItem
----@return string
-local function get_completion_word(item)
- if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
- if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
- return item.textEdit.newText
- else
- return parse_snippet(item.textEdit.newText)
- end
- elseif item.insertText ~= nil and item.insertText ~= '' then
- if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
- return item.insertText
- else
- return parse_snippet(item.insertText)
- end
- end
- return item.label
-end
-
---- Applies the given defaults to the completion item, modifying it in place.
----
---- @param item lsp.CompletionItem
---- @param defaults lsp.ItemDefaults?
-local function apply_defaults(item, defaults)
- if not defaults then
- return
- end
-
- item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat
- item.insertTextMode = item.insertTextMode or defaults.insertTextMode
- item.data = item.data or defaults.data
- if defaults.editRange then
- local textEdit = item.textEdit or {}
- item.textEdit = textEdit
- textEdit.newText = textEdit.newText or item.textEditText or item.insertText
- if defaults.editRange.start then
- textEdit.range = textEdit.range or defaults.editRange
- elseif defaults.editRange.insert then
- textEdit.insert = defaults.editRange.insert
- textEdit.replace = defaults.editRange.replace
- end
- end
-end
-
----@param result vim.lsp.CompletionResult
----@return lsp.CompletionItem[]
-local function get_items(result)
- if result.items then
- for _, item in ipairs(result.items) do
- ---@diagnostic disable-next-line: param-type-mismatch
- apply_defaults(item, result.itemDefaults)
- end
- return result.items
- else
- return result
- end
-end
-
---- Turns the result of a `textDocument/completion` request into vim-compatible
---- |complete-items|.
----
----@param result vim.lsp.CompletionResult Result of `textDocument/completion`
----@param prefix string prefix to filter the completion items
----@return table[]
----@see complete-items
-function M._lsp_to_complete_items(result, prefix)
- local items = get_items(result)
- if vim.tbl_isempty(items) then
- return {}
- end
-
- local function matches_prefix(item)
- return vim.startswith(get_completion_word(item), prefix)
- end
-
- items = vim.tbl_filter(matches_prefix, items) --[[@as lsp.CompletionItem[]|]]
- table.sort(items, function(a, b)
- return (a.sortText or a.label) < (b.sortText or b.label)
- end)
-
- local matches = {}
- for _, item in ipairs(items) do
- local info = ''
- local documentation = item.documentation
- if documentation then
- if type(documentation) == 'string' and documentation ~= '' then
- info = documentation
- elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
- info = documentation.value
- else
- vim.notify(
- ('invalid documentation value %s'):format(vim.inspect(documentation)),
- vim.log.levels.WARN
- )
- end
- end
- local word = get_completion_word(item)
- table.insert(matches, {
- word = word,
- abbr = item.label,
- kind = protocol.CompletionItemKind[item.kind] or 'Unknown',
- menu = item.detail or '',
- info = #info > 0 and info or nil,
- icase = 1,
- dup = 1,
- empty = 1,
- user_data = {
- nvim = {
- lsp = {
- completion_item = item,
- },
- },
- },
- })
- end
- return matches
-end
-
----@param lnum integer 0-indexed
----@param items lsp.CompletionItem[]
-local function adjust_start_col(lnum, line, items, encoding)
- local min_start_char = nil
- for _, item in pairs(items) do
- if item.textEdit and item.textEdit.range.start.line == lnum then
- if min_start_char and min_start_char ~= item.textEdit.range.start.character then
- return nil
- end
- min_start_char = item.textEdit.range.start.character
- end
- end
- if min_start_char then
- return vim.lsp.util._str_byteindex_enc(line, min_start_char, encoding)
- else
- return nil
- end
-end
-
----@private
----@param line string line content
----@param lnum integer 0-indexed line number
----@param client_start_boundary integer 0-indexed word boundary
----@param server_start_boundary? integer 0-indexed word boundary, based on textEdit.range.start.character
----@param result vim.lsp.CompletionResult
----@param encoding string
----@return table[] matches
----@return integer? server_start_boundary
-function M._convert_results(
- line,
- lnum,
- cursor_col,
- client_start_boundary,
- server_start_boundary,
- result,
- encoding
-)
- -- Completion response items may be relative to a position different than `client_start_boundary`.
- -- Concrete example, with lua-language-server:
- --
- -- require('plenary.asy|
- -- ▲ ▲ ▲
- -- │ │ └── cursor_pos: 20
- -- │ └────── client_start_boundary: 17
- -- └────────────── textEdit.range.start.character: 9
- -- .newText = 'plenary.async'
- -- ^^^
- -- prefix (We'd remove everything not starting with `asy`,
- -- so we'd eliminate the `plenary.async` result
- --
- -- `adjust_start_col` is used to prefer the language server boundary.
- --
- local candidates = get_items(result)
- local curstartbyte = adjust_start_col(lnum, line, candidates, encoding)
- if server_start_boundary == nil then
- server_start_boundary = curstartbyte
- elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then
- server_start_boundary = client_start_boundary
- end
- local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
- local matches = M._lsp_to_complete_items(result, prefix)
- return matches, server_start_boundary
-end
-
----@param findstart integer 0 or 1, decides behavior
----@param base integer findstart=0, text to match against
----@return integer|table Decided by {findstart}:
---- - findstart=0: column where the completion starts, or -2 or -3
---- - findstart=1: list of matches (actually just calls |complete()|)
-function M.omnifunc(findstart, base)
- assert(base) -- silence luals
- local bufnr = api.nvim_get_current_buf()
- local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion })
- local remaining = #clients
- if remaining == 0 then
- return findstart == 1 and -1 or {}
- end
-
- local win = api.nvim_get_current_win()
- local cursor = api.nvim_win_get_cursor(win)
- local lnum = cursor[1] - 1
- local cursor_col = cursor[2]
- local line = api.nvim_get_current_line()
- local line_to_cursor = line:sub(1, cursor_col)
- local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$') --[[@as integer]]
- local server_start_boundary = nil
- local items = {}
-
- local function on_done()
- local mode = api.nvim_get_mode()['mode']
- if mode == 'i' or mode == 'ic' then
- vim.fn.complete((server_start_boundary or client_start_boundary) + 1, items)
- end
- end
-
- local util = vim.lsp.util
- for _, client in ipairs(clients) do
- local params = util.make_position_params(win, client.offset_encoding)
- client.request(ms.textDocument_completion, params, function(err, result)
- if err then
- vim.lsp.log.warn(err.message)
- end
- if result and vim.fn.mode() == 'i' then
- local matches
- matches, server_start_boundary = M._convert_results(
- line,
- lnum,
- cursor_col,
- client_start_boundary,
- server_start_boundary,
- result,
- client.offset_encoding
- )
- vim.list_extend(items, matches)
- end
- remaining = remaining - 1
- if remaining == 0 then
- vim.schedule(on_done)
- end
- end, bufnr)
- end
-
- -- Return -2 to signal that we should continue completion so that we can
- -- async complete.
- return -2
-end
-
-return M
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 49833eaeec..299b68e134 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -135,7 +135,7 @@ end
---@param mode "v"|"V"
---@return table {start={row,col}, end={row,col}} using (1, 0) indexing
local function range_from_selection(bufnr, mode)
- -- TODO: Use `vim.region()` instead https://github.com/neovim/neovim/pull/13896
+ -- TODO: Use `vim.fn.getregionpos()` instead.
-- [bufnum, lnum, col, off]; both row and column 1-indexed
local start = vim.fn.getpos('v')
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index 58ea7d02b3..327cd19125 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -868,7 +868,8 @@ end
--- @param command lsp.Command
--- @param context? {bufnr: integer}
--- @param handler? lsp.Handler only called if a server command
-function Client:_exec_cmd(command, context, handler)
+--- @param on_unsupported? function handler invoked when the command is not supported by the client.
+function Client:_exec_cmd(command, context, handler, on_unsupported)
context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]]
context.bufnr = context.bufnr or api.nvim_get_current_buf()
context.client_id = self.id
@@ -882,14 +883,18 @@ function Client:_exec_cmd(command, context, handler)
local command_provider = self.server_capabilities.executeCommandProvider
local commands = type(command_provider) == 'table' and command_provider.commands or {}
if not vim.list_contains(commands, cmdname) then
- vim.notify_once(
- string.format(
- 'Language server `%s` does not support command `%s`. This command may require a client extension.',
- self.name,
- cmdname
- ),
- vim.log.levels.WARN
- )
+ if on_unsupported then
+ on_unsupported()
+ else
+ vim.notify_once(
+ string.format(
+ 'Language server `%s` does not support command `%s`. This command may require a client extension.',
+ self.name,
+ cmdname
+ ),
+ vim.log.levels.WARN
+ )
+ end
return
end
-- Not using command directly to exclude extra properties,
diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua
new file mode 100644
index 0000000000..25b3d53c8c
--- /dev/null
+++ b/runtime/lua/vim/lsp/completion.lua
@@ -0,0 +1,734 @@
+local M = {}
+
+local api = vim.api
+local lsp = vim.lsp
+local protocol = lsp.protocol
+local ms = protocol.Methods
+
+local rtt_ms = 50
+local ns_to_ms = 0.000001
+
+--- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[]
+
+-- TODO(mariasolos): Remove this declaration once we figure out a better way to handle
+-- literal/anonymous types (see https://github.com/neovim/neovim/pull/27542/files#r1495259331).
+--- @nodoc
+--- @class lsp.ItemDefaults
+--- @field editRange lsp.Range | { insert: lsp.Range, replace: lsp.Range } | nil
+--- @field insertTextFormat lsp.InsertTextFormat?
+--- @field insertTextMode lsp.InsertTextMode?
+--- @field data any
+
+--- @nodoc
+--- @class vim.lsp.completion.BufHandle
+--- @field clients table<integer, vim.lsp.Client>
+--- @field triggers table<string, vim.lsp.Client[]>
+
+--- @type table<integer, vim.lsp.completion.BufHandle>
+local buf_handles = {}
+
+--- @nodoc
+--- @class vim.lsp.completion.Context
+local Context = {
+ cursor = nil, --- @type { [1]: integer, [2]: integer }?
+ last_request_time = nil, --- @type integer?
+ pending_requests = {}, --- @type function[]
+ isIncomplete = false,
+}
+
+--- @nodoc
+function Context:cancel_pending()
+ for _, cancel in ipairs(self.pending_requests) do
+ cancel()
+ end
+
+ self.pending_requests = {}
+end
+
+--- @nodoc
+function Context:reset()
+ -- Note that the cursor isn't reset here, it needs to survive a `CompleteDone` event.
+ self.isIncomplete = false
+ self.last_request_time = nil
+ self:cancel_pending()
+end
+
+--- @type uv.uv_timer_t?
+local completion_timer = nil
+
+--- @return uv.uv_timer_t
+local function new_timer()
+ return assert(vim.uv.new_timer())
+end
+
+local function reset_timer()
+ if completion_timer then
+ completion_timer:stop()
+ completion_timer:close()
+ end
+
+ completion_timer = nil
+end
+
+--- @param window integer
+--- @param warmup integer
+--- @return fun(sample: number): number
+local function exp_avg(window, warmup)
+ local count = 0
+ local sum = 0
+ local value = 0
+
+ return function(sample)
+ if count < warmup then
+ count = count + 1
+ sum = sum + sample
+ value = sum / count
+ else
+ local factor = 2.0 / (window + 1)
+ value = value * (1 - factor) + sample * factor
+ end
+ return value
+ end
+end
+local compute_new_average = exp_avg(10, 10)
+
+--- @return number
+local function next_debounce()
+ if not Context.last_request_time then
+ return rtt_ms
+ end
+
+ local ms_since_request = (vim.uv.hrtime() - Context.last_request_time) * ns_to_ms
+ return math.max((ms_since_request - rtt_ms) * -1, 0)
+end
+
+--- @param input string Unparsed snippet
+--- @return string # Parsed snippet if successful, else returns its input
+local function parse_snippet(input)
+ local ok, parsed = pcall(function()
+ return lsp._snippet_grammar.parse(input)
+ end)
+ return ok and tostring(parsed) or input
+end
+
+--- @param item lsp.CompletionItem
+--- @param suffix? string
+local function apply_snippet(item, suffix)
+ if item.textEdit then
+ vim.snippet.expand(item.textEdit.newText .. suffix)
+ elseif item.insertText then
+ vim.snippet.expand(item.insertText .. suffix)
+ end
+end
+
+--- Returns text that should be inserted when a selecting completion item. The
+--- precedence is as follows: textEdit.newText > insertText > label
+---
+--- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
+---
+--- @param item lsp.CompletionItem
+--- @return string
+local function get_completion_word(item)
+ if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
+ if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
+ return item.textEdit.newText
+ else
+ return parse_snippet(item.textEdit.newText)
+ end
+ elseif item.insertText ~= nil and item.insertText ~= '' then
+ if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
+ return item.insertText
+ else
+ return parse_snippet(item.insertText)
+ end
+ end
+ return item.label
+end
+
+--- Applies the given defaults to the completion item, modifying it in place.
+---
+--- @param item lsp.CompletionItem
+--- @param defaults lsp.ItemDefaults?
+local function apply_defaults(item, defaults)
+ if not defaults then
+ return
+ end
+
+ item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat
+ item.insertTextMode = item.insertTextMode or defaults.insertTextMode
+ item.data = item.data or defaults.data
+ if defaults.editRange then
+ local textEdit = item.textEdit or {}
+ item.textEdit = textEdit
+ textEdit.newText = textEdit.newText or item.textEditText or item.insertText
+ if defaults.editRange.start then
+ textEdit.range = textEdit.range or defaults.editRange
+ elseif defaults.editRange.insert then
+ textEdit.insert = defaults.editRange.insert
+ textEdit.replace = defaults.editRange.replace
+ end
+ end
+end
+
+--- @param result vim.lsp.CompletionResult
+--- @return lsp.CompletionItem[]
+local function get_items(result)
+ if result.items then
+ -- When we have a list, apply the defaults and return an array of items.
+ for _, item in ipairs(result.items) do
+ ---@diagnostic disable-next-line: param-type-mismatch
+ apply_defaults(item, result.itemDefaults)
+ end
+ return result.items
+ else
+ -- Else just return the items as they are.
+ return result
+ end
+end
+
+--- Turns the result of a `textDocument/completion` request into vim-compatible
+--- |complete-items|.
+---
+--- @private
+--- @param result vim.lsp.CompletionResult Result of `textDocument/completion`
+--- @param prefix string prefix to filter the completion items
+--- @param client_id integer? Client ID
+--- @return table[]
+--- @see complete-items
+function M._lsp_to_complete_items(result, prefix, client_id)
+ local items = get_items(result)
+ if vim.tbl_isempty(items) then
+ return {}
+ end
+
+ local function matches_prefix(item)
+ return vim.startswith(get_completion_word(item), prefix)
+ end
+
+ items = vim.tbl_filter(matches_prefix, items) --[[@as lsp.CompletionItem[]|]]
+ table.sort(items, function(a, b)
+ return (a.sortText or a.label) < (b.sortText or b.label)
+ end)
+
+ local matches = {}
+ for _, item in ipairs(items) do
+ local info = ''
+ local documentation = item.documentation
+ if documentation then
+ if type(documentation) == 'string' and documentation ~= '' then
+ info = documentation
+ elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
+ info = documentation.value
+ else
+ vim.notify(
+ ('invalid documentation value %s'):format(vim.inspect(documentation)),
+ vim.log.levels.WARN
+ )
+ end
+ end
+ local word = get_completion_word(item)
+ table.insert(matches, {
+ word = word,
+ abbr = item.label,
+ kind = protocol.CompletionItemKind[item.kind] or 'Unknown',
+ menu = item.detail or '',
+ info = #info > 0 and info or '',
+ icase = 1,
+ dup = 1,
+ empty = 1,
+ user_data = {
+ nvim = {
+ lsp = {
+ completion_item = item,
+ client_id = client_id,
+ },
+ },
+ },
+ })
+ end
+ return matches
+end
+
+--- @param lnum integer 0-indexed
+--- @param line string
+--- @param items lsp.CompletionItem[]
+--- @param encoding string
+--- @return integer?
+local function adjust_start_col(lnum, line, items, encoding)
+ local min_start_char = nil
+ for _, item in pairs(items) do
+ if item.textEdit and item.textEdit.range.start.line == lnum then
+ if min_start_char and min_start_char ~= item.textEdit.range.start.character then
+ return nil
+ end
+ min_start_char = item.textEdit.range.start.character
+ end
+ end
+ if min_start_char then
+ return lsp.util._str_byteindex_enc(line, min_start_char, encoding)
+ else
+ return nil
+ end
+end
+
+--- @private
+--- @param line string line content
+--- @param lnum integer 0-indexed line number
+--- @param cursor_col integer
+--- @param client_id integer client ID
+--- @param client_start_boundary integer 0-indexed word boundary
+--- @param server_start_boundary? integer 0-indexed word boundary, based on textEdit.range.start.character
+--- @param result vim.lsp.CompletionResult
+--- @param encoding string
+--- @return table[] matches
+--- @return integer? server_start_boundary
+function M._convert_results(
+ line,
+ lnum,
+ cursor_col,
+ client_id,
+ client_start_boundary,
+ server_start_boundary,
+ result,
+ encoding
+)
+ -- Completion response items may be relative to a position different than `client_start_boundary`.
+ -- Concrete example, with lua-language-server:
+ --
+ -- require('plenary.asy|
+ -- ▲ ▲ ▲
+ -- │ │ └── cursor_pos: 20
+ -- │ └────── client_start_boundary: 17
+ -- └────────────── textEdit.range.start.character: 9
+ -- .newText = 'plenary.async'
+ -- ^^^
+ -- prefix (We'd remove everything not starting with `asy`,
+ -- so we'd eliminate the `plenary.async` result
+ --
+ -- `adjust_start_col` is used to prefer the language server boundary.
+ --
+ local candidates = get_items(result)
+ local curstartbyte = adjust_start_col(lnum, line, candidates, encoding)
+ if server_start_boundary == nil then
+ server_start_boundary = curstartbyte
+ elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then
+ server_start_boundary = client_start_boundary
+ end
+ local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
+ local matches = M._lsp_to_complete_items(result, prefix, client_id)
+ return matches, server_start_boundary
+end
+
+--- Implements 'omnifunc' compatible LSP completion.
+---
+--- @see |complete-functions|
+--- @see |complete-items|
+--- @see |CompleteDone|
+---
+--- @param findstart integer 0 or 1, decides behavior
+--- @param base integer findstart=0, text to match against
+---
+--- @return integer|table Decided by {findstart}:
+--- - findstart=0: column where the completion starts, or -2 or -3
+--- - findstart=1: list of matches (actually just calls |complete()|)
+function M._omnifunc(findstart, base)
+ vim.lsp.log.debug('omnifunc.findstart', { findstart = findstart, base = base })
+ assert(base) -- silence luals
+ local bufnr = api.nvim_get_current_buf()
+ local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion })
+ local remaining = #clients
+ if remaining == 0 then
+ return findstart == 1 and -1 or {}
+ end
+
+ local win = api.nvim_get_current_win()
+ local cursor = api.nvim_win_get_cursor(win)
+ local lnum = cursor[1] - 1
+ local cursor_col = cursor[2]
+ local line = api.nvim_get_current_line()
+ local line_to_cursor = line:sub(1, cursor_col)
+ local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$') --[[@as integer]]
+ local server_start_boundary = nil
+ local items = {}
+
+ local function on_done()
+ local mode = api.nvim_get_mode()['mode']
+ if mode == 'i' or mode == 'ic' then
+ vim.fn.complete((server_start_boundary or client_start_boundary) + 1, items)
+ end
+ end
+
+ local util = vim.lsp.util
+ for _, client in ipairs(clients) do
+ local params = util.make_position_params(win, client.offset_encoding)
+ client.request(ms.textDocument_completion, params, function(err, result)
+ if err then
+ lsp.log.warn(err.message)
+ end
+ if result and vim.fn.mode() == 'i' then
+ local matches
+ matches, server_start_boundary = M._convert_results(
+ line,
+ lnum,
+ cursor_col,
+ client.id,
+ client_start_boundary,
+ server_start_boundary,
+ result,
+ client.offset_encoding
+ )
+ vim.list_extend(items, matches)
+ end
+ remaining = remaining - 1
+ if remaining == 0 then
+ vim.schedule(on_done)
+ end
+ end, bufnr)
+ end
+
+ -- Return -2 to signal that we should continue completion so that we can
+ -- async complete.
+ return -2
+end
+
+--- @param clients table<integer, vim.lsp.Client>
+--- @param bufnr integer
+--- @param win integer
+--- @param callback fun(responses: table<integer, { err: lsp.ResponseError, result: vim.lsp.CompletionResult }>)
+--- @return function # Cancellation function
+local function request(clients, bufnr, win, callback)
+ local responses = {} --- @type table<integer, { err: lsp.ResponseError, result: any }>
+ local request_ids = {} --- @type table<integer, integer>
+ local remaining_requests = vim.tbl_count(clients)
+
+ for client_id, client in pairs(clients) do
+ local params = lsp.util.make_position_params(win, client.offset_encoding)
+ local ok, request_id = client.request(ms.textDocument_completion, params, function(err, result)
+ responses[client_id] = { err = err, result = result }
+ remaining_requests = remaining_requests - 1
+ if remaining_requests == 0 then
+ callback(responses)
+ end
+ end, bufnr)
+
+ if ok then
+ request_ids[client_id] = request_id
+ end
+ end
+
+ return function()
+ for client_id, request_id in pairs(request_ids) do
+ local client = lsp.get_client_by_id(client_id)
+ if client then
+ client.cancel_request(request_id)
+ end
+ end
+ end
+end
+
+--- @param handle vim.lsp.completion.BufHandle
+local function on_insert_char_pre(handle)
+ if tonumber(vim.fn.pumvisible()) == 1 then
+ if Context.isIncomplete then
+ reset_timer()
+
+ local debounce_ms = next_debounce()
+ if debounce_ms == 0 then
+ vim.schedule(M.trigger)
+ else
+ completion_timer = new_timer()
+ completion_timer:start(debounce_ms, 0, vim.schedule_wrap(M.trigger))
+ end
+ end
+
+ return
+ end
+
+ local char = api.nvim_get_vvar('char')
+ if not completion_timer and handle.triggers[char] then
+ completion_timer = assert(vim.uv.new_timer())
+ completion_timer:start(25, 0, function()
+ reset_timer()
+ vim.schedule(M.trigger)
+ end)
+ end
+end
+
+local function on_insert_leave()
+ reset_timer()
+ Context.cursor = nil
+ Context:reset()
+end
+
+local function on_complete_done()
+ local completed_item = api.nvim_get_vvar('completed_item')
+ if not completed_item or not completed_item.user_data or not completed_item.user_data.nvim then
+ Context:reset()
+ return
+ end
+
+ local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(0)) --- @type integer, integer
+ cursor_row = cursor_row - 1
+ local completion_item = completed_item.user_data.nvim.lsp.completion_item --- @type lsp.CompletionItem
+ local client_id = completed_item.user_data.nvim.lsp.client_id --- @type integer
+ if not completion_item or not client_id then
+ Context:reset()
+ return
+ end
+
+ local bufnr = api.nvim_get_current_buf()
+ local expand_snippet = completion_item.insertTextFormat == protocol.InsertTextFormat.Snippet
+ and (completion_item.textEdit ~= nil or completion_item.insertText ~= nil)
+
+ Context:reset()
+
+ local client = lsp.get_client_by_id(client_id)
+ if not client then
+ return
+ end
+
+ local offset_encoding = client.offset_encoding or 'utf-16'
+ local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider
+
+ local function clear_word()
+ if not expand_snippet then
+ return nil
+ end
+
+ -- Remove the already inserted word.
+ local start_char = cursor_col - #completed_item.word
+ local line = api.nvim_buf_get_lines(bufnr, cursor_row, cursor_row + 1, true)[1]
+ api.nvim_buf_set_text(bufnr, cursor_row, start_char, cursor_row, #line, { '' })
+ return line:sub(cursor_col + 1)
+ end
+
+ --- @param suffix? string
+ local function apply_snippet_and_command(suffix)
+ if expand_snippet then
+ apply_snippet(completion_item, suffix)
+ end
+
+ local command = completion_item.command
+ if command then
+ client:_exec_cmd(command, { bufnr = bufnr }, nil, function()
+ vim.lsp.log.warn(
+ string.format(
+ 'Language server `%s` does not support command `%s`. This command may require a client extension.',
+ client.name,
+ command.command
+ )
+ )
+ end)
+ end
+ end
+
+ if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then
+ local suffix = clear_word()
+ lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, offset_encoding)
+ apply_snippet_and_command(suffix)
+ elseif resolve_provider and type(completion_item) == 'table' then
+ local changedtick = vim.b[bufnr].changedtick
+
+ --- @param result lsp.CompletionItem
+ client.request(ms.completionItem_resolve, completion_item, function(err, result)
+ if changedtick ~= vim.b[bufnr].changedtick then
+ return
+ end
+
+ local suffix = clear_word()
+ if err then
+ vim.notify_once(err.message, vim.log.levels.WARN)
+ elseif result and result.additionalTextEdits then
+ lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, offset_encoding)
+ if result.command then
+ completion_item.command = result.command
+ end
+ end
+
+ apply_snippet_and_command(suffix)
+ end, bufnr)
+ else
+ local suffix = clear_word()
+ apply_snippet_and_command(suffix)
+ end
+end
+
+--- @class vim.lsp.completion.BufferOpts
+--- @field autotrigger? boolean Whether to trigger completion automatically. Default: false
+
+--- @param client_id integer
+---@param bufnr integer
+---@param opts vim.lsp.completion.BufferOpts
+local function enable_completions(client_id, bufnr, opts)
+ if not buf_handles[bufnr] then
+ buf_handles[bufnr] = { clients = {}, triggers = {} }
+
+ -- Attach to buffer events.
+ api.nvim_buf_attach(bufnr, false, {
+ on_detach = function(_, buf)
+ buf_handles[buf] = nil
+ end,
+ on_reload = function(_, buf)
+ M.enable(true, client_id, buf, opts)
+ end,
+ })
+
+ -- Set up autocommands.
+ local group =
+ api.nvim_create_augroup(string.format('vim/lsp/completion-%d', bufnr), { clear = true })
+ api.nvim_create_autocmd('CompleteDone', {
+ group = group,
+ buffer = bufnr,
+ callback = function()
+ local reason = api.nvim_get_vvar('event').reason --- @type string
+ if reason == 'accept' then
+ on_complete_done()
+ end
+ end,
+ })
+ if opts.autotrigger then
+ api.nvim_create_autocmd('InsertCharPre', {
+ group = group,
+ buffer = bufnr,
+ callback = function()
+ on_insert_char_pre(buf_handles[bufnr])
+ end,
+ })
+ api.nvim_create_autocmd('InsertLeave', {
+ group = group,
+ buffer = bufnr,
+ callback = on_insert_leave,
+ })
+ end
+ end
+
+ if not buf_handles[bufnr].clients[client_id] then
+ local client = lsp.get_client_by_id(client_id)
+ assert(client, 'invalid client ID')
+
+ -- Add the new client to the buffer's clients.
+ buf_handles[bufnr].clients[client_id] = client
+
+ -- Add the new client to the clients that should be triggered by its trigger characters.
+ --- @type string[]
+ local triggers = vim.tbl_get(
+ client.server_capabilities,
+ 'completionProvider',
+ 'triggerCharacters'
+ ) or {}
+ for _, char in ipairs(triggers) do
+ local clients_for_trigger = buf_handles[bufnr].triggers[char]
+ if not clients_for_trigger then
+ clients_for_trigger = {}
+ buf_handles[bufnr].triggers[char] = clients_for_trigger
+ end
+ local client_exists = vim.iter(clients_for_trigger):any(function(c)
+ return c.id == client_id
+ end)
+ if not client_exists then
+ table.insert(clients_for_trigger, client)
+ end
+ end
+ end
+end
+
+--- @param client_id integer
+--- @param bufnr integer
+local function disable_completions(client_id, bufnr)
+ local handle = buf_handles[bufnr]
+ if not handle then
+ return
+ end
+
+ handle.clients[client_id] = nil
+ if not next(handle.clients) then
+ buf_handles[bufnr] = nil
+ api.nvim_del_augroup_by_name(string.format('vim/lsp/completion-%d', bufnr))
+ else
+ for char, clients in pairs(handle.triggers) do
+ --- @param c vim.lsp.Client
+ handle.triggers[char] = vim.tbl_filter(function(c)
+ return c.id ~= client_id
+ end, clients)
+ end
+ end
+end
+
+--- Enables or disables completions from the given language client in the given buffer.
+---
+--- @param enable boolean True to enable, false to disable
+--- @param client_id integer Client ID
+--- @param bufnr integer Buffer handle, or 0 for the current buffer
+--- @param opts? vim.lsp.completion.BufferOpts
+function M.enable(enable, client_id, bufnr, opts)
+ bufnr = (bufnr == 0 and api.nvim_get_current_buf()) or bufnr
+
+ if enable then
+ enable_completions(client_id, bufnr, opts or {})
+ else
+ disable_completions(client_id, bufnr)
+ end
+end
+
+--- Trigger LSP completion in the current buffer.
+function M.trigger()
+ reset_timer()
+ Context:cancel_pending()
+
+ local win = api.nvim_get_current_win()
+ local bufnr = api.nvim_get_current_buf()
+ local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer
+ local line = api.nvim_get_current_line()
+ local line_to_cursor = line:sub(1, cursor_col)
+ local clients = (buf_handles[bufnr] or {}).clients or {}
+ local word_boundary = vim.fn.match(line_to_cursor, '\\k*$')
+ local start_time = vim.uv.hrtime()
+ Context.last_request_time = start_time
+
+ local cancel_request = request(clients, bufnr, win, function(responses)
+ local end_time = vim.uv.hrtime()
+ rtt_ms = compute_new_average((end_time - start_time) * ns_to_ms)
+
+ Context.pending_requests = {}
+ Context.isIncomplete = false
+
+ local row_changed = api.nvim_win_get_cursor(win)[1] ~= cursor_row
+ local mode = api.nvim_get_mode().mode
+ if row_changed or not (mode == 'i' or mode == 'ic') then
+ return
+ end
+
+ local matches = {}
+ local server_start_boundary --- @type integer?
+ for client_id, response in pairs(responses) do
+ if response.err then
+ vim.notify_once(response.err.message, vim.log.levels.warn)
+ end
+
+ local result = response.result
+ if result then
+ Context.isIncomplete = Context.isIncomplete or result.isIncomplete
+ local client = lsp.get_client_by_id(client_id)
+ local encoding = client and client.offset_encoding or 'utf-16'
+ local client_matches
+ client_matches, server_start_boundary = M._convert_results(
+ line,
+ cursor_row - 1,
+ cursor_col,
+ client_id,
+ word_boundary,
+ nil,
+ result,
+ encoding
+ )
+ vim.list_extend(matches, client_matches)
+ end
+ end
+ local start_col = (server_start_boundary or word_boundary) + 1
+ vim.fn.complete(start_col, matches)
+ end)
+
+ table.insert(Context.pending_requests, cancel_request)
+end
+
+return M
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index f9d394642c..38c43893eb 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -3,7 +3,7 @@ local protocol = require('vim.lsp.protocol')
local ms = protocol.Methods
local util = require('vim.lsp.util')
local api = vim.api
-local completion = require('vim.lsp._completion')
+local completion = require('vim.lsp.completion')
--- @type table<string,lsp.Handler>
local M = {}
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 419c2ff644..8ac4cc794b 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -738,14 +738,16 @@ function protocol.make_client_capabilities()
completion = {
dynamicRegistration = false,
completionItem = {
- -- Until we can actually expand snippet, move cursor and allow for true snippet experience,
- -- this should be disabled out of the box.
- -- However, users can turn this back on if they have a snippet plugin.
- snippetSupport = false,
+ snippetSupport = true,
commitCharactersSupport = false,
preselectSupport = false,
deprecatedSupport = false,
documentationFormat = { constants.MarkupKind.Markdown, constants.MarkupKind.PlainText },
+ resolveSupport = {
+ properties = {
+ 'additionalTextEdits',
+ },
+ },
},
completionItemKind = {
valueSet = get_value_set(constants.CompletionItemKind),
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index 2641d1feb0..0ec79e1dc7 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -379,7 +379,7 @@ local function tbl_extend(behavior, deep_extend, ...)
for i = 1, select('#', ...) do
local tbl = select(i, ...)
- vim.validate({ ['after the second argument'] = { tbl, 't' } })
+ vim.validate('after the second argument', tbl, 'table')
--- @cast tbl table<any,any>
if tbl then
for k, v in pairs(tbl) do
diff --git a/runtime/lua/vim/snippet.lua b/runtime/lua/vim/snippet.lua
index 8e384e0f97..8cd454b908 100644
--- a/runtime/lua/vim/snippet.lua
+++ b/runtime/lua/vim/snippet.lua
@@ -2,6 +2,8 @@ local G = vim.lsp._snippet_grammar
local snippet_group = vim.api.nvim_create_augroup('vim/snippet', {})
local snippet_ns = vim.api.nvim_create_namespace('vim/snippet')
local hl_group = 'SnippetTabstop'
+local jump_forward_key = '<tab>'
+local jump_backward_key = '<s-tab>'
--- Returns the 0-based cursor position.
---
@@ -182,6 +184,8 @@ end
--- @field extmark_id integer
--- @field tabstops table<integer, vim.snippet.Tabstop[]>
--- @field current_tabstop vim.snippet.Tabstop
+--- @field tab_keymaps { i: table<string, any>?, s: table<string, any>? }
+--- @field shift_tab_keymaps { i: table<string, any>?, s: table<string, any>? }
local Session = {}
--- Creates a new snippet session in the current buffer.
@@ -197,6 +201,8 @@ function Session.new(bufnr, snippet_extmark, tabstop_data)
extmark_id = snippet_extmark,
tabstops = {},
current_tabstop = Tabstop.new(0, bufnr, { 0, 0, 0, 0 }),
+ tab_keymaps = { i = nil, s = nil },
+ shift_tab_keymaps = { i = nil, s = nil },
}, { __index = Session })
-- Create the tabstops.
@@ -207,9 +213,64 @@ function Session.new(bufnr, snippet_extmark, tabstop_data)
end
end
+ self:set_keymaps()
+
return self
end
+--- Sets the snippet navigation keymaps.
+---
+--- @package
+function Session:set_keymaps()
+ local function maparg(key, mode)
+ local map = vim.fn.maparg(key, mode, false, true) --[[ @as table ]]
+ if not vim.tbl_isempty(map) and map.buffer == 1 then
+ return map
+ else
+ return nil
+ end
+ end
+
+ local function set(jump_key, direction)
+ vim.keymap.set({ 'i', 's' }, jump_key, function()
+ return vim.snippet.active({ direction = direction })
+ and '<cmd>lua vim.snippet.jump(' .. direction .. ')<cr>'
+ or jump_key
+ end, { expr = true, silent = true, buffer = self.bufnr })
+ end
+
+ self.tab_keymaps = {
+ i = maparg(jump_forward_key, 'i'),
+ s = maparg(jump_forward_key, 's'),
+ }
+ self.shift_tab_keymaps = {
+ i = maparg(jump_backward_key, 'i'),
+ s = maparg(jump_backward_key, 's'),
+ }
+ set(jump_forward_key, 1)
+ set(jump_backward_key, -1)
+end
+
+--- Restores/deletes the keymaps used for snippet navigation.
+---
+--- @package
+function Session:restore_keymaps()
+ local function restore(keymap, lhs, mode)
+ if keymap then
+ vim.api.nvim_buf_call(self.bufnr, function()
+ vim.fn.mapset(keymap)
+ end)
+ else
+ vim.api.nvim_buf_del_keymap(self.bufnr, mode, lhs)
+ end
+ end
+
+ restore(self.tab_keymaps.i, jump_forward_key, 'i')
+ restore(self.tab_keymaps.s, jump_forward_key, 's')
+ restore(self.shift_tab_keymaps.i, jump_backward_key, 'i')
+ restore(self.shift_tab_keymaps.s, jump_backward_key, 's')
+end
+
--- Returns the destination tabstop index when jumping in the given direction.
---
--- @package
@@ -628,6 +689,8 @@ function M.stop()
return
end
+ M._session:restore_keymaps()
+
vim.api.nvim_clear_autocmds({ group = snippet_group, buffer = M._session.bufnr })
vim.api.nvim_buf_clear_namespace(M._session.bufnr, snippet_ns, 0, -1)
diff --git a/scripts/gen_vimdoc.lua b/scripts/gen_vimdoc.lua
index b88bdff99b..dac6c6f461 100755
--- a/scripts/gen_vimdoc.lua
+++ b/scripts/gen_vimdoc.lua
@@ -273,6 +273,7 @@ local config = {
'buf.lua',
'diagnostic.lua',
'codelens.lua',
+ 'completion.lua',
'inlay_hint.lua',
'tagfunc.lua',
'semantic_tokens.lua',
diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c
index f18dc0f747..cc2608433d 100644
--- a/src/nvim/ex_getln.c
+++ b/src/nvim/ex_getln.c
@@ -2532,6 +2532,10 @@ static bool cmdpreview_may_show(CommandLineState *s)
goto end;
}
+ // Flush now: external cmdline may itself wish to update the screen which is
+ // currently disallowed during cmdpreview(no longer needed in case that changes).
+ cmdline_ui_flush();
+
// Swap invalid command range if needed
if ((ea.argt & EX_RANGE) && ea.line1 > ea.line2) {
linenr_T lnum = ea.line1;
diff --git a/src/nvim/main.c b/src/nvim/main.c
index 17a0bbf082..cf1324d37f 100644
--- a/src/nvim/main.c
+++ b/src/nvim/main.c
@@ -1100,23 +1100,13 @@ static void command_line_scan(mparm_T *parmp)
// set stdout to binary to avoid crlf in --api-info output
_setmode(STDOUT_FILENO, _O_BINARY);
#endif
- FileDescriptor fp;
- const int fof_ret = file_open_fd(&fp, STDOUT_FILENO,
- kFileWriteOnly);
- if (fof_ret != 0) {
- semsg(_("E5421: Failed to open stdin: %s"), os_strerror(fof_ret));
- }
String data = api_metadata_raw();
- const ptrdiff_t written_bytes = file_write(&fp, data.data, data.size);
+ const ptrdiff_t written_bytes = os_write(STDOUT_FILENO, data.data, data.size, false);
if (written_bytes < 0) {
- msgpack_file_write_error((int)written_bytes);
+ semsg(_("E5420: Failed to write to file: %s"), os_strerror((int)written_bytes));
}
- const int ff_ret = file_flush(&fp);
- if (ff_ret < 0) {
- msgpack_file_write_error(ff_ret);
- }
os_exit(0);
} else if (STRICMP(argv[0] + argv_idx, "headless") == 0) {
headless_mode = true;
diff --git a/src/nvim/os/fileio.c b/src/nvim/os/fileio.c
index 4575c394b8..e58eb96c2e 100644
--- a/src/nvim/os/fileio.c
+++ b/src/nvim/os/fileio.c
@@ -385,14 +385,3 @@ ptrdiff_t file_skip(FileDescriptor *const fp, const size_t size)
return (ptrdiff_t)read_bytes;
}
-
-/// Print error which occurs when failing to write msgpack data
-///
-/// @param[in] error Error code of the error to print.
-///
-/// @return -1 (error return for msgpack_packer callbacks).
-int msgpack_file_write_error(const int error)
-{
- semsg(_("E5420: Failed to write to file: %s"), os_strerror(error));
- return -1;
-}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 3ae11bd1a7..8db05ac839 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -19,7 +19,7 @@ if(LUA_HAS_FFI)
${TEST_OPTIONS}
-P ${PROJECT_SOURCE_DIR}/cmake/RunTests.cmake
USES_TERMINAL)
- add_dependencies(unittest lua-dev-deps nvim)
+ add_dependencies(unittest lua_dev_deps nvim)
else()
message(WARNING "disabling unit tests: no Luajit FFI in ${LUA_PRG}")
endif()
@@ -35,7 +35,7 @@ add_custom_target(functionaltest
-P ${PROJECT_SOURCE_DIR}/cmake/RunTests.cmake
DEPENDS printenv-test printargs-test shell-test pwsh-test streams-test tty-test
USES_TERMINAL)
-add_dependencies(functionaltest lua-dev-deps nvim)
+add_dependencies(functionaltest lua_dev_deps nvim)
add_custom_target(benchmark
COMMAND ${CMAKE_COMMAND}
@@ -44,4 +44,4 @@ add_custom_target(benchmark
-P ${PROJECT_SOURCE_DIR}/cmake/RunTests.cmake
DEPENDS tty-test
USES_TERMINAL)
-add_dependencies(benchmark lua-dev-deps nvim)
+add_dependencies(benchmark lua_dev_deps nvim)
diff --git a/test/functional/lua/diagnostic_spec.lua b/test/functional/lua/diagnostic_spec.lua
index 05082bc132..a4f882e363 100644
--- a/test/functional/lua/diagnostic_spec.lua
+++ b/test/functional/lua/diagnostic_spec.lua
@@ -843,17 +843,18 @@ describe('vim.diagnostic', function()
end)
end)
- describe('get_next_pos()', function()
+ describe('get_next()', 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()
- ]]
+ vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, {
+ make_error('Diagnostic #1', 1, 1, 1, 1),
+ })
+ vim.api.nvim_win_set_buf(0, diagnostic_bufnr)
+ local next = vim.diagnostic.get_next()
+ return { next.lnum, next.col }
+ ]]
)
end)
@@ -861,14 +862,15 @@ describe('vim.diagnostic', 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 }
- ]]
+ 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})
+ local next = vim.diagnostic.get_next({ namespace = diagnostic_ns })
+ return { next.lnum, next.col }
+ ]]
)
end)
@@ -876,27 +878,29 @@ describe('vim.diagnostic', 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 }
- ]]
+ 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})
+ local next = vim.diagnostic.get_next({ namespace = diagnostic_ns })
+ return { next.lnum, next.col }
+ ]]
)
end)
it('will not cycle when wrap is off', function()
eq(
- false,
+ vim.NIL,
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 }
- ]]
+ 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})
+ local next = vim.diagnostic.get_next({ namespace = diagnostic_ns, wrap = false })
+ return next
+ ]]
)
end)
@@ -904,13 +908,14 @@ describe('vim.diagnostic', 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 }
- ]]
+ 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})
+ local prev = vim.diagnostic.get_prev({ namespace = diagnostic_ns })
+ return { prev.lnum, prev.col }
+ ]]
)
end)
@@ -918,15 +923,16 @@ describe('vim.diagnostic', function()
eq(
{ 4, 0 },
exec_lua [[
- vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, {
- make_error('Diagnostic #1', 3, 9001, 3, 9001),
- make_error('Diagnostic #2', 4, 0, 4, 0),
- })
- vim.api.nvim_win_set_buf(0, diagnostic_bufnr)
- vim.api.nvim_win_set_cursor(0, {1, 1})
- vim.diagnostic.goto_next { float = false }
- return vim.diagnostic.get_next_pos { namespace = diagnostic_ns }
- ]]
+ vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, {
+ make_error('Diagnostic #1', 3, 9001, 3, 9001),
+ make_error('Diagnostic #2', 4, 0, 4, 0),
+ })
+ vim.api.nvim_win_set_buf(0, diagnostic_bufnr)
+ vim.api.nvim_win_set_cursor(0, {1, 1})
+ vim.diagnostic.jump({ count = 1, float = false })
+ local next = vim.diagnostic.get_next({ namespace = diagnostic_ns })
+ return { next.lnum, next.col }
+ ]]
)
end)
@@ -935,13 +941,14 @@ describe('vim.diagnostic', function()
{ 4, 0 },
exec_lua [[
vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, {
- make_error('Diagnostic #1', 3, 9001, 3, 9001),
- make_error('Diagnostic #2', 4, -1, 4, -1),
+ make_error('Diagnostic #1', 3, 9001, 3, 9001),
+ make_error('Diagnostic #2', 4, -1, 4, -1),
})
vim.api.nvim_win_set_buf(0, diagnostic_bufnr)
vim.api.nvim_win_set_cursor(0, {1, 1})
- vim.diagnostic.goto_next { float = false }
- return vim.diagnostic.get_next_pos { namespace = diagnostic_ns }
+ vim.diagnostic.jump({ count = 1, float = false })
+ local next = vim.diagnostic.get_next({ namespace = diagnostic_ns })
+ return { next.lnum, next.col }
]]
)
end)
@@ -962,7 +969,7 @@ describe('vim.diagnostic', function()
eq(
{ 3, 0 },
exec_lua([[
- vim.diagnostic.goto_next({_highest = true})
+ vim.diagnostic.jump({ count = 1, _highest = true })
return vim.api.nvim_win_get_cursor(0)
]])
)
@@ -970,7 +977,7 @@ describe('vim.diagnostic', function()
eq(
{ 5, 0 },
exec_lua([[
- vim.diagnostic.goto_next({_highest = true})
+ vim.diagnostic.jump({ count = 1, _highest = true })
return vim.api.nvim_win_get_cursor(0)
]])
)
@@ -991,7 +998,7 @@ describe('vim.diagnostic', function()
eq(
{ 4, 0 },
exec_lua([[
- vim.diagnostic.goto_next({_highest = true})
+ vim.diagnostic.jump({ count = 1, _highest = true })
return vim.api.nvim_win_get_cursor(0)
]])
)
@@ -999,7 +1006,7 @@ describe('vim.diagnostic', function()
eq(
{ 6, 0 },
exec_lua([[
- vim.diagnostic.goto_next({_highest = true})
+ vim.diagnostic.jump({ count = 1, _highest = true })
return vim.api.nvim_win_get_cursor(0)
]])
)
@@ -1021,7 +1028,7 @@ describe('vim.diagnostic', function()
eq(
{ 2, 0 },
exec_lua([[
- vim.diagnostic.goto_next()
+ vim.diagnostic.jump({ count = 1 })
return vim.api.nvim_win_get_cursor(0)
]])
)
@@ -1029,7 +1036,7 @@ describe('vim.diagnostic', function()
eq(
{ 3, 0 },
exec_lua([[
- vim.diagnostic.goto_next()
+ vim.diagnostic.jump({ count = 1 })
return vim.api.nvim_win_get_cursor(0)
]])
)
@@ -1037,40 +1044,42 @@ describe('vim.diagnostic', function()
eq(
{ 4, 0 },
exec_lua([[
- vim.diagnostic.goto_next()
+ vim.diagnostic.jump({ count = 1 })
return vim.api.nvim_win_get_cursor(0)
]])
)
end)
end)
- describe('get_prev_pos()', function()
- it('can find the prev pos with only one namespace', function()
+ describe('get_prev()', function()
+ it('can find the previous diagnostic 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()
- ]]
+ 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})
+ local prev = vim.diagnostic.get_prev()
+ return { prev.lnum, prev.col }
+ ]]
)
end)
- it('can find prev pos with two errors', function()
+ it('can find the previous diagnostic 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 }
- ]]
+ 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})
+ local prev = vim.diagnostic.get_prev({ namespace = diagnostic_ns })
+ return { prev.lnum, prev.col }
+ ]]
)
end)
@@ -1078,27 +1087,29 @@ describe('vim.diagnostic', 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 }
- ]]
+ 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})
+ local prev = vim.diagnostic.get_prev({ namespace = diagnostic_ns })
+ return { prev.lnum, prev.col }
+ ]]
)
end)
it('respects wrap parameter', function()
eq(
- false,
+ vim.NIL,
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}
- ]]
+ 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})
+ local prev = vim.diagnostic.get_prev({ namespace = diagnostic_ns, wrap = false })
+ return prev
+ ]]
)
end)
@@ -1126,6 +1137,118 @@ describe('vim.diagnostic', function()
end)
end)
+ describe('jump()', function()
+ before_each(function()
+ exec_lua([[
+ vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, {
+ make_error('Diagnostic #1', 0, 0, 0, 2),
+ make_error('Diagnostic #2', 1, 1, 1, 4),
+ make_warning('Diagnostic #3', 2, -1, 2, -1),
+ make_info('Diagnostic #4', 3, 0, 3, 3),
+ })
+ vim.api.nvim_win_set_buf(0, diagnostic_bufnr)
+ ]])
+ end)
+
+ it('can move forward', function()
+ eq(
+ { 2, 1 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 1, 0 })
+ vim.diagnostic.jump({ count = 1 })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+
+ eq(
+ { 4, 0 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 1, 0 })
+ vim.diagnostic.jump({ count = 3 })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+
+ eq(
+ { 4, 0 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 1, 0 })
+ vim.diagnostic.jump({ count = math.huge, wrap = false })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+ end)
+
+ it('can move backward', function()
+ eq(
+ { 3, 0 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 4, 0 })
+ vim.diagnostic.jump({ count = -1 })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+
+ eq(
+ { 1, 0 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 4, 0 })
+ vim.diagnostic.jump({ count = -3 })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+
+ eq(
+ { 1, 0 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 4, 0 })
+ vim.diagnostic.jump({ count = -math.huge, wrap = false })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+ end)
+
+ it('can filter by severity', function()
+ eq(
+ { 3, 0 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 1, 0 })
+ vim.diagnostic.jump({ count = 1, severity = vim.diagnostic.severity.WARN })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+
+ eq(
+ { 3, 0 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 1, 0 })
+ vim.diagnostic.jump({ count = 9999, severity = vim.diagnostic.severity.WARN })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+ end)
+
+ it('can wrap', function()
+ eq(
+ { 1, 0 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 4, 0 })
+ vim.diagnostic.jump({ count = 1, wrap = true })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+
+ eq(
+ { 4, 0 },
+ exec_lua([[
+ vim.api.nvim_win_set_cursor(0, { 1, 0 })
+ vim.diagnostic.jump({ count = -1, wrap = true })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+ )
+ 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})]])
diff --git a/test/functional/lua/snippet_spec.lua b/test/functional/lua/snippet_spec.lua
index 413aa93994..bca0a59cb4 100644
--- a/test/functional/lua/snippet_spec.lua
+++ b/test/functional/lua/snippet_spec.lua
@@ -1,3 +1,5 @@
+---@diagnostic disable: no-unknown
+
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
@@ -16,11 +18,6 @@ local retry = t.retry
describe('vim.snippet', function()
before_each(function()
clear()
-
- exec_lua([[
- vim.keymap.set({ 'i', 's' }, '<Tab>', function() vim.snippet.jump(1) end, { buffer = true })
- vim.keymap.set({ 'i', 's' }, '<S-Tab>', function() vim.snippet.jump(-1) end, { buffer = true })
- ]])
end)
after_each(clear)
@@ -286,4 +283,24 @@ describe('vim.snippet', function()
]]
)
end)
+
+ it('restores snippet navigation keymaps', function()
+ -- Create a buffer keymap in insert mode that deletes all lines.
+ local curbuf = api.nvim_get_current_buf()
+ exec_lua('vim.api.nvim_buf_set_keymap(..., "i", "<Tab>", "<cmd>normal ggdG<cr>", {})', curbuf)
+
+ test_expand_success({ 'var $1 = $2' }, { 'var = ' })
+
+ -- While the snippet is active, <Tab> should navigate between tabstops.
+ feed('x')
+ poke_eventloop()
+ feed('<Tab>0')
+ eq({ 'var x = 0' }, buf_lines(0))
+
+ exec_lua('vim.snippet.stop()')
+
+ -- After exiting the snippet, the buffer keymap should be restored.
+ feed('<Esc>O<cr><Tab>')
+ eq({ '' }, buf_lines(0))
+ end)
end)
diff --git a/test/functional/lua/ui_event_spec.lua b/test/functional/lua/ui_event_spec.lua
index 1e80c88403..0a6deaa41c 100644
--- a/test/functional/lua/ui_event_spec.lua
+++ b/test/functional/lua/ui_event_spec.lua
@@ -37,6 +37,9 @@ describe('vim.ui_attach', function()
[2] = { bold = true },
[3] = { background = Screen.colors.Grey },
[4] = { background = Screen.colors.LightMagenta },
+ [5] = { reverse = true },
+ [6] = { reverse = true, bold = true },
+ [7] = { background = Screen.colors.Yellow1 },
})
screen:attach()
end)
@@ -188,6 +191,56 @@ describe('vim.ui_attach', function()
feed('version<CR><CR>v<Esc>')
n.assert_alive()
end)
+
+ it("preserved 'incsearch/command' screen state after :redraw from ext_cmdline", function()
+ exec_lua([[
+ vim.cmd.norm('ifoobar')
+ vim.cmd('1split cmdline')
+ local buf = vim.api.nvim_get_current_buf()
+ vim.cmd.wincmd('p')
+ vim.ui_attach(ns, { ext_cmdline = true }, function(event, ...)
+ if event == 'cmdline_show' then
+ local content = select(1, ...)
+ vim.api.nvim_buf_set_lines(buf, -2, -1, false, {content[1][2]})
+ vim.cmd('redraw')
+ end
+ return true
+ end)
+ ]])
+ -- Updates a cmdline window
+ feed(':cmdline')
+ screen:expect({
+ grid = [[
+ cmdline |
+ {5:cmdline [+] }|
+ fooba^r |
+ {6:[No Name] [+] }|
+ |
+ ]],
+ })
+ -- Does not clear 'incsearch' highlighting
+ feed('<Esc>/foo')
+ screen:expect({
+ grid = [[
+ foo |
+ {5:cmdline [+] }|
+ {5:foo}ba^r |
+ {6:[No Name] [+] }|
+ |
+ ]],
+ })
+ -- Shows new cmdline state during 'inccommand'
+ feed('<Esc>:%s/bar/baz')
+ screen:expect({
+ grid = [[
+ %s/bar/baz |
+ {5:cmdline [+] }|
+ foo{7:ba^z} |
+ {6:[No Name] [+] }|
+ |
+ ]],
+ })
+ end)
end)
describe('vim.ui_attach', function()
diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua
index 2798d57381..5b7232ad7e 100644
--- a/test/functional/plugin/lsp/completion_spec.lua
+++ b/test/functional/plugin/lsp/completion_spec.lua
@@ -1,9 +1,16 @@
---@diagnostic disable: no-unknown
local t = require('test.testutil')
+local t_lsp = require('test.functional.plugin.lsp.testutil')
local n = require('test.functional.testnvim')()
+local clear = n.clear
local eq = t.eq
+local neq = t.neq
local exec_lua = n.exec_lua
+local feed = n.feed
+local retry = t.retry
+
+local create_server_definition = t_lsp.create_server_definition
--- Convert completion results.
---
@@ -21,10 +28,11 @@ local function complete(line, candidates, lnum)
local line, cursor_col, lnum, result = ...
local line_to_cursor = line:sub(1, cursor_col)
local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$')
- local items, server_start_boundary = require("vim.lsp._completion")._convert_results(
+ local items, server_start_boundary = require("vim.lsp.completion")._convert_results(
line,
lnum,
cursor_col,
+ 1,
client_start_boundary,
nil,
result,
@@ -42,7 +50,7 @@ local function complete(line, candidates, lnum)
)
end
-describe('vim.lsp._completion', function()
+describe('vim.lsp.completion: item conversion', function()
before_each(n.clear)
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
@@ -159,6 +167,7 @@ describe('vim.lsp._completion', function()
end, result.items)
eq(expected, result)
end)
+
it('uses correct start boundary', function()
local completion_list = {
isIncomplete = false,
@@ -186,6 +195,7 @@ describe('vim.lsp._completion', function()
dup = 1,
empty = 1,
icase = 1,
+ info = '',
kind = 'Module',
menu = '',
word = 'this_thread',
@@ -240,6 +250,7 @@ describe('vim.lsp._completion', function()
dup = 1,
empty = 1,
icase = 1,
+ info = '',
kind = 'Module',
menu = '',
word = 'this_thread',
@@ -278,4 +289,224 @@ describe('vim.lsp._completion', function()
eq('item-property-has-priority', item.data)
eq({ line = 1, character = 1 }, item.textEdit.range.start)
end)
+
+ it(
+ 'uses insertText as textEdit.newText if there are editRange defaults but no textEditText',
+ function()
+ --- @type lsp.CompletionList
+ local completion_list = {
+ isIncomplete = false,
+ itemDefaults = {
+ editRange = {
+ start = { line = 1, character = 1 },
+ ['end'] = { line = 1, character = 4 },
+ },
+ insertTextFormat = 2,
+ data = 'foobar',
+ },
+ items = {
+ {
+ insertText = 'the-insertText',
+ label = 'hello',
+ data = 'item-property-has-priority',
+ },
+ },
+ }
+ local result = complete('|', completion_list)
+ eq(1, #result.items)
+ local text = result.items[1].user_data.nvim.lsp.completion_item.textEdit.newText
+ eq('the-insertText', text)
+ end
+ )
+end)
+
+describe('vim.lsp.completion: protocol', function()
+ before_each(function()
+ clear()
+ exec_lua(create_server_definition)
+ exec_lua([[
+ _G.capture = {}
+ vim.fn.complete = function(col, matches)
+ _G.capture.col = col
+ _G.capture.matches = matches
+ end
+ ]])
+ end)
+
+ after_each(clear)
+
+ --- @param completion_result lsp.CompletionList
+ --- @return integer
+ local function create_server(completion_result)
+ return exec_lua(
+ [[
+ local result = ...
+ local server = _create_server({
+ capabilities = {
+ completionProvider = {
+ triggerCharacters = { '.' }
+ }
+ },
+ handlers = {
+ ['textDocument/completion'] = function(_, _, callback)
+ callback(nil, result)
+ end
+ }
+ })
+
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ return vim.lsp.start({ name = 'dummy', cmd = server.cmd, on_attach = function(client, bufnr)
+ vim.lsp.completion.enable(true, client.id, bufnr)
+ end})
+ ]],
+ completion_result
+ )
+ end
+
+ local function assert_matches(fn)
+ retry(nil, nil, function()
+ fn(exec_lua('return _G.capture.matches'))
+ end)
+ end
+
+ --- @param pos { [1]: integer, [2]: integer }
+ local function trigger_at_pos(pos)
+ exec_lua(
+ [[
+ local win = vim.api.nvim_get_current_win()
+ vim.api.nvim_win_set_cursor(win, ...)
+ vim.lsp.completion.trigger()
+ ]],
+ pos
+ )
+
+ retry(nil, nil, function()
+ neq(nil, exec_lua('return _G.capture.col'))
+ end)
+ end
+
+ it('fetches completions and shows them using complete on trigger', function()
+ create_server({
+ isIncomplete = false,
+ items = {
+ {
+ label = 'hello',
+ },
+ },
+ })
+
+ feed('ih')
+ trigger_at_pos({ 1, 1 })
+
+ assert_matches(function(matches)
+ eq({
+ {
+ abbr = 'hello',
+ dup = 1,
+ empty = 1,
+ icase = 1,
+ info = '',
+ kind = 'Unknown',
+ menu = '',
+ user_data = {
+ nvim = {
+ lsp = {
+ client_id = 1,
+ completion_item = {
+ label = 'hello',
+ },
+ },
+ },
+ },
+ word = 'hello',
+ },
+ }, matches)
+ end)
+ end)
+
+ it('merges results from multiple clients', function()
+ create_server({
+ isIncomplete = false,
+ items = {
+ {
+ label = 'hello',
+ },
+ },
+ })
+ create_server({
+ isIncomplete = false,
+ items = {
+ {
+ label = 'hallo',
+ },
+ },
+ })
+
+ feed('ih')
+ trigger_at_pos({ 1, 1 })
+
+ assert_matches(function(matches)
+ eq(2, #matches)
+ eq('hello', matches[1].word)
+ eq('hallo', matches[2].word)
+ end)
+ end)
+
+ it('executes commands', function()
+ local completion_list = {
+ isIncomplete = false,
+ items = {
+ {
+ label = 'hello',
+ command = {
+ arguments = { '1', '0' },
+ command = 'dummy',
+ title = '',
+ },
+ },
+ },
+ }
+ local client_id = create_server(completion_list)
+
+ exec_lua(
+ [[
+ _G.called = false
+ local client = vim.lsp.get_client_by_id(...)
+ client.commands.dummy = function ()
+ _G.called = true
+ end
+ ]],
+ client_id
+ )
+
+ feed('ih')
+ trigger_at_pos({ 1, 1 })
+
+ exec_lua(
+ [[
+ local client_id, item = ...
+ vim.v.completed_item = {
+ user_data = {
+ nvim = {
+ lsp = {
+ client_id = client_id,
+ completion_item = item
+ }
+ }
+ }
+ }
+ ]],
+ client_id,
+ completion_list.items[1]
+ )
+
+ feed('<C-x><C-o><C-y>')
+
+ assert_matches(function(matches)
+ eq(1, #matches)
+ eq('hello', matches[1].word)
+ eq(true, exec_lua('return _G.called'))
+ end)
+ end)
end)