Compare commits

...

58 Commits

Author SHA1 Message Date
0159e4daae NVIM v0.5.1
BREAKING CHANGES:
d83df7f7b5 feat(lua)!: register_keystroke_callback => on_key
cd8f6c5fb7 feat(lsp)!: change handler signature #15504

FEATURES:
915dda3f96 feat(jobstart): add parameter to close stdin

FIXES:
f8e0011534 #15732 fix(inccommand): ignore trailing commands only for *previewed* command
2132c063af backport: fix(windowing): positioning of relative floats
51d6b26729 #15495 backport: tests(lua/on_yank): assert conditions that fail correctly
f7002337c0 #15482 backport: fix(lua): verify buffer in highlight.on_yank
6bda2f56eb #15454 backport: fix(window.c): win_close from other tabpage
be58ba250e #15372 backport: fix(autocmd.c): fix conditions in block_autocmds, unblock_autocmds
d0e9a11e39 backport: refactor(sign): include longer sign column option
5c42376c15 backport: fix(sign): reset auto sign column with minimum in float win minimal style
41f761130e backport: fix(decorations): crash when :bdelete (extmark_free_all) after clear_namespace
cf62554e5a #15111 backport: fix(:source): copy curbuf lines to memory before sourcing
6436100b6e #14809 backport: fix(:source, nvim_exec): handle Vimscript line continuations
917f306666 #15043 backport: test/memory_usage_spec: skip on MacOS
a9cca1b050 #14984 backport: fixup(clipboard): Fix error not properly handled
ae89330ec0 #14982 backport: fix(vim.opt): vimL map string values not trimmed
2229e99ef9 #14962 backport: fixup(clipboard): Use case matching
b6b12ea7c3 #15489 fix(man.vim): filetype=man is too eager
6f965f41df build: use RelWithDebInfo build for nightlies, Release for releases
f027c5e1e4 build: update appdata.xml version in release commit
8336488ce1 test(treesitter): skip all parsers tests if parsers aren't installed
008b83f5a2 Rename stdin to stdin_mode (fixes Windows build)

FIXES (LSP):
132053c1d2 #15523 backport: fix(lsp): resolve bufnr in buf_is_attached
a265201307 backport: fix(lsp): Ensure human readable errors are printed
33000bd9cf backport: fix(lsp): Ensure users get feedback on references/symbols errors or empty results
9f73b7c214 #14954 backport: fix(lsp): correctly check for windows in lsp logger
eaa1c47377 #15023 backport: fix(lsp): restore diagnostics extmarks that were moved to the last edit line
989ccb8222 #15011 backport: fix(lsp): restore diagnostics extmarks on buffer changes
2ae4c96d91 backport: fix(lsp): prevent double <text> for cached plaintext markup
7b0ae589f0 feat(lsp): allow root_dir to be nil (#15430) (Mathias Fußenegger)
8ec5bc9126 lsp(start_client): Allow passing custom workspaceFolders to the LSP (#15132) (sim)
959cf5e53c fix(lsp): check if buffer is valid in changetracking (#15505) (Jose Alvarez)
dc15b3a92c fix(lsp): avoid ipairs on non-sequential tables (#15059) (Michael Lingelbach)
18375c6df6 feat(lsp): improve vim.lsp.util.apply_text_edits (#15561) (hrsh7th)
7b1315fe61 feat(lsp): improve logging (#15636) (Michael Lingelbach)
2021-09-26 15:15:30 -07:00
c9e4f86b75 fix(release.sh): ignore failed "rm" 2021-09-26 15:15:30 -07:00
14cdaca6a8 fix(lsp): fix handler signature, tests
- not necessary on master: got lost in the vim.lsp.diagnostic => vim.diagnostic migration
- fix tests which accidentally depended on previous session
- ref #15504
2021-09-26 15:15:03 -07:00
7b0ae589f0 feat(lsp): allow root_dir to be nil (#15430)
According to the protocol definition `rootPath`, `rootUri` and
`workspaceFolders` are allowed to be null.

Some language servers utilize this to provide "single file" support.
If all three are null, they don't attempt to index a directory but
instead only provide capabilities for a single file.
2021-09-26 11:28:28 -07:00
sim
8ec5bc9126 lsp(start_client): Allow passing custom workspaceFolders to the LSP (#15132)
Some language servers *cough*rust-analyzer*cough* need an empty/custom
workspaceFolders for certain usecases. For example, rust-analyzer
needs an empty workspaceFolders table for standalone file support
(See https://github.com/rust-analyzer/rust-analyzer/pull/8955).

This can also be useful for other languages that need to commonly
open a certain directory (like flutter or lua), which would help
prevent spinning up a new language server altogether.

In case no workspaceFolders are passed, we fallback to what we had
before.
2021-09-26 11:28:28 -07:00
959cf5e53c fix(lsp): check if buffer is valid in changetracking (#15505) 2021-09-26 11:28:28 -07:00
dc15b3a92c fix(lsp): avoid ipairs on non-sequential tables (#15059)
ipairs terminates on the first nil index when iterating over table keys:

for i,k in ipairs( {[1] = 'test', [3] = 'test'} ) do
  print(i, k)
end

prints:
1 test

Instead, use pairs which continues iterating over the entire table:

for i,k in pairs( {[1] = 'test', [3] = 'test'} ) do
  print(i, k)
end

prints:
1 test
3 test
2021-09-26 11:28:28 -07:00
18375c6df6 feat(lsp): improve vim.lsp.util.apply_text_edits (#15561)
- Fix the cursor position after applying TextEdits
- Support reversed range of TextEdit
- Invoke nvim_buf_set_text one by one
2021-09-26 11:28:28 -07:00
7b1315fe61 feat(lsp): improve logging (#15636)
* Simplify rpc encode/decode messages to rpc.send/rcp.receive
* Make missing handlers message throw a warning
* Clean up formatting style in log
* Move all non-RPC loop messages to trace instead of debug
* Add format func option to log to allow newlines in per log entry
2021-09-26 11:28:28 -07:00
27bac13be6 fix(lsp): update lsp-handler signature in call_hierarchy (#15738)
This fixes the handler signature and also prevents n+1 requests firing
if there are multiple clients.

(The first `prepareCallHierarchy` handler is called once per client,
each invocation used `buf_request` to make more requests using *all*
clients)
2021-09-26 10:25:17 -07:00
d26d489e2e fix(lsp): adapt codelens resolve to handler signature change (#15578)
Follow up to https://github.com/neovim/neovim/pull/15504
2021-09-26 10:25:17 -07:00
a6eab6e25e fix(lsp): update workspace/applyEdit handler signature (#15573) 2021-09-26 10:25:17 -07:00
cd8f6c5fb7 feat(lsp)!: change handler signature #15504 2021-09-26 10:25:17 -07:00
f8e0011534 fix(inccommand): ignore trailing commands only for *previewed* command #15732
Since the `State` is global, other scripts are unexpectedly affected during the
'inccommand' preview. This commit introduces a new flag for `do_cmdline`, in
order to ignore trailing '|'-separated commands only for the command invoking
the preview.

fix #8796, update #7494

Co-authored-by: itchyny <itchyny@hatena.ne.jp>
2021-09-20 08:42:18 -07:00
7d67bd5865 Merge #15677 release-0.5: backports 2021-09-16 12:00:13 -07:00
2132c063af backport: fix(windowing): positioning of relative floats
Fix relative floating windows so that they open in the correct position
relative to each other. Also make sure that their positions are correct
immediately after creation without a redraw.
2021-09-16 15:00:31 +01:00
132053c1d2 backport: fix(lsp): resolve bufnr in buf_is_attached (#15523) 2021-09-16 14:53:45 +01:00
51d6b26729 backport: tests(lua/on_yank): assert conditions that fail correctly #15495
The test added in 274a3504a7
does not fail if the code changes are reverted.
2021-09-16 14:46:52 +01:00
f7002337c0 backport: fix(lua): verify buffer in highlight.on_yank (#15482)
Resolve an issue with deferred clearing of highlight failing if the
buffer is deleted before the timeout by checking whether the
buffer is valid first.
2021-09-16 14:46:27 +01:00
6bda2f56eb backport: fix(window.c): win_close from other tabpage #15454
Fix #15313
2021-09-16 14:44:59 +01:00
be58ba250e backport: fix(autocmd.c): fix conditions in block_autocmds, unblock_autocmds #15372
Logic got swapped in 7574918dc7.
We didn't notice it since v:termresponse isn't really used yet. #6279
2021-09-16 14:43:36 +01:00
d0e9a11e39 backport: refactor(sign): include longer sign column option 2021-09-16 14:42:22 +01:00
5c42376c15 backport: fix(sign): reset auto sign column with minimum in float win minimal style 2021-09-16 14:42:06 +01:00
41f761130e backport: fix(decorations): crash when :bdelete (extmark_free_all) after clear_namespace
fixes #15212
2021-09-16 14:40:32 +01:00
a265201307 backport: fix(lsp): Ensure human readable errors are printed
`return err_message(tostring(err))` caused errors to be printed as
`table: 0x123456789` instead of showing the error code and error
message.

This also removes some `if err` blocks that never got called because at
the end of `handlers.lua` all the handlers are wrapped with logic that
adds generic error handling.
2021-09-16 14:37:20 +01:00
33000bd9cf backport: fix(lsp): Ensure users get feedback on references/symbols errors or empty results
Relates to https://github.com/neovim/neovim/issues/15050

Users should get some indication if there was an error or an empty
result.
2021-09-16 14:36:49 +01:00
9f73b7c214 backport: fix(lsp): correctly check for windows in lsp logger (#14954) 2021-09-16 14:31:49 +01:00
eaa1c47377 backport: fix(lsp): restore diagnostics extmarks that were moved to the last edit line (#15023) 2021-09-16 14:26:05 +01:00
942b16adf7 Merge #15671 backport: :source and nvim_exec fixes 2021-09-15 06:10:48 -07:00
cf62554e5a backport: fix(:source): copy curbuf lines to memory before sourcing #15111
It's possible for weirdness to happen if curbuf is modified while
sourcing from it via :source (with no arguments). For example:

- Deleting lines from or wiping curbuf can cause internal error E315 to
  be thrown from ml_get.
- Changing the curbuf to another buffer while sourcing can cause lines
  from the new curbuf to then be sourced instead.
2021-09-15 13:34:59 +01:00
6436100b6e backport: fix(:source, nvim_exec): handle Vimscript line continuations #14809
Problem:
Anonymous :source (no args) and nvim_exec() don't support Vimscript line continuations.

Solution:
Factor out the concat logic into concat_continued_line() and a
CONCAT_CONTINUED_LINES macro for simple concatenations where lines are
fetched individually.

Closes #14807
2021-09-15 13:34:54 +01:00
04cde576ed Merge #15667 release-0.5: backports 2021-09-14 09:22:31 -07:00
cd864748d3 test: use $TEST_TIMEOUT to specify timeout 2021-09-14 07:52:33 -07:00
09ff3146f3 test: timeout at 20 minutes #15597
Set a maximum test run-time of 20 minutes to:
- fail faster
- avoid wasting CI resources
- set a bound on the test time (if tests take longer than 20 minutes we
  need to invest in parallelizing them...)

Timeout looks like:

    -- Tests exited non-zero: Process terminated due to timeout
    -- No output to stderr.
    CMake Error at /…/neovim/cmake/RunTests.cmake:86
    (message):
      functional tests failed with error: Process terminated due to timeout
2021-09-14 07:52:27 -07:00
f809664f89 ci: skip "cancels stale events on channel close" #15278
- ref #14083 #15251
- also: docs: naming conventions
2021-09-14 07:51:01 -07:00
d83df7f7b5 feat(lua)!: register_keystroke_callback => on_key
Analogous to nodejs's `on('data', …)` interface, here on_key is the "add
listener" interface.

ref 3ccdbc570d #12536

BREAKING_CHANGE: vim.register_keystroke_callback() is now an error.
2021-09-14 07:29:46 -07:00
64dc7a1b55 fix(lsp): correctly parse LSP snippets #15579
Fixes #15522
2021-09-14 07:11:40 -07:00
5a813160ae Merge #15664 backport PRs
backport: PRs #14962, #14982, #14984, #14989, #15011, #15043
2021-09-14 07:02:06 -07:00
917f306666 backport: test/memory_usage_spec: skip on MacOS #15043
Memory compression could complicate the measurements.
2021-09-14 14:41:25 +01:00
a9cca1b050 backport: fixup(clipboard): Fix error not properly handled #14984
fixes #14967
2021-09-14 13:15:39 +01:00
989ccb8222 backport: fix(lsp): restore diagnostics extmarks on buffer changes (#15011) 2021-09-14 13:15:38 +01:00
2ae4c96d91 backport: fix(lsp): prevent double <text> for cached plaintext markup 2021-09-14 13:15:38 +01:00
ae89330ec0 backport: fix(vim.opt): vimL map string values not trimmed (#14982)
Options formatted as a list of comma-separated key-value pairs may have
values that contain leading and trailing whitespace characters. For
example, the `listchars` option has a default value of
`"tab:> ,trail:-,nbsp:+"`. When converting this value to a lua table,
leading and trailing whitespace should not be trimmed.

Co-authored-by: Robert Hrusecky <robert.hrusecky@utexas.edu>
2021-09-14 13:15:38 +01:00
2229e99ef9 backport: fixup(clipboard): Use case matching #14962
Context: https://github.com/neovim/neovim/pull/14848#discussion_r663203173
2021-09-14 13:15:37 +01:00
88336851ee Merge pull request #15496 from jamessan/stdin_closed_backport
backport: feat(job): add parameter to close stdin
2021-08-27 07:26:32 -04:00
3a0543bd61 Add test case for 'null' stdin mode 2021-08-26 21:57:25 -04:00
008b83f5a2 Rename stdin to stdin_mode
stdin is a macro in Windows builds.
2021-08-26 21:57:11 -04:00
915dda3f96 feat(job): add parameter to close stdin
Some programs behave differently when they detect that stdin is being
piped. This can be problematic when these programs are used with the job
control API where stdin is attached, but not typically used. It is
possible to run the job using a PTY which circumvents this problem, but
that includes a lot of overhead when simply closing the stdin pipe would
suffice.

To enable this behavior, add a new parameter to the jobstart options
dict called "stdin" with two valid values: "pipe" (the default)
implements the existing behavior of opening a channel for stdin and
"null" which disconnects stdin (or, if you prefer, connects it to
/dev/null). This is extensible so that other modes can be added in the
future.
2021-08-26 21:56:45 -04:00
b6b12ea7c3 fix(man.vim): filetype=man is too eager #15489
Problem:
"set filetype=man" assumes the user wants :Man features, this does extra
stuff like renaming the buffer as "man://".

Solution:
- old entrypoint was ":set filetype=man", but this is too presumptuous #15487
- make the entrypoints more explicit:
  1. when the ":Man" command is run
  2. when a "man://" buffer is opened
- remove the tricky b:man_sect checks in ftplugin/man.vim and syntax/man.vim
- MANPAGER is supported via ":Man!", as documented.

fixes #15487
2021-08-26 02:50:30 -07:00
502a56867d Merge pull request #15445 from jamessan/appdata-version
fix: add 0.5.0 release to appdata
2021-08-21 15:21:45 -04:00
78482138ae fix: add 0.5.0 release to appdata
[skip ci]
2021-08-21 14:36:12 -04:00
fe815244f0 Merge pull request #15389 from jamessan/32-bit-revert
[release-0.5] Revert "tests: unit: fix preprocess: pass -m32 for 32bit ABI (#11073)"
2021-08-16 07:55:38 -04:00
56d86970b0 Merge pull request #15375 from jamessan/fragility
[release-0.5] test(lsp): disable finicky test when TEST_SKIP_FRAGILE is set
2021-08-14 22:11:01 -04:00
6c4f66f381 Merge pull request #15376 from jamessan/release-fixes
[release-0.5] Clean up release handling
2021-08-14 21:46:38 -04:00
6f965f41df build: use RelWithDebInfo build for nightlies, Release for releases
Unlike Release build type, RelWithDebInfo does not disable asserts.
This helps get better debug info from people brave enough to use the
nightly builds, but shouldn't be used for official releases.

[skip ci]
2021-08-13 23:32:15 -04:00
f027c5e1e4 build: update appdata.xml version in release commit
Adding the version we just released in the "version bump" commit is
useless, since that means the actual release only reports the old
version.

Closes #15362

[skip ci]
2021-08-13 23:32:10 -04:00
f589c2619b Merge pull request #15289 from jamessan/pending-c-parsers
[release-0.5] test(treesitter): skip all parsers tests if parsers aren't installed
2021-08-06 07:24:19 -04:00
8336488ce1 test(treesitter): skip all parsers tests if parsers aren't installed 2021-08-05 21:41:55 -04:00
63 changed files with 2125 additions and 665 deletions

View File

@ -28,10 +28,14 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y autoconf automake build-essential cmake gcc-11 gettext gperf libtool-bin locales ninja-build pkg-config unzip
- if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name != 'nightly')
run: printf 'NVIM_BUILD_TYPE=Release\n' >> $GITHUB_ENV
- if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name == 'nightly')
run: printf 'NVIM_BUILD_TYPE=RelWithDebInfo\n' >> $GITHUB_ENV
- name: Build release
id: build
run: |
CC=gcc-11 make CMAKE_BUILD_TYPE=RelWithDebInfo CMAKE_EXTRA_FLAGS="-DCMAKE_INSTALL_PREFIX:PATH="
CC=gcc-11 make CMAKE_BUILD_TYPE=${NVIM_BUILD_TYPE} CMAKE_EXTRA_FLAGS="-DCMAKE_INSTALL_PREFIX:PATH="
printf '::set-output name=version::%s\n' "$(./build/bin/nvim --version | head -n 3 | sed -z 's/\n/%0A/g')"
printf '::set-output name=release::%s\n' "$(./build/bin/nvim --version | head -n 1)"
make DESTDIR="$GITHUB_WORKSPACE/build/release/nvim-linux64" install
@ -80,9 +84,13 @@ jobs:
brew update >/dev/null
brew upgrade
brew install automake ninja
- if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name != 'nightly')
run: printf 'NVIM_BUILD_TYPE=Release\n' >> $GITHUB_ENV
- if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name == 'nightly')
run: printf 'NVIM_BUILD_TYPE=RelWithDebInfo\n' >> $GITHUB_ENV
- name: Build release
run: |
make CMAKE_BUILD_TYPE=Release CMAKE_EXTRA_FLAGS="-DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_OSX_DEPLOYMENT_TARGET=10.11"
make CMAKE_BUILD_TYPE=${NVIM_BUILD_TYPE} CMAKE_EXTRA_FLAGS="-DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_OSX_DEPLOYMENT_TARGET=10.11"
make DESTDIR="$GITHUB_WORKSPACE/build/release/nvim-osx64" install
- name: Create package
run: |

View File

@ -1,4 +1,4 @@
.DONE:
@echo "Please use GNU Make (gmake) to build neovim"
@echo "Use GNU Make (gmake) to build neovim"
.DEFAULT:
@echo "Please use GNU Make (gmake) to build neovim"
@echo "Use GNU Make (gmake) to build neovim"

View File

@ -137,7 +137,7 @@ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY
# version string, else they are combined with the result of `git describe`.
set(NVIM_VERSION_MAJOR 0)
set(NVIM_VERSION_MINOR 5)
set(NVIM_VERSION_PATCH 0)
set(NVIM_VERSION_PATCH 1)
set(NVIM_VERSION_PRERELEASE "") # for package maintainers
# API level

View File

@ -28,10 +28,10 @@ Reporting problems
Developer guidelines
--------------------
- Nvim contributors should read `:help dev`.
- External UI developers should read `:help dev-ui`.
- API client developers should read `:help dev-api-client`.
- Nvim developers are _strongly encouraged_ to install `ninja` for faster builds.
- Read `:help dev` if you are working on Nvim core.
- Read `:help dev-ui` if you are developing a UI.
- Read `:help dev-api-client` if you are developing an API client.
- Install `ninja` for faster builds of Nvim.
```
sudo apt-get install ninja-build
make distclean

View File

@ -49,6 +49,10 @@ endif()
set(ENV{TMPDIR} "${BUILD_DIR}/Xtest_tmpdir/${TEST_PATH}")
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory $ENV{TMPDIR})
if(NOT DEFINED ENV{TEST_TIMEOUT} OR "$ENV{TEST_TIMEOUT}" STREQUAL "")
set(ENV{TEST_TIMEOUT} 1200)
endif()
set(ENV{SYSTEM_NAME} ${CMAKE_HOST_SYSTEM_NAME}) # used by test/helpers.lua.
execute_process(
COMMAND ${BUSTED_PRG} -v -o test.busted.outputHandlers.${BUSTED_OUTPUT_TYPE}
@ -58,6 +62,7 @@ execute_process(
--lpath=?.lua
${BUSTED_ARGS}
${TEST_PATH}
TIMEOUT $ENV{TEST_TIMEOUT}
WORKING_DIRECTORY ${WORKING_DIR}
ERROR_VARIABLE err
RESULT_VARIABLE res

View File

@ -65,6 +65,7 @@ function! man#open_page(count, mods, ...) abort
let b:man_sect = sect
endfunction
" Called when a man:// buffer is opened.
function! man#read_page(ref) abort
try
let [sect, name] = s:extract_sect_and_name_ref(a:ref)
@ -121,6 +122,15 @@ function! s:system(cmd, ...) abort
return opts.stdout
endfunction
function! s:set_options(pager) abort
setlocal filetype=man
setlocal noswapfile buftype=nofile bufhidden=hide
setlocal nomodified readonly nomodifiable
if a:pager
nnoremap <silent> <buffer> <nowait> q :lclose<CR>:q<CR>
endif
endfunction
function! s:get_page(path) abort
" Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065).
" Soft-wrap: ftplugin/man.vim sets wrap/breakindent/….
@ -134,9 +144,7 @@ function! s:get_page(path) abort
endfunction
function! s:put_page(page) abort
setlocal modifiable
setlocal noreadonly
setlocal noswapfile
setlocal modifiable noreadonly noswapfile
silent keepjumps %delete _
silent put =a:page
while getline(1) =~# '^\s*$'
@ -148,7 +156,7 @@ function! s:put_page(page) abort
silent! keeppatterns keepjumps %s/\s\{199,}/\=repeat(' ', 10)/g
1
lua require("man").highlight_man_page()
setlocal filetype=man
call s:set_options(v:false)
endfunction
function! man#show_toc() abort
@ -397,6 +405,7 @@ function! s:format_candidate(path, psect) abort
endif
endfunction
" Called when Nvim is invoked as $MANPAGER.
function! man#init_pager() abort
" https://github.com/neovim/neovim/issues/6828
let og_modifiable = &modifiable
@ -420,6 +429,7 @@ function! man#init_pager() abort
execute 'silent file man://'.tolower(fnameescape(ref))
endif
call s:set_options(v:true)
let &l:modifiable = og_modifiable
endfunction

View File

@ -158,7 +158,9 @@ function! s:clipboard.get(reg) abort
end
let clipboard_data = s:try_cmd(s:paste[a:reg])
if match(&clipboard, '\v(unnamed|unnamedplus)') >= 0 && get(s:selections[a:reg].data, 0, []) == clipboard_data
if match(&clipboard, '\v(unnamed|unnamedplus)') >= 0
\ && type(clipboard_data) == v:t_list
\ && get(s:selections[a:reg].data, 0, []) ==# clipboard_data
" When system clipboard return is same as our cache return the cache
" as it contains regtype information
return s:selections[a:reg].data

View File

@ -54,6 +54,8 @@ Functions ~
without stopping the job. Use chanclose(id) to close
any socket.
Lua ~
*vim.register_keystroke_callback()* Use |vim.on_key()| instead.
Modifiers ~
*cpo-<*

View File

@ -127,14 +127,20 @@ Sometimes a GUI or other application may want to force a provider to
DOCUMENTATION *dev-doc*
- Do not prefix help tags with "nvim-". Use |vim_diff.txt| to document
differences from Vim; no other distinction is necessary.
- If a Vim feature is removed, delete its help section and move its tag to
|vim_diff.txt|.
- Move deprecated features to |deprecated.txt|.
- "Just say it". Avoid mushy, colloquial phrasing in all documentation
(docstrings, user manual, website materials, newsletters, …). Don't mince
words. Personality and flavor, used sparingly, are welcome--but in general,
optimize for the reader's time and energy: be "precise yet concise".
- Prefer the active voice: "Foo does X", not "X is done by Foo".
- Vim differences:
- Do not prefix help tags with "nvim-". Use |vim_diff.txt| to catalog
differences from Vim; no other distinction is necessary.
- If a Vim feature is removed, delete its help section and move its tag to
|vim_diff.txt|.
- Mention deprecated features in |deprecated.txt| and delete their old doc.
- Use consistent language.
- "terminal" in a help tag always means "the embedded terminal emulator", not
"the user host terminal".
- "terminal" in a help tag always means "the embedded terminal emulator",
not "the user host terminal".
- Use "tui-" to prefix help tags related to the host terminal, and "TUI"
in prose if possible.
- Docstrings: do not start parameter descriptions with "The" or "A" unless it
@ -222,13 +228,13 @@ LUA *dev-lua*
- Keep the core Lua modules |lua-stdlib| simple. Avoid elaborate OOP or
pseudo-OOP designs. Plugin authors just want functions to call, they don't
want to learn a big, fancy inheritance hierarchy. So we should avoid complex
objects: tables are usually better.
want to learn a big, fancy inheritance hierarchy. Thus avoid specialized
objects; tables or values are usually better.
API *dev-api*
Use this template to name new API functions:
Use this template to name new RPC |API| functions:
nvim_{thing}_{action}_{arbitrary-qualifiers}
If the function acts on an object then {thing} is the name of that object
@ -356,4 +362,19 @@ External UIs are expected to implement these common features:
published in this event. See also "mouse_on", "mouse_off".
NAMING *dev-naming*
Naming is important. Consistent naming in the API and UI helps both users and
developers discover and intuitively understand related concepts ("families"),
and reduces cognitive burden. Discoverability encourages code re-use and
likewise avoids redundant, overlapping mechanisms, which reduces code
surface-area, and thereby minimizes bugs...
Naming conventions ~
Use the "on_" prefix to name event handlers and also the interface for
"registering" such handlers (on_key). The dual nature is acceptable to avoid
a confused collection of naming conventions for these related concepts.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -5641,6 +5641,9 @@ jobstart({cmd}[, {opts}]) *jobstart()*
before invoking `on_stderr`. |channel-buffered|
stdout_buffered: (boolean) Collect data until EOF (stream
closed) before invoking `on_stdout`. |channel-buffered|
stdin: (string) Either "pipe" (default) to connect the
job's stdin to a channel or "null" to disconnect
stdin.
width: (number) Width of the `pty` terminal.
{opts} is passed as |self| dictionary to the callback; the

View File

@ -202,23 +202,28 @@ responses and notifications from LSP servers.
For |lsp-request|, each |lsp-handler| has this signature: >
function(err, method, result, client_id, bufnr, config)
function(err, result, ctx, config)
<
Parameters: ~
{err} (table|nil)
When the language server is unable to complete a
request, a table with information about the error
is sent. Otherwise, it is `nil`. See |lsp-response|.
{method} (string)
The |lsp-method| name.
{result} (Result | Params | nil)
When the language server is able to succesfully
complete a request, this contains the `result` key
of the response. See |lsp-response|.
{client_id} (number)
The ID of the |vim.lsp.client|.
{bufnr} (Buffer)
Buffer handle, or 0 for current.
{ctx} (table)
Context describes additional calling state
associated with the handler. It consists of the
following key, value pairs:
{method} (string)
The |lsp-method| name.
{client_id} (number)
The ID of the |vim.lsp.client|.
{bufnr} (Buffer)
Buffer handle, or 0 for current.
{config} (table)
Configuration for the handler.
@ -238,21 +243,24 @@ For |lsp-request|, each |lsp-handler| has this signature: >
For |lsp-notification|, each |lsp-handler| has this signature: >
function(err, method, params, client_id, bufnr, config)
function(err, result, ctx, config)
<
Parameters: ~
{err} (nil)
This is always `nil`.
See |lsp-notification|
{method} (string)
The |lsp-method| name.
{params} (Params)
{result} (Result)
This contains the `params` key of the notification.
See |lsp-notification|
{client_id} (number)
The ID of the |vim.lsp.client|
{bufnr} (nil)
`nil`, as the server doesn't have an associated buffer.
{ctx} (table)
Context describes additional calling state
associated with the handler. It consists of the
following key, value pairs:
{method} (string)
The |lsp-method| name.
{client_id} (number)
The ID of the |vim.lsp.client|.
{config} (table)
Configuration for the handler.
@ -1355,7 +1363,7 @@ goto_prev({opts}) *vim.lsp.diagnostic.goto_prev()*
{opts} table See |vim.lsp.diagnostic.goto_next()|
*vim.lsp.diagnostic.on_publish_diagnostics()*
on_publish_diagnostics({_}, {_}, {params}, {client_id}, {_}, {config})
on_publish_diagnostics({_}, {result}, {ctx}, {config})
|lsp-handler| for the method "textDocument/publishDiagnostics"
Note:
@ -1581,7 +1589,7 @@ get({bufnr}) *vim.lsp.codelens.get()*
table ( `CodeLens[]` )
*vim.lsp.codelens.on_codelens()*
on_codelens({err}, {_}, {result}, {client_id}, {bufnr})
on_codelens({err}, {result}, {ctx}, {_})
|lsp-handler| for the method `textDocument/codeLens`
refresh() *vim.lsp.codelens.refresh()*
@ -1614,17 +1622,25 @@ progress_handler({_}, {_}, {params}, {client_id})
See also: ~
https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
*vim.lsp.handlers.signature_help()*
signature_help({_}, {method}, {result}, {_}, {bufnr}, {config})
Parameters: ~
{config} table Configuration table.
• border: (default=nil)
• Add borders to the floating window
• See |vim.api.nvim_open_win()|
hover({_}, {result}, {ctx}, {config}) *vim.lsp.handlers.hover()*
|lsp-handler| for the method "textDocument/hover" >
vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
vim.lsp.handlers.hover, {
-- Use a sharp border with `FloatBorder` highlights
border = "single"
}
)
<
See also: ~
https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration@seehttps://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition@seehttps://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition@seehttps://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation|lsp-handler| for the method "textDocument/signatureHelp">
*vim.lsp.handlers.signature_help()*
signature_help({_}, {result}, {ctx}, {config})
|lsp-handler| for the method "textDocument/signatureHelp". The
active parameter is highlighted with
|hl-LspSignatureActiveParameter|. >
vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
vim.lsp.handlers.signature_help, {
-- Use a sharp border with `FloatBorder` highlights

View File

@ -572,11 +572,11 @@ If you want to exclude visual selections from highlighting on yank, use
vim.highlight.on_yank({opts}) *vim.highlight.on_yank()*
Highlights the yanked text. The fields of the optional dict {opts}
control the highlight:
- {higroup} highlight group for yanked region (default `"IncSearch"`)
- {higroup} highlight group for yanked region (default |hl-IncSearch|)
- {timeout} time in ms before highlight is cleared (default `150`)
- {on_macro} highlight when executing macro (default `false`)
- {on_visual} highlight when yanking visual selection (default `true`)
- {event} event structure (default `vim.v.event`)
- {event} event structure (default |v:event|)
vim.highlight.range({bufnr}, {ns}, {higroup}, {start}, {finish}, {rtype}, {inclusive})
*vim.highlight.range()*
@ -1167,37 +1167,6 @@ region({bufnr}, {pos1}, {pos2}, {regtype}, {inclusive}) *vim.region()*
Return: ~
region lua table of the form {linenr = {startcol,endcol}}
*vim.register_keystroke_callback()*
register_keystroke_callback({fn}, {ns_id})
Register a lua {fn} with an {id} to be run after every
keystroke.
If {fn} is nil, it removes the callback for the associated
{ns_id}
Note:
{fn} will not be cleared from |nvim_buf_clear_namespace()|
Note:
{fn} will receive the keystrokes after mappings have been
evaluated
Parameters: ~
{fn} function: Function to call. It should take one
argument, which is a string. The string will contain
the literal keys typed. See |i_CTRL-V|
{ns_id} number? Namespace ID. If not passed or 0, will
generate and return a new namespace ID from
|nvim_create_namesapce()|
Return: ~
number Namespace ID associated with {fn}
Note:
{fn} will be automatically removed if an error occurs
while calling. This is to prevent the annoying situation
of every keystroke erroring while trying to remove a
broken callback.
schedule_wrap({cb}) *vim.schedule_wrap()*
Defers callback `cb` until the Nvim API is safe to call.
@ -1505,14 +1474,12 @@ validate({opt}) *vim.validate()*
vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
=> NOP (success)
<
>
vim.validate{arg1={1, 'table'}}
=> error('arg1: expected table, got number')
<
>
vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
=> error('arg1: expected even number, got 3')
vim.validate{arg1={1, 'table'}}
=> error('arg1: expected table, got number')
vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
=> error('arg1: expected even number, got 3')
<
Parameters: ~

View File

@ -329,12 +329,12 @@ argument.
-w{number} Set the 'window' option to {number}.
*-w*
-w {scriptout} All the characters that you type are recorded in the file
"scriptout", until you exit Vim. This is useful if you want
to create a script file to be used with "vim -s" or
":source!". When the "scriptout" file already exists, new
characters are appended. See also |complex-repeat|.
{scriptout} cannot start with a digit.
-w {scriptout} All keys that you type are recorded in the file "scriptout",
until you exit Vim. Useful to create a script file to be used
with "vim -s" or ":source!". Appends to the "scriptout" file
if it already exists. {scriptout} cannot start with a digit.
See also |vim.on_key()|.
See also |complex-repeat|.
*-W*
-W {scriptout} Like -w, but do not append, overwrite an existing file.

View File

@ -474,11 +474,9 @@ Query:iter_matches({self}, {node}, {source}, {start}, {stop})
for id, node in pairs(match) do
local name = query.captures[id]
-- `node` was captured by the `name` capture in the match
<
>
local node_data = metadata[id] -- Node level metadata
<
>
local node_data = metadata[id] -- Node level metadata
... use the info here ...
end
end

View File

@ -6,16 +6,16 @@
Differences between Nvim and Vim *vim-differences*
Nvim differs from Vim in many ways, although editor and VimL features are
mostly identical. This document is a complete and centralized reference of
the differences.
Nvim differs from Vim in many ways, although editor and Vimscript (not
Vim9script) features are mostly identical. This document is a complete and
centralized reference of the differences.
Type |gO| to see the table of contents.
==============================================================================
1. Configuration *nvim-config*
- Use `$XDG_CONFIG_HOME/nvim/init.vim` instead of `.vimrc` for configuration.
- Use `$XDG_CONFIG_HOME/nvim/init.vim` instead of `.vimrc` for your |config|.
- Use `$XDG_CONFIG_HOME/nvim` instead of `.vim` to store configuration files.
- Use `$XDG_DATA_HOME/nvim/shada/main.shada` instead of `.viminfo` for persistent
session information. |shada|
@ -78,17 +78,19 @@ the differences.
MAJOR COMPONENTS ~
API |API|
Lua scripting |lua|
Job control |job-control|
Remote plugins |remote-plugin|
LSP framework |lsp|
Lua scripting |lua|
Parsing engine |treesitter|
Providers
Clipboard |provider-clipboard|
Node.js plugins |provider-nodejs|
Python plugins |provider-python|
Ruby plugins |provider-ruby|
Remote plugins |remote-plugin|
Shared data |shada|
Embedded terminal |terminal|
VimL parser |nvim_parse_expression()|
Terminal emulator |terminal|
Vimscript parser |nvim_parse_expression()|
XDG base directories |xdg|
USER EXPERIENCE ~
@ -137,7 +139,7 @@ FEATURES ~
Command-line highlighting:
The expression prompt (|@=|, |c_CTRL-R_=|, |i_CTRL-R_=|) is highlighted
using a built-in VimL expression parser. |expr-highlight|
using a built-in Vimscript expression parser. |expr-highlight|
*E5408* *E5409*
|input()|, |inputdialog()| support custom highlighting. |input()-highlight|
*g:Nvim_color_cmdline*
@ -380,7 +382,7 @@ TUI:
UI/Display:
|Visual| selection highlights the character at cursor. |visual-use|
VimL (Vim script) compatibility:
Vimscript compatibility:
`count` does not alias to |v:count|
`errmsg` does not alias to |v:errmsg|
`shell_error` does not alias to |v:shell_error|

View File

@ -6,14 +6,6 @@ if exists('b:did_ftplugin') || &filetype !=# 'man'
endif
let b:did_ftplugin = 1
let s:pager = !exists('b:man_sect')
if s:pager
call man#init_pager()
endif
setlocal noswapfile buftype=nofile bufhidden=hide
setlocal nomodified readonly nomodifiable
setlocal noexpandtab tabstop=8 softtabstop=8 shiftwidth=8
setlocal wrap breakindent linebreak
@ -32,11 +24,7 @@ if !exists('g:no_plugin_maps') && !exists('g:no_man_maps')
nnoremap <silent> <buffer> k gk
nnoremap <silent> <buffer> gO :call man#show_toc()<CR>
nnoremap <silent> <buffer> <2-LeftMouse> :Man<CR>
if s:pager
nnoremap <silent> <buffer> <nowait> q :lclose<CR>:q<CR>
else
nnoremap <silent> <buffer> <nowait> q :lclose<CR><C-W>c
endif
nnoremap <silent> <buffer> <nowait> q :lclose<CR><C-W>c
endif
if get(g:, 'ft_man_folding_enable', 0)

View File

@ -494,7 +494,6 @@ local convert_value_to_lua = (function()
for _, key_value_str in ipairs(comma_split) do
local key, value = unpack(vim.split(key_value_str, ":"))
key = vim.trim(key)
value = vim.trim(value)
result[key] = value
end

View File

@ -85,7 +85,11 @@ function highlight.on_yank(opts)
highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive)
vim.defer_fn(
function() api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) end,
function()
if api.nvim_buf_is_valid(bufnr) then
api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1)
end
end,
timeout
)
end

View File

@ -218,7 +218,7 @@ local function validate_client_config(config)
config = { config, 't' };
}
validate {
root_dir = { config.root_dir, is_dir, "directory" };
root_dir = { config.root_dir, optional_validator(is_dir), "directory" };
handlers = { config.handlers, "t", true };
capabilities = { config.capabilities, "t", true };
cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" };
@ -380,7 +380,7 @@ do
end
state.pending_change = function()
state.pending_change = nil
if client.is_stopped() then
if client.is_stopped() or not vim.api.nvim_buf_is_valid(bufnr) then
return
end
local contentChanges
@ -455,12 +455,15 @@ local function text_document_did_open_handler(bufnr, client)
vim.schedule(function()
vim.lsp.handlers["textDocument/publishDiagnostics"](
nil,
"textDocument/publishDiagnostics",
{
diagnostics = vim.lsp.diagnostic.get(bufnr, client.id),
uri = vim.uri_from_bufnr(bufnr),
},
client.id
{
method="textDocument/publishDiagnostics",
client_id=client.id,
bufnr=bufnr,
}
)
end)
end
@ -555,7 +558,7 @@ end
---
--- The following parameters describe fields in the {config} table.
---
--@param root_dir: (required, string) Directory where the LSP server will base
--@param root_dir: (string) Directory where the LSP server will base
--- its rootUri on initialization.
---
--@param cmd: (required, string or list treated like |jobstart()|) Base command
@ -590,6 +593,10 @@ end
--- as `initializationOptions`. See `initialize` in the LSP spec.
---
--@param name (string, default=client-id) Name in log messages.
--
--@param workspace_folders (table) List of workspace folders passed to the
--- language server. Defaults to root_dir if not set. See `workspaceFolders` in
--- the LSP spec
---
--@param get_language_id function(bufnr, filetype) -> language ID as string.
--- Defaults to the filetype.
@ -677,11 +684,11 @@ function lsp.start_client(config)
--@param method (string) LSP method name
--@param params (table) The parameters for that method.
function dispatch.notification(method, params)
local _ = log.debug() and log.debug('notification', method, params)
local _ = log.trace() and log.trace('notification', method, params)
local handler = resolve_handler(method)
if handler then
-- Method name is provided here for convenience.
handler(nil, method, params, client_id)
handler(nil, params, {method=method, client_id=client_id})
end
end
@ -691,13 +698,13 @@ function lsp.start_client(config)
--@param method (string) LSP method name
--@param params (table) The parameters for that method
function dispatch.server_request(method, params)
local _ = log.debug() and log.debug('server_request', method, params)
local _ = log.trace() and log.trace('server_request', method, params)
local handler = resolve_handler(method)
if handler then
local _ = log.debug() and log.debug("server_request: found handler for", method)
return handler(nil, method, params, client_id)
local _ = log.trace() and log.trace("server_request: found handler for", method)
return handler(nil, params, {method=method, client_id=client_id})
end
local _ = log.debug() and log.debug("server_request: no handler found for", method)
local _ = log.warn() and log.warn("server_request: no handler found for", method)
return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
end
@ -775,6 +782,14 @@ function lsp.start_client(config)
off = 'off'; messages = 'messages'; verbose = 'verbose';
}
local version = vim.version()
if config.root_dir and not config.workspace_folders then
config.workspace_folders = {{
uri = vim.uri_from_fname(config.root_dir);
name = string.format("%s", config.root_dir);
}};
end
local initialize_params = {
-- The process Id of the parent process that started the server. Is null if
-- the process has not been started by another process. If the parent
@ -793,7 +808,7 @@ function lsp.start_client(config)
rootPath = config.root_dir;
-- The rootUri of the workspace. Is null if no folder is open. If both
-- `rootPath` and `rootUri` are set `rootUri` wins.
rootUri = vim.uri_from_fname(config.root_dir);
rootUri = config.root_dir and vim.uri_from_fname(config.root_dir);
-- User provided initialization options.
initializationOptions = config.init_options;
-- The capabilities provided by the client (editor or tool)
@ -815,16 +830,13 @@ function lsp.start_client(config)
-- -- workspace folder in the user interface.
-- name
-- }
workspaceFolders = {{
uri = vim.uri_from_fname(config.root_dir);
name = string.format("%s", config.root_dir);
}};
workspaceFolders = config.workspace_folders,
}
if config.before_init then
-- TODO(ashkan) handle errors here.
pcall(config.before_init, initialize_params, config)
end
local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params)
local _ = log.trace() and log.trace(log_prefix, "initialize_params", initialize_params)
rpc.request('initialize', initialize_params, function(init_err, result)
assert(not init_err, tostring(init_err))
assert(result, "server sent empty result")
@ -894,7 +906,7 @@ function lsp.start_client(config)
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr)
return rpc.request(method, params, function(err, result)
handler(err, method, result, client_id, bufnr)
handler(err, result, {method=method, client_id=client_id, bufnr=bufnr})
end)
end
@ -915,7 +927,7 @@ function lsp.start_client(config)
--@see |vim.lsp.buf_request_sync()|
function client.request_sync(method, params, timeout_ms, bufnr)
local request_result = nil
local function _sync_handler(err, _, result)
local function _sync_handler(err, result)
request_result = { err = err, result = result }
end
@ -1150,7 +1162,7 @@ end
---@param bufnr (number) Buffer handle, or 0 for current
---@param client_id (number) the client id
function lsp.buf_is_attached(bufnr, client_id)
return (all_buffer_active_clients[bufnr] or {})[client_id] == true
return (all_buffer_active_clients[resolve_bufnr(bufnr)] or {})[client_id] == true
end
--- Gets a client by id, or nil if the id is invalid.
@ -1274,7 +1286,7 @@ function lsp.buf_request(bufnr, method, params, handler)
local unsupported_err = lsp._unsupported_method(method)
handler = handler or lsp.handlers[method]
if handler then
handler(unsupported_err, method, bufnr)
handler(unsupported_err, nil, {method=method, bufnr=bufnr})
end
return
end
@ -1314,8 +1326,8 @@ function lsp.buf_request_all(bufnr, method, params, callback)
end
end)
local function _sync_handler(err, _, result, client_id)
request_results[client_id] = { error = err, result = result }
local function _sync_handler(err, result, ctx)
request_results[ctx.client_id] = { error = err, result = result }
result_count = result_count + 1
set_expected_result_count()
@ -1421,7 +1433,7 @@ function lsp.omnifunc(findstart, base)
local params = util.make_position_params()
local items = {}
lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, _, result)
lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, result)
if err or not result or vim.fn.mode() ~= "i" then return end
local matches = util.text_document_completion_list_to_complete_items(result, prefix)
-- TODO(ashkan): is this the best way to do this?
@ -1496,8 +1508,8 @@ end
--@param handler (function) See |lsp-handler|
--@param override_config (table) Table containing the keys to override behavior of the {handler}
function lsp.with(handler, override_config)
return function(err, method, params, client_id, bufnr, config)
return handler(err, method, params, client_id, bufnr, vim.tbl_deep_extend("force", config or {}, override_config))
return function(err, result, ctx, config)
return handler(err, result, ctx, vim.tbl_deep_extend("force", config or {}, override_config))
end
end

View File

@ -0,0 +1,399 @@
local P = {}
---Take characters until the target characters (The escape sequence is '\' + char)
---@param targets string[] The character list for stop consuming text.
---@param specials string[] If the character isn't contained in targets/specials, '\' will be left.
P.take_until = function(targets, specials)
targets = targets or {}
specials = specials or {}
return function(input, pos)
local new_pos = pos
local raw = {}
local esc = {}
while new_pos <= #input do
local c = string.sub(input, new_pos, new_pos)
if c == '\\' then
table.insert(raw, '\\')
new_pos = new_pos + 1
c = string.sub(input, new_pos, new_pos)
if not vim.tbl_contains(targets, c) and not vim.tbl_contains(specials, c) then
table.insert(esc, '\\')
end
table.insert(raw, c)
table.insert(esc, c)
new_pos = new_pos + 1
else
if vim.tbl_contains(targets, c) then
break
end
table.insert(raw, c)
table.insert(esc, c)
new_pos = new_pos + 1
end
end
if new_pos == pos then
return P.unmatch(pos)
end
return {
parsed = true,
value = {
raw = table.concat(raw, ''),
esc = table.concat(esc, '')
},
pos = new_pos,
}
end
end
P.unmatch = function(pos)
return {
parsed = false,
value = nil,
pos = pos,
}
end
P.map = function(parser, map)
return function(input, pos)
local result = parser(input, pos)
if result.parsed then
return {
parsed = true,
value = map(result.value),
pos = result.pos,
}
end
return P.unmatch(pos)
end
end
P.lazy = function(factory)
return function(input, pos)
return factory()(input, pos)
end
end
P.token = function(token)
return function(input, pos)
local maybe_token = string.sub(input, pos, pos + #token - 1)
if token == maybe_token then
return {
parsed = true,
value = maybe_token,
pos = pos + #token,
}
end
return P.unmatch(pos)
end
end
P.pattern = function(p)
return function(input, pos)
local maybe_match = string.match(string.sub(input, pos), '^' .. p)
if maybe_match then
return {
parsed = true,
value = maybe_match,
pos = pos + #maybe_match,
}
end
return P.unmatch(pos)
end
end
P.many = function(parser)
return function(input, pos)
local values = {}
local new_pos = pos
while new_pos <= #input do
local result = parser(input, new_pos)
if not result.parsed then
break
end
table.insert(values, result.value)
new_pos = result.pos
end
if #values > 0 then
return {
parsed = true,
value = values,
pos = new_pos,
}
end
return P.unmatch(pos)
end
end
P.any = function(...)
local parsers = { ... }
return function(input, pos)
for _, parser in ipairs(parsers) do
local result = parser(input, pos)
if result.parsed then
return result
end
end
return P.unmatch(pos)
end
end
P.opt = function(parser)
return function(input, pos)
local result = parser(input, pos)
return {
parsed = true,
value = result.value,
pos = result.pos,
}
end
end
P.seq = function(...)
local parsers = { ... }
return function(input, pos)
local values = {}
local new_pos = pos
for _, parser in ipairs(parsers) do
local result = parser(input, new_pos)
if result.parsed then
table.insert(values, result.value)
new_pos = result.pos
else
return P.unmatch(pos)
end
end
return {
parsed = true,
value = values,
pos = new_pos,
}
end
end
local Node = {}
Node.Type = {
SNIPPET = 0,
TABSTOP = 1,
PLACEHOLDER = 2,
VARIABLE = 3,
CHOICE = 4,
TRANSFORM = 5,
FORMAT = 6,
TEXT = 7,
}
function Node:__tostring()
local insert_text = {}
if self.type == Node.Type.SNIPPET then
for _, c in ipairs(self.children) do
table.insert(insert_text, tostring(c))
end
elseif self.type == Node.Type.CHOICE then
table.insert(insert_text, self.items[1])
elseif self.type == Node.Type.PLACEHOLDER then
for _, c in ipairs(self.children or {}) do
table.insert(insert_text, tostring(c))
end
elseif self.type == Node.Type.TEXT then
table.insert(insert_text, self.esc)
end
return table.concat(insert_text, '')
end
--@see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
local S = {}
S.dollar = P.token('$')
S.open = P.token('{')
S.close = P.token('}')
S.colon = P.token(':')
S.slash = P.token('/')
S.comma = P.token(',')
S.pipe = P.token('|')
S.plus = P.token('+')
S.minus = P.token('-')
S.question = P.token('?')
S.int = P.map(P.pattern('[0-9]+'), function(value)
return tonumber(value, 10)
end)
S.var = P.pattern('[%a_][%w_]+')
S.text = function(targets, specials)
return P.map(P.take_until(targets, specials), function(value)
return setmetatable({
type = Node.Type.TEXT,
raw = value.raw,
esc = value.esc,
}, Node)
end)
end
S.toplevel = P.lazy(function()
return P.any(S.placeholder, S.tabstop, S.variable, S.choice)
end)
S.format = P.any(
P.map(P.seq(S.dollar, S.int), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[2],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[3],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.slash, P.any(
P.token('upcase'),
P.token('downcase'),
P.token('capitalize'),
P.token('camelcase'),
P.token('pascalcase')
), S.close), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[3],
modifier = values[6],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.any(
P.seq(S.question, P.take_until({ ':' }, { '\\' }), S.colon, P.take_until({ '}' }, { '\\' })),
P.seq(S.plus, P.take_until({ '}' }, { '\\' })),
P.seq(S.minus, P.take_until({ '}' }, { '\\' }))
), S.close), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[3],
if_text = values[5][2].esc,
else_text = (values[5][4] or {}).esc,
}, Node)
end)
)
S.transform = P.map(P.seq(
S.slash,
P.take_until({ '/' }, { '\\' }),
S.slash,
P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))),
S.slash,
P.opt(P.pattern('[ig]+'))
), function(values)
return setmetatable({
type = Node.Type.TRANSFORM,
pattern = values[2].raw,
format = values[4],
option = values[6],
}, Node)
end)
S.tabstop = P.any(
P.map(P.seq(S.dollar, S.int), function(values)
return setmetatable({
type = Node.Type.TABSTOP,
tabstop = values[2],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
return setmetatable({
type = Node.Type.TABSTOP,
tabstop = values[3],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.transform, S.close), function(values)
return setmetatable({
type = Node.Type.TABSTOP,
tabstop = values[3],
transform = values[4],
}, Node)
end)
)
S.placeholder = P.any(
P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
return setmetatable({
type = Node.Type.PLACEHOLDER,
tabstop = values[3],
children = values[5],
}, Node)
end)
)
S.choice = P.map(P.seq(
S.dollar,
S.open,
S.int,
S.pipe,
P.many(
P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values)
return values[1].esc
end)
),
S.pipe,
S.close
), function(values)
return setmetatable({
type = Node.Type.CHOICE,
tabstop = values[3],
items = values[5],
}, Node)
end)
S.variable = P.any(
P.map(P.seq(S.dollar, S.var), function(values)
return setmetatable({
type = Node.Type.VARIABLE,
name = values[2],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.var, S.close), function(values)
return setmetatable({
type = Node.Type.VARIABLE,
name = values[3],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.var, S.transform, S.close), function(values)
return setmetatable({
type = Node.Type.VARIABLE,
name = values[3],
transform = values[4],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.var, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
return setmetatable({
type = Node.Type.VARIABLE,
name = values[3],
children = values[5],
}, Node)
end)
)
S.snippet = P.map(P.many(P.any(S.toplevel, S.text({ '$' }, { '}', '\\' }))), function(values)
return setmetatable({
type = Node.Type.SNIPPET,
children = values,
}, Node)
end)
local M = {}
---The snippet node type enum
---@types table<string, number>
M.NodeType = Node.Type
---Parse snippet string and returns the AST
---@param input string
---@return table
function M.parse(input)
local result = S.snippet(input, 1)
if not result.parsed then
error('snippet parsing failed.')
end
return result.value
end
return M

View File

@ -126,7 +126,7 @@ local function select_client(method)
if #clients > 1 then
local choices = {}
for k,v in ipairs(clients) do
for k,v in pairs(clients) do
table.insert(choices, string.format("%d %s", k, v.name))
end
local user_choice = vim.fn.confirm(
@ -204,9 +204,9 @@ function M.formatting_seq_sync(options, timeout_ms, order)
local clients = vim.tbl_values(vim.lsp.buf_get_clients());
-- sort the clients according to `order`
for _, client_name in ipairs(order or {}) do
for _, client_name in pairs(order or {}) do
-- if the client exists, move to the end of the list
for i, client in ipairs(clients) do
for i, client in pairs(clients) do
if client.name == client_name then
table.insert(clients, table.remove(clients, i))
break
@ -215,7 +215,7 @@ function M.formatting_seq_sync(options, timeout_ms, order)
end
-- loop through the clients and make synchronous formatting requests
for _, client in ipairs(clients) do
for _, client in pairs(clients) do
if client.resolved_capabilities.document_formatting then
local params = util.make_formatting_params(options)
local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
@ -286,7 +286,7 @@ local function pick_call_hierarchy_item(call_hierarchy_items)
return call_hierarchy_items[1]
end
local items = {}
for i, item in ipairs(call_hierarchy_items) do
for i, item in pairs(call_hierarchy_items) do
local entry = item.detail or item.name
table.insert(items, string.format("%d. %s", i, entry))
end
@ -300,13 +300,21 @@ end
--@private
local function call_hierarchy(method)
local params = util.make_position_params()
request('textDocument/prepareCallHierarchy', params, function(err, _, result)
request('textDocument/prepareCallHierarchy', params, function(err, result, ctx)
if err then
vim.notify(err.message, vim.log.levels.WARN)
return
end
local call_hierarchy_item = pick_call_hierarchy_item(result)
vim.lsp.buf_request(0, method, { item = call_hierarchy_item })
local client = vim.lsp.get_client_by_id(ctx.client_id)
if client then
client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr)
else
vim.notify(string.format(
'Client with id=%d disappeared during call hierarchy request', ctx.client_id),
vim.log.levels.WARN
)
end
end)
end
@ -328,8 +336,8 @@ end
---
function M.list_workspace_folders()
local workspace_folders = {}
for _, client in ipairs(vim.lsp.buf_get_clients()) do
for _, folder in ipairs(client.workspaceFolders) do
for _, client in pairs(vim.lsp.buf_get_clients()) do
for _, folder in pairs(client.workspaceFolders) do
table.insert(workspace_folders, folder.name)
end
end
@ -347,9 +355,9 @@ function M.add_workspace_folder(workspace_folder)
return
end
local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}})
for _, client in ipairs(vim.lsp.buf_get_clients()) do
for _, client in pairs(vim.lsp.buf_get_clients()) do
local found = false
for _, folder in ipairs(client.workspaceFolders) do
for _, folder in pairs(client.workspaceFolders) do
if folder.name == workspace_folder then
found = true
print(workspace_folder, "is already part of this workspace")
@ -371,8 +379,8 @@ function M.remove_workspace_folder(workspace_folder)
vim.api.nvim_command("redraw")
if not (workspace_folder and #workspace_folder > 0) then return end
local params = util.make_workspace_params({{}}, {{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}})
for _, client in ipairs(vim.lsp.buf_get_clients()) do
for idx, folder in ipairs(client.workspaceFolders) do
for _, client in pairs(vim.lsp.buf_get_clients()) do
for idx, folder in pairs(client.workspaceFolders) do
if folder.name == workspace_folder then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
client.workspaceFolders[idx] = nil
@ -422,6 +430,21 @@ function M.clear_references()
util.buf_clear_references()
end
--- Requests code actions from all clients and calls the handler exactly once
--- with all aggregated results
---@private
local function code_action_request(params)
local bufnr = vim.api.nvim_get_current_buf()
local method = 'textDocument/codeAction'
vim.lsp.buf_request_all(bufnr, method, params, function(results)
local actions = {}
for _, r in pairs(results) do
vim.list_extend(actions, r.result or {})
end
vim.lsp.handlers[method](nil, actions, {bufnr=bufnr, method=method})
end)
end
--- Selects a code action from the input list that is available at the current
--- cursor position.
--

View File

@ -169,7 +169,7 @@ local function resolve_lenses(lenses, bufnr, client_id, callback)
if lens.command then
countdown()
else
client.request('codeLens/resolve', lens, function(_, _, result)
client.request('codeLens/resolve', lens, function(_, result)
if result and result.command then
lens.command = result.command
-- Eager display to have some sort of incremental feedback
@ -192,17 +192,17 @@ end
--- |lsp-handler| for the method `textDocument/codeLens`
---
function M.on_codelens(err, _, result, client_id, bufnr)
function M.on_codelens(err, result, ctx, _)
assert(not err, vim.inspect(err))
M.save(result, bufnr, client_id)
M.save(result, ctx.bufnr, ctx.client_id)
-- Eager display for any resolved (and unresolved) lenses and refresh them
-- once resolved.
M.display(result, bufnr, client_id)
resolve_lenses(result, bufnr, client_id, function()
M.display(result, bufnr, client_id)
active_refreshes[bufnr] = nil
M.display(result, ctx.bufnr, ctx.client_id)
resolve_lenses(result, ctx.bufnr, ctx.client_id, function()
M.display(result, ctx.bufnr, ctx.client_id)
active_refreshes[ctx.bufnr] = nil
end)
end

View File

@ -203,8 +203,10 @@ local bufnr_and_client_cacher_mt = {
-- Diagnostic Saving & Caching {{{
local _diagnostic_cleanup = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_cache = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_cache_lines = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_cache_counts = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_attached_buffers = {}
local _bufs_waiting_to_update = setmetatable({}, bufnr_and_client_cacher_mt)
@ -826,6 +828,7 @@ function M.clear(bufnr, client_id, diagnostic_ns, sign_ns)
diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
sign_ns = sign_ns or M._get_sign_namespace(client_id)
diagnostic_cache_extmarks[bufnr][client_id] = {}
assert(bufnr, "bufnr is required")
assert(diagnostic_ns, "Need diagnostic_ns, got nil")
@ -1005,15 +1008,16 @@ end
--- - Update diagnostics in InsertMode or wait until InsertLeave
--- - severity_sort: (default=false)
--- - Sort diagnostics (and thus signs and virtual text)
function M.on_publish_diagnostics(_, _, params, client_id, _, config)
local uri = params.uri
function M.on_publish_diagnostics(_, result, ctx, config)
local client_id = ctx.client_id
local uri = result.uri
local bufnr = vim.uri_to_bufnr(uri)
if not bufnr then
return
end
local diagnostics = params.diagnostics
local diagnostics = result.diagnostics
if config and if_nil(config.severity_sort, false) then
table.sort(diagnostics, function(a, b) return a.severity > b.severity end)
@ -1038,6 +1042,54 @@ function M.on_publish_diagnostics(_, _, params, client_id, _, config)
M.display(diagnostics, bufnr, client_id, config)
end
-- restores the extmarks set by M.display
--- @param last number last line that was changed
-- @private
local function restore_extmarks(bufnr, last)
for client_id, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do
local ns = M._get_diagnostic_namespace(client_id)
local extmarks_current = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
local found = {}
for _, extmark in ipairs(extmarks_current) do
-- nvim_buf_set_lines will move any extmark to the line after the last
-- nvim_buf_set_text will move any extmark to the last line
if extmark[2] ~= last + 1 then
found[extmark[1]] = true
end
end
for _, extmark in ipairs(extmarks) do
if not found[extmark[1]] then
local opts = extmark[4]
opts.id = extmark[1]
-- HACK: end_row should be end_line
if opts.end_row then
opts.end_line = opts.end_row
opts.end_row = nil
end
pcall(api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
end
end
end
end
-- caches the extmarks set by M.display
-- @private
local function save_extmarks(bufnr, client_id)
bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
if not diagnostic_attached_buffers[bufnr] then
api.nvim_buf_attach(bufnr, false, {
on_lines = function(_, _, _, _, _, last)
restore_extmarks(bufnr, last - 1)
end,
on_detach = function()
diagnostic_cache_extmarks[bufnr] = nil
end})
diagnostic_attached_buffers[bufnr] = true
end
local ns = M._get_diagnostic_namespace(client_id)
diagnostic_cache_extmarks[bufnr][client_id] = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
end
--@private
--- Display diagnostics for the buffer, given a configuration.
function M.display(diagnostics, bufnr, client_id, config)
@ -1108,9 +1160,46 @@ function M.display(diagnostics, bufnr, client_id, config)
if signs_opts then
M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts)
end
-- cache extmarks
save_extmarks(bufnr, client_id)
end
-- }}}
-- Diagnostic User Functions {{{
--- Redraw diagnostics for the given buffer and client
---
--- This calls the "textDocument/publishDiagnostics" handler manually using
--- the cached diagnostics already received from the server. This can be useful
--- for redrawing diagnostics after making changes in diagnostics
--- configuration. |lsp-handler-configuration|
---
---@param bufnr (optional, number): Buffer handle, defaults to current
---@param client_id (optional, number): Redraw diagnostics for the given
--- client. The default is to redraw diagnostics for all attached
--- clients.
function M.redraw(bufnr, client_id)
bufnr = get_bufnr(bufnr)
if not client_id then
return vim.lsp.for_each_buffer_client(bufnr, function(client)
M.redraw(bufnr, client.id)
end)
end
-- We need to invoke the publishDiagnostics handler directly instead of just
-- calling M.display so that we can preserve any custom configuration options
-- the user may have set with vim.lsp.with.
vim.lsp.handlers["textDocument/publishDiagnostics"](
nil,
{
uri = vim.uri_from_bufnr(bufnr),
diagnostics = M.get(bufnr, client_id),
},
{
method = "textDocument/publishDiagnostics",
client_id = client_id,
bufnr = bufnr,
}
)
end
--- Open a floating window with the diagnostics from {line_nr}
---

View File

@ -17,23 +17,21 @@ local function err_message(...)
api.nvim_command("redraw")
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
M['workspace/executeCommand'] = function(err, _)
if err then
error("Could not execute code action: "..err.message)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
M['workspace/executeCommand'] = function(_, _, _, _)
-- Error handling is done implicitly by wrapping all handlers; see end of this file
end
-- @msg of type ProgressParams
-- Basically a token of type number/string
local function progress_handler(_, _, params, client_id)
---@private
local function progress_handler(_, result, ctx, _)
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
err_message("LSP[", client_name, "] client has shut down after sending the message")
end
local val = params.value -- unspecified yet
local token = params.token -- string or number
local val = result.value -- unspecified yet
local token = result.token -- string or number
if val.kind then
@ -64,10 +62,11 @@ end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
M['$/progress'] = progress_handler
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
M['window/workDoneProgress/create'] = function(_, _, params, client_id)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
M['window/workDoneProgress/create'] = function(_, result, ctx)
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
local token = params.token -- string or number
local token = result.token -- string or number
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
err_message("LSP[", client_name, "] client has shut down after sending the message")
@ -76,12 +75,12 @@ M['window/workDoneProgress/create'] = function(_, _, params, client_id)
return vim.NIL
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
M['window/showMessageRequest'] = function(_, _, params)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
M['window/showMessageRequest'] = function(_, result)
local actions = params.actions
print(params.message)
local option_strings = {params.message, "\nRequest Actions:"}
local actions = result.actions
print(result.message)
local option_strings = {result.message, "\nRequest Actions:"}
for i, action in ipairs(actions) do
local title = action.title:gsub('\r\n', '\\r\\n')
title = title:gsub('\n', '\\n')
@ -97,8 +96,9 @@ M['window/showMessageRequest'] = function(_, _, params)
end
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
M['client/registerCapability'] = function(_, _, _, client_id)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
M['client/registerCapability'] = function(_, _, ctx)
local client_id = ctx.client_id
local warning_tpl = "The language server %s triggers a registerCapability "..
"handler despite dynamicRegistration set to false. "..
"Report upstream, this warning is harmless"
@ -109,25 +109,25 @@ M['client/registerCapability'] = function(_, _, _, client_id)
return vim.NIL
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
M['textDocument/codeAction'] = function(_, _, actions)
if actions == nil or vim.tbl_isempty(actions) then
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
M['textDocument/codeAction'] = function(_, result)
if result == nil or vim.tbl_isempty(result) then
print("No code actions available")
return
end
local option_strings = {"Code Actions:"}
for i, action in ipairs(actions) do
local option_strings = {"Code actions:"}
for i, action in ipairs(result) do
local title = action.title:gsub('\r\n', '\\r\\n')
title = title:gsub('\n', '\\n')
table.insert(option_strings, string.format("%d. %s", i, title))
end
local choice = vim.fn.inputlist(option_strings)
if choice < 1 or choice > #actions then
if choice < 1 or choice > #result then
return
end
local action_chosen = actions[choice]
local action_chosen = result[choice]
-- textDocument/codeAction can return either Command[] or CodeAction[].
-- If it is a CodeAction, it can have either an edit, a command or both.
-- Edits should be executed first
@ -143,8 +143,8 @@ M['textDocument/codeAction'] = function(_, _, actions)
end
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
M['workspace/applyEdit'] = function(_, _, workspace_edit)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
M['workspace/applyEdit'] = function(_, workspace_edit)
if not workspace_edit then return end
-- TODO(ashkan) Do something more with label?
if workspace_edit.label then
@ -157,30 +157,30 @@ M['workspace/applyEdit'] = function(_, _, workspace_edit)
}
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration
M['workspace/configuration'] = function(err, _, params, client_id)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration
M['workspace/configuration'] = function(_, result, ctx)
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
if not client then
err_message("LSP[id=", client_id, "] client has shut down after sending the message")
return
end
if err then error(vim.inspect(err)) end
if not params.items then
if not result.items then
return {}
end
local result = {}
for _, item in ipairs(params.items) do
local response = {}
for _, item in ipairs(result.items) do
if item.section then
local value = util.lookup_section(client.config.settings, item.section) or vim.NIL
-- For empty sections with no explicit '' key, return settings as is
if value == vim.NIL and item.section == '' then
value = client.config.settings or vim.NIL
end
table.insert(result, value)
table.insert(response, value)
end
end
return result
return response
end
M['textDocument/publishDiagnostics'] = function(...)
@ -191,51 +191,65 @@ M['textDocument/codeLens'] = function(...)
return require('vim.lsp.codelens').on_codelens(...)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
M['textDocument/references'] = function(_, _, result)
if not result then return end
util.set_qflist(util.locations_to_items(result))
api.nvim_command("copen")
---@private
--- Return a function that converts LSP responses to list items and opens the list
---
--- The returned function has an optional {config} parameter that accepts a table
--- with the following keys:
---
--- loclist: (boolean) use the location list (default is to use the quickfix list)
---
---@param map_result function `((resp, bufnr) -> list)` to convert the response
---@param entity name of the resource used in a `not found` error message
local function response_to_list(map_result, entity)
return function(_,result, ctx, config)
if not result or vim.tbl_isempty(result) then
vim.notify('No ' .. entity .. ' found')
else
config = config or {}
if config.loclist then
util.set_loclist(map_result(result, ctx.bufnr))
api.nvim_command("lopen")
else
util.set_qflist(map_result(result, ctx.bufnr))
api.nvim_command("copen")
end
end
end
end
--@private
--- Prints given list of symbols to the quickfix list.
--@param _ (not used)
--@param _ (not used)
--@param result (list of Symbols) LSP method name
--@param result (table) result of LSP method; a location or a list of locations.
---(`textDocument/definition` can return `Location` or `Location[]`
local symbol_handler = function(_, _, result, _, bufnr)
if not result or vim.tbl_isempty(result) then return end
util.set_qflist(util.symbols_to_items(result, bufnr))
api.nvim_command("copen")
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
M['textDocument/documentSymbol'] = symbol_handler
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
M['workspace/symbol'] = symbol_handler
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
M['textDocument/references'] = response_to_list(util.locations_to_items, 'references')
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename
M['textDocument/rename'] = function(_, _, result)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols')
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols')
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename
M['textDocument/rename'] = function(_, result, _)
if not result then return end
util.apply_workspace_edit(result)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting
M['textDocument/rangeFormatting'] = function(_, _, result)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting
M['textDocument/rangeFormatting'] = function(_, result, ctx, _)
if not result then return end
util.apply_text_edits(result)
util.apply_text_edits(result, ctx.bufnr)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
M['textDocument/formatting'] = function(_, _, result)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
M['textDocument/formatting'] = function(_, result, ctx, _)
if not result then return end
util.apply_text_edits(result)
util.apply_text_edits(result, ctx.bufnr)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
M['textDocument/completion'] = function(_, _, result)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
M['textDocument/completion'] = function(_, result, _, _)
if vim.tbl_isempty(result or {}) then return end
local row, col = unpack(api.nvim_win_get_cursor(0))
local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1])
@ -260,9 +274,9 @@ end
--- - border: (default=nil)
--- - Add borders to the floating window
--- - See |vim.api.nvim_open_win()|
function M.hover(_, method, result, _, _, config)
function M.hover(_, result, ctx, config)
config = config or {}
config.focus_id = method
config.focus_id = ctx.method
if not (result and result.contents) then
-- return { 'No information available' }
return
@ -281,13 +295,13 @@ M['textDocument/hover'] = M.hover
--@private
--- Jumps to a location. Used as a handler for multiple LSP methods.
--@param _ (not used)
--@param method (string) LSP method name
--@param result (table) result of LSP method; a location or a list of locations.
---@param _ (not used)
---@param result (table) result of LSP method; a location or a list of locations.
---@param ctx (table) table containing the context of the request, including the method
---(`textDocument/definition` can return `Location` or `Location[]`
local function location_handler(_, method, result)
local function location_handler(_, result, ctx, _)
if result == nil or vim.tbl_isempty(result) then
local _ = log.info() and log.info(method, 'No location found')
local _ = log.info() and log.info(ctx.method, 'No location found')
return nil
end
@ -328,17 +342,19 @@ M['textDocument/implementation'] = location_handler
--- - border: (default=nil)
--- - Add borders to the floating window
--- - See |vim.api.nvim_open_win()|
function M.signature_help(_, method, result, _, bufnr, config)
function M.signature_help(_, result, ctx, config)
config = config or {}
config.focus_id = method
config.focus_id = ctx.method
-- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler
-- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore
if not (result and result.signatures and result.signatures[1]) then
print('No signature help available')
return
end
local ft = api.nvim_buf_get_option(bufnr, 'filetype')
local lines = util.convert_signature_help_to_markdown_lines(result, ft)
local client = vim.lsp.get_client_by_id(ctx.client_id)
local triggers = client.resolved_capabilities.signature_help_trigger_characters
local ft = api.nvim_buf_get_option(ctx.bufnr, 'filetype')
local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers)
lines = util.trim_empty_lines(lines)
if vim.tbl_isempty(lines) then
print('No signature help available')
@ -350,10 +366,10 @@ end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
M['textDocument/signatureHelp'] = M.signature_help
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
M['textDocument/documentHighlight'] = function(_, _, result, _, bufnr, _)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
M['textDocument/documentHighlight'] = function(_, result, ctx, _)
if not result then return end
util.buf_highlight_references(bufnr, result)
util.buf_highlight_references(ctx.bufnr, result)
end
--@private
@ -364,7 +380,7 @@ end
--@returns `CallHierarchyIncomingCall[]` if {direction} is `"from"`,
--@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`,
local make_call_hierarchy_handler = function(direction)
return function(_, _, result)
return function(_, result)
if not result then return end
local items = {}
for _, call_hierarchy_call in pairs(result) do
@ -389,10 +405,11 @@ M['callHierarchy/incomingCalls'] = make_call_hierarchy_handler('from')
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/outgoingCalls
M['callHierarchy/outgoingCalls'] = make_call_hierarchy_handler('to')
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/logMessage
M['window/logMessage'] = function(_, _, result, client_id)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage
M['window/logMessage'] = function(_, result, ctx, _)
local message_type = result.type
local message = result.message
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
@ -402,7 +419,7 @@ M['window/logMessage'] = function(_, _, result, client_id)
log.error(message)
elseif message_type == protocol.MessageType.Warning then
log.warn(message)
elseif message_type == protocol.MessageType.Info then
elseif message_type == protocol.MessageType.Info or message_type == protocol.MessageType.Log then
log.info(message)
else
log.debug(message)
@ -410,10 +427,11 @@ M['window/logMessage'] = function(_, _, result, client_id)
return result
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/showMessage
M['window/showMessage'] = function(_, _, result, client_id)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessage
M['window/showMessage'] = function(_, result, ctx, _)
local message_type = result.type
local message = result.message
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
@ -430,16 +448,23 @@ end
-- Add boilerplate error validation and logging for all of these.
for k, fn in pairs(M) do
M[k] = function(err, method, params, client_id, bufnr, config)
local _ = log.debug() and log.debug('default_handler', method, {
params = params, client_id = client_id, err = err, bufnr = bufnr, config = config
M[k] = function(err, result, ctx, config)
local _ = log.trace() and log.trace('default_handler', ctx.method, {
err = err, result = result, ctx=vim.inspect(ctx), config = config
})
if err then
return err_message(tostring(err))
local client = vim.lsp.get_client_by_id(ctx.client_id)
local client_name = client and client.name or string.format("client_id=%d", ctx.client_id)
-- LSP spec:
-- interface ResponseError:
-- code: integer;
-- message: string;
-- data?: string | number | boolean | array | object | null;
return err_message(tostring(err.code) .. ': ' .. err.message)
end
return fn(err, method, params, client_id, bufnr, config)
return fn(err, result, ctx, config)
end
end

View File

@ -14,10 +14,11 @@ log.levels = vim.deepcopy(vim.log.levels)
-- Default log level is warn.
local current_log_level = log.levels.WARN
local log_date_format = "%FT%H:%M:%S%z"
local log_date_format = "%F %H:%M:%S"
local format_func = function(arg) return vim.inspect(arg, {newline=''}) end
do
local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/"
local path_sep = vim.loop.os_uname().version:match("Windows") and "\\" or "/"
--@private
local function path_join(...)
return table.concat(vim.tbl_flatten{...}, path_sep)
@ -33,7 +34,7 @@ do
vim.fn.mkdir(vim.fn.stdpath('cache'), "p")
local logfile = assert(io.open(logfilename, "a+"))
-- Start message for logging
logfile:write(string.format("[ START ] %s ] LSP logging initiated\n", os.date(log_date_format)))
logfile:write(string.format("[START][%s] LSP logging initiated\n", os.date(log_date_format)))
for level, levelnr in pairs(log.levels) do
-- Also export the log level on the root object.
log[level] = levelnr
@ -56,14 +57,14 @@ do
if levelnr < current_log_level then return false end
if argc == 0 then return true end
local info = debug.getinfo(2, "Sl")
local fileinfo = string.format("%s:%s", info.short_src, info.currentline)
local parts = { table.concat({"[", level, "]", os.date(log_date_format), "]", fileinfo, "]"}, " ") }
local header = string.format("[%s][%s] ...%s:%s", level, os.date(log_date_format), string.sub(info.short_src, #info.short_src - 15), info.currentline)
local parts = { header }
for i = 1, argc do
local arg = select(i, ...)
if arg == nil then
table.insert(parts, "nil")
else
table.insert(parts, vim.inspect(arg, {newline=''}))
table.insert(parts, format_func(arg))
end
end
logfile:write(table.concat(parts, '\t'), "\n")
@ -88,6 +89,18 @@ function log.set_level(level)
end
end
--- Gets the current log level.
function log.get_level()
return current_log_level
end
--- Sets formatting function used to format logs
---@param handle function function to apply to logging arguments, pass vim.inspect for multi-line formatting
function log.set_format_func(handle)
assert(handle == vim.inspect or type(handle) == 'function', "handle must be a function")
format_func = handle
end
--- Checks whether the level is sufficient for logging.
--@param level number log level
--@returns (bool) true if would log, false if not

View File

@ -392,7 +392,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
--@param payload (table) Converted into a JSON string, see |json_encode()|
--@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing.
local function encode_and_send(payload)
local _ = log.debug() and log.debug("rpc.send.payload", payload)
local _ = log.debug() and log.debug("rpc.send", payload)
if handle == nil or handle:is_closing() then return false end
-- TODO(ashkan) remove this once we have a Lua json_encode
schedule(function()
@ -493,7 +493,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
-- on_error(client_errors.INVALID_SERVER_JSON, err)
return
end
local _ = log.debug() and log.debug("decoded", decoded)
local _ = log.debug() and log.debug("rpc.receive", decoded)
if type(decoded.method) == 'string' and decoded.id then
-- Server Request

View File

@ -1,4 +1,5 @@
local protocol = require 'vim.lsp.protocol'
local snippet = require 'vim.lsp._snippet'
local vim = vim
local validate = vim.validate
local api = vim.api
@ -147,10 +148,6 @@ local function sort_by_key(fn)
return false
end
end
--@private
local edit_sort_key = sort_by_key(function(e)
return {e.A[1], e.A[2], e.i}
end)
--@private
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
@ -175,6 +172,7 @@ local function get_line_byte_from_position(bufnr, position)
if ok then
return result
end
return math.min(#lines[1], col)
end
end
return col
@ -238,53 +236,119 @@ function M.get_progress_messages()
end
--- Applies a list of text edits to a buffer.
--@param text_edits (table) list of `TextEdit` objects
--@param buf_nr (number) Buffer id
---@param text_edits table list of `TextEdit` objects
---@param bufnr number Buffer id
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
function M.apply_text_edits(text_edits, bufnr)
if not next(text_edits) then return end
if not api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
end
api.nvim_buf_set_option(bufnr, 'buflisted', true)
local start_line, finish_line = math.huge, -1
local cleaned = {}
for i, e in ipairs(text_edits) do
-- adjust start and end column for UTF-16 encoding of non-ASCII characters
local start_row = e.range.start.line
local start_col = get_line_byte_from_position(bufnr, e.range.start)
local end_row = e.range["end"].line
local end_col = get_line_byte_from_position(bufnr, e.range['end'])
start_line = math.min(e.range.start.line, start_line)
finish_line = math.max(e.range["end"].line, finish_line)
-- TODO(ashkan) sanity check ranges for overlap.
table.insert(cleaned, {
i = i;
A = {start_row; start_col};
B = {end_row; end_col};
lines = vim.split(e.newText, '\n', true);
-- Fix reversed range and indexing each text_edits
local index = 0
text_edits = vim.tbl_map(function(text_edit)
index = index + 1
text_edit._index = index
if text_edit.range.start.line > text_edit.range['end'].line or text_edit.range.start.line == text_edit.range['end'].line and text_edit.range.start.character > text_edit.range['end'].character then
local start = text_edit.range.start
text_edit.range.start = text_edit.range['end']
text_edit.range['end'] = start
end
return text_edit
end, text_edits)
-- Sort text_edits
table.sort(text_edits, function(a, b)
if a.range.start.line ~= b.range.start.line then
return a.range.start.line > b.range.start.line
end
if a.range.start.character ~= b.range.start.character then
return a.range.start.character > b.range.start.character
end
if a._index ~= b._index then
return a._index > b._index
end
end)
-- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
local has_eol_text_edit = false
local max = vim.api.nvim_buf_line_count(bufnr)
local len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '')
text_edits = vim.tbl_map(function(text_edit)
if max <= text_edit.range.start.line then
text_edit.range.start.line = max - 1
text_edit.range.start.character = len
text_edit.newText = '\n' .. text_edit.newText
has_eol_text_edit = true
end
if max <= text_edit.range['end'].line then
text_edit.range['end'].line = max - 1
text_edit.range['end'].character = len
has_eol_text_edit = true
end
return text_edit
end, text_edits)
-- Some LSP servers are depending on the VSCode behavior.
-- The VSCode will re-locate the cursor position after applying TextEdit so we also do it.
local is_current_buf = vim.api.nvim_get_current_buf() == bufnr
local cursor = (function()
if not is_current_buf then
return {
row = -1,
col = -1,
}
end
local cursor = vim.api.nvim_win_get_cursor(0)
return {
row = cursor[1] - 1,
col = cursor[2],
}
end)()
-- Apply text edits.
local is_cursor_fixed = false
for _, text_edit in ipairs(text_edits) do
local e = {
start_row = text_edit.range.start.line,
start_col = get_line_byte_from_position(bufnr, text_edit.range.start),
end_row = text_edit.range['end'].line,
end_col = get_line_byte_from_position(bufnr, text_edit.range['end']),
text = vim.split(text_edit.newText, '\n', true),
}
vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
local row_count = (e.end_row - e.start_row) + 1
if e.end_row < cursor.row then
cursor.row = cursor.row + (#e.text - row_count)
is_cursor_fixed = true
elseif e.end_row == cursor.row and e.end_col <= cursor.col then
cursor.row = cursor.row + (#e.text - row_count)
cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
if #e.text == 1 then
cursor.col = cursor.col + e.start_col
end
is_cursor_fixed = true
end
end
if is_cursor_fixed then
vim.api.nvim_win_set_cursor(0, {
cursor.row + 1,
math.min(cursor.col, #(vim.api.nvim_buf_get_lines(bufnr, cursor.row, cursor.row + 1, false)[1] or ''))
})
end
-- Reverse sort the orders so we can apply them without interfering with
-- eachother. Also add i as a sort key to mimic a stable sort.
table.sort(cleaned, edit_sort_key)
local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false)
local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol')
local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) <= finish_line + 1
if set_eol and (#lines == 0 or #lines[#lines] ~= 0) then
table.insert(lines, '')
-- Remove final line if needed
local fix_eol = has_eol_text_edit
fix_eol = fix_eol and api.nvim_buf_get_option(bufnr, 'fixeol')
fix_eol = fix_eol and (vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') == ''
if fix_eol then
vim.api.nvim_buf_set_lines(bufnr, -2, -1, false, {})
end
for i = #cleaned, 1, -1 do
local e = cleaned[i]
local A = {e.A[1] - start_line, e.A[2]}
local B = {e.B[1] - start_line, e.B[2]}
lines = M.set_lines(lines, A, B, e.lines)
end
if set_eol and #lines[#lines] == 0 then
table.remove(lines)
end
api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines)
end
-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
@ -579,9 +643,13 @@ end
--@param input (string) unparsed snippet
--@returns (string) parsed snippet
function M.parse_snippet(input)
local res, _ = parse_snippet_rec(input, false)
return res
local ok, parsed = pcall(function()
return tostring(snippet.parse(input))
end)
if not ok then
return input
end
return parsed
end
--@private
@ -810,16 +878,16 @@ function M.convert_input_to_markdown_lines(input, contents)
-- If it's plaintext, then wrap it in a <text></text> block
-- Some servers send input.value as empty, so let's ignore this :(
input.value = input.value or ''
local value = input.value or ''
if input.kind == "plaintext" then
-- wrap this in a <text></text> block so that stylize_markdown
-- can properly process it as plaintext
input.value = string.format("<text>\n%s\n</text>", input.value or "")
value = string.format("<text>\n%s\n</text>", value)
end
-- assert(type(input.value) == 'string')
list_extend(contents, split_lines(input.value))
-- assert(type(value) == 'string')
list_extend(contents, split_lines(value))
-- MarkupString variation 2
elseif input.language then
-- Some servers send input.value as empty, so let's ignore this :(

View File

@ -26,6 +26,8 @@
</screenshots>
<releases>
<release date="2021-09-26" version="0.5.1"/>
<release date="2021-07-02" version="0.5.0"/>
<release date="2020-08-04" version="0.4.4"/>
<release date="2019-11-06" version="0.4.3"/>
<release date="2019-09-15" version="0.4.2"/>

View File

@ -6,7 +6,7 @@ endif
let g:loaded_man = 1
command! -bang -bar -range=-1 -complete=customlist,man#complete -nargs=* Man
\ if <bang>0 | set ft=man |
\ if <bang>0 | call man#init_pager() |
\ else | call man#open_page(<count>, <q-mods>, <f-args>) | endif
augroup man

View File

@ -27,11 +27,7 @@ if &filetype != 'man'
finish
endif
if !exists('b:man_sect')
call man#init_pager()
endif
if b:man_sect =~# '^[023]'
if get(b:, 'man_sect', '') =~# '^[023]'
syntax case match
syntax include @c $VIMRUNTIME/syntax/c.vim
syntax match manCFuncDefinition display '\<\h\w*\>\ze\(\s\|\n\)*(' contained

View File

@ -12,6 +12,7 @@
# - CMakeLists.txt: Unset NVIM_VERSION_PRERELEASE
# - CMakeLists.txt: Unset NVIM_API_PRERELEASE
# - Create test/functional/fixtures/api_level_N.mpack
# - Add date and version to runtime/nvim.appdata.xml
# - Tag the commit.
# Create the "version bump" commit:
# - CMakeLists.txt: Set NVIM_VERSION_PRERELEASE to "-dev"
@ -62,6 +63,10 @@ _do_release_commit() {
git add test/functional/fixtures/api_level_$__API_LEVEL.mpack
fi
$__sed -i.bk 's,(<releases>),\1\
<release date="'"${__DATE}"'" version="'"${__VERSION}"'"/>,' runtime/nvim.appdata.xml
git add runtime/nvim.appdata.xml
if ! test "$ARG1" = '--use-current-commit' ; then
echo "Building changelog since ${__LAST_TAG}..."
@ -75,14 +80,12 @@ _do_release_commit() {
_do_bump_commit() {
$__sed -i.bk 's/(NVIM_VERSION_PRERELEASE) ""/\1 "-dev"/' CMakeLists.txt
$__sed -i.bk 's/set\((NVIM_VERSION_PATCH) [[:digit:]]/set(\1 ?/' CMakeLists.txt
$__sed -i.bk 's,(<releases>),\1\
<release date="'"${__DATE}"'" version="xxx"/>,' runtime/nvim.appdata.xml
rm CMakeLists.txt.bk
rm runtime/nvim.appdata.xml.bk
rm -f CMakeLists.txt.bk
rm -f runtime/nvim.appdata.xml.bk
nvim +'/NVIM_VERSION' +1new +'exe "norm! iUpdate version numbers!!!"' \
-O CMakeLists.txt runtime/nvim.appdata.xml
-O CMakeLists.txt
git add CMakeLists.txt runtime/nvim.appdata.xml
git add CMakeLists.txt
git commit -m "$__BUMP_MSG"
}
@ -92,11 +95,7 @@ fi
_do_bump_commit
echo "
Next steps:
- Update runtime/nvim.appdata.xml on _master_
- Run tests/CI (version_spec.lua)!
- Push the tag:
git push --follow-tags
- Update the 'stable' tag:
git push --force upstream HEAD^:refs/tags/stable
git fetch --tags
- Update website: index.html"

View File

@ -1724,7 +1724,7 @@ BYPASS_AU:
void block_autocmds(void)
{
// Remember the value of v:termresponse.
if (is_autocmd_blocked()) {
if (!is_autocmd_blocked()) {
old_termresponse = get_vim_var_str(VV_TERMRESPONSE);
}
autocmd_blocked++;
@ -1737,7 +1737,7 @@ void unblock_autocmds(void)
// When v:termresponse was set while autocommands were blocked, trigger
// the autocommands now. Esp. useful when executing a shell command
// during startup (nvim -d).
if (is_autocmd_blocked()
if (!is_autocmd_blocked()
&& get_vim_var_str(VV_TERMRESPONSE) != old_termresponse) {
apply_autocmds(EVENT_TERMRESPONSE, NULL, NULL, false, curbuf);
}

View File

@ -289,6 +289,9 @@ static void close_cb(Stream *stream, void *data)
/// `on_stdout` is ignored
/// @param[in] detach True if the job should not be killed when nvim exits,
/// ignored if `pty` is true
/// @param[in] stdin_mode Stdin mode. Either kChannelStdinPipe to open a
/// channel for stdin or kChannelStdinNull to leave
/// stdin disconnected.
/// @param[in] cwd Initial working directory for the job. Nvim's working
/// directory if `cwd` is NULL
/// @param[in] pty_width Width of the pty, ignored if `pty` is false
@ -302,7 +305,7 @@ static void close_cb(Stream *stream, void *data)
Channel *channel_job_start(char **argv, CallbackReader on_stdout,
CallbackReader on_stderr, Callback on_exit,
bool pty, bool rpc, bool overlapped, bool detach,
const char *cwd,
ChannelStdinMode stdin_mode, const char *cwd,
uint16_t pty_width, uint16_t pty_height,
dict_T *env, varnumber_T *status_out)
{
@ -345,7 +348,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
proc->overlapped = overlapped;
char *cmd = xstrdup(proc->argv[0]);
bool has_out, has_err;
bool has_in, has_out, has_err;
if (proc->type == kProcessTypePty) {
has_out = true;
has_err = false;
@ -353,7 +356,17 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
has_out = rpc || callback_reader_set(chan->on_data);
has_err = callback_reader_set(chan->on_stderr);
}
int status = process_spawn(proc, true, has_out, has_err);
switch (stdin_mode) {
case kChannelStdinPipe:
has_in = true;
break;
case kChannelStdinNull:
has_in = false;
break;
}
int status = process_spawn(proc, has_in, has_out, has_err);
if (status) {
EMSG3(_(e_jobspawn), os_strerror(status), cmd);
xfree(cmd);
@ -369,7 +382,9 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
tv_dict_free(proc->env);
}
wstream_init(&proc->in, 0);
if (has_in) {
wstream_init(&proc->in, 0);
}
if (has_out) {
rstream_init(&proc->out, 0);
}

View File

@ -28,6 +28,10 @@ typedef enum {
kChannelPartAll
} ChannelPart;
typedef enum {
kChannelStdinPipe,
kChannelStdinNull,
} ChannelStdinMode;
typedef struct {
Stream in;

View File

@ -1446,15 +1446,29 @@ void getvcols(win_T *wp, pos_T *pos1, pos_T *pos2, colnr_T *left,
/// skipwhite: skip over ' ' and '\t'.
///
/// @param[in] q String to skip in.
/// @param[in] p String to skip in.
///
/// @return Pointer to character after the skipped whitespace.
char_u *skipwhite(const char_u *q)
char_u *skipwhite(const char_u *const p)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
FUNC_ATTR_NONNULL_RET
{
const char_u *p = q;
while (ascii_iswhite(*p)) {
return skipwhite_len(p, STRLEN(p));
}
/// Like `skipwhite`, but skip up to `len` characters.
/// @see skipwhite
///
/// @param[in] p String to skip in.
/// @param[in] len Max length to skip.
///
/// @return Pointer to character after the skipped whitespace, or the `len`-th
/// character in the string.
char_u *skipwhite_len(const char_u *p, size_t len)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
FUNC_ATTR_NONNULL_RET
{
for (; len > 0 && ascii_iswhite(*p); len--) {
p++;
}
return (char_u *)p;
@ -1600,6 +1614,18 @@ char_u* skiptowhite_esc(char_u *p) {
return p;
}
/// Skip over text until '\n' or NUL.
///
/// @param[in] p Text to skip over.
///
/// @return Pointer to the next '\n' or NUL character.
char_u *skip_to_newline(const char_u *const p)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
FUNC_ATTR_NONNULL_RET
{
return (char_u *)xstrchrnul((const char *)p, NL);
}
/// Gets a number from a string and skips over it, signalling overflow.
///
/// @param[out] pp A pointer to a pointer to char_u.

View File

@ -5181,6 +5181,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
bool pty = false;
bool clear_env = false;
bool overlapped = false;
ChannelStdinMode stdin_mode = kChannelStdinPipe;
CallbackReader on_stdout = CALLBACK_READER_INIT,
on_stderr = CALLBACK_READER_INIT;
Callback on_exit = CALLBACK_NONE;
@ -5195,6 +5196,17 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
clear_env = tv_dict_get_number(job_opts, "clear_env") != 0;
overlapped = tv_dict_get_number(job_opts, "overlapped") != 0;
char *s = tv_dict_get_string(job_opts, "stdin", false);
if (s) {
if (!strncmp(s, "null", NUMBUFLEN)) {
stdin_mode = kChannelStdinNull;
} else if (!strncmp(s, "pipe", NUMBUFLEN)) {
// Nothing to do, default value
} else {
EMSG3(_(e_invargNval), "stdin", s);
}
}
if (pty && rpc) {
EMSG2(_(e_invarg2), "job cannot have both 'pty' and 'rpc' options set");
shell_free_argv(argv);
@ -5251,8 +5263,8 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
env = create_environment(job_env, clear_env, pty, term_name);
Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit, pty,
rpc, overlapped, detach, cwd, width, height,
env, &rettv->vval.v_number);
rpc, overlapped, detach, stdin_mode, cwd,
width, height, env, &rettv->vval.v_number);
if (chan) {
channel_create_event(chan, NULL);
}
@ -7732,8 +7744,9 @@ static void f_rpcstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
Channel *chan = channel_job_start(argv, CALLBACK_READER_INIT,
CALLBACK_READER_INIT, CALLBACK_NONE,
false, true, false, false, NULL, 0, 0,
NULL, &rettv->vval.v_number);
false, true, false, false,
kChannelStdinPipe, NULL, 0, 0, NULL,
&rettv->vval.v_number);
if (chan) {
channel_create_event(chan, NULL);
}
@ -10849,10 +10862,11 @@ static void f_termopen(typval_T *argvars, typval_T *rettv, FunPtr fptr)
const bool rpc = false;
const bool overlapped = false;
const bool detach = false;
ChannelStdinMode stdin_mode = kChannelStdinPipe;
uint16_t term_width = MAX(0, curwin->w_width_inner - win_col_off(curwin));
Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit,
pty, rpc, overlapped, detach, cwd,
term_width, curwin->w_height_inner,
pty, rpc, overlapped, detach, stdin_mode,
cwd, term_width, curwin->w_height_inner,
env, &rettv->vval.v_number);
if (rettv->vval.v_number <= 0) {
return;

View File

@ -2635,44 +2635,42 @@ static void cmd_source(char_u *fname, exarg_T *eap)
}
}
/// Concatenate VimL line if it starts with a line continuation into a growarray
/// (excluding the continuation chars and leading whitespace)
///
/// @note Growsize of the growarray may be changed to speed up concatenations!
///
/// @param ga the growarray to append to
/// @param init_growsize the starting growsize value of the growarray
/// @param p pointer to the beginning of the line to consider
/// @param len the length of this line
///
/// @return true if this line did begin with a continuation (the next line
/// should also be considered, if it exists); false otherwise
static bool concat_continued_line(garray_T *const ga, const int init_growsize,
const char_u *const p, size_t len)
FUNC_ATTR_NONNULL_ALL
{
const char_u *const line = skipwhite_len(p, len);
len -= (size_t)(line - p);
// Skip lines starting with '\" ', concat lines starting with '\'
if (len >= 3 && STRNCMP(line, "\"\\ ", 3) == 0) {
return true;
} else if (len == 0 || line[0] != '\\') {
return false;
}
if (ga->ga_len > init_growsize) {
ga_set_growsize(ga, MAX(ga->ga_len, 8000));
}
ga_concat_len(ga, (const char *)line + 1, len - 1);
return true;
}
typedef struct {
linenr_T curr_lnum;
const linenr_T final_lnum;
} GetBufferLineCookie;
/// Get one line from the current selection in the buffer.
/// Called by do_cmdline() when it's called from cmd_source_buffer().
///
/// @return pointer to allocated line, or NULL for end-of-file or
/// some error.
static char_u *get_buffer_line(int c, void *cookie, int indent, bool do_concat)
{
GetBufferLineCookie *p = cookie;
if (p->curr_lnum > p->final_lnum) {
return NULL;
}
char_u *curr_line = ml_get(p->curr_lnum);
p->curr_lnum++;
return (char_u *)xstrdup((const char *)curr_line);
}
static void cmd_source_buffer(const exarg_T *eap)
FUNC_ATTR_NONNULL_ALL
{
GetBufferLineCookie cookie = {
.curr_lnum = eap->line1,
.final_lnum = eap->line2,
};
if (curbuf != NULL && curbuf->b_fname
&& path_with_extension((const char *)curbuf->b_fname, "lua")) {
nlua_source_using_linegetter(get_buffer_line, (void *)&cookie,
":source (no file)");
} else {
source_using_linegetter((void *)&cookie, get_buffer_line,
":source (no file)");
}
}
/// ":source" and associated commands.
///
/// @return address holding the next breakpoint line for a source cookie
@ -2725,17 +2723,27 @@ typedef struct {
static char_u *get_str_line(int c, void *cookie, int indent, bool do_concat)
{
GetStrLineCookie *p = cookie;
size_t i = p->offset;
if (strlen((char *)p->buf) <= p->offset) {
if (STRLEN(p->buf) <= p->offset) {
return NULL;
}
while (!(p->buf[i] == '\n' || p->buf[i] == '\0')) {
i++;
const char_u *line = p->buf + p->offset;
const char_u *eol = skip_to_newline(line);
garray_T ga;
ga_init(&ga, sizeof(char_u), 400);
ga_concat_len(&ga, (const char *)line, (size_t)(eol - line));
if (do_concat && vim_strchr(p_cpo, CPO_CONCAT) == NULL) {
while (eol[0] != NUL) {
line = eol + 1;
const char_u *const next_eol = skip_to_newline(line);
if (!concat_continued_line(&ga, 400, line, (size_t)(next_eol - line))) {
break;
}
eol = next_eol;
}
}
size_t line_length = i - p->offset;
char_u *buf = xmemdupz(p->buf + p->offset, line_length);
p->offset = i + 1;
return buf;
ga_append(&ga, NUL);
p->offset = (size_t)(eol - p->buf) + 1;
return ga.ga_data;
}
static int source_using_linegetter(void *cookie,
@ -2770,6 +2778,40 @@ static int source_using_linegetter(void *cookie,
return retval;
}
static void cmd_source_buffer(const exarg_T *const eap)
FUNC_ATTR_NONNULL_ALL
{
if (curbuf == NULL) {
return;
}
garray_T ga;
ga_init(&ga, sizeof(char_u), 400);
const linenr_T final_lnum = eap->line2;
// Copy the contents to be executed.
for (linenr_T curr_lnum = eap->line1; curr_lnum <= final_lnum; curr_lnum++) {
// Adjust growsize to current length to speed up concatenating many lines.
if (ga.ga_len > 400) {
ga_set_growsize(&ga, MAX(ga.ga_len, 8000));
}
ga_concat(&ga, ml_get(curr_lnum));
ga_append(&ga, NL);
}
((char_u *)ga.ga_data)[ga.ga_len - 1] = NUL;
const GetStrLineCookie cookie = {
.buf = ga.ga_data,
.offset = 0,
};
if (curbuf->b_fname
&& path_with_extension((const char *)curbuf->b_fname, "lua")) {
nlua_source_using_linegetter(get_str_line, (void *)&cookie,
":source (no file)");
} else {
source_using_linegetter((void *)&cookie, get_str_line,
":source (no file)");
}
ga_clear(&ga);
}
/// Executes lines in `src` as Ex commands.
///
/// @see do_source()
@ -3227,26 +3269,11 @@ char_u *getsourceline(int c, void *cookie, int indent, bool do_concat)
ga_init(&ga, (int)sizeof(char_u), 400);
ga_concat(&ga, line);
if (*p == '\\') {
ga_concat(&ga, p + 1);
}
for (;; ) {
while (sp->nextline != NULL
&& concat_continued_line(&ga, 400, sp->nextline,
STRLEN(sp->nextline))) {
xfree(sp->nextline);
sp->nextline = get_one_sourceline(sp);
if (sp->nextline == NULL) {
break;
}
p = skipwhite(sp->nextline);
if (*p == '\\') {
// Adjust the growsize to the current length to speed up
// concatenating many lines.
if (ga.ga_len > 400) {
ga_set_growsize(&ga, (ga.ga_len > 8000) ? 8000 : ga.ga_len);
}
ga_concat(&ga, p + 1);
} else if (p[0] != '"' || p[1] != '\\' || p[2] != ' ') {
break;
}
}
ga_append(&ga, NUL);
xfree(line);

View File

@ -303,6 +303,7 @@ int do_cmdline_cmd(const char *cmd)
/// DOCMD_KEYTYPED - Don't reset KeyTyped.
/// DOCMD_EXCRESET - Reset the exception environment (used for debugging).
/// DOCMD_KEEPLINE - Store first typed line (for repeating with ".").
/// DOCMD_PREVIEW - During 'inccommand' preview.
///
/// @return FAIL if cmdline could not be executed, OK otherwise
int do_cmdline(char_u *cmdline, LineGetter fgetline,
@ -601,7 +602,7 @@ int do_cmdline(char_u *cmdline, LineGetter fgetline,
recursive--;
// Ignore trailing '|'-separated commands in preview-mode ('inccommand').
if (State & CMDPREVIEW) {
if ((State & CMDPREVIEW) && (flags & DOCMD_PREVIEW)) {
next_cmdline = NULL;
}

View File

@ -11,6 +11,7 @@
#define DOCMD_KEYTYPED 0x08 // don't reset KeyTyped
#define DOCMD_EXCRESET 0x10 // reset exception environment (for debugging
#define DOCMD_KEEPLINE 0x20 // keep typed line for repeating with "."
#define DOCMD_PREVIEW 0x40 // during 'inccommand' preview
/* defines for eval_vars() */
#define VALID_PATH 1

View File

@ -2251,7 +2251,7 @@ static int command_line_changed(CommandLineState *s)
State |= CMDPREVIEW;
emsg_silent++; // Block error reporting as the command may be incomplete
msg_silent++; // Block messages, namely ones that prompt
do_cmdline(ccline.cmdbuff, NULL, NULL, DOCMD_KEEPLINE|DOCMD_NOWAIT);
do_cmdline(ccline.cmdbuff, NULL, NULL, DOCMD_KEEPLINE|DOCMD_NOWAIT|DOCMD_PREVIEW);
msg_silent--; // Unblock messages
emsg_silent--; // Unblock error reporting

View File

@ -268,7 +268,7 @@ bool extmark_clear(buf_T *buf, uint64_t ns_id,
}
ExtmarkNs *my_ns = all_ns ? buf_ns_ref(buf, item.ns_id, false) : ns;
map_del(uint64_t, uint64_t)(my_ns->map, item.mark_id);
map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark.id);
map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, start_id);
marktree_del_itr(buf->b_marktree, itr, false);
} else {
marktree_itr_next(buf->b_marktree, itr);

View File

@ -1555,8 +1555,8 @@ int vgetc(void)
*/
may_garbage_collect = false;
// Exec lua callbacks for on_keystroke
nlua_execute_log_keystroke(c);
// Execute Lua on_key callbacks.
nlua_execute_on_key(c);
return c;
}

View File

@ -1490,7 +1490,7 @@ int nlua_expand_pat(expand_T *xp,
lua_getfield(lstate, -1, "_expand_pat");
luaL_checktype(lstate, -1, LUA_TFUNCTION);
// [ vim, vim._log_keystroke, buf ]
// [ vim, vim._on_key, buf ]
lua_pushlstring(lstate, (const char *)pat, STRLEN(pat));
if (lua_pcall(lstate, 1, 2, 0) != 0) {
@ -1764,7 +1764,7 @@ char_u *nlua_register_table_as_callable(typval_T *const arg)
return name;
}
void nlua_execute_log_keystroke(int c)
void nlua_execute_on_key(int c)
{
char_u buf[NUMBUFLEN];
size_t buf_len = special_to_buf(c, mod_mask, false, buf);
@ -1778,17 +1778,17 @@ void nlua_execute_log_keystroke(int c)
// [ vim ]
lua_getglobal(lstate, "vim");
// [ vim, vim._log_keystroke ]
lua_getfield(lstate, -1, "_log_keystroke");
// [ vim, vim._on_key]
lua_getfield(lstate, -1, "_on_key");
luaL_checktype(lstate, -1, LUA_TFUNCTION);
// [ vim, vim._log_keystroke, buf ]
// [ vim, vim._on_key, buf ]
lua_pushlstring(lstate, (const char *)buf, buf_len);
if (lua_pcall(lstate, 1, 0, 0)) {
nlua_error(
lstate,
_("Error executing vim.log_keystroke lua callback: %.*s"));
_("Error executing vim.on_key Lua callback: %.*s"));
}
// [ vim ]

View File

@ -4,6 +4,7 @@
-- 1. runtime/lua/vim/ (the runtime): For "nice to have" features, e.g. the
-- `inspect` and `lpeg` modules.
-- 2. runtime/lua/vim/shared.lua: Code shared between Nvim and tests.
-- (This will go away if we migrate to nvim as the test-runner.)
-- 3. src/nvim/lua/: Compiled-into Nvim itself.
--
-- Guideline: "If in doubt, put it in the runtime".
@ -425,26 +426,35 @@ function vim.notify(msg, log_level, _opts)
end
local on_keystroke_callbacks = {}
function vim.register_keystroke_callback()
error('vim.register_keystroke_callback is deprecated, instead use: vim.on_key')
end
--- Register a lua {fn} with an {id} to be run after every keystroke.
local on_key_cbs = {}
--- Adds Lua function {fn} with namespace id {ns_id} as a listener to every,
--- yes every, input key.
---
--@param fn function: Function to call. It should take one argument, which is a string.
--- The string will contain the literal keys typed.
--- See |i_CTRL-V|
--- The Nvim command-line option |-w| is related but does not support callbacks
--- and cannot be toggled dynamically.
---
---@param fn function: Callback function. It should take one string argument.
--- On each key press, Nvim passes the key char to fn(). |i_CTRL-V|
--- If {fn} is nil, it removes the callback for the associated {ns_id}
--@param ns_id number? Namespace ID. If not passed or 0, will generate and return a new
--- namespace ID from |nvim_create_namesapce()|
---@param ns_id number? Namespace ID. If nil or 0, generates and returns a new
--- |nvim_create_namesapce()| id.
---
--@return number Namespace ID associated with {fn}
---@return number Namespace id associated with {fn}. Or count of all callbacks
---if on_key() is called without arguments.
---
--@note {fn} will be automatically removed if an error occurs while calling.
--- This is to prevent the annoying situation of every keystroke erroring
--- while trying to remove a broken callback.
--@note {fn} will not be cleared from |nvim_buf_clear_namespace()|
--@note {fn} will receive the keystrokes after mappings have been evaluated
function vim.register_keystroke_callback(fn, ns_id)
---@note {fn} will be removed if an error occurs while calling.
---@note {fn} will not be cleared by |nvim_buf_clear_namespace()|
---@note {fn} will receive the keys after mappings have been evaluated
function vim.on_key(fn, ns_id)
if fn == nil and ns_id == nil then
return #on_key_cbs
end
vim.validate {
fn = { fn, 'c', true},
ns_id = { ns_id, 'n', true }
@ -454,20 +464,19 @@ function vim.register_keystroke_callback(fn, ns_id)
ns_id = vim.api.nvim_create_namespace('')
end
on_keystroke_callbacks[ns_id] = fn
on_key_cbs[ns_id] = fn
return ns_id
end
--- Function that executes the keystroke callbacks.
--@private
function vim._log_keystroke(char)
--- Executes the on_key callbacks.
---@private
function vim._on_key(char)
local failed_ns_ids = {}
local failed_messages = {}
for k, v in pairs(on_keystroke_callbacks) do
for k, v in pairs(on_key_cbs) do
local ok, err_msg = pcall(v, char)
if not ok then
vim.register_keystroke_callback(nil, k)
vim.on_key(nil, k)
table.insert(failed_ns_ids, k)
table.insert(failed_messages, err_msg)
end
@ -475,7 +484,7 @@ function vim._log_keystroke(char)
if failed_ns_ids[1] then
error(string.format(
"Error executing 'on_keystroke' with ns_ids of '%s'\n With messages: %s",
"Error executing 'on_key' with ns_ids '%s'\n Messages: %s",
table.concat(failed_ns_ids, ", "),
table.concat(failed_messages, "\n")))
end

View File

@ -338,7 +338,7 @@ int main(int argc, char **argv)
// prepare screen now, so external UIs can display messages
starting = NO_BUFFERS;
screenclear();
TIME_MSG("initialized screen early for UI");
TIME_MSG("init screen for UI");
}

View File

@ -676,7 +676,7 @@ void win_set_minimal_style(win_T *wp)
}
// signcolumn: use 'auto'
if (wp->w_p_scl[0] != 'a') {
if (wp->w_p_scl[0] != 'a' || STRLEN(wp->w_p_scl) >= 8) {
xfree(wp->w_p_scl);
wp->w_p_scl = (char_u *)xstrdup("auto");
}
@ -737,6 +737,37 @@ void win_config_float(win_T *wp, FloatConfig fconfig)
redraw_later(wp, NOT_VALID);
}
// compute initial position
if (wp->w_float_config.relative == kFloatRelativeWindow) {
int row = wp->w_float_config.row;
int col = wp->w_float_config.col;
Error dummy = ERROR_INIT;
win_T *parent = find_window_by_handle(wp->w_float_config.window, &dummy);
if (parent) {
row += parent->w_winrow;
col += parent->w_wincol;
ScreenGrid *grid = &parent->w_grid;
int row_off = 0, col_off = 0;
screen_adjust_grid(&grid, &row_off, &col_off);
row += row_off;
col += col_off;
}
api_clear_error(&dummy);
if (wp->w_float_config.bufpos.lnum >= 0) {
pos_T pos = { wp->w_float_config.bufpos.lnum + 1,
wp->w_float_config.bufpos.col, 0 };
int trow, tcol, tcolc, tcole;
textpos2screenpos(wp, &pos, &trow, &tcol, &tcolc, &tcole, true);
row += trow - 1;
col += tcol - 1;
}
wp->w_winrow = row;
wp->w_wincol = col;
} else {
wp->w_winrow = fconfig.row;
wp->w_wincol = fconfig.col;
}
// changing border style while keeping border only requires redrawing border
if (fconfig.border) {
wp->w_redr_border = true;
@ -770,7 +801,6 @@ int win_fdccol_count(win_T *wp)
}
}
void ui_ext_win_position(win_T *wp)
{
if (!wp->w_floating) {
@ -817,6 +847,8 @@ void ui_ext_win_position(win_T *wp)
int comp_row = (int)row - (south ? wp->w_height : 0);
int comp_col = (int)col - (east ? wp->w_width : 0);
comp_row += grid->comp_row;
comp_col += grid->comp_col;
comp_row = MAX(MIN(comp_row, Rows-wp->w_height_outer-1), 0);
comp_col = MAX(MIN(comp_col, Columns-wp->w_width_outer), 0);
wp->w_winrow = comp_row;
@ -2702,7 +2734,11 @@ static win_T *win_free_mem(
// When deleting the current window of another tab page select a new
// current window.
if (tp != NULL && win == tp->tp_curwin) {
tp->tp_curwin = wp;
if (win_valid(tp->tp_prevwin) && tp->tp_prevwin != win) {
tp->tp_curwin = tp->tp_prevwin;
} else {
tp->tp_curwin = tp->tp_firstwin;
}
}
return wp;

View File

@ -258,6 +258,9 @@ Number; !must be defined to function properly):
- `TEST_SKIP_FRAGILE` (F) (D): makes test suite skip some fragile tests.
- `TEST_TIMEOUT` (FU) (I): specifies maximum time, in seconds, before the test
suite run is killed
- `NVIM_PROG`, `NVIM_PRG` (F) (S): override path to Neovim executable (default
to `build/bin/nvim`).

View File

@ -5,6 +5,7 @@ local eq, clear, eval, command, nvim, next_msg =
local meths = helpers.meths
local exec_lua = helpers.exec_lua
local retry = helpers.retry
local isCI = helpers.isCI
describe('notify', function()
local channel
@ -76,6 +77,10 @@ describe('notify', function()
end)
it('cancels stale events on channel close', function()
if isCI() then
pending('hangs on CI #14083 #15251')
return
end
if helpers.pending_win32(pending) then return end
local catchan = eval("jobstart(['cat'], {'rpc': v:true})")
eq({id=catchan, stream='job', mode='rpc', client = {}}, exec_lua ([[

View File

@ -115,6 +115,19 @@ describe('API', function()
nvim('exec','autocmd BufAdd * :let x1 = "Hello"', false)
nvim('command', 'new foo')
eq('Hello', request('nvim_eval', 'g:x1'))
-- Line continuations
nvim('exec', [[
let abc = #{
\ a: 1,
"\ b: 2,
\ c: 3
\ }]], false)
eq({a = 1, c = 3}, request('nvim_eval', 'g:abc'))
-- try no spaces before continuations to catch off-by-one error
nvim('exec', 'let ab = #{\n\\a: 98,\n"\\ b: 2\n\\}', false)
eq({a = 98}, request('nvim_eval', 'g:ab'))
end)
it('non-ASCII input', function()

View File

@ -346,6 +346,21 @@ describe('API/win', function()
eq(2, #meths.list_wins())
eq('', funcs.getcmdwintype())
end)
it('closing current (float) window of another tabpage #15313', function()
command('tabedit')
eq(2, eval('tabpagenr()'))
local win = meths.open_win(0, true, {
relative='editor', row=10, col=10, width=50, height=10
})
local tabpage = eval('tabpagenr()')
command('tabprevious')
eq(1, eval('tabpagenr()'))
meths.win_close(win, false)
eq(1001, meths.tabpage_get_win(tabpage).id)
helpers.assert_alive()
end)
end)
describe('hide', function()

View File

@ -348,6 +348,12 @@ describe('jobs', function()
eq(false, pcall(function()
nvim('command', 'call jobsend(j, ["some data"])')
end))
command("let g:job_opts.stdin = 'null'")
nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
eq(false, pcall(function()
nvim('command', 'call jobsend(j, ["some data"])')
end))
end)
it('disallows jobsend on a non-existent job', function()

View File

@ -8,6 +8,8 @@ local feed = helpers.feed
local feed_command = helpers.feed_command
local write_file = helpers.write_file
local exec = helpers.exec
local exc_exec = helpers.exc_exec
local exec_lua = helpers.exec_lua
local eval = helpers.eval
local exec_capture = helpers.exec_capture
local neq = helpers.neq
@ -18,16 +20,30 @@ describe(':source', function()
end)
it('current buffer', function()
insert('let a = 2')
insert([[
let a = 2
let b = #{
\ k: "v"
"\ (o_o)
\ }]])
command('source')
eq('2', meths.exec('echo a', true))
eq("{'k': 'v'}", meths.exec('echo b', true))
exec('set cpoptions+=C')
eq('Vim(let):E15: Invalid expression: #{', exc_exec('source'))
end)
it('selection in current buffer', function()
insert(
'let a = 2\n'..
'let a = 3\n'..
'let a = 4\n')
insert([[
let a = 2
let a = 3
let a = 4
let b = #{
"\ (>_<)
\ K: "V"
\ }]])
-- Source the 2nd line only
feed('ggjV')
@ -38,13 +54,26 @@ describe(':source', function()
feed('ggjVG')
feed_command(':source')
eq('4', meths.exec('echo a', true))
eq("{'K': 'V'}", meths.exec('echo b', true))
exec('set cpoptions+=C')
eq('Vim(let):E15: Invalid expression: #{', exc_exec("'<,'>source"))
end)
it('does not break if current buffer is modified while sourced', function()
insert [[
bwipeout!
let a = 123
]]
command('source')
eq('123', meths.exec('echo a', true))
end)
it('multiline heredoc command', function()
insert(
'lua << EOF\n'..
'y = 4\n'..
'EOF\n')
insert([[
lua << EOF
y = 4
EOF]])
command('source')
eq('4', meths.exec('echo luaeval("y")', true))
@ -67,13 +96,21 @@ describe(':source', function()
vim.g.b = 5
vim.g.b = 6
vim.g.b = 7
a = [=[
"\ a
\ b]=]
]])
command('edit '..test_file)
feed('ggjV')
feed_command(':source')
eq(6, eval('g:b'))
feed('GVkk')
feed_command(':source')
eq(' "\\ a\n \\ b', exec_lua('return _G.a'))
os.remove(test_file)
end)
@ -84,12 +121,16 @@ describe(':source', function()
vim.g.c = 10
vim.g.c = 11
vim.g.c = 12
a = [=[
\ 1
"\ 2]=]
]])
command('edit '..test_file)
feed_command(':source')
eq(12, eval('g:c'))
eq(' \\ 1\n "\\ 2', exec_lua('return _G.a'))
os.remove(test_file)
end)

View File

@ -168,6 +168,9 @@ describe('memory usage', function()
end)
it('releases memory when closing windows when folds exist', function()
if helpers.is_os('mac') then
pending('macOS memory compression causes flakiness')
end
local pid = eval('getpid()')
source([[
new

View File

@ -0,0 +1,25 @@
local helpers = require('test.functional.helpers')(after_each)
local exec_lua = helpers.exec_lua
local eq = helpers.eq
local eval = helpers.eval
local command = helpers.command
local clear = helpers.clear
describe('vim.highlight.on_yank', function()
before_each(function()
clear()
end)
it('does not show errors even if buffer is wiped before timeout', function()
command('new')
exec_lua[[
vim.highlight.on_yank({timeout = 10, on_macro = true, event = {operator = "y", regtype = "v"}})
vim.cmd('bwipeout!')
]]
helpers.sleep(10)
helpers.feed('<cr>') -- avoid hang if error message exists
eq('', eval('v:errmsg'))
end)
end)

View File

@ -6,6 +6,7 @@ local funcs = helpers.funcs
local meths = helpers.meths
local dedent = helpers.dedent
local command = helpers.command
local insert = helpers.insert
local clear = helpers.clear
local eq = helpers.eq
local ok = helpers.ok
@ -1332,12 +1333,12 @@ describe('lua stdlib', function()
it('should work for key-value pair options', function()
local listchars = exec_lua [[
vim.opt.listchars = "tab:>~,space:_"
vim.opt.listchars = "tab:> ,space:_"
return vim.opt.listchars:get()
]]
eq({
tab = ">~",
tab = "> ",
space = "_",
}, listchars)
end)
@ -1855,7 +1856,7 @@ describe('lua stdlib', function()
end)
it('vim.region', function()
helpers.insert(helpers.dedent( [[
insert(helpers.dedent( [[
text tααt tααt text
text tαxt txtα tex
text tαxt tαxt
@ -1863,65 +1864,67 @@ describe('lua stdlib', function()
eq({5,15}, exec_lua[[ return vim.region(0,{1,5},{1,14},'v',true)[1] ]])
end)
describe('vim.execute_on_keystroke', function()
it('should keep track of keystrokes', function()
helpers.insert([[hello world ]])
describe('vim.on_key', function()
it('tracks keystrokes', function()
insert([[hello world ]])
exec_lua [[
KeysPressed = {}
keys = {}
vim.register_keystroke_callback(function(buf)
vim.on_key(function(buf)
if buf:byte() == 27 then
buf = "<ESC>"
end
table.insert(KeysPressed, buf)
table.insert(keys, buf)
end)
]]
helpers.insert([[next 🤦 lines å ]])
insert([[next 🤦 lines å ]])
-- It has escape in the keys pressed
eq('inext 🤦 lines å <ESC>', exec_lua [[return table.concat(KeysPressed, '')]])
eq('inext 🤦 lines å <ESC>', exec_lua [[return table.concat(keys, '')]])
end)
it('should allow removing trackers.', function()
helpers.insert([[hello world]])
it('allows removing on_key listeners', function()
insert([[hello world]])
exec_lua [[
KeysPressed = {}
keys = {}
return vim.register_keystroke_callback(function(buf)
return vim.on_key(function(buf)
if buf:byte() == 27 then
buf = "<ESC>"
end
table.insert(KeysPressed, buf)
table.insert(keys, buf)
end, vim.api.nvim_create_namespace("logger"))
]]
helpers.insert([[next lines]])
insert([[next lines]])
exec_lua("vim.register_keystroke_callback(nil, vim.api.nvim_create_namespace('logger'))")
eq(1, exec_lua('return vim.on_key()'))
exec_lua("vim.on_key(nil, vim.api.nvim_create_namespace('logger'))")
eq(0, exec_lua('return vim.on_key()'))
helpers.insert([[more lines]])
insert([[more lines]])
-- It has escape in the keys pressed
eq('inext lines<ESC>', exec_lua [[return table.concat(KeysPressed, '')]])
eq('inext lines<ESC>', exec_lua [[return table.concat(keys, '')]])
end)
it('should not call functions that error again.', function()
helpers.insert([[hello world]])
it('skips any function that caused an error', function()
insert([[hello world]])
exec_lua [[
KeysPressed = {}
keys = {}
return vim.register_keystroke_callback(function(buf)
return vim.on_key(function(buf)
if buf:byte() == 27 then
buf = "<ESC>"
end
table.insert(KeysPressed, buf)
table.insert(keys, buf)
if buf == 'l' then
error("Dumb Error")
@ -1929,35 +1932,30 @@ describe('lua stdlib', function()
end)
]]
helpers.insert([[next lines]])
helpers.insert([[more lines]])
insert([[next lines]])
insert([[more lines]])
-- Only the first letter gets added. After that we remove the callback
eq('inext l', exec_lua [[ return table.concat(KeysPressed, '') ]])
eq('inext l', exec_lua [[ return table.concat(keys, '') ]])
end)
it('should process mapped keys, not unmapped keys', function()
it('processes mapped keys, not unmapped keys', function()
exec_lua [[
KeysPressed = {}
keys = {}
vim.cmd("inoremap hello world")
vim.register_keystroke_callback(function(buf)
vim.on_key(function(buf)
if buf:byte() == 27 then
buf = "<ESC>"
end
table.insert(KeysPressed, buf)
table.insert(keys, buf)
end)
]]
insert("hello")
helpers.insert("hello")
local next_status = exec_lua [[
return table.concat(KeysPressed, '')
]]
eq("iworld<ESC>", next_status)
eq('iworld<ESC>', exec_lua[[return table.concat(keys, '')]])
end)
end)

View File

@ -32,7 +32,7 @@ describe('vim.lsp.codelens', function()
command = { title = 'Lens1', command = 'Dummy' }
},
}
vim.lsp.codelens.on_codelens(nil, 'textDocument/codeLens', lenses, 1, bufnr)
vim.lsp.codelens.on_codelens(nil, lenses, {method='textDocument/codeLens', client_id=1, bufnr=bufnr})
]], bufnr)
local stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', bufnr)

View File

@ -205,8 +205,8 @@ describe('vim.lsp.diagnostic', function()
make_warning("Warning 1", 2, 1, 2, 5),
}
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { uri = fake_uri, diagnostics = server_1_diags }, 1)
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { uri = fake_uri, diagnostics = server_2_diags }, 2)
vim.lsp.diagnostic.on_publish_diagnostics(nil, { uri = fake_uri, diagnostics = server_1_diags }, {client_id=1})
vim.lsp.diagnostic.on_publish_diagnostics(nil, { uri = fake_uri, diagnostics = server_2_diags }, {client_id=2})
return {
vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1),
vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", 2),
@ -258,8 +258,8 @@ describe('vim.lsp.diagnostic', function()
make_warning("Warning 1", 2, 1, 2, 5),
}
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { uri = fake_uri, diagnostics = server_1_diags }, 1)
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { uri = fake_uri, diagnostics = server_2_diags }, 2)
vim.lsp.diagnostic.on_publish_diagnostics(nil, { uri = fake_uri, diagnostics = server_1_diags }, {client_id=1})
vim.lsp.diagnostic.on_publish_diagnostics(nil, { uri = fake_uri, diagnostics = server_2_diags }, {client_id=2})
return {
vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1),
vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", 2),
@ -435,14 +435,14 @@ describe('vim.lsp.diagnostic', function()
it('should return all diagnostics when no severity is supplied', function()
eq(2, exec_lua [[
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, {
vim.lsp.diagnostic.on_publish_diagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_error("Error 1", 1, 1, 1, 5),
make_warning("Warning on Server 1", 1, 1, 2, 5),
make_error("Error On Other Line", 2, 1, 1, 5),
}
}, 1)
}, {client_id=1})
return #vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1)
]])
@ -450,7 +450,7 @@ describe('vim.lsp.diagnostic', function()
it('should return only requested diagnostics when severity_limit is supplied', function()
eq(2, exec_lua [[
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, {
vim.lsp.diagnostic.on_publish_diagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_error("Error 1", 1, 1, 1, 5),
@ -458,7 +458,7 @@ describe('vim.lsp.diagnostic', function()
make_information("Ignored information", 1, 1, 2, 5),
make_error("Error On Other Line", 2, 1, 1, 5),
}
}, 1)
}, {client_id=1})
return #vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1, { severity_limit = "Warning" })
]])
@ -470,12 +470,12 @@ describe('vim.lsp.diagnostic', function()
exec_lua [[
vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, {
virtual_text = function() return true end,
})(nil, nil, {
})(nil, {
uri = fake_uri,
diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4),
}
}, 1
}, {client_id=1}
)
]]
@ -487,12 +487,12 @@ describe('vim.lsp.diagnostic', function()
exec_lua [[
vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, {
virtual_text = function() return false end,
})(nil, nil, {
})(nil, {
uri = fake_uri,
diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4),
}
}, 1
}, {client_id=1}
)
]]
@ -509,12 +509,12 @@ describe('vim.lsp.diagnostic', function()
exec_lua [[
vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, {
update_in_insert = false,
})(nil, nil, {
})(nil, {
uri = fake_uri,
diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4),
}
}, 1
}, {client_id=1}
)
]]
@ -551,12 +551,12 @@ describe('vim.lsp.diagnostic', function()
return SetVirtualTextOriginal(...)
end
PublishDiagnostics(nil, nil, {
PublishDiagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4),
}
}, 1
}, {client_id=1}
)
]]
@ -605,12 +605,12 @@ describe('vim.lsp.diagnostic', function()
return SetVirtualTextOriginal(...)
end
PublishDiagnostics(nil, nil, {
PublishDiagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4),
}
}, 1
}, {client_id=1}
)
]]
@ -647,12 +647,12 @@ describe('vim.lsp.diagnostic', function()
exec_lua [[
vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, {
update_in_insert = true,
})(nil, nil, {
})(nil, {
uri = fake_uri,
diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4),
}
}, 1
}, {client_id=1}
)
]]
@ -677,12 +677,12 @@ describe('vim.lsp.diagnostic', function()
},
})
PublishDiagnostics(nil, nil, {
PublishDiagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4),
}
}, 1
}, {client_id=1}
)
return vim.api.nvim_buf_get_extmarks(
@ -714,12 +714,12 @@ describe('vim.lsp.diagnostic', function()
end,
})
PublishDiagnostics(nil, nil, {
PublishDiagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4),
}
}, 1
}, {client_id=1}
)
return vim.api.nvim_buf_get_extmarks(
@ -747,12 +747,12 @@ describe('vim.lsp.diagnostic', function()
},
})
PublishDiagnostics(nil, nil, {
PublishDiagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_warning('Delayed Diagnostic', 4, 4, 4, 4),
}
}, 1
}, {client_id=1}
)
return count_of_extmarks_for_client(diagnostic_bufnr, 1)
@ -838,10 +838,10 @@ describe('vim.lsp.diagnostic', function()
}
vim.api.nvim_win_set_buf(0, diagnostic_bufnr)
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, {
vim.lsp.diagnostic.on_publish_diagnostics(nil, {
uri = fake_uri,
diagnostics = diagnostics
}, 1
}, {client_id=1}
)
vim.lsp.diagnostic.set_signs(diagnostics, diagnostic_bufnr, 1)
@ -863,13 +863,13 @@ describe('vim.lsp.diagnostic', function()
local loc_list = exec_lua [[
vim.api.nvim_win_set_buf(0, diagnostic_bufnr)
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, {
vim.lsp.diagnostic.on_publish_diagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_error('Farther Diagnostic', 4, 4, 4, 4),
make_error('Lower Diagnostic', 1, 1, 1, 1),
}
}, 1
}, {client_id=1}
)
vim.lsp.diagnostic.set_loclist()
@ -884,20 +884,20 @@ describe('vim.lsp.diagnostic', function()
local loc_list = exec_lua [[
vim.api.nvim_win_set_buf(0, diagnostic_bufnr)
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, {
vim.lsp.diagnostic.on_publish_diagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_error('Lower Diagnostic', 1, 1, 1, 1),
}
}, 1
}, {client_id=1}
)
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, {
vim.lsp.diagnostic.on_publish_diagnostics(nil, {
uri = fake_uri,
diagnostics = {
make_warning('Farther Diagnostic', 4, 4, 4, 4),
}
}, 2
}, {client_id=2}
)
vim.lsp.diagnostic.set_loclist()

View File

@ -0,0 +1,152 @@
local helpers = require('test.functional.helpers')(after_each)
local snippet = require('vim.lsp._snippet')
local eq = helpers.eq
local exec_lua = helpers.exec_lua
describe('vim.lsp._snippet', function()
before_each(helpers.clear)
after_each(helpers.clear)
local parse = function(...)
return exec_lua('return require("vim.lsp._snippet").parse(...)', ...)
end
it('should parse only text', function()
eq({
type = snippet.NodeType.SNIPPET,
children = {
{
type = snippet.NodeType.TEXT,
raw = 'TE\\$\\}XT',
esc = 'TE$}XT'
}
}
}, parse('TE\\$\\}XT'))
end)
it('should parse tabstop', function()
eq({
type = snippet.NodeType.SNIPPET,
children = {
{
type = snippet.NodeType.TABSTOP,
tabstop = 1,
},
{
type = snippet.NodeType.TABSTOP,
tabstop = 2,
}
}
}, parse('$1${2}'))
end)
it('should parse placeholders', function()
eq({
type = snippet.NodeType.SNIPPET,
children = {
{
type = snippet.NodeType.PLACEHOLDER,
tabstop = 1,
children = {
{
type = snippet.NodeType.PLACEHOLDER,
tabstop = 2,
children = {
{
type = snippet.NodeType.TEXT,
raw = 'TE\\$\\}XT',
esc = 'TE$}XT'
},
{
type = snippet.NodeType.TABSTOP,
tabstop = 3,
},
{
type = snippet.NodeType.TABSTOP,
tabstop = 1,
transform = {
type = snippet.NodeType.TRANSFORM,
pattern = 'regex',
option = 'i',
format = {
{
type = snippet.NodeType.FORMAT,
capture_index = 1,
modifier = 'upcase'
}
}
},
},
{
type = snippet.NodeType.TEXT,
raw = 'TE\\$\\}XT',
esc = 'TE$}XT'
},
}
}
}
},
}
}, parse('${1:${2:TE\\$\\}XT$3${1/regex/${1:/upcase}/i}TE\\$\\}XT}}'))
end)
it('should parse variables', function()
eq({
type = snippet.NodeType.SNIPPET,
children = {
{
type = snippet.NodeType.VARIABLE,
name = 'VAR',
},
{
type = snippet.NodeType.VARIABLE,
name = 'VAR',
},
{
type = snippet.NodeType.VARIABLE,
name = 'VAR',
children = {
{
type = snippet.NodeType.TABSTOP,
tabstop = 1,
}
}
},
{
type = snippet.NodeType.VARIABLE,
name = 'VAR',
transform = {
type = snippet.NodeType.TRANSFORM,
pattern = 'regex',
format = {
{
type = snippet.NodeType.FORMAT,
capture_index = 1,
modifier = 'upcase',
}
}
}
},
}
}, parse('$VAR${VAR}${VAR:$1}${VAR/regex/${1:/upcase}/}'))
end)
it('should parse choice', function()
eq({
type = snippet.NodeType.SNIPPET,
children = {
{
type = snippet.NodeType.CHOICE,
tabstop = 1,
items = {
',',
'|'
}
}
}
}, parse('${1|\\,,\\||}'))
end)
end)

View File

@ -130,9 +130,12 @@ local function test_rpc_server(config)
end
describe('LSP', function()
before_each(function()
clear_notrace()
end)
describe('server_name specified', function()
before_each(function()
clear_notrace()
-- Run an instance of nvim on the file which contains our "scripts".
-- Pass TEST_NAME to pick the script.
local test_name = "basic_init"
@ -216,7 +219,7 @@ describe('LSP', function()
it('should run correctly', function()
local expected_handlers = {
{NIL, "test", {}, 1};
{NIL, {}, {method="test", client_id=1}};
}
test_rpc_server {
test_name = "basic_init";
@ -241,7 +244,7 @@ describe('LSP', function()
it('should fail', function()
local expected_handlers = {
{NIL, "test", {}, 1};
{NIL, {}, {method="test", client_id=1}};
}
test_rpc_server {
test_name = "basic_init";
@ -269,8 +272,8 @@ describe('LSP', function()
return
end
local expected_handlers = {
{NIL, "shutdown", {}, 1, NIL};
{NIL, "test", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="test", client_id=1}};
}
test_rpc_server {
test_name = "basic_init";
@ -292,12 +295,12 @@ describe('LSP', function()
it('client should return settings via workspace/configuration handler', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "workspace/configuration", { items = {
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, { items = {
{ section = "testSetting1" };
{ section = "testSetting2" };
}}, 1};
{NIL, "start", {}, 1};
}}, { method="workspace/configuration", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -309,9 +312,9 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'start' then
on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'start' then
exec_lua([=[
local client = vim.lsp.get_client_by_id(TEST_RPC_CLIENT_ID)
client.config.settings = {
@ -319,35 +322,34 @@ describe('LSP', function()
testSetting2 = false;
}]=])
end
if method == 'workspace/configuration' then
local result = exec_lua([=[
if ctx.method == 'workspace/configuration' then
local server_result = exec_lua([=[
local method, params = ...
return require'vim.lsp.handlers'['workspace/configuration'](err, method, params, TEST_RPC_CLIENT_ID)]=], method, params)
client.notify('workspace/configuration', result)
return require'vim.lsp.handlers'['workspace/configuration'](err, params, {method=method, client_id=TEST_RPC_CLIENT_ID})]=], ctx.method, result)
client.notify('workspace/configuration', server_result)
end
if method == 'shutdown' then
if ctx.method == 'shutdown' then
client.stop()
end
end;
}
end)
it('workspace/configuration returns NIL per section if client was started without config.settings', function()
clear_notrace()
fake_lsp_server_setup('workspace/configuration no settings')
eq({ NIL, NIL, }, exec_lua [[
local params = {
local result = {
items = {
{section = 'foo'},
{section = 'bar'},
}
}
return vim.lsp.handlers['workspace/configuration'](nil, nil, params, TEST_RPC_CLIENT_ID)
return vim.lsp.handlers['workspace/configuration'](nil, result, {client_id=TEST_RPC_CLIENT_ID})
]])
end)
it('should verify capabilities sent', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
}
test_rpc_server {
test_name = "basic_check_capabilities";
@ -371,7 +373,7 @@ describe('LSP', function()
it('client.supports_methods() should validate capabilities', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
}
test_rpc_server {
test_name = "capabilities_for_client_supports_method";
@ -405,7 +407,7 @@ describe('LSP', function()
it('should call unsupported_method when trying to call an unsupported method', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
}
test_rpc_server {
test_name = "capabilities_for_client_supports_method";
@ -413,7 +415,8 @@ describe('LSP', function()
exec_lua([=[
BUFFER = vim.api.nvim_get_current_buf()
lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
vim.lsp.handlers['textDocument/typeDefinition'] = function(err, method)
vim.lsp.handlers['textDocument/typeDefinition'] = function(err, result, ctx)
local method = ctx.method
vim.lsp._last_lsp_handler = { err = err; method = method }
end
vim.lsp._unsupported_method = function(method)
@ -446,7 +449,7 @@ describe('LSP', function()
it('shouldn\'t call unsupported_method when no client and trying to call an unsupported method', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
}
test_rpc_server {
test_name = "capabilities_for_client_supports_method";
@ -479,8 +482,8 @@ describe('LSP', function()
it('should not send didOpen if the buffer closes before init', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
}
local client
test_rpc_server {
@ -511,9 +514,9 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -522,9 +525,9 @@ describe('LSP', function()
it('should check the body sent attaching before init', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "start", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -554,12 +557,12 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
if method == 'start' then
on_handler = function(err, result, ctx)
if ctx.method == 'start' then
client.notify('finish')
end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -568,9 +571,9 @@ describe('LSP', function()
it('should check the body sent attaching after init', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "start", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -597,12 +600,12 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
if method == 'start' then
on_handler = function(err, result, ctx)
if ctx.method == 'start' then
client.notify('finish')
end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -611,9 +614,9 @@ describe('LSP', function()
it('should check the body and didChange full', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "start", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -640,8 +643,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
if method == 'start' then
on_handler = function(err, result, ctx)
if ctx.method == 'start' then
exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"boop";
@ -649,8 +652,8 @@ describe('LSP', function()
]]
client.notify('finish')
end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -659,9 +662,9 @@ describe('LSP', function()
it('should check the body and didChange full with noeol', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "start", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -689,8 +692,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
if method == 'start' then
on_handler = function(err, result, ctx)
if ctx.method == 'start' then
exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"boop";
@ -698,8 +701,8 @@ describe('LSP', function()
]]
client.notify('finish')
end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -708,9 +711,9 @@ describe('LSP', function()
it('should check the body and didChange incremental', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "start", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -738,8 +741,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
if method == 'start' then
on_handler = function(err, result, ctx)
if ctx.method == 'start' then
exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"123boop";
@ -747,8 +750,8 @@ describe('LSP', function()
]]
client.notify('finish')
end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -758,9 +761,9 @@ describe('LSP', function()
-- TODO(askhan) we don't support full for now, so we can disable these tests.
pending('should check the body and didChange incremental normal mode editing', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "start", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -787,13 +790,13 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
if method == 'start' then
on_handler = function(err, result, ctx)
if ctx.method == 'start' then
helpers.command("normal! 1Go")
client.notify('finish')
end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -802,9 +805,9 @@ describe('LSP', function()
it('should check the body and didChange full with 2 changes', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "start", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -831,8 +834,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
if method == 'start' then
on_handler = function(err, result, ctx)
if ctx.method == 'start' then
exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"321";
@ -843,8 +846,8 @@ describe('LSP', function()
]]
client.notify('finish')
end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -853,9 +856,9 @@ describe('LSP', function()
it('should check the body and didChange full lifecycle', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "start", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -882,8 +885,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
if method == 'start' then
on_handler = function(err, result,ctx)
if ctx.method == 'start' then
exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"321";
@ -895,8 +898,8 @@ describe('LSP', function()
]]
client.notify('finish')
end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -907,9 +910,9 @@ describe('LSP', function()
describe("parsing tests", function()
it('should handle invalid content-length correctly', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "start", {}, 1};
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -924,22 +927,22 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
end;
}
end)
it('should not trim vim.NIL from the end of a list', function()
local expected_handlers = {
{NIL, "shutdown", {}, 1};
{NIL, "finish", {}, 1};
{NIL, "workspace/executeCommand", {
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL,{
arguments = { "EXTRACT_METHOD", {metadata = {}}, 3, 0, 6123, NIL },
command = "refactor.perform",
title = "EXTRACT_METHOD"
}, 1};
{NIL, "start", {}, 1};
}, {method="workspace/executeCommand", client_id=1}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
@ -963,9 +966,9 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
end;
on_handler = function(err, method, params, client_id)
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler")
if method == 'finish' then
on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
@ -1088,6 +1091,30 @@ describe('LSP', function()
'å å ɧ 汉语 ↥ 🤦 🦄';
}, buf_lines(1))
end)
it('applies complex edits (reversed range)', function()
local edits = {
make_edit(0, 0, 0, 0, {"", "12"});
make_edit(0, 0, 0, 0, {"3", "foo"});
make_edit(0, 1, 0, 1, {"bar", "123"});
make_edit(0, #"First line of text", 0, #"First ", {"guy"});
make_edit(1, #'Second', 1, 0, {"baz"});
make_edit(2, #"Third", 2, #'Th', {"e next"});
make_edit(3, #"Fourth", 3, #'', {"another line of text", "before this"});
make_edit(3, #"Fourth line of text", 3, #'Fourth', {"!"});
}
exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
eq({
'';
'123';
'fooFbar';
'123irst guy';
'baz line of text';
'The next line of text';
'another line of text';
'before this!';
'å å ɧ 汉语 ↥ 🤦 🦄';
}, buf_lines(1))
end)
it('applies non-ASCII characters edits', function()
local edits = {
make_edit(4, 3, 4, 4, {"ä"});
@ -1116,6 +1143,86 @@ describe('LSP', function()
}, buf_lines(1))
end)
describe('cursor position', function()
it('don\'t fix the cursor if the range contains the cursor', function()
funcs.nvim_win_set_cursor(0, { 2, 6 })
local edits = {
make_edit(1, 0, 1, 19, 'Second line of text')
}
exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
eq({
'First line of text';
'Second line of text';
'Third line of text';
'Fourth line of text';
'å å ɧ 汉语 ↥ 🤦 🦄';
}, buf_lines(1))
eq({ 2, 6 }, funcs.nvim_win_get_cursor(0))
end)
it('fix the cursor to the valid column if the content was removed', function()
funcs.nvim_win_set_cursor(0, { 2, 6 })
local edits = {
make_edit(1, 0, 1, 19, '')
}
exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
eq({
'First line of text';
'';
'Third line of text';
'Fourth line of text';
'å å ɧ 汉语 ↥ 🤦 🦄';
}, buf_lines(1))
eq({ 2, 0 }, funcs.nvim_win_get_cursor(0))
end)
it('fix the cursor row', function()
funcs.nvim_win_set_cursor(0, { 3, 0 })
local edits = {
make_edit(1, 0, 2, 0, '')
}
exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
eq({
'First line of text';
'Third line of text';
'Fourth line of text';
'å å ɧ 汉语 ↥ 🤦 🦄';
}, buf_lines(1))
eq({ 2, 0 }, funcs.nvim_win_get_cursor(0))
end)
it('fix the cursor col', function()
funcs.nvim_win_set_cursor(0, { 2, 11 })
local edits = {
make_edit(1, 7, 1, 11, '')
}
exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
eq({
'First line of text';
'Second of text';
'Third line of text';
'Fourth line of text';
'å å ɧ 汉语 ↥ 🤦 🦄';
}, buf_lines(1))
eq({ 2, 7 }, funcs.nvim_win_get_cursor(0))
end)
it('fix the cursor row and col', function()
funcs.nvim_win_set_cursor(0, { 2, 12 })
local edits = {
make_edit(0, 11, 1, 12, '')
}
exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
eq({
'First line of text';
'Third line of text';
'Fourth line of text';
'å å ɧ 汉语 ↥ 🤦 🦄';
}, buf_lines(1))
eq({ 1, 11 }, funcs.nvim_win_get_cursor(0))
end)
end)
describe('with LSP end line after what Vim considers to be the end line', function()
it('applies edits when the last linebreak is considered a new line', function()
local edits = {
@ -1223,7 +1330,7 @@ describe('LSP', function()
label = nil;
edit = {};
}
return vim.lsp.handlers['workspace/applyEdit'](nil, nil, apply_edit)
return vim.lsp.handlers['workspace/applyEdit'](nil, apply_edit)
]])
end)
end)
@ -1441,8 +1548,10 @@ describe('LSP', function()
{ label='foocar', sortText="h", insertText='foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', insertTextFormat=2, textEdit={} },
-- nested snippet tokens
{ label='foocar', sortText="i", insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', insertTextFormat=2, textEdit={} },
-- braced tabstop
{ label='foocar', sortText="j", insertText='foodar()${0}', insertTextFormat=2, textEdit={} },
-- plain text
{ label='foocar', sortText="j", insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} },
{ label='foocar', sortText="k", insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} },
}
local completion_list_items = {items=completion_list}
local expected = {
@ -1454,8 +1563,9 @@ describe('LSP', function()
{ abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="f", textEdit={newText='foobar'} } } } } },
{ abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar(place holder, more ...holder{})', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="g", insertText='foodar', insertTextFormat=2, textEdit={newText='foobar(${1:place holder}, ${2:more ...holder{\\}})'} } } } } },
{ abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(var1 typ1, var2 *typ2) {}', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="h", insertText='foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', insertTextFormat=2, textEdit={} } } } } },
{ abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(var1 typ2,typ3 tail) {}', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="i", insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', insertTextFormat=2, textEdit={} } } } } },
{ abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(${1:var1})', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="j", insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} } } } } },
{ abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(var1 typ2 tail) {}', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="i", insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', insertTextFormat=2, textEdit={} } } } } },
{ abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar()', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="j", insertText='foodar()${0}', insertTextFormat=2, textEdit={} } } } } },
{ abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(${1:var1})', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="k", insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} } } } } },
}
eq(expected, exec_lua([[return vim.lsp.util.text_document_completion_list_to_complete_items(...)]], completion_list, prefix))
@ -1973,7 +2083,7 @@ describe('LSP', function()
describe('vim.lsp.buf.outgoing_calls', function()
it('does nothing for an empty response', function()
local qflist_count = exec_lua([=[
require'vim.lsp.handlers'['callHierarchy/outgoingCalls']()
require'vim.lsp.handlers'['callHierarchy/outgoingCalls'](nil, nil, {}, nil)
return #vim.fn.getqflist()
]=])
eq(0, qflist_count)
@ -2020,7 +2130,7 @@ describe('LSP', function()
}
} }
local handler = require'vim.lsp.handlers'['callHierarchy/outgoingCalls']
handler(nil, nil, rust_analyzer_response)
handler(nil, rust_analyzer_response, {})
return vim.fn.getqflist()
]=])
@ -2044,7 +2154,7 @@ describe('LSP', function()
describe('vim.lsp.buf.incoming_calls', function()
it('does nothing for an empty response', function()
local qflist_count = exec_lua([=[
require'vim.lsp.handlers'['callHierarchy/incomingCalls']()
require'vim.lsp.handlers'['callHierarchy/incomingCalls'](nil, nil, {})
return #vim.fn.getqflist()
]=])
eq(0, qflist_count)
@ -2092,7 +2202,7 @@ describe('LSP', function()
} }
local handler = require'vim.lsp.handlers'['callHierarchy/incomingCalls']
handler(nil, nil, rust_analyzer_response)
handler(nil, rust_analyzer_response, {})
return vim.fn.getqflist()
]=])

View File

@ -10,9 +10,11 @@ local pending_c_parser = helpers.pending_c_parser
before_each(clear)
describe('treesitter parser API', function()
clear()
if pending_c_parser(pending) then return end
it('parses buffer', function()
if helpers.pending_win32(pending) or pending_c_parser(pending) then return end
if helpers.pending_win32(pending) then return end
insert([[
int main() {
@ -103,8 +105,6 @@ void ui_refresh(void)
}]]
it('allows to iterate over nodes children', function()
if pending_c_parser(pending) then return end
insert(test_text);
local res = exec_lua([[
@ -127,8 +127,6 @@ void ui_refresh(void)
end)
it('allows to get a child by field', function()
if pending_c_parser(pending) then return end
insert(test_text);
local res = exec_lua([[
@ -162,8 +160,6 @@ void ui_refresh(void)
]]
it("supports runtime queries", function()
if pending_c_parser(pending) then return end
local ret = exec_lua [[
return require"vim.treesitter.query".get_query("c", "highlights").captures[1]
]]
@ -172,8 +168,6 @@ void ui_refresh(void)
end)
it('support query and iter by capture', function()
if pending_c_parser(pending) then return end
insert(test_text)
local res = exec_lua([[
@ -203,8 +197,6 @@ void ui_refresh(void)
end)
it('support query and iter by match', function()
if pending_c_parser(pending) then return end
insert(test_text)
local res = exec_lua([[
@ -236,8 +228,6 @@ void ui_refresh(void)
end)
it('can match special regex characters like \\ * + ( with `vim-match?`', function()
if pending_c_parser(pending) then return end
insert('char* astring = "\\n"; (1 + 1) * 2 != 2;')
local res = exec_lua([[
@ -271,8 +261,6 @@ void ui_refresh(void)
end)
it('supports builtin query predicate any-of?', function()
if pending_c_parser(pending) then return end
insert([[
#include <stdio.h>
@ -330,8 +318,6 @@ void ui_refresh(void)
end)
it('allow loading query with escaped quotes and capture them with `lua-match?` and `vim-match?`', function()
if pending_c_parser(pending) then return end
insert('char* astring = "Hello World!";')
local res = exec_lua([[
@ -407,8 +393,6 @@ void ui_refresh(void)
it('allows to set simple ranges', function()
if pending_c_parser(pending) then return end
insert(test_text)
local res = exec_lua [[
@ -450,8 +434,6 @@ void ui_refresh(void)
eq(range_tbl, { { { 0, 0, 0, 17, 1, 508 } } })
end)
it("allows to set complex ranges", function()
if pending_c_parser() then return end
insert(test_text)
local res = exec_lua [[

View File

@ -697,4 +697,50 @@ end]]
|
]]}
end)
it('does not crash when deleting a cleared buffer #15212', function()
exec_lua [[
ns = vim.api.nvim_create_namespace("myplugin")
vim.api.nvim_buf_set_extmark(0, ns, 0, 0, {virt_text = {{"a"}}, end_col = 0})
]]
screen:expect{grid=[[
^ a |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
exec_lua [[
vim.api.nvim_buf_clear_namespace(0, ns, 0, -1)
vim.cmd("bdelete")
]]
screen:expect{grid=[[
^ |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
helpers.assert_alive()
end)
end)

View File

@ -152,6 +152,132 @@ describe('float window', function()
eq(10, width)
end)
it('opened with correct position', function()
local pos = exec_lua([[
local bufnr = vim.api.nvim_create_buf(false, true)
local opts = {
width = 10,
height = 10,
col = 7,
row = 9,
relative = 'editor',
style = 'minimal'
}
local win_id = vim.api.nvim_open_win(bufnr, false, opts)
return vim.api.nvim_win_get_position(win_id)
]])
eq(9, pos[1])
eq(7, pos[2])
end)
it('opened with correct position relative to the cursor', function()
local pos = exec_lua([[
local bufnr = vim.api.nvim_create_buf(false, true)
local opts = {
width = 10,
height = 10,
col = 7,
row = 9,
relative = 'cursor',
style = 'minimal'
}
local win_id = vim.api.nvim_open_win(bufnr, false, opts)
return vim.api.nvim_win_get_position(win_id)
]])
eq(9, pos[1])
eq(7, pos[2])
end)
it('opened with correct position relative to another window', function()
local pos = exec_lua([[
local bufnr = vim.api.nvim_create_buf(false, true)
local par_opts = {
width = 50,
height = 50,
col = 7,
row = 9,
relative = 'editor',
style = 'minimal'
}
local par_win_id = vim.api.nvim_open_win(bufnr, false, par_opts)
local opts = {
width = 10,
height = 10,
col = 7,
row = 9,
relative = 'win',
style = 'minimal',
win = par_win_id
}
local win_id = vim.api.nvim_open_win(bufnr, false, opts)
return vim.api.nvim_win_get_position(win_id)
]])
eq(18, pos[1])
eq(14, pos[2])
end)
it('opened with correct position relative to another relative window', function()
local pos = exec_lua([[
local bufnr = vim.api.nvim_create_buf(false, true)
local root_opts = {
width = 50,
height = 50,
col = 7,
row = 9,
relative = 'editor',
style = 'minimal'
}
local root_win_id = vim.api.nvim_open_win(bufnr, false, root_opts)
local par_opts = {
width = 20,
height = 20,
col = 2,
row = 3,
relative = 'win',
win = root_win_id,
style = 'minimal'
}
local par_win_id = vim.api.nvim_open_win(bufnr, false, par_opts)
local opts = {
width = 10,
height = 10,
col = 3,
row = 2,
relative = 'win',
win = par_win_id,
style = 'minimal'
}
local win_id = vim.api.nvim_open_win(bufnr, false, opts)
return vim.api.nvim_win_get_position(win_id)
]])
eq(14, pos[1])
eq(12, pos[2])
end)
local function with_ext_multigrid(multigrid)
local screen
before_each(function()
@ -620,6 +746,134 @@ describe('float window', function()
end
end)
it("would not break 'minimal' style with signcolumn=auto:[min]-[max]", function()
command('set number')
command('set signcolumn=auto:1-3')
command('set colorcolumn=1')
command('set cursorline')
command('set foldcolumn=1')
command('hi NormalFloat guibg=#333333')
feed('ix<cr>y<cr><esc>gg')
local win = meths.open_win(0, false, {relative='editor', width=20, height=4, row=4, col=10, style='minimal'})
if multigrid then
screen:expect{grid=[[
## grid 1
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[3:----------------------------------------]|
## grid 2
{19: }{20: 1 }{22:^x}{21: }|
{19: }{14: 2 }{22:y} |
{19: }{14: 3 }{22: } |
{0:~ }|
{0:~ }|
{0:~ }|
## grid 3
|
## grid 4
{15:x }|
{15:y }|
{15: }|
{15: }|
]], float_pos={[4] = {{id = 1001}, "NW", 1, 4, 10, true}}}
else
screen:expect{grid=[[
{19: }{20: 1 }{22:^x}{21: }|
{19: }{14: 2 }{22:y} |
{19: }{14: 3 }{22: } {15:x } |
{0:~ }{15:y }{0: }|
{0:~ }{15: }{0: }|
{0:~ }{15: }{0: }|
|
]]}
end
command('sign define piet1 text=𐌢̀́̂̃̅̄𐌢̀́̂̃̅̄ texthl=Search')
command('sign place 1 line=1 name=piet1 buffer=1')
-- signcolumn=auto:1-3 still works if there actually are signs
if multigrid then
screen:expect{grid=[[
## grid 1
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[3:----------------------------------------]|
## grid 2
{19: }{17:𐌢̀́̂̃̅̄𐌢̀́̂̃̅̄}{20: 1 }{22:^x}{21: }|
{19: }{14: 2 }{22:y} |
{19: }{14: 3 }{22: } |
{0:~ }|
{0:~ }|
{0:~ }|
## grid 3
|
## grid 4
{17:𐌢̀́̂̃̅̄𐌢̀́̂̃̅̄}{15:x }|
{19: }{15:y }|
{19: }{15: }|
{15: }|
]], float_pos={[4] = {{id = 1001}, "NW", 1, 4, 10, true}}}
else
screen:expect([[
{19: }{17:𐌢̀́̂̃̅̄𐌢̀́̂̃̅̄}{20: 1 }{22:^x}{21: }|
{19: }{14: 2 }{22:y} |
{19: }{14: 3 }{22: } {17:𐌢̀́̂̃̅̄𐌢̀́̂̃̅̄}{15:x } |
{0:~ }{19: }{15:y }{0: }|
{0:~ }{19: }{15: }{0: }|
{0:~ }{15: }{0: }|
|
]])
end
command('sign unplace 1 buffer=1')
local buf = meths.create_buf(false, true)
meths.win_set_buf(win, buf)
if multigrid then
screen:expect{grid=[[
## grid 1
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[2:----------------------------------------]|
[3:----------------------------------------]|
## grid 2
{19: }{20: 1 }{22:^x}{21: }|
{19: }{14: 2 }{22:y} |
{19: }{14: 3 }{22: } |
{0:~ }|
{0:~ }|
{0:~ }|
## grid 3
|
## grid 4
{15: }|
{15: }|
{15: }|
{15: }|
]], float_pos={[4] = {{id = 1001}, "NW", 1, 4, 10, true}}}
else
screen:expect([[
{19: }{20: 1 }{22:^x}{21: }|
{19: }{14: 2 }{22:y} |
{19: }{14: 3 }{22: } {15: } |
{0:~ }{15: }{0: }|
{0:~ }{15: }{0: }|
{0:~ }{15: }{0: }|
|
]])
end
end)
it('can have border', function()
local buf = meths.create_buf(false, false)
meths.buf_set_lines(buf, 0, -1, true, {' halloj! ',

View File

@ -1487,6 +1487,29 @@ describe("inccommand=nosplit", function()
]])
eq(eval('v:null'), eval('v:exiting'))
end)
it("does not break bar-separated command #8796", function()
source([[
function! F()
if v:false | return | endif
endfun
]])
command('call timer_start(10, {-> F()}, {"repeat":-1})')
feed(':%s/')
sleep(20) -- Allow some timer activity.
screen:expect([[
Inc substitution on |
two lines |
Inc substitution on |
two lines |
|
{15:~ }|
{15:~ }|
{15:~ }|
{15:~ }|
:%s/^ |
]])
end)
end)
describe(":substitute, 'inccommand' with a failing expression", function()