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: | run: |
sudo apt-get update 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 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 - name: Build release
id: build id: build
run: | 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=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)" printf '::set-output name=release::%s\n' "$(./build/bin/nvim --version | head -n 1)"
make DESTDIR="$GITHUB_WORKSPACE/build/release/nvim-linux64" install make DESTDIR="$GITHUB_WORKSPACE/build/release/nvim-linux64" install
@ -80,9 +84,13 @@ jobs:
brew update >/dev/null brew update >/dev/null
brew upgrade brew upgrade
brew install automake ninja 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 - name: Build release
run: | 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 make DESTDIR="$GITHUB_WORKSPACE/build/release/nvim-osx64" install
- name: Create package - name: Create package
run: | run: |

View File

@ -1,4 +1,4 @@
.DONE: .DONE:
@echo "Please use GNU Make (gmake) to build neovim" @echo "Use GNU Make (gmake) to build neovim"
.DEFAULT: .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`. # version string, else they are combined with the result of `git describe`.
set(NVIM_VERSION_MAJOR 0) set(NVIM_VERSION_MAJOR 0)
set(NVIM_VERSION_MINOR 5) set(NVIM_VERSION_MINOR 5)
set(NVIM_VERSION_PATCH 0) set(NVIM_VERSION_PATCH 1)
set(NVIM_VERSION_PRERELEASE "") # for package maintainers set(NVIM_VERSION_PRERELEASE "") # for package maintainers
# API level # API level

View File

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

View File

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

View File

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

View File

@ -158,7 +158,9 @@ function! s:clipboard.get(reg) abort
end end
let clipboard_data = s:try_cmd(s:paste[a:reg]) 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 " When system clipboard return is same as our cache return the cache
" as it contains regtype information " as it contains regtype information
return s:selections[a:reg].data return s:selections[a:reg].data

View File

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

View File

@ -127,14 +127,20 @@ Sometimes a GUI or other application may want to force a provider to
DOCUMENTATION *dev-doc* DOCUMENTATION *dev-doc*
- Do not prefix help tags with "nvim-". Use |vim_diff.txt| to document - "Just say it". Avoid mushy, colloquial phrasing in all documentation
differences from Vim; no other distinction is necessary. (docstrings, user manual, website materials, newsletters, …). Don't mince
- If a Vim feature is removed, delete its help section and move its tag to words. Personality and flavor, used sparingly, are welcome--but in general,
|vim_diff.txt|. optimize for the reader's time and energy: be "precise yet concise".
- Move deprecated features to |deprecated.txt|. - 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. - Use consistent language.
- "terminal" in a help tag always means "the embedded terminal emulator", not - "terminal" in a help tag always means "the embedded terminal emulator",
"the user host terminal". not "the user host terminal".
- Use "tui-" to prefix help tags related to the host terminal, and "TUI" - Use "tui-" to prefix help tags related to the host terminal, and "TUI"
in prose if possible. in prose if possible.
- Docstrings: do not start parameter descriptions with "The" or "A" unless it - 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 - 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 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 want to learn a big, fancy inheritance hierarchy. Thus avoid specialized
objects: tables are usually better. objects; tables or values are usually better.
API *dev-api* 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} nvim_{thing}_{action}_{arbitrary-qualifiers}
If the function acts on an object then {thing} is the name of that object 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". 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: 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| before invoking `on_stderr`. |channel-buffered|
stdout_buffered: (boolean) Collect data until EOF (stream stdout_buffered: (boolean) Collect data until EOF (stream
closed) before invoking `on_stdout`. |channel-buffered| 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. width: (number) Width of the `pty` terminal.
{opts} is passed as |self| dictionary to the callback; the {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: > For |lsp-request|, each |lsp-handler| has this signature: >
function(err, method, result, client_id, bufnr, config) function(err, result, ctx, config)
< <
Parameters: ~ Parameters: ~
{err} (table|nil) {err} (table|nil)
When the language server is unable to complete a When the language server is unable to complete a
request, a table with information about the error request, a table with information about the error
is sent. Otherwise, it is `nil`. See |lsp-response|. is sent. Otherwise, it is `nil`. See |lsp-response|.
{method} (string)
The |lsp-method| name.
{result} (Result | Params | nil) {result} (Result | Params | nil)
When the language server is able to succesfully When the language server is able to succesfully
complete a request, this contains the `result` key complete a request, this contains the `result` key
of the response. See |lsp-response|. of the response. See |lsp-response|.
{client_id} (number) {ctx} (table)
The ID of the |vim.lsp.client|. Context describes additional calling state
{bufnr} (Buffer) associated with the handler. It consists of the
Buffer handle, or 0 for current. 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) {config} (table)
Configuration for the handler. 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: > For |lsp-notification|, each |lsp-handler| has this signature: >
function(err, method, params, client_id, bufnr, config) function(err, result, ctx, config)
< <
Parameters: ~ Parameters: ~
{err} (nil) {err} (nil)
This is always `nil`. This is always `nil`.
See |lsp-notification| See |lsp-notification|
{method} (string) {result} (Result)
The |lsp-method| name.
{params} (Params)
This contains the `params` key of the notification. This contains the `params` key of the notification.
See |lsp-notification| See |lsp-notification|
{client_id} (number) {ctx} (table)
The ID of the |vim.lsp.client| Context describes additional calling state
{bufnr} (nil) associated with the handler. It consists of the
`nil`, as the server doesn't have an associated buffer. following key, value pairs:
{method} (string)
The |lsp-method| name.
{client_id} (number)
The ID of the |vim.lsp.client|.
{config} (table) {config} (table)
Configuration for the handler. Configuration for the handler.
@ -1355,7 +1363,7 @@ goto_prev({opts}) *vim.lsp.diagnostic.goto_prev()*
{opts} table See |vim.lsp.diagnostic.goto_next()| {opts} table See |vim.lsp.diagnostic.goto_next()|
*vim.lsp.diagnostic.on_publish_diagnostics()* *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" |lsp-handler| for the method "textDocument/publishDiagnostics"
Note: Note:
@ -1581,7 +1589,7 @@ get({bufnr}) *vim.lsp.codelens.get()*
table ( `CodeLens[]` ) table ( `CodeLens[]` )
*vim.lsp.codelens.on_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` |lsp-handler| for the method `textDocument/codeLens`
refresh() *vim.lsp.codelens.refresh()* refresh() *vim.lsp.codelens.refresh()*
@ -1614,17 +1622,25 @@ progress_handler({_}, {_}, {params}, {client_id})
See also: ~ See also: ~
https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
*vim.lsp.handlers.signature_help()* hover({_}, {result}, {ctx}, {config}) *vim.lsp.handlers.hover()*
signature_help({_}, {method}, {result}, {_}, {bufnr}, {config}) |lsp-handler| for the method "textDocument/hover" >
Parameters: ~
{config} table Configuration table.
• border: (default=nil)
• Add borders to the floating window
• See |vim.api.nvim_open_win()|
vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
vim.lsp.handlers.hover, {
-- Use a sharp border with `FloatBorder` highlights
border = "single"
}
)
<
See also: ~ 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"> 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["textDocument/signatureHelp"] = vim.lsp.with(
vim.lsp.handlers.signature_help, { vim.lsp.handlers.signature_help, {
-- Use a sharp border with `FloatBorder` highlights -- 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()* vim.highlight.on_yank({opts}) *vim.highlight.on_yank()*
Highlights the yanked text. The fields of the optional dict {opts} Highlights the yanked text. The fields of the optional dict {opts}
control the highlight: 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`) - {timeout} time in ms before highlight is cleared (default `150`)
- {on_macro} highlight when executing macro (default `false`) - {on_macro} highlight when executing macro (default `false`)
- {on_visual} highlight when yanking visual selection (default `true`) - {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({bufnr}, {ns}, {higroup}, {start}, {finish}, {rtype}, {inclusive})
*vim.highlight.range()* *vim.highlight.range()*
@ -1167,37 +1167,6 @@ region({bufnr}, {pos1}, {pos2}, {regtype}, {inclusive}) *vim.region()*
Return: ~ Return: ~
region lua table of the form {linenr = {startcol,endcol}} 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()* schedule_wrap({cb}) *vim.schedule_wrap()*
Defers callback `cb` until the Nvim API is safe to call. 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'}} vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
=> NOP (success) => NOP (success)
<
> vim.validate{arg1={1, 'table'}}
vim.validate{arg1={1, 'table'}} => error('arg1: expected table, got number')
=> 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={3, function(a) return (a % 2) == 0 end, 'even number'}}
=> error('arg1: expected even number, got 3')
< <
Parameters: ~ Parameters: ~

View File

@ -329,12 +329,12 @@ argument.
-w{number} Set the 'window' option to {number}. -w{number} Set the 'window' option to {number}.
*-w* *-w*
-w {scriptout} All the characters that you type are recorded in the file -w {scriptout} All keys that you type are recorded in the file "scriptout",
"scriptout", until you exit Vim. This is useful if you want until you exit Vim. Useful to create a script file to be used
to create a script file to be used with "vim -s" or with "vim -s" or ":source!". Appends to the "scriptout" file
":source!". When the "scriptout" file already exists, new if it already exists. {scriptout} cannot start with a digit.
characters are appended. See also |complex-repeat|. See also |vim.on_key()|.
{scriptout} cannot start with a digit. See also |complex-repeat|.
*-W* *-W*
-W {scriptout} Like -w, but do not append, overwrite an existing file. -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 for id, node in pairs(match) do
local name = query.captures[id] local name = query.captures[id]
-- `node` was captured by the `name` capture in the match -- `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 ... ... use the info here ...
end end
end end

View File

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

View File

@ -6,14 +6,6 @@ if exists('b:did_ftplugin') || &filetype !=# 'man'
endif endif
let b:did_ftplugin = 1 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 noexpandtab tabstop=8 softtabstop=8 shiftwidth=8
setlocal wrap breakindent linebreak 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> k gk
nnoremap <silent> <buffer> gO :call man#show_toc()<CR> nnoremap <silent> <buffer> gO :call man#show_toc()<CR>
nnoremap <silent> <buffer> <2-LeftMouse> :Man<CR> nnoremap <silent> <buffer> <2-LeftMouse> :Man<CR>
if s:pager nnoremap <silent> <buffer> <nowait> q :lclose<CR><C-W>c
nnoremap <silent> <buffer> <nowait> q :lclose<CR>:q<CR>
else
nnoremap <silent> <buffer> <nowait> q :lclose<CR><C-W>c
endif
endif endif
if get(g:, 'ft_man_folding_enable', 0) 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 for _, key_value_str in ipairs(comma_split) do
local key, value = unpack(vim.split(key_value_str, ":")) local key, value = unpack(vim.split(key_value_str, ":"))
key = vim.trim(key) key = vim.trim(key)
value = vim.trim(value)
result[key] = value result[key] = value
end end

View File

@ -85,7 +85,11 @@ function highlight.on_yank(opts)
highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive) highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive)
vim.defer_fn( 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 timeout
) )
end end

View File

@ -218,7 +218,7 @@ local function validate_client_config(config)
config = { config, 't' }; config = { config, 't' };
} }
validate { validate {
root_dir = { config.root_dir, is_dir, "directory" }; root_dir = { config.root_dir, optional_validator(is_dir), "directory" };
handlers = { config.handlers, "t", true }; handlers = { config.handlers, "t", true };
capabilities = { config.capabilities, "t", true }; capabilities = { config.capabilities, "t", true };
cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" };
@ -380,7 +380,7 @@ do
end end
state.pending_change = function() state.pending_change = function()
state.pending_change = nil state.pending_change = nil
if client.is_stopped() then if client.is_stopped() or not vim.api.nvim_buf_is_valid(bufnr) then
return return
end end
local contentChanges local contentChanges
@ -455,12 +455,15 @@ local function text_document_did_open_handler(bufnr, client)
vim.schedule(function() vim.schedule(function()
vim.lsp.handlers["textDocument/publishDiagnostics"]( vim.lsp.handlers["textDocument/publishDiagnostics"](
nil, nil,
"textDocument/publishDiagnostics",
{ {
diagnostics = vim.lsp.diagnostic.get(bufnr, client.id), diagnostics = vim.lsp.diagnostic.get(bufnr, client.id),
uri = vim.uri_from_bufnr(bufnr), uri = vim.uri_from_bufnr(bufnr),
}, },
client.id {
method="textDocument/publishDiagnostics",
client_id=client.id,
bufnr=bufnr,
}
) )
end) end)
end end
@ -555,7 +558,7 @@ end
--- ---
--- The following parameters describe fields in the {config} table. --- 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. --- its rootUri on initialization.
--- ---
--@param cmd: (required, string or list treated like |jobstart()|) Base command --@param cmd: (required, string or list treated like |jobstart()|) Base command
@ -590,6 +593,10 @@ end
--- as `initializationOptions`. See `initialize` in the LSP spec. --- as `initializationOptions`. See `initialize` in the LSP spec.
--- ---
--@param name (string, default=client-id) Name in log messages. --@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. --@param get_language_id function(bufnr, filetype) -> language ID as string.
--- Defaults to the filetype. --- Defaults to the filetype.
@ -677,11 +684,11 @@ function lsp.start_client(config)
--@param method (string) LSP method name --@param method (string) LSP method name
--@param params (table) The parameters for that method. --@param params (table) The parameters for that method.
function dispatch.notification(method, params) 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) local handler = resolve_handler(method)
if handler then if handler then
-- Method name is provided here for convenience. -- Method name is provided here for convenience.
handler(nil, method, params, client_id) handler(nil, params, {method=method, client_id=client_id})
end end
end end
@ -691,13 +698,13 @@ function lsp.start_client(config)
--@param method (string) LSP method name --@param method (string) LSP method name
--@param params (table) The parameters for that method --@param params (table) The parameters for that method
function dispatch.server_request(method, params) 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) local handler = resolve_handler(method)
if handler then if handler then
local _ = log.debug() and log.debug("server_request: found handler for", method) local _ = log.trace() and log.trace("server_request: found handler for", method)
return handler(nil, method, params, client_id) return handler(nil, params, {method=method, client_id=client_id})
end 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) return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
end end
@ -775,6 +782,14 @@ function lsp.start_client(config)
off = 'off'; messages = 'messages'; verbose = 'verbose'; off = 'off'; messages = 'messages'; verbose = 'verbose';
} }
local version = vim.version() 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 = { local initialize_params = {
-- The process Id of the parent process that started the server. Is null if -- 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 -- 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; rootPath = config.root_dir;
-- The rootUri of the workspace. Is null if no folder is open. If both -- The rootUri of the workspace. Is null if no folder is open. If both
-- `rootPath` and `rootUri` are set `rootUri` wins. -- `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. -- User provided initialization options.
initializationOptions = config.init_options; initializationOptions = config.init_options;
-- The capabilities provided by the client (editor or tool) -- The capabilities provided by the client (editor or tool)
@ -815,16 +830,13 @@ function lsp.start_client(config)
-- -- workspace folder in the user interface. -- -- workspace folder in the user interface.
-- name -- name
-- } -- }
workspaceFolders = {{ workspaceFolders = config.workspace_folders,
uri = vim.uri_from_fname(config.root_dir);
name = string.format("%s", config.root_dir);
}};
} }
if config.before_init then if config.before_init then
-- TODO(ashkan) handle errors here. -- TODO(ashkan) handle errors here.
pcall(config.before_init, initialize_params, config) pcall(config.before_init, initialize_params, config)
end 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) rpc.request('initialize', initialize_params, function(init_err, result)
assert(not init_err, tostring(init_err)) assert(not init_err, tostring(init_err))
assert(result, "server sent empty result") 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) local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr)
return rpc.request(method, params, function(err, result) 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)
end end
@ -915,7 +927,7 @@ function lsp.start_client(config)
--@see |vim.lsp.buf_request_sync()| --@see |vim.lsp.buf_request_sync()|
function client.request_sync(method, params, timeout_ms, bufnr) function client.request_sync(method, params, timeout_ms, bufnr)
local request_result = nil local request_result = nil
local function _sync_handler(err, _, result) local function _sync_handler(err, result)
request_result = { err = err, result = result } request_result = { err = err, result = result }
end end
@ -1150,7 +1162,7 @@ end
---@param bufnr (number) Buffer handle, or 0 for current ---@param bufnr (number) Buffer handle, or 0 for current
---@param client_id (number) the client id ---@param client_id (number) the client id
function lsp.buf_is_attached(bufnr, 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 end
--- Gets a client by id, or nil if the id is invalid. --- 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) local unsupported_err = lsp._unsupported_method(method)
handler = handler or lsp.handlers[method] handler = handler or lsp.handlers[method]
if handler then if handler then
handler(unsupported_err, method, bufnr) handler(unsupported_err, nil, {method=method, bufnr=bufnr})
end end
return return
end end
@ -1314,8 +1326,8 @@ function lsp.buf_request_all(bufnr, method, params, callback)
end end
end) end)
local function _sync_handler(err, _, result, client_id) local function _sync_handler(err, result, ctx)
request_results[client_id] = { error = err, result = result } request_results[ctx.client_id] = { error = err, result = result }
result_count = result_count + 1 result_count = result_count + 1
set_expected_result_count() set_expected_result_count()
@ -1421,7 +1433,7 @@ function lsp.omnifunc(findstart, base)
local params = util.make_position_params() local params = util.make_position_params()
local items = {} 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 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) local matches = util.text_document_completion_list_to_complete_items(result, prefix)
-- TODO(ashkan): is this the best way to do this? -- TODO(ashkan): is this the best way to do this?
@ -1496,8 +1508,8 @@ end
--@param handler (function) See |lsp-handler| --@param handler (function) See |lsp-handler|
--@param override_config (table) Table containing the keys to override behavior of the {handler} --@param override_config (table) Table containing the keys to override behavior of the {handler}
function lsp.with(handler, override_config) function lsp.with(handler, override_config)
return function(err, method, params, client_id, bufnr, config) return function(err, result, ctx, config)
return handler(err, method, params, client_id, bufnr, vim.tbl_deep_extend("force", config or {}, override_config)) return handler(err, result, ctx, vim.tbl_deep_extend("force", config or {}, override_config))
end end
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 if #clients > 1 then
local choices = {} 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)) table.insert(choices, string.format("%d %s", k, v.name))
end end
local user_choice = vim.fn.confirm( 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()); local clients = vim.tbl_values(vim.lsp.buf_get_clients());
-- sort the clients according to `order` -- 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 -- 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 if client.name == client_name then
table.insert(clients, table.remove(clients, i)) table.insert(clients, table.remove(clients, i))
break break
@ -215,7 +215,7 @@ function M.formatting_seq_sync(options, timeout_ms, order)
end end
-- loop through the clients and make synchronous formatting requests -- 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 if client.resolved_capabilities.document_formatting then
local params = util.make_formatting_params(options) local params = util.make_formatting_params(options)
local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf()) 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] return call_hierarchy_items[1]
end end
local items = {} 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 local entry = item.detail or item.name
table.insert(items, string.format("%d. %s", i, entry)) table.insert(items, string.format("%d. %s", i, entry))
end end
@ -300,13 +300,21 @@ end
--@private --@private
local function call_hierarchy(method) local function call_hierarchy(method)
local params = util.make_position_params() local params = util.make_position_params()
request('textDocument/prepareCallHierarchy', params, function(err, _, result) request('textDocument/prepareCallHierarchy', params, function(err, result, ctx)
if err then if err then
vim.notify(err.message, vim.log.levels.WARN) vim.notify(err.message, vim.log.levels.WARN)
return return
end end
local call_hierarchy_item = pick_call_hierarchy_item(result) 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)
end end
@ -328,8 +336,8 @@ end
--- ---
function M.list_workspace_folders() function M.list_workspace_folders()
local workspace_folders = {} local workspace_folders = {}
for _, client in ipairs(vim.lsp.buf_get_clients()) do for _, client in pairs(vim.lsp.buf_get_clients()) do
for _, folder in ipairs(client.workspaceFolders) do for _, folder in pairs(client.workspaceFolders) do
table.insert(workspace_folders, folder.name) table.insert(workspace_folders, folder.name)
end end
end end
@ -347,9 +355,9 @@ function M.add_workspace_folder(workspace_folder)
return return
end end
local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}}) 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 local found = false
for _, folder in ipairs(client.workspaceFolders) do for _, folder in pairs(client.workspaceFolders) do
if folder.name == workspace_folder then if folder.name == workspace_folder then
found = true found = true
print(workspace_folder, "is already part of this workspace") 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") vim.api.nvim_command("redraw")
if not (workspace_folder and #workspace_folder > 0) then return end 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}}) 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
for idx, folder in ipairs(client.workspaceFolders) do for idx, folder in pairs(client.workspaceFolders) do
if folder.name == workspace_folder then if folder.name == workspace_folder then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params) vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
client.workspaceFolders[idx] = nil client.workspaceFolders[idx] = nil
@ -422,6 +430,21 @@ function M.clear_references()
util.buf_clear_references() util.buf_clear_references()
end 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 --- Selects a code action from the input list that is available at the current
--- cursor position. --- cursor position.
-- --

View File

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

View File

@ -203,8 +203,10 @@ local bufnr_and_client_cacher_mt = {
-- Diagnostic Saving & Caching {{{ -- Diagnostic Saving & Caching {{{
local _diagnostic_cleanup = setmetatable({}, bufnr_and_client_cacher_mt) local _diagnostic_cleanup = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_cache = 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_lines = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_cache_counts = 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) 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) diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
sign_ns = sign_ns or M._get_sign_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(bufnr, "bufnr is required")
assert(diagnostic_ns, "Need diagnostic_ns, got nil") assert(diagnostic_ns, "Need diagnostic_ns, got nil")
@ -1005,15 +1008,16 @@ end
--- - Update diagnostics in InsertMode or wait until InsertLeave --- - Update diagnostics in InsertMode or wait until InsertLeave
--- - severity_sort: (default=false) --- - severity_sort: (default=false)
--- - Sort diagnostics (and thus signs and virtual text) --- - Sort diagnostics (and thus signs and virtual text)
function M.on_publish_diagnostics(_, _, params, client_id, _, config) function M.on_publish_diagnostics(_, result, ctx, config)
local uri = params.uri local client_id = ctx.client_id
local uri = result.uri
local bufnr = vim.uri_to_bufnr(uri) local bufnr = vim.uri_to_bufnr(uri)
if not bufnr then if not bufnr then
return return
end end
local diagnostics = params.diagnostics local diagnostics = result.diagnostics
if config and if_nil(config.severity_sort, false) then if config and if_nil(config.severity_sort, false) then
table.sort(diagnostics, function(a, b) return a.severity > b.severity end) 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) M.display(diagnostics, bufnr, client_id, config)
end 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 --@private
--- Display diagnostics for the buffer, given a configuration. --- Display diagnostics for the buffer, given a configuration.
function M.display(diagnostics, bufnr, client_id, config) function M.display(diagnostics, bufnr, client_id, config)
@ -1108,9 +1160,46 @@ function M.display(diagnostics, bufnr, client_id, config)
if signs_opts then if signs_opts then
M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts) M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts)
end end
-- cache extmarks
save_extmarks(bufnr, client_id)
end 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} --- Open a floating window with the diagnostics from {line_nr}
--- ---

View File

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

View File

@ -14,10 +14,11 @@ log.levels = vim.deepcopy(vim.log.levels)
-- Default log level is warn. -- Default log level is warn.
local current_log_level = log.levels.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 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 --@private
local function path_join(...) local function path_join(...)
return table.concat(vim.tbl_flatten{...}, path_sep) return table.concat(vim.tbl_flatten{...}, path_sep)
@ -33,7 +34,7 @@ do
vim.fn.mkdir(vim.fn.stdpath('cache'), "p") vim.fn.mkdir(vim.fn.stdpath('cache'), "p")
local logfile = assert(io.open(logfilename, "a+")) local logfile = assert(io.open(logfilename, "a+"))
-- Start message for logging -- 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 for level, levelnr in pairs(log.levels) do
-- Also export the log level on the root object. -- Also export the log level on the root object.
log[level] = levelnr log[level] = levelnr
@ -56,14 +57,14 @@ do
if levelnr < current_log_level then return false end if levelnr < current_log_level then return false end
if argc == 0 then return true end if argc == 0 then return true end
local info = debug.getinfo(2, "Sl") local info = debug.getinfo(2, "Sl")
local fileinfo = string.format("%s:%s", info.short_src, info.currentline) 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 = { table.concat({"[", level, "]", os.date(log_date_format), "]", fileinfo, "]"}, " ") } local parts = { header }
for i = 1, argc do for i = 1, argc do
local arg = select(i, ...) local arg = select(i, ...)
if arg == nil then if arg == nil then
table.insert(parts, "nil") table.insert(parts, "nil")
else else
table.insert(parts, vim.inspect(arg, {newline=''})) table.insert(parts, format_func(arg))
end end
end end
logfile:write(table.concat(parts, '\t'), "\n") logfile:write(table.concat(parts, '\t'), "\n")
@ -88,6 +89,18 @@ function log.set_level(level)
end end
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. --- Checks whether the level is sufficient for logging.
--@param level number log level --@param level number log level
--@returns (bool) true if would log, false if not --@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()| --@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. --@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 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 if handle == nil or handle:is_closing() then return false end
-- TODO(ashkan) remove this once we have a Lua json_encode -- TODO(ashkan) remove this once we have a Lua json_encode
schedule(function() schedule(function()
@ -493,7 +493,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
-- on_error(client_errors.INVALID_SERVER_JSON, err) -- on_error(client_errors.INVALID_SERVER_JSON, err)
return return
end 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 if type(decoded.method) == 'string' and decoded.id then
-- Server Request -- Server Request

View File

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

View File

@ -26,6 +26,8 @@
</screenshots> </screenshots>
<releases> <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="2020-08-04" version="0.4.4"/>
<release date="2019-11-06" version="0.4.3"/> <release date="2019-11-06" version="0.4.3"/>
<release date="2019-09-15" version="0.4.2"/> <release date="2019-09-15" version="0.4.2"/>

View File

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

View File

@ -27,11 +27,7 @@ if &filetype != 'man'
finish finish
endif endif
if !exists('b:man_sect') if get(b:, 'man_sect', '') =~# '^[023]'
call man#init_pager()
endif
if b:man_sect =~# '^[023]'
syntax case match syntax case match
syntax include @c $VIMRUNTIME/syntax/c.vim syntax include @c $VIMRUNTIME/syntax/c.vim
syntax match manCFuncDefinition display '\<\h\w*\>\ze\(\s\|\n\)*(' contained 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_VERSION_PRERELEASE
# - CMakeLists.txt: Unset NVIM_API_PRERELEASE # - CMakeLists.txt: Unset NVIM_API_PRERELEASE
# - Create test/functional/fixtures/api_level_N.mpack # - Create test/functional/fixtures/api_level_N.mpack
# - Add date and version to runtime/nvim.appdata.xml
# - Tag the commit. # - Tag the commit.
# Create the "version bump" commit: # Create the "version bump" commit:
# - CMakeLists.txt: Set NVIM_VERSION_PRERELEASE to "-dev" # - 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 git add test/functional/fixtures/api_level_$__API_LEVEL.mpack
fi 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 if ! test "$ARG1" = '--use-current-commit' ; then
echo "Building changelog since ${__LAST_TAG}..." echo "Building changelog since ${__LAST_TAG}..."
@ -75,14 +80,12 @@ _do_release_commit() {
_do_bump_commit() { _do_bump_commit() {
$__sed -i.bk 's/(NVIM_VERSION_PRERELEASE) ""/\1 "-dev"/' CMakeLists.txt $__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/set\((NVIM_VERSION_PATCH) [[:digit:]]/set(\1 ?/' CMakeLists.txt
$__sed -i.bk 's,(<releases>),\1\ rm -f CMakeLists.txt.bk
<release date="'"${__DATE}"'" version="xxx"/>,' runtime/nvim.appdata.xml rm -f runtime/nvim.appdata.xml.bk
rm CMakeLists.txt.bk
rm runtime/nvim.appdata.xml.bk
nvim +'/NVIM_VERSION' +1new +'exe "norm! iUpdate version numbers!!!"' \ 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" git commit -m "$__BUMP_MSG"
} }
@ -92,11 +95,7 @@ fi
_do_bump_commit _do_bump_commit
echo " echo "
Next steps: Next steps:
- Update runtime/nvim.appdata.xml on _master_
- Run tests/CI (version_spec.lua)! - Run tests/CI (version_spec.lua)!
- Push the tag: - Push the tag:
git push --follow-tags git push --follow-tags
- Update the 'stable' tag:
git push --force upstream HEAD^:refs/tags/stable
git fetch --tags
- Update website: index.html" - Update website: index.html"

View File

@ -1724,7 +1724,7 @@ BYPASS_AU:
void block_autocmds(void) void block_autocmds(void)
{ {
// Remember the value of v:termresponse. // Remember the value of v:termresponse.
if (is_autocmd_blocked()) { if (!is_autocmd_blocked()) {
old_termresponse = get_vim_var_str(VV_TERMRESPONSE); old_termresponse = get_vim_var_str(VV_TERMRESPONSE);
} }
autocmd_blocked++; autocmd_blocked++;
@ -1737,7 +1737,7 @@ void unblock_autocmds(void)
// When v:termresponse was set while autocommands were blocked, trigger // When v:termresponse was set while autocommands were blocked, trigger
// the autocommands now. Esp. useful when executing a shell command // the autocommands now. Esp. useful when executing a shell command
// during startup (nvim -d). // during startup (nvim -d).
if (is_autocmd_blocked() if (!is_autocmd_blocked()
&& get_vim_var_str(VV_TERMRESPONSE) != old_termresponse) { && get_vim_var_str(VV_TERMRESPONSE) != old_termresponse) {
apply_autocmds(EVENT_TERMRESPONSE, NULL, NULL, false, curbuf); 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 /// `on_stdout` is ignored
/// @param[in] detach True if the job should not be killed when nvim exits, /// @param[in] detach True if the job should not be killed when nvim exits,
/// ignored if `pty` is true /// 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 /// @param[in] cwd Initial working directory for the job. Nvim's working
/// directory if `cwd` is NULL /// directory if `cwd` is NULL
/// @param[in] pty_width Width of the pty, ignored if `pty` is false /// @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, Channel *channel_job_start(char **argv, CallbackReader on_stdout,
CallbackReader on_stderr, Callback on_exit, CallbackReader on_stderr, Callback on_exit,
bool pty, bool rpc, bool overlapped, bool detach, 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, uint16_t pty_width, uint16_t pty_height,
dict_T *env, varnumber_T *status_out) dict_T *env, varnumber_T *status_out)
{ {
@ -345,7 +348,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
proc->overlapped = overlapped; proc->overlapped = overlapped;
char *cmd = xstrdup(proc->argv[0]); char *cmd = xstrdup(proc->argv[0]);
bool has_out, has_err; bool has_in, has_out, has_err;
if (proc->type == kProcessTypePty) { if (proc->type == kProcessTypePty) {
has_out = true; has_out = true;
has_err = false; 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_out = rpc || callback_reader_set(chan->on_data);
has_err = callback_reader_set(chan->on_stderr); 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) { if (status) {
EMSG3(_(e_jobspawn), os_strerror(status), cmd); EMSG3(_(e_jobspawn), os_strerror(status), cmd);
xfree(cmd); xfree(cmd);
@ -369,7 +382,9 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
tv_dict_free(proc->env); tv_dict_free(proc->env);
} }
wstream_init(&proc->in, 0); if (has_in) {
wstream_init(&proc->in, 0);
}
if (has_out) { if (has_out) {
rstream_init(&proc->out, 0); rstream_init(&proc->out, 0);
} }

View File

@ -28,6 +28,10 @@ typedef enum {
kChannelPartAll kChannelPartAll
} ChannelPart; } ChannelPart;
typedef enum {
kChannelStdinPipe,
kChannelStdinNull,
} ChannelStdinMode;
typedef struct { typedef struct {
Stream in; 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'. /// 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. /// @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_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
FUNC_ATTR_NONNULL_RET FUNC_ATTR_NONNULL_RET
{ {
const char_u *p = q; return skipwhite_len(p, STRLEN(p));
while (ascii_iswhite(*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++; p++;
} }
return (char_u *)p; return (char_u *)p;
@ -1600,6 +1614,18 @@ char_u* skiptowhite_esc(char_u *p) {
return 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. /// Gets a number from a string and skips over it, signalling overflow.
/// ///
/// @param[out] pp A pointer to a pointer to char_u. /// @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 pty = false;
bool clear_env = false; bool clear_env = false;
bool overlapped = false; bool overlapped = false;
ChannelStdinMode stdin_mode = kChannelStdinPipe;
CallbackReader on_stdout = CALLBACK_READER_INIT, CallbackReader on_stdout = CALLBACK_READER_INIT,
on_stderr = CALLBACK_READER_INIT; on_stderr = CALLBACK_READER_INIT;
Callback on_exit = CALLBACK_NONE; 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; clear_env = tv_dict_get_number(job_opts, "clear_env") != 0;
overlapped = tv_dict_get_number(job_opts, "overlapped") != 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) { if (pty && rpc) {
EMSG2(_(e_invarg2), "job cannot have both 'pty' and 'rpc' options set"); EMSG2(_(e_invarg2), "job cannot have both 'pty' and 'rpc' options set");
shell_free_argv(argv); 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); env = create_environment(job_env, clear_env, pty, term_name);
Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit, pty, Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit, pty,
rpc, overlapped, detach, cwd, width, height, rpc, overlapped, detach, stdin_mode, cwd,
env, &rettv->vval.v_number); width, height, env, &rettv->vval.v_number);
if (chan) { if (chan) {
channel_create_event(chan, NULL); 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, Channel *chan = channel_job_start(argv, CALLBACK_READER_INIT,
CALLBACK_READER_INIT, CALLBACK_NONE, CALLBACK_READER_INIT, CALLBACK_NONE,
false, true, false, false, NULL, 0, 0, false, true, false, false,
NULL, &rettv->vval.v_number); kChannelStdinPipe, NULL, 0, 0, NULL,
&rettv->vval.v_number);
if (chan) { if (chan) {
channel_create_event(chan, NULL); 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 rpc = false;
const bool overlapped = false; const bool overlapped = false;
const bool detach = false; const bool detach = false;
ChannelStdinMode stdin_mode = kChannelStdinPipe;
uint16_t term_width = MAX(0, curwin->w_width_inner - win_col_off(curwin)); 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, Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit,
pty, rpc, overlapped, detach, cwd, pty, rpc, overlapped, detach, stdin_mode,
term_width, curwin->w_height_inner, cwd, term_width, curwin->w_height_inner,
env, &rettv->vval.v_number); env, &rettv->vval.v_number);
if (rettv->vval.v_number <= 0) { if (rettv->vval.v_number <= 0) {
return; 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 { typedef struct {
linenr_T curr_lnum; linenr_T curr_lnum;
const linenr_T final_lnum; const linenr_T final_lnum;
} GetBufferLineCookie; } 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. /// ":source" and associated commands.
/// ///
/// @return address holding the next breakpoint line for a source cookie /// @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) static char_u *get_str_line(int c, void *cookie, int indent, bool do_concat)
{ {
GetStrLineCookie *p = cookie; GetStrLineCookie *p = cookie;
size_t i = p->offset; if (STRLEN(p->buf) <= p->offset) {
if (strlen((char *)p->buf) <= p->offset) {
return NULL; return NULL;
} }
while (!(p->buf[i] == '\n' || p->buf[i] == '\0')) { const char_u *line = p->buf + p->offset;
i++; 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; ga_append(&ga, NUL);
char_u *buf = xmemdupz(p->buf + p->offset, line_length); p->offset = (size_t)(eol - p->buf) + 1;
p->offset = i + 1; return ga.ga_data;
return buf;
} }
static int source_using_linegetter(void *cookie, static int source_using_linegetter(void *cookie,
@ -2770,6 +2778,40 @@ static int source_using_linegetter(void *cookie,
return retval; 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. /// Executes lines in `src` as Ex commands.
/// ///
/// @see do_source() /// @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_init(&ga, (int)sizeof(char_u), 400);
ga_concat(&ga, line); ga_concat(&ga, line);
if (*p == '\\') { while (sp->nextline != NULL
ga_concat(&ga, p + 1); && concat_continued_line(&ga, 400, sp->nextline,
} STRLEN(sp->nextline))) {
for (;; ) {
xfree(sp->nextline); xfree(sp->nextline);
sp->nextline = get_one_sourceline(sp); 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); ga_append(&ga, NUL);
xfree(line); xfree(line);

View File

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

View File

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

View File

@ -2251,7 +2251,7 @@ static int command_line_changed(CommandLineState *s)
State |= CMDPREVIEW; State |= CMDPREVIEW;
emsg_silent++; // Block error reporting as the command may be incomplete emsg_silent++; // Block error reporting as the command may be incomplete
msg_silent++; // Block messages, namely ones that prompt 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 msg_silent--; // Unblock messages
emsg_silent--; // Unblock error reporting 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; 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, 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); marktree_del_itr(buf->b_marktree, itr, false);
} else { } else {
marktree_itr_next(buf->b_marktree, itr); marktree_itr_next(buf->b_marktree, itr);

View File

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

View File

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

View File

@ -4,6 +4,7 @@
-- 1. runtime/lua/vim/ (the runtime): For "nice to have" features, e.g. the -- 1. runtime/lua/vim/ (the runtime): For "nice to have" features, e.g. the
-- `inspect` and `lpeg` modules. -- `inspect` and `lpeg` modules.
-- 2. runtime/lua/vim/shared.lua: Code shared between Nvim and tests. -- 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. -- 3. src/nvim/lua/: Compiled-into Nvim itself.
-- --
-- Guideline: "If in doubt, put it in the runtime". -- Guideline: "If in doubt, put it in the runtime".
@ -425,26 +426,35 @@ function vim.notify(msg, log_level, _opts)
end 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 Nvim command-line option |-w| is related but does not support callbacks
--- The string will contain the literal keys typed. --- and cannot be toggled dynamically.
--- See |i_CTRL-V|
--- ---
---@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} --- 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 ---@param ns_id number? Namespace ID. If nil or 0, generates and returns a new
--- namespace ID from |nvim_create_namesapce()| --- |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. ---@note {fn} will be removed if an error occurs while calling.
--- This is to prevent the annoying situation of every keystroke erroring ---@note {fn} will not be cleared by |nvim_buf_clear_namespace()|
--- while trying to remove a broken callback. ---@note {fn} will receive the keys after mappings have been evaluated
--@note {fn} will not be cleared from |nvim_buf_clear_namespace()| function vim.on_key(fn, ns_id)
--@note {fn} will receive the keystrokes after mappings have been evaluated if fn == nil and ns_id == nil then
function vim.register_keystroke_callback(fn, ns_id) return #on_key_cbs
end
vim.validate { vim.validate {
fn = { fn, 'c', true}, fn = { fn, 'c', true},
ns_id = { ns_id, 'n', 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('') ns_id = vim.api.nvim_create_namespace('')
end end
on_keystroke_callbacks[ns_id] = fn on_key_cbs[ns_id] = fn
return ns_id return ns_id
end end
--- Function that executes the keystroke callbacks. --- Executes the on_key callbacks.
--@private ---@private
function vim._log_keystroke(char) function vim._on_key(char)
local failed_ns_ids = {} local failed_ns_ids = {}
local failed_messages = {} 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) local ok, err_msg = pcall(v, char)
if not ok then if not ok then
vim.register_keystroke_callback(nil, k) vim.on_key(nil, k)
table.insert(failed_ns_ids, k) table.insert(failed_ns_ids, k)
table.insert(failed_messages, err_msg) table.insert(failed_messages, err_msg)
end end
@ -475,7 +484,7 @@ function vim._log_keystroke(char)
if failed_ns_ids[1] then if failed_ns_ids[1] then
error(string.format( 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_ns_ids, ", "),
table.concat(failed_messages, "\n"))) table.concat(failed_messages, "\n")))
end end

View File

@ -338,7 +338,7 @@ int main(int argc, char **argv)
// prepare screen now, so external UIs can display messages // prepare screen now, so external UIs can display messages
starting = NO_BUFFERS; starting = NO_BUFFERS;
screenclear(); 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' // 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); xfree(wp->w_p_scl);
wp->w_p_scl = (char_u *)xstrdup("auto"); 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); 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 // changing border style while keeping border only requires redrawing border
if (fconfig.border) { if (fconfig.border) {
wp->w_redr_border = true; wp->w_redr_border = true;
@ -770,7 +801,6 @@ int win_fdccol_count(win_T *wp)
} }
} }
void ui_ext_win_position(win_T *wp) void ui_ext_win_position(win_T *wp)
{ {
if (!wp->w_floating) { 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_row = (int)row - (south ? wp->w_height : 0);
int comp_col = (int)col - (east ? wp->w_width : 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_row = MAX(MIN(comp_row, Rows-wp->w_height_outer-1), 0);
comp_col = MAX(MIN(comp_col, Columns-wp->w_width_outer), 0); comp_col = MAX(MIN(comp_col, Columns-wp->w_width_outer), 0);
wp->w_winrow = comp_row; 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 // When deleting the current window of another tab page select a new
// current window. // current window.
if (tp != NULL && win == tp->tp_curwin) { 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; 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_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 - `NVIM_PROG`, `NVIM_PRG` (F) (S): override path to Neovim executable (default
to `build/bin/nvim`). to `build/bin/nvim`).

View File

@ -5,6 +5,7 @@ local eq, clear, eval, command, nvim, next_msg =
local meths = helpers.meths local meths = helpers.meths
local exec_lua = helpers.exec_lua local exec_lua = helpers.exec_lua
local retry = helpers.retry local retry = helpers.retry
local isCI = helpers.isCI
describe('notify', function() describe('notify', function()
local channel local channel
@ -76,6 +77,10 @@ describe('notify', function()
end) end)
it('cancels stale events on channel close', function() 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 if helpers.pending_win32(pending) then return end
local catchan = eval("jobstart(['cat'], {'rpc': v:true})") local catchan = eval("jobstart(['cat'], {'rpc': v:true})")
eq({id=catchan, stream='job', mode='rpc', client = {}}, exec_lua ([[ 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('exec','autocmd BufAdd * :let x1 = "Hello"', false)
nvim('command', 'new foo') nvim('command', 'new foo')
eq('Hello', request('nvim_eval', 'g:x1')) 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) end)
it('non-ASCII input', function() it('non-ASCII input', function()

View File

@ -346,6 +346,21 @@ describe('API/win', function()
eq(2, #meths.list_wins()) eq(2, #meths.list_wins())
eq('', funcs.getcmdwintype()) eq('', funcs.getcmdwintype())
end) 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) end)
describe('hide', function() describe('hide', function()

View File

@ -348,6 +348,12 @@ describe('jobs', function()
eq(false, pcall(function() eq(false, pcall(function()
nvim('command', 'call jobsend(j, ["some data"])') nvim('command', 'call jobsend(j, ["some data"])')
end)) 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) end)
it('disallows jobsend on a non-existent job', function() 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 feed_command = helpers.feed_command
local write_file = helpers.write_file local write_file = helpers.write_file
local exec = helpers.exec local exec = helpers.exec
local exc_exec = helpers.exc_exec
local exec_lua = helpers.exec_lua
local eval = helpers.eval local eval = helpers.eval
local exec_capture = helpers.exec_capture local exec_capture = helpers.exec_capture
local neq = helpers.neq local neq = helpers.neq
@ -18,16 +20,30 @@ describe(':source', function()
end) end)
it('current buffer', function() it('current buffer', function()
insert('let a = 2') insert([[
let a = 2
let b = #{
\ k: "v"
"\ (o_o)
\ }]])
command('source') command('source')
eq('2', meths.exec('echo a', true)) 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) end)
it('selection in current buffer', function() it('selection in current buffer', function()
insert( insert([[
'let a = 2\n'.. let a = 2
'let a = 3\n'.. let a = 3
'let a = 4\n') let a = 4
let b = #{
"\ (>_<)
\ K: "V"
\ }]])
-- Source the 2nd line only -- Source the 2nd line only
feed('ggjV') feed('ggjV')
@ -38,13 +54,26 @@ describe(':source', function()
feed('ggjVG') feed('ggjVG')
feed_command(':source') feed_command(':source')
eq('4', meths.exec('echo a', true)) 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) end)
it('multiline heredoc command', function() it('multiline heredoc command', function()
insert( insert([[
'lua << EOF\n'.. lua << EOF
'y = 4\n'.. y = 4
'EOF\n') EOF]])
command('source') command('source')
eq('4', meths.exec('echo luaeval("y")', true)) eq('4', meths.exec('echo luaeval("y")', true))
@ -67,13 +96,21 @@ describe(':source', function()
vim.g.b = 5 vim.g.b = 5
vim.g.b = 6 vim.g.b = 6
vim.g.b = 7 vim.g.b = 7
a = [=[
"\ a
\ b]=]
]]) ]])
command('edit '..test_file) command('edit '..test_file)
feed('ggjV') feed('ggjV')
feed_command(':source') feed_command(':source')
eq(6, eval('g:b')) eq(6, eval('g:b'))
feed('GVkk')
feed_command(':source')
eq(' "\\ a\n \\ b', exec_lua('return _G.a'))
os.remove(test_file) os.remove(test_file)
end) end)
@ -84,12 +121,16 @@ describe(':source', function()
vim.g.c = 10 vim.g.c = 10
vim.g.c = 11 vim.g.c = 11
vim.g.c = 12 vim.g.c = 12
a = [=[
\ 1
"\ 2]=]
]]) ]])
command('edit '..test_file) command('edit '..test_file)
feed_command(':source') feed_command(':source')
eq(12, eval('g:c')) eq(12, eval('g:c'))
eq(' \\ 1\n "\\ 2', exec_lua('return _G.a'))
os.remove(test_file) os.remove(test_file)
end) end)

View File

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

View File

@ -32,7 +32,7 @@ describe('vim.lsp.codelens', function()
command = { title = 'Lens1', command = 'Dummy' } 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) ]], bufnr)
local stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', 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), 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, { uri = fake_uri, diagnostics = server_1_diags }, {client_id=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_2_diags }, {client_id=2})
return { return {
vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1), vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1),
vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", 2), 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), 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, { uri = fake_uri, diagnostics = server_1_diags }, {client_id=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_2_diags }, {client_id=2})
return { return {
vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1), vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Error", 1),
vim.lsp.diagnostic.get_count(diagnostic_bufnr, "Warning", 2), 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() it('should return all diagnostics when no severity is supplied', function()
eq(2, exec_lua [[ eq(2, exec_lua [[
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { vim.lsp.diagnostic.on_publish_diagnostics(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_error("Error 1", 1, 1, 1, 5), make_error("Error 1", 1, 1, 1, 5),
make_warning("Warning on Server 1", 1, 1, 2, 5), make_warning("Warning on Server 1", 1, 1, 2, 5),
make_error("Error On Other Line", 2, 1, 1, 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) 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() it('should return only requested diagnostics when severity_limit is supplied', function()
eq(2, exec_lua [[ eq(2, exec_lua [[
vim.lsp.diagnostic.on_publish_diagnostics(nil, nil, { vim.lsp.diagnostic.on_publish_diagnostics(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_error("Error 1", 1, 1, 1, 5), 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_information("Ignored information", 1, 1, 2, 5),
make_error("Error On Other Line", 2, 1, 1, 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" }) return #vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1, { severity_limit = "Warning" })
]]) ]])
@ -470,12 +470,12 @@ describe('vim.lsp.diagnostic', function()
exec_lua [[ exec_lua [[
vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, {
virtual_text = function() return true end, virtual_text = function() return true end,
})(nil, nil, { })(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4), make_error('Delayed Diagnostic', 4, 4, 4, 4),
} }
}, 1 }, {client_id=1}
) )
]] ]]
@ -487,12 +487,12 @@ describe('vim.lsp.diagnostic', function()
exec_lua [[ exec_lua [[
vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, {
virtual_text = function() return false end, virtual_text = function() return false end,
})(nil, nil, { })(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4), make_error('Delayed Diagnostic', 4, 4, 4, 4),
} }
}, 1 }, {client_id=1}
) )
]] ]]
@ -509,12 +509,12 @@ describe('vim.lsp.diagnostic', function()
exec_lua [[ exec_lua [[
vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, {
update_in_insert = false, update_in_insert = false,
})(nil, nil, { })(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4), make_error('Delayed Diagnostic', 4, 4, 4, 4),
} }
}, 1 }, {client_id=1}
) )
]] ]]
@ -551,12 +551,12 @@ describe('vim.lsp.diagnostic', function()
return SetVirtualTextOriginal(...) return SetVirtualTextOriginal(...)
end end
PublishDiagnostics(nil, nil, { PublishDiagnostics(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4), make_error('Delayed Diagnostic', 4, 4, 4, 4),
} }
}, 1 }, {client_id=1}
) )
]] ]]
@ -605,12 +605,12 @@ describe('vim.lsp.diagnostic', function()
return SetVirtualTextOriginal(...) return SetVirtualTextOriginal(...)
end end
PublishDiagnostics(nil, nil, { PublishDiagnostics(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4), make_error('Delayed Diagnostic', 4, 4, 4, 4),
} }
}, 1 }, {client_id=1}
) )
]] ]]
@ -647,12 +647,12 @@ describe('vim.lsp.diagnostic', function()
exec_lua [[ exec_lua [[
vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, {
update_in_insert = true, update_in_insert = true,
})(nil, nil, { })(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4), 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, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4), make_error('Delayed Diagnostic', 4, 4, 4, 4),
} }
}, 1 }, {client_id=1}
) )
return vim.api.nvim_buf_get_extmarks( return vim.api.nvim_buf_get_extmarks(
@ -714,12 +714,12 @@ describe('vim.lsp.diagnostic', function()
end, end,
}) })
PublishDiagnostics(nil, nil, { PublishDiagnostics(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Delayed Diagnostic', 4, 4, 4, 4), make_error('Delayed Diagnostic', 4, 4, 4, 4),
} }
}, 1 }, {client_id=1}
) )
return vim.api.nvim_buf_get_extmarks( return vim.api.nvim_buf_get_extmarks(
@ -747,12 +747,12 @@ describe('vim.lsp.diagnostic', function()
}, },
}) })
PublishDiagnostics(nil, nil, { PublishDiagnostics(nil, {
uri = fake_uri, uri = fake_uri,
diagnostics = { diagnostics = {
make_warning('Delayed Diagnostic', 4, 4, 4, 4), make_warning('Delayed Diagnostic', 4, 4, 4, 4),
} }
}, 1 }, {client_id=1}
) )
return count_of_extmarks_for_client(diagnostic_bufnr, 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.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, uri = fake_uri,
diagnostics = diagnostics diagnostics = diagnostics
}, 1 }, {client_id=1}
) )
vim.lsp.diagnostic.set_signs(diagnostics, diagnostic_bufnr, 1) vim.lsp.diagnostic.set_signs(diagnostics, diagnostic_bufnr, 1)
@ -863,13 +863,13 @@ describe('vim.lsp.diagnostic', function()
local loc_list = exec_lua [[ local loc_list = exec_lua [[
vim.api.nvim_win_set_buf(0, diagnostic_bufnr) 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, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Farther Diagnostic', 4, 4, 4, 4), make_error('Farther Diagnostic', 4, 4, 4, 4),
make_error('Lower Diagnostic', 1, 1, 1, 1), make_error('Lower Diagnostic', 1, 1, 1, 1),
} }
}, 1 }, {client_id=1}
) )
vim.lsp.diagnostic.set_loclist() vim.lsp.diagnostic.set_loclist()
@ -884,20 +884,20 @@ describe('vim.lsp.diagnostic', function()
local loc_list = exec_lua [[ local loc_list = exec_lua [[
vim.api.nvim_win_set_buf(0, diagnostic_bufnr) 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, uri = fake_uri,
diagnostics = { diagnostics = {
make_error('Lower Diagnostic', 1, 1, 1, 1), 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, uri = fake_uri,
diagnostics = { diagnostics = {
make_warning('Farther Diagnostic', 4, 4, 4, 4), make_warning('Farther Diagnostic', 4, 4, 4, 4),
} }
}, 2 }, {client_id=2}
) )
vim.lsp.diagnostic.set_loclist() 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 end
describe('LSP', function() describe('LSP', function()
before_each(function()
clear_notrace()
end)
describe('server_name specified', function() describe('server_name specified', function()
before_each(function() before_each(function()
clear_notrace()
-- Run an instance of nvim on the file which contains our "scripts". -- Run an instance of nvim on the file which contains our "scripts".
-- Pass TEST_NAME to pick the script. -- Pass TEST_NAME to pick the script.
local test_name = "basic_init" local test_name = "basic_init"
@ -216,7 +219,7 @@ describe('LSP', function()
it('should run correctly', function() it('should run correctly', function()
local expected_handlers = { local expected_handlers = {
{NIL, "test", {}, 1}; {NIL, {}, {method="test", client_id=1}};
} }
test_rpc_server { test_rpc_server {
test_name = "basic_init"; test_name = "basic_init";
@ -241,7 +244,7 @@ describe('LSP', function()
it('should fail', function() it('should fail', function()
local expected_handlers = { local expected_handlers = {
{NIL, "test", {}, 1}; {NIL, {}, {method="test", client_id=1}};
} }
test_rpc_server { test_rpc_server {
test_name = "basic_init"; test_name = "basic_init";
@ -269,8 +272,8 @@ describe('LSP', function()
return return
end end
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1, NIL}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "test", {}, 1}; {NIL, {}, {method="test", client_id=1}};
} }
test_rpc_server { test_rpc_server {
test_name = "basic_init"; test_name = "basic_init";
@ -292,12 +295,12 @@ describe('LSP', function()
it('client should return settings via workspace/configuration handler', function() it('client should return settings via workspace/configuration handler', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "workspace/configuration", { items = { {NIL, { items = {
{ section = "testSetting1" }; { section = "testSetting1" };
{ section = "testSetting2" }; { section = "testSetting2" };
}}, 1}; }}, { method="workspace/configuration", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -309,9 +312,9 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'start' then if ctx.method == 'start' then
exec_lua([=[ exec_lua([=[
local client = vim.lsp.get_client_by_id(TEST_RPC_CLIENT_ID) local client = vim.lsp.get_client_by_id(TEST_RPC_CLIENT_ID)
client.config.settings = { client.config.settings = {
@ -319,35 +322,34 @@ describe('LSP', function()
testSetting2 = false; testSetting2 = false;
}]=]) }]=])
end end
if method == 'workspace/configuration' then if ctx.method == 'workspace/configuration' then
local result = exec_lua([=[ local server_result = exec_lua([=[
local method, params = ... local method, params = ...
return require'vim.lsp.handlers'['workspace/configuration'](err, method, params, TEST_RPC_CLIENT_ID)]=], method, params) return require'vim.lsp.handlers'['workspace/configuration'](err, params, {method=method, client_id=TEST_RPC_CLIENT_ID})]=], ctx.method, result)
client.notify('workspace/configuration', result) client.notify('workspace/configuration', server_result)
end end
if method == 'shutdown' then if ctx.method == 'shutdown' then
client.stop() client.stop()
end end
end; end;
} }
end) end)
it('workspace/configuration returns NIL per section if client was started without config.settings', function() 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') fake_lsp_server_setup('workspace/configuration no settings')
eq({ NIL, NIL, }, exec_lua [[ eq({ NIL, NIL, }, exec_lua [[
local params = { local result = {
items = { items = {
{section = 'foo'}, {section = 'foo'},
{section = 'bar'}, {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) end)
it('should verify capabilities sent', function() it('should verify capabilities sent', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
} }
test_rpc_server { test_rpc_server {
test_name = "basic_check_capabilities"; test_name = "basic_check_capabilities";
@ -371,7 +373,7 @@ describe('LSP', function()
it('client.supports_methods() should validate capabilities', function() it('client.supports_methods() should validate capabilities', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
} }
test_rpc_server { test_rpc_server {
test_name = "capabilities_for_client_supports_method"; 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() it('should call unsupported_method when trying to call an unsupported method', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
} }
test_rpc_server { test_rpc_server {
test_name = "capabilities_for_client_supports_method"; test_name = "capabilities_for_client_supports_method";
@ -413,7 +415,8 @@ describe('LSP', function()
exec_lua([=[ exec_lua([=[
BUFFER = vim.api.nvim_get_current_buf() BUFFER = vim.api.nvim_get_current_buf()
lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID) 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 } vim.lsp._last_lsp_handler = { err = err; method = method }
end end
vim.lsp._unsupported_method = function(method) 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() it('shouldn\'t call unsupported_method when no client and trying to call an unsupported method', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
} }
test_rpc_server { test_rpc_server {
test_name = "capabilities_for_client_supports_method"; 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() it('should not send didOpen if the buffer closes before init', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -511,9 +514,9 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -522,9 +525,9 @@ describe('LSP', function()
it('should check the body sent attaching before init', function() it('should check the body sent attaching before init', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -554,12 +557,12 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
if method == 'start' then if ctx.method == 'start' then
client.notify('finish') client.notify('finish')
end end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -568,9 +571,9 @@ describe('LSP', function()
it('should check the body sent attaching after init', function() it('should check the body sent attaching after init', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -597,12 +600,12 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
if method == 'start' then if ctx.method == 'start' then
client.notify('finish') client.notify('finish')
end end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -611,9 +614,9 @@ describe('LSP', function()
it('should check the body and didChange full', function() it('should check the body and didChange full', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -640,8 +643,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
if method == 'start' then if ctx.method == 'start' then
exec_lua [[ exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, { vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"boop"; "boop";
@ -649,8 +652,8 @@ describe('LSP', function()
]] ]]
client.notify('finish') client.notify('finish')
end end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -659,9 +662,9 @@ describe('LSP', function()
it('should check the body and didChange full with noeol', function() it('should check the body and didChange full with noeol', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -689,8 +692,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
if method == 'start' then if ctx.method == 'start' then
exec_lua [[ exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, { vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"boop"; "boop";
@ -698,8 +701,8 @@ describe('LSP', function()
]] ]]
client.notify('finish') client.notify('finish')
end end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -708,9 +711,9 @@ describe('LSP', function()
it('should check the body and didChange incremental', function() it('should check the body and didChange incremental', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -738,8 +741,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
if method == 'start' then if ctx.method == 'start' then
exec_lua [[ exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, { vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"123boop"; "123boop";
@ -747,8 +750,8 @@ describe('LSP', function()
]] ]]
client.notify('finish') client.notify('finish')
end end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -758,9 +761,9 @@ describe('LSP', function()
-- TODO(askhan) we don't support full for now, so we can disable these tests. -- 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() pending('should check the body and didChange incremental normal mode editing', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -787,13 +790,13 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
if method == 'start' then if ctx.method == 'start' then
helpers.command("normal! 1Go") helpers.command("normal! 1Go")
client.notify('finish') client.notify('finish')
end end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -802,9 +805,9 @@ describe('LSP', function()
it('should check the body and didChange full with 2 changes', function() it('should check the body and didChange full with 2 changes', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -831,8 +834,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
if method == 'start' then if ctx.method == 'start' then
exec_lua [[ exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, { vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"321"; "321";
@ -843,8 +846,8 @@ describe('LSP', function()
]] ]]
client.notify('finish') client.notify('finish')
end end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -853,9 +856,9 @@ describe('LSP', function()
it('should check the body and didChange full lifecycle', function() it('should check the body and didChange full lifecycle', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -882,8 +885,8 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result,ctx)
if method == 'start' then if ctx.method == 'start' then
exec_lua [[ exec_lua [[
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, { vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
"321"; "321";
@ -895,8 +898,8 @@ describe('LSP', function()
]] ]]
client.notify('finish') client.notify('finish')
end end
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -907,9 +910,9 @@ describe('LSP', function()
describe("parsing tests", function() describe("parsing tests", function()
it('should handle invalid content-length correctly', function() it('should handle invalid content-length correctly', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -924,22 +927,22 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
end; end;
} }
end) end)
it('should not trim vim.NIL from the end of a list', function() it('should not trim vim.NIL from the end of a list', function()
local expected_handlers = { local expected_handlers = {
{NIL, "shutdown", {}, 1}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, "finish", {}, 1}; {NIL, {}, {method="finish", client_id=1}};
{NIL, "workspace/executeCommand", { {NIL,{
arguments = { "EXTRACT_METHOD", {metadata = {}}, 3, 0, 6123, NIL }, arguments = { "EXTRACT_METHOD", {metadata = {}}, 3, 0, 6123, NIL },
command = "refactor.perform", command = "refactor.perform",
title = "EXTRACT_METHOD" title = "EXTRACT_METHOD"
}, 1}; }, {method="workspace/executeCommand", client_id=1}};
{NIL, "start", {}, 1}; {NIL, {}, {method="start", client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -963,9 +966,9 @@ describe('LSP', function()
eq(0, code, "exit code", fake_lsp_logfile) eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile) eq(0, signal, "exit signal", fake_lsp_logfile)
end; end;
on_handler = function(err, method, params, client_id) on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), {err, method, params, client_id}, "expected handler") eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if method == 'finish' then if ctx.method == 'finish' then
client.stop() client.stop()
end end
end; end;
@ -1088,6 +1091,30 @@ describe('LSP', function()
'å å ɧ 汉语 ↥ 🤦 🦄'; 'å å ɧ 汉语 ↥ 🤦 🦄';
}, buf_lines(1)) }, buf_lines(1))
end) 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() it('applies non-ASCII characters edits', function()
local edits = { local edits = {
make_edit(4, 3, 4, 4, {"ä"}); make_edit(4, 3, 4, 4, {"ä"});
@ -1116,6 +1143,86 @@ describe('LSP', function()
}, buf_lines(1)) }, buf_lines(1))
end) 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() 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() it('applies edits when the last linebreak is considered a new line', function()
local edits = { local edits = {
@ -1223,7 +1330,7 @@ describe('LSP', function()
label = nil; label = nil;
edit = {}; edit = {};
} }
return vim.lsp.handlers['workspace/applyEdit'](nil, nil, apply_edit) return vim.lsp.handlers['workspace/applyEdit'](nil, apply_edit)
]]) ]])
end) end)
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={} }, { label='foocar', sortText="h", insertText='foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', insertTextFormat=2, textEdit={} },
-- nested snippet tokens -- nested snippet tokens
{ label='foocar', sortText="i", insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', insertTextFormat=2, textEdit={} }, { 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 -- 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 completion_list_items = {items=completion_list}
local expected = { 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', 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 = '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 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(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(${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()', 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)) 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() describe('vim.lsp.buf.outgoing_calls', function()
it('does nothing for an empty response', function() it('does nothing for an empty response', function()
local qflist_count = exec_lua([=[ local qflist_count = exec_lua([=[
require'vim.lsp.handlers'['callHierarchy/outgoingCalls']() require'vim.lsp.handlers'['callHierarchy/outgoingCalls'](nil, nil, {}, nil)
return #vim.fn.getqflist() return #vim.fn.getqflist()
]=]) ]=])
eq(0, qflist_count) eq(0, qflist_count)
@ -2020,7 +2130,7 @@ describe('LSP', function()
} }
} } } }
local handler = require'vim.lsp.handlers'['callHierarchy/outgoingCalls'] local handler = require'vim.lsp.handlers'['callHierarchy/outgoingCalls']
handler(nil, nil, rust_analyzer_response) handler(nil, rust_analyzer_response, {})
return vim.fn.getqflist() return vim.fn.getqflist()
]=]) ]=])
@ -2044,7 +2154,7 @@ describe('LSP', function()
describe('vim.lsp.buf.incoming_calls', function() describe('vim.lsp.buf.incoming_calls', function()
it('does nothing for an empty response', function() it('does nothing for an empty response', function()
local qflist_count = exec_lua([=[ local qflist_count = exec_lua([=[
require'vim.lsp.handlers'['callHierarchy/incomingCalls']() require'vim.lsp.handlers'['callHierarchy/incomingCalls'](nil, nil, {})
return #vim.fn.getqflist() return #vim.fn.getqflist()
]=]) ]=])
eq(0, qflist_count) eq(0, qflist_count)
@ -2092,7 +2202,7 @@ describe('LSP', function()
} } } }
local handler = require'vim.lsp.handlers'['callHierarchy/incomingCalls'] local handler = require'vim.lsp.handlers'['callHierarchy/incomingCalls']
handler(nil, nil, rust_analyzer_response) handler(nil, rust_analyzer_response, {})
return vim.fn.getqflist() return vim.fn.getqflist()
]=]) ]=])

View File

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

View File

@ -697,4 +697,50 @@ end]]
| |
]]} ]]}
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) end)

View File

@ -152,6 +152,132 @@ describe('float window', function()
eq(10, width) eq(10, width)
end) 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 function with_ext_multigrid(multigrid)
local screen local screen
before_each(function() before_each(function()
@ -620,6 +746,134 @@ describe('float window', function()
end end
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() it('can have border', function()
local buf = meths.create_buf(false, false) local buf = meths.create_buf(false, false)
meths.buf_set_lines(buf, 0, -1, true, {' halloj! ', 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')) eq(eval('v:null'), eval('v:exiting'))
end) 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) end)
describe(":substitute, 'inccommand' with a failing expression", function() describe(":substitute, 'inccommand' with a failing expression", function()