aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.luacov3
-rw-r--r--CMakeLists.txt6
-rw-r--r--Makefile12
-rw-r--r--appveyor.yml2
-rwxr-xr-xci/before_install.sh8
-rw-r--r--ci/build.ps144
-rwxr-xr-xci/install.sh4
-rwxr-xr-xci/run_lint.sh4
-rw-r--r--runtime/autoload/health/provider.vim3
-rw-r--r--runtime/autoload/lsp.vim45
-rw-r--r--runtime/autoload/man.vim18
-rw-r--r--runtime/autoload/provider/pythonx.vim9
-rw-r--r--runtime/autoload/spellfile.vim7
-rw-r--r--runtime/doc/api.txt221
-rw-r--r--runtime/doc/develop.txt81
-rw-r--r--runtime/doc/eval.txt21
-rw-r--r--runtime/doc/help.txt2
-rw-r--r--runtime/doc/if_lua.txt772
-rw-r--r--runtime/doc/index.txt5
-rw-r--r--runtime/doc/lsp.txt662
-rw-r--r--runtime/doc/lua.txt994
-rw-r--r--runtime/doc/motion.txt22
-rw-r--r--runtime/doc/msgpack_rpc.txt7
-rw-r--r--runtime/doc/options.txt87
-rw-r--r--runtime/doc/quickfix.txt30
-rw-r--r--runtime/doc/quickref.txt1
-rw-r--r--runtime/doc/usr_25.txt9
-rw-r--r--runtime/doc/vim_diff.txt22
-rw-r--r--runtime/lua/vim/inspect.lua2
-rw-r--r--runtime/lua/vim/lsp.lua1055
-rw-r--r--runtime/lua/vim/lsp/builtin_callbacks.lua296
-rw-r--r--runtime/lua/vim/lsp/log.lua95
-rw-r--r--runtime/lua/vim/lsp/protocol.lua936
-rw-r--r--runtime/lua/vim/lsp/rpc.lua451
-rw-r--r--runtime/lua/vim/lsp/util.lua557
-rw-r--r--runtime/lua/vim/shared.lua305
-rw-r--r--runtime/lua/vim/uri.lua89
-rw-r--r--runtime/pack/dist/opt/termdebug/plugin/termdebug.vim1
-rw-r--r--runtime/tutor/en/vim-01-beginner.tutor40
-rw-r--r--runtime/tutor/en/vim-01-beginner.tutor.json84
-rwxr-xr-xscripts/gen_vimdoc.py45
-rw-r--r--scripts/lua2dox.lua34
-rwxr-xr-xscripts/shadacat.py2
-rwxr-xr-xscripts/update_version_stamp.lua7
-rwxr-xr-xscripts/vim-patch.sh10
-rw-r--r--src/nvim/api/buffer.c349
-rw-r--r--src/nvim/api/private/helpers.c126
-rw-r--r--src/nvim/api/private/helpers.h14
-rw-r--r--src/nvim/api/vim.c24
-rw-r--r--src/nvim/auevents.lua6
-rw-r--r--src/nvim/buffer.c3
-rw-r--r--src/nvim/buffer_defs.h29
-rw-r--r--src/nvim/change.c18
-rw-r--r--src/nvim/diff.c32
-rw-r--r--src/nvim/diff.h7
-rw-r--r--src/nvim/edit.c53
-rw-r--r--src/nvim/eval.c173
-rw-r--r--src/nvim/eval.h1
-rw-r--r--src/nvim/ex_cmds.c306
-rw-r--r--src/nvim/ex_cmds.lua24
-rw-r--r--src/nvim/ex_docmd.c777
-rw-r--r--src/nvim/ex_getln.c9
-rw-r--r--src/nvim/fileio.c51
-rw-r--r--src/nvim/fold.c5
-rw-r--r--src/nvim/getchar.c1
-rw-r--r--src/nvim/globals.h6
-rw-r--r--src/nvim/highlight.c19
-rw-r--r--src/nvim/highlight_defs.h2
-rw-r--r--src/nvim/lib/kbtree.h6
-rw-r--r--src/nvim/lua/converter.c54
-rw-r--r--src/nvim/lua/executor.c254
-rw-r--r--src/nvim/lua/executor.h2
-rw-r--r--src/nvim/lua/vim.lua39
-rw-r--r--src/nvim/main.c1
-rw-r--r--src/nvim/mark.c31
-rw-r--r--src/nvim/mark_extended.c1135
-rw-r--r--src/nvim/mark_extended.h282
-rw-r--r--src/nvim/mark_extended_defs.h54
-rw-r--r--src/nvim/memline.c3
-rw-r--r--src/nvim/memory.c2
-rw-r--r--src/nvim/misc1.c3
-rw-r--r--src/nvim/normal.c40
-rw-r--r--src/nvim/ops.c144
-rw-r--r--src/nvim/options.lua2
-rw-r--r--src/nvim/os/tty.c1
-rw-r--r--src/nvim/po/check.vim11
-rw-r--r--src/nvim/pos.h4
-rw-r--r--src/nvim/quickfix.c3366
-rw-r--r--src/nvim/screen.c5
-rw-r--r--src/nvim/search.c90
-rw-r--r--src/nvim/search.h9
-rw-r--r--src/nvim/spell.c13
-rw-r--r--src/nvim/spellfile.c17
-rw-r--r--src/nvim/syntax.c12
-rw-r--r--src/nvim/tag.c26
-rw-r--r--src/nvim/terminal.c19
-rw-r--r--src/nvim/testdir/shared.vim12
-rw-r--r--src/nvim/testdir/test_alot.vim1
-rw-r--r--src/nvim/testdir/test_backup.vim58
-rw-r--r--src/nvim/testdir/test_diffmode.vim25
-rw-r--r--src/nvim/testdir/test_functions.vim7
-rw-r--r--src/nvim/testdir/test_gf.vim25
-rw-r--r--src/nvim/testdir/test_gn.vim27
-rw-r--r--src/nvim/testdir/test_join.vim21
-rw-r--r--src/nvim/testdir/test_let.vim13
-rw-r--r--src/nvim/testdir/test_normal.vim24
-rw-r--r--src/nvim/testdir/test_quickfix.vim380
-rw-r--r--src/nvim/testdir/test_spell.vim37
-rw-r--r--src/nvim/testdir/test_substitute.vim10
-rw-r--r--src/nvim/testdir/test_tagjump.vim24
-rw-r--r--src/nvim/testdir/test_textobjects.vim3
-rw-r--r--src/nvim/testdir/test_vimscript.vim2
-rw-r--r--src/nvim/testdir/test_virtualedit.vim9
-rw-r--r--src/nvim/tui/input.c8
-rw-r--r--src/nvim/tui/tui.c41
-rw-r--r--src/nvim/undo.c77
-rw-r--r--src/nvim/undo_defs.h16
-rw-r--r--src/nvim/version.c32
-rw-r--r--src/nvim/window.c24
-rw-r--r--test/functional/api/mark_extended_spec.lua1375
-rw-r--r--test/functional/core/fileio_spec.lua25
-rw-r--r--test/functional/core/main_spec.lua2
-rw-r--r--test/functional/fixtures/lsp-test-rpc-server.lua424
-rw-r--r--test/functional/legacy/022_line_ending_spec.lua25
-rw-r--r--test/functional/legacy/041_writing_and_reading_hundred_kbyte_spec.lua43
-rw-r--r--test/functional/legacy/077_mf_hash_grow_spec.lua52
-rw-r--r--test/functional/legacy/084_curswant_spec.lua49
-rw-r--r--test/functional/legacy/098_scrollbind_spec.lua48
-rw-r--r--test/functional/legacy/104_let_assignment_spec.lua54
-rw-r--r--test/functional/lua/api_spec.lua30
-rw-r--r--test/functional/lua/commands_spec.lua99
-rw-r--r--test/functional/lua/luaeval_spec.lua96
-rw-r--r--test/functional/lua/overrides_spec.lua27
-rw-r--r--test/functional/lua/treesitter_spec.lua4
-rw-r--r--test/functional/lua/uri_spec.lua107
-rw-r--r--test/functional/lua/utility_functions_spec.lua291
-rw-r--r--test/functional/lua/vim_spec.lua552
-rw-r--r--test/functional/plugin/lsp/lsp_spec.lua634
-rw-r--r--test/functional/terminal/highlight_spec.lua57
-rw-r--r--test/functional/terminal/scrollback_spec.lua2
-rw-r--r--test/functional/terminal/tui_spec.lua99
-rw-r--r--test/functional/ui/bufhl_spec.lua16
-rw-r--r--test/functional/ui/hlstate_spec.lua10
-rw-r--r--test/functional/ui/inccommand_spec.lua100
-rw-r--r--test/functional/ui/messages_spec.lua151
-rw-r--r--test/functional/ui/screen.lua22
-rw-r--r--test/helpers.lua13
-rw-r--r--third-party/CMakeLists.txt8
-rw-r--r--third-party/cmake/BuildLuajit.cmake7
-rw-r--r--third-party/cmake/BuildLuarocks.cmake26
151 files changed, 16258 insertions, 3837 deletions
diff --git a/.gitignore b/.gitignore
index b7f710d1d7..6e8cbd0321 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ compile_commands.json
/dist/
/.deps/
/tmp/
+/.clangd/
*.mo
.*.sw?
diff --git a/.luacov b/.luacov
index 422783b858..f8eb28e3f7 100644
--- a/.luacov
+++ b/.luacov
@@ -14,6 +14,9 @@ return {
-- Relative (non-hidden) paths.
'^[^/\\.]',
},
+ modules = {
+ ['vim'] = 'runtime/lua/vim/shared.lua'
+ },
}
-- vim: ft=lua tw=80 sw=2 et
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 790fc9fb41..d25cd89342 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -125,9 +125,9 @@ set(NVIM_VERSION_PATCH 0)
set(NVIM_VERSION_PRERELEASE "-dev") # for package maintainers
# API level
-set(NVIM_API_LEVEL 6) # Bump this after any API change.
+set(NVIM_API_LEVEL 7) # Bump this after any API change.
set(NVIM_API_LEVEL_COMPAT 0) # Adjust this after a _breaking_ API change.
-set(NVIM_API_PRERELEASE false)
+set(NVIM_API_PRERELEASE true)
set(NVIM_VERSION_BUILD_TYPE "${CMAKE_BUILD_TYPE}")
# NVIM_VERSION_CFLAGS set further below.
@@ -303,8 +303,10 @@ if(UNIX)
if(HAS_FSTACK_PROTECTOR_STRONG_FLAG)
add_compile_options(-fstack-protector-strong)
+ link_libraries(-fstack-protector-strong)
elseif(HAS_FSTACK_PROTECTOR_FLAG)
add_compile_options(-fstack-protector --param ssp-buffer-size=4)
+ link_libraries(-fstack-protector --param ssp-buffer-size=4)
endif()
endif()
diff --git a/Makefile b/Makefile
index f5b9459b0c..3952c37fd4 100644
--- a/Makefile
+++ b/Makefile
@@ -138,6 +138,14 @@ functionaltest-lua: | nvim
lualint: | build/.ran-cmake deps
$(BUILD_CMD) -C build lualint
+shlint:
+ @shellcheck --version | head -n 2
+ shellcheck scripts/vim-patch.sh
+
+_opt_shlint:
+ @command -v shellcheck && { $(MAKE) shlint; exit $$?; } \
+ || echo "SKIP: shlint (shellcheck not found)"
+
pylint:
flake8 contrib/ scripts/ src/ test/
@@ -188,7 +196,7 @@ appimage:
appimage-%:
bash scripts/genappimage.sh $*
-lint: check-single-includes clint lualint _opt_pylint
+lint: check-single-includes clint lualint _opt_pylint _opt_shlint
# Generic pattern rules, allowing for `make build/bin/nvim` etc.
# Does not work with "Unix Makefiles".
@@ -200,4 +208,4 @@ $(DEPS_BUILD_DIR)/%:
$(BUILD_CMD) -C $(DEPS_BUILD_DIR) $(patsubst $(DEPS_BUILD_DIR)/%,%,$@)
endif
-.PHONY: test lualint pylint functionaltest unittest lint clint clean distclean nvim libnvim cmake deps install appimage checkprefix
+.PHONY: test lualint pylint shlint functionaltest unittest lint clint clean distclean nvim libnvim cmake deps install appimage checkprefix
diff --git a/appveyor.yml b/appveyor.yml
index bb7bb1c4e9..7e2aef345b 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -24,7 +24,7 @@ matrix:
fast_finish: true
install: []
before_build:
-- ps: Install-Product node 8
+- ps: Install-Product node 10
build_script:
- powershell ci\build.ps1
after_build:
diff --git a/ci/before_install.sh b/ci/before_install.sh
index 283605e113..5810bec71a 100755
--- a/ci/before_install.sh
+++ b/ci/before_install.sh
@@ -3,10 +3,6 @@
set -e
set -o pipefail
-if [[ "${CI_TARGET}" == lint ]]; then
- exit
-fi
-
echo 'Python info:'
(
set -x
@@ -47,8 +43,8 @@ if [[ "${TRAVIS_OS_NAME}" == osx ]] || [ ! -f ~/.nvm/nvm.sh ]; then
fi
source ~/.nvm/nvm.sh
-nvm install --lts
-nvm use --lts
+nvm install 10
+nvm use 10
if [[ -n "$CMAKE_URL" ]]; then
echo "Installing custom CMake: $CMAKE_URL"
diff --git a/ci/build.ps1 b/ci/build.ps1
index 6d91b97aed..244b4766b2 100644
--- a/ci/build.ps1
+++ b/ci/build.ps1
@@ -1,10 +1,11 @@
-$ErrorActionPreference = 'stop'
-Set-PSDebug -Strict -Trace 1
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$ProgressPreference = 'SilentlyContinue'
$isPullRequest = ($env:APPVEYOR_PULL_REQUEST_HEAD_COMMIT -ne $null)
$env:CONFIGURATION -match '^(?<compiler>\w+)_(?<bits>32|64)(?:-(?<option>\w+))?$'
$compiler = $Matches.compiler
-$compileOption = $Matches.option
+$compileOption = if ($Matches -contains 'option') {$Matches.option} else {''}
$bits = $Matches.bits
$cmakeBuildType = $(if ($env:CMAKE_BUILD_TYPE -ne $null) {$env:CMAKE_BUILD_TYPE} else {'RelWithDebInfo'});
$buildDir = [System.IO.Path]::GetFullPath("$(pwd)")
@@ -23,7 +24,6 @@ $uploadToCodeCov = $false
function exitIfFailed() {
if ($LastExitCode -ne 0) {
- Set-PSDebug -Off
exit $LastExitCode
}
}
@@ -98,27 +98,26 @@ npm.cmd install -g neovim
Get-Command -CommandType Application neovim-node-host.cmd
npm.cmd link neovim
-#npm.cmd install -g tree-sitter-cli
-#npm.cmd link tree-sitter-cli
-mkdir c:\treesitter
-$env:TREE_SITTER_DIR = "c:\treesitter"
-#$env:PATH = "c:\treesitter;$env:PATH"
-$client = new-object System.Net.WebClient
-cd c:\treesitter
+$env:TREE_SITTER_DIR = $env:USERPROFILE + "\tree-sitter-build"
+mkdir "$env:TREE_SITTER_DIR\bin"
-if ($bits -eq 32) {
- $client.DownloadFile("https://github.com/tree-sitter/tree-sitter/releases/download/0.15.9/tree-sitter-windows-x86.gz", "c:\treesitter\tree-sitter-cli.gz")
-}
-elseif ($bits -eq 64) {
- $client.DownloadFile("https://github.com/tree-sitter/tree-sitter/releases/download/0.15.9/tree-sitter-windows-x64.gz", "c:\treesitter\tree-sitter-cli.gz")
+$xbits = if ($bits -eq '32') {'x86'} else {'x64'}
+Invoke-WebRequest -UseBasicParsing -Uri "https://github.com/tree-sitter/tree-sitter/releases/download/0.15.9/tree-sitter-windows-$xbits.gz" -OutFile tree-sitter.exe.gz
+C:\msys64\usr\bin\gzip -d tree-sitter.exe.gz
+
+Invoke-WebRequest -UseBasicParsing -Uri "https://codeload.github.com/tree-sitter/tree-sitter-c/zip/v0.15.2" -OutFile tree_sitter_c.zip
+Expand-Archive .\tree_sitter_c.zip -DestinationPath .
+cd tree-sitter-c-0.15.2
+..\tree-sitter.exe test
+if (-Not (Test-Path -PathType Leaf "$env:TREE_SITTER_DIR\bin\c.dll")) {
+ exit 1
}
-python -c "import gzip, shutil; f1,f2 = gzip.open('tree-sitter-cli.gz', 'rb'), open('tree-sitter.exe', 'wb'); shutil.copyfileobj(f1, f2); f2.close()"
-$client.DownloadFile("https://codeload.github.com/tree-sitter/tree-sitter-c/zip/v0.15.2","c:\treesitter\tree_sitter_c.zip")
-Expand-Archive c:\treesitter\tree_sitter_c.zip -DestinationPath c:\treesitter\
-cd c:\treesitter\tree-sitter-c-0.15.2
-c:\treesitter\tree-sitter.exe test
+if ($compiler -eq 'MSVC') {
+ # Required for LuaRocks (https://github.com/luarocks/luarocks/issues/1039#issuecomment-507296940).
+ $env:VCINSTALLDIR = "C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Tools/MSVC/14.16.27023/"
+}
function convertToCmakeArgs($vars) {
return $vars.GetEnumerator() | foreach { "-D$($_.Key)=$($_.Value)" }
@@ -146,8 +145,6 @@ if ($env:USE_LUACOV -eq 1) {
# Functional tests
# The $LastExitCode from MSBuild can't be trusted
$failed = $false
-# Temporarily turn off tracing to reduce log file output
-Set-PSDebug -Off
cmake --build . --config $cmakeBuildType --target functionaltest -- $cmakeGeneratorArgs 2>&1 |
foreach { $failed = $failed -or
$_ -match 'functional tests failed with error'; $_ }
@@ -161,7 +158,6 @@ if ($uploadToCodecov) {
if ($failed) {
exit $LastExitCode
}
-Set-PSDebug -Strict -Trace 1
# Old tests
# Add MSYS to path, required for e.g. `find` used in test scripts.
diff --git a/ci/install.sh b/ci/install.sh
index 24a4bd7450..a6cd955da5 100755
--- a/ci/install.sh
+++ b/ci/install.sh
@@ -4,7 +4,7 @@ set -e
set -o pipefail
if [[ "${CI_TARGET}" == lint ]]; then
- python -m pip -q install --user --upgrade flake8
+ python3 -m pip -q install --user --upgrade flake8
exit
fi
@@ -31,7 +31,6 @@ echo "Install tree-sitter npm package"
# https://github.com/tree-sitter/tree-sitter/commit/e14e285a1087264a8c74a7c62fcaecc49db9d904
# If queries added to tree-sitter-c, we can use latest tree-sitter-cli
npm install -g tree-sitter-cli@v0.15.9
-npm link tree-sitter-cli
echo "Install tree-sitter c parser"
curl "https://codeload.github.com/tree-sitter/tree-sitter-c/tar.gz/v0.15.2" -o tree_sitter_c.tar.gz
@@ -48,3 +47,4 @@ else
cd src/
gcc -m32 -o "$TREE_SITTER_DIR/bin/c.so" -shared parser.c -I.
fi
+test -f "$TREE_SITTER_DIR/bin/c.so"
diff --git a/ci/run_lint.sh b/ci/run_lint.sh
index 88af163e80..8373a3cb36 100755
--- a/ci/run_lint.sh
+++ b/ci/run_lint.sh
@@ -20,6 +20,10 @@ enter_suite 'pylint'
run_test 'make pylint' pylint
exit_suite --continue
+enter_suite 'shlint'
+run_test 'make shlint' shlint
+exit_suite --continue
+
enter_suite single-includes
CLICOLOR_FORCE=1 run_test_wd \
--allow-hang \
diff --git a/runtime/autoload/health/provider.vim b/runtime/autoload/health/provider.vim
index c750a954fa..ad7a614ff5 100644
--- a/runtime/autoload/health/provider.vim
+++ b/runtime/autoload/health/provider.vim
@@ -202,7 +202,8 @@ function! s:version_info(python) abort
let nvim_path = s:trim(s:system([
\ a:python, '-c',
- \ 'import sys; sys.path.remove(""); ' .
+ \ 'import sys; ' .
+ \ 'sys.path = list(filter(lambda x: x != "", sys.path)); ' .
\ 'import neovim; print(neovim.__file__)']))
if s:shell_error || empty(nvim_path)
return [python_version, 'unable to load neovim Python module', pypi_version,
diff --git a/runtime/autoload/lsp.vim b/runtime/autoload/lsp.vim
new file mode 100644
index 0000000000..4c8f8b396a
--- /dev/null
+++ b/runtime/autoload/lsp.vim
@@ -0,0 +1,45 @@
+function! lsp#add_filetype_config(config) abort
+ call luaeval('vim.lsp.add_filetype_config(_A)', a:config)
+endfunction
+
+function! lsp#set_log_level(level) abort
+ call luaeval('vim.lsp.set_log_level(_A)', a:level)
+endfunction
+
+function! lsp#get_log_path() abort
+ return luaeval('vim.lsp.get_log_path()')
+endfunction
+
+function! lsp#omnifunc(findstart, base) abort
+ return luaeval("vim.lsp.omnifunc(_A[1], _A[2])", [a:findstart, a:base])
+endfunction
+
+function! lsp#text_document_hover() abort
+ lua vim.lsp.buf_request(nil, 'textDocument/hover', vim.lsp.protocol.make_text_document_position_params())
+ return ''
+endfunction
+
+function! lsp#text_document_declaration() abort
+ lua vim.lsp.buf_request(nil, 'textDocument/declaration', vim.lsp.protocol.make_text_document_position_params())
+ return ''
+endfunction
+
+function! lsp#text_document_definition() abort
+ lua vim.lsp.buf_request(nil, 'textDocument/definition', vim.lsp.protocol.make_text_document_position_params())
+ return ''
+endfunction
+
+function! lsp#text_document_signature_help() abort
+ lua vim.lsp.buf_request(nil, 'textDocument/signatureHelp', vim.lsp.protocol.make_text_document_position_params())
+ return ''
+endfunction
+
+function! lsp#text_document_type_definition() abort
+ lua vim.lsp.buf_request(nil, 'textDocument/typeDefinition', vim.lsp.protocol.make_text_document_position_params())
+ return ''
+endfunction
+
+function! lsp#text_document_implementation() abort
+ lua vim.lsp.buf_request(nil, 'textDocument/implementation', vim.lsp.protocol.make_text_document_position_params())
+ return ''
+endfunction
diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim
index 6c74617aca..36f42c0003 100644
--- a/runtime/autoload/man.vim
+++ b/runtime/autoload/man.vim
@@ -254,20 +254,16 @@ function! s:extract_sect_and_name_path(path) abort
endfunction
function! s:find_man() abort
- if &filetype ==# 'man'
- return 1
- elseif winnr('$') ==# 1
- return 0
- endif
- let thiswin = winnr()
- while 1
- wincmd w
- if &filetype ==# 'man'
+ let l:win = 1
+ while l:win <= winnr('$')
+ let l:buf = winbufnr(l:win)
+ if getbufvar(l:buf, '&filetype', '') ==# 'man'
+ execute l:win.'wincmd w'
return 1
- elseif thiswin ==# winnr()
- return 0
endif
+ let l:win += 1
endwhile
+ return 0
endfunction
function! s:error(msg) abort
diff --git a/runtime/autoload/provider/pythonx.vim b/runtime/autoload/provider/pythonx.vim
index 6ce7165467..23e7ff8f64 100644
--- a/runtime/autoload/provider/pythonx.vim
+++ b/runtime/autoload/provider/pythonx.vim
@@ -10,7 +10,8 @@ function! provider#pythonx#Require(host) abort
" Python host arguments
let prog = (ver == '2' ? provider#python#Prog() : provider#python3#Prog())
- let args = [prog, '-c', 'import sys; sys.path.remove(""); import neovim; neovim.start_host()']
+ let args = [prog, '-c', 'import sys; sys.path = list(filter(lambda x: x != "", sys.path)); import neovim; neovim.start_host()']
+
" Collect registered Python plugins into args
let python_plugins = remote#host#PluginsForHost(a:host.name)
@@ -28,8 +29,8 @@ endfunction
function! s:get_python_candidates(major_version) abort
return {
\ 2: ['python2', 'python2.7', 'python2.6', 'python'],
- \ 3: ['python3', 'python3.7', 'python3.6', 'python3.5', 'python3.4', 'python3.3',
- \ 'python']
+ \ 3: ['python3', 'python3.8', 'python3.7', 'python3.6', 'python3.5',
+ \ 'python3.4', 'python3.3', 'python']
\ }[a:major_version]
endfunction
@@ -66,7 +67,7 @@ endfunction
function! s:import_module(prog, module) abort
let prog_version = system([a:prog, '-c' , printf(
\ 'import sys; ' .
- \ 'sys.path.remove(""); ' .
+ \ 'sys.path = list(filter(lambda x: x != "", sys.path)); ' .
\ 'sys.stdout.write(str(sys.version_info[0]) + "." + str(sys.version_info[1])); ' .
\ 'import pkgutil; ' .
\ 'exit(2*int(pkgutil.get_loader("%s") is None))',
diff --git a/runtime/autoload/spellfile.vim b/runtime/autoload/spellfile.vim
index c0ef51cdfe..d098902305 100644
--- a/runtime/autoload/spellfile.vim
+++ b/runtime/autoload/spellfile.vim
@@ -13,6 +13,13 @@ let s:spellfile_URL = '' " Start with nothing so that s:donedict is reset.
" This function is used for the spellfile plugin.
function! spellfile#LoadFile(lang)
+ " Check for sandbox/modeline. #11359
+ try
+ :!
+ catch /\<E12\>/
+ throw 'Cannot download spellfile in sandbox/modeline. Try ":set spell" from the cmdline.'
+ endtry
+
" If the netrw plugin isn't loaded we silently skip everything.
if !exists(":Nread")
if &verbose
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt
index 98dd330b48..57a72e6173 100644
--- a/runtime/doc/api.txt
+++ b/runtime/doc/api.txt
@@ -439,6 +439,43 @@ Example: create a float with scratch buffer: >
>
==============================================================================
+Extended marks *api-extended-marks*
+
+Extended marks (extmarks) represent buffer annotations that track text changes
+in the buffer. They could be used to represent cursors, folds, misspelled
+words, and anything else that needs to track a logical location in the buffer
+over time.
+
+Example:
+
+We will set an extmark at the first row and third column. |api-indexing| is
+zero-indexed, so we use row=0 and column=2. Passing id=0 creates a new mark
+and returns the id: >
+
+ let g:mark_ns = nvim_create_namespace('myplugin')
+ let g:mark_id = nvim_buf_set_extmark(0, g:mark_ns, 0, 0, 2, {})
+
+We can get a mark by its id: >
+
+ echo nvim_buf_get_extmark_by_id(0, g:mark_ns, g:mark_id)
+ => [0, 2]
+
+We can get all marks in a buffer for our namespace (or by a range): >
+
+ echo nvim_buf_get_extmarks(0, g:mark_ns, 0, -1, {})
+ => [[1, 0, 2]]
+
+Deleting all text surrounding an extmark does not remove the extmark. To
+remove an extmark use |nvim_buf_del_extmark()|.
+
+Namespaces allow your plugin to manage only its own extmarks, ignoring those
+created by another plugin.
+
+Extmark positions changed by an edit will be restored on undo/redo. Creating
+and deleting extmarks is not a buffer change, thus new undo states are not
+created for extmark changes.
+
+==============================================================================
Global Functions *api-global*
nvim_command({command}) *nvim_command()*
@@ -850,10 +887,10 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()*
{enter} Enter the window (make it the current window)
{config} Map defining the window configuration. Keys:
• `relative` : Sets the window layout to "floating", placed
- at (row,col) coordinates relative to one of:
+ at (row,col) coordinates relative to:
• "editor" The global editor grid
• "win" Window given by the `win` field, or
- current window by default.
+ current window.
• "cursor" Cursor position in current window.
• `win` : |window-ID| for relative="win".
@@ -1476,45 +1513,73 @@ nvim_buf_line_count({buffer}) *nvim_buf_line_count()*
Line count, or 0 for unloaded buffer. |api-buffer|
nvim_buf_attach({buffer}, {send_buffer}, {opts}) *nvim_buf_attach()*
- Activates buffer-update events on a channel, or as lua
+ Activates buffer-update events on a channel, or as Lua
callbacks.
+ Example (Lua): capture buffer updates in a global `events` variable (use "print(vim.inspect(events))" to see its
+ contents): >
+ events = {}
+ vim.api.nvim_buf_attach(0, false, {
+ on_lines=function(...) table.insert(events, {...}) end})
+<
+
Parameters: ~
{buffer} Buffer handle, or 0 for current buffer
- {send_buffer} Set to true if the initial notification
- should contain the whole buffer. If so, the
- first notification will be a
- `nvim_buf_lines_event` . Otherwise, the
- first notification will be a
- `nvim_buf_changedtick_event` . Not used for
- lua callbacks.
+ {send_buffer} True if the initial notification should
+ contain the whole buffer: first
+ notification will be `nvim_buf_lines_event`
+ . Else the first notification will be
+ `nvim_buf_changedtick_event` . Not for Lua
+ callbacks.
{opts} Optional parameters.
- • `on_lines` : lua callback received on
- change.
- • `on_changedtick` : lua callback received
- on changedtick increment without text
- change.
- • `utf_sizes` : include UTF-32 and UTF-16
- size of the replaced region. See
- |api-buffer-updates-lua| for more
- information
+ • on_lines: Lua callback invoked on change.
+ Return `true` to detach. Args:
+ • buffer handle
+ • b:changedtick
+ • first line that changed (zero-indexed)
+ • last line that was changed
+ • last line in the updated range
+ • byte count of previous contents
+ • deleted_codepoints (if `utf_sizes` is
+ true)
+ • deleted_codeunits (if `utf_sizes` is
+ true)
+
+ • on_changedtick: Lua callback invoked on
+ changedtick increment without text
+ change. Args:
+ • buffer handle
+ • b:changedtick
+
+ • on_detach: Lua callback invoked on
+ detach. Args:
+ • buffer handle
+
+ • utf_sizes: include UTF-32 and UTF-16 size
+ of the replaced region, as args to
+ `on_lines` .
+
+ Return: ~
+ False if attach failed (invalid parameter, or buffer isn't
+ loaded); otherwise True. TODO: LUA_API_NO_EVAL
- Return: ~
- False when updates couldn't be enabled because the buffer
- isn't loaded or `opts` contained an invalid key; otherwise
- True. TODO: LUA_API_NO_EVAL
+ See also: ~
+ |nvim_buf_detach()|
+ |api-buffer-updates-lua|
nvim_buf_detach({buffer}) *nvim_buf_detach()*
Deactivates buffer-update events on the channel.
- For Lua callbacks see |api-lua-detach|.
-
Parameters: ~
{buffer} Buffer handle, or 0 for current buffer
Return: ~
- False when updates couldn't be disabled because the buffer
- isn't loaded; otherwise True.
+ False if detach failed (because the buffer isn't loaded);
+ otherwise True.
+
+ See also: ~
+ |nvim_buf_attach()|
+ |api-lua-detach| for detaching Lua callbacks
*nvim_buf_get_lines()*
nvim_buf_get_lines({buffer}, {start}, {end}, {strict_indexing})
@@ -1726,6 +1791,87 @@ nvim_buf_get_mark({buffer}, {name}) *nvim_buf_get_mark()*
Return: ~
(row, col) tuple
+ *nvim_buf_get_extmark_by_id()*
+nvim_buf_get_extmark_by_id({buffer}, {ns_id}, {id})
+ Returns position for a given extmark id
+
+ Parameters: ~
+ {buffer} The buffer handle
+ {namespace} a identifier returned previously with
+ nvim_create_namespace
+ {id} the extmark id
+
+ Return: ~
+ (row, col) tuple or empty list () if extmark id was absent
+
+ *nvim_buf_get_extmarks()*
+nvim_buf_get_extmarks({buffer}, {ns_id}, {start}, {end}, {opts})
+ List extmarks in a range (inclusive)
+
+ range ends can be specified as (row, col) tuples, as well as
+ extmark ids in the same namespace. In addition, 0 and -1 works
+ as shorthands for (0,0) and (-1,-1) respectively, so that all
+ marks in the buffer can be queried as:
+
+ all_marks = nvim_buf_get_extmarks(0, my_ns, 0, -1, {})
+
+ If end is a lower position than start, then the range will be
+ traversed backwards. This is mostly useful with limited
+ amount, to be able to get the first marks prior to a given
+ position.
+
+ Parameters: ~
+ {buffer} The buffer handle
+ {ns_id} An id returned previously from
+ nvim_create_namespace
+ {start} One of: extmark id, (row, col) or 0, -1 for
+ buffer ends
+ {end} One of: extmark id, (row, col) or 0, -1 for
+ buffer ends
+ {opts} additional options. Supports the keys:
+ • amount: Maximum number of marks to return
+
+ Return: ~
+ [[extmark_id, row, col], ...]
+
+ *nvim_buf_set_extmark()*
+nvim_buf_set_extmark({buffer}, {ns_id}, {id}, {line}, {col}, {opts})
+ Create or update an extmark at a position
+
+ If an invalid namespace is given, an error will be raised.
+
+ To create a new extmark, pass in id=0. The new extmark id will
+ be returned. To move an existing mark, pass in its id.
+
+ It is also allowed to create a new mark by passing in a
+ previously unused id, but the caller must then keep track of
+ existing and unused ids itself. This is mainly useful over
+ RPC, to avoid needing to wait for the return value.
+
+ Parameters: ~
+ {buffer} The buffer handle
+ {ns_id} a identifier returned previously with
+ nvim_create_namespace
+ {id} The extmark's id or 0 to create a new mark.
+ {line} The row to set the extmark to.
+ {col} The column to set the extmark to.
+ {opts} Optional parameters. Currently not used.
+
+ Return: ~
+ the id of the extmark.
+
+nvim_buf_del_extmark({buffer}, {ns_id}, {id}) *nvim_buf_del_extmark()*
+ Remove an extmark
+
+ Parameters: ~
+ {buffer} The buffer handle
+ {ns_id} a identifier returned previously with
+ nvim_create_namespace
+ {id} The extmarks's id
+
+ Return: ~
+ true on success, false if the extmark was not found.
+
*nvim_buf_add_highlight()*
nvim_buf_add_highlight({buffer}, {ns_id}, {hl_group}, {line},
{col_start}, {col_end})
@@ -1821,6 +1967,27 @@ nvim_buf_set_virtual_text({buffer}, {ns_id}, {line}, {chunks}, {opts})
Return: ~
The ns_id that was used
+nvim_buf_get_virtual_text({buffer}, {lnum}) *nvim_buf_get_virtual_text()*
+ Get the virtual text (annotation) for a buffer line.
+
+ The virtual text is returned as list of lists, whereas the
+ inner lists have either one or two elements. The first element
+ is the actual text, the optional second element is the
+ highlight group.
+
+ The format is exactly the same as given to
+ nvim_buf_set_virtual_text().
+
+ If there is no virtual text associated with the given line, an
+ empty list is returned.
+
+ Parameters: ~
+ {buffer} Buffer handle, or 0 for current buffer
+ {line} Line to get the virtual text from (zero-indexed)
+
+ Return: ~
+ List of virtual text chunks
+
nvim__buf_stats({buffer}) *nvim__buf_stats()*
TODO: Documentation
diff --git a/runtime/doc/develop.txt b/runtime/doc/develop.txt
index 90c2e30771..ba887a83c8 100644
--- a/runtime/doc/develop.txt
+++ b/runtime/doc/develop.txt
@@ -143,6 +143,87 @@ DOCUMENTATION *dev-doc*
/// @param dirname The path fragment before `pend`
<
+C docstrings ~
+
+Nvim API documentation lives in the source code, as docstrings (Doxygen
+comments) on the function definitions. The |api| :help is generated
+from the docstrings defined in src/nvim/api/*.c.
+
+Docstring format:
+- Lines start with `///`
+- Special tokens start with `@` followed by the token name:
+ `@note`, `@param`, `@returns`
+- Limited markdown is supported.
+ - List-items start with `-` (useful to nest or "indent")
+- Use `<pre>` for code samples.
+
+Example: the help for |nvim_open_win()| is generated from a docstring defined
+in src/nvim/api/vim.c like this: >
+
+ /// Opens a new window.
+ /// ...
+ ///
+ /// Example (Lua): window-relative float
+ /// <pre>
+ /// vim.api.nvim_open_win(0, false,
+ /// {relative='win', row=3, col=3, width=12, height=3})
+ /// </pre>
+ ///
+ /// @param buffer Buffer to display
+ /// @param enter Enter the window
+ /// @param config Map defining the window configuration. Keys:
+ /// - relative: Sets the window layout, relative to:
+ /// - "editor" The global editor grid.
+ /// - "win" Window given by the `win` field.
+ /// - "cursor" Cursor position in current window.
+ /// ...
+ /// @param[out] err Error details, if any
+ ///
+ /// @return Window handle, or 0 on error
+
+
+Lua docstrings ~
+ *dev-lua-doc*
+Lua documentation lives in the source code, as docstrings on the function
+definitions. The |lua-vim| :help is generated from the docstrings.
+
+Docstring format:
+- Lines in the main description start with `---`
+- Special tokens start with `--@` followed by the token name:
+ `--@see`, `--@param`, `--@returns`
+- Limited markdown is supported.
+ - List-items start with `-` (useful to nest or "indent")
+- Use `<pre>` for code samples.
+
+Example: the help for |vim.paste()| is generated from a docstring decorating
+vim.paste in src/nvim/lua/vim.lua like this: >
+
+ --- Paste handler, invoked by |nvim_paste()| when a conforming UI
+ --- (such as the |TUI|) pastes text into the editor.
+ ---
+ --- Example: To remove ANSI color codes when pasting:
+ --- <pre>
+ --- vim.paste = (function()
+ --- local overridden = vim.paste
+ --- ...
+ --- end)()
+ --- </pre>
+ ---
+ --@see |paste|
+ ---
+ --@param lines ...
+ --@param phase ...
+ --@returns false if client should cancel the paste.
+
+
+LUA *dev-lua*
+
+- Keep the core Lua modules |lua-stdlib| simple. Avoid elaborate OOP or
+ pseudo-OOP designs. Plugin authors just want functions to call, they don't
+ want to learn a big, fancy inheritance hierarchy. So we should avoid complex
+ objects: tables are usually better.
+
+
API *dev-api*
Use this template to name new API functions:
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index 77b6ee24a4..84a893a205 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -1217,7 +1217,7 @@ lambda expression *expr-lambda* *lambda*
{args -> expr1} lambda expression
A lambda expression creates a new unnamed function which returns the result of
-evaluating |expr1|. Lambda expressions differ from |user-functions| in
+evaluating |expr1|. Lambda expressions differ from |user-function|s in
the following ways:
1. The body of the lambda expression is an |expr1| and not a sequence of |Ex|
@@ -1737,6 +1737,10 @@ v:lnum Line number for the 'foldexpr' |fold-expr|, 'formatexpr' and
expressions is being evaluated. Read-only when in the
|sandbox|.
+ *v:lua* *lua-variable*
+v:lua Prefix for calling Lua functions from expressions.
+ See |v:lua-call| for more information.
+
*v:mouse_win* *mouse_win-variable*
v:mouse_win Window number for a mouse click obtained with |getchar()|.
First window has number 1, like with |winnr()|. The value is
@@ -1986,9 +1990,12 @@ v:windowid Application-specific window "handle" which may be set by any
|window-ID|.
==============================================================================
-4. Builtin Functions *functions*
+4. Builtin Functions *vim-function* *functions*
+
+The Vimscript subsystem (referred to as "eval" internally) provides the
+following builtin functions. Scripts can also define |user-function|s.
-See |function-list| for a list grouped by what the function is used for.
+See |function-list| to browse functions by topic.
(Use CTRL-] on the function name to jump to the full explanation.)
@@ -3543,7 +3550,7 @@ exists({expr}) The result is a Number, which is |TRUE| if {expr} is
string)
*funcname built-in function (see |functions|)
or user defined function (see
- |user-functions|). Also works for a
+ |user-function|). Also works for a
variable that is a Funcref.
varname internal variable (see
|internal-variables|). Also works
@@ -4553,6 +4560,10 @@ getloclist({nr},[, {what}]) *getloclist()*
If the optional {what} dictionary argument is supplied, then
returns the items listed in {what} as a dictionary. Refer to
|getqflist()| for the supported items in {what}.
+ If {what} contains 'filewinid', then returns the id of the
+ window used to display files from the location list. This
+ field is applicable only when called from a location list
+ window.
getmatches() *getmatches()*
Returns a |List| with all matches previously defined for the
@@ -9239,7 +9250,7 @@ Don't forget that "^" will only match at the first character of the String and
"\n".
==============================================================================
-5. Defining functions *user-functions*
+5. Defining functions *user-function*
New functions can be defined. These can be called just like builtin
functions. The function executes a sequence of Ex commands. Normal mode
diff --git a/runtime/doc/help.txt b/runtime/doc/help.txt
index 284cd26583..6090fa96bb 100644
--- a/runtime/doc/help.txt
+++ b/runtime/doc/help.txt
@@ -129,6 +129,7 @@ Advanced editing ~
|autocmd.txt| automatically executing commands on an event
|eval.txt| expression evaluation, conditional commands
|fold.txt| hide (fold) ranges of lines
+|lua.txt| Lua API
Special issues ~
|print.txt| printing
@@ -157,7 +158,6 @@ GUI ~
Interfaces ~
|if_cscop.txt| using Cscope with Vim
-|if_lua.txt| Lua interface
|if_pyth.txt| Python interface
|if_ruby.txt| Ruby interface
|sign.txt| debugging signs
diff --git a/runtime/doc/if_lua.txt b/runtime/doc/if_lua.txt
index 8528085f47..34bcf0f039 100644
--- a/runtime/doc/if_lua.txt
+++ b/runtime/doc/if_lua.txt
@@ -1,774 +1,8 @@
-*if_lua.txt* Nvim
- NVIM REFERENCE MANUAL
+ NVIM REFERENCE MANUAL
-
-Lua engine *lua* *Lua*
-
- Type |gO| to see the table of contents.
-
-==============================================================================
-Introduction *lua-intro*
-
-The Lua 5.1 language is builtin and always available. Try this command to get
-an idea of what lurks beneath: >
-
- :lua print(vim.inspect(package.loaded))
-
-Nvim includes a "standard library" |lua-stdlib| for Lua. It complements the
-"editor stdlib" (|functions| and Ex commands) and the |API|, all of which can
-be used from Lua code.
-
-Module conflicts are resolved by "last wins". For example if both of these
-are on 'runtimepath':
- runtime/lua/foo.lua
- ~/.config/nvim/lua/foo.lua
-then `require('foo')` loads "~/.config/nvim/lua/foo.lua", and
-"runtime/lua/foo.lua" is not used. See |lua-require| to understand how Nvim
-finds and loads Lua modules. The conventions are similar to VimL plugins,
-with some extra features. See |lua-require-example| for a walkthrough.
-
-==============================================================================
-Importing Lua modules *lua-require*
-
-Nvim automatically adjusts `package.path` and `package.cpath` according to
-effective 'runtimepath' value. Adjustment happens whenever 'runtimepath' is
-changed. `package.path` is adjusted by simply appending `/lua/?.lua` and
-`/lua/?/init.lua` to each directory from 'runtimepath' (`/` is actually the
-first character of `package.config`).
-
-Similarly to `package.path`, modified directories from 'runtimepath' are also
-added to `package.cpath`. In this case, instead of appending `/lua/?.lua` and
-`/lua/?/init.lua` to each runtimepath, all unique `?`-containing suffixes of
-the existing `package.cpath` are used. Example:
-
-1. Given that
- - 'runtimepath' contains `/foo/bar,/xxx;yyy/baz,/abc`;
- - initial (defined at compile-time or derived from
- `$LUA_CPATH`/`$LUA_INIT`) `package.cpath` contains
- `./?.so;/def/ghi/a?d/j/g.elf;/def/?.so`.
-2. It finds `?`-containing suffixes `/?.so`, `/a?d/j/g.elf` and `/?.so`, in
- order: parts of the path starting from the first path component containing
- question mark and preceding path separator.
-3. The suffix of `/def/?.so`, namely `/?.so` is not unique, as it’s the same
- as the suffix of the first path from `package.path` (i.e. `./?.so`). Which
- leaves `/?.so` and `/a?d/j/g.elf`, in this order.
-4. 'runtimepath' has three paths: `/foo/bar`, `/xxx;yyy/baz` and `/abc`. The
- second one contains semicolon which is a paths separator so it is out,
- leaving only `/foo/bar` and `/abc`, in order.
-5. The cartesian product of paths from 4. and suffixes from 3. is taken,
- giving four variants. In each variant `/lua` path segment is inserted
- between path and suffix, leaving
-
- - `/foo/bar/lua/?.so`
- - `/foo/bar/lua/a?d/j/g.elf`
- - `/abc/lua/?.so`
- - `/abc/lua/a?d/j/g.elf`
-
-6. New paths are prepended to the original `package.cpath`.
-
-The result will look like this:
-
- `/foo/bar,/xxx;yyy/baz,/abc` ('runtimepath')
- × `./?.so;/def/ghi/a?d/j/g.elf;/def/?.so` (`package.cpath`)
-
- = `/foo/bar/lua/?.so;/foo/bar/lua/a?d/j/g.elf;/abc/lua/?.so;/abc/lua/a?d/j/g.elf;./?.so;/def/ghi/a?d/j/g.elf;/def/?.so`
-
-Note:
-
-- To track 'runtimepath' updates, paths added at previous update are
- remembered and removed at the next update, while all paths derived from the
- new 'runtimepath' are prepended as described above. This allows removing
- paths when path is removed from 'runtimepath', adding paths when they are
- added and reordering `package.path`/`package.cpath` content if 'runtimepath'
- was reordered.
-
-- Although adjustments happen automatically, Nvim does not track current
- values of `package.path` or `package.cpath`. If you happen to delete some
- paths from there you can set 'runtimepath' to trigger an update: >
- let &runtimepath = &runtimepath
-
-- Skipping paths from 'runtimepath' which contain semicolons applies both to
- `package.path` and `package.cpath`. Given that there are some badly written
- plugins using shell which will not work with paths containing semicolons it
- is better to not have them in 'runtimepath' at all.
-
-------------------------------------------------------------------------------
-LUA PLUGIN EXAMPLE *lua-require-example*
-
-The following example plugin adds a command `:MakeCharBlob` which transforms
-current buffer into a long `unsigned char` array. Lua contains transformation
-function in a module `lua/charblob.lua` which is imported in
-`autoload/charblob.vim` (`require("charblob")`). Example plugin is supposed
-to be put into any directory from 'runtimepath', e.g. `~/.config/nvim` (in
-this case `lua/charblob.lua` means `~/.config/nvim/lua/charblob.lua`).
-
-autoload/charblob.vim: >
-
- function charblob#encode_buffer()
- call setline(1, luaeval(
- \ 'require("charblob").encode(unpack(_A))',
- \ [getline(1, '$'), &textwidth, ' ']))
- endfunction
-
-plugin/charblob.vim: >
-
- if exists('g:charblob_loaded')
- finish
- endif
- let g:charblob_loaded = 1
-
- command MakeCharBlob :call charblob#encode_buffer()
-
-lua/charblob.lua: >
-
- local function charblob_bytes_iter(lines)
- local init_s = {
- next_line_idx = 1,
- next_byte_idx = 1,
- lines = lines,
- }
- local function next(s, _)
- if lines[s.next_line_idx] == nil then
- return nil
- end
- if s.next_byte_idx > #(lines[s.next_line_idx]) then
- s.next_line_idx = s.next_line_idx + 1
- s.next_byte_idx = 1
- return ('\n'):byte()
- end
- local ret = lines[s.next_line_idx]:byte(s.next_byte_idx)
- if ret == ('\n'):byte() then
- ret = 0 -- See :h NL-used-for-NUL.
- end
- s.next_byte_idx = s.next_byte_idx + 1
- return ret
- end
- return next, init_s, nil
- end
-
- local function charblob_encode(lines, textwidth, indent)
- local ret = {
- 'const unsigned char blob[] = {',
- indent,
- }
- for byte in charblob_bytes_iter(lines) do
- -- .- space + number (width 3) + comma
- if #(ret[#ret]) + 5 > textwidth then
- ret[#ret + 1] = indent
- else
- ret[#ret] = ret[#ret] .. ' '
- end
- ret[#ret] = ret[#ret] .. (('%3u,'):format(byte))
- end
- ret[#ret + 1] = '};'
- return ret
- end
-
- return {
- bytes_iter = charblob_bytes_iter,
- encode = charblob_encode,
- }
+Moved to |lua.txt|
==============================================================================
-Commands *lua-commands*
-
- *:lua*
-:[range]lua {chunk}
- Execute Lua chunk {chunk}.
-
-Examples:
->
- :lua vim.api.nvim_command('echo "Hello, Nvim!"')
-<
-To see the Lua version: >
- :lua print(_VERSION)
-
-To see the LuaJIT version: >
- :lua print(jit.version)
-<
-
-:[range]lua << [endmarker]
-{script}
-{endmarker}
- Execute Lua script {script}. Useful for including Lua
- code in Vim scripts.
-
-The {endmarker} must NOT be preceded by any white space.
-
-If [endmarker] is omitted from after the "<<", a dot '.' must be used after
-{script}, like for the |:append| and |:insert| commands.
-
-Example:
->
- function! CurrentLineInfo()
- lua << EOF
- local linenr = vim.api.nvim_win_get_cursor(0)[1]
- local curline = vim.api.nvim_buf_get_lines(
- 0, linenr, linenr + 1, false)[1]
- print(string.format("Current line [%d] has %d bytes",
- linenr, #curline))
- EOF
- endfunction
-
-Note that the `local` variables will disappear when block finishes. This is
-not the case for globals.
-
- *:luado*
-:[range]luado {body} Execute Lua function "function (line, linenr) {body}
- end" for each line in the [range], with the function
- argument being set to the text of each line in turn,
- without a trailing <EOL>, and the current line number.
- If the value returned by the function is a string it
- becomes the text of the line in the current turn. The
- default for [range] is the whole file: "1,$".
-
-Examples:
->
- :luado return string.format("%s\t%d", line:reverse(), #line)
-
- :lua require"lpeg"
- :lua -- balanced parenthesis grammar:
- :lua bp = lpeg.P{ "(" * ((1 - lpeg.S"()") + lpeg.V(1))^0 * ")" }
- :luado if bp:match(line) then return "-->\t" .. line end
-<
-
- *:luafile*
-:[range]luafile {file}
- Execute Lua script in {file}.
- The whole argument is used as a single file name.
-
-Examples:
->
- :luafile script.lua
- :luafile %
-<
-
-All these commands execute a Lua chunk from either the command line (:lua and
-:luado) or a file (:luafile) with the given line [range]. Similarly to the Lua
-interpreter, each chunk has its own scope and so only global variables are
-shared between command calls. All Lua default libraries are available. In
-addition, Lua "print" function has its output redirected to the Nvim message
-area, with arguments separated by a white space instead of a tab.
-
-Lua uses the "vim" module (see |lua-vim|) to issue commands to Nvim. However,
-procedures that alter buffer content, open new buffers, and change cursor
-position are restricted when the command is executed in the |sandbox|.
-
-
-==============================================================================
-luaeval() *lua-eval* *luaeval()*
-
-The (dual) equivalent of "vim.eval" for passing Lua values to Nvim is
-"luaeval". "luaeval" takes an expression string and an optional argument used
-for _A inside expression and returns the result of the expression. It is
-semantically equivalent in Lua to:
->
- local chunkheader = "local _A = select(1, ...) return "
- function luaeval (expstr, arg)
- local chunk = assert(loadstring(chunkheader .. expstr, "luaeval"))
- return chunk(arg) -- return typval
- end
-
-Lua nils, numbers, strings, tables and booleans are converted to their
-respective VimL types. An error is thrown if conversion of any other Lua types
-is attempted.
-
-The magic global "_A" contains the second argument to luaeval().
-
-Example: >
- :echo luaeval('_A[1] + _A[2]', [40, 2])
- 42
- :echo luaeval('string.match(_A, "[a-z]+")', 'XYXfoo123')
- foo
-
-Lua tables are used as both dictionaries and lists, so it is impossible to
-determine whether empty table is meant to be empty list or empty dictionary.
-Additionally lua does not have integer numbers. To distinguish between these
-cases there is the following agreement:
-
-0. Empty table is empty list.
-1. Table with N incrementally growing integral numbers, starting from 1 and
- ending with N is considered to be a list.
-2. Table with string keys, none of which contains NUL byte, is considered to
- be a dictionary.
-3. Table with string keys, at least one of which contains NUL byte, is also
- considered to be a dictionary, but this time it is converted to
- a |msgpack-special-map|.
- *lua-special-tbl*
-4. Table with `vim.type_idx` key may be a dictionary, a list or floating-point
- value:
- - `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to
- a floating-point 1.0. Note that by default integral lua numbers are
- converted to |Number|s, non-integral are converted to |Float|s. This
- variant allows integral |Float|s.
- - `{[vim.type_idx]=vim.types.dictionary}` is converted to an empty
- dictionary, `{[vim.type_idx]=vim.types.dictionary, [42]=1, a=2}` is
- converted to a dictionary `{'a': 42}`: non-string keys are ignored.
- Without `vim.type_idx` key tables with keys not fitting in 1., 2. or 3.
- are errors.
- - `{[vim.type_idx]=vim.types.list}` is converted to an empty list. As well
- as `{[vim.type_idx]=vim.types.list, [42]=1}`: integral keys that do not
- form a 1-step sequence from 1 to N are ignored, as well as all
- non-integral keys.
-
-Examples: >
-
- :echo luaeval('math.pi')
- :function Rand(x,y) " random uniform between x and y
- : return luaeval('(_A.y-_A.x)*math.random()+_A.x', {'x':a:x,'y':a:y})
- : endfunction
- :echo Rand(1,10)
-
-Note that currently second argument to `luaeval` undergoes VimL to lua
-conversion, so changing containers in lua do not affect values in VimL. Return
-value is also always converted. When converting, |msgpack-special-dict|s are
-treated specially.
-
-==============================================================================
-Lua standard modules *lua-stdlib*
-
-The Nvim Lua "standard library" (stdlib) is the `vim` module, which exposes
-various functions and sub-modules. It is always loaded, thus require("vim")
-is unnecessary.
-
-You can peek at the module properties: >
-
- :lua print(vim.inspect(vim))
-
-Result is something like this: >
-
- {
- _os_proc_children = <function 1>,
- _os_proc_info = <function 2>,
- ...
- api = {
- nvim__id = <function 5>,
- nvim__id_array = <function 6>,
- ...
- },
- deepcopy = <function 106>,
- gsplit = <function 107>,
- ...
- }
-
-To find documentation on e.g. the "deepcopy" function: >
-
- :help vim.deepcopy
-
-Note that underscore-prefixed functions (e.g. "_os_proc_children") are
-internal/private and must not be used by plugins.
-
-------------------------------------------------------------------------------
-VIM.API *lua-api* *vim.api*
-
-`vim.api` exposes the full Nvim |API| as a table of Lua functions.
-
-Example: to use the "nvim_get_current_line()" API function, call
-"vim.api.nvim_get_current_line()": >
-
- print(tostring(vim.api.nvim_get_current_line()))
-
-------------------------------------------------------------------------------
-VIM.LOOP *lua-loop* *vim.loop*
-
-`vim.loop` exposes all features of the Nvim event-loop. This is a low-level
-API that provides functionality for networking, filesystem, and process
-management. Try this command to see available functions: >
-
- :lua print(vim.inspect(vim.loop))
-
-Reference: http://docs.libuv.org
-Examples: https://github.com/luvit/luv/tree/master/examples
-
- *E5560* *lua-loop-callbacks*
-It is an error to directly invoke `vim.api` functions (except |api-fast|) in
-`vim.loop` callbacks. For example, this is an error: >
-
- local timer = vim.loop.new_timer()
- timer:start(1000, 0, function()
- vim.api.nvim_command('echomsg "test"')
- end)
-
-To avoid the error use |vim.schedule_wrap()| to defer the callback: >
-
- local timer = vim.loop.new_timer()
- timer:start(1000, 0, vim.schedule_wrap(function()
- vim.api.nvim_command('echomsg "test"')
- end))
-
-Example: repeating timer
- 1. Save this code to a file.
- 2. Execute it with ":luafile %". >
-
- -- Create a timer handle (implementation detail: uv_timer_t).
- local timer = vim.loop.new_timer()
- local i = 0
- -- Waits 1000ms, then repeats every 750ms until timer:close().
- timer:start(1000, 750, function()
- print('timer invoked! i='..tostring(i))
- if i > 4 then
- timer:close() -- Always close handles to avoid leaks.
- end
- i = i + 1
- end)
- print('sleeping');
-
-
-Example: TCP echo-server *tcp-server*
- 1. Save this code to a file.
- 2. Execute it with ":luafile %".
- 3. Note the port number.
- 4. Connect from any TCP client (e.g. "nc 0.0.0.0 36795"): >
-
- local function create_server(host, port, on_connection)
- local server = vim.loop.new_tcp()
- server:bind(host, port)
- server:listen(128, function(err)
- assert(not err, err) -- Check for errors.
- local sock = vim.loop.new_tcp()
- server:accept(sock) -- Accept client connection.
- on_connection(sock) -- Start reading messages.
- end)
- return server
- end
- local server = create_server('0.0.0.0', 0, function(sock)
- sock:read_start(function(err, chunk)
- assert(not err, err) -- Check for errors.
- if chunk then
- sock:write(chunk) -- Echo received messages to the channel.
- else -- EOF (stream closed).
- sock:close() -- Always close handles to avoid leaks.
- end
- end)
- end)
- print('TCP echo-server listening on port: '..server:getsockname().port)
-
-------------------------------------------------------------------------------
-VIM.TREESITTER *lua-treesitter*
-
-Nvim integrates the tree-sitter library for incremental parsing of buffers.
-
-Currently Nvim does not provide the tree-sitter parsers, instead these must
-be built separately, for instance using the tree-sitter utility.
-The parser is loaded into nvim using >
-
- vim.treesitter.add_language("/path/to/c_parser.so", "c")
-
-<Create a parser for a buffer and a given language (if another plugin uses the
-same buffer/language combination, it will be safely reused). Use >
-
- parser = vim.treesitter.get_parser(bufnr, lang)
-
-<`bufnr=0` can be used for current buffer. `lang` will default to 'filetype' (this
-doesn't work yet for some filetypes like "cpp") Currently, the parser will be
-retained for the lifetime of a buffer but this is subject to change. A plugin
-should keep a reference to the parser object as long as it wants incremental
-updates.
-
-Whenever you need to access the current syntax tree, parse the buffer: >
-
- tstree = parser:parse()
-
-<This will return an immutable tree that represents the current state of the
-buffer. When the plugin wants to access the state after a (possible) edit
-it should call `parse()` again. If the buffer wasn't edited, the same tree will
-be returned again without extra work. If the buffer was parsed before,
-incremental parsing will be done of the changed parts.
-
-NB: to use the parser directly inside a |nvim_buf_attach| lua callback, you must
-call `get_parser()` before you register your callback. But preferably parsing
-shouldn't be done directly in the change callback anyway as they will be very
-frequent. Rather a plugin that does any kind of analysis on a tree should use
-a timer to throttle too frequent updates.
-
-Tree methods *lua-treesitter-tree*
-
-tstree:root() *tstree:root()*
- Return the root node of this tree.
-
-
-Node methods *lua-treesitter-node*
-
-tsnode:parent() *tsnode:parent()*
- Get the node's immediate parent.
-
-tsnode:child_count() *tsnode:child_count()*
- Get the node's number of children.
-
-tsnode:child(N) *tsnode:child()*
- Get the node's child at the given index, where zero represents the
- first child.
-
-tsnode:named_child_count() *tsnode:named_child_count()*
- Get the node's number of named children.
-
-tsnode:named_child(N) *tsnode:named_child()*
- Get the node's named child at the given index, where zero represents
- the first named child.
-
-tsnode:start() *tsnode:start()*
- Get the node's start position. Return three values: the row, column
- and total byte count (all zero-based).
-
-tsnode:end_() *tsnode:end_()*
- Get the node's end position. Return three values: the row, column
- and total byte count (all zero-based).
-
-tsnode:range() *tsnode:range()*
- Get the range of the node. Return four values: the row, column
- of the start position, then the row, column of the end position.
-
-tsnode:type() *tsnode:type()*
- Get the node's type as a string.
-
-tsnode:symbol() *tsnode:symbol()*
- Get the node's type as a numerical id.
-
-tsnode:named() *tsnode:named()*
- Check if the node is named. Named nodes correspond to named rules in
- the grammar, whereas anonymous nodes correspond to string literals
- in the grammar.
-
-tsnode:missing() *tsnode:missing()*
- Check if the node is missing. Missing nodes are inserted by the
- parser in order to recover from certain kinds of syntax errors.
-
-tsnode:has_error() *tsnode:has_error()*
- Check if the node is a syntax error or contains any syntax errors.
-
-tsnode:sexpr() *tsnode:sexpr()*
- Get an S-expression representing the node as a string.
-
-tsnode:descendant_for_range(start_row, start_col, end_row, end_col)
- *tsnode:descendant_for_range()*
- Get the smallest node within this node that spans the given range of
- (row, column) positions
-
-tsnode:named_descendant_for_range(start_row, start_col, end_row, end_col)
- *tsnode:named_descendant_for_range()*
- Get the smallest named node within this node that spans the given
- range of (row, column) positions
-
-------------------------------------------------------------------------------
-VIM *lua-util*
-
-vim.in_fast_event() *vim.in_fast_event()*
- Returns true if the code is executing as part of a "fast" event
- handler, where most of the API is disabled. These are low-level events
- (e.g. |lua-loop-callbacks|) which can be invoked whenever Nvim polls
- for input. When this is `false` most API functions are callable (but
- may be subject to other restrictions such as |textlock|).
-
-vim.stricmp({a}, {b}) *vim.stricmp()*
- Compares strings case-insensitively. Returns 0, 1 or -1 if strings
- are equal, {a} is greater than {b} or {a} is lesser than {b},
- respectively.
-
-vim.str_utfindex({str}[, {index}]) *vim.str_utfindex()*
- Convert byte index to UTF-32 and UTF-16 indicies. If {index} is not
- supplied, the length of the string is used. All indicies are zero-based.
- Returns two values: the UTF-32 and UTF-16 indicies respectively.
-
- Embedded NUL bytes are treated as terminating the string. Invalid
- UTF-8 bytes, and embedded surrogates are counted as one code
- point each. An {index} in the middle of a UTF-8 sequence is rounded
- upwards to the end of that sequence.
-
-vim.str_byteindex({str}, {index}[, {use_utf16}]) *vim.str_byteindex()*
- Convert UTF-32 or UTF-16 {index} to byte index. If {use_utf16} is not
- supplied, it defaults to false (use UTF-32). Returns the byte index.
-
- Invalid UTF-8 and NUL is treated like by |vim.str_byteindex()|. An {index}
- in the middle of a UTF-16 sequence is rounded upwards to the end of that
- sequence.
-
-vim.schedule({callback}) *vim.schedule()*
- Schedules {callback} to be invoked soon by the main event-loop. Useful
- to avoid |textlock| or other temporary restrictions.
-
-vim.type_idx *vim.type_idx*
- Type index for use in |lua-special-tbl|. Specifying one of the
- values from |vim.types| allows typing the empty table (it is
- unclear whether empty lua table represents empty list or empty array)
- and forcing integral numbers to be |Float|. See |lua-special-tbl| for
- more details.
-
-vim.val_idx *vim.val_idx*
- Value index for tables representing |Float|s. A table representing
- floating-point value 1.0 looks like this: >
- {
- [vim.type_idx] = vim.types.float,
- [vim.val_idx] = 1.0,
- }
-< See also |vim.type_idx| and |lua-special-tbl|.
-
-vim.types *vim.types*
- Table with possible values for |vim.type_idx|. Contains two sets
- of key-value pairs: first maps possible values for |vim.type_idx|
- to human-readable strings, second maps human-readable type names to
- values for |vim.type_idx|. Currently contains pairs for `float`,
- `array` and `dictionary` types.
-
- Note: one must expect that values corresponding to `vim.types.float`,
- `vim.types.array` and `vim.types.dictionary` fall under only two
- following assumptions:
- 1. Value may serve both as a key and as a value in a table. Given the
- properties of lua tables this basically means “value is not `nil`”.
- 2. For each value in `vim.types` table `vim.types[vim.types[value]]`
- is the same as `value`.
- No other restrictions are put on types, and it is not guaranteed that
- values corresponding to `vim.types.float`, `vim.types.array` and
- `vim.types.dictionary` will not change or that `vim.types` table will
- only contain values for these three types.
-
-==============================================================================
-Lua module: vim *lua-vim*
-
-inspect({object}, {options}) *vim.inspect()*
- Return a human-readable representation of the given object.
-
- See also: ~
- https://github.com/kikito/inspect.lua
- https://github.com/mpeterv/vinspect
-
-paste({lines}, {phase}) *vim.paste()*
- Paste handler, invoked by |nvim_paste()| when a conforming UI
- (such as the |TUI|) pastes text into the editor.
-
- Parameters: ~
- {lines} |readfile()|-style list of lines to paste.
- |channel-lines|
- {phase} -1: "non-streaming" paste: the call contains all
- lines. If paste is "streamed", `phase` indicates the stream state:
- • 1: starts the paste (exactly once)
- • 2: continues the paste (zero or more times)
- • 3: ends the paste (exactly once)
-
- Return: ~
- false if client should cancel the paste.
-
- See also: ~
- |paste|
-
-schedule_wrap({cb}) *vim.schedule_wrap()*
- Defers callback `cb` until the Nvim API is safe to call.
-
- See also: ~
- |lua-loop-callbacks|
- |vim.schedule()|
- |vim.in_fast_event()|
-
-
-
-
-deepcopy({orig}) *vim.deepcopy()*
- Returns a deep copy of the given object. Non-table objects are
- copied as in a typical Lua assignment, whereas table objects
- are copied recursively.
-
- Parameters: ~
- {orig} Table to copy
-
- Return: ~
- New table of copied keys and (nested) values.
-
-gsplit({s}, {sep}, {plain}) *vim.gsplit()*
- Splits a string at each instance of a separator.
-
- Parameters: ~
- {s} String to split
- {sep} Separator string or pattern
- {plain} If `true` use `sep` literally (passed to
- String.find)
-
- Return: ~
- Iterator over the split components
-
- See also: ~
- |vim.split()|
- https://www.lua.org/pil/20.2.html
- http://lua-users.org/wiki/StringLibraryTutorial
-
-split({s}, {sep}, {plain}) *vim.split()*
- Splits a string at each instance of a separator.
-
- Examples: >
- split(":aa::b:", ":") --> {'','aa','','bb',''}
- split("axaby", "ab?") --> {'','x','y'}
- split(x*yz*o, "*", true) --> {'x','yz','o'}
-<
-
- Parameters: ~
- {s} String to split
- {sep} Separator string or pattern
- {plain} If `true` use `sep` literally (passed to
- String.find)
-
- Return: ~
- List-like table of the split components.
-
- See also: ~
- |vim.gsplit()|
-
-tbl_contains({t}, {value}) *vim.tbl_contains()*
- Checks if a list-like (vector) table contains `value` .
-
- Parameters: ~
- {t} Table to check
- {value} Value to compare
-
- Return: ~
- true if `t` contains `value`
-
-tbl_extend({behavior}, {...}) *vim.tbl_extend()*
- Merges two or more map-like tables.
-
- Parameters: ~
- {behavior} Decides what to do if a key is found in more
- than one map:
- • "error": raise an error
- • "keep": use value from the leftmost map
- • "force": use value from the rightmost map
- {...} Two or more map-like tables.
-
- See also: ~
- |extend()|
-
-tbl_flatten({t}) *vim.tbl_flatten()*
- Creates a copy of a list-like table such that any nested
- tables are "unrolled" and appended to the result.
-
- Parameters: ~
- {t} List-like table
-
- Return: ~
- Flattened copy of the given list-like table.
-
-trim({s}) *vim.trim()*
- Trim whitespace (Lua pattern "%s") from both sides of a
- string.
-
- Parameters: ~
- {s} String to trim
-
- Return: ~
- String with whitespace removed from its beginning and end
-
- See also: ~
- https://www.lua.org/pil/20.2.html
-
-pesc({s}) *vim.pesc()*
- Escapes magic chars in a Lua pattern string.
-
- Parameters: ~
- {s} String to escape
-
- Return: ~
- %-escaped pattern string
-
- See also: ~
- https://github.com/rxi/lume
-
- vim:tw=78:ts=8:ft=help:norl:
+ vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt
index be9e25113a..211b7be2cc 100644
--- a/runtime/doc/index.txt
+++ b/runtime/doc/index.txt
@@ -767,6 +767,7 @@ tag char note action in Normal mode ~
|gn| gn 1,2 find the next match with the last used
search pattern and Visually select it
|gm| gm 1 go to character at middle of the screenline
+|gM| gM 1 go to character at middle of the text line
|go| go 1 cursor to byte N in the buffer
|gp| ["x]gp 2 put the text [from register x] after the
cursor N times, leave the cursor after it
@@ -1163,11 +1164,13 @@ tag command action ~
|:cNfile| :cNf[ile] go to last error in previous file
|:cabbrev| :ca[bbrev] like ":abbreviate" but for Command-line mode
|:cabclear| :cabc[lear] clear all abbreviations for Command-line mode
+|:cabove| :cabo[ve] go to error above current line
|:caddbuffer| :cad[dbuffer] add errors from buffer
|:caddexpr| :cadde[xpr] add errors from expr
|:caddfile| :caddf[ile] add error message to current quickfix list
|:call| :cal[l] call a function
|:catch| :cat[ch] part of a :try command
+|:cbelow| :cbe[low] go to error below current line
|:cbottom| :cbo[ttom] scroll to the bottom of the quickfix window
|:cbuffer| :cb[uffer] parse error messages and jump to first error
|:cc| :cc go to specific error
@@ -1324,12 +1327,14 @@ tag command action ~
|:lNext| :lN[ext] go to previous entry in location list
|:lNfile| :lNf[ile] go to last entry in previous file
|:list| :l[ist] print lines
+|:labove| :lab[ove] go to location above current line
|:laddexpr| :lad[dexpr] add locations from expr
|:laddbuffer| :laddb[uffer] add locations from buffer
|:laddfile| :laddf[ile] add locations to current location list
|:last| :la[st] go to the last file in the argument list
|:language| :lan[guage] set the language (locale)
|:later| :lat[er] go to newer change, redo
+|:lbelow| :lbe[low] go to location below current line
|:lbottom| :lbo[ttom] scroll to the bottom of the location window
|:lbuffer| :lb[uffer] parse locations and jump to first location
|:lcd| :lc[d] change directory locally
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
new file mode 100644
index 0000000000..26850b3683
--- /dev/null
+++ b/runtime/doc/lsp.txt
@@ -0,0 +1,662 @@
+*lsp.txt* The Language Server Protocol
+
+ NVIM REFERENCE MANUAL
+
+
+Neovim Language Server Protocol (LSP) API
+
+Neovim exposes a powerful API that conforms to Microsoft's published Language
+Server Protocol specification. The documentation can be found here:
+
+ https://microsoft.github.io/language-server-protocol/
+
+
+================================================================================
+ *lsp-api*
+
+Neovim exposes a API for the language server protocol. To get the real benefits
+of this API, a language server must be installed.
+Many examples can be found here:
+
+ https://microsoft.github.io/language-server-protocol/implementors/servers/
+
+After installing a language server to your machine, you must let Neovim know
+how to start and interact with that language server.
+
+To do so, you can either:
+- Use the |vim.lsp.add_filetype_config()|, which solves the common use-case of
+ a single server for one or more filetypes. This can also be used from vim
+ via |lsp#add_filetype_config()|.
+- Or |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()|. These are the
+ backbone of the LSP API. These are easy to use enough for basic or more
+ complex configurations such as in |lsp-advanced-js-example|.
+
+================================================================================
+ *lsp-filetype-config*
+
+These are utilities specific to filetype based configurations.
+
+ *lsp#add_filetype_config()*
+ *vim.lsp.add_filetype_config()*
+lsp#add_filetype_config({config}) for Vim.
+vim.lsp.add_filetype_config({config}) for Lua
+
+ These are functions which can be used to create a simple configuration which
+ will start a language server for a list of filetypes based on the |FileType|
+ event.
+ It will lazily start start the server, meaning that it will only start once
+ a matching filetype is encountered.
+
+ The {config} options are the same as |vim.lsp.start_client()|, but
+ with a few additions and distinctions:
+
+ Additional parameters:~
+ `filetype`
+ {string} or {list} of filetypes to attach to.
+ `name`
+ A unique identifying string among all other servers configured with
+ |vim.lsp.add_filetype_config|.
+
+ Differences:~
+ `root_dir`
+ Will default to |getcwd()| instead of being required.
+
+ NOTE: the function options in {config} like {config.on_init} are for Lua
+ callbacks, not Vim callbacks.
+>
+ " Go example
+ call lsp#add_filetype_config({
+ \ 'filetype': 'go',
+ \ 'name': 'gopls',
+ \ 'cmd': 'gopls'
+ \ })
+ " Python example
+ call lsp#add_filetype_config({
+ \ 'filetype': 'python',
+ \ 'name': 'pyls',
+ \ 'cmd': 'pyls'
+ \ })
+ " Rust example
+ call lsp#add_filetype_config({
+ \ 'filetype': 'rust',
+ \ 'name': 'rls',
+ \ 'cmd': 'rls',
+ \ 'capabilities': {
+ \ 'clippy_preference': 'on',
+ \ 'all_targets': v:false,
+ \ 'build_on_save': v:true,
+ \ 'wait_to_build': 0
+ \ }})
+<
+>
+ -- From Lua
+ vim.lsp.add_filetype_config {
+ name = "clangd";
+ filetype = {"c", "cpp"};
+ cmd = "clangd -background-index";
+ capabilities = {
+ offsetEncoding = {"utf-8", "utf-16"};
+ };
+ on_init = vim.schedule_wrap(function(client, result)
+ if result.offsetEncoding then
+ client.offset_encoding = result.offsetEncoding
+ end
+ end)
+ }
+<
+ *vim.lsp.copy_filetype_config()*
+vim.lsp.copy_filetype_config({existing_name}, [{override_config}])
+
+ You can use this to copy an existing filetype configuration and change it by
+ specifying {override_config} which will override any properties in the
+ existing configuration. If you don't specify a new unique name with
+ {override_config.name} then it will try to create one and return it.
+
+ Returns:~
+ `name` the new configuration name.
+
+ *vim.lsp.get_filetype_client_by_name()*
+vim.lsp.get_filetype_client_by_name({name})
+
+ Use this to look up a client by its name created from
+ |vim.lsp.add_filetype_config()|.
+
+ Returns nil if the client is not active or the name is not valid.
+
+================================================================================
+ *lsp-core-api*
+These are the core api functions for working with clients. You will mainly be
+using |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()| for operations
+and |vim.lsp.get_client_by_id()| to retrieve a client by its id after it has
+initialized (or {config.on_init}. see below)
+
+ *vim.lsp.start_client()*
+
+vim.lsp.start_client({config})
+
+ The main function used for starting clients.
+ Start a client and initialize it.
+
+ Its arguments are passed via a configuration object {config}.
+
+ Mandatory parameters:~
+
+ `root_dir`
+ {string} specifying the directory where the LSP server will base
+ as its rootUri on initialization.
+
+ `cmd`
+ {string} or {list} which is the base command to execute for the LSP. A
+ string will be run using |'shell'| and a list will be interpreted as a
+ bare command with arguments passed. This is the same as |jobstart()|.
+
+ Optional parameters:~
+
+ `cmd_cwd`
+ {string} specifying the directory to launch the `cmd` process. This is not
+ related to `root_dir`.
+ By default, |getcwd()| is used.
+
+ `cmd_env`
+ {table} specifying the environment flags to pass to the LSP on spawn.
+ This can be specified using keys like a map or as a list with `k=v` pairs
+ or both. Non-string values are coerced to a string.
+ For example:
+ `{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`
+
+ `capabilities`
+ A {table} which will be used instead of
+ `vim.lsp.protocol.make_client_capabilities()` which contains neovim's
+ default capabilities and passed to the language server on initialization.
+ You'll probably want to use make_client_capabilities() and modify the
+ result.
+ NOTE:
+ To send an empty dictionary, you should use
+ `{[vim.type_idx]=vim.types.dictionary}` Otherwise, it will be encoded as
+ an array.
+
+ `callbacks`
+ A {table} of whose keys are language server method names and the values
+ are `function(err, method, params, client_id)` See |lsp-callbacks| for
+ more. This will be combined with |lsp-builtin-callbacks| to provide
+ defaults.
+
+ `init_options`
+ A {table} of values to pass in the initialization request as
+ `initializationOptions`. See the `initialize` in the LSP spec.
+
+ `name`
+ A {string} used in log messages. Defaults to {client_id}
+
+ `offset_encoding`
+ One of "utf-8", "utf-16", or "utf-32" which is the encoding that the LSP
+ server expects.
+ The default encoding for Language Server Protocol is UTF-16, but there are
+ language servers which may use other encodings.
+ By default, it is "utf-16" as specified in the LSP specification. The
+ client does not verify this is correct.
+
+ `on_error(code, ...)`
+ A function for handling errors thrown by client operation. {code} is a
+ number describing the error. Other arguments may be passed depending on
+ the error kind. See |vim.lsp.client_errors| for possible errors.
+ `vim.lsp.client_errors[code]` can be used to retrieve a human
+ understandable string.
+
+ `on_init(client, initialize_result)`
+ A function which is called after the request `initialize` is completed.
+ `initialize_result` contains `capabilities` and anything else the server
+ may send. For example, `clangd` sends `initialize_result.offsetEncoding`
+ if `capabilities.offsetEncoding` was sent to it. You can *only* modify the
+ `client.offset_encoding` here before any notifications are sent.
+
+ `on_attach(client, bufnr)`
+ A function which is called after the client is attached to a buffer.
+
+ `on_exit(code, signal, client_id)`
+ A function which is called after the client has exited. code is the exit
+ code of the process, and signal is a number describing the signal used to
+ terminate (if any).
+
+ `trace`
+ "off" | "messages" | "verbose" | nil passed directly to the language
+ server in the initialize request.
+ Invalid/empty values will default to "off"
+
+ Returns:~
+ {client_id}
+ You can use |vim.lsp.get_client_by_id()| to get the actual client object.
+ See |lsp-client| for what the client structure will be.
+
+ NOTE: The client is only available *after* it has been initialized, which
+ may happen after a small delay (or never if there is an error). For this
+ reason, you may want to use `on_init` to do any actions once the client has
+ been initialized.
+
+ *lsp-client*
+
+The client object has some methods and members related to using the client.
+
+ Methods:~
+
+ `request(method, params, [callback])`
+ Send a request to the server. If callback is not specified, it will use
+ {client.callbacks} to try to find a callback. If one is not found there,
+ then an error will occur.
+ This is a thin wrapper around {client.rpc.request} with some additional
+ checking.
+ Returns a boolean to indicate if the notification was successful. If it
+ is false, then it will always be false (the client has shutdown).
+ If it was successful, then it will return the request id as the second
+ result. You can use this with `notify("$/cancel", { id = request_id })`
+ to cancel the request. This helper is made automatically with
+ |vim.lsp.buf_request()|
+ Returns: status, [client_id]
+
+ `notify(method, params)`
+ This is just {client.rpc.notify}()
+ Returns a boolean to indicate if the notification was successful. If it
+ is false, then it will always be false (the client has shutdown).
+ Returns: status
+
+ `cancel_request(id)`
+ This is just {client.rpc.notify}("$/cancelRequest", { id = id })
+ Returns the same as `notify()`.
+
+ `stop([force])`
+ Stop a client, optionally with force.
+ By default, it will just ask the server to shutdown without force.
+ If you request to stop a client which has previously been requested to
+ shutdown, it will automatically escalate and force shutdown.
+
+ `is_stopped()`
+ Returns true if the client is fully stopped.
+
+ Members: ~
+ `id` (number)
+ The id allocated to the client.
+
+ `name` (string)
+ If a name is specified on creation, that will be used. Otherwise it is
+ just the client id. This is used for logs and messages.
+
+ `offset_encoding` (string)
+ The encoding used for communicating with the server. You can modify this
+ in the `on_init` method before text is sent to the server.
+
+ `callbacks` (table)
+ The callbacks used by the client as described in |lsp-callbacks|.
+
+ `config` (table)
+ A copy of the table that was passed by the user to
+ |vim.lsp.start_client()|.
+
+ `server_capabilities` (table)
+ The response from the server sent on `initialize` describing the
+ server's capabilities.
+
+ `resolved_capabilities` (table)
+ A normalized table of capabilities that we have detected based on the
+ initialize response from the server in `server_capabilities`.
+
+
+ *vim.lsp.buf_attach_client()*
+vim.lsp.buf_attach_client({bufnr}, {client_id})
+
+ Implements the `textDocument/did*` notifications required to track a buffer
+ for any language server.
+
+ Without calling this, the server won't be notified of changes to a buffer.
+
+ *vim.lsp.get_client_by_id()*
+vim.lsp.get_client_by_id({client_id})
+
+ Look up an active client by its id, returns nil if it is not yet initialized
+ or is not a valid id. Returns |lsp-client|
+
+ *vim.lsp.stop_client()*
+vim.lsp.stop_client({client_id}, [{force}])
+
+ Stop a client, optionally with force.
+ By default, it will just ask the server to shutdown without force.
+ If you request to stop a client which has previously been requested to
+ shutdown, it will automatically escalate and force shutdown.
+
+ You can also use `client.stop()` if you have access to the client.
+
+ *vim.lsp.stop_all_clients()*
+vim.lsp.stop_all_clients([{force}])
+
+ |vim.lsp.stop_client()|, but for all active clients.
+
+ *vim.lsp.get_active_clients()*
+vim.lsp.get_active_clients()
+
+ Return a list of all of the active clients. See |lsp-client| for a
+ description of what a client looks like.
+
+ *vim.lsp.rpc_response_error()*
+vim.lsp.rpc_response_error({code}, [{message}], [{data}])
+
+ Helper function to create an RPC response object/table. This is an alias for
+ |vim.lsp.rpc.rpc_response_error|. Code must be an RPC error code as
+ described in `vim.lsp.protocol.ErrorCodes`.
+
+ You can describe an optional {message} string or arbitrary {data} to send to
+ the server.
+
+================================================================================
+ *vim.lsp.builtin_callbacks*
+
+The |vim.lsp.builtin_callbacks| table contains the default |lsp-callbacks|
+that are used when creating a new client. The keys are the LSP method names.
+
+The following requests and notifications have built-in callbacks defined to
+handle the response in an idiomatic way.
+
+ textDocument/completion
+ textDocument/declaration
+ textDocument/definition
+ textDocument/hover
+ textDocument/implementation
+ textDocument/rename
+ textDocument/signatureHelp
+ textDocument/typeDefinition
+ window/logMessage
+ window/showMessage
+
+You can check these via `vim.tbl_keys(vim.lsp.builtin_callbacks)`.
+
+These will be automatically used and can be overridden by users (either by
+modifying the |vim.lsp.builtin_callbacks| object or on a per-client basis
+by passing in a table via the {callbacks} parameter on |vim.lsp.start_client|
+or |vim.lsp.add_filetype_config|.
+
+More information about callbacks can be found in |lsp-callbacks|.
+
+================================================================================
+ *lsp-callbacks*
+
+Callbacks are functions which are called in a variety of situations by the
+client. Their signature is `function(err, method, params, client_id)` They can
+be set by the {callbacks} parameter for |vim.lsp.start_client| and
+|vim.lsp.add_filetype_config| or via the |vim.lsp.builtin_callbacks|.
+
+This will be called for:
+- notifications from the server, where `err` will always be `nil`
+- requests initiated by the server. The parameter `err` will be `nil` here as
+ well.
+ For these, you can respond by returning two values: `result, err` The
+ err must be in the format of an RPC error, which is
+ `{ code, message, data? }`
+ You can use |vim.lsp.rpc_response_error()| to help with creating this object.
+- as a callback for requests initiated by the client if the request doesn't
+ explicitly specify a callback (such as in |vim.lsp.buf_request|).
+
+================================================================================
+ *vim.lsp.protocol*
+vim.lsp.protocol
+
+ Contains constants as described in the Language Server Protocol
+ specification and helper functions for creating protocol related objects.
+
+ https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md
+
+ Useful examples are `vim.lsp.protocol.ErrorCodes`. These objects allow
+ reverse lookup by either the number or string name.
+
+ e.g. vim.lsp.protocol.TextDocumentSyncKind.Full == 1
+ vim.lsp.protocol.TextDocumentSyncKind[1] == "Full"
+
+ Utility functions used internally are:
+ `vim.lsp.make_client_capabilities()`
+ Make a ClientCapabilities object. These are the builtin
+ capabilities.
+ `vim.lsp.make_text_document_position_params()`
+ Make a TextDocumentPositionParams object.
+ `vim.lsp.resolve_capabilities(server_capabilites)`
+ Creates a normalized object describing capabilities from the server
+ capabilities.
+
+================================================================================
+ *vim.lsp.util*
+
+TODO: Describe the utils here for handling/applying things from LSP.
+
+================================================================================
+ *lsp-buf-methods*
+There are methods which operate on the buffer level for all of the active
+clients attached to the buffer.
+
+ *vim.lsp.buf_request()*
+vim.lsp.buf_request({bufnr}, {method}, {params}, [{callback}])
+ Send a async request for all the clients active and attached to the buffer.
+
+ Parameters: ~
+ {bufnr}: The buffer handle or 0 for the current buffer.
+
+ {method}: The LSP method name.
+
+ {params}: The parameters to send.
+
+ {callback}: An optional `function(err, method, params, client_id)` which
+ will be called for this request. If you do not specify it, then it will
+ use the client's callback in {client.callbacks}. See |lsp-callbacks| for
+ more information.
+
+ Returns:~
+
+ A table from client id to the request id for all of the successful
+ requests.
+
+ The second result is a function which can be used to cancel all the
+ requests. You can do this individually with `client.cancel_request()`
+
+ *vim.lsp.buf_request_sync()*
+vim.lsp.buf_request_sync({bufnr}, {method}, {params}, [{timeout_ms}])
+ Calls |vim.lsp.buf_request()|, but it will wait for the result and block Vim
+ in the process.
+ The parameters are the same as |vim.lsp.buf_request()|, but the return
+ result is different.
+ It will wait maximum of {timeout_ms} which defaults to 100ms.
+
+ Returns:~
+
+ If the timeout is exceeded or a cancel is sent or an error, it will cancel
+ the request and return `nil, err` where `err` is a string that describes
+ the reason why it failed.
+
+ If it is successful, it will return a table from client id to result id.
+
+ *vim.lsp.buf_notify()*
+vim.lsp.buf_notify({bufnr}, {method}, {params})
+ Send a notification to all servers on the buffer.
+
+ Parameters: ~
+ {bufnr}: The buffer handle or 0 for the current buffer.
+
+ {method}: The LSP method name.
+
+ {params}: The parameters to send.
+
+================================================================================
+ *lsp-logging*
+
+ *lsp#set_log_level()*
+lsp#set_log_level({level})
+ You can set the log level for language server client logging.
+ Possible values: "trace", "debug", "info", "warn", "error"
+
+ Default: "warn"
+
+ Example: `call lsp#set_log_level("debug")`
+
+ *lsp#get_log_path()*
+ *vim.lsp.get_log_path()*
+lsp#get_log_path()
+vim.lsp.get_log_path()
+ Returns the path that LSP logs are written.
+
+ *vim.lsp.log_levels*
+vim.lsp.log_levels
+ Log level dictionary with reverse lookup as well.
+
+ Can be used to lookup the number from the name or the name from the number.
+ Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
+ Level numbers begin with 'trace' at 0
+
+================================================================================
+ *lsp-omnifunc*
+ *vim.lsp.omnifunc()*
+ *lsp#omnifunc*
+lsp#omnifunc({findstart}, {base})
+vim.lsp.omnifunc({findstart}, {base})
+
+To configure omnifunc, add the following in your init.vim:
+>
+ set omnifunc=lsp#omnifunc
+
+ " This is optional, but you may find it useful
+ autocmd CompleteDone * pclose
+<
+================================================================================
+ *lsp-vim-functions*
+
+These methods can be used in mappings and are the equivalent of using the
+request from lua as follows:
+
+>
+ lua vim.lsp.buf_request(0, "textDocument/hover", vim.lsp.protocol.make_text_document_position_params())
+<
+
+ lsp#text_document_declaration()
+ lsp#text_document_definition()
+ lsp#text_document_hover()
+ lsp#text_document_implementation()
+ lsp#text_document_signature_help()
+ lsp#text_document_type_definition()
+
+>
+ " Example config
+ autocmd Filetype rust,python,go,c,cpp setl omnifunc=lsp#omnifunc
+ nnoremap <silent> ;dc :call lsp#text_document_declaration()<CR>
+ nnoremap <silent> ;df :call lsp#text_document_definition()<CR>
+ nnoremap <silent> ;h :call lsp#text_document_hover()<CR>
+ nnoremap <silent> ;i :call lsp#text_document_implementation()<CR>
+ nnoremap <silent> ;s :call lsp#text_document_signature_help()<CR>
+ nnoremap <silent> ;td :call lsp#text_document_type_definition()<CR>
+<
+================================================================================
+ *lsp-advanced-js-example*
+
+For more advanced configurations where just filtering by filetype isn't
+sufficient, you can use the `vim.lsp.start_client()` and
+`vim.lsp.buf_attach_client()` commands to easily customize the configuration
+however you please. For example, if you want to do your own filtering, or
+start a new LSP client based on the root directory for if you plan to work
+with multiple projects in a single session. Below is a fully working Lua
+example which can do exactly that.
+
+The example will:
+1. Check for each new buffer whether or not we want to start an LSP client.
+2. Try to find a root directory by ascending from the buffer's path.
+3. Create a new LSP for that root directory if one doesn't exist.
+4. Attach the buffer to the client for that root directory.
+
+>
+ -- Some path manipulation utilities
+ local function is_dir(filename)
+ local stat = vim.loop.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+ end
+
+ local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/"
+ -- Asumes filepath is a file.
+ local function dirname(filepath)
+ local is_changed = false
+ local result = filepath:gsub(path_sep.."([^"..path_sep.."]+)$", function()
+ is_changed = true
+ return ""
+ end)
+ return result, is_changed
+ end
+
+ local function path_join(...)
+ return table.concat(vim.tbl_flatten {...}, path_sep)
+ end
+
+ -- Ascend the buffer's path until we find the rootdir.
+ -- is_root_path is a function which returns bool
+ local function buffer_find_root_dir(bufnr, is_root_path)
+ local bufname = vim.api.nvim_buf_get_name(bufnr)
+ if vim.fn.filereadable(bufname) == 0 then
+ return nil
+ end
+ local dir = bufname
+ -- Just in case our algo is buggy, don't infinite loop.
+ for _ = 1, 100 do
+ local did_change
+ dir, did_change = dirname(dir)
+ if is_root_path(dir, bufname) then
+ return dir, bufname
+ end
+ -- If we can't ascend further, then stop looking.
+ if not did_change then
+ return nil
+ end
+ end
+ end
+
+ -- A table to store our root_dir to client_id lookup. We want one LSP per
+ -- root directory, and this is how we assert that.
+ local javascript_lsps = {}
+ -- Which filetypes we want to consider.
+ local javascript_filetypes = {
+ ["javascript.jsx"] = true;
+ ["javascript"] = true;
+ ["typescript"] = true;
+ ["typescript.jsx"] = true;
+ }
+
+ -- Create a template configuration for a server to start, minus the root_dir
+ -- which we will specify later.
+ local javascript_lsp_config = {
+ name = "javascript";
+ cmd = { path_join(os.getenv("JAVASCRIPT_LANGUAGE_SERVER_DIRECTORY"), "lib", "language-server-stdio.js") };
+ }
+
+ -- This needs to be global so that we can call it from the autocmd.
+ function check_start_javascript_lsp()
+ local bufnr = vim.api.nvim_get_current_buf()
+ -- Filter which files we are considering.
+ if not javascript_filetypes[vim.api.nvim_buf_get_option(bufnr, 'filetype')] then
+ return
+ end
+ -- Try to find our root directory. We will define this as a directory which contains
+ -- node_modules. Another choice would be to check for `package.json`, or for `.git`.
+ local root_dir = buffer_find_root_dir(bufnr, function(dir)
+ return is_dir(path_join(dir, 'node_modules'))
+ -- return vim.fn.filereadable(path_join(dir, 'package.json')) == 1
+ -- return is_dir(path_join(dir, '.git'))
+ end)
+ -- We couldn't find a root directory, so ignore this file.
+ if not root_dir then return end
+
+ -- Check if we have a client alredy or start and store it.
+ local client_id = javascript_lsps[root_dir]
+ if not client_id then
+ local new_config = vim.tbl_extend("error", javascript_lsp_config, {
+ root_dir = root_dir;
+ })
+ client_id = vim.lsp.start_client(new_config)
+ javascript_lsps[root_dir] = client_id
+ end
+ -- Finally, attach to the buffer to track changes. This will do nothing if we
+ -- are already attached.
+ vim.lsp.buf_attach_client(bufnr, client_id)
+ end
+
+ vim.api.nvim_command [[autocmd BufReadPost * lua check_start_javascript_lsp()]]
+<
+
+vim:tw=78:ts=8:ft=help:norl:
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
new file mode 100644
index 0000000000..edcf246295
--- /dev/null
+++ b/runtime/doc/lua.txt
@@ -0,0 +1,994 @@
+*lua.txt* Nvim
+
+
+ NVIM REFERENCE MANUAL
+
+
+Lua engine *lua* *Lua*
+
+ Type |gO| to see the table of contents.
+
+==============================================================================
+Introduction *lua-intro*
+
+The Lua 5.1 language is builtin and always available. Try this command to get
+an idea of what lurks beneath: >
+
+ :lua print(vim.inspect(package.loaded))
+
+Nvim includes a "standard library" |lua-stdlib| for Lua. It complements the
+"editor stdlib" (|functions| and Ex commands) and the |API|, all of which can
+be used from Lua code.
+
+Module conflicts are resolved by "last wins". For example if both of these
+are on 'runtimepath':
+ runtime/lua/foo.lua
+ ~/.config/nvim/lua/foo.lua
+then `require('foo')` loads "~/.config/nvim/lua/foo.lua", and
+"runtime/lua/foo.lua" is not used. See |lua-require| to understand how Nvim
+finds and loads Lua modules. The conventions are similar to VimL plugins,
+with some extra features. See |lua-require-example| for a walkthrough.
+
+==============================================================================
+Importing Lua modules *lua-require*
+
+ *lua-package-path*
+Nvim automatically adjusts `package.path` and `package.cpath` according to
+effective 'runtimepath' value. Adjustment happens whenever 'runtimepath' is
+changed. `package.path` is adjusted by simply appending `/lua/?.lua` and
+`/lua/?/init.lua` to each directory from 'runtimepath' (`/` is actually the
+first character of `package.config`).
+
+Similarly to `package.path`, modified directories from 'runtimepath' are also
+added to `package.cpath`. In this case, instead of appending `/lua/?.lua` and
+`/lua/?/init.lua` to each runtimepath, all unique `?`-containing suffixes of
+the existing `package.cpath` are used. Example:
+
+1. Given that
+ - 'runtimepath' contains `/foo/bar,/xxx;yyy/baz,/abc`;
+ - initial (defined at compile-time or derived from
+ `$LUA_CPATH`/`$LUA_INIT`) `package.cpath` contains
+ `./?.so;/def/ghi/a?d/j/g.elf;/def/?.so`.
+2. It finds `?`-containing suffixes `/?.so`, `/a?d/j/g.elf` and `/?.so`, in
+ order: parts of the path starting from the first path component containing
+ question mark and preceding path separator.
+3. The suffix of `/def/?.so`, namely `/?.so` is not unique, as it’s the same
+ as the suffix of the first path from `package.path` (i.e. `./?.so`). Which
+ leaves `/?.so` and `/a?d/j/g.elf`, in this order.
+4. 'runtimepath' has three paths: `/foo/bar`, `/xxx;yyy/baz` and `/abc`. The
+ second one contains semicolon which is a paths separator so it is out,
+ leaving only `/foo/bar` and `/abc`, in order.
+5. The cartesian product of paths from 4. and suffixes from 3. is taken,
+ giving four variants. In each variant `/lua` path segment is inserted
+ between path and suffix, leaving
+
+ - `/foo/bar/lua/?.so`
+ - `/foo/bar/lua/a?d/j/g.elf`
+ - `/abc/lua/?.so`
+ - `/abc/lua/a?d/j/g.elf`
+
+6. New paths are prepended to the original `package.cpath`.
+
+The result will look like this:
+
+ `/foo/bar,/xxx;yyy/baz,/abc` ('runtimepath')
+ × `./?.so;/def/ghi/a?d/j/g.elf;/def/?.so` (`package.cpath`)
+
+ = `/foo/bar/lua/?.so;/foo/bar/lua/a?d/j/g.elf;/abc/lua/?.so;/abc/lua/a?d/j/g.elf;./?.so;/def/ghi/a?d/j/g.elf;/def/?.so`
+
+Note:
+
+- To track 'runtimepath' updates, paths added at previous update are
+ remembered and removed at the next update, while all paths derived from the
+ new 'runtimepath' are prepended as described above. This allows removing
+ paths when path is removed from 'runtimepath', adding paths when they are
+ added and reordering `package.path`/`package.cpath` content if 'runtimepath'
+ was reordered.
+
+- Although adjustments happen automatically, Nvim does not track current
+ values of `package.path` or `package.cpath`. If you happen to delete some
+ paths from there you can set 'runtimepath' to trigger an update: >
+ let &runtimepath = &runtimepath
+
+- Skipping paths from 'runtimepath' which contain semicolons applies both to
+ `package.path` and `package.cpath`. Given that there are some badly written
+ plugins using shell which will not work with paths containing semicolons it
+ is better to not have them in 'runtimepath' at all.
+
+------------------------------------------------------------------------------
+LUA PLUGIN EXAMPLE *lua-require-example*
+
+The following example plugin adds a command `:MakeCharBlob` which transforms
+current buffer into a long `unsigned char` array. Lua contains transformation
+function in a module `lua/charblob.lua` which is imported in
+`autoload/charblob.vim` (`require("charblob")`). Example plugin is supposed
+to be put into any directory from 'runtimepath', e.g. `~/.config/nvim` (in
+this case `lua/charblob.lua` means `~/.config/nvim/lua/charblob.lua`).
+
+autoload/charblob.vim: >
+
+ function charblob#encode_buffer()
+ call setline(1, luaeval(
+ \ 'require("charblob").encode(unpack(_A))',
+ \ [getline(1, '$'), &textwidth, ' ']))
+ endfunction
+
+plugin/charblob.vim: >
+
+ if exists('g:charblob_loaded')
+ finish
+ endif
+ let g:charblob_loaded = 1
+
+ command MakeCharBlob :call charblob#encode_buffer()
+
+lua/charblob.lua: >
+
+ local function charblob_bytes_iter(lines)
+ local init_s = {
+ next_line_idx = 1,
+ next_byte_idx = 1,
+ lines = lines,
+ }
+ local function next(s, _)
+ if lines[s.next_line_idx] == nil then
+ return nil
+ end
+ if s.next_byte_idx > #(lines[s.next_line_idx]) then
+ s.next_line_idx = s.next_line_idx + 1
+ s.next_byte_idx = 1
+ return ('\n'):byte()
+ end
+ local ret = lines[s.next_line_idx]:byte(s.next_byte_idx)
+ if ret == ('\n'):byte() then
+ ret = 0 -- See :h NL-used-for-NUL.
+ end
+ s.next_byte_idx = s.next_byte_idx + 1
+ return ret
+ end
+ return next, init_s, nil
+ end
+
+ local function charblob_encode(lines, textwidth, indent)
+ local ret = {
+ 'const unsigned char blob[] = {',
+ indent,
+ }
+ for byte in charblob_bytes_iter(lines) do
+ -- .- space + number (width 3) + comma
+ if #(ret[#ret]) + 5 > textwidth then
+ ret[#ret + 1] = indent
+ else
+ ret[#ret] = ret[#ret] .. ' '
+ end
+ ret[#ret] = ret[#ret] .. (('%3u,'):format(byte))
+ end
+ ret[#ret + 1] = '};'
+ return ret
+ end
+
+ return {
+ bytes_iter = charblob_bytes_iter,
+ encode = charblob_encode,
+ }
+
+==============================================================================
+Commands *lua-commands*
+
+These commands execute a Lua chunk from either the command line (:lua, :luado)
+or a file (:luafile) on the given line [range]. As always in Lua, each chunk
+has its own scope (closure), so only global variables are shared between
+command calls. The |lua-stdlib| modules, user modules, and anything else on
+|lua-package-path| are available.
+
+The Lua print() function redirects its output to the Nvim message area, with
+arguments separated by " " (space) instead of "\t" (tab).
+
+ *:lua*
+:[range]lua {chunk}
+ Executes Lua chunk {chunk}.
+
+ Examples: >
+ :lua vim.api.nvim_command('echo "Hello, Nvim!"')
+< To see the Lua version: >
+ :lua print(_VERSION)
+< To see the LuaJIT version: >
+ :lua print(jit.version)
+<
+ *:lua-heredoc*
+:[range]lua << [endmarker]
+{script}
+{endmarker}
+ Executes Lua script {script} from within Vimscript.
+ {endmarker} must NOT be preceded by whitespace. You
+ can omit [endmarker] after the "<<" and use a dot "."
+ after {script} (similar to |:append|, |:insert|).
+
+ Example:
+ >
+ function! CurrentLineInfo()
+ lua << EOF
+ local linenr = vim.api.nvim_win_get_cursor(0)[1]
+ local curline = vim.api.nvim_buf_get_lines(
+ 0, linenr, linenr + 1, false)[1]
+ print(string.format("Current line [%d] has %d bytes",
+ linenr, #curline))
+ EOF
+ endfunction
+
+< Note that the `local` variables will disappear when
+ the block finishes. But not globals.
+
+ *:luado*
+:[range]luado {body} Executes Lua chunk "function(line, linenr) {body} end"
+ for each buffer line in [range], where `line` is the
+ current line text (without <EOL>), and `linenr` is the
+ current line number. If the function returns a string
+ that becomes the text of the corresponding buffer
+ line. Default [range] is the whole file: "1,$".
+
+ Examples:
+ >
+ :luado return string.format("%s\t%d", line:reverse(), #line)
+
+ :lua require"lpeg"
+ :lua -- balanced parenthesis grammar:
+ :lua bp = lpeg.P{ "(" * ((1 - lpeg.S"()") + lpeg.V(1))^0 * ")" }
+ :luado if bp:match(line) then return "-->\t" .. line end
+<
+
+ *:luafile*
+:[range]luafile {file}
+ Execute Lua script in {file}.
+ The whole argument is used as a single file name.
+
+ Examples:
+ >
+ :luafile script.lua
+ :luafile %
+<
+
+==============================================================================
+luaeval() *lua-eval* *luaeval()*
+
+The (dual) equivalent of "vim.eval" for passing Lua values to Nvim is
+"luaeval". "luaeval" takes an expression string and an optional argument used
+for _A inside expression and returns the result of the expression. It is
+semantically equivalent in Lua to:
+>
+ local chunkheader = "local _A = select(1, ...) return "
+ function luaeval (expstr, arg)
+ local chunk = assert(loadstring(chunkheader .. expstr, "luaeval"))
+ return chunk(arg) -- return typval
+ end
+
+Lua nils, numbers, strings, tables and booleans are converted to their
+respective VimL types. An error is thrown if conversion of any other Lua types
+is attempted.
+
+The magic global "_A" contains the second argument to luaeval().
+
+Example: >
+ :echo luaeval('_A[1] + _A[2]', [40, 2])
+ 42
+ :echo luaeval('string.match(_A, "[a-z]+")', 'XYXfoo123')
+ foo
+
+Lua tables are used as both dictionaries and lists, so it is impossible to
+determine whether empty table is meant to be empty list or empty dictionary.
+Additionally Lua does not have integer numbers. To distinguish between these
+cases there is the following agreement:
+
+0. Empty table is empty list.
+1. Table with N incrementally growing integral numbers, starting from 1 and
+ ending with N is considered to be a list.
+2. Table with string keys, none of which contains NUL byte, is considered to
+ be a dictionary.
+3. Table with string keys, at least one of which contains NUL byte, is also
+ considered to be a dictionary, but this time it is converted to
+ a |msgpack-special-map|.
+ *lua-special-tbl*
+4. Table with `vim.type_idx` key may be a dictionary, a list or floating-point
+ value:
+ - `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to
+ a floating-point 1.0. Note that by default integral Lua numbers are
+ converted to |Number|s, non-integral are converted to |Float|s. This
+ variant allows integral |Float|s.
+ - `{[vim.type_idx]=vim.types.dictionary}` is converted to an empty
+ dictionary, `{[vim.type_idx]=vim.types.dictionary, [42]=1, a=2}` is
+ converted to a dictionary `{'a': 42}`: non-string keys are ignored.
+ Without `vim.type_idx` key tables with keys not fitting in 1., 2. or 3.
+ are errors.
+ - `{[vim.type_idx]=vim.types.list}` is converted to an empty list. As well
+ as `{[vim.type_idx]=vim.types.list, [42]=1}`: integral keys that do not
+ form a 1-step sequence from 1 to N are ignored, as well as all
+ non-integral keys.
+
+Examples: >
+
+ :echo luaeval('math.pi')
+ :function Rand(x,y) " random uniform between x and y
+ : return luaeval('(_A.y-_A.x)*math.random()+_A.x', {'x':a:x,'y':a:y})
+ : endfunction
+ :echo Rand(1,10)
+
+Note: second argument to `luaeval` undergoes VimL to Lua conversion
+("marshalled"), so changes to Lua containers do not affect values in VimL.
+Return value is also always converted. When converting,
+|msgpack-special-dict|s are treated specially.
+
+==============================================================================
+Vimscript v:lua interface *v:lua-call*
+
+From Vimscript the special `v:lua` prefix can be used to call Lua functions
+which are global or accessible from global tables. The expression >
+ v:lua.func(arg1, arg2)
+is equivalent to the Lua chunk >
+ return func(...)
+where the args are converted to Lua values. The expression >
+ v:lua.somemod.func(args)
+is equivalent to the Lua chunk >
+ return somemod.func(...)
+
+You can use `v:lua` in "func" options like 'tagfunc', 'omnifunc', etc.
+For example consider the following Lua omnifunc handler: >
+
+ function mymod.omnifunc(findstart, base)
+ if findstart == 1 then
+ return 0
+ else
+ return {'stuff', 'steam', 'strange things'}
+ end
+ end
+ vim.api.nvim_buf_set_option(0, 'omnifunc', 'v:lua.mymod.omnifunc')
+
+Note: the module ("mymod" in the above example) must be a Lua global.
+
+Note: `v:lua` without a call is not allowed in a Vimscript expression:
+|Funcref|s cannot represent Lua functions. The following are errors: >
+
+ let g:Myvar = v:lua.myfunc " Error
+ call SomeFunc(v:lua.mycallback) " Error
+ let g:foo = v:lua " Error
+ let g:foo = v:['lua'] " Error
+
+
+==============================================================================
+Lua standard modules *lua-stdlib*
+
+The Nvim Lua "standard library" (stdlib) is the `vim` module, which exposes
+various functions and sub-modules. It is always loaded, thus require("vim")
+is unnecessary.
+
+You can peek at the module properties: >
+
+ :lua print(vim.inspect(vim))
+
+Result is something like this: >
+
+ {
+ _os_proc_children = <function 1>,
+ _os_proc_info = <function 2>,
+ ...
+ api = {
+ nvim__id = <function 5>,
+ nvim__id_array = <function 6>,
+ ...
+ },
+ deepcopy = <function 106>,
+ gsplit = <function 107>,
+ ...
+ }
+
+To find documentation on e.g. the "deepcopy" function: >
+
+ :help vim.deepcopy()
+
+Note that underscore-prefixed functions (e.g. "_os_proc_children") are
+internal/private and must not be used by plugins.
+
+------------------------------------------------------------------------------
+VIM.LOOP *lua-loop* *vim.loop*
+
+`vim.loop` exposes all features of the Nvim event-loop. This is a low-level
+API that provides functionality for networking, filesystem, and process
+management. Try this command to see available functions: >
+
+ :lua print(vim.inspect(vim.loop))
+
+Reference: http://docs.libuv.org
+Examples: https://github.com/luvit/luv/tree/master/examples
+
+ *E5560* *lua-loop-callbacks*
+It is an error to directly invoke `vim.api` functions (except |api-fast|) in
+`vim.loop` callbacks. For example, this is an error: >
+
+ local timer = vim.loop.new_timer()
+ timer:start(1000, 0, function()
+ vim.api.nvim_command('echomsg "test"')
+ end)
+
+To avoid the error use |vim.schedule_wrap()| to defer the callback: >
+
+ local timer = vim.loop.new_timer()
+ timer:start(1000, 0, vim.schedule_wrap(function()
+ vim.api.nvim_command('echomsg "test"')
+ end))
+
+Example: repeating timer
+ 1. Save this code to a file.
+ 2. Execute it with ":luafile %". >
+
+ -- Create a timer handle (implementation detail: uv_timer_t).
+ local timer = vim.loop.new_timer()
+ local i = 0
+ -- Waits 1000ms, then repeats every 750ms until timer:close().
+ timer:start(1000, 750, function()
+ print('timer invoked! i='..tostring(i))
+ if i > 4 then
+ timer:close() -- Always close handles to avoid leaks.
+ end
+ i = i + 1
+ end)
+ print('sleeping');
+
+
+Example: File-change detection *watch-file*
+ 1. Save this code to a file.
+ 2. Execute it with ":luafile %".
+ 3. Use ":Watch %" to watch any file.
+ 4. Try editing the file from another text editor.
+ 5. Observe that the file reloads in Nvim (because on_change() calls
+ |:checktime|). >
+
+ local w = vim.loop.new_fs_event()
+ local function on_change(err, fname, status)
+ -- Do work...
+ vim.api.nvim_command('checktime')
+ -- Debounce: stop/start.
+ w:stop()
+ watch_file(fname)
+ end
+ function watch_file(fname)
+ local fullpath = vim.api.nvim_call_function(
+ 'fnamemodify', {fname, ':p'})
+ w:start(fullpath, {}, vim.schedule_wrap(function(...)
+ on_change(...) end))
+ end
+ vim.api.nvim_command(
+ "command! -nargs=1 Watch call luaeval('watch_file(_A)', expand('<args>'))")
+
+
+Example: TCP echo-server *tcp-server*
+ 1. Save this code to a file.
+ 2. Execute it with ":luafile %".
+ 3. Note the port number.
+ 4. Connect from any TCP client (e.g. "nc 0.0.0.0 36795"): >
+
+ local function create_server(host, port, on_connect)
+ local server = vim.loop.new_tcp()
+ server:bind(host, port)
+ server:listen(128, function(err)
+ assert(not err, err) -- Check for errors.
+ local sock = vim.loop.new_tcp()
+ server:accept(sock) -- Accept client connection.
+ on_connect(sock) -- Start reading messages.
+ end)
+ return server
+ end
+ local server = create_server('0.0.0.0', 0, function(sock)
+ sock:read_start(function(err, chunk)
+ assert(not err, err) -- Check for errors.
+ if chunk then
+ sock:write(chunk) -- Echo received messages to the channel.
+ else -- EOF (stream closed).
+ sock:close() -- Always close handles to avoid leaks.
+ end
+ end)
+ end)
+ print('TCP echo-server listening on port: '..server:getsockname().port)
+
+------------------------------------------------------------------------------
+VIM.TREESITTER *lua-treesitter*
+
+Nvim integrates the tree-sitter library for incremental parsing of buffers.
+
+Currently Nvim does not provide the tree-sitter parsers, instead these must
+be built separately, for instance using the tree-sitter utility.
+The parser is loaded into nvim using >
+
+ vim.treesitter.add_language("/path/to/c_parser.so", "c")
+
+<Create a parser for a buffer and a given language (if another plugin uses the
+same buffer/language combination, it will be safely reused). Use >
+
+ parser = vim.treesitter.get_parser(bufnr, lang)
+
+<`bufnr=0` can be used for current buffer. `lang` will default to 'filetype' (this
+doesn't work yet for some filetypes like "cpp") Currently, the parser will be
+retained for the lifetime of a buffer but this is subject to change. A plugin
+should keep a reference to the parser object as long as it wants incremental
+updates.
+
+Whenever you need to access the current syntax tree, parse the buffer: >
+
+ tstree = parser:parse()
+
+<This will return an immutable tree that represents the current state of the
+buffer. When the plugin wants to access the state after a (possible) edit
+it should call `parse()` again. If the buffer wasn't edited, the same tree will
+be returned again without extra work. If the buffer was parsed before,
+incremental parsing will be done of the changed parts.
+
+NB: to use the parser directly inside a |nvim_buf_attach| Lua callback, you must
+call `get_parser()` before you register your callback. But preferably parsing
+shouldn't be done directly in the change callback anyway as they will be very
+frequent. Rather a plugin that does any kind of analysis on a tree should use
+a timer to throttle too frequent updates.
+
+Tree methods *lua-treesitter-tree*
+
+tstree:root() *tstree:root()*
+ Return the root node of this tree.
+
+
+Node methods *lua-treesitter-node*
+
+tsnode:parent() *tsnode:parent()*
+ Get the node's immediate parent.
+
+tsnode:child_count() *tsnode:child_count()*
+ Get the node's number of children.
+
+tsnode:child(N) *tsnode:child()*
+ Get the node's child at the given index, where zero represents the
+ first child.
+
+tsnode:named_child_count() *tsnode:named_child_count()*
+ Get the node's number of named children.
+
+tsnode:named_child(N) *tsnode:named_child()*
+ Get the node's named child at the given index, where zero represents
+ the first named child.
+
+tsnode:start() *tsnode:start()*
+ Get the node's start position. Return three values: the row, column
+ and total byte count (all zero-based).
+
+tsnode:end_() *tsnode:end_()*
+ Get the node's end position. Return three values: the row, column
+ and total byte count (all zero-based).
+
+tsnode:range() *tsnode:range()*
+ Get the range of the node. Return four values: the row, column
+ of the start position, then the row, column of the end position.
+
+tsnode:type() *tsnode:type()*
+ Get the node's type as a string.
+
+tsnode:symbol() *tsnode:symbol()*
+ Get the node's type as a numerical id.
+
+tsnode:named() *tsnode:named()*
+ Check if the node is named. Named nodes correspond to named rules in
+ the grammar, whereas anonymous nodes correspond to string literals
+ in the grammar.
+
+tsnode:missing() *tsnode:missing()*
+ Check if the node is missing. Missing nodes are inserted by the
+ parser in order to recover from certain kinds of syntax errors.
+
+tsnode:has_error() *tsnode:has_error()*
+ Check if the node is a syntax error or contains any syntax errors.
+
+tsnode:sexpr() *tsnode:sexpr()*
+ Get an S-expression representing the node as a string.
+
+tsnode:descendant_for_range(start_row, start_col, end_row, end_col)
+ *tsnode:descendant_for_range()*
+ Get the smallest node within this node that spans the given range of
+ (row, column) positions
+
+tsnode:named_descendant_for_range(start_row, start_col, end_row, end_col)
+ *tsnode:named_descendant_for_range()*
+ Get the smallest named node within this node that spans the given
+ range of (row, column) positions
+
+------------------------------------------------------------------------------
+VIM *lua-builtin*
+
+vim.api.{func}({...}) *vim.api*
+ Invokes Nvim |API| function {func} with arguments {...}.
+ Example: call the "nvim_get_current_line()" API function: >
+ print(tostring(vim.api.nvim_get_current_line()))
+
+vim.call({func}, {...}) *vim.call()*
+ Invokes |vim-function| or |user-function| {func} with arguments {...}.
+ See also |vim.fn|. Equivalent to: >
+ vim.fn[func]({...})
+
+vim.in_fast_event() *vim.in_fast_event()*
+ Returns true if the code is executing as part of a "fast" event
+ handler, where most of the API is disabled. These are low-level events
+ (e.g. |lua-loop-callbacks|) which can be invoked whenever Nvim polls
+ for input. When this is `false` most API functions are callable (but
+ may be subject to other restrictions such as |textlock|).
+
+vim.NIL *vim.NIL*
+ Special value used to represent NIL in msgpack-rpc and |v:null| in
+ vimL interaction, and similar cases. Lua `nil` cannot be used as
+ part of a lua table representing a Dictionary or Array, as it
+ is equivalent to a missing value: `{"foo", nil}` is the same as
+ `{"foo"}`
+
+vim.rpcnotify({channel}, {method}[, {args}...]) *vim.rpcnotify()*
+ Sends {event} to {channel} via |RPC| and returns immediately.
+ If {channel} is 0, the event is broadcast to all channels.
+
+ This function also works in a fast callback |lua-loop-callbacks|.
+
+vim.rpcrequest({channel}, {method}[, {args}...]) *vim.rpcrequest()*
+ Sends a request to {channel} to invoke {method} via
+ |RPC| and blocks until a response is received.
+
+ Note: NIL values as part of the return value is represented as
+ |vim.NIL| special value
+
+vim.stricmp({a}, {b}) *vim.stricmp()*
+ Compares strings case-insensitively. Returns 0, 1 or -1 if strings
+ are equal, {a} is greater than {b} or {a} is lesser than {b},
+ respectively.
+
+vim.str_utfindex({str}[, {index}]) *vim.str_utfindex()*
+ Convert byte index to UTF-32 and UTF-16 indicies. If {index} is not
+ supplied, the length of the string is used. All indicies are zero-based.
+ Returns two values: the UTF-32 and UTF-16 indicies respectively.
+
+ Embedded NUL bytes are treated as terminating the string. Invalid
+ UTF-8 bytes, and embedded surrogates are counted as one code
+ point each. An {index} in the middle of a UTF-8 sequence is rounded
+ upwards to the end of that sequence.
+
+vim.str_byteindex({str}, {index}[, {use_utf16}]) *vim.str_byteindex()*
+ Convert UTF-32 or UTF-16 {index} to byte index. If {use_utf16} is not
+ supplied, it defaults to false (use UTF-32). Returns the byte index.
+
+ Invalid UTF-8 and NUL is treated like by |vim.str_byteindex()|. An {index}
+ in the middle of a UTF-16 sequence is rounded upwards to the end of that
+ sequence.
+
+vim.schedule({callback}) *vim.schedule()*
+ Schedules {callback} to be invoked soon by the main event-loop. Useful
+ to avoid |textlock| or other temporary restrictions.
+
+vim.fn.{func}({...}) *vim.fn*
+ Invokes |vim-function| or |user-function| {func} with arguments {...}.
+ To call autoload functions, use the syntax: >
+ vim.fn['some#function']({...})
+<
+ Unlike vim.api.|nvim_call_function| this converts directly between Vim
+ objects and Lua objects. If the Vim function returns a float, it will
+ be represented directly as a Lua number. Empty lists and dictionaries
+ both are represented by an empty table.
+
+ Note: |v:null| values as part of the return value is represented as
+ |vim.NIL| special value
+
+ Note: vim.fn keys are generated lazily, thus `pairs(vim.fn)` only
+ enumerates functions that were called at least once.
+
+vim.type_idx *vim.type_idx*
+ Type index for use in |lua-special-tbl|. Specifying one of the
+ values from |vim.types| allows typing the empty table (it is
+ unclear whether empty Lua table represents empty list or empty array)
+ and forcing integral numbers to be |Float|. See |lua-special-tbl| for
+ more details.
+
+vim.val_idx *vim.val_idx*
+ Value index for tables representing |Float|s. A table representing
+ floating-point value 1.0 looks like this: >
+ {
+ [vim.type_idx] = vim.types.float,
+ [vim.val_idx] = 1.0,
+ }
+< See also |vim.type_idx| and |lua-special-tbl|.
+
+vim.types *vim.types*
+ Table with possible values for |vim.type_idx|. Contains two sets
+ of key-value pairs: first maps possible values for |vim.type_idx|
+ to human-readable strings, second maps human-readable type names to
+ values for |vim.type_idx|. Currently contains pairs for `float`,
+ `array` and `dictionary` types.
+
+ Note: one must expect that values corresponding to `vim.types.float`,
+ `vim.types.array` and `vim.types.dictionary` fall under only two
+ following assumptions:
+ 1. Value may serve both as a key and as a value in a table. Given the
+ properties of Lua tables this basically means “value is not `nil`”.
+ 2. For each value in `vim.types` table `vim.types[vim.types[value]]`
+ is the same as `value`.
+ No other restrictions are put on types, and it is not guaranteed that
+ values corresponding to `vim.types.float`, `vim.types.array` and
+ `vim.types.dictionary` will not change or that `vim.types` table will
+ only contain values for these three types.
+
+==============================================================================
+Lua module: vim *lua-vim*
+
+inspect({object}, {options}) *vim.inspect()*
+ Return a human-readable representation of the given object.
+
+ See also: ~
+ https://github.com/kikito/inspect.lua
+ https://github.com/mpeterv/vinspect
+
+paste({lines}, {phase}) *vim.paste()*
+ Paste handler, invoked by |nvim_paste()| when a conforming UI
+ (such as the |TUI|) pastes text into the editor.
+
+ Example: To remove ANSI color codes when pasting: >
+
+ vim.paste = (function(overridden)
+ return function(lines, phase)
+ for i,line in ipairs(lines) do
+ -- Scrub ANSI color codes from paste input.
+ lines[i] = line:gsub('\27%[[0-9;mK]+', '')
+ end
+ overridden(lines, phase)
+ end
+ end)(vim.paste)
+<
+
+ Parameters: ~
+ {lines} |readfile()|-style list of lines to paste.
+ |channel-lines|
+ {phase} -1: "non-streaming" paste: the call contains all
+ lines. If paste is "streamed", `phase` indicates the stream state:
+ • 1: starts the paste (exactly once)
+ • 2: continues the paste (zero or more times)
+ • 3: ends the paste (exactly once)
+
+ Return: ~
+ false if client should cancel the paste.
+
+ See also: ~
+ |paste|
+
+schedule_wrap({cb}) *vim.schedule_wrap()*
+ Defers callback `cb` until the Nvim API is safe to call.
+
+ See also: ~
+ |lua-loop-callbacks|
+ |vim.schedule()|
+ |vim.in_fast_event()|
+
+
+
+
+deepcopy({orig}) *vim.deepcopy()*
+ Returns a deep copy of the given object. Non-table objects are
+ copied as in a typical Lua assignment, whereas table objects
+ are copied recursively.
+
+ Parameters: ~
+ {orig} Table to copy
+
+ Return: ~
+ New table of copied keys and (nested) values.
+
+gsplit({s}, {sep}, {plain}) *vim.gsplit()*
+ Splits a string at each instance of a separator.
+
+ Parameters: ~
+ {s} String to split
+ {sep} Separator string or pattern
+ {plain} If `true` use `sep` literally (passed to
+ String.find)
+
+ Return: ~
+ Iterator over the split components
+
+ See also: ~
+ |vim.split()|
+ https://www.lua.org/pil/20.2.html
+ http://lua-users.org/wiki/StringLibraryTutorial
+
+split({s}, {sep}, {plain}) *vim.split()*
+ Splits a string at each instance of a separator.
+
+ Examples: >
+ split(":aa::b:", ":") --> {'','aa','','bb',''}
+ split("axaby", "ab?") --> {'','x','y'}
+ split(x*yz*o, "*", true) --> {'x','yz','o'}
+<
+
+ Parameters: ~
+ {s} String to split
+ {sep} Separator string or pattern
+ {plain} If `true` use `sep` literally (passed to
+ String.find)
+
+ Return: ~
+ List-like table of the split components.
+
+ See also: ~
+ |vim.gsplit()|
+
+tbl_keys({t}) *vim.tbl_keys()*
+ Return a list of all keys used in a table. However, the order
+ of the return table of keys is not guaranteed.
+
+ Parameters: ~
+ {t} Table
+
+ Return: ~
+ list of keys
+
+ See also: ~
+ Fromhttps://github.com/premake/premake-core/blob/master/src/base/table.lua
+
+tbl_values({t}) *vim.tbl_values()*
+ Return a list of all values used in a table. However, the
+ order of the return table of values is not guaranteed.
+
+ Parameters: ~
+ {t} Table
+
+ Return: ~
+ list of values
+
+tbl_contains({t}, {value}) *vim.tbl_contains()*
+ Checks if a list-like (vector) table contains `value` .
+
+ Parameters: ~
+ {t} Table to check
+ {value} Value to compare
+
+ Return: ~
+ true if `t` contains `value`
+
+tbl_isempty({t}) *vim.tbl_isempty()*
+ See also: ~
+ Fromhttps://github.com/premake/premake-core/blob/master/src/base/table.lua@paramt Table to check
+
+tbl_extend({behavior}, {...}) *vim.tbl_extend()*
+ Merges two or more map-like tables.
+
+ Parameters: ~
+ {behavior} Decides what to do if a key is found in more
+ than one map:
+ • "error": raise an error
+ • "keep": use value from the leftmost map
+ • "force": use value from the rightmost map
+ {...} Two or more map-like tables.
+
+ See also: ~
+ |extend()|
+
+deep_equal({a}, {b}) *vim.deep_equal()*
+ TODO: Documentation
+
+tbl_add_reverse_lookup({o}) *vim.tbl_add_reverse_lookup()*
+ Add the reverse lookup values to an existing table. For
+ example: `tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A =
+ 1 }`
+
+ Parameters: ~
+ {o} table The table to add the reverse to.
+
+list_extend({dst}, {src}) *vim.list_extend()*
+ Extends a list-like table with the values of another list-like
+ table.
+
+ Parameters: ~
+ {dst} The list which will be modified and appended to.
+ {src} The list from which values will be inserted.
+
+ See also: ~
+ |extend()|
+
+tbl_flatten({t}) *vim.tbl_flatten()*
+ Creates a copy of a list-like table such that any nested
+ tables are "unrolled" and appended to the result.
+
+ Parameters: ~
+ {t} List-like table
+
+ Return: ~
+ Flattened copy of the given list-like table.
+
+ See also: ~
+ Fromhttps://github.com/premake/premake-core/blob/master/src/base/table.lua
+
+tbl_islist({t}) *vim.tbl_islist()*
+ Table
+
+ Return: ~
+ true: A non-empty array, false: A non-empty table, nil: An
+ empty table
+
+trim({s}) *vim.trim()*
+ Trim whitespace (Lua pattern "%s") from both sides of a
+ string.
+
+ Parameters: ~
+ {s} String to trim
+
+ Return: ~
+ String with whitespace removed from its beginning and end
+
+ See also: ~
+ https://www.lua.org/pil/20.2.html
+
+pesc({s}) *vim.pesc()*
+ Escapes magic chars in a Lua pattern string.
+
+ Parameters: ~
+ {s} String to escape
+
+ Return: ~
+ %-escaped pattern string
+
+ See also: ~
+ https://github.com/rxi/lume
+
+validate({opt}) *vim.validate()*
+ Validates a parameter specification (types and values).
+
+ Usage example: >
+
+ function user.new(name, age, hobbies)
+ vim.validate{
+ name={name, 'string'},
+ age={age, 'number'},
+ hobbies={hobbies, 'table'},
+ }
+ ...
+ end
+<
+
+ Examples with explicit argument values (can be run directly): >
+
+ vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
+ => NOP (success)
+<
+>
+ vim.validate{arg1={1, 'table'}}
+ => error('arg1: expected table, got number')
+<
+>
+ vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
+ => error('arg1: expected even number, got 3')
+<
+
+ Parameters: ~
+ {opt} Map of parameter names to validations. Each key is
+ a parameter name; each value is a tuple in one of
+ these forms:
+ 1. (arg_value, type_name, optional)
+ • arg_value: argument value
+ • type_name: string type name, one of: ("table",
+ "t", "string", "s", "number", "n", "boolean",
+ "b", "function", "f", "nil", "thread",
+ "userdata")
+ • optional: (optional) boolean, if true, `nil`
+ is valid
+
+ 2. (arg_value, fn, msg)
+ • arg_value: argument value
+ • fn: any function accepting one argument,
+ returns true if and only if the argument is
+ valid
+ • msg: (optional) error string if validation
+ fails
+
+is_callable({f}) *vim.is_callable()*
+ Returns true if object `f` can be called as a function.
+
+ Parameters: ~
+ {f} Any object
+
+ Return: ~
+ true if `f` is callable, else false
+
+ vim:tw=78:ts=8:ft=help:norl:
diff --git a/runtime/doc/motion.txt b/runtime/doc/motion.txt
index 97c7d1cc43..e93c833c76 100644
--- a/runtime/doc/motion.txt
+++ b/runtime/doc/motion.txt
@@ -219,6 +219,12 @@ g^ When lines wrap ('wrap' on): To the first non-blank
gm Like "g0", but half a screenwidth to the right (or as
much as possible).
+ *gM*
+gM Like "g0", but to halfway the text of the line.
+ With a count: to this percentage of text in the line.
+ Thus "10gM" is near the start of the text and "90gM"
+ is near the end of the text.
+
*g$* *g<End>*
g$ or g<End> When lines wrap ('wrap' on): To the last character of
the screen line and [count - 1] screen lines downward
@@ -412,35 +418,35 @@ between Vi and Vim.
5. Text object motions *object-motions*
*(*
-( [count] sentences backward. |exclusive| motion.
+( [count] |sentence|s backward. |exclusive| motion.
*)*
-) [count] sentences forward. |exclusive| motion.
+) [count] |sentence|s forward. |exclusive| motion.
*{*
-{ [count] paragraphs backward. |exclusive| motion.
+{ [count] |paragraph|s backward. |exclusive| motion.
*}*
-} [count] paragraphs forward. |exclusive| motion.
+} [count] |paragraph|s forward. |exclusive| motion.
*]]*
-]] [count] sections forward or to the next '{' in the
+]] [count] |section|s forward or to the next '{' in the
first column. When used after an operator, then also
stops below a '}' in the first column. |exclusive|
Note that |exclusive-linewise| often applies.
*][*
-][ [count] sections forward or to the next '}' in the
+][ [count] |section|s forward or to the next '}' in the
first column. |exclusive|
Note that |exclusive-linewise| often applies.
*[[*
-[[ [count] sections backward or to the previous '{' in
+[[ [count] |section|s backward or to the previous '{' in
the first column. |exclusive|
Note that |exclusive-linewise| often applies.
*[]*
-[] [count] sections backward or to the previous '}' in
+[] [count] |section|s backward or to the previous '}' in
the first column. |exclusive|
Note that |exclusive-linewise| often applies.
diff --git a/runtime/doc/msgpack_rpc.txt b/runtime/doc/msgpack_rpc.txt
index f5d42dfeb2..5368cf0f4f 100644
--- a/runtime/doc/msgpack_rpc.txt
+++ b/runtime/doc/msgpack_rpc.txt
@@ -1,7 +1,8 @@
- NVIM REFERENCE MANUAL by Thiago de Arruda
-
-
+ NVIM REFERENCE MANUAL
This document was merged into |api.txt| and |develop.txt|.
+
+==============================================================================
+ vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index e12a7d4986..386fcdf8c0 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -843,6 +843,14 @@ A jump table for the options with a short description can be found at |Q_op|.
name, precede it with a backslash.
- To include a comma in a directory name precede it with a backslash.
- A directory name may end in an '/'.
+ - For Unix and Win32, if a directory ends in two path separators "//",
+ the swap file name will be built from the complete path to the file
+ with all path separators changed to percent '%' signs. This will
+ ensure file name uniqueness in the backup directory.
+ On Win32, it is also possible to end with "\\". However, When a
+ separating comma is following, you must use "//", since "\\" will
+ include the comma in the file name. Therefore it is recommended to
+ use '//', instead of '\\'.
- Environment variables are expanded |:set_env|.
- Careful with '\' characters, type one before a space, type two to
get one in the option (see |option-backslash|), for example: >
@@ -1875,7 +1883,7 @@ A jump table for the options with a short description can be found at |Q_op|.
security reasons.
*'dip'* *'diffopt'*
-'diffopt' 'dip' string (default "internal,filler")
+'diffopt' 'dip' string (default "internal,filler,closeoff")
global
Option settings for diff mode. It can consist of the following items.
All are optional. Items must be separated by a comma.
@@ -1932,6 +1940,12 @@ A jump table for the options with a short description can be found at |Q_op|.
vertical Start diff mode with vertical splits (unless
explicitly specified otherwise).
+ closeoff When a window is closed where 'diff' is set
+ and there is only one window remaining in the
+ same tab page with 'diff' set, execute
+ `:diffoff` in that window. This undoes a
+ `:diffsplit` command.
+
hiddenoff Do not use diff mode for a buffer when it
becomes hidden.
@@ -1986,12 +2000,14 @@ A jump table for the options with a short description can be found at |Q_op|.
- A directory starting with "./" (or ".\" for Windows) means to
put the swap file relative to where the edited file is. The leading
"." is replaced with the path name of the edited file.
- - For Unix and Win32, if a directory ends in two path separators "//"
- or "\\", the swap file name will be built from the complete path to
- the file with all path separators substituted to percent '%' signs.
- This will ensure file name uniqueness in the preserve directory.
- On Win32, when a separating comma is following, you must use "//",
- since "\\" will include the comma in the file name.
+ - For Unix and Win32, if a directory ends in two path separators "//",
+ the swap file name will be built from the complete path to the file
+ with all path separators substituted to percent '%' signs. This will
+ ensure file name uniqueness in the preserve directory.
+ On Win32, it is also possible to end with "\\". However, When a
+ separating comma is following, you must use "//", since "\\" will
+ include the comma in the file name. Therefore it is recommended to
+ use '//', instead of '\\'.
- Spaces after the comma are ignored, other spaces are considered part
of the directory name. To have a space at the start of a directory
name, precede it with a backslash.
@@ -2242,8 +2258,7 @@ A jump table for the options with a short description can be found at |Q_op|.
*'fileformat'* *'ff'*
'fileformat' 'ff' string (Windows default: "dos",
- Unix default: "unix",
- Macintosh default: "mac")
+ Unix default: "unix")
local to buffer
This gives the <EOL> of the current buffer, which is used for
reading/writing the buffer from/to a file:
@@ -2265,7 +2280,6 @@ A jump table for the options with a short description can be found at |Q_op|.
'fileformats' 'ffs' string (default:
Vim+Vi Win32: "dos,unix",
Vim Unix: "unix,dos",
- Vim Mac: "mac,unix,dos",
Vi others: "")
global
This gives the end-of-line (<EOL>) formats that will be tried when
@@ -6159,14 +6173,14 @@ A jump table for the options with a short description can be found at |Q_op|.
match Match case
smart Ignore case unless an upper case letter is used
- *'tagfunc'* *'tfu'*
- 'tagfunc' 'tfu' string (default: empty)
- local to buffer
- This option specifies a function to be used to perform tag searches.
- The function gets the tag pattern and should return a List of matching
- tags. See |tag-function| for an explanation of how to write the
- function and an example.
-
+ *'tagfunc'* *'tfu'*
+'tagfunc' 'tfu' string (default: empty)
+ local to buffer
+ This option specifies a function to be used to perform tag searches.
+ The function gets the tag pattern and should return a List of matching
+ tags. See |tag-function| for an explanation of how to write the
+ function and an example.
+
*'taglength'* *'tl'*
'taglength' 'tl' number (default 0)
global
@@ -6638,22 +6652,18 @@ A jump table for the options with a short description can be found at |Q_op|.
*'wildmenu'* *'wmnu'* *'nowildmenu'* *'nowmnu'*
'wildmenu' 'wmnu' boolean (default on)
global
- When 'wildmenu' is on, command-line completion operates in an enhanced
- mode. On pressing 'wildchar' (usually <Tab>) to invoke completion,
- the possible matches are shown just above the command line, with the
- first match highlighted (overwriting the status line, if there is
- one). Keys that show the previous/next match, such as <Tab> or
- CTRL-P/CTRL-N, cause the highlight to move to the appropriate match.
- When 'wildmode' is used, "wildmenu" mode is used where "full" is
- specified. "longest" and "list" do not start "wildmenu" mode.
- You can check the current mode with |wildmenumode()|.
- If there are more matches than can fit in the line, a ">" is shown on
- the right and/or a "<" is shown on the left. The status line scrolls
- as needed.
- The "wildmenu" mode is abandoned when a key is hit that is not used
- for selecting a completion.
- While the "wildmenu" is active the following keys have special
- meanings:
+ Enables "enhanced mode" of command-line completion. When user hits
+ <Tab> (or 'wildchar') to invoke completion, the possible matches are
+ shown in a menu just above the command-line (see 'wildoptions'), with
+ the first match highlighted (overwriting the statusline). Keys that
+ show the previous/next match (<Tab>/CTRL-P/CTRL-N) highlight the
+ match.
+ 'wildmode' must specify "full": "longest" and "list" do not start
+ 'wildmenu' mode. You can check the current mode with |wildmenumode()|.
+ The menu is canceled when a key is hit that is not used for selecting
+ a completion.
+
+ While the menu is active these keys have special meanings:
<Left> <Right> - select previous/next match (like CTRL-P/CTRL-N)
<Down> - in filename/menu name completion: move into a
@@ -6663,15 +6673,12 @@ A jump table for the options with a short description can be found at |Q_op|.
<Up> - in filename/menu name completion: move up into
parent directory or parent menu.
- This makes the menus accessible from the console |console-menus|.
-
- If you prefer the <Left> and <Right> keys to move the cursor instead
- of selecting a different match, use this: >
+ If you want <Left> and <Right> to move the cursor instead of selecting
+ a different match, use this: >
:cnoremap <Left> <Space><BS><Left>
:cnoremap <Right> <Space><BS><Right>
<
- The "WildMenu" highlighting is used for displaying the current match
- |hl-WildMenu|.
+ |hl-WildMenu| highlights the current match.
*'wildmode'* *'wim'*
'wildmode' 'wim' string (default: "full")
diff --git a/runtime/doc/quickfix.txt b/runtime/doc/quickfix.txt
index 3ae6d9461f..61e090cc78 100644
--- a/runtime/doc/quickfix.txt
+++ b/runtime/doc/quickfix.txt
@@ -109,6 +109,36 @@ processing a quickfix or location list command, it will be aborted.
list for the current window is used instead of the
quickfix list.
+ *:cabo* *:cabove*
+:[count]cabo[ve] Go to the [count] error above the current line in the
+ current buffer. If [count] is omitted, then 1 is
+ used. If there are no errors, then an error message
+ is displayed. Assumes that the entries in a quickfix
+ list are sorted by their buffer number and line
+ number. If there are multiple errors on the same line,
+ then only the first entry is used. If [count] exceeds
+ the number of entries above the current line, then the
+ first error in the file is selected.
+
+ *:lab* *:labove*
+:[count]lab[ove] Same as ":cabove", except the location list for the
+ current window is used instead of the quickfix list.
+
+ *:cbe* *:cbelow*
+:[count]cbe[low] Go to the [count] error below the current line in the
+ current buffer. If [count] is omitted, then 1 is
+ used. If there are no errors, then an error message
+ is displayed. Assumes that the entries in a quickfix
+ list are sorted by their buffer number and line
+ number. If there are multiple errors on the same
+ line, then only the first entry is used. If [count]
+ exceeds the number of entries below the current line,
+ then the last error in the file is selected.
+
+ *:lbe* *:lbelow*
+:[count]lbe[low] Same as ":cbelow", except the location list for the
+ current window is used instead of the quickfix list.
+
*:cnf* *:cnfile*
:[count]cnf[ile][!] Display the first error in the [count] next file in
the list that includes a file name. If there are no
diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt
index 87cb9b54f5..dfa7218bdf 100644
--- a/runtime/doc/quickref.txt
+++ b/runtime/doc/quickref.txt
@@ -47,6 +47,7 @@ N is used to indicate an optional count that can be given before the command.
|g$| N g$ to last character in screen line (differs from "$"
when lines wrap)
|gm| gm to middle of the screen line
+|gM| gM to middle of the line
|bar| N | to column N (default: 1)
|f| N f{char} to the Nth occurrence of {char} to the right
|F| N F{char} to the Nth occurrence of {char} to the left
diff --git a/runtime/doc/usr_25.txt b/runtime/doc/usr_25.txt
index 3a58af6412..2efb67e55f 100644
--- a/runtime/doc/usr_25.txt
+++ b/runtime/doc/usr_25.txt
@@ -346,12 +346,13 @@ scroll:
g0 to first visible character in this line
g^ to first non-blank visible character in this line
- gm to middle of this line
+ gm to middle of screen line
+ gM to middle of the text in this line
g$ to last visible character in this line
- |<-- window -->|
- some long text, part of which is visible ~
- g0 g^ gm g$
+ |<-- window -->|
+ some long text, part of which is visible in one line ~
+ g0 g^ gm gM g$
BREAKING AT WORDS *edit-no-break*
diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt
index 45a94bb961..4267aefbbf 100644
--- a/runtime/doc/vim_diff.txt
+++ b/runtime/doc/vim_diff.txt
@@ -296,7 +296,7 @@ coerced to strings. See |id()| for more details, currently it uses
|c_CTRL-R| pasting a non-special register into |cmdline| omits the last <CR>.
-Lua interface (|if_lua.txt|):
+Lua interface (|lua.txt|):
- `:lua print("a\0b")` will print `a^@b`, like with `:echomsg "a\nb"` . In Vim
that prints `a` and `b` on separate lines, exactly like
@@ -307,15 +307,15 @@ Lua interface (|if_lua.txt|):
- Lua package.path and package.cpath are automatically updated according to
'runtimepath': |lua-require|.
-|input()| and |inputdialog()| support for each other’s features (return on
-cancel and completion respectively) via dictionary argument (replaces all
-other arguments if used).
-
-|input()| and |inputdialog()| support user-defined cmdline highlighting.
-
Commands:
|:doautocmd| does not warn about "No matching autocommands".
+Functions:
+ |input()| and |inputdialog()| support for each other’s features (return on
+ cancel and completion respectively) via dictionary argument (replaces all
+ other arguments if used).
+ |input()| and |inputdialog()| support user-defined cmdline highlighting.
+
Highlight groups:
|hl-ColorColumn|, |hl-CursorColumn| are lower priority than most other
groups
@@ -399,10 +399,10 @@ VimL (Vim script) compatibility:
Some legacy Vim features are not implemented:
-- |if_py|: *python-bindeval* *python-Function* are not supported
-- |if_lua|: the `vim` object is missing some legacy methods
-- *if_perl*
+- |if_lua|: Nvim Lua API is not compatible with Vim's "if_lua"
- *if_mzscheme*
+- *if_perl*
+- |if_py|: *python-bindeval* *python-Function* are not supported
- *if_tcl*
==============================================================================
@@ -524,4 +524,4 @@ TUI:
always uses 7-bit control sequences.
==============================================================================
- vim:tw=78:ts=8:sw=2:noet:ft=help:norl:
+ vim:tw=78:ts=8:sw=2:et:ft=help:norl:
diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua
index 7cb40ca64d..0f3b908dc1 100644
--- a/runtime/lua/vim/inspect.lua
+++ b/runtime/lua/vim/inspect.lua
@@ -289,7 +289,7 @@ function Inspector:putValue(v)
if tv == 'string' then
self:puts(smartQuote(escape(v)))
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
- tv == 'cdata' or tv == 'ctype' then
+ tv == 'cdata' or tv == 'ctype' or (vim and v == vim.NIL) then
self:puts(tostring(v))
elseif tv == 'table' then
self:putTable(v)
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
new file mode 100644
index 0000000000..9dbe03dace
--- /dev/null
+++ b/runtime/lua/vim/lsp.lua
@@ -0,0 +1,1055 @@
+local builtin_callbacks = require 'vim.lsp.builtin_callbacks'
+local log = require 'vim.lsp.log'
+local lsp_rpc = require 'vim.lsp.rpc'
+local protocol = require 'vim.lsp.protocol'
+local util = require 'vim.lsp.util'
+
+local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option
+ = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option
+local uv = vim.loop
+local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend
+local validate = vim.validate
+
+local lsp = {
+ protocol = protocol;
+ builtin_callbacks = builtin_callbacks;
+ util = util;
+ -- Allow raw RPC access.
+ rpc = lsp_rpc;
+ -- Export these directly from rpc.
+ rpc_response_error = lsp_rpc.rpc_response_error;
+ -- You probably won't need this directly, since __tostring is set for errors
+ -- by the RPC.
+ -- format_rpc_error = lsp_rpc.format_rpc_error;
+}
+
+-- TODO consider whether 'eol' or 'fixeol' should change the nvim_buf_get_lines that send.
+-- TODO improve handling of scratch buffers with LSP attached.
+
+local function resolve_bufnr(bufnr)
+ validate { bufnr = { bufnr, 'n', true } }
+ if bufnr == nil or bufnr == 0 then
+ return vim.api.nvim_get_current_buf()
+ end
+ return bufnr
+end
+
+local function is_dir(filename)
+ validate{filename={filename,'s'}}
+ local stat = uv.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+end
+
+-- TODO Use vim.wait when that is available, but provide an alternative for now.
+local wait = vim.wait or function(timeout_ms, condition, interval)
+ validate {
+ timeout_ms = { timeout_ms, 'n' };
+ condition = { condition, 'f' };
+ interval = { interval, 'n', true };
+ }
+ assert(timeout_ms > 0, "timeout_ms must be > 0")
+ local _ = log.debug() and log.debug("wait.fallback", timeout_ms)
+ interval = interval or 200
+ local interval_cmd = "sleep "..interval.."m"
+ local timeout = timeout_ms + uv.now()
+ -- TODO is there a better way to sync this?
+ while true do
+ uv.update_time()
+ if condition() then
+ return 0
+ end
+ if uv.now() >= timeout then
+ return -1
+ end
+ nvim_command(interval_cmd)
+ -- vim.loop.sleep(10)
+ end
+end
+local wait_result_reason = { [-1] = "timeout"; [-2] = "interrupted"; [-3] = "error" }
+
+local valid_encodings = {
+ ["utf-8"] = 'utf-8'; ["utf-16"] = 'utf-16'; ["utf-32"] = 'utf-32';
+ ["utf8"] = 'utf-8'; ["utf16"] = 'utf-16'; ["utf32"] = 'utf-32';
+ UTF8 = 'utf-8'; UTF16 = 'utf-16'; UTF32 = 'utf-32';
+}
+
+local client_index = 0
+local function next_client_id()
+ client_index = client_index + 1
+ return client_index
+end
+-- Tracks all clients created via lsp.start_client
+local active_clients = {}
+local all_buffer_active_clients = {}
+local uninitialized_clients = {}
+
+local function for_each_buffer_client(bufnr, callback)
+ validate {
+ callback = { callback, 'f' };
+ }
+ bufnr = resolve_bufnr(bufnr)
+ local client_ids = all_buffer_active_clients[bufnr]
+ if not client_ids or tbl_isempty(client_ids) then
+ return
+ end
+ for client_id in pairs(client_ids) do
+ -- This is unlikely to happen. Could only potentially happen in a race
+ -- condition between literally a single statement.
+ -- We could skip this error, but let's error for now.
+ local client = active_clients[client_id]
+ -- or error(string.format("Client %d has already shut down.", client_id))
+ if client then
+ callback(client, client_id)
+ end
+ end
+end
+
+-- Error codes to be used with `on_error` from |vim.lsp.start_client|.
+-- Can be used to look up the string from a the number or the number
+-- from the string.
+lsp.client_errors = tbl_extend("error", lsp_rpc.client_errors, vim.tbl_add_reverse_lookup {
+ ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1;
+})
+
+local function validate_encoding(encoding)
+ validate {
+ encoding = { encoding, 's' };
+ }
+ return valid_encodings[encoding:lower()]
+ or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding))
+end
+
+local function validate_command(input)
+ local cmd, cmd_args
+ if type(input) == 'string' then
+ -- Use a shell to execute the command if it is a string.
+ cmd = vim.api.nvim_get_option('shell')
+ cmd_args = {vim.api.nvim_get_option('shellcmdflag'), input}
+ elseif vim.tbl_islist(input) then
+ cmd = input[1]
+ cmd_args = {}
+ -- Don't mutate our input.
+ for i, v in ipairs(input) do
+ assert(type(v) == 'string', "input arguments must be strings")
+ if i > 1 then
+ table.insert(cmd_args, v)
+ end
+ end
+ else
+ error("cmd type must be string or list.")
+ end
+ return cmd, cmd_args
+end
+
+local function optional_validator(fn)
+ return function(v)
+ return v == nil or fn(v)
+ end
+end
+
+local function validate_client_config(config)
+ validate {
+ config = { config, 't' };
+ }
+ validate {
+ root_dir = { config.root_dir, is_dir, "directory" };
+ callbacks = { config.callbacks, "t", true };
+ capabilities = { config.capabilities, "t", true };
+ -- cmd = { config.cmd, "s", false };
+ cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" };
+ cmd_env = { config.cmd_env, "f", true };
+ name = { config.name, 's', true };
+ on_error = { config.on_error, "f", true };
+ on_exit = { config.on_exit, "f", true };
+ on_init = { config.on_init, "f", true };
+ offset_encoding = { config.offset_encoding, "s", true };
+ }
+ local cmd, cmd_args = validate_command(config.cmd)
+ local offset_encoding = valid_encodings.UTF16
+ if config.offset_encoding then
+ offset_encoding = validate_encoding(config.offset_encoding)
+ end
+ return {
+ cmd = cmd; cmd_args = cmd_args;
+ offset_encoding = offset_encoding;
+ }
+end
+
+local function text_document_did_open_handler(bufnr, client)
+ if not client.resolved_capabilities.text_document_open_close then
+ return
+ end
+ if not vim.api.nvim_buf_is_loaded(bufnr) then
+ return
+ end
+ local params = {
+ textDocument = {
+ version = 0;
+ uri = vim.uri_from_bufnr(bufnr);
+ -- TODO make sure our filetypes are compatible with languageId names.
+ languageId = nvim_buf_get_option(bufnr, 'filetype');
+ text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n');
+ }
+ }
+ client.notify('textDocument/didOpen', params)
+end
+
+
+--- Start a client and initialize it.
+-- Its arguments are passed via a configuration object.
+--
+-- Mandatory parameters:
+--
+-- root_dir: {string} specifying the directory where the LSP server will base
+-- as its rootUri on initialization.
+--
+-- cmd: {string} or {list} which is the base command to execute for the LSP. A
+-- string will be run using |'shell'| and a list will be interpreted as a bare
+-- command with arguments passed. This is the same as |jobstart()|.
+--
+-- Optional parameters:
+
+-- cmd_cwd: {string} specifying the directory to launch the `cmd` process. This
+-- is not related to `root_dir`. By default, |getcwd()| is used.
+--
+-- cmd_env: {table} specifying the environment flags to pass to the LSP on
+-- spawn. This can be specified using keys like a map or as a list with `k=v`
+-- pairs or both. Non-string values are coerced to a string.
+-- For example: `{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`.
+--
+-- capabilities: A {table} which will be used instead of
+-- `vim.lsp.protocol.make_client_capabilities()` which contains neovim's
+-- default capabilities and passed to the language server on initialization.
+-- You'll probably want to use make_client_capabilities() and modify the
+-- result.
+-- NOTE:
+-- To send an empty dictionary, you should use
+-- `{[vim.type_idx]=vim.types.dictionary}` Otherwise, it will be encoded as
+-- an array.
+--
+-- callbacks: A {table} of whose keys are language server method names and the
+-- values are `function(err, method, params, client_id)`.
+-- This will be called for:
+-- - notifications from the server, where `err` will always be `nil`
+-- - requests initiated by the server. For these, you can respond by returning
+-- two values: `result, err`. The err must be in the format of an RPC error,
+-- which is `{ code, message, data? }`. You can use |vim.lsp.rpc_response_error()|
+-- to help with this.
+-- - as a callback for requests initiated by the client if the request doesn't
+-- explicitly specify a callback.
+--
+-- init_options: A {table} of values to pass in the initialization request
+-- as `initializationOptions`. See the `initialize` in the LSP spec.
+--
+-- name: A {string} used in log messages. Defaults to {client_id}
+--
+-- offset_encoding: One of 'utf-8', 'utf-16', or 'utf-32' which is the
+-- encoding that the LSP server expects. By default, it is 'utf-16' as
+-- specified in the LSP specification. The client does not verify this
+-- is correct.
+--
+-- on_error(code, ...): A function for handling errors thrown by client
+-- operation. {code} is a number describing the error. Other arguments may be
+-- passed depending on the error kind. @see |vim.lsp.client_errors| for
+-- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a
+-- human understandable string.
+--
+-- on_init(client, initialize_result): A function which is called after the
+-- request `initialize` is completed. `initialize_result` contains
+-- `capabilities` and anything else the server may send. For example, `clangd`
+-- sends `result.offsetEncoding` if `capabilities.offsetEncoding` was sent to
+-- it.
+--
+-- on_exit(code, signal, client_id): A function which is called after the
+-- client has exited. code is the exit code of the process, and signal is a
+-- number describing the signal used to terminate (if any).
+--
+-- on_attach(client, bufnr): A function which is called after the client is
+-- attached to a buffer.
+--
+-- trace: 'off' | 'messages' | 'verbose' | nil passed directly to the language
+-- server in the initialize request. Invalid/empty values will default to 'off'
+--
+-- @returns client_id You can use |vim.lsp.get_client_by_id()| to get the
+-- actual client.
+--
+-- NOTE: The client is only available *after* it has been initialized, which
+-- may happen after a small delay (or never if there is an error).
+-- For this reason, you may want to use `on_init` to do any actions once the
+-- client has been initialized.
+function lsp.start_client(config)
+ local cleaned_config = validate_client_config(config)
+ local cmd, cmd_args, offset_encoding = cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding
+
+ local client_id = next_client_id()
+
+ local callbacks = tbl_extend("keep", config.callbacks or {}, builtin_callbacks)
+ -- Copy metatable if it has one.
+ if config.callbacks and config.callbacks.__metatable then
+ setmetatable(callbacks, getmetatable(config.callbacks))
+ end
+ local name = config.name or tostring(client_id)
+ local log_prefix = string.format("LSP[%s]", name)
+
+ local handlers = {}
+
+ function handlers.notification(method, params)
+ local _ = log.debug() and log.debug('notification', method, params)
+ local callback = callbacks[method]
+ if callback then
+ -- Method name is provided here for convenience.
+ callback(nil, method, params, client_id)
+ end
+ end
+
+ function handlers.server_request(method, params)
+ local _ = log.debug() and log.debug('server_request', method, params)
+ local callback = callbacks[method]
+ if callback then
+ local _ = log.debug() and log.debug("server_request: found callback for", method)
+ return callback(nil, method, params, client_id)
+ end
+ local _ = log.debug() and log.debug("server_request: no callback found for", method)
+ return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
+ end
+
+ function handlers.on_error(code, err)
+ local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err })
+ nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err)))
+ if config.on_error then
+ local status, usererr = pcall(config.on_error, code, err)
+ if not status then
+ local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr })
+ nvim_err_writeln(log_prefix.." user on_error failed: "..tostring(usererr))
+ end
+ end
+ end
+
+ function handlers.on_exit(code, signal)
+ active_clients[client_id] = nil
+ uninitialized_clients[client_id] = nil
+ for _, client_ids in pairs(all_buffer_active_clients) do
+ client_ids[client_id] = nil
+ end
+ if config.on_exit then
+ pcall(config.on_exit, code, signal, client_id)
+ end
+ end
+
+ -- Start the RPC client.
+ local rpc = lsp_rpc.start(cmd, cmd_args, handlers, {
+ cwd = config.cmd_cwd;
+ env = config.cmd_env;
+ })
+
+ local client = {
+ id = client_id;
+ name = name;
+ rpc = rpc;
+ offset_encoding = offset_encoding;
+ callbacks = callbacks;
+ config = config;
+ }
+
+ -- Store the uninitialized_clients for cleanup in case we exit before
+ -- initialize finishes.
+ uninitialized_clients[client_id] = client;
+
+ local function initialize()
+ local valid_traces = {
+ off = 'off'; messages = 'messages'; verbose = 'verbose';
+ }
+ local initialize_params = {
+ -- The process Id of the parent process that started the server. Is null if
+ -- the process has not been started by another process. If the parent
+ -- process is not alive then the server should exit (see exit notification)
+ -- its process.
+ processId = uv.getpid();
+ -- The rootPath of the workspace. Is null if no folder is open.
+ --
+ -- @deprecated in favour of rootUri.
+ rootPath = nil;
+ -- The rootUri of the workspace. Is null if no folder is open. If both
+ -- `rootPath` and `rootUri` are set `rootUri` wins.
+ rootUri = vim.uri_from_fname(config.root_dir);
+-- rootUri = vim.uri_from_fname(vim.fn.expand("%:p:h"));
+ -- User provided initialization options.
+ initializationOptions = config.init_options;
+ -- The capabilities provided by the client (editor or tool)
+ capabilities = config.capabilities or protocol.make_client_capabilities();
+ -- The initial trace setting. If omitted trace is disabled ('off').
+ -- trace = 'off' | 'messages' | 'verbose';
+ trace = valid_traces[config.trace] or 'off';
+ -- The workspace folders configured in the client when the server starts.
+ -- This property is only available if the client supports workspace folders.
+ -- It can be `null` if the client supports workspace folders but none are
+ -- configured.
+ --
+ -- Since 3.6.0
+ -- workspaceFolders?: WorkspaceFolder[] | null;
+ -- export interface WorkspaceFolder {
+ -- -- The associated URI for this workspace folder.
+ -- uri
+ -- -- The name of the workspace folder. Used to refer to this
+ -- -- workspace folder in the user interface.
+ -- name
+ -- }
+ workspaceFolders = nil;
+ }
+ local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params)
+ rpc.request('initialize', initialize_params, function(init_err, result)
+ assert(not init_err, tostring(init_err))
+ assert(result, "server sent empty result")
+ rpc.notify('initialized', {})
+ client.initialized = true
+ uninitialized_clients[client_id] = nil
+ client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities")
+ -- These are the cleaned up capabilities we use for dynamically deciding
+ -- when to send certain events to clients.
+ client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities)
+ if config.on_init then
+ local status, err = pcall(config.on_init, client, result)
+ if not status then
+ pcall(handlers.on_error, lsp.client_errors.ON_INIT_CALLBACK_ERROR, err)
+ end
+ end
+ local _ = log.debug() and log.debug(log_prefix, "server_capabilities", client.server_capabilities)
+ local _ = log.info() and log.info(log_prefix, "initialized", { resolved_capabilities = client.resolved_capabilities })
+
+ -- Only assign after initialized.
+ active_clients[client_id] = client
+ -- If we had been registered before we start, then send didOpen This can
+ -- happen if we attach to buffers before initialize finishes or if
+ -- someone restarts a client.
+ for bufnr, client_ids in pairs(all_buffer_active_clients) do
+ if client_ids[client_id] then
+ client._on_attach(bufnr)
+ end
+ end
+ end)
+ end
+
+ local function unsupported_method(method)
+ local msg = "server doesn't support "..method
+ local _ = log.warn() and log.warn(msg)
+ nvim_err_writeln(msg)
+ return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg)
+ end
+
+ --- Checks capabilities before rpc.request-ing.
+ function client.request(method, params, callback)
+ if not callback then
+ callback = client.callbacks[method]
+ or error(string.format("request callback is empty and no default was found for client %s", client.name))
+ end
+ local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback)
+ -- TODO keep these checks or just let it go anyway?
+ if (not client.resolved_capabilities.hover and method == 'textDocument/hover')
+ or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp')
+ or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition')
+ or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation')
+ then
+ callback(unsupported_method(method), method, nil, client_id)
+ return
+ end
+ return rpc.request(method, params, function(err, result)
+ callback(err, method, result, client_id)
+ end)
+ end
+
+ function client.notify(...)
+ return rpc.notify(...)
+ end
+
+ function client.cancel_request(id)
+ validate{id = {id, 'n'}}
+ return rpc.notify("$/cancelRequest", { id = id })
+ end
+
+ -- Track this so that we can escalate automatically if we've alredy tried a
+ -- graceful shutdown
+ local tried_graceful_shutdown = false
+ function client.stop(force)
+ local handle = rpc.handle
+ if handle:is_closing() then
+ return
+ end
+ if force or (not client.initialized) or tried_graceful_shutdown then
+ handle:kill(15)
+ return
+ end
+ tried_graceful_shutdown = true
+ -- Sending a signal after a process has exited is acceptable.
+ rpc.request('shutdown', nil, function(err, _)
+ if err == nil then
+ rpc.notify('exit')
+ else
+ -- If there was an error in the shutdown request, then term to be safe.
+ handle:kill(15)
+ end
+ end)
+ end
+
+ function client.is_stopped()
+ return rpc.handle:is_closing()
+ end
+
+ function client._on_attach(bufnr)
+ text_document_did_open_handler(bufnr, client)
+ if config.on_attach then
+ -- TODO(ashkan) handle errors.
+ pcall(config.on_attach, client, bufnr)
+ end
+ end
+
+ initialize()
+
+ return client_id
+end
+
+local function once(fn)
+ local value
+ return function(...)
+ if not value then value = fn(...) end
+ return value
+ end
+end
+
+local text_document_did_change_handler
+do
+ local encoding_index = { ["utf-8"] = 1; ["utf-16"] = 2; ["utf-32"] = 3; }
+ text_document_did_change_handler = function(_, bufnr, changedtick,
+ firstline, lastline, new_lastline, old_byte_size, old_utf32_size,
+ old_utf16_size)
+ local _ = log.debug() and log.debug("on_lines", bufnr, changedtick, firstline,
+ lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size, nvim_buf_get_lines(bufnr, firstline, new_lastline, true))
+ if old_byte_size == 0 then
+ return
+ end
+ -- Don't do anything if there are no clients attached.
+ if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then
+ return
+ end
+ -- Lazy initialize these because clients may not even need them.
+ local incremental_changes = once(function(client)
+ local size_index = encoding_index[client.offset_encoding]
+ local length = select(size_index, old_byte_size, old_utf16_size, old_utf32_size)
+ local lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
+ -- This is necessary because we are specifying the full line including the
+ -- newline in range. Therefore, we must replace the newline as well.
+ if #lines > 0 then
+ table.insert(lines, '')
+ end
+ return {
+ range = {
+ start = { line = firstline, character = 0 };
+ ["end"] = { line = lastline, character = 0 };
+ };
+ rangeLength = length;
+ text = table.concat(lines, '\n');
+ };
+ end)
+ local full_changes = once(function()
+ return {
+ text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), "\n");
+ };
+ end)
+ local uri = vim.uri_from_bufnr(bufnr)
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ local text_document_did_change = client.resolved_capabilities.text_document_did_change
+ local changes
+ if text_document_did_change == protocol.TextDocumentSyncKind.None then
+ return
+ --[=[ TODO(ashkan) there seem to be problems with the byte_sizes sent by
+ -- neovim right now so only send the full content for now. In general, we
+ -- can assume that servers *will* support both versions anyway, as there
+ -- is no way to specify the sync capability by the client.
+ -- See https://github.com/palantir/python-language-server/commit/cfd6675bc10d5e8dbc50fc50f90e4a37b7178821#diff-f68667852a14e9f761f6ebf07ba02fc8 for an example of pyls handling both.
+ --]=]
+ elseif true or text_document_did_change == protocol.TextDocumentSyncKind.Full then
+ changes = full_changes(client)
+ elseif text_document_did_change == protocol.TextDocumentSyncKind.Incremental then
+ changes = incremental_changes(client)
+ end
+ client.notify("textDocument/didChange", {
+ textDocument = {
+ uri = uri;
+ version = changedtick;
+ };
+ contentChanges = { changes; }
+ })
+ end)
+ end
+end
+
+-- Buffer lifecycle handler for textDocument/didSave
+function lsp._text_document_did_save_handler(bufnr)
+ bufnr = resolve_bufnr(bufnr)
+ local uri = vim.uri_from_bufnr(bufnr)
+ local text = once(function()
+ return table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n')
+ end)
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ if client.resolved_capabilities.text_document_save then
+ local included_text
+ if client.resolved_capabilities.text_document_save_include_text then
+ included_text = text()
+ end
+ client.notify('textDocument/didSave', {
+ textDocument = {
+ uri = uri;
+ text = included_text;
+ }
+ })
+ end
+ end)
+end
+
+-- Implements the textDocument/did* notifications required to track a buffer
+-- for any language server.
+-- @param bufnr [number] buffer handle or 0 for current
+-- @param client_id [number] the client id
+function lsp.buf_attach_client(bufnr, client_id)
+ validate {
+ bufnr = {bufnr, 'n', true};
+ client_id = {client_id, 'n'};
+ }
+ bufnr = resolve_bufnr(bufnr)
+ local buffer_client_ids = all_buffer_active_clients[bufnr]
+ -- This is our first time attaching to this buffer.
+ if not buffer_client_ids then
+ buffer_client_ids = {}
+ all_buffer_active_clients[bufnr] = buffer_client_ids
+
+ local uri = vim.uri_from_bufnr(bufnr)
+ nvim_command(string.format("autocmd BufWritePost <buffer=%d> lua vim.lsp._text_document_did_save_handler(0)", bufnr))
+ -- First time, so attach and set up stuff.
+ vim.api.nvim_buf_attach(bufnr, false, {
+ on_lines = text_document_did_change_handler;
+ on_detach = function()
+ local params = { textDocument = { uri = uri; } }
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ if client.resolved_capabilities.text_document_open_close then
+ client.notify('textDocument/didClose', params)
+ end
+ end)
+ all_buffer_active_clients[bufnr] = nil
+ end;
+ -- TODO if we know all of the potential clients ahead of time, then we
+ -- could conditionally set this.
+ -- utf_sizes = size_index > 1;
+ utf_sizes = true;
+ })
+ end
+ if buffer_client_ids[client_id] then return end
+ -- This is our first time attaching this client to this buffer.
+ buffer_client_ids[client_id] = true
+
+ local client = active_clients[client_id]
+ -- Send didOpen for the client if it is initialized. If it isn't initialized
+ -- then it will send didOpen on initialize.
+ if client then
+ client._on_attach(bufnr)
+ end
+ return true
+end
+
+-- Check if a buffer is attached for a particular client.
+-- @param bufnr [number] buffer handle or 0 for current
+-- @param client_id [number] the client id
+function lsp.buf_is_attached(bufnr, client_id)
+ return (all_buffer_active_clients[bufnr] or {})[client_id] == true
+end
+
+-- Look up an active client by its id, returns nil if it is not yet initialized
+-- or is not a valid id.
+-- @param client_id number the client id.
+function lsp.get_client_by_id(client_id)
+ return active_clients[client_id]
+end
+
+-- Stop a client by its id, optionally with force.
+-- You can also use the `stop()` function on a client if you already have
+-- access to it.
+-- By default, it will just ask the server to shutdown without force.
+-- If you request to stop a client which has previously been requested to shutdown,
+-- it will automatically force shutdown.
+-- @param client_id number the client id.
+-- @param force boolean (optional) whether to use force or request shutdown
+function lsp.stop_client(client_id, force)
+ local client
+ client = active_clients[client_id]
+ if client then
+ client.stop(force)
+ return
+ end
+ client = uninitialized_clients[client_id]
+ if client then
+ client.stop(true)
+ end
+end
+
+-- Returns a list of all the active clients.
+function lsp.get_active_clients()
+ return vim.tbl_values(active_clients)
+end
+
+-- Stop all the clients, optionally with force.
+-- You can also use the `stop()` function on a client if you already have
+-- access to it.
+-- By default, it will just ask the server to shutdown without force.
+-- If you request to stop a client which has previously been requested to shutdown,
+-- it will automatically force shutdown.
+-- @param force boolean (optional) whether to use force or request shutdown
+function lsp.stop_all_clients(force)
+ for _, client in pairs(uninitialized_clients) do
+ client.stop(true)
+ end
+ for _, client in pairs(active_clients) do
+ client.stop(force)
+ end
+end
+
+function lsp._vim_exit_handler()
+ log.info("exit_handler", active_clients)
+ for _, client in pairs(uninitialized_clients) do
+ client.stop(true)
+ end
+ -- TODO handle v:dying differently?
+ if tbl_isempty(active_clients) then
+ return
+ end
+ for _, client in pairs(active_clients) do
+ client.stop()
+ end
+ local wait_result = wait(500, function() return tbl_isempty(active_clients) end, 50)
+ if wait_result ~= 0 then
+ for _, client in pairs(active_clients) do
+ client.stop(true)
+ end
+ end
+end
+
+nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()")
+
+---
+--- Buffer level client functions.
+---
+
+--- Send a request to a server and return the response
+-- @param bufnr [number] Buffer handle or 0 for current.
+-- @param method [string] Request method name
+-- @param params [table|nil] Parameters to send to the server
+-- @param callback [function|nil] Request callback (or uses the client's callbacks)
+--
+-- @returns: client_request_ids, cancel_all_requests
+function lsp.buf_request(bufnr, method, params, callback)
+ validate {
+ bufnr = { bufnr, 'n', true };
+ method = { method, 's' };
+ callback = { callback, 'f', true };
+ }
+ local client_request_ids = {}
+ for_each_buffer_client(bufnr, function(client, client_id)
+ local request_success, request_id = client.request(method, params, callback)
+
+ -- This could only fail if the client shut down in the time since we looked
+ -- it up and we did the request, which should be rare.
+ if request_success then
+ client_request_ids[client_id] = request_id
+ end
+ end)
+
+ local function cancel_all_requests()
+ for client_id, request_id in pairs(client_request_ids) do
+ local client = active_clients[client_id]
+ client.cancel_request(request_id)
+ end
+ end
+
+ return client_request_ids, cancel_all_requests
+end
+
+--- Send a request to a server and wait for the response.
+-- @param bufnr [number] Buffer handle or 0 for current.
+-- @param method [string] Request method name
+-- @param params [string] Parameters to send to the server
+-- @param timeout_ms [number|100] Maximum ms to wait for a result
+--
+-- @returns: The table of {[client_id] = request_result}
+function lsp.buf_request_sync(bufnr, method, params, timeout_ms)
+ local request_results = {}
+ local result_count = 0
+ local function callback(err, _method, result, client_id)
+ request_results[client_id] = { error = err, result = result }
+ result_count = result_count + 1
+ end
+ local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, callback)
+ local expected_result_count = 0
+ for _ in pairs(client_request_ids) do
+ expected_result_count = expected_result_count + 1
+ end
+ local wait_result = wait(timeout_ms or 100, function()
+ return result_count >= expected_result_count
+ end, 10)
+ if wait_result ~= 0 then
+ cancel()
+ return nil, wait_result_reason[wait_result]
+ end
+ return request_results
+end
+
+--- Send a notification to a server
+-- @param bufnr [number] (optional): The number of the buffer
+-- @param method [string]: Name of the request method
+-- @param params [string]: Arguments to send to the server
+--
+-- @returns nil
+function lsp.buf_notify(bufnr, method, params)
+ validate {
+ bufnr = { bufnr, 'n', true };
+ method = { method, 's' };
+ }
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ client.rpc.notify(method, params)
+ end)
+end
+
+--- Function which can be called to generate omnifunc compatible completion.
+function lsp.omnifunc(findstart, base)
+ local _ = log.debug() and log.debug("omnifunc.findstart", { findstart = findstart, base = base })
+
+ local bufnr = resolve_bufnr()
+ local has_buffer_clients = not tbl_isempty(all_buffer_active_clients[bufnr] or {})
+ if not has_buffer_clients then
+ if findstart == 1 then
+ return -1
+ else
+ return {}
+ end
+ end
+
+ if findstart == 1 then
+ return vim.fn.col('.')
+ else
+ local pos = vim.api.nvim_win_get_cursor(0)
+ local line = assert(nvim_buf_get_lines(bufnr, pos[1]-1, pos[1], false)[1])
+ local _ = log.trace() and log.trace("omnifunc.line", pos, line)
+ local line_to_cursor = line:sub(1, pos[2]+1)
+ local _ = log.trace() and log.trace("omnifunc.line_to_cursor", line_to_cursor)
+ local params = {
+ textDocument = {
+ uri = vim.uri_from_bufnr(bufnr);
+ };
+ position = {
+ -- 0-indexed for both line and character
+ line = pos[1] - 1,
+ character = pos[2],
+ };
+ -- The completion context. This is only available if the client specifies
+ -- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true`
+ -- context = nil or {
+ -- triggerKind = protocol.CompletionTriggerKind.Invoked;
+ -- triggerCharacter = nil or "";
+ -- };
+ }
+ -- TODO handle timeout error differently? Like via an error?
+ local client_responses = lsp.buf_request_sync(bufnr, 'textDocument/completion', params) or {}
+ local matches = {}
+ for _, response in pairs(client_responses) do
+ -- TODO how to handle errors?
+ if not response.error then
+ local data = response.result
+ local completion_items = util.text_document_completion_list_to_complete_items(data or {}, line_to_cursor)
+ local _ = log.trace() and log.trace("omnifunc.completion_items", completion_items)
+ vim.list_extend(matches, completion_items)
+ end
+ end
+ return matches
+ end
+end
+
+---
+--- FileType based configuration utility
+---
+
+local all_filetype_configs = {}
+
+-- Lookup a filetype config client by its name.
+function lsp.get_filetype_client_by_name(name)
+ local config = all_filetype_configs[name]
+ if config.client_id then
+ return active_clients[config.client_id]
+ end
+end
+
+local function start_filetype_config(config)
+ config.client_id = lsp.start_client(config)
+ nvim_command(string.format(
+ "autocmd FileType %s silent lua vim.lsp.buf_attach_client(0, %d)",
+ table.concat(config.filetypes, ','),
+ config.client_id))
+ return config.client_id
+end
+
+-- Easy configuration option for common LSP use-cases.
+-- This will lazy initialize the client when the filetypes specified are
+-- encountered and attach to those buffers.
+--
+-- The configuration options are the same as |vim.lsp.start_client()|, but
+-- with a few additions and distinctions:
+--
+-- Additional parameters:
+-- - filetype: {string} or {list} of filetypes to attach to.
+-- - name: A unique string among all other servers configured with
+-- |vim.lsp.add_filetype_config|.
+--
+-- Differences:
+-- - root_dir: will default to |getcwd()|
+--
+function lsp.add_filetype_config(config)
+ -- Additional defaults.
+ -- Keep a copy of the user's input for debugging reasons.
+ local user_config = config
+ config = tbl_extend("force", {}, user_config)
+ config.root_dir = config.root_dir or uv.cwd()
+ -- Validate config.
+ validate_client_config(config)
+ validate {
+ name = { config.name, 's' };
+ }
+ assert(config.filetype, "config must have 'filetype' key")
+
+ local filetypes
+ if type(config.filetype) == 'string' then
+ filetypes = { config.filetype }
+ elseif type(config.filetype) == 'table' then
+ filetypes = config.filetype
+ assert(not tbl_isempty(filetypes), "config.filetype must not be an empty table")
+ else
+ error("config.filetype must be a string or a list of strings")
+ end
+
+ if all_filetype_configs[config.name] then
+ -- If the client exists, then it is likely that they are doing some kind of
+ -- reload flow, so let's not throw an error here.
+ if all_filetype_configs[config.name].client_id then
+ -- TODO log here? It might be unnecessarily annoying.
+ return
+ end
+ error(string.format('A configuration with the name %q already exists. They must be unique', config.name))
+ end
+
+ all_filetype_configs[config.name] = tbl_extend("keep", config, {
+ client_id = nil;
+ filetypes = filetypes;
+ user_config = user_config;
+ })
+
+ nvim_command(string.format(
+ "autocmd FileType %s ++once silent lua vim.lsp._start_filetype_config_client(%q)",
+ table.concat(filetypes, ','),
+ config.name))
+end
+
+-- Create a copy of an existing configuration, and override config with values
+-- from new_config.
+-- This is useful if you wish you create multiple LSPs with different root_dirs
+-- or other use cases.
+--
+-- You can specify a new unique name, but if you do not, a unique name will be
+-- created like `name-dup_count`.
+--
+-- existing_name: the name of the existing config to copy.
+-- new_config: the new configuration options. @see |vim.lsp.start_client()|.
+-- @returns string the new name.
+function lsp.copy_filetype_config(existing_name, new_config)
+ local config = all_filetype_configs[existing_name]
+ or error(string.format("Configuration with name %q doesn't exist", existing_name))
+ config = tbl_extend("force", config, new_config or {})
+ config.client_id = nil
+ config.original_config_name = existing_name
+
+ -- If the user didn't rename it, we will.
+ if config.name == existing_name then
+ -- Create a new, unique name.
+ local duplicate_count = 0
+ for _, conf in pairs(all_filetype_configs) do
+ if conf.original_config_name == existing_name then
+ duplicate_count = duplicate_count + 1
+ end
+ end
+ config.name = string.format("%s-%d", existing_name, duplicate_count + 1)
+ end
+ print("New config name:", config.name)
+ lsp.add_filetype_config(config)
+ return config.name
+end
+
+-- Autocmd handler to actually start the client when an applicable filetype is
+-- encountered.
+function lsp._start_filetype_config_client(name)
+ local config = all_filetype_configs[name]
+ -- If it exists and is running, don't make it again.
+ if config.client_id and active_clients[config.client_id] then
+ -- TODO log here?
+ return
+ end
+ lsp.buf_attach_client(0, start_filetype_config(config))
+ return config.client_id
+end
+
+---
+--- Miscellaneous utilities.
+---
+
+-- Retrieve a map from client_id to client of all active buffer clients.
+-- @param bufnr [number] (optional): buffer handle or 0 for current
+function lsp.buf_get_clients(bufnr)
+ bufnr = resolve_bufnr(bufnr)
+ local result = {}
+ for_each_buffer_client(bufnr, function(client, client_id)
+ result[client_id] = client
+ end)
+ return result
+end
+
+-- Print some debug information about the current buffer clients.
+-- The output of this function should not be relied upon and may change.
+function lsp.buf_print_debug_info(bufnr)
+ print(vim.inspect(lsp.buf_get_clients(bufnr)))
+end
+
+-- Print some debug information about all LSP related things.
+-- The output of this function should not be relied upon and may change.
+function lsp.print_debug_info()
+ print(vim.inspect({ clients = active_clients, filetype_configs = all_filetype_configs }))
+end
+
+-- Log level dictionary with reverse lookup as well.
+--
+-- Can be used to lookup the number from the name or the
+-- name from the number.
+-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
+-- Level numbers begin with 'trace' at 0
+lsp.log_levels = log.levels
+
+-- Set the log level for lsp logging.
+-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
+-- Level numbers begin with 'trace' at 0
+-- @param level [number|string] the case insensitive level name or number @see |vim.lsp.log_levels|
+function lsp.set_log_level(level)
+ if type(level) == 'string' or type(level) == 'number' then
+ log.set_level(level)
+ else
+ error(string.format("Invalid log level: %q", level))
+ end
+end
+
+-- Return the path of the logfile used by the LSP client.
+function lsp.get_log_path()
+ return log.get_filename()
+end
+
+return lsp
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua
new file mode 100644
index 0000000000..cc739ce3ad
--- /dev/null
+++ b/runtime/lua/vim/lsp/builtin_callbacks.lua
@@ -0,0 +1,296 @@
+--- Implements the following default callbacks:
+--
+-- vim.api.nvim_buf_set_lines(0, 0, 0, false, vim.tbl_keys(vim.lsp.builtin_callbacks))
+--
+
+-- textDocument/completion
+-- textDocument/declaration
+-- textDocument/definition
+-- textDocument/hover
+-- textDocument/implementation
+-- textDocument/publishDiagnostics
+-- textDocument/rename
+-- textDocument/signatureHelp
+-- textDocument/typeDefinition
+-- TODO codeLens/resolve
+-- TODO completionItem/resolve
+-- TODO documentLink/resolve
+-- TODO textDocument/codeAction
+-- TODO textDocument/codeLens
+-- TODO textDocument/documentHighlight
+-- TODO textDocument/documentLink
+-- TODO textDocument/documentSymbol
+-- TODO textDocument/formatting
+-- TODO textDocument/onTypeFormatting
+-- TODO textDocument/rangeFormatting
+-- TODO textDocument/references
+-- window/logMessage
+-- window/showMessage
+
+local log = require 'vim.lsp.log'
+local protocol = require 'vim.lsp.protocol'
+local util = require 'vim.lsp.util'
+local api = vim.api
+
+local function split_lines(value)
+ return vim.split(value, '\n', true)
+end
+
+local builtin_callbacks = {}
+
+-- textDocument/completion
+-- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
+builtin_callbacks['textDocument/completion'] = function(_, _, result)
+ if not result or vim.tbl_isempty(result) then
+ return
+ end
+ local pos = api.nvim_win_get_cursor(0)
+ local row, col = pos[1], pos[2]
+ local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1])
+ local line_to_cursor = line:sub(col+1)
+
+ local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor)
+ local match_result = vim.fn.matchstrpos(line_to_cursor, '\\k\\+$')
+ local match_start, match_finish = match_result[2], match_result[3]
+
+ vim.fn.complete(col + 1 - (match_finish - match_start), matches)
+end
+
+-- textDocument/rename
+builtin_callbacks['textDocument/rename'] = function(_, _, result)
+ if not result then return end
+ util.workspace_apply_workspace_edit(result)
+end
+
+local function uri_to_bufnr(uri)
+ return vim.fn.bufadd((vim.uri_to_fname(uri)))
+end
+
+builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result)
+ if not result then return end
+ local uri = result.uri
+ local bufnr = uri_to_bufnr(uri)
+ if not bufnr then
+ api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri))
+ return
+ end
+ util.buf_clear_diagnostics(bufnr)
+ util.buf_diagnostics_save_positions(bufnr, result.diagnostics)
+ util.buf_diagnostics_underline(bufnr, result.diagnostics)
+ util.buf_diagnostics_virtual_text(bufnr, result.diagnostics)
+ -- util.buf_loclist(bufnr, result.diagnostics)
+end
+
+-- textDocument/hover
+-- https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
+-- @params MarkedString | MarkedString[] | MarkupContent
+builtin_callbacks['textDocument/hover'] = function(_, _, result)
+ if result == nil or vim.tbl_isempty(result) then
+ return
+ end
+
+ if result.contents ~= nil then
+ local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
+ if vim.tbl_isempty(markdown_lines) then
+ markdown_lines = { 'No information available' }
+ end
+ util.open_floating_preview(markdown_lines, 'markdown')
+ end
+end
+
+builtin_callbacks['textDocument/peekDefinition'] = function(_, _, result)
+ if result == nil or vim.tbl_isempty(result) then return end
+ -- TODO(ashkan) what to do with multiple locations?
+ result = result[1]
+ local bufnr = uri_to_bufnr(result.uri)
+ assert(bufnr)
+ local start = result.range.start
+ local finish = result.range["end"]
+ util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 })
+ util.open_floating_preview({"*Peek:*", string.rep(" ", finish.character - start.character + 1) }, 'markdown', { offset_y = -(finish.line - start.line) })
+end
+
+--- Convert SignatureHelp response to preview contents.
+-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp
+local function signature_help_to_preview_contents(input)
+ if not input.signatures then
+ return
+ end
+ --The active signature. If omitted or the value lies outside the range of
+ --`signatures` the value defaults to zero or is ignored if `signatures.length
+ --=== 0`. Whenever possible implementors should make an active decision about
+ --the active signature and shouldn't rely on a default value.
+ local contents = {}
+ local active_signature = input.activeSignature or 0
+ -- If the activeSignature is not inside the valid range, then clip it.
+ if active_signature >= #input.signatures then
+ active_signature = 0
+ end
+ local signature = input.signatures[active_signature + 1]
+ if not signature then
+ return
+ end
+ vim.list_extend(contents, split_lines(signature.label))
+ if signature.documentation then
+ util.convert_input_to_markdown_lines(signature.documentation, contents)
+ end
+ if input.parameters then
+ local active_parameter = input.activeParameter or 0
+ -- If the activeParameter is not inside the valid range, then clip it.
+ if active_parameter >= #input.parameters then
+ active_parameter = 0
+ end
+ local parameter = signature.parameters and signature.parameters[active_parameter]
+ if parameter then
+ --[=[
+ --Represents a parameter of a callable-signature. A parameter can
+ --have a label and a doc-comment.
+ interface ParameterInformation {
+ --The label of this parameter information.
+ --
+ --Either a string or an inclusive start and exclusive end offsets within its containing
+ --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
+ --string representation as `Position` and `Range` does.
+ --
+ --*Note*: a label of type string should be a substring of its containing signature label.
+ --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
+ label: string | [number, number];
+ --The human-readable doc-comment of this parameter. Will be shown
+ --in the UI but can be omitted.
+ documentation?: string | MarkupContent;
+ }
+ --]=]
+ -- TODO highlight parameter
+ if parameter.documentation then
+ util.convert_input_to_markdown_lines(parameter.documentation, contents)
+ end
+ end
+ end
+ return contents
+end
+
+-- textDocument/signatureHelp
+-- https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp
+builtin_callbacks['textDocument/signatureHelp'] = function(_, _, result)
+ if result == nil or vim.tbl_isempty(result) then
+ return
+ end
+
+ -- TODO show empty popup when signatures is empty?
+ if #result.signatures > 0 then
+ local markdown_lines = signature_help_to_preview_contents(result)
+ if vim.tbl_isempty(markdown_lines) then
+ markdown_lines = { 'No signature available' }
+ end
+ util.open_floating_preview(markdown_lines, 'markdown')
+ end
+end
+
+local function update_tagstack()
+ local bufnr = api.nvim_get_current_buf()
+ local line = vim.fn.line('.')
+ local col = vim.fn.col('.')
+ local tagname = vim.fn.expand('<cWORD>')
+ local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname }
+ local winid = vim.fn.win_getid()
+ local tagstack = vim.fn.gettagstack(winid)
+
+ local action
+
+ if tagstack.length == tagstack.curidx then
+ action = 'r'
+ tagstack.items[tagstack.curidx] = item
+ elseif tagstack.length > tagstack.curidx then
+ action = 'r'
+ if tagstack.curidx > 1 then
+ tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item)
+ else
+ tagstack.items = { item }
+ end
+ else
+ action = 'a'
+ tagstack.items = { item }
+ end
+
+ tagstack.curidx = tagstack.curidx + 1
+ vim.fn.settagstack(winid, tagstack, action)
+end
+
+local function handle_location(result)
+ -- We can sometimes get a list of locations, so set the first value as the
+ -- only value we want to handle
+ -- TODO(ashkan) was this correct^? We could use location lists.
+ if result[1] ~= nil then
+ result = result[1]
+ end
+ if result.uri == nil then
+ api.nvim_err_writeln('[LSP] Could not find a valid location')
+ return
+ end
+ local result_file = vim.uri_to_fname(result.uri)
+ local bufnr = vim.fn.bufadd(result_file)
+ update_tagstack()
+ api.nvim_set_current_buf(bufnr)
+ local start = result.range.start
+ api.nvim_win_set_cursor(0, {start.line + 1, start.character})
+end
+
+local function location_callback(_, method, result)
+ if result == nil or vim.tbl_isempty(result) then
+ local _ = log.info() and log.info(method, 'No location found')
+ return nil
+ end
+ handle_location(result)
+ return true
+end
+
+local location_callbacks = {
+ -- https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration
+ 'textDocument/declaration';
+ -- https://microsoft.github.io/language-server-protocol/specification#textDocument_definition
+ 'textDocument/definition';
+ -- https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation
+ 'textDocument/implementation';
+ -- https://microsoft.github.io/language-server-protocol/specification#textDocument_typeDefinition
+ 'textDocument/typeDefinition';
+}
+
+for _, location_method in ipairs(location_callbacks) do
+ builtin_callbacks[location_method] = location_callback
+end
+
+local function log_message(_, _, result, client_id)
+ local message_type = result.type
+ local message = result.message
+ local client = vim.lsp.get_client_by_id(client_id)
+ local client_name = client and client.name or string.format("id=%d", client_id)
+ if not client then
+ api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name))
+ end
+ if message_type == protocol.MessageType.Error then
+ -- Might want to not use err_writeln,
+ -- but displaying a message with red highlights or something
+ api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message))
+ else
+ local message_type_name = protocol.MessageType[message_type]
+ api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message))
+ end
+ return result
+end
+
+builtin_callbacks['window/showMessage'] = log_message
+builtin_callbacks['window/logMessage'] = log_message
+
+-- Add boilerplate error validation and logging for all of these.
+for k, fn in pairs(builtin_callbacks) do
+ builtin_callbacks[k] = function(err, method, params, client_id)
+ local _ = log.debug() and log.debug('builtin_callback', method, { params = params, client_id = client_id, err = err })
+ if err then
+ error(tostring(err))
+ end
+ return fn(err, method, params, client_id)
+ end
+end
+
+return builtin_callbacks
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
new file mode 100644
index 0000000000..974eaae38c
--- /dev/null
+++ b/runtime/lua/vim/lsp/log.lua
@@ -0,0 +1,95 @@
+-- Logger for language client plugin.
+
+local log = {}
+
+-- Log level dictionary with reverse lookup as well.
+--
+-- Can be used to lookup the number from the name or the name from the number.
+-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
+-- Level numbers begin with 'trace' at 0
+log.levels = {
+ TRACE = 0;
+ DEBUG = 1;
+ INFO = 2;
+ WARN = 3;
+ ERROR = 4;
+ -- FATAL = 4;
+}
+
+-- Default log level is warn.
+local current_log_level = log.levels.WARN
+local log_date_format = "%FT%H:%M:%SZ%z"
+
+do
+ local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/"
+ local function path_join(...)
+ return table.concat(vim.tbl_flatten{...}, path_sep)
+ end
+ local logfilename = path_join(vim.fn.stdpath('data'), 'vim-lsp.log')
+
+ --- Return the log filename.
+ function log.get_filename()
+ return logfilename
+ end
+
+ vim.fn.mkdir(vim.fn.stdpath('data'), "p")
+ local logfile = assert(io.open(logfilename, "a+"))
+ for level, levelnr in pairs(log.levels) do
+ -- Also export the log level on the root object.
+ log[level] = levelnr
+ -- Set the lowercase name as the main use function.
+ -- If called without arguments, it will check whether the log level is
+ -- greater than or equal to this one. When called with arguments, it will
+ -- log at that level (if applicable, it is checked either way).
+ --
+ -- Recommended usage:
+ -- ```
+ -- local _ = log.warn() and log.warn("123")
+ -- ```
+ --
+ -- This way you can avoid string allocations if the log level isn't high enough.
+ log[level:lower()] = function(...)
+ local argc = select("#", ...)
+ if levelnr < current_log_level then return false end
+ if argc == 0 then return true end
+ local info = debug.getinfo(2, "Sl")
+ local fileinfo = string.format("%s:%s", info.short_src, info.currentline)
+ local parts = { table.concat({"[", level, "]", os.date(log_date_format), "]", fileinfo, "]"}, " ") }
+ for i = 1, argc do
+ local arg = select(i, ...)
+ if arg == nil then
+ table.insert(parts, "nil")
+ else
+ table.insert(parts, vim.inspect(arg, {newline=''}))
+ end
+ end
+ logfile:write(table.concat(parts, '\t'), "\n")
+ logfile:flush()
+ end
+ end
+ -- Add some space to make it easier to distinguish different neovim runs.
+ logfile:write("\n")
+end
+
+-- This is put here on purpose after the loop above so that it doesn't
+-- interfere with iterating the levels
+vim.tbl_add_reverse_lookup(log.levels)
+
+function log.set_level(level)
+ if type(level) == 'string' then
+ current_log_level = assert(log.levels[level:upper()], string.format("Invalid log level: %q", level))
+ else
+ assert(type(level) == 'number', "level must be a number or string")
+ assert(log.levels[level], string.format("Invalid log level: %d", level))
+ current_log_level = level
+ end
+end
+
+-- Return whether the level is sufficient for logging.
+-- @param level number log level
+function log.should_log(level)
+ return level >= current_log_level
+end
+
+return log
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
new file mode 100644
index 0000000000..1413a88ce2
--- /dev/null
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -0,0 +1,936 @@
+-- Protocol for the Microsoft Language Server Protocol (mslsp)
+
+local protocol = {}
+
+local function ifnil(a, b)
+ if a == nil then return b end
+ return a
+end
+
+
+--[=[
+-- Useful for interfacing with:
+-- https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md
+-- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md
+function transform_schema_comments()
+ nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]]
+ nvim.command [[silent! '<,'>s/^\(\s*\) \* \=\(.*\)/\1--\2/]]
+end
+function transform_schema_to_table()
+ transform_schema_comments()
+ nvim.command [[silent! '<,'>s/: \S\+//]]
+ nvim.command [[silent! '<,'>s/export const //]]
+ nvim.command [[silent! '<,'>s/export namespace \(\S*\)\s*{/protocol.\1 = {/]]
+ nvim.command [[silent! '<,'>s/namespace \(\S*\)\s*{/protocol.\1 = {/]]
+end
+--]=]
+
+local constants = {
+ DiagnosticSeverity = {
+ -- Reports an error.
+ Error = 1;
+ -- Reports a warning.
+ Warning = 2;
+ -- Reports an information.
+ Information = 3;
+ -- Reports a hint.
+ Hint = 4;
+ };
+
+ MessageType = {
+ -- An error message.
+ Error = 1;
+ -- A warning message.
+ Warning = 2;
+ -- An information message.
+ Info = 3;
+ -- A log message.
+ Log = 4;
+ };
+
+ -- The file event type.
+ FileChangeType = {
+ -- The file got created.
+ Created = 1;
+ -- The file got changed.
+ Changed = 2;
+ -- The file got deleted.
+ Deleted = 3;
+ };
+
+ -- The kind of a completion entry.
+ CompletionItemKind = {
+ Text = 1;
+ Method = 2;
+ Function = 3;
+ Constructor = 4;
+ Field = 5;
+ Variable = 6;
+ Class = 7;
+ Interface = 8;
+ Module = 9;
+ Property = 10;
+ Unit = 11;
+ Value = 12;
+ Enum = 13;
+ Keyword = 14;
+ Snippet = 15;
+ Color = 16;
+ File = 17;
+ Reference = 18;
+ Folder = 19;
+ EnumMember = 20;
+ Constant = 21;
+ Struct = 22;
+ Event = 23;
+ Operator = 24;
+ TypeParameter = 25;
+ };
+
+ -- How a completion was triggered
+ CompletionTriggerKind = {
+ -- Completion was triggered by typing an identifier (24x7 code
+ -- complete), manual invocation (e.g Ctrl+Space) or via API.
+ Invoked = 1;
+ -- Completion was triggered by a trigger character specified by
+ -- the `triggerCharacters` properties of the `CompletionRegistrationOptions`.
+ TriggerCharacter = 2;
+ -- Completion was re-triggered as the current completion list is incomplete.
+ TriggerForIncompleteCompletions = 3;
+ };
+
+ -- A document highlight kind.
+ DocumentHighlightKind = {
+ -- A textual occurrence.
+ Text = 1;
+ -- Read-access of a symbol, like reading a variable.
+ Read = 2;
+ -- Write-access of a symbol, like writing to a variable.
+ Write = 3;
+ };
+
+ -- A symbol kind.
+ SymbolKind = {
+ File = 1;
+ Module = 2;
+ Namespace = 3;
+ Package = 4;
+ Class = 5;
+ Method = 6;
+ Property = 7;
+ Field = 8;
+ Constructor = 9;
+ Enum = 10;
+ Interface = 11;
+ Function = 12;
+ Variable = 13;
+ Constant = 14;
+ String = 15;
+ Number = 16;
+ Boolean = 17;
+ Array = 18;
+ Object = 19;
+ Key = 20;
+ Null = 21;
+ EnumMember = 22;
+ Struct = 23;
+ Event = 24;
+ Operator = 25;
+ TypeParameter = 26;
+ };
+
+ -- Represents reasons why a text document is saved.
+ TextDocumentSaveReason = {
+ -- Manually triggered, e.g. by the user pressing save, by starting debugging,
+ -- or by an API call.
+ Manual = 1;
+ -- Automatic after a delay.
+ AfterDelay = 2;
+ -- When the editor lost focus.
+ FocusOut = 3;
+ };
+
+ ErrorCodes = {
+ -- Defined by JSON RPC
+ ParseError = -32700;
+ InvalidRequest = -32600;
+ MethodNotFound = -32601;
+ InvalidParams = -32602;
+ InternalError = -32603;
+ serverErrorStart = -32099;
+ serverErrorEnd = -32000;
+ ServerNotInitialized = -32002;
+ UnknownErrorCode = -32001;
+ -- Defined by the protocol.
+ RequestCancelled = -32800;
+ ContentModified = -32801;
+ };
+
+ -- Describes the content type that a client supports in various
+ -- result literals like `Hover`, `ParameterInfo` or `CompletionItem`.
+ --
+ -- Please note that `MarkupKinds` must not start with a `$`. This kinds
+ -- are reserved for internal usage.
+ MarkupKind = {
+ -- Plain text is supported as a content format
+ PlainText = 'plaintext';
+ -- Markdown is supported as a content format
+ Markdown = 'markdown';
+ };
+
+ ResourceOperationKind = {
+ -- Supports creating new files and folders.
+ Create = 'create';
+ -- Supports renaming existing files and folders.
+ Rename = 'rename';
+ -- Supports deleting existing files and folders.
+ Delete = 'delete';
+ };
+
+ FailureHandlingKind = {
+ -- Applying the workspace change is simply aborted if one of the changes provided
+ -- fails. All operations executed before the failing operation stay executed.
+ Abort = 'abort';
+ -- All operations are executed transactionally. That means they either all
+ -- succeed or no changes at all are applied to the workspace.
+ Transactional = 'transactional';
+ -- If the workspace edit contains only textual file changes they are executed transactionally.
+ -- If resource changes (create, rename or delete file) are part of the change the failure
+ -- handling strategy is abort.
+ TextOnlyTransactional = 'textOnlyTransactional';
+ -- The client tries to undo the operations already executed. But there is no
+ -- guarantee that this succeeds.
+ Undo = 'undo';
+ };
+
+ -- Known error codes for an `InitializeError`;
+ InitializeError = {
+ -- If the protocol version provided by the client can't be handled by the server.
+ -- @deprecated This initialize error got replaced by client capabilities. There is
+ -- no version handshake in version 3.0x
+ unknownProtocolVersion = 1;
+ };
+
+ -- Defines how the host (editor) should sync document changes to the language server.
+ TextDocumentSyncKind = {
+ -- Documents should not be synced at all.
+ None = 0;
+ -- Documents are synced by always sending the full content
+ -- of the document.
+ Full = 1;
+ -- Documents are synced by sending the full content on open.
+ -- After that only incremental updates to the document are
+ -- send.
+ Incremental = 2;
+ };
+
+ WatchKind = {
+ -- Interested in create events.
+ Create = 1;
+ -- Interested in change events
+ Change = 2;
+ -- Interested in delete events
+ Delete = 4;
+ };
+
+ -- Defines whether the insert text in a completion item should be interpreted as
+ -- plain text or a snippet.
+ InsertTextFormat = {
+ -- The primary text to be inserted is treated as a plain string.
+ PlainText = 1;
+ -- The primary text to be inserted is treated as a snippet.
+ --
+ -- A snippet can define tab stops and placeholders with `$1`, `$2`
+ -- and `${3:foo};`. `$0` defines the final tab stop, it defaults to
+ -- the end of the snippet. Placeholders with equal identifiers are linked,
+ -- that is typing in one will update others too.
+ Snippet = 2;
+ };
+
+ -- A set of predefined code action kinds
+ CodeActionKind = {
+ -- Empty kind.
+ Empty = '';
+ -- Base kind for quickfix actions
+ QuickFix = 'quickfix';
+ -- Base kind for refactoring actions
+ Refactor = 'refactor';
+ -- Base kind for refactoring extraction actions
+ --
+ -- Example extract actions:
+ --
+ -- - Extract method
+ -- - Extract function
+ -- - Extract variable
+ -- - Extract interface from class
+ -- - ...
+ RefactorExtract = 'refactor.extract';
+ -- Base kind for refactoring inline actions
+ --
+ -- Example inline actions:
+ --
+ -- - Inline function
+ -- - Inline variable
+ -- - Inline constant
+ -- - ...
+ RefactorInline = 'refactor.inline';
+ -- Base kind for refactoring rewrite actions
+ --
+ -- Example rewrite actions:
+ --
+ -- - Convert JavaScript function to class
+ -- - Add or remove parameter
+ -- - Encapsulate field
+ -- - Make method static
+ -- - Move method to base class
+ -- - ...
+ RefactorRewrite = 'refactor.rewrite';
+ -- Base kind for source actions
+ --
+ -- Source code actions apply to the entire file.
+ Source = 'source';
+ -- Base kind for an organize imports source action
+ SourceOrganizeImports = 'source.organizeImports';
+ };
+}
+
+for k, v in pairs(constants) do
+ vim.tbl_add_reverse_lookup(v)
+ protocol[k] = v
+end
+
+--[=[
+--Text document specific client capabilities.
+export interface TextDocumentClientCapabilities {
+ synchronization?: {
+ --Whether text document synchronization supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports sending will save notifications.
+ willSave?: boolean;
+ --The client supports sending a will save request and
+ --waits for a response providing text edits which will
+ --be applied to the document before it is saved.
+ willSaveWaitUntil?: boolean;
+ --The client supports did save notifications.
+ didSave?: boolean;
+ }
+ --Capabilities specific to the `textDocument/completion`
+ completion?: {
+ --Whether completion supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports the following `CompletionItem` specific
+ --capabilities.
+ completionItem?: {
+ --The client supports snippets as insert text.
+ --
+ --A snippet can define tab stops and placeholders with `$1`, `$2`
+ --and `${3:foo}`. `$0` defines the final tab stop, it defaults to
+ --the end of the snippet. Placeholders with equal identifiers are linked,
+ --that is typing in one will update others too.
+ snippetSupport?: boolean;
+ --The client supports commit characters on a completion item.
+ commitCharactersSupport?: boolean
+ --The client supports the following content formats for the documentation
+ --property. The order describes the preferred format of the client.
+ documentationFormat?: MarkupKind[];
+ --The client supports the deprecated property on a completion item.
+ deprecatedSupport?: boolean;
+ --The client supports the preselect property on a completion item.
+ preselectSupport?: boolean;
+ }
+ completionItemKind?: {
+ --The completion item kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ --
+ --If this property is not present the client only supports
+ --the completion items kinds from `Text` to `Reference` as defined in
+ --the initial version of the protocol.
+ valueSet?: CompletionItemKind[];
+ },
+ --The client supports to send additional context information for a
+ --`textDocument/completion` request.
+ contextSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/hover`
+ hover?: {
+ --Whether hover supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports the follow content formats for the content
+ --property. The order describes the preferred format of the client.
+ contentFormat?: MarkupKind[];
+ };
+ --Capabilities specific to the `textDocument/signatureHelp`
+ signatureHelp?: {
+ --Whether signature help supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports the following `SignatureInformation`
+ --specific properties.
+ signatureInformation?: {
+ --The client supports the follow content formats for the documentation
+ --property. The order describes the preferred format of the client.
+ documentationFormat?: MarkupKind[];
+ --Client capabilities specific to parameter information.
+ parameterInformation?: {
+ --The client supports processing label offsets instead of a
+ --simple label string.
+ --
+ --Since 3.14.0
+ labelOffsetSupport?: boolean;
+ }
+ };
+ };
+ --Capabilities specific to the `textDocument/references`
+ references?: {
+ --Whether references supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentHighlight`
+ documentHighlight?: {
+ --Whether document highlight supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentSymbol`
+ documentSymbol?: {
+ --Whether document symbol supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --Specific capabilities for the `SymbolKind`.
+ symbolKind?: {
+ --The symbol kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ --
+ --If this property is not present the client only supports
+ --the symbol kinds from `File` to `Array` as defined in
+ --the initial version of the protocol.
+ valueSet?: SymbolKind[];
+ }
+ --The client supports hierarchical document symbols.
+ hierarchicalDocumentSymbolSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/formatting`
+ formatting?: {
+ --Whether formatting supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/rangeFormatting`
+ rangeFormatting?: {
+ --Whether range formatting supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/onTypeFormatting`
+ onTypeFormatting?: {
+ --Whether on type formatting supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/declaration`
+ declaration?: {
+ --Whether declaration supports dynamic registration. If this is set to `true`
+ --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of declaration links.
+ --
+ --Since 3.14.0
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/definition`.
+ --
+ --Since 3.14.0
+ definition?: {
+ --Whether definition supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of definition links.
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/typeDefinition`
+ --
+ --Since 3.6.0
+ typeDefinition?: {
+ --Whether typeDefinition supports dynamic registration. If this is set to `true`
+ --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of definition links.
+ --
+ --Since 3.14.0
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/implementation`.
+ --
+ --Since 3.6.0
+ implementation?: {
+ --Whether implementation supports dynamic registration. If this is set to `true`
+ --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of definition links.
+ --
+ --Since 3.14.0
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/codeAction`
+ codeAction?: {
+ --Whether code action supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client support code action literals as a valid
+ --response of the `textDocument/codeAction` request.
+ --
+ --Since 3.8.0
+ codeActionLiteralSupport?: {
+ --The code action kind is support with the following value
+ --set.
+ codeActionKind: {
+ --The code action kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ valueSet: CodeActionKind[];
+ };
+ };
+ };
+ --Capabilities specific to the `textDocument/codeLens`
+ codeLens?: {
+ --Whether code lens supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentLink`
+ documentLink?: {
+ --Whether document link supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentColor` and the
+ --`textDocument/colorPresentation` request.
+ --
+ --Since 3.6.0
+ colorProvider?: {
+ --Whether colorProvider supports dynamic registration. If this is set to `true`
+ --the client supports the new `(ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ }
+ --Capabilities specific to the `textDocument/rename`
+ rename?: {
+ --Whether rename supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports testing for validity of rename operations
+ --before execution.
+ prepareSupport?: boolean;
+ };
+ --Capabilities specific to `textDocument/publishDiagnostics`.
+ publishDiagnostics?: {
+ --Whether the clients accepts diagnostics with related information.
+ relatedInformation?: boolean;
+ };
+ --Capabilities specific to `textDocument/foldingRange` requests.
+ --
+ --Since 3.10.0
+ foldingRange?: {
+ --Whether implementation supports dynamic registration for folding range providers. If this is set to `true`
+ --the client supports the new `(FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The maximum number of folding ranges that the client prefers to receive per document. The value serves as a
+ --hint, servers are free to follow the limit.
+ rangeLimit?: number;
+ --If set, the client signals that it only supports folding complete lines. If set, client will
+ --ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange.
+ lineFoldingOnly?: boolean;
+ };
+}
+--]=]
+
+--[=[
+--Workspace specific client capabilities.
+export interface WorkspaceClientCapabilities {
+ --The client supports applying batch edits to the workspace by supporting
+ --the request 'workspace/applyEdit'
+ applyEdit?: boolean;
+ --Capabilities specific to `WorkspaceEdit`s
+ workspaceEdit?: {
+ --The client supports versioned document changes in `WorkspaceEdit`s
+ documentChanges?: boolean;
+ --The resource operations the client supports. Clients should at least
+ --support 'create', 'rename' and 'delete' files and folders.
+ resourceOperations?: ResourceOperationKind[];
+ --The failure handling strategy of a client if applying the workspace edit
+ --fails.
+ failureHandling?: FailureHandlingKind;
+ };
+ --Capabilities specific to the `workspace/didChangeConfiguration` notification.
+ didChangeConfiguration?: {
+ --Did change configuration notification supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `workspace/didChangeWatchedFiles` notification.
+ didChangeWatchedFiles?: {
+ --Did change watched files notification supports dynamic registration. Please note
+ --that the current protocol doesn't support static configuration for file changes
+ --from the server side.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `workspace/symbol` request.
+ symbol?: {
+ --Symbol request supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --Specific capabilities for the `SymbolKind` in the `workspace/symbol` request.
+ symbolKind?: {
+ --The symbol kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ --
+ --If this property is not present the client only supports
+ --the symbol kinds from `File` to `Array` as defined in
+ --the initial version of the protocol.
+ valueSet?: SymbolKind[];
+ }
+ };
+ --Capabilities specific to the `workspace/executeCommand` request.
+ executeCommand?: {
+ --Execute command supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --The client has support for workspace folders.
+ --
+ --Since 3.6.0
+ workspaceFolders?: boolean;
+ --The client supports `workspace/configuration` requests.
+ --
+ --Since 3.6.0
+ configuration?: boolean;
+}
+--]=]
+
+function protocol.make_client_capabilities()
+ return {
+ textDocument = {
+ synchronization = {
+ dynamicRegistration = false;
+
+ -- TODO(ashkan) Send textDocument/willSave before saving (BufWritePre)
+ willSave = false;
+
+ -- TODO(ashkan) Implement textDocument/willSaveWaitUntil
+ willSaveWaitUntil = false;
+
+ -- Send textDocument/didSave after saving (BufWritePost)
+ didSave = true;
+ };
+ completion = {
+ dynamicRegistration = false;
+ completionItem = {
+
+ -- TODO(tjdevries): Is it possible to implement this in plain lua?
+ snippetSupport = false;
+ commitCharactersSupport = false;
+ preselectSupport = false;
+ deprecatedSupport = false;
+ documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ };
+ completionItemKind = {
+ valueSet = (function()
+ local res = {}
+ for k in pairs(protocol.CompletionItemKind) do
+ if type(k) == 'number' then table.insert(res, k) end
+ end
+ return res
+ end)();
+ };
+
+ -- TODO(tjdevries): Implement this
+ contextSupport = false;
+ };
+ hover = {
+ dynamicRegistration = false;
+ contentFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ };
+ signatureHelp = {
+ dynamicRegistration = false;
+ signatureInformation = {
+ documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ -- parameterInformation = {
+ -- labelOffsetSupport = false;
+ -- };
+ };
+ };
+ references = {
+ dynamicRegistration = false;
+ };
+ documentHighlight = {
+ dynamicRegistration = false
+ };
+ -- documentSymbol = {
+ -- dynamicRegistration = false;
+ -- symbolKind = {
+ -- valueSet = (function()
+ -- local res = {}
+ -- for k in pairs(protocol.SymbolKind) do
+ -- if type(k) == 'string' then table.insert(res, k) end
+ -- end
+ -- return res
+ -- end)();
+ -- };
+ -- hierarchicalDocumentSymbolSupport = false;
+ -- };
+ };
+ workspace = nil;
+ experimental = nil;
+ }
+end
+
+function protocol.make_text_document_position_params()
+ local position = vim.api.nvim_win_get_cursor(0)
+ return {
+ textDocument = {
+ uri = vim.uri_from_bufnr()
+ };
+ position = {
+ line = position[1] - 1;
+ character = position[2];
+ }
+ }
+end
+
+--[=[
+export interface DocumentFilter {
+ --A language id, like `typescript`.
+ language?: string;
+ --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
+ scheme?: string;
+ --A glob pattern, like `*.{ts,js}`.
+ --
+ --Glob patterns can have the following syntax:
+ --- `*` to match one or more characters in a path segment
+ --- `?` to match on one character in a path segment
+ --- `**` to match any number of path segments, including none
+ --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
+ --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+ --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+ pattern?: string;
+}
+--]=]
+
+--[[
+--Static registration options to be returned in the initialize request.
+interface StaticRegistrationOptions {
+ --The id used to register the request. The id can be used to deregister
+ --the request again. See also Registration#id.
+ id?: string;
+}
+
+export interface DocumentFilter {
+ --A language id, like `typescript`.
+ language?: string;
+ --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
+ scheme?: string;
+ --A glob pattern, like `*.{ts,js}`.
+ --
+ --Glob patterns can have the following syntax:
+ --- `*` to match one or more characters in a path segment
+ --- `?` to match on one character in a path segment
+ --- `**` to match any number of path segments, including none
+ --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
+ --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+ --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+ pattern?: string;
+}
+export type DocumentSelector = DocumentFilter[];
+export interface TextDocumentRegistrationOptions {
+ --A document selector to identify the scope of the registration. If set to null
+ --the document selector provided on the client side will be used.
+ documentSelector: DocumentSelector | null;
+}
+
+--Code Action options.
+export interface CodeActionOptions {
+ --CodeActionKinds that this server may return.
+ --
+ --The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server
+ --may list out every specific kind they provide.
+ codeActionKinds?: CodeActionKind[];
+}
+
+interface ServerCapabilities {
+ --Defines how text documents are synced. Is either a detailed structure defining each notification or
+ --for backwards compatibility the TextDocumentSyncKind number. If omitted it defaults to `TextDocumentSyncKind.None`.
+ textDocumentSync?: TextDocumentSyncOptions | number;
+ --The server provides hover support.
+ hoverProvider?: boolean;
+ --The server provides completion support.
+ completionProvider?: CompletionOptions;
+ --The server provides signature help support.
+ signatureHelpProvider?: SignatureHelpOptions;
+ --The server provides goto definition support.
+ definitionProvider?: boolean;
+ --The server provides Goto Type Definition support.
+ --
+ --Since 3.6.0
+ typeDefinitionProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides Goto Implementation support.
+ --
+ --Since 3.6.0
+ implementationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides find references support.
+ referencesProvider?: boolean;
+ --The server provides document highlight support.
+ documentHighlightProvider?: boolean;
+ --The server provides document symbol support.
+ documentSymbolProvider?: boolean;
+ --The server provides workspace symbol support.
+ workspaceSymbolProvider?: boolean;
+ --The server provides code actions. The `CodeActionOptions` return type is only
+ --valid if the client signals code action literal support via the property
+ --`textDocument.codeAction.codeActionLiteralSupport`.
+ codeActionProvider?: boolean | CodeActionOptions;
+ --The server provides code lens.
+ codeLensProvider?: CodeLensOptions;
+ --The server provides document formatting.
+ documentFormattingProvider?: boolean;
+ --The server provides document range formatting.
+ documentRangeFormattingProvider?: boolean;
+ --The server provides document formatting on typing.
+ documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions;
+ --The server provides rename support. RenameOptions may only be
+ --specified if the client states that it supports
+ --`prepareSupport` in its initial `initialize` request.
+ renameProvider?: boolean | RenameOptions;
+ --The server provides document link support.
+ documentLinkProvider?: DocumentLinkOptions;
+ --The server provides color provider support.
+ --
+ --Since 3.6.0
+ colorProvider?: boolean | ColorProviderOptions | (ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides folding provider support.
+ --
+ --Since 3.10.0
+ foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides go to declaration support.
+ --
+ --Since 3.14.0
+ declarationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides execute command support.
+ executeCommandProvider?: ExecuteCommandOptions;
+ --Workspace specific server capabilities
+ workspace?: {
+ --The server supports workspace folder.
+ --
+ --Since 3.6.0
+ workspaceFolders?: {
+ * The server has support for workspace folders
+ supported?: boolean;
+ * Whether the server wants to receive workspace folder
+ * change notifications.
+ *
+ * If a strings is provided the string is treated as a ID
+ * under which the notification is registered on the client
+ * side. The ID can be used to unregister for these events
+ * using the `client/unregisterCapability` request.
+ changeNotifications?: string | boolean;
+ }
+ }
+ --Experimental server capabilities.
+ experimental?: any;
+}
+--]]
+function protocol.resolve_capabilities(server_capabilities)
+ local general_properties = {}
+ local text_document_sync_properties
+ do
+ local TextDocumentSyncKind = protocol.TextDocumentSyncKind
+ local textDocumentSync = server_capabilities.textDocumentSync
+ if textDocumentSync == nil then
+ -- Defaults if omitted.
+ text_document_sync_properties = {
+ text_document_open_close = false;
+ text_document_did_change = TextDocumentSyncKind.None;
+-- text_document_did_change = false;
+ text_document_will_save = false;
+ text_document_will_save_wait_until = false;
+ text_document_save = false;
+ text_document_save_include_text = false;
+ }
+ elseif type(textDocumentSync) == 'number' then
+ -- Backwards compatibility
+ if not TextDocumentSyncKind[textDocumentSync] then
+ return nil, "Invalid server TextDocumentSyncKind for textDocumentSync"
+ end
+ text_document_sync_properties = {
+ text_document_open_close = true;
+ text_document_did_change = textDocumentSync;
+ text_document_will_save = false;
+ text_document_will_save_wait_until = false;
+ text_document_save = false;
+ text_document_save_include_text = false;
+ }
+ elseif type(textDocumentSync) == 'table' then
+ text_document_sync_properties = {
+ text_document_open_close = ifnil(textDocumentSync.openClose, false);
+ text_document_did_change = ifnil(textDocumentSync.change, TextDocumentSyncKind.None);
+ text_document_will_save = ifnil(textDocumentSync.willSave, false);
+ text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false);
+ text_document_save = ifnil(textDocumentSync.save, false);
+ text_document_save_include_text = ifnil(textDocumentSync.save and textDocumentSync.save.includeText, false);
+ }
+ else
+ return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync))
+ end
+ end
+ general_properties.hover = server_capabilities.hoverProvider or false
+ general_properties.goto_definition = server_capabilities.definitionProvider or false
+ general_properties.find_references = server_capabilities.referencesProvider or false
+ general_properties.document_highlight = server_capabilities.documentHighlightProvider or false
+ general_properties.document_symbol = server_capabilities.documentSymbolProvider or false
+ general_properties.workspace_symbol = server_capabilities.workspaceSymbolProvider or false
+ general_properties.document_formatting = server_capabilities.documentFormattingProvider or false
+ general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false
+
+ if server_capabilities.codeActionProvider == nil then
+ general_properties.code_action = false
+ elseif type(server_capabilities.codeActionProvider) == 'boolean' then
+ general_properties.code_action = server_capabilities.codeActionProvider
+ elseif type(server_capabilities.codeActionProvider) == 'table' then
+ -- TODO(ashkan) support CodeActionKind
+ general_properties.code_action = false
+ else
+ error("The server sent invalid codeActionProvider")
+ end
+
+ if server_capabilities.implementationProvider == nil then
+ general_properties.implementation = false
+ elseif type(server_capabilities.implementationProvider) == 'boolean' then
+ general_properties.implementation = server_capabilities.implementationProvider
+ elseif type(server_capabilities.implementationProvider) == 'table' then
+ -- TODO(ashkan) support more detailed implementation options.
+ general_properties.implementation = false
+ else
+ error("The server sent invalid implementationProvider")
+ end
+
+ local signature_help_properties
+ if server_capabilities.signatureHelpProvider == nil then
+ signature_help_properties = {
+ signature_help = false;
+ signature_help_trigger_characters = {};
+ }
+ elseif type(server_capabilities.signatureHelpProvider) == 'table' then
+ signature_help_properties = {
+ signature_help = true;
+ -- The characters that trigger signature help automatically.
+ signature_help_trigger_characters = server_capabilities.signatureHelpProvider.triggerCharacters or {};
+ }
+ else
+ error("The server sent invalid signatureHelpProvider")
+ end
+
+ return vim.tbl_extend("error"
+ , text_document_sync_properties
+ , signature_help_properties
+ , general_properties
+ )
+end
+
+return protocol
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
new file mode 100644
index 0000000000..e0ec8863d6
--- /dev/null
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -0,0 +1,451 @@
+local uv = vim.loop
+local log = require('vim.lsp.log')
+local protocol = require('vim.lsp.protocol')
+local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap
+
+-- TODO replace with a better implementation.
+local function json_encode(data)
+ local status, result = pcall(vim.fn.json_encode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+local function json_decode(data)
+ local status, result = pcall(vim.fn.json_decode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+
+local function is_dir(filename)
+ local stat = vim.loop.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+end
+
+local NIL = vim.NIL
+local function convert_NIL(v)
+ if v == NIL then return nil end
+ return v
+end
+
+-- If a dictionary is passed in, turn it into a list of string of "k=v"
+-- Accepts a table which can be composed of k=v strings or map-like
+-- specification, such as:
+--
+-- ```
+-- {
+-- "PRODUCTION=false";
+-- "PATH=/usr/bin/";
+-- PORT = 123;
+-- HOST = "0.0.0.0";
+-- }
+-- ```
+--
+-- Non-string values will be cast with `tostring`
+local function force_env_list(final_env)
+ if final_env then
+ local env = final_env
+ final_env = {}
+ for k,v in pairs(env) do
+ -- If it's passed in as a dict, then convert to list of "k=v"
+ if type(k) == "string" then
+ table.insert(final_env, k..'='..tostring(v))
+ elseif type(v) == 'string' then
+ table.insert(final_env, v)
+ else
+ -- TODO is this right or should I exception here?
+ -- Try to coerce other values to string.
+ table.insert(final_env, tostring(v))
+ end
+ end
+ return final_env
+ end
+end
+
+local function format_message_with_content_length(encoded_message)
+ return table.concat {
+ 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n';
+ encoded_message;
+ }
+end
+
+--- Parse an LSP Message's header
+-- @param header: The header to parse.
+local function parse_headers(header)
+ if type(header) ~= 'string' then
+ return nil
+ end
+ local headers = {}
+ for line in vim.gsplit(header, '\r\n', true) do
+ if line == '' then
+ break
+ end
+ local key, value = line:match("^%s*(%S+)%s*:%s*(.+)%s*$")
+ if key then
+ key = key:lower():gsub('%-', '_')
+ headers[key] = value
+ else
+ local _ = log.error() and log.error("invalid header line %q", line)
+ error(string.format("invalid header line %q", line))
+ end
+ end
+ headers.content_length = tonumber(headers.content_length)
+ or error(string.format("Content-Length not found in headers. %q", header))
+ return headers
+end
+
+-- This is the start of any possible header patterns. The gsub converts it to a
+-- case insensitive pattern.
+local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end)
+
+local function request_parser_loop()
+ local buffer = ''
+ while true do
+ -- A message can only be complete if it has a double CRLF and also the full
+ -- payload, so first let's check for the CRLFs
+ local start, finish = buffer:find('\r\n\r\n', 1, true)
+ -- Start parsing the headers
+ if start then
+ -- This is a workaround for servers sending initial garbage before
+ -- sending headers, such as if a bash script sends stdout. It assumes
+ -- that we know all of the headers ahead of time. At this moment, the
+ -- only valid headers start with "Content-*", so that's the thing we will
+ -- be searching for.
+ -- TODO(ashkan) I'd like to remove this, but it seems permanent :(
+ local buffer_start = buffer:find(header_start_pattern)
+ local headers = parse_headers(buffer:sub(buffer_start, start-1))
+ buffer = buffer:sub(finish+1)
+ local content_length = headers.content_length
+ -- Keep waiting for data until we have enough.
+ while #buffer < content_length do
+ buffer = buffer..(coroutine.yield()
+ or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
+ end
+ local body = buffer:sub(1, content_length)
+ buffer = buffer:sub(content_length + 1)
+ -- Yield our data.
+ buffer = buffer..(coroutine.yield(headers, body)
+ or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
+ else
+ -- Get more data since we don't have enough.
+ buffer = buffer..(coroutine.yield()
+ or error("Expected more data for the header. The server may have died.")) -- TODO hmm.
+ end
+ end
+end
+
+local client_errors = vim.tbl_add_reverse_lookup {
+ INVALID_SERVER_MESSAGE = 1;
+ INVALID_SERVER_JSON = 2;
+ NO_RESULT_CALLBACK_FOUND = 3;
+ READ_ERROR = 4;
+ NOTIFICATION_HANDLER_ERROR = 5;
+ SERVER_REQUEST_HANDLER_ERROR = 6;
+ SERVER_RESULT_CALLBACK_ERROR = 7;
+}
+
+local function format_rpc_error(err)
+ validate {
+ err = { err, 't' };
+ }
+ local code_name = assert(protocol.ErrorCodes[err.code], "err.code is invalid")
+ local message_parts = {"RPC", code_name}
+ if err.message then
+ table.insert(message_parts, "message = ")
+ table.insert(message_parts, string.format("%q", err.message))
+ end
+ if err.data then
+ table.insert(message_parts, "data = ")
+ table.insert(message_parts, vim.inspect(err.data))
+ end
+ return table.concat(message_parts, ' ')
+end
+
+local function rpc_response_error(code, message, data)
+ -- TODO should this error or just pick a sane error (like InternalError)?
+ local code_name = assert(protocol.ErrorCodes[code], 'Invalid rpc error code')
+ return setmetatable({
+ code = code;
+ message = message or code_name;
+ data = data;
+ }, {
+ __tostring = format_rpc_error;
+ })
+end
+
+local default_handlers = {}
+function default_handlers.notification(method, params)
+ local _ = log.debug() and log.debug('notification', method, params)
+end
+function default_handlers.server_request(method, params)
+ local _ = log.debug() and log.debug('server_request', method, params)
+ return nil, rpc_response_error(protocol.ErrorCodes.MethodNotFound)
+end
+function default_handlers.on_exit(code, signal)
+ local _ = log.info() and log.info("client exit", { code = code, signal = signal })
+end
+function default_handlers.on_error(code, err)
+ local _ = log.error() and log.error('client_error:', client_errors[code], err)
+end
+
+--- Create and start an RPC client.
+-- @param cmd [
+local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_params)
+ local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params})
+ validate {
+ cmd = { cmd, 's' };
+ cmd_args = { cmd_args, 't' };
+ handlers = { handlers, 't', true };
+ }
+
+ if not (vim.fn.executable(cmd) == 1) then
+ error(string.format("The given command %q is not executable.", cmd))
+ end
+ if handlers then
+ local user_handlers = handlers
+ handlers = {}
+ for handle_name, default_handler in pairs(default_handlers) do
+ local user_handler = user_handlers[handle_name]
+ if user_handler then
+ if type(user_handler) ~= 'function' then
+ error(string.format("handler.%s must be a function", handle_name))
+ end
+ -- server_request is wrapped elsewhere.
+ if not (handle_name == 'server_request'
+ or handle_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
+ then
+ user_handler = schedule_wrap(user_handler)
+ end
+ handlers[handle_name] = user_handler
+ else
+ handlers[handle_name] = default_handler
+ end
+ end
+ else
+ handlers = default_handlers
+ end
+
+ local stdin = uv.new_pipe(false)
+ local stdout = uv.new_pipe(false)
+ local stderr = uv.new_pipe(false)
+
+ local message_index = 0
+ local message_callbacks = {}
+
+ local handle, pid
+ do
+ local function onexit(code, signal)
+ stdin:close()
+ stdout:close()
+ stderr:close()
+ handle:close()
+ -- Make sure that message_callbacks can be gc'd.
+ message_callbacks = nil
+ handlers.on_exit(code, signal)
+ end
+ local spawn_params = {
+ args = cmd_args;
+ stdio = {stdin, stdout, stderr};
+ }
+ if extra_spawn_params then
+ spawn_params.cwd = extra_spawn_params.cwd
+ if spawn_params.cwd then
+ assert(is_dir(spawn_params.cwd), "cwd must be a directory")
+ end
+ spawn_params.env = force_env_list(extra_spawn_params.env)
+ end
+ handle, pid = uv.spawn(cmd, spawn_params, onexit)
+ end
+
+ local function encode_and_send(payload)
+ local _ = log.debug() and log.debug("rpc.send.payload", payload)
+ if handle:is_closing() then return false end
+ -- TODO(ashkan) remove this once we have a Lua json_encode
+ schedule(function()
+ local encoded = assert(json_encode(payload))
+ stdin:write(format_message_with_content_length(encoded))
+ end)
+ return true
+ end
+
+ local function send_notification(method, params)
+ local _ = log.debug() and log.debug("rpc.notify", method, params)
+ return encode_and_send {
+ jsonrpc = "2.0";
+ method = method;
+ params = params;
+ }
+ end
+
+ local function send_response(request_id, err, result)
+ return encode_and_send {
+ id = request_id;
+ jsonrpc = "2.0";
+ error = err;
+ result = result;
+ }
+ end
+
+ local function send_request(method, params, callback)
+ validate {
+ callback = { callback, 'f' };
+ }
+ message_index = message_index + 1
+ local message_id = message_index
+ local result = encode_and_send {
+ id = message_id;
+ jsonrpc = "2.0";
+ method = method;
+ params = params;
+ }
+ if result then
+ message_callbacks[message_id] = schedule_wrap(callback)
+ return result, message_id
+ else
+ return false
+ end
+ end
+
+ stderr:read_start(function(_err, chunk)
+ if chunk then
+ local _ = log.error() and log.error("rpc", cmd, "stderr", chunk)
+ end
+ end)
+
+ local function on_error(errkind, ...)
+ assert(client_errors[errkind])
+ -- TODO what to do if this fails?
+ pcall(handlers.on_error, errkind, ...)
+ end
+ local function pcall_handler(errkind, status, head, ...)
+ if not status then
+ on_error(errkind, head, ...)
+ return status, head
+ end
+ return status, head, ...
+ end
+ local function try_call(errkind, fn, ...)
+ return pcall_handler(errkind, pcall(fn, ...))
+ end
+
+ -- TODO periodically check message_callbacks for old requests past a certain
+ -- time and log them. This would require storing the timestamp. I could call
+ -- them with an error then, perhaps.
+
+ local function handle_body(body)
+ local decoded, err = json_decode(body)
+ if not decoded then
+ on_error(client_errors.INVALID_SERVER_JSON, err)
+ end
+ local _ = log.debug() and log.debug("decoded", decoded)
+
+ if type(decoded.method) == 'string' and decoded.id then
+ -- Server Request
+ decoded.params = convert_NIL(decoded.params)
+ -- Schedule here so that the users functions don't trigger an error and
+ -- we can still use the result.
+ schedule(function()
+ local status, result
+ status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR,
+ handlers.server_request, decoded.method, decoded.params)
+ local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err })
+ if status then
+ if not (result or err) then
+ -- TODO this can be a problem if `null` is sent for result. needs vim.NIL
+ error(string.format("method %q: either a result or an error must be sent to the server in response", decoded.method))
+ end
+ if err then
+ assert(type(err) == 'table', "err must be a table. Use rpc_response_error to help format errors.")
+ local code_name = assert(protocol.ErrorCodes[err.code], "Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.")
+ err.message = err.message or code_name
+ end
+ else
+ -- On an exception, result will contain the error message.
+ err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
+ result = nil
+ end
+ send_response(decoded.id, err, result)
+ end)
+ -- This works because we are expecting vim.NIL here
+ elseif decoded.id and (decoded.result or decoded.error) then
+ -- Server Result
+ decoded.error = convert_NIL(decoded.error)
+ decoded.result = convert_NIL(decoded.result)
+
+ -- We sent a number, so we expect a number.
+ local result_id = tonumber(decoded.id)
+ local callback = message_callbacks[result_id]
+ if callback then
+ message_callbacks[result_id] = nil
+ validate {
+ callback = { callback, 'f' };
+ }
+ if decoded.error then
+ decoded.error = setmetatable(decoded.error, {
+ __tostring = format_rpc_error;
+ })
+ end
+ try_call(client_errors.SERVER_RESULT_CALLBACK_ERROR,
+ callback, decoded.error, decoded.result)
+ else
+ on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
+ local _ = log.error() and log.error("No callback found for server response id "..result_id)
+ end
+ elseif type(decoded.method) == 'string' then
+ -- Notification
+ decoded.params = convert_NIL(decoded.params)
+ try_call(client_errors.NOTIFICATION_HANDLER_ERROR,
+ handlers.notification, decoded.method, decoded.params)
+ else
+ -- Invalid server message
+ on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
+ end
+ end
+ -- TODO(ashkan) remove this once we have a Lua json_decode
+ handle_body = schedule_wrap(handle_body)
+
+ local request_parser = coroutine.wrap(request_parser_loop)
+ request_parser()
+ stdout:read_start(function(err, chunk)
+ if err then
+ -- TODO better handling. Can these be intermittent errors?
+ on_error(client_errors.READ_ERROR, err)
+ return
+ end
+ -- This should signal that we are done reading from the client.
+ if not chunk then return end
+ -- Flush anything in the parser by looping until we don't get a result
+ -- anymore.
+ while true do
+ local headers, body = request_parser(chunk)
+ -- If we successfully parsed, then handle the response.
+ if headers then
+ handle_body(body)
+ -- Set chunk to empty so that we can call request_parser to get
+ -- anything existing in the parser to flush.
+ chunk = ''
+ else
+ break
+ end
+ end
+ end)
+
+ return {
+ pid = pid;
+ handle = handle;
+ request = send_request;
+ notify = send_notification;
+ }
+end
+
+return {
+ start = create_and_start_client;
+ rpc_response_error = rpc_response_error;
+ format_rpc_error = format_rpc_error;
+ client_errors = client_errors;
+}
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
new file mode 100644
index 0000000000..f96e0f01a8
--- /dev/null
+++ b/runtime/lua/vim/lsp/util.lua
@@ -0,0 +1,557 @@
+local protocol = require 'vim.lsp.protocol'
+local validate = vim.validate
+local api = vim.api
+
+local M = {}
+
+local split = vim.split
+local function split_lines(value)
+ return split(value, '\n', true)
+end
+
+local list_extend = vim.list_extend
+
+--- Find the longest shared prefix between prefix and word.
+-- e.g. remove_prefix("123tes", "testing") == "ting"
+local function remove_prefix(prefix, word)
+ local max_prefix_length = math.min(#prefix, #word)
+ local prefix_length = 0
+ for i = 1, max_prefix_length do
+ local current_line_suffix = prefix:sub(-i)
+ local word_prefix = word:sub(1, i)
+ if current_line_suffix == word_prefix then
+ prefix_length = i
+ end
+ end
+ return word:sub(prefix_length + 1)
+end
+
+local function resolve_bufnr(bufnr)
+ if bufnr == nil or bufnr == 0 then
+ return api.nvim_get_current_buf()
+ end
+ return bufnr
+end
+
+-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
+-- local valid_unix_path_characters = "[^/]"
+-- https://github.com/davidm/lua-glob-pattern
+-- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
+-- function M.glob_to_regex(glob)
+-- end
+
+--- Apply the TextEdit response.
+-- @params TextEdit [table] see https://microsoft.github.io/language-server-protocol/specification
+function M.text_document_apply_text_edit(text_edit, bufnr)
+ bufnr = resolve_bufnr(bufnr)
+ local range = text_edit.range
+ local start = range.start
+ local finish = range['end']
+ local new_lines = split_lines(text_edit.newText)
+ if start.character == 0 and finish.character == 0 then
+ api.nvim_buf_set_lines(bufnr, start.line, finish.line, false, new_lines)
+ return
+ end
+ api.nvim_err_writeln('apply_text_edit currently only supports character ranges starting at 0')
+ error('apply_text_edit currently only supports character ranges starting at 0')
+ return
+ -- TODO test and finish this support for character ranges.
+-- local lines = api.nvim_buf_get_lines(0, start.line, finish.line + 1, false)
+-- local suffix = lines[#lines]:sub(finish.character+2)
+-- local prefix = lines[1]:sub(start.character+2)
+-- new_lines[#new_lines] = new_lines[#new_lines]..suffix
+-- new_lines[1] = prefix..new_lines[1]
+-- api.nvim_buf_set_lines(0, start.line, finish.line, false, new_lines)
+end
+
+-- textDocument/completion response returns one of CompletionItem[], CompletionList or null.
+-- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
+function M.extract_completion_items(result)
+ if type(result) == 'table' and result.items then
+ return result.items
+ elseif result ~= nil then
+ return result
+ else
+ return {}
+ end
+end
+
+--- Apply the TextDocumentEdit response.
+-- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification
+function M.text_document_apply_text_document_edit(text_document_edit, bufnr)
+ -- local text_document = text_document_edit.textDocument
+ -- TODO use text_document_version?
+ -- local text_document_version = text_document.version
+
+ -- TODO technically, you could do this without doing multiple buf_get/set
+ -- by getting the full region (smallest line and largest line) and doing
+ -- the edits on the buffer, and then applying the buffer at the end.
+ -- I'm not sure if that's better.
+ for _, text_edit in ipairs(text_document_edit.edits) do
+ M.text_document_apply_text_edit(text_edit, bufnr)
+ end
+end
+
+function M.get_current_line_to_cursor()
+ local pos = api.nvim_win_get_cursor(0)
+ local line = assert(api.nvim_buf_get_lines(0, pos[1]-1, pos[1], false)[1])
+ return line:sub(pos[2]+1)
+end
+
+--- Getting vim complete-items with incomplete flag.
+-- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion)
+-- @return { matches = complete-items table, incomplete = boolean }
+function M.text_document_completion_list_to_complete_items(result, line_prefix)
+ local items = M.extract_completion_items(result)
+ if vim.tbl_isempty(items) then
+ return {}
+ end
+ -- Only initialize if we have some items.
+ if not line_prefix then
+ line_prefix = M.get_current_line_to_cursor()
+ end
+
+ local matches = {}
+
+ for _, completion_item in ipairs(items) do
+ local info = ' '
+ local documentation = completion_item.documentation
+ if documentation then
+ if type(documentation) == 'string' and documentation ~= '' then
+ info = documentation
+ elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
+ info = documentation.value
+ -- else
+ -- TODO(ashkan) Validation handling here?
+ end
+ end
+
+ local word = completion_item.insertText or completion_item.label
+
+ -- Ref: `:h complete-items`
+ table.insert(matches, {
+ word = remove_prefix(line_prefix, word),
+ abbr = completion_item.label,
+ kind = protocol.CompletionItemKind[completion_item.kind] or '',
+ menu = completion_item.detail or '',
+ info = info,
+ icase = 1,
+ dup = 0,
+ empty = 1,
+ })
+ end
+
+ return matches
+end
+
+-- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification
+function M.workspace_apply_workspace_edit(workspace_edit)
+ if workspace_edit.documentChanges then
+ for _, change in ipairs(workspace_edit.documentChanges) do
+ if change.kind then
+ -- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile
+ error(string.format("Unsupported change: %q", vim.inspect(change)))
+ else
+ M.text_document_apply_text_document_edit(change)
+ end
+ end
+ return
+ end
+
+ if workspace_edit.changes == nil or #workspace_edit.changes == 0 then
+ return
+ end
+
+ for uri, changes in pairs(workspace_edit.changes) do
+ local fname = vim.uri_to_fname(uri)
+ -- TODO improve this approach. Try to edit open buffers without switching.
+ -- Not sure how to handle files which aren't open. This is deprecated
+ -- anyway, so I guess it could be left as is.
+ api.nvim_command('edit '..fname)
+ for _, change in ipairs(changes) do
+ M.text_document_apply_text_edit(change)
+ end
+ end
+end
+
+--- Convert any of MarkedString | MarkedString[] | MarkupContent into markdown text lines
+-- see https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_hover
+-- Useful for textDocument/hover, textDocument/signatureHelp, and potentially others.
+function M.convert_input_to_markdown_lines(input, contents)
+ contents = contents or {}
+ -- MarkedString variation 1
+ if type(input) == 'string' then
+ list_extend(contents, split_lines(input))
+ else
+ assert(type(input) == 'table', "Expected a table for Hover.contents")
+ -- MarkupContent
+ if input.kind then
+ -- The kind can be either plaintext or markdown. However, either way we
+ -- will just be rendering markdown, so we handle them both the same way.
+ -- TODO these can have escaped/sanitized html codes in markdown. We
+ -- should make sure we handle this correctly.
+
+ -- Some servers send input.value as empty, so let's ignore this :(
+ -- assert(type(input.value) == 'string')
+ list_extend(contents, split_lines(input.value or ''))
+ -- MarkupString variation 2
+ elseif input.language then
+ -- Some servers send input.value as empty, so let's ignore this :(
+ -- assert(type(input.value) == 'string')
+ table.insert(contents, "```"..input.language)
+ list_extend(contents, split_lines(input.value or ''))
+ table.insert(contents, "```")
+ -- By deduction, this must be MarkedString[]
+ else
+ -- Use our existing logic to handle MarkedString
+ for _, marked_string in ipairs(input) do
+ M.convert_input_to_markdown_lines(marked_string, contents)
+ end
+ end
+ end
+ if contents[1] == '' or contents[1] == nil then
+ return {}
+ end
+ return contents
+end
+
+function M.make_floating_popup_options(width, height, opts)
+ validate {
+ opts = { opts, 't', true };
+ }
+ opts = opts or {}
+ validate {
+ ["opts.offset_x"] = { opts.offset_x, 'n', true };
+ ["opts.offset_y"] = { opts.offset_y, 'n', true };
+ }
+
+ local anchor = ''
+ local row, col
+
+ if vim.fn.winline() <= height then
+ anchor = anchor..'N'
+ row = 1
+ else
+ anchor = anchor..'S'
+ row = 0
+ end
+
+ if vim.fn.wincol() + width <= api.nvim_get_option('columns') then
+ anchor = anchor..'W'
+ col = 0
+ else
+ anchor = anchor..'E'
+ col = 1
+ end
+
+ return {
+ anchor = anchor,
+ col = col + (opts.offset_x or 0),
+ height = height,
+ relative = 'cursor',
+ row = row + (opts.offset_y or 0),
+ style = 'minimal',
+ width = width,
+ }
+end
+
+function M.open_floating_preview(contents, filetype, opts)
+ validate {
+ contents = { contents, 't' };
+ filetype = { filetype, 's', true };
+ opts = { opts, 't', true };
+ }
+
+ -- Trim empty lines from the end.
+ for i = #contents, 1, -1 do
+ if #contents[i] == 0 then
+ table.remove(contents)
+ else
+ break
+ end
+ end
+
+ local width = 0
+ local height = #contents
+ for i, line in ipairs(contents) do
+ -- Clean up the input and add left pad.
+ line = " "..line:gsub("\r", "")
+ -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
+ local line_width = vim.fn.strdisplaywidth(line)
+ width = math.max(line_width, width)
+ contents[i] = line
+ end
+ -- Add right padding of 1 each.
+ width = width + 1
+
+ local floating_bufnr = api.nvim_create_buf(false, true)
+ if filetype then
+ api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype)
+ end
+ local float_option = M.make_floating_popup_options(width, height, opts)
+ local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
+ if filetype == 'markdown' then
+ api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
+ end
+ api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
+ api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
+ api.nvim_command("autocmd CursorMoved <buffer> ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
+ return floating_bufnr, floating_winnr
+end
+
+local function validate_lsp_position(pos)
+ validate { pos = {pos, 't'} }
+ validate {
+ line = {pos.line, 'n'};
+ character = {pos.character, 'n'};
+ }
+ return true
+end
+
+function M.open_floating_peek_preview(bufnr, start, finish, opts)
+ validate {
+ bufnr = {bufnr, 'n'};
+ start = {start, validate_lsp_position, 'valid start Position'};
+ finish = {finish, validate_lsp_position, 'valid finish Position'};
+ opts = { opts, 't', true };
+ }
+ local width = math.max(finish.character - start.character + 1, 1)
+ local height = math.max(finish.line - start.line + 1, 1)
+ local floating_winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
+ api.nvim_win_set_cursor(floating_winnr, {start.line+1, start.character})
+ api.nvim_command("autocmd CursorMoved * ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
+ return floating_winnr
+end
+
+
+local function highlight_range(bufnr, ns, hiname, start, finish)
+ if start[1] == finish[1] then
+ -- TODO care about encoding here since this is in byte index?
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], finish[2])
+ else
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], -1)
+ for line = start[1] + 1, finish[1] - 1 do
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, line, 0, -1)
+ end
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, finish[1], 0, finish[2])
+ end
+end
+
+do
+ local all_buffer_diagnostics = {}
+
+ local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics")
+
+ local default_severity_highlight = {
+ [protocol.DiagnosticSeverity.Error] = { guifg = "Red" };
+ [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" };
+ [protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" };
+ [protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
+ }
+
+ local underline_highlight_name = "LspDiagnosticsUnderline"
+ api.nvim_command(string.format("highlight %s gui=underline cterm=underline", underline_highlight_name))
+
+ local function find_color_rgb(color)
+ local rgb_hex = api.nvim_get_color_by_name(color)
+ validate { color = {color, function() return rgb_hex ~= -1 end, "valid color name"} }
+ return rgb_hex
+ end
+
+ --- Determine whether to use black or white text
+ -- Ref: https://stackoverflow.com/a/1855903/837964
+ -- https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
+ local function color_is_bright(r, g, b)
+ -- Counting the perceptive luminance - human eye favors green color
+ local luminance = (0.299*r + 0.587*g + 0.114*b)/255
+ if luminance > 0.5 then
+ return true -- Bright colors, black font
+ else
+ return false -- Dark colors, white font
+ end
+ end
+
+ local severity_highlights = {}
+
+ function M.set_severity_highlights(highlights)
+ validate {highlights = {highlights, 't'}}
+ for severity, default_color in pairs(default_severity_highlight) do
+ local severity_name = protocol.DiagnosticSeverity[severity]
+ local highlight_name = "LspDiagnostics"..severity_name
+ local hi_info = highlights[severity] or default_color
+ -- Try to fill in the foreground color with a sane default.
+ if not hi_info.guifg and hi_info.guibg then
+ -- TODO(ashkan) move this out when bitop is guaranteed to be included.
+ local bit = require 'bit'
+ local band, rshift = bit.band, bit.rshift
+ local rgb = find_color_rgb(hi_info.guibg)
+ local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF))
+ hi_info.guifg = is_bright and "Black" or "White"
+ end
+ if not hi_info.ctermfg and hi_info.ctermbg then
+ -- TODO(ashkan) move this out when bitop is guaranteed to be included.
+ local bit = require 'bit'
+ local band, rshift = bit.band, bit.rshift
+ local rgb = find_color_rgb(hi_info.ctermbg)
+ local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF))
+ hi_info.ctermfg = is_bright and "Black" or "White"
+ end
+ local cmd_parts = {"highlight", highlight_name}
+ for k, v in pairs(hi_info) do
+ table.insert(cmd_parts, k.."="..v)
+ end
+ api.nvim_command(table.concat(cmd_parts, ' '))
+ severity_highlights[severity] = highlight_name
+ end
+ end
+
+ function M.buf_clear_diagnostics(bufnr)
+ validate { bufnr = {bufnr, 'n', true} }
+ bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
+ api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1)
+ end
+
+ -- Initialize with the defaults.
+ M.set_severity_highlights(default_severity_highlight)
+
+ function M.get_severity_highlight_name(severity)
+ return severity_highlights[severity]
+ end
+
+ function M.show_line_diagnostics()
+ local bufnr = api.nvim_get_current_buf()
+ local line = api.nvim_win_get_cursor(0)[1] - 1
+ -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {})
+ -- if #marks == 0 then
+ -- return
+ -- end
+ -- local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+ local lines = {"Diagnostics:"}
+ local highlights = {{0, "Bold"}}
+
+ local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+ if not buffer_diagnostics then return end
+ local line_diagnostics = buffer_diagnostics[line]
+ if not line_diagnostics then return end
+
+ for i, diagnostic in ipairs(line_diagnostics) do
+ -- for i, mark in ipairs(marks) do
+ -- local mark_id = mark[1]
+ -- local diagnostic = buffer_diagnostics[mark_id]
+
+ -- TODO(ashkan) make format configurable?
+ local prefix = string.format("%d. ", i)
+ local hiname = severity_highlights[diagnostic.severity]
+ local message_lines = split_lines(diagnostic.message)
+ table.insert(lines, prefix..message_lines[1])
+ table.insert(highlights, {#prefix + 1, hiname})
+ for j = 2, #message_lines do
+ table.insert(lines, message_lines[j])
+ table.insert(highlights, {0, hiname})
+ end
+ end
+ local popup_bufnr, winnr = M.open_floating_preview(lines, 'plaintext')
+ for i, hi in ipairs(highlights) do
+ local prefixlen, hiname = unpack(hi)
+ -- Start highlight after the prefix
+ api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1)
+ end
+ return popup_bufnr, winnr
+ end
+
+ function M.buf_diagnostics_save_positions(bufnr, diagnostics)
+ validate {
+ bufnr = {bufnr, 'n', true};
+ diagnostics = {diagnostics, 't', true};
+ }
+ if not diagnostics then return end
+ bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
+
+ if not all_buffer_diagnostics[bufnr] then
+ -- Clean up our data when the buffer unloads.
+ api.nvim_buf_attach(bufnr, false, {
+ on_detach = function(b)
+ all_buffer_diagnostics[b] = nil
+ end
+ })
+ end
+ all_buffer_diagnostics[bufnr] = {}
+ local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+
+ for _, diagnostic in ipairs(diagnostics) do
+ local start = diagnostic.range.start
+ -- local mark_id = api.nvim_buf_set_extmark(bufnr, diagnostic_ns, 0, start.line, 0, {})
+ -- buffer_diagnostics[mark_id] = diagnostic
+ local line_diagnostics = buffer_diagnostics[start.line]
+ if not line_diagnostics then
+ line_diagnostics = {}
+ buffer_diagnostics[start.line] = line_diagnostics
+ end
+ table.insert(line_diagnostics, diagnostic)
+ end
+ end
+
+
+ function M.buf_diagnostics_underline(bufnr, diagnostics)
+ for _, diagnostic in ipairs(diagnostics) do
+ local start = diagnostic.range.start
+ local finish = diagnostic.range["end"]
+
+ -- TODO care about encoding here since this is in byte index?
+ highlight_range(bufnr, diagnostic_ns, underline_highlight_name,
+ {start.line, start.character},
+ {finish.line, finish.character}
+ )
+ end
+ end
+
+ function M.buf_diagnostics_virtual_text(bufnr, diagnostics)
+ local buffer_line_diagnostics = all_buffer_diagnostics[bufnr]
+ if not buffer_line_diagnostics then
+ M.buf_diagnostics_save_positions(bufnr, diagnostics)
+ end
+ buffer_line_diagnostics = all_buffer_diagnostics[bufnr]
+ if not buffer_line_diagnostics then
+ return
+ end
+ for line, line_diags in pairs(buffer_line_diagnostics) do
+ local virt_texts = {}
+ for i = 1, #line_diags - 1 do
+ table.insert(virt_texts, {"■", severity_highlights[line_diags[i].severity]})
+ end
+ local last = line_diags[#line_diags]
+ -- TODO(ashkan) use first line instead of subbing 2 spaces?
+ table.insert(virt_texts, {"■ "..last.message:gsub("\r", ""):gsub("\n", " "), severity_highlights[last.severity]})
+ api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {})
+ end
+ end
+end
+
+function M.buf_loclist(bufnr, locations)
+ local targetwin
+ for _, winnr in ipairs(api.nvim_list_wins()) do
+ local winbuf = api.nvim_win_get_buf(winnr)
+ if winbuf == bufnr then
+ targetwin = winnr
+ break
+ end
+ end
+ if not targetwin then return end
+
+ local items = {}
+ local path = api.nvim_buf_get_name(bufnr)
+ for _, d in ipairs(locations) do
+ -- TODO: URL parsing here?
+ local start = d.range.start
+ table.insert(items, {
+ filename = path,
+ lnum = start.line + 1,
+ col = start.character + 1,
+ text = d.message,
+ })
+ end
+ vim.fn.setloclist(targetwin, items, ' ', 'Language Server')
+end
+
+return M
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index cd6f8a04d8..ff89acc524 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -4,34 +4,37 @@
-- test-suite. If, in the future, Nvim itself is used to run the test-suite
-- instead of "vanilla Lua", these functions could move to src/nvim/lua/vim.lua
+local vim = {}
--- Returns a deep copy of the given object. Non-table objects are copied as
--- in a typical Lua assignment, whereas table objects are copied recursively.
---
--@param orig Table to copy
--@returns New table of copied keys and (nested) values.
-local function deepcopy(orig)
- error(orig)
-end
-local function _id(v)
- return v
-end
-local deepcopy_funcs = {
- table = function(orig)
- local copy = {}
- for k, v in pairs(orig) do
- copy[deepcopy(k)] = deepcopy(v)
- end
- return copy
- end,
- number = _id,
- string = _id,
- ['nil'] = _id,
- boolean = _id,
-}
-deepcopy = function(orig)
- return deepcopy_funcs[type(orig)](orig)
-end
+function vim.deepcopy(orig) end -- luacheck: no unused
+vim.deepcopy = (function()
+ local function _id(v)
+ return v
+ end
+
+ local deepcopy_funcs = {
+ table = function(orig)
+ local copy = {}
+ for k, v in pairs(orig) do
+ copy[vim.deepcopy(k)] = vim.deepcopy(v)
+ end
+ return copy
+ end,
+ number = _id,
+ string = _id,
+ ['nil'] = _id,
+ boolean = _id,
+ }
+
+ return function(orig)
+ return deepcopy_funcs[type(orig)](orig)
+ end
+end)()
--- Splits a string at each instance of a separator.
---
@@ -43,10 +46,8 @@ end
--@param sep Separator string or pattern
--@param plain If `true` use `sep` literally (passed to String.find)
--@returns Iterator over the split components
-local function gsplit(s, sep, plain)
- assert(type(s) == "string")
- assert(type(sep) == "string")
- assert(type(plain) == "boolean" or type(plain) == "nil")
+function vim.gsplit(s, sep, plain)
+ vim.validate{s={s,'s'},sep={sep,'s'},plain={plain,'b',true}}
local start = 1
local done = false
@@ -92,20 +93,51 @@ end
--@param sep Separator string or pattern
--@param plain If `true` use `sep` literally (passed to String.find)
--@returns List-like table of the split components.
-local function split(s,sep,plain)
- local t={} for c in gsplit(s, sep, plain) do table.insert(t,c) end
+function vim.split(s,sep,plain)
+ local t={} for c in vim.gsplit(s, sep, plain) do table.insert(t,c) end
return t
end
+--- Return a list of all keys used in a table.
+--- However, the order of the return table of keys is not guaranteed.
+---
+--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+---
+--@param t Table
+--@returns list of keys
+function vim.tbl_keys(t)
+ assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
+
+ local keys = {}
+ for k, _ in pairs(t) do
+ table.insert(keys, k)
+ end
+ return keys
+end
+
+--- Return a list of all values used in a table.
+--- However, the order of the return table of values is not guaranteed.
+---
+--@param t Table
+--@returns list of values
+function vim.tbl_values(t)
+ assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
+
+ local values = {}
+ for _, v in pairs(t) do
+ table.insert(values, v)
+ end
+ return values
+end
+
--- Checks if a list-like (vector) table contains `value`.
---
--@param t Table to check
--@param value Value to compare
--@returns true if `t` contains `value`
-local function tbl_contains(t, value)
- if type(t) ~= 'table' then
- error('t must be a table')
- end
+function vim.tbl_contains(t, value)
+ vim.validate{t={t,'t'}}
+
for _,v in ipairs(t) do
if v == value then
return true
@@ -114,6 +146,16 @@ local function tbl_contains(t, value)
return false
end
+-- Returns true if the table is empty, and contains no indexed or keyed values.
+--
+--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+--
+--@param t Table to check
+function vim.tbl_isempty(t)
+ assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
+ return next(t) == nil
+end
+
--- Merges two or more map-like tables.
---
--@see |extend()|
@@ -123,7 +165,7 @@ end
--- - "keep": use value from the leftmost map
--- - "force": use value from the rightmost map
--@param ... Two or more map-like tables.
-local function tbl_extend(behavior, ...)
+function vim.tbl_extend(behavior, ...)
if (behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force') then
error('invalid "behavior": '..tostring(behavior))
end
@@ -145,13 +187,69 @@ local function tbl_extend(behavior, ...)
return ret
end
+--- Deep compare values for equality
+function vim.deep_equal(a, b)
+ if a == b then return true end
+ if type(a) ~= type(b) then return false end
+ if type(a) == 'table' then
+ -- TODO improve this algorithm's performance.
+ for k, v in pairs(a) do
+ if not vim.deep_equal(v, b[k]) then
+ return false
+ end
+ end
+ for k, v in pairs(b) do
+ if not vim.deep_equal(v, a[k]) then
+ return false
+ end
+ end
+ return true
+ end
+ return false
+end
+
+--- Add the reverse lookup values to an existing table.
+--- For example:
+--- `tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A = 1 }`
+--
+--Do note that it *modifies* the input.
+--@param o table The table to add the reverse to.
+function vim.tbl_add_reverse_lookup(o)
+ local keys = vim.tbl_keys(o)
+ for _, k in ipairs(keys) do
+ local v = o[k]
+ if o[v] then
+ error(string.format("The reverse lookup found an existing value for %q while processing key %q", tostring(v), tostring(k)))
+ end
+ o[v] = k
+ end
+ return o
+end
+
+--- Extends a list-like table with the values of another list-like table.
+---
+--NOTE: This *mutates* dst!
+--@see |extend()|
+---
+--@param dst The list which will be modified and appended to.
+--@param src The list from which values will be inserted.
+function vim.list_extend(dst, src)
+ assert(type(dst) == 'table', "dst must be a table")
+ assert(type(src) == 'table', "src must be a table")
+ for _, v in ipairs(src) do
+ table.insert(dst, v)
+ end
+ return dst
+end
+
--- Creates a copy of a list-like table such that any nested tables are
--- "unrolled" and appended to the result.
---
+--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+---
--@param t List-like table
--@returns Flattened copy of the given list-like table.
-local function tbl_flatten(t)
- -- From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+function vim.tbl_flatten(t)
local result = {}
local function _tbl_flatten(_t)
local n = #_t
@@ -168,13 +266,39 @@ local function tbl_flatten(t)
return result
end
+-- Determine whether a Lua table can be treated as an array.
+---
+--@params Table
+--@returns true: A non-empty array, false: A non-empty table, nil: An empty table
+function vim.tbl_islist(t)
+ if type(t) ~= 'table' then
+ return false
+ end
+
+ local count = 0
+
+ for k, _ in pairs(t) do
+ if type(k) == "number" then
+ count = count + 1
+ else
+ return false
+ end
+ end
+
+ if count > 0 then
+ return true
+ else
+ return nil
+ end
+end
+
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
---
--@see https://www.lua.org/pil/20.2.html
--@param s String to trim
--@returns String with whitespace removed from its beginning and end
-local function trim(s)
- assert(type(s) == 'string', 'Only strings can be trimmed')
+function vim.trim(s)
+ vim.validate{s={s,'s'}}
return s:match('^%s*(.*%S)') or ''
end
@@ -183,19 +307,100 @@ end
--@see https://github.com/rxi/lume
--@param s String to escape
--@returns %-escaped pattern string
-local function pesc(s)
- assert(type(s) == 'string')
+function vim.pesc(s)
+ vim.validate{s={s,'s'}}
return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1')
end
-local module = {
- deepcopy = deepcopy,
- gsplit = gsplit,
- pesc = pesc,
- split = split,
- tbl_contains = tbl_contains,
- tbl_extend = tbl_extend,
- tbl_flatten = tbl_flatten,
- trim = trim,
-}
-return module
+--- Validates a parameter specification (types and values).
+---
+--- Usage example:
+--- <pre>
+--- function user.new(name, age, hobbies)
+--- vim.validate{
+--- name={name, 'string'},
+--- age={age, 'number'},
+--- hobbies={hobbies, 'table'},
+--- }
+--- ...
+--- end
+--- </pre>
+---
+--- Examples with explicit argument values (can be run directly):
+--- <pre>
+--- vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
+--- => NOP (success)
+---
+--- vim.validate{arg1={1, 'table'}}
+--- => error('arg1: expected table, got number')
+---
+--- vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
+--- => error('arg1: expected even number, got 3')
+--- </pre>
+---
+--@param opt Map of parameter names to validations. Each key is a parameter
+--- name; each value is a tuple in one of these forms:
+--- 1. (arg_value, type_name, optional)
+--- - arg_value: argument value
+--- - type_name: string type name, one of: ("table", "t", "string",
+--- "s", "number", "n", "boolean", "b", "function", "f", "nil",
+--- "thread", "userdata")
+--- - optional: (optional) boolean, if true, `nil` is valid
+--- 2. (arg_value, fn, msg)
+--- - arg_value: argument value
+--- - fn: any function accepting one argument, returns true if and
+--- only if the argument is valid
+--- - msg: (optional) error string if validation fails
+function vim.validate(opt) end -- luacheck: no unused
+vim.validate = (function()
+ local type_names = {
+ t='table', s='string', n='number', b='boolean', f='function', c='callable',
+ ['table']='table', ['string']='string', ['number']='number',
+ ['boolean']='boolean', ['function']='function', ['callable']='callable',
+ ['nil']='nil', ['thread']='thread', ['userdata']='userdata',
+ }
+ local function _type_name(t)
+ local tname = type_names[t]
+ if tname == nil then
+ error(string.format('invalid type name: %s', tostring(t)))
+ end
+ return tname
+ end
+ local function _is_type(val, t)
+ return t == 'callable' and vim.is_callable(val) or type(val) == t
+ end
+
+ return function(opt)
+ assert(type(opt) == 'table', string.format('opt: expected table, got %s', type(opt)))
+ for param_name, spec in pairs(opt) do
+ assert(type(spec) == 'table', string.format('%s: expected table, got %s', param_name, type(spec)))
+
+ local val = spec[1] -- Argument value.
+ local t = spec[2] -- Type name, or callable.
+ local optional = (true == spec[3])
+
+ if not vim.is_callable(t) then -- Check type name.
+ if (not optional or val ~= nil) and not _is_type(val, _type_name(t)) then
+ error(string.format("%s: expected %s, got %s", param_name, _type_name(t), type(val)))
+ end
+ elseif not t(val) then -- Check user-provided validation function.
+ error(string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), val))
+ end
+ end
+ return true
+ end
+end)()
+
+--- Returns true if object `f` can be called as a function.
+---
+--@param f Any object
+--@return true if `f` is callable, else false
+function vim.is_callable(f)
+ if type(f) == 'function' then return true end
+ local m = getmetatable(f)
+ if m == nil then return false end
+ return type(m.__call) == 'function'
+end
+
+return vim
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua
new file mode 100644
index 0000000000..0a6e0fcb97
--- /dev/null
+++ b/runtime/lua/vim/uri.lua
@@ -0,0 +1,89 @@
+--- TODO: This is implemented only for files now.
+-- https://tools.ietf.org/html/rfc3986
+-- https://tools.ietf.org/html/rfc2732
+-- https://tools.ietf.org/html/rfc2396
+
+
+local uri_decode
+do
+ local schar = string.char
+ local function hex_to_char(hex)
+ return schar(tonumber(hex, 16))
+ end
+ uri_decode = function(str)
+ return str:gsub("%%([a-fA-F0-9][a-fA-F0-9])", hex_to_char)
+ end
+end
+
+local uri_encode
+do
+ local PATTERNS = {
+ --- RFC 2396
+ -- https://tools.ietf.org/html/rfc2396#section-2.2
+ rfc2396 = "^A-Za-z0-9%-_.!~*'()";
+ --- RFC 2732
+ -- https://tools.ietf.org/html/rfc2732
+ rfc2732 = "^A-Za-z0-9%-_.!~*'()[]";
+ --- RFC 3986
+ -- https://tools.ietf.org/html/rfc3986#section-2.2
+ rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/";
+ }
+ local sbyte, tohex = string.byte
+ if jit then
+ tohex = require'bit'.tohex
+ else
+ tohex = function(b) return string.format("%02x", b) end
+ end
+ local function percent_encode_char(char)
+ return "%"..tohex(sbyte(char), 2)
+ end
+ uri_encode = function(text, rfc)
+ if not text then return end
+ local pattern = PATTERNS[rfc] or PATTERNS.rfc3986
+ return text:gsub("(["..pattern.."])", percent_encode_char)
+ end
+end
+
+
+local function is_windows_file_uri(uri)
+ return uri:match('^file:///[a-zA-Z]:') ~= nil
+end
+
+local function uri_from_fname(path)
+ local volume_path, fname = path:match("^([a-zA-Z]:)(.*)")
+ local is_windows = volume_path ~= nil
+ if is_windows then
+ path = volume_path..uri_encode(fname:gsub("\\", "/"))
+ else
+ path = uri_encode(path)
+ end
+ local uri_parts = {"file://"}
+ if is_windows then
+ table.insert(uri_parts, "/")
+ end
+ table.insert(uri_parts, path)
+ return table.concat(uri_parts)
+end
+
+local function uri_from_bufnr(bufnr)
+ return uri_from_fname(vim.api.nvim_buf_get_name(bufnr))
+end
+
+local function uri_to_fname(uri)
+ -- TODO improve this.
+ if is_windows_file_uri(uri) then
+ uri = uri:gsub('^file:///', '')
+ uri = uri:gsub('/', '\\')
+ else
+ uri = uri:gsub('^file://', '')
+ end
+
+ return uri_decode(uri)
+end
+
+return {
+ uri_from_fname = uri_from_fname,
+ uri_from_bufnr = uri_from_bufnr,
+ uri_to_fname = uri_to_fname,
+}
+-- vim:sw=2 ts=2 et
diff --git a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
index 52b4829f5f..b9fc77dc37 100644
--- a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
+++ b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
@@ -583,6 +583,7 @@ func s:HandleEvaluate(msg)
endif
let s:evalFromBalloonExprResult = split(s:evalFromBalloonExprResult, '\\n')
call s:OpenHoverPreview(s:evalFromBalloonExprResult, v:null)
+ let s:evalFromBalloonExprResult = ''
else
echomsg '"' . s:evalexpr . '": ' . value
endif
diff --git a/runtime/tutor/en/vim-01-beginner.tutor b/runtime/tutor/en/vim-01-beginner.tutor
index 4e6154b24a..5ae0fde0da 100644
--- a/runtime/tutor/en/vim-01-beginner.tutor
+++ b/runtime/tutor/en/vim-01-beginner.tutor
@@ -91,7 +91,7 @@ NOTE: [:q!](:q) <Enter> discards any changes you made. In a few lessons you
** Press `x`{normal} to delete the character under the cursor. **
- 1. Move the cursor to the line below marked --->.
+ 1. Move the cursor to the line below marked ✗.
2. To fix the errors, move the cursor until it is on top of the
character to be deleted.
@@ -111,7 +111,7 @@ NOTE: As you go through this tutor, do not try to memorize, learn by
** Press `i`{normal} to insert text. **
- 1. Move the cursor to the first line below marked --->.
+ 1. Move the cursor to the first line below marked ✗.
2. To make the first line the same as the second, move the cursor on top
of the first character AFTER where the text is to be inserted.
@@ -130,7 +130,7 @@ There is some text missing from this line.
** Press `A`{normal} to append text. **
- 1. Move the cursor to the first line below marked --->.
+ 1. Move the cursor to the first line below marked ✗.
It does not matter on what character the cursor is in that line.
2. Press [A](A) and type in the necessary additions.
@@ -138,7 +138,7 @@ There is some text missing from this line.
3. As the text has been appended press `<Esc>`{normal} to return to Normal
mode.
- 4. Move the cursor to the second line marked ---> and repeat
+ 4. Move the cursor to the second line marked ✗ and repeat
steps 2 and 3 to correct this sentence.
There is some text missing from th
@@ -211,7 +211,7 @@ Now continue with Lesson 2.
1. Press `<Esc>`{normal} to make sure you are in Normal mode.
- 2. Move the cursor to the line below marked --->.
+ 2. Move the cursor to the line below marked ✗.
3. Move the cursor to the beginning of a word that needs to be deleted.
@@ -227,7 +227,7 @@ There are a some words fun that don't belong paper in this sentence.
1. Press `<Esc>`{normal} to make sure you are in Normal mode.
- 2. Move the cursor to the line below marked --->.
+ 2. Move the cursor to the line below marked ✗.
3. Move the cursor to the end of the correct line (AFTER the first . ).
@@ -263,7 +263,7 @@ NOTE: Pressing just the motion while in Normal mode without an operator
** Typing a number before a motion repeats it that many times. **
- 1. Move the cursor to the start of the line marked ---> below.
+ 1. Move the cursor to the start of the line marked ✓ below.
2. Type `2w`{normal} to move the cursor two words forward.
@@ -285,7 +285,7 @@ In the combination of the delete operator and a motion mentioned above you
insert a count before the motion to delete more:
d number motion
- 1. Move the cursor to the first UPPER CASE word in the line marked --->.
+ 1. Move the cursor to the first UPPER CASE word in the line marked ✗.
2. Type `d2w`{normal} to delete the two UPPER CASE words
@@ -318,7 +318,7 @@ it would be easier to simply type two d's to delete a line.
** Press `u`{normal} to undo the last commands, `U`{normal} to fix a whole line. **
- 1. Move the cursor to the line below marked ---> and place it on the
+ 1. Move the cursor to the line below marked ✗ and place it on the
first error.
2. Type `x`{normal} to delete the first unwanted character.
3. Now type `u`{normal} to undo the last command executed.
@@ -359,7 +359,7 @@ Fiix the errors oon thhis line and reeplace them witth undo.
** Type `p`{normal} to put previously deleted text after the cursor. **
- 1. Move the cursor to the first ---> line below.
+ 1. Move the cursor to the first ✓ line below.
2. Type `dd`{normal} to delete the line and store it in a Vim register.
@@ -378,7 +378,7 @@ a) Roses are red,
** Type `rx`{normal} to replace the character at the cursor with x. **
- 1. Move the cursor to the first line below marked --->.
+ 1. Move the cursor to the first line below marked ✗.
2. Move the cursor so that it is on top of the first error.
@@ -397,7 +397,7 @@ NOTE: Remember that you should be learning by doing, not memorization.
** To change until the end of a word, type `ce`{normal}. **
- 1. Move the cursor to the first line below marked --->.
+ 1. Move the cursor to the first line below marked ✗.
2. Place the cursor on the "u" in "lubw".
@@ -423,7 +423,7 @@ Notice that [c](c)e deletes the word and places you in Insert mode.
2. The motions are the same, such as `w`{normal} (word) and `$`{normal} (end of line).
- 3. Move to the first line below marked --->.
+ 3. Move to the first line below marked ✗.
4. Move the cursor to the first error.
@@ -503,7 +503,7 @@ NOTE: When the search reaches the end of the file it will continue at the
** Type `%`{normal} to find a matching ),], or }. **
- 1. Place the cursor on any (, [, or { in the line below marked --->.
+ 1. Place the cursor on any (, [, or { in the line below marked ✓.
2. Now type the [%](%) character.
@@ -521,7 +521,7 @@ NOTE: This is very useful in debugging a program with unmatched parentheses!
** Type `:s/old/new/g` to substitute "new" for "old". **
- 1. Move the cursor to the line below marked --->.
+ 1. Move the cursor to the line below marked ✗.
2. Type
~~~ cmd
@@ -725,7 +725,7 @@ NOTE: You can also read the output of an external command. For example,
** Type `o`{normal} to open a line below the cursor and place you in Insert mode. **
- 1. Move the cursor to the line below marked --->.
+ 1. Move the cursor to the line below marked ✓.
2. Type the lowercase letter `o`{normal} to [open](o) up a line BELOW the
cursor and place you in Insert mode.
@@ -743,7 +743,7 @@ Open up a line above this by typing O while the cursor is on this line.
** Type `a`{normal} to insert text AFTER the cursor. **
- 1. Move the cursor to the start of the line below marked --->.
+ 1. Move the cursor to the start of the line below marked ✗.
2. Press `e`{normal} until the cursor is on the end of "li".
@@ -766,7 +766,7 @@ NOTE: [a](a), [i](i) and [A](A) all go to the same Insert mode, the only
** Type a capital `R`{normal} to replace more than one character. **
- 1. Move the cursor to the first line below marked --->. Move the cursor to
+ 1. Move the cursor to the first line below marked ✗. Move the cursor to
the beginning of the first "xxx".
2. Now press `R`{normal} ([capital R](R)) and type the number below it in the
@@ -787,7 +787,7 @@ NOTE: Replace mode is like Insert mode, but every typed character deletes an
** Use the `y`{normal} operator to copy text and `p`{normal} to paste it. **
- 1. Go to the line marked with ---> below and place the cursor after "a)".
+ 1. Go to the line marked with ✓ below and place the cursor after "a)".
2. Start Visual mode with `v`{normal} and move the cursor to just before
"first".
@@ -805,7 +805,7 @@ NOTE: Replace mode is like Insert mode, but every typed character deletes an
end of the next line with `j$`{normal} and put the text there with `p`{normal}
a) This is the first item.
- b)
+b)
NOTE: you can use `y`{normal} as an operator: `yw`{normal} yanks one word.
diff --git a/runtime/tutor/en/vim-01-beginner.tutor.json b/runtime/tutor/en/vim-01-beginner.tutor.json
index 2f87d7543f..af22cf2aca 100644
--- a/runtime/tutor/en/vim-01-beginner.tutor.json
+++ b/runtime/tutor/en/vim-01-beginner.tutor.json
@@ -1,43 +1,45 @@
{
- "expect": {
- "24": -1,
- "103": "The cow jumped over the moon.",
- "124": "There is some text missing from this line.",
- "125": "There is some text missing from this line.",
- "144": "There is some text missing from this line.",
- "145": "There is some text missing from this line.",
- "146": "There is also some text missing here.",
- "147": "There is also some text missing here.",
- "220": "There are some words that don't belong in this sentence.",
- "236": "Somebody typed the end of this line twice.",
- "276": -1,
- "295": "This line of words is cleaned up.",
- "309": -1,
- "310": -1,
- "311": -1,
- "312": -1,
- "313": -1,
- "314": -1,
- "315": -1,
- "332": "Fix the errors on this line and replace them with undo.",
- "372": -1,
- "373": -1,
- "374": -1,
- "375": -1,
- "389": "When this line was typed in, someone pressed some wrong keys!",
- "390": "When this line was typed in, someone pressed some wrong keys!",
- "411": "This line has a few words that need changing using the change operator.",
- "412": "This line has a few words that need changing using the change operator.",
- "432": "The end of this line needs to be corrected using the c$ command.",
- "433": "The end of this line needs to be corrected using the c$ command.",
- "497": -1,
- "516": -1,
- "541": "Usually the best time to see the flowers is in the spring.",
- "759": "This line will allow you to practice appending text to a line.",
- "760": "This line will allow you to practice appending text to a line.",
- "780": "Adding 123 to 456 gives you 579.",
- "781": "Adding 123 to 456 gives you 579.",
- "807": "a) This is the first item.",
- "808": " b) This is the second item."
- }
+ "expect": {
+ "24": -1,
+ "103": "The cow jumped over the moon.",
+ "124": "There is some text missing from this line.",
+ "125": "There is some text missing from this line.",
+ "144": "There is some text missing from this line.",
+ "145": "There is some text missing from this line.",
+ "146": "There is also some text missing here.",
+ "147": "There is also some text missing here.",
+ "220": "There are some words that don't belong in this sentence.",
+ "236": "Somebody typed the end of this line twice.",
+ "276": -1,
+ "295": "This line of words is cleaned up.",
+ "309": -1,
+ "310": -1,
+ "311": -1,
+ "312": -1,
+ "313": -1,
+ "314": -1,
+ "315": -1,
+ "332": "Fix the errors on this line and replace them with undo.",
+ "372": -1,
+ "373": -1,
+ "374": -1,
+ "375": -1,
+ "389": "When this line was typed in, someone pressed some wrong keys!",
+ "390": "When this line was typed in, someone pressed some wrong keys!",
+ "411": "This line has a few words that need changing using the change operator.",
+ "412": "This line has a few words that need changing using the change operator.",
+ "432": "The end of this line needs to be corrected using the `c$` command.",
+ "433": "The end of this line needs to be corrected using the `c$` command.",
+ "497": -1,
+ "516": -1,
+ "541": "Usually the best time to see the flowers is in the spring.",
+ "735": -1,
+ "740": -1,
+ "759": "This line will allow you to practice appending text to a line.",
+ "760": "This line will allow you to practice appending text to a line.",
+ "780": "Adding 123 to 456 gives you 579.",
+ "781": "Adding 123 to 456 gives you 579.",
+ "807": "a) This is the first item.",
+ "808": "b) This is the second item."
+ }
}
diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py
index 373a58d11e..4d71d5e15e 100755
--- a/scripts/gen_vimdoc.py
+++ b/scripts/gen_vimdoc.py
@@ -36,11 +36,12 @@ import shutil
import textwrap
import subprocess
import collections
+import msgpack
from xml.dom import minidom
-if sys.version_info[0] < 3:
- print("use Python 3")
+if sys.version_info[0] < 3 or sys.version_info[1] < 5:
+ print("requires Python 3.5+")
sys.exit(1)
DEBUG = ('DEBUG' in os.environ)
@@ -84,7 +85,7 @@ CONFIG = {
'append_only': [],
},
'lua': {
- 'filename': 'if_lua.txt',
+ 'filename': 'lua.txt',
'section_start_token': '*lua-vim*',
'section_order': [
'vim.lua',
@@ -453,7 +454,7 @@ def parse_source_xml(filename, mode):
"""
global xrefs
xrefs = set()
- functions = []
+ functions = {} # Map of func_name:docstring.
deprecated_functions = []
dom = minidom.parse(filename)
@@ -577,11 +578,13 @@ def parse_source_xml(filename, mode):
if 'Deprecated' in xrefs:
deprecated_functions.append(func_doc)
elif name.startswith(CONFIG[mode]['func_name_prefix']):
- functions.append(func_doc)
+ functions[name] = func_doc
xrefs.clear()
- return '\n\n'.join(functions), '\n\n'.join(deprecated_functions)
+ return ('\n\n'.join(list(functions.values())),
+ '\n\n'.join(deprecated_functions),
+ functions)
def delete_lines_below(filename, tokenstr):
@@ -604,6 +607,13 @@ def gen_docs(config):
Doxygen is called and configured through stdin.
"""
for mode in CONFIG:
+ functions = {} # Map of func_name:docstring.
+ mpack_file = os.path.join(
+ base_dir, 'runtime', 'doc',
+ CONFIG[mode]['filename'].replace('.txt', '.mpack'))
+ if os.path.exists(mpack_file):
+ os.remove(mpack_file)
+
output_dir = out_dir.format(mode=mode)
p = subprocess.Popen(['doxygen', '-'], stdin=subprocess.PIPE)
p.communicate(
@@ -645,14 +655,15 @@ def gen_docs(config):
filename = get_text(find_first(compound, 'name'))
if filename.endswith('.c') or filename.endswith('.lua'):
- functions, deprecated = parse_source_xml(
- os.path.join(base, '%s.xml' %
- compound.getAttribute('refid')), mode)
+ functions_text, deprecated_text, fns = parse_source_xml(
+ os.path.join(base, '{}.xml'.format(
+ compound.getAttribute('refid'))), mode)
+ # Collect functions from all modules (for the current `mode`).
+ functions = {**functions, **fns}
- if not functions and not deprecated:
+ if not functions_text and not deprecated_text:
continue
-
- if functions or deprecated:
+ else:
name = os.path.splitext(os.path.basename(filename))[0]
if name == 'ui':
name = name.upper()
@@ -665,12 +676,12 @@ def gen_docs(config):
if intro:
doc += '\n\n' + intro
- if functions:
- doc += '\n\n' + functions
+ if functions_text:
+ doc += '\n\n' + functions_text
- if INCLUDE_DEPRECATED and deprecated:
+ if INCLUDE_DEPRECATED and deprecated_text:
doc += '\n\n\nDeprecated %s Functions: ~\n\n' % name
- doc += deprecated
+ doc += deprecated_text
if doc:
filename = os.path.basename(filename)
@@ -713,6 +724,8 @@ def gen_docs(config):
delete_lines_below(doc_file, CONFIG[mode]['section_start_token'])
with open(doc_file, 'ab') as fp:
fp.write(docs.encode('utf8'))
+ with open(mpack_file, 'wb') as fp:
+ fp.write(msgpack.packb(functions, use_bin_type=True))
shutil.rmtree(output_dir)
diff --git a/scripts/lua2dox.lua b/scripts/lua2dox.lua
index 77cdabcc4b..438f734917 100644
--- a/scripts/lua2dox.lua
+++ b/scripts/lua2dox.lua
@@ -543,7 +543,6 @@ function TLua2DoX_filter.readfile(this,AppStamp,Filename)
local fn = TString_removeCommentFromLine(string_trim(string.sub(line,pos_fn+8)))
if fn_magic then
fn = fn_magic
- fn_magic = nil
end
if string.sub(fn,1,1)=='(' then
@@ -554,49 +553,20 @@ function TLua2DoX_filter.readfile(this,AppStamp,Filename)
-- want to fix for iffy declarations
local open_paren = string.find(fn,'[%({]')
- local fn0 = fn
if open_paren then
- fn0 = string.sub(fn,1,open_paren-1)
-- we might have a missing close paren
if not string.find(fn,'%)') then
fn = fn .. ' ___MissingCloseParenHere___)'
end
end
- local dot = string.find(fn0,'[%.:]')
- if dot then -- it's a method
- local klass = string.sub(fn,1,dot-1)
- local method = string.sub(fn,dot+1)
- --TCore_IO_writeln('function ' .. klass .. '::' .. method .. ftail .. '{}')
- --TCore_IO_writeln(klass .. '::' .. method .. ftail .. '{}')
- outStream:writeln(
- '/*! \\memberof ' .. klass .. ' */ '
- .. method .. '{}'
- )
- else
- -- add vanilla function
-
- outStream:writeln(fn_type .. 'function ' .. fn .. '{}')
- end
+ -- add vanilla function
+ outStream:writeln(fn_type .. 'function ' .. fn .. '{}')
end
else
this:warning(inStream:getLineNo(),'something weird here')
end
fn_magic = nil -- mustn't indavertently use it again
- elseif string.find(line,'=%s*class%(') then
- state = 'in_class' -- it's a class declaration
- local tailComment
- line,tailComment = TString_removeCommentFromLine(line)
- local equals = string.find(line,'=')
- local klass = string_trim(string.sub(line,1,equals-1))
- local tail = string_trim(string.sub(line,equals+1))
- -- class(wibble wibble)
- -- ....v.
- local parent = string.sub(tail,7,-2)
- if #parent>0 then
- parent = ' :public ' .. parent
- end
- outStream:writeln('class ' .. klass .. parent .. '{};')
else
state = '' -- unknown
if #line>0 then -- we don't know what this line means, so just comment it out
diff --git a/scripts/shadacat.py b/scripts/shadacat.py
index 89846427a5..2b71fc2385 100755
--- a/scripts/shadacat.py
+++ b/scripts/shadacat.py
@@ -66,7 +66,7 @@ except IndexError:
def filt(entry): return True
else:
_filt = filt
- def filt(entry): return eval(_filt, globals(), {'entry': entry})
+ def filt(entry): return eval(_filt, globals(), {'entry': entry}) # noqa
poswidth = len(str(os.stat(fname).st_size or 1000))
diff --git a/scripts/update_version_stamp.lua b/scripts/update_version_stamp.lua
index 394c4f7694..11b521fab6 100755
--- a/scripts/update_version_stamp.lua
+++ b/scripts/update_version_stamp.lua
@@ -13,6 +13,10 @@ local function die(msg)
os.exit(0)
end
+local function iswin()
+ return package.config:sub(1,1) == '\\'
+end
+
if #arg ~= 2 then
die(string.format("Expected two args, got %d", #arg))
end
@@ -20,7 +24,8 @@ end
local versiondeffile = arg[1]
local prefix = arg[2]
-local described = io.popen('git describe --first-parent --dirty 2>/dev/null'):read('*l')
+local dev_null = iswin() and 'NUL' or '/dev/null'
+local described = io.popen('git describe --first-parent --dirty 2>'..dev_null):read('*l')
if not described then
described = io.popen('git describe --first-parent --tags --always --dirty'):read('*l')
end
diff --git a/scripts/vim-patch.sh b/scripts/vim-patch.sh
index 2a04805606..b6a0df4649 100755
--- a/scripts/vim-patch.sh
+++ b/scripts/vim-patch.sh
@@ -501,13 +501,15 @@ show_vimpatches() {
fi
done
- printf "\nInstructions:
+ cat << EOF
+
+Instructions:
To port one of the above patches to Neovim, execute this script with the patch revision as argument and follow the instructions, e.g.
- '%s -p v8.0.1234', or '%s -P v8.0.1234'
+ '${BASENAME} -p v8.0.1234', or '${BASENAME} -P v8.0.1234'
NOTE: Please port the _oldest_ patch if you possibly can.
- You can use '%s -l path/to/file' to see what patches are missing for a file.
-" "${BASENAME}" "${BASENAME}"
+ You can use '${BASENAME} -l path/to/file' to see what patches are missing for a file.
+EOF
}
review_commit() {
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c
index 3e1209d1b1..a5f8b0974e 100644
--- a/src/nvim/api/buffer.c
+++ b/src/nvim/api/buffer.c
@@ -23,7 +23,10 @@
#include "nvim/memory.h"
#include "nvim/misc1.h"
#include "nvim/ex_cmds.h"
+#include "nvim/map_defs.h"
+#include "nvim/map.h"
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/fileio.h"
#include "nvim/move.h"
#include "nvim/syntax.h"
@@ -101,25 +104,47 @@ String buffer_get_line(Buffer buffer, Integer index, Error *err)
return rv;
}
-/// Activates buffer-update events on a channel, or as lua callbacks.
+/// Activates buffer-update events on a channel, or as Lua callbacks.
+///
+/// Example (Lua): capture buffer updates in a global `events` variable
+/// (use "print(vim.inspect(events))" to see its contents):
+/// <pre>
+/// events = {}
+/// vim.api.nvim_buf_attach(0, false, {
+/// on_lines=function(...) table.insert(events, {...}) end})
+/// </pre>
+///
+/// @see |nvim_buf_detach()|
+/// @see |api-buffer-updates-lua|
///
/// @param channel_id
/// @param buffer Buffer handle, or 0 for current buffer
-/// @param send_buffer Set to true if the initial notification should contain
-/// the whole buffer. If so, the first notification will be a
-/// `nvim_buf_lines_event`. Otherwise, the first notification will be
-/// a `nvim_buf_changedtick_event`. Not used for lua callbacks.
+/// @param send_buffer True if the initial notification should contain the
+/// whole buffer: first notification will be `nvim_buf_lines_event`.
+/// Else the first notification will be `nvim_buf_changedtick_event`.
+/// Not for Lua callbacks.
/// @param opts Optional parameters.
-/// - `on_lines`: lua callback received on change.
-/// - `on_changedtick`: lua callback received on changedtick
-/// increment without text change.
-/// - `utf_sizes`: include UTF-32 and UTF-16 size of
-/// the replaced region.
-/// See |api-buffer-updates-lua| for more information
+/// - on_lines: Lua callback invoked on change.
+/// Return `true` to detach. Args:
+/// - buffer handle
+/// - b:changedtick
+/// - first line that changed (zero-indexed)
+/// - last line that was changed
+/// - last line in the updated range
+/// - byte count of previous contents
+/// - deleted_codepoints (if `utf_sizes` is true)
+/// - deleted_codeunits (if `utf_sizes` is true)
+/// - on_changedtick: Lua callback invoked on changedtick
+/// increment without text change. Args:
+/// - buffer handle
+/// - b:changedtick
+/// - on_detach: Lua callback invoked on detach. Args:
+/// - buffer handle
+/// - utf_sizes: include UTF-32 and UTF-16 size of the replaced
+/// region, as args to `on_lines`.
/// @param[out] err Error details, if any
-/// @return False when updates couldn't be enabled because the buffer isn't
-/// loaded or `opts` contained an invalid key; otherwise True.
-/// TODO: LUA_API_NO_EVAL
+/// @return False if attach failed (invalid parameter, or buffer isn't loaded);
+/// otherwise True. TODO: LUA_API_NO_EVAL
Boolean nvim_buf_attach(uint64_t channel_id,
Buffer buffer,
Boolean send_buffer,
@@ -183,13 +208,14 @@ error:
/// Deactivates buffer-update events on the channel.
///
-/// For Lua callbacks see |api-lua-detach|.
+/// @see |nvim_buf_attach()|
+/// @see |api-lua-detach| for detaching Lua callbacks
///
/// @param channel_id
/// @param buffer Buffer handle, or 0 for current buffer
/// @param[out] err Error details, if any
-/// @return False when updates couldn't be disabled because the buffer
-/// isn't loaded; otherwise True.
+/// @return False if detach failed (because the buffer isn't loaded);
+/// otherwise True.
Boolean nvim_buf_detach(uint64_t channel_id,
Buffer buffer,
Error *err)
@@ -529,7 +555,8 @@ void nvim_buf_set_lines(uint64_t channel_id,
(linenr_T)(end - 1),
MAXLNUM,
(long)extra,
- false);
+ false,
+ kExtmarkUndo);
changed_lines((linenr_T)start, 0, (linenr_T)end, (long)extra, true);
fix_cursor((linenr_T)start, (linenr_T)end, (linenr_T)extra);
@@ -984,6 +1011,238 @@ ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buffer, String name, Error *err)
return rv;
}
+/// Returns position for a given extmark id
+///
+/// @param buffer The buffer handle
+/// @param namespace a identifier returned previously with nvim_create_namespace
+/// @param id the extmark id
+/// @param[out] err Details of an error that may have occurred
+/// @return (row, col) tuple or empty list () if extmark id was absent
+ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id,
+ Integer id, Error *err)
+ FUNC_API_SINCE(7)
+{
+ Array rv = ARRAY_DICT_INIT;
+
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+
+ if (!buf) {
+ return rv;
+ }
+
+ if (!ns_initialized((uint64_t)ns_id)) {
+ api_set_error(err, kErrorTypeValidation, _("Invalid ns_id"));
+ return rv;
+ }
+
+ Extmark *extmark = extmark_from_id(buf, (uint64_t)ns_id, (uint64_t)id);
+ if (!extmark) {
+ return rv;
+ }
+ ADD(rv, INTEGER_OBJ((Integer)extmark->line->lnum-1));
+ ADD(rv, INTEGER_OBJ((Integer)extmark->col-1));
+ return rv;
+}
+
+/// List extmarks in a range (inclusive)
+///
+/// range ends can be specified as (row, col) tuples, as well as extmark
+/// ids in the same namespace. In addition, 0 and -1 works as shorthands
+/// for (0,0) and (-1,-1) respectively, so that all marks in the buffer can be
+/// queried as:
+///
+/// all_marks = nvim_buf_get_extmarks(0, my_ns, 0, -1, {})
+///
+/// If end is a lower position than start, then the range will be traversed
+/// backwards. This is mostly useful with limited amount, to be able to get the
+/// first marks prior to a given position.
+///
+/// @param buffer The buffer handle
+/// @param ns_id An id returned previously from nvim_create_namespace
+/// @param start One of: extmark id, (row, col) or 0, -1 for buffer ends
+/// @param end One of: extmark id, (row, col) or 0, -1 for buffer ends
+/// @param opts additional options. Supports the keys:
+/// - amount: Maximum number of marks to return
+/// @param[out] err Details of an error that may have occurred
+/// @return [[extmark_id, row, col], ...]
+Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id,
+ Object start, Object end, Dictionary opts,
+ Error *err)
+ FUNC_API_SINCE(7)
+{
+ Array rv = ARRAY_DICT_INIT;
+
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+ if (!buf) {
+ return rv;
+ }
+
+ if (!ns_initialized((uint64_t)ns_id)) {
+ api_set_error(err, kErrorTypeValidation, _("Invalid ns_id"));
+ return rv;
+ }
+ Integer amount = -1;
+
+ for (size_t i = 0; i < opts.size; i++) {
+ String k = opts.items[i].key;
+ Object *v = &opts.items[i].value;
+ if (strequal("amount", k.data)) {
+ if (v->type != kObjectTypeInteger) {
+ api_set_error(err, kErrorTypeValidation, "amount is not an integer");
+ return rv;
+ }
+ amount = v->data.integer;
+ v->data.integer = LUA_NOREF;
+ } else {
+ api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
+ return rv;
+ }
+ }
+
+ if (amount == 0) {
+ return rv;
+ }
+
+
+ bool reverse = false;
+
+ linenr_T l_lnum;
+ colnr_T l_col;
+ if (!set_extmark_index_from_obj(buf, ns_id, start, &l_lnum, &l_col, err)) {
+ return rv;
+ }
+
+ linenr_T u_lnum;
+ colnr_T u_col;
+ if (!set_extmark_index_from_obj(buf, ns_id, end, &u_lnum, &u_col, err)) {
+ return rv;
+ }
+
+ if (l_lnum > u_lnum || (l_lnum == u_lnum && l_col > u_col)) {
+ reverse = true;
+ linenr_T tmp_lnum = l_lnum;
+ l_lnum = u_lnum;
+ u_lnum = tmp_lnum;
+ colnr_T tmp_col = l_col;
+ l_col = u_col;
+ u_col = tmp_col;
+ }
+
+
+ ExtmarkArray marks = extmark_get(buf, (uint64_t)ns_id, l_lnum, l_col,
+ u_lnum, u_col, (int64_t)amount,
+ reverse);
+
+ for (size_t i = 0; i < kv_size(marks); i++) {
+ Array mark = ARRAY_DICT_INIT;
+ Extmark *extmark = kv_A(marks, i);
+ ADD(mark, INTEGER_OBJ((Integer)extmark->mark_id));
+ ADD(mark, INTEGER_OBJ(extmark->line->lnum-1));
+ ADD(mark, INTEGER_OBJ(extmark->col-1));
+ ADD(rv, ARRAY_OBJ(mark));
+ }
+
+ kv_destroy(marks);
+ return rv;
+}
+
+/// Create or update an extmark at a position
+///
+/// If an invalid namespace is given, an error will be raised.
+///
+/// To create a new extmark, pass in id=0. The new extmark id will be
+/// returned. To move an existing mark, pass in its id.
+///
+/// It is also allowed to create a new mark by passing in a previously unused
+/// id, but the caller must then keep track of existing and unused ids itself.
+/// This is mainly useful over RPC, to avoid needing to wait for the return
+/// value.
+///
+/// @param buffer The buffer handle
+/// @param ns_id a identifier returned previously with nvim_create_namespace
+/// @param id The extmark's id or 0 to create a new mark.
+/// @param line The row to set the extmark to.
+/// @param col The column to set the extmark to.
+/// @param opts Optional parameters. Currently not used.
+/// @param[out] err Details of an error that may have occurred
+/// @return the id of the extmark.
+Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer id,
+ Integer line, Integer col,
+ Dictionary opts, Error *err)
+ FUNC_API_SINCE(7)
+{
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+ if (!buf) {
+ return 0;
+ }
+
+ if (!ns_initialized((uint64_t)ns_id)) {
+ api_set_error(err, kErrorTypeValidation, _("Invalid ns_id"));
+ return 0;
+ }
+
+ if (opts.size > 0) {
+ api_set_error(err, kErrorTypeValidation, "opts dict isn't empty");
+ return 0;
+ }
+
+ size_t len = 0;
+ if (line < 0 || line > buf->b_ml.ml_line_count) {
+ api_set_error(err, kErrorTypeValidation, "line value outside range");
+ return 0;
+ } else if (line < buf->b_ml.ml_line_count) {
+ len = STRLEN(ml_get_buf(curbuf, (linenr_T)line+1, false));
+ }
+
+ if (col == -1) {
+ col = (Integer)len;
+ } else if (col < -1 || col > (Integer)len) {
+ api_set_error(err, kErrorTypeValidation, "col value outside range");
+ return 0;
+ }
+
+ uint64_t id_num;
+ if (id == 0) {
+ id_num = extmark_free_id_get(buf, (uint64_t)ns_id);
+ } else if (id > 0) {
+ id_num = (uint64_t)id;
+ } else {
+ api_set_error(err, kErrorTypeValidation, _("Invalid mark id"));
+ return 0;
+ }
+
+ extmark_set(buf, (uint64_t)ns_id, id_num,
+ (linenr_T)line+1, (colnr_T)col+1, kExtmarkUndo);
+
+ return (Integer)id_num;
+}
+
+/// Remove an extmark
+///
+/// @param buffer The buffer handle
+/// @param ns_id a identifier returned previously with nvim_create_namespace
+/// @param id The extmarks's id
+/// @param[out] err Details of an error that may have occurred
+/// @return true on success, false if the extmark was not found.
+Boolean nvim_buf_del_extmark(Buffer buffer,
+ Integer ns_id,
+ Integer id,
+ Error *err)
+ FUNC_API_SINCE(7)
+{
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+
+ if (!buf) {
+ return false;
+ }
+ if (!ns_initialized((uint64_t)ns_id)) {
+ api_set_error(err, kErrorTypeValidation, _("Invalid ns_id"));
+ return false;
+ }
+
+ return extmark_del(buf, (uint64_t)ns_id, (uint64_t)id, kExtmarkUndo);
+}
+
/// Adds a highlight to buffer.
///
/// Useful for plugins that dynamically generate highlights to a buffer
@@ -1082,6 +1341,10 @@ void nvim_buf_clear_namespace(Buffer buffer,
}
bufhl_clear_line_range(buf, (int)ns_id, (int)line_start+1, (int)line_end);
+ extmark_clear(buf, ns_id == -1 ? 0 : (uint64_t)ns_id,
+ (linenr_T)line_start+1,
+ (linenr_T)line_end,
+ kExtmarkUndo);
}
/// Clears highlights and virtual text from namespace and range of lines
@@ -1192,6 +1455,56 @@ free_exit:
return 0;
}
+/// Get the virtual text (annotation) for a buffer line.
+///
+/// The virtual text is returned as list of lists, whereas the inner lists have
+/// either one or two elements. The first element is the actual text, the
+/// optional second element is the highlight group.
+///
+/// The format is exactly the same as given to nvim_buf_set_virtual_text().
+///
+/// If there is no virtual text associated with the given line, an empty list
+/// is returned.
+///
+/// @param buffer Buffer handle, or 0 for current buffer
+/// @param line Line to get the virtual text from (zero-indexed)
+/// @param[out] err Error details, if any
+/// @return List of virtual text chunks
+Array nvim_buf_get_virtual_text(Buffer buffer, Integer lnum, Error *err)
+ FUNC_API_SINCE(7)
+{
+ Array chunks = ARRAY_DICT_INIT;
+
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+ if (!buf) {
+ return chunks;
+ }
+
+ if (lnum < 0 || lnum >= MAXLNUM) {
+ api_set_error(err, kErrorTypeValidation, "Line number outside range");
+ return chunks;
+ }
+
+ BufhlLine *lineinfo = bufhl_tree_ref(&buf->b_bufhl_info, (linenr_T)(lnum + 1),
+ false);
+ if (!lineinfo) {
+ return chunks;
+ }
+
+ for (size_t i = 0; i < lineinfo->virt_text.size; i++) {
+ Array chunk = ARRAY_DICT_INIT;
+ VirtTextChunk *vtc = &lineinfo->virt_text.items[i];
+ ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text)));
+ if (vtc->hl_id > 0) {
+ ADD(chunk, STRING_OBJ(cstr_to_string(
+ (const char *)syn_id2name(vtc->hl_id))));
+ }
+ ADD(chunks, ARRAY_OBJ(chunk));
+ }
+
+ return chunks;
+}
+
Dictionary nvim__buf_stats(Buffer buffer, Error *err)
{
Dictionary rv = ARRAY_DICT_INIT;
diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c
index 2056cb07e3..fbfdb27827 100644
--- a/src/nvim/api/private/helpers.c
+++ b/src/nvim/api/private/helpers.c
@@ -10,6 +10,7 @@
#include "nvim/api/private/helpers.h"
#include "nvim/api/private/defs.h"
#include "nvim/api/private/handle.h"
+#include "nvim/api/vim.h"
#include "nvim/msgpack_rpc/helpers.h"
#include "nvim/lua/executor.h"
#include "nvim/ascii.h"
@@ -23,6 +24,7 @@
#include "nvim/eval/typval.h"
#include "nvim/map_defs.h"
#include "nvim/map.h"
+#include "nvim/mark_extended.h"
#include "nvim/option.h"
#include "nvim/option_defs.h"
#include "nvim/version.h"
@@ -1505,3 +1507,127 @@ ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf)
return mappings;
}
+
+// Returns an extmark given an id or a positional index
+// If throw == true then an error will be raised if nothing
+// was found
+// Returns NULL if something went wrong
+Extmark *extmark_from_id_or_pos(Buffer buffer, Integer namespace, Object id,
+ Error *err, bool throw)
+{
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+
+ if (!buf) {
+ return NULL;
+ }
+
+ Extmark *extmark = NULL;
+ if (id.type == kObjectTypeArray) {
+ if (id.data.array.size != 2) {
+ api_set_error(err, kErrorTypeValidation,
+ _("Position must have 2 elements"));
+ return NULL;
+ }
+ linenr_T row = (linenr_T)id.data.array.items[0].data.integer;
+ colnr_T col = (colnr_T)id.data.array.items[1].data.integer;
+ if (row < 1 || col < 1) {
+ if (throw) {
+ api_set_error(err, kErrorTypeValidation, _("Row and column MUST be > 0"));
+ }
+ return NULL;
+ }
+ extmark = extmark_from_pos(buf, (uint64_t)namespace, row, col);
+ } else if (id.type != kObjectTypeInteger) {
+ if (throw) {
+ api_set_error(err, kErrorTypeValidation,
+ _("Mark id must be an int or [row, col]"));
+ }
+ return NULL;
+ } else if (id.data.integer < 0) {
+ if (throw) {
+ api_set_error(err, kErrorTypeValidation, _("Mark id must be positive"));
+ }
+ return NULL;
+ } else {
+ extmark = extmark_from_id(buf,
+ (uint64_t)namespace,
+ (uint64_t)id.data.integer);
+ }
+
+ if (!extmark) {
+ if (throw) {
+ api_set_error(err, kErrorTypeValidation, _("Mark doesn't exist"));
+ }
+ return NULL;
+ }
+ return extmark;
+}
+
+// Is the Namespace in use?
+bool ns_initialized(uint64_t ns)
+{
+ if (ns < 1) {
+ return false;
+ }
+ return ns < (uint64_t)next_namespace_id;
+}
+
+/// Get line and column from extmark object
+///
+/// Extmarks may be queried from position or name or even special names
+/// in the future such as "cursor". This function sets the line and col
+/// to make the extmark functions recognize what's required
+///
+/// @param[out] lnum lnum to be set
+/// @param[out] colnr col to be set
+bool set_extmark_index_from_obj(buf_T *buf, Integer namespace,
+ Object obj, linenr_T *lnum, colnr_T *colnr,
+ Error *err)
+{
+ // Check if it is mark id
+ if (obj.type == kObjectTypeInteger) {
+ Integer id = obj.data.integer;
+ if (id == 0) {
+ *lnum = 1;
+ *colnr = 1;
+ return true;
+ } else if (id == -1) {
+ *lnum = MAXLNUM;
+ *colnr = MAXCOL;
+ return true;
+ } else if (id < 0) {
+ api_set_error(err, kErrorTypeValidation, _("Mark id must be positive"));
+ return false;
+ }
+
+ Extmark *extmark = extmark_from_id(buf, (uint64_t)namespace, (uint64_t)id);
+ if (extmark) {
+ *lnum = extmark->line->lnum;
+ *colnr = extmark->col;
+ return true;
+ } else {
+ api_set_error(err, kErrorTypeValidation, _("No mark with requested id"));
+ return false;
+ }
+
+ // Check if it is a position
+ } else if (obj.type == kObjectTypeArray) {
+ Array pos = obj.data.array;
+ if (pos.size != 2
+ || pos.items[0].type != kObjectTypeInteger
+ || pos.items[1].type != kObjectTypeInteger) {
+ api_set_error(err, kErrorTypeValidation,
+ _("Position must have 2 integer elements"));
+ return false;
+ }
+ Integer line = pos.items[0].data.integer;
+ Integer col = pos.items[1].data.integer;
+ *lnum = (linenr_T)(line >= 0 ? line + 1 : MAXLNUM);
+ *colnr = (colnr_T)(col >= 0 ? col + 1 : MAXCOL);
+ return true;
+ } else {
+ api_set_error(err, kErrorTypeValidation,
+ _("Position must be a mark id Integer or position Array"));
+ return false;
+ }
+}
diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h
index 0ea7667428..8930f252f6 100644
--- a/src/nvim/api/private/helpers.h
+++ b/src/nvim/api/private/helpers.h
@@ -102,6 +102,20 @@ typedef struct {
int did_emsg;
} TryState;
+// `msg_list` controls the collection of abort-causing non-exception errors,
+// which would otherwise be ignored. This pattern is from do_cmdline().
+//
+// TODO(bfredl): prepare error-handling at "top level" (nv_event).
+#define TRY_WRAP(code) \
+ do { \
+ struct msglist **saved_msg_list = msg_list; \
+ struct msglist *private_msg_list; \
+ msg_list = &private_msg_list; \
+ private_msg_list = NULL; \
+ code \
+ msg_list = saved_msg_list; /* Restore the exception context. */ \
+ } while (0)
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "api/private/helpers.h.generated.h"
#endif
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c
index 602733fd31..10f7dd1a7b 100644
--- a/src/nvim/api/vim.c
+++ b/src/nvim/api/vim.c
@@ -39,6 +39,7 @@
#include "nvim/ops.h"
#include "nvim/option.h"
#include "nvim/state.h"
+#include "nvim/mark_extended.h"
#include "nvim/syntax.h"
#include "nvim/getchar.h"
#include "nvim/os/input.h"
@@ -53,20 +54,6 @@
# include "api/vim.c.generated.h"
#endif
-// `msg_list` controls the collection of abort-causing non-exception errors,
-// which would otherwise be ignored. This pattern is from do_cmdline().
-//
-// TODO(bfredl): prepare error-handling at "top level" (nv_event).
-#define TRY_WRAP(code) \
- do { \
- struct msglist **saved_msg_list = msg_list; \
- struct msglist *private_msg_list; \
- msg_list = &private_msg_list; \
- private_msg_list = NULL; \
- code \
- msg_list = saved_msg_list; /* Restore the exception context. */ \
- } while (0)
-
void api_vim_init(void)
FUNC_API_NOEXPORT
{
@@ -1054,10 +1041,9 @@ fail:
/// @param enter Enter the window (make it the current window)
/// @param config Map defining the window configuration. Keys:
/// - `relative`: Sets the window layout to "floating", placed at (row,col)
-/// coordinates relative to one of:
+/// coordinates relative to:
/// - "editor" The global editor grid
-/// - "win" Window given by the `win` field, or current window by
-/// default.
+/// - "win" Window given by the `win` field, or current window.
/// - "cursor" Cursor position in current window.
/// - `win`: |window-ID| for relative="win".
/// - `anchor`: Decides which corner of the float to place at (row,col):
@@ -1266,7 +1252,7 @@ Boolean nvim_paste(String data, Boolean crlf, Integer phase, Error *err)
draining = true;
goto theend;
}
- if (!(State & CMDLINE) && !(State & INSERT) && (phase == -1 || phase == 1)) {
+ if (!(State & (CMDLINE | INSERT)) && (phase == -1 || phase == 1)) {
ResetRedobuff();
AppendCharToRedobuff('a'); // Dot-repeat.
}
@@ -1284,7 +1270,7 @@ Boolean nvim_paste(String data, Boolean crlf, Integer phase, Error *err)
}
}
}
- if (!(State & CMDLINE) && !(State & INSERT) && (phase == -1 || phase == 3)) {
+ if (!(State & (CMDLINE | INSERT)) && (phase == -1 || phase == 3)) {
AppendCharToRedobuff(ESC); // Dot-repeat.
}
theend:
diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua
index c223679596..a52789c795 100644
--- a/src/nvim/auevents.lua
+++ b/src/nvim/auevents.lua
@@ -21,9 +21,9 @@ return {
'BufWritePre', -- before writing a buffer
'ChanInfo', -- info was received about channel
'ChanOpen', -- channel was opened
- 'CmdLineChanged', -- command line was modified
- 'CmdLineEnter', -- after entering cmdline mode
- 'CmdLineLeave', -- before leaving cmdline mode
+ 'CmdlineChanged', -- command line was modified
+ 'CmdlineEnter', -- after entering cmdline mode
+ 'CmdlineLeave', -- before leaving cmdline mode
'CmdUndefined', -- command undefined
'CmdWinEnter', -- after entering the cmdline window
'CmdWinLeave', -- before leaving the cmdline window
diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c
index 1d5aa8ba9b..79f339b3aa 100644
--- a/src/nvim/buffer.c
+++ b/src/nvim/buffer.c
@@ -53,6 +53,7 @@
#include "nvim/indent_c.h"
#include "nvim/main.h"
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
@@ -816,6 +817,7 @@ static void free_buffer_stuff(buf_T *buf, int free_flags)
}
uc_clear(&buf->b_ucmds); // clear local user commands
buf_delete_signs(buf, (char_u *)"*"); // delete any signs
+ extmark_free_all(buf); // delete any extmarks
bufhl_clear_all(buf); // delete any highligts
map_clear_int(buf, MAP_ALL_MODES, true, false); // clear local mappings
map_clear_int(buf, MAP_ALL_MODES, true, true); // clear local abbrevs
@@ -5496,6 +5498,7 @@ void bufhl_clear_line_range(buf_T *buf,
linenr_T line_start,
linenr_T line_end)
{
+ // TODO(bfredl): implement kb_itr_interval to jump directly to the first line
kbitr_t(bufhl) itr;
BufhlLine *l, t = BUFHLLINE_INIT(line_start);
if (!kb_itr_get(bufhl, &buf->b_bufhl_info, &t, &itr)) {
diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h
index ca740dea21..700d8b82e6 100644
--- a/src/nvim/buffer_defs.h
+++ b/src/nvim/buffer_defs.h
@@ -115,6 +115,9 @@ typedef uint16_t disptick_T; // display tick type
#include "nvim/os/fs_defs.h" // for FileID
#include "nvim/terminal.h" // for Terminal
+#include "nvim/lib/kbtree.h"
+#include "nvim/mark_extended.h"
+
/*
* The taggy struct is used to store the information about a :tag command.
*/
@@ -805,6 +808,10 @@ struct file_buffer {
kvec_t(BufhlLine *) b_bufhl_move_space; // temporary space for highlights
+ PMap(uint64_t) *b_extmark_ns; // extmark namespaces
+ kbtree_t(extmarklines) b_extlines; // extmarks
+ kvec_t(ExtmarkLine *) b_extmark_move_space; // temp space for extmarks
+
// array of channel_id:s which have asked to receive updates for this
// buffer.
kvec_t(uint64_t) update_channels;
@@ -911,19 +918,19 @@ typedef struct w_line {
* or row (FR_ROW) layout or is a leaf, which has a window.
*/
struct frame_S {
- char fr_layout; /* FR_LEAF, FR_COL or FR_ROW */
+ char fr_layout; // FR_LEAF, FR_COL or FR_ROW
int fr_width;
- int fr_newwidth; /* new width used in win_equal_rec() */
+ int fr_newwidth; // new width used in win_equal_rec()
int fr_height;
- int fr_newheight; /* new height used in win_equal_rec() */
- frame_T *fr_parent; /* containing frame or NULL */
- frame_T *fr_next; /* frame right or below in same parent, NULL
- for first */
- frame_T *fr_prev; /* frame left or above in same parent, NULL
- for last */
- /* fr_child and fr_win are mutually exclusive */
- frame_T *fr_child; /* first contained frame */
- win_T *fr_win; /* window that fills this frame */
+ int fr_newheight; // new height used in win_equal_rec()
+ frame_T *fr_parent; // containing frame or NULL
+ frame_T *fr_next; // frame right or below in same parent, NULL
+ // for last
+ frame_T *fr_prev; // frame left or above in same parent, NULL
+ // for first
+ // fr_child and fr_win are mutually exclusive
+ frame_T *fr_child; // first contained frame
+ win_T *fr_win; // window that fills this frame
};
#define FR_LEAF 0 /* frame is a leaf */
diff --git a/src/nvim/change.c b/src/nvim/change.c
index ba80e71ae6..7558055696 100644
--- a/src/nvim/change.c
+++ b/src/nvim/change.c
@@ -17,6 +17,7 @@
#include "nvim/indent.h"
#include "nvim/indent_c.h"
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/memline.h"
#include "nvim/misc1.h"
#include "nvim/move.h"
@@ -372,7 +373,7 @@ void appended_lines_mark(linenr_T lnum, long count)
// Skip mark_adjust when adding a line after the last one, there can't
// be marks there. But it's still needed in diff mode.
if (lnum + count < curbuf->b_ml.ml_line_count || curwin->w_p_diff) {
- mark_adjust(lnum + 1, (linenr_T)MAXLNUM, count, 0L, false);
+ mark_adjust(lnum + 1, (linenr_T)MAXLNUM, count, 0L, false, kExtmarkUndo);
}
changed_lines(lnum + 1, 0, lnum + 1, count, true);
}
@@ -390,7 +391,8 @@ void deleted_lines(linenr_T lnum, long count)
/// be triggered to display the cursor.
void deleted_lines_mark(linenr_T lnum, long count)
{
- mark_adjust(lnum, (linenr_T)(lnum + count - 1), (long)MAXLNUM, -count, false);
+ mark_adjust(lnum, (linenr_T)(lnum + count - 1), (long)MAXLNUM, -count, false,
+ kExtmarkUndo);
changed_lines(lnum, 0, lnum + count, -count, true);
}
@@ -951,6 +953,9 @@ int open_line(
bool did_append; // appended a new line
int saved_pi = curbuf->b_p_pi; // copy of preserveindent setting
+ linenr_T lnum = curwin->w_cursor.lnum;
+ colnr_T mincol = curwin->w_cursor.col + 1;
+
// make a copy of the current line so we can mess with it
char_u *saved_line = vim_strsave(get_cursor_line_ptr());
@@ -1574,7 +1579,8 @@ int open_line(
// be marks there. But still needed in diff mode.
if (curwin->w_cursor.lnum + 1 < curbuf->b_ml.ml_line_count
|| curwin->w_p_diff) {
- mark_adjust(curwin->w_cursor.lnum + 1, (linenr_T)MAXLNUM, 1L, 0L, false);
+ mark_adjust(curwin->w_cursor.lnum + 1, (linenr_T)MAXLNUM, 1L, 0L, false,
+ kExtmarkUndo);
}
did_append = true;
} else {
@@ -1663,8 +1669,12 @@ int open_line(
if (flags & OPENLINE_MARKFIX) {
mark_col_adjust(curwin->w_cursor.lnum,
curwin->w_cursor.col + less_cols_off,
- 1L, (long)-less_cols, 0);
+ 1L, (long)-less_cols, 0, kExtmarkNOOP);
}
+ // Always move extmarks - Here we move only the line where the
+ // cursor is, the previous mark_adjust takes care of the lines after
+ extmark_col_adjust(curbuf, lnum, mincol, 1L, (long)-less_cols,
+ kExtmarkUndo);
} else {
changed_bytes(curwin->w_cursor.lnum, curwin->w_cursor.col);
}
diff --git a/src/nvim/diff.c b/src/nvim/diff.c
index 31552929dc..dccde01d29 100644
--- a/src/nvim/diff.c
+++ b/src/nvim/diff.c
@@ -44,7 +44,7 @@
#include "nvim/os/shell.h"
static int diff_busy = false; // using diff structs, don't change them
-static int diff_need_update = false; // ex_diffupdate needs to be called
+static bool diff_need_update = false; // ex_diffupdate needs to be called
// Flags obtained from the 'diffopt' option
#define DIFF_FILLER 0x001 // display filler lines
@@ -57,8 +57,9 @@ static int diff_need_update = false; // ex_diffupdate needs to be called
#define DIFF_VERTICAL 0x080 // vertical splits
#define DIFF_HIDDEN_OFF 0x100 // diffoff when hidden
#define DIFF_INTERNAL 0x200 // use internal xdiff algorithm
+#define DIFF_CLOSE_OFF 0x400 // diffoff when closing window
#define ALL_WHITE_DIFF (DIFF_IWHITE | DIFF_IWHITEALL | DIFF_IWHITEEOL)
-static int diff_flags = DIFF_INTERNAL | DIFF_FILLER;
+static int diff_flags = DIFF_INTERNAL | DIFF_FILLER | DIFF_CLOSE_OFF;
static long diff_algorithm = 0;
@@ -490,7 +491,8 @@ static void diff_mark_adjust_tp(tabpage_T *tp, int idx, linenr_T line1,
}
if (tp == curtab) {
- diff_redraw(true);
+ // Don't redraw right away, this updates the diffs, which can be slow.
+ need_diff_redraw = true;
// Need to recompute the scroll binding, may remove or add filler
// lines (e.g., when adding lines above w_topline). But it's slow when
@@ -634,8 +636,9 @@ static int diff_check_sanity(tabpage_T *tp, diff_T *dp)
/// Mark all diff buffers in the current tab page for redraw.
///
/// @param dofold Also recompute the folds
-static void diff_redraw(int dofold)
+void diff_redraw(bool dofold)
{
+ need_diff_redraw = false;
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
if (!wp->w_p_diff) {
continue;
@@ -1472,6 +1475,13 @@ void ex_diffoff(exarg_T *eap)
diff_buf_clear();
}
+ if (!diffwin) {
+ diff_need_update = false;
+ curtab->tp_diff_invalid = false;
+ curtab->tp_diff_update = false;
+ diff_clear(curtab);
+ }
+
// Remove "hor" from from 'scrollopt' if there are no diff windows left.
if (!diffwin && (vim_strchr(p_sbo, 'h') != NULL)) {
do_cmdline_cmd("set sbo-=hor");
@@ -1712,6 +1722,7 @@ static void diff_copy_entry(diff_T *dprev, diff_T *dp, int idx_orig,
///
/// @param tp
void diff_clear(tabpage_T *tp)
+ FUNC_ATTR_NONNULL_ALL
{
diff_T *p;
diff_T *next_p;
@@ -2141,6 +2152,9 @@ int diffopt_changed(void)
} else if (STRNCMP(p, "hiddenoff", 9) == 0) {
p += 9;
diff_flags_new |= DIFF_HIDDEN_OFF;
+ } else if (STRNCMP(p, "closeoff", 8) == 0) {
+ p += 8;
+ diff_flags_new |= DIFF_CLOSE_OFF;
} else if (STRNCMP(p, "indent-heuristic", 16) == 0) {
p += 16;
diff_indent_heuristic = XDF_INDENT_HEURISTIC;
@@ -2216,6 +2230,13 @@ bool diffopt_hiddenoff(void)
return (diff_flags & DIFF_HIDDEN_OFF) != 0;
}
+// Return true if 'diffopt' contains "closeoff".
+bool diffopt_closeoff(void)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ return (diff_flags & DIFF_CLOSE_OFF) != 0;
+}
+
/// Find the difference within a changed line.
///
/// @param wp window whose current buffer to check
@@ -2690,7 +2711,8 @@ void ex_diffgetput(exarg_T *eap)
// Adjust marks. This will change the following entries!
if (added != 0) {
- mark_adjust(lnum, lnum + count - 1, (long)MAXLNUM, (long)added, false);
+ mark_adjust(lnum, lnum + count - 1, (long)MAXLNUM, (long)added, false,
+ kExtmarkUndo);
if (curwin->w_cursor.lnum >= lnum) {
// Adjust the cursor position if it's in/after the changed
// lines.
diff --git a/src/nvim/diff.h b/src/nvim/diff.h
index 3624ce29bb..99a60381bd 100644
--- a/src/nvim/diff.h
+++ b/src/nvim/diff.h
@@ -4,6 +4,13 @@
#include "nvim/pos.h"
#include "nvim/ex_cmds_defs.h"
+// Value set from 'diffopt'.
+EXTERN int diff_context INIT(= 6); // context for folds
+EXTERN int diff_foldcolumn INIT(= 2); // 'foldcolumn' for diff mode
+EXTERN bool diff_need_scrollbind INIT(= false);
+
+EXTERN bool need_diff_redraw INIT(= false); // need to call diff_redraw()
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "diff.h.generated.h"
#endif
diff --git a/src/nvim/edit.c b/src/nvim/edit.c
index 62e4f77e6e..cd0f3f4b9d 100644
--- a/src/nvim/edit.c
+++ b/src/nvim/edit.c
@@ -28,6 +28,7 @@
#include "nvim/indent.h"
#include "nvim/indent_c.h"
#include "nvim/main.h"
+#include "nvim/mark_extended.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
@@ -1837,6 +1838,13 @@ change_indent (
xfree(new_line);
}
+
+ // change_indent seems to bec called twice, this combination only triggers
+ // once for both calls
+ if (new_cursor_col - vcol != 0) {
+ extmark_col_adjust(curbuf, curwin->w_cursor.lnum, 0, 0, amount,
+ kExtmarkUndo);
+ }
}
/*
@@ -3048,7 +3056,9 @@ static void ins_compl_clear(void)
XFREE_CLEAR(compl_orig_text);
compl_enter_selects = false;
// clear v:completed_item
- set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc());
+ dict_T *const d = tv_dict_alloc();
+ d->dv_lock = VAR_FIXED;
+ set_vim_var_dict(VV_COMPLETED_ITEM, d);
}
/// Check that Insert completion is active.
@@ -4118,7 +4128,7 @@ static int ins_compl_get_exp(pos_T *ini)
compl_direction,
compl_pattern, 1L,
SEARCH_KEEP + SEARCH_NFMSG,
- RE_LAST, (linenr_T)0, NULL, NULL);
+ RE_LAST, NULL);
}
msg_silent--;
if (!compl_started || set_match_pos) {
@@ -4305,7 +4315,9 @@ static void ins_compl_delete(void)
// causes flicker, thus we can't do that.
changed_cline_bef_curs();
// clear v:completed_item
- set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc());
+ dict_T *const d = tv_dict_alloc();
+ d->dv_lock = VAR_FIXED;
+ set_vim_var_dict(VV_COMPLETED_ITEM, d);
}
// Insert the new text being completed.
@@ -4327,6 +4339,7 @@ static dict_T *ins_compl_dict_alloc(compl_T *match)
{
// { word, abbr, menu, kind, info }
dict_T *dict = tv_dict_alloc();
+ dict->dv_lock = VAR_FIXED;
tv_dict_add_str(
dict, S_LEN("word"),
(const char *)EMPTY_IF_NULL(match->cp_str));
@@ -5587,6 +5600,9 @@ insertchar (
do_digraph(buf[i-1]); /* may be the start of a digraph */
buf[i] = NUL;
ins_str(buf);
+ extmark_col_adjust(curbuf, curwin->w_cursor.lnum,
+ (colnr_T)(curwin->w_cursor.col + 1), 0,
+ (long)STRLEN(buf), kExtmarkUndo);
if (flags & INSCHAR_CTRLV) {
redo_literal(*buf);
i = 1;
@@ -5597,6 +5613,9 @@ insertchar (
} else {
int cc;
+ extmark_col_adjust(curbuf, curwin->w_cursor.lnum,
+ (colnr_T)(curwin->w_cursor.col + 1), 0,
+ 1, kExtmarkUndo);
if ((cc = utf_char2len(c)) > 1) {
char_u buf[MB_MAXBYTES + 1];
@@ -5606,10 +5625,11 @@ insertchar (
AppendCharToRedobuff(c);
} else {
ins_char(c);
- if (flags & INSCHAR_CTRLV)
+ if (flags & INSCHAR_CTRLV) {
redo_literal(c);
- else
+ } else {
AppendCharToRedobuff(c);
+ }
}
}
}
@@ -6891,8 +6911,9 @@ static void mb_replace_pop_ins(int cc)
for (i = 1; i < n; ++i)
buf[i] = replace_pop();
ins_bytes_len(buf, n);
- } else
+ } else {
ins_char(cc);
+ }
if (enc_utf8)
/* Handle composing chars. */
@@ -8002,9 +8023,9 @@ static bool ins_bs(int c, int mode, int *inserted_space_p)
Insstart_orig.col = curwin->w_cursor.col;
}
- if (State & VREPLACE_FLAG)
+ if (State & VREPLACE_FLAG) {
ins_char(' ');
- else {
+ } else {
ins_str((char_u *)" ");
if ((State & REPLACE_FLAG))
replace_push(NUL);
@@ -8482,8 +8503,17 @@ static bool ins_tab(void)
} else { // otherwise use "tabstop"
temp = (int)curbuf->b_p_ts;
}
+
temp -= get_nolist_virtcol() % temp;
+ // Move extmarks
+ extmark_col_adjust(curbuf,
+ curwin->w_cursor.lnum,
+ curwin->w_cursor.col,
+ 0,
+ temp,
+ kExtmarkUndo);
+
/*
* Insert the first space with ins_char(). It will delete one char in
* replace mode. Insert the rest with ins_str(); it will not delete any
@@ -8491,12 +8521,13 @@ static bool ins_tab(void)
*/
ins_char(' ');
while (--temp > 0) {
- if (State & VREPLACE_FLAG)
+ if (State & VREPLACE_FLAG) {
ins_char(' ');
- else {
+ } else {
ins_str((char_u *)" ");
- if (State & REPLACE_FLAG) /* no char replaced */
+ if (State & REPLACE_FLAG) { // no char replaced
replace_push(NUL);
+ }
}
}
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index 71ffb26cc2..9fe92a92cc 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -422,6 +422,7 @@ static struct vimvar {
VV(VV_TYPE_BOOL, "t_bool", VAR_NUMBER, VV_RO),
VV(VV_ECHOSPACE, "echospace", VAR_NUMBER, VV_RO),
VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO),
+ VV(VV_LUA, "lua", VAR_PARTIAL, VV_RO),
};
#undef VV
@@ -433,11 +434,14 @@ static struct vimvar {
#define vv_str vv_di.di_tv.vval.v_string
#define vv_list vv_di.di_tv.vval.v_list
#define vv_dict vv_di.di_tv.vval.v_dict
+#define vv_partial vv_di.di_tv.vval.v_partial
#define vv_tv vv_di.di_tv
/// Variable used for v:
static ScopeDictDictItem vimvars_var;
+static partial_T *vvlua_partial;
+
/// v: hashtab
#define vimvarht vimvardict.dv_hashtab
@@ -639,6 +643,13 @@ void eval_init(void)
set_vim_var_nr(VV_ECHOSPACE, sc_col - 1);
+ vimvars[VV_LUA].vv_type = VAR_PARTIAL;
+ vvlua_partial = xcalloc(1, sizeof(partial_T));
+ vimvars[VV_LUA].vv_partial = vvlua_partial;
+ // this value shouldn't be printed, but if it is, do not crash
+ vvlua_partial->pt_name = xmallocz(0);
+ vvlua_partial->pt_refcount++;
+
set_reg_var(0); // default for v:register is not 0 but '"'
}
@@ -1313,12 +1324,25 @@ int call_vim_function(
{
int doesrange;
int ret;
+ int len = (int)STRLEN(func);
+ partial_T *pt = NULL;
+
+ if (len >= 6 && !memcmp(func, "v:lua.", 6)) {
+ func += 6;
+ len = check_luafunc_name((const char *)func, false);
+ if (len == 0) {
+ ret = FAIL;
+ goto fail;
+ }
+ pt = vvlua_partial;
+ }
rettv->v_type = VAR_UNKNOWN; // tv_clear() uses this.
- ret = call_func(func, (int)STRLEN(func), rettv, argc, argv, NULL,
+ ret = call_func(func, len, rettv, argc, argv, NULL,
curwin->w_cursor.lnum, curwin->w_cursor.lnum,
- &doesrange, true, NULL, NULL);
+ &doesrange, true, pt, NULL);
+fail:
if (ret == FAIL) {
tv_clear(rettv);
}
@@ -2462,6 +2486,13 @@ static char_u *get_lval(char_u *const name, typval_T *const rettv,
}
}
+ if (lp->ll_di != NULL && tv_is_luafunc(&lp->ll_di->di_tv)
+ && len == -1 && rettv == NULL) {
+ tv_clear(&var1);
+ EMSG2(e_illvar, "v:['lua']");
+ return NULL;
+ }
+
if (lp->ll_di == NULL) {
// Can't add "v:" or "a:" variable.
if (lp->ll_dict == &vimvardict
@@ -4699,7 +4730,7 @@ eval_index(
if (evaluate) {
n1 = 0;
- if (!empty1 && rettv->v_type != VAR_DICT) {
+ if (!empty1 && rettv->v_type != VAR_DICT && !tv_is_luafunc(rettv)) {
n1 = tv_get_number(&var1);
tv_clear(&var1);
}
@@ -4823,7 +4854,7 @@ eval_index(
if (len == -1) {
tv_clear(&var1);
}
- if (item == NULL) {
+ if (item == NULL || tv_is_luafunc(&item->di_tv)) {
return FAIL;
}
@@ -6334,7 +6365,7 @@ static char_u *deref_func_name(const char *name, int *lenp,
*/
static int
get_func_tv(
- char_u *name, // name of the function
+ const char_u *name, // name of the function
int len, // length of "name"
typval_T *rettv,
char_u **arg, // argument, pointing to the '('
@@ -6590,7 +6621,15 @@ call_func(
rettv->vval.v_number = 0;
error = ERROR_UNKNOWN;
- if (!builtin_function((const char *)rfname, -1)) {
+ if (partial == vvlua_partial) {
+ if (len > 0) {
+ error = ERROR_NONE;
+ executor_call_lua((const char *)funcname, len,
+ argvars, argcount, rettv);
+ } else {
+ error = ERROR_UNKNOWN;
+ }
+ } else if (!builtin_function((const char *)rfname, -1)) {
// User defined function.
if (partial != NULL && partial->pt_func != NULL) {
fp = partial->pt_func;
@@ -6707,14 +6746,14 @@ call_func(
///
/// @param ermsg must be passed without translation (use N_() instead of _()).
/// @param name function name
-static void emsg_funcname(char *ermsg, char_u *name)
+static void emsg_funcname(char *ermsg, const char_u *name)
{
char_u *p;
if (*name == K_SPECIAL) {
p = concat_str((char_u *)"<SNR>", name + 3);
} else {
- p = name;
+ p = (char_u *)name;
}
EMSG2(_(ermsg), p);
@@ -8711,7 +8750,7 @@ static void f_getenv(typval_T *argvars, typval_T *rettv, FunPtr fptr)
if (p == NULL) {
rettv->v_type = VAR_SPECIAL;
- rettv->vval.v_number = kSpecialVarNull;
+ rettv->vval.v_special = kSpecialVarNull;
return;
}
rettv->vval.v_string = p;
@@ -14617,6 +14656,7 @@ static int search_cmn(typval_T *argvars, pos_T *match_pos, int *flagsp)
long time_limit = 0;
int options = SEARCH_KEEP;
int subpatnum;
+ searchit_arg_T sia;
const char *const pat = tv_get_string(&argvars[0]);
dir = get_search_arg(&argvars[1], flagsp); // May set p_ws.
@@ -14664,8 +14704,11 @@ static int search_cmn(typval_T *argvars, pos_T *match_pos, int *flagsp)
}
pos = save_cursor = curwin->w_cursor;
+ memset(&sia, 0, sizeof(sia));
+ sia.sa_stop_lnum = (linenr_T)lnum_stop;
+ sia.sa_tm = &tm;
subpatnum = searchit(curwin, curbuf, &pos, NULL, dir, (char_u *)pat, 1,
- options, RE_SEARCH, (linenr_T)lnum_stop, &tm, NULL);
+ options, RE_SEARCH, &sia);
if (subpatnum != FAIL) {
if (flags & SP_SUBPAT)
retval = subpatnum;
@@ -15237,8 +15280,13 @@ do_searchpair(
clearpos(&foundpos);
pat = pat3;
for (;; ) {
+ searchit_arg_T sia;
+ memset(&sia, 0, sizeof(sia));
+ sia.sa_stop_lnum = lnum_stop;
+ sia.sa_tm = &tm;
+
n = searchit(curwin, curbuf, &pos, NULL, dir, pat, 1L,
- options, RE_SEARCH, lnum_stop, &tm, NULL);
+ options, RE_SEARCH, &sia);
if (n == FAIL || (firstpos.lnum != 0 && equalpos(pos, firstpos))) {
// didn't find it or found the first match again: FAIL
break;
@@ -15660,7 +15708,7 @@ static void f_setenv(typval_T *argvars, typval_T *rettv, FunPtr fptr)
const char *name = tv_get_string_buf(&argvars[0], namebuf);
if (argvars[1].v_type == VAR_SPECIAL
- && argvars[1].vval.v_number == kSpecialVarNull) {
+ && argvars[1].vval.v_special == kSpecialVarNull) {
os_unsetenv(name);
} else {
os_setenv(name, tv_get_string_buf(&argvars[1], valbuf), 1);
@@ -20159,6 +20207,26 @@ static void check_vars(const char *name, size_t len)
}
}
+/// check if special v:lua value for calling lua functions
+static bool tv_is_luafunc(typval_T *tv)
+{
+ return tv->v_type == VAR_PARTIAL && tv->vval.v_partial == vvlua_partial;
+}
+
+/// check the function name after "v:lua."
+static int check_luafunc_name(const char *str, bool paren)
+{
+ const char *p = str;
+ while (ASCII_ISALNUM(*p) || *p == '_' || *p == '.') {
+ p++;
+ }
+ if (*p != (paren ? '(' : NUL)) {
+ return 0;
+ } else {
+ return (int)(p-str);
+ }
+}
+
/// Handle expr[expr], expr[expr:expr] subscript and .name lookup.
/// Also handle function call with Funcref variable: func(expr)
/// Can all be combined: dict.func(expr)[idx]['func'](expr)
@@ -20172,9 +20240,30 @@ handle_subscript(
{
int ret = OK;
dict_T *selfdict = NULL;
- char_u *s;
+ const char_u *s;
int len;
typval_T functv;
+ int slen = 0;
+ bool lua = false;
+
+ if (tv_is_luafunc(rettv)) {
+ if (**arg != '.') {
+ tv_clear(rettv);
+ ret = FAIL;
+ } else {
+ (*arg)++;
+
+ lua = true;
+ s = (char_u *)(*arg);
+ slen = check_luafunc_name(*arg, true);
+ if (slen == 0) {
+ tv_clear(rettv);
+ ret = FAIL;
+ }
+ (*arg) += slen;
+ }
+ }
+
while (ret == OK
&& (**arg == '['
@@ -20191,14 +20280,16 @@ handle_subscript(
// Invoke the function. Recursive!
if (functv.v_type == VAR_PARTIAL) {
pt = functv.vval.v_partial;
- s = partial_name(pt);
+ if (!lua) {
+ s = partial_name(pt);
+ }
} else {
s = functv.vval.v_string;
}
} else {
s = (char_u *)"";
}
- ret = get_func_tv(s, (int)STRLEN(s), rettv, (char_u **)arg,
+ ret = get_func_tv(s, lua ? slen : (int)STRLEN(s), rettv, (char_u **)arg,
curwin->w_cursor.lnum, curwin->w_cursor.lnum,
&len, evaluate, pt, selfdict);
@@ -21733,22 +21824,31 @@ void ex_function(exarg_T *eap)
}
// Check for ":let v =<< [trim] EOF"
+ // and ":let [a, b] =<< [trim] EOF"
arg = skipwhite(skiptowhite(p));
- arg = skipwhite(skiptowhite(arg));
- if (arg[0] == '=' && arg[1] == '<' && arg[2] =='<'
- && ((p[0] == 'l' && p[1] == 'e'
- && (!ASCII_ISALNUM(p[2])
- || (p[2] == 't' && !ASCII_ISALNUM(p[3])))))) {
- p = skipwhite(arg + 3);
- if (STRNCMP(p, "trim", 4) == 0) {
- // Ignore leading white space.
- p = skipwhite(p + 4);
- heredoc_trimmed = vim_strnsave(theline,
- (int)(skipwhite(theline) - theline));
+ if (*arg == '[') {
+ arg = vim_strchr(arg, ']');
+ }
+ if (arg != NULL) {
+ arg = skipwhite(skiptowhite(arg));
+ if (arg[0] == '='
+ && arg[1] == '<'
+ && arg[2] =='<'
+ && (p[0] == 'l'
+ && p[1] == 'e'
+ && (!ASCII_ISALNUM(p[2])
+ || (p[2] == 't' && !ASCII_ISALNUM(p[3]))))) {
+ p = skipwhite(arg + 3);
+ if (STRNCMP(p, "trim", 4) == 0) {
+ // Ignore leading white space.
+ p = skipwhite(p + 4);
+ heredoc_trimmed =
+ vim_strnsave(theline, (int)(skipwhite(theline) - theline));
+ }
+ skip_until = vim_strnsave(p, (int)(skiptowhite(p) - p));
+ do_concat = false;
+ is_heredoc = true;
}
- skip_until = vim_strnsave(p, (int)(skiptowhite(p) - p));
- do_concat = false;
- is_heredoc = true;
}
}
@@ -22021,8 +22121,19 @@ trans_function_name(
*pp = (char_u *)end;
} else if (lv.ll_tv->v_type == VAR_PARTIAL
&& lv.ll_tv->vval.v_partial != NULL) {
- name = vim_strsave(partial_name(lv.ll_tv->vval.v_partial));
- *pp = (char_u *)end;
+ if (lv.ll_tv->vval.v_partial == vvlua_partial && *end == '.') {
+ len = check_luafunc_name((const char *)end+1, true);
+ if (len == 0) {
+ EMSG2(e_invexpr2, "v:lua");
+ goto theend;
+ }
+ name = xmallocz(len);
+ memcpy(name, end+1, len);
+ *pp = (char_u *)end+1+len;
+ } else {
+ name = vim_strsave(partial_name(lv.ll_tv->vval.v_partial));
+ *pp = (char_u *)end;
+ }
if (partial != NULL) {
*partial = lv.ll_tv->vval.v_partial;
}
diff --git a/src/nvim/eval.h b/src/nvim/eval.h
index e099de831a..2aa08e2074 100644
--- a/src/nvim/eval.h
+++ b/src/nvim/eval.h
@@ -117,6 +117,7 @@ typedef enum {
VV_TYPE_BOOL,
VV_ECHOSPACE,
VV_EXITING,
+ VV_LUA,
} VimVarIndex;
/// All recognized msgpack types
diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c
index 2e8bd79c81..4725246764 100644
--- a/src/nvim/ex_cmds.c
+++ b/src/nvim/ex_cmds.c
@@ -39,6 +39,7 @@
#include "nvim/buffer_updates.h"
#include "nvim/main.h"
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/message.h"
@@ -658,10 +659,10 @@ void ex_sort(exarg_T *eap)
deleted = (long)(count - (lnum - eap->line2));
if (deleted > 0) {
mark_adjust(eap->line2 - deleted, eap->line2, (long)MAXLNUM, -deleted,
- false);
+ false, kExtmarkUndo);
msgmore(-deleted);
} else if (deleted < 0) {
- mark_adjust(eap->line2, MAXLNUM, -deleted, 0L, false);
+ mark_adjust(eap->line2, MAXLNUM, -deleted, 0L, false, kExtmarkUndo);
}
if (change_occurred || deleted != 0) {
changed_lines(eap->line1, 0, eap->line2 + 1, -deleted, true);
@@ -874,10 +875,12 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest)
* their final destination at the new text position -- webb
*/
last_line = curbuf->b_ml.ml_line_count;
- mark_adjust_nofold(line1, line2, last_line - line2, 0L, true);
+ mark_adjust_nofold(line1, line2, last_line - line2, 0L, true, kExtmarkNoUndo);
+ extmark_adjust(curbuf, line1, line2, last_line - line2, 0L, kExtmarkNoUndo,
+ true);
changed_lines(last_line - num_lines + 1, 0, last_line + 1, num_lines, false);
if (dest >= line2) {
- mark_adjust_nofold(line2 + 1, dest, -num_lines, 0L, false);
+ mark_adjust_nofold(line2 + 1, dest, -num_lines, 0L, false, kExtmarkNoUndo);
FOR_ALL_TAB_WINDOWS(tab, win) {
if (win->w_buffer == curbuf) {
foldMoveRange(&win->w_folds, line1, line2, dest);
@@ -886,7 +889,8 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest)
curbuf->b_op_start.lnum = dest - num_lines + 1;
curbuf->b_op_end.lnum = dest;
} else {
- mark_adjust_nofold(dest + 1, line1 - 1, num_lines, 0L, false);
+ mark_adjust_nofold(dest + 1, line1 - 1, num_lines, 0L, false,
+ kExtmarkNoUndo);
FOR_ALL_TAB_WINDOWS(tab, win) {
if (win->w_buffer == curbuf) {
foldMoveRange(&win->w_folds, dest + 1, line1 - 1, line2);
@@ -897,7 +901,9 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest)
}
curbuf->b_op_start.col = curbuf->b_op_end.col = 0;
mark_adjust_nofold(last_line - num_lines + 1, last_line,
- -(last_line - dest - extra), 0L, true);
+ -(last_line - dest - extra), 0L, true, kExtmarkNoUndo);
+
+ u_extmark_move(curbuf, line1, line2, last_line, dest, num_lines, extra);
changed_lines(last_line - num_lines + 1, 0, last_line + 1, -extra, false);
// send update regarding the new lines that were added
@@ -1281,12 +1287,14 @@ static void do_filter(
if (cmdmod.keepmarks || vim_strchr(p_cpo, CPO_REMMARK) == NULL) {
if (read_linecount >= linecount) {
// move all marks from old lines to new lines
- mark_adjust(line1, line2, linecount, 0L, false);
+ mark_adjust(line1, line2, linecount, 0L, false, kExtmarkUndo);
} else {
// move marks from old lines to new lines, delete marks
// that are in deleted lines
- mark_adjust(line1, line1 + read_linecount - 1, linecount, 0L, false);
- mark_adjust(line1 + read_linecount, line2, MAXLNUM, 0L, false);
+ mark_adjust(line1, line1 + read_linecount - 1, linecount, 0L, false,
+ kExtmarkUndo);
+ mark_adjust(line1 + read_linecount, line2, MAXLNUM, 0L, false,
+ kExtmarkUndo);
}
}
@@ -3214,6 +3222,189 @@ static char_u *sub_parse_flags(char_u *cmd, subflags_T *subflags,
return cmd;
}
+static void extmark_move_regmatch_single(lpos_T startpos,
+ lpos_T endpos,
+ linenr_T lnum,
+ int sublen)
+{
+ colnr_T mincol;
+ colnr_T endcol;
+ colnr_T col_amount;
+
+ mincol = startpos.col + 1;
+ endcol = endpos.col + 1;
+
+ // There are cases such as :s/^/x/ where this happens
+ // a delete is simply not required.
+ if (mincol + 1 <= endcol) {
+ extmark_col_adjust_delete(curbuf,
+ lnum, mincol + 1, endcol, kExtmarkUndo, 0);
+ }
+
+ // Insert, sublen seems to be the value we need but + 1...
+ col_amount = sublen - 1;
+ extmark_col_adjust(curbuf, lnum, mincol, 0, col_amount, kExtmarkUndo);
+}
+
+static void extmark_move_regmatch_multi(ExtmarkSubMulti s, long i)
+{
+ colnr_T mincol;
+ linenr_T u_lnum;
+ mincol = s.startpos.col + 1;
+
+ linenr_T n_u_lnum = s.lnum + s.endpos.lnum - s.startpos.lnum;
+ colnr_T n_after_newline_in_pat = s.endpos.col;
+ colnr_T n_before_newline_in_pat = mincol - s.cm_start.col;
+ long n_after_newline_in_sub;
+ if (!s.newline_in_sub) {
+ n_after_newline_in_sub = s.cm_end.col - s.cm_start.col;
+ } else {
+ n_after_newline_in_sub = s.cm_end.col;
+ }
+
+ if (s.newline_in_pat && !s.newline_in_sub) {
+ // -- Delete Pattern --
+ // 1. Move marks in the pattern
+ mincol = s.startpos.col + 1;
+ u_lnum = n_u_lnum;
+ assert(n_u_lnum == u_lnum);
+ extmark_copy_and_place(curbuf,
+ s.lnum, mincol,
+ u_lnum, n_after_newline_in_pat,
+ s.lnum, mincol,
+ kExtmarkUndo, true, NULL);
+ // 2. Move marks on last newline
+ mincol = mincol - n_before_newline_in_pat;
+ extmark_col_adjust(curbuf,
+ u_lnum,
+ n_after_newline_in_pat + 1,
+ -s.newline_in_pat,
+ mincol - n_after_newline_in_pat,
+ kExtmarkUndo);
+ // Take care of the lines after
+ extmark_adjust(curbuf,
+ u_lnum,
+ u_lnum,
+ MAXLNUM,
+ -s.newline_in_pat,
+ kExtmarkUndo,
+ false);
+ // 1. first insert the text in the substitutaion
+ extmark_col_adjust(curbuf,
+ s.lnum,
+ mincol + 1,
+ s.newline_in_sub,
+ n_after_newline_in_sub,
+ kExtmarkUndo);
+
+ } else {
+ // The data in sub_obj is as if the substituons above had already taken
+ // place. For our extmarks they haven't as we work from the bottom of the
+ // buffer up. Readjust the data.
+ n_u_lnum = s.lnum + s.endpos.lnum - s.startpos.lnum;
+ n_u_lnum = n_u_lnum - s.lnum_added;
+
+ // adjusted = L - (i-1)N
+ // where L = lnum value, N= lnum_added and i = iteration
+ linenr_T a_l_lnum = s.cm_start.lnum - ((i -1) * s.lnum_added);
+ linenr_T a_u_lnum = a_l_lnum + s.endpos.lnum;
+ assert(s.startpos.lnum == 0);
+
+ mincol = s.startpos.col + 1;
+ u_lnum = n_u_lnum;
+
+ if (!s.newline_in_pat && s.newline_in_sub) {
+ // -- Delete Pattern --
+ // 1. Move marks in the pattern
+ extmark_col_adjust_delete(curbuf,
+ a_l_lnum,
+ mincol + 1,
+ s.endpos.col + 1,
+ kExtmarkUndo,
+ s.eol);
+
+ extmark_adjust(curbuf,
+ a_u_lnum + 1,
+ MAXLNUM,
+ (long)s.newline_in_sub,
+ 0,
+ kExtmarkUndo,
+ false);
+ // 3. Insert
+ extmark_col_adjust(curbuf,
+ a_l_lnum,
+ mincol,
+ s.newline_in_sub,
+ (long)-mincol + 1 + n_after_newline_in_sub,
+ kExtmarkUndo);
+ } else if (s.newline_in_pat && s.newline_in_sub) {
+ if (s.lnum_added >= 0) {
+ linenr_T u_col = n_after_newline_in_pat == 0
+ ? 1 : n_after_newline_in_pat;
+ extmark_copy_and_place(curbuf,
+ a_l_lnum, mincol,
+ a_u_lnum, u_col,
+ a_l_lnum, mincol,
+ kExtmarkUndo, true, NULL);
+ // 2. Move marks on last newline
+ mincol = mincol - (colnr_T)n_before_newline_in_pat;
+ extmark_col_adjust(curbuf,
+ a_u_lnum,
+ (colnr_T)(n_after_newline_in_pat + 1),
+ -s.newline_in_pat,
+ mincol - n_after_newline_in_pat,
+ kExtmarkUndo);
+ // TODO(timeyyy): nothing to do here if lnum_added = 0
+ extmark_adjust(curbuf,
+ a_u_lnum + 1,
+ MAXLNUM,
+ (long)s.lnum_added,
+ 0,
+ kExtmarkUndo,
+ false);
+
+ extmark_col_adjust(curbuf,
+ a_l_lnum,
+ mincol + 1,
+ s.newline_in_sub,
+ (long)-mincol + n_after_newline_in_sub,
+ kExtmarkUndo);
+ } else {
+ mincol = s.startpos.col + 1;
+ a_l_lnum = s.startpos.lnum + 1;
+ a_u_lnum = s.endpos.lnum + 1;
+ extmark_copy_and_place(curbuf,
+ a_l_lnum, mincol,
+ a_u_lnum, n_after_newline_in_pat,
+ a_l_lnum, mincol,
+ kExtmarkUndo, true, NULL);
+ // 2. Move marks on last newline
+ mincol = mincol - (colnr_T)n_before_newline_in_pat;
+ extmark_col_adjust(curbuf,
+ a_u_lnum,
+ (colnr_T)(n_after_newline_in_pat + 1),
+ -s.newline_in_pat,
+ mincol - n_after_newline_in_pat,
+ kExtmarkUndo);
+ extmark_adjust(curbuf,
+ a_u_lnum,
+ a_u_lnum,
+ MAXLNUM,
+ s.lnum_added,
+ kExtmarkUndo,
+ false);
+ // 3. Insert
+ extmark_col_adjust(curbuf,
+ a_l_lnum,
+ mincol + 1,
+ s.newline_in_sub,
+ (long)-mincol + n_after_newline_in_sub,
+ kExtmarkUndo);
+ }
+ }
+ }
+}
+
/// Perform a substitution from line eap->line1 to line eap->line2 using the
/// command pointed to by eap->arg which should be of the form:
///
@@ -3260,6 +3451,17 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
int save_ma = 0;
int save_b_changed = curbuf->b_changed;
bool preview = (State & CMDPREVIEW);
+ extmark_sub_multi_vec_t extmark_sub_multi = KV_INITIAL_VALUE;
+ extmark_sub_single_vec_t extmark_sub_single = KV_INITIAL_VALUE;
+ linenr_T no_of_lines_changed = 0;
+ linenr_T newline_in_pat = 0;
+ linenr_T newline_in_sub = 0;
+
+ // inccommand tests fail without this check
+ if (!preview) {
+ // Required for Undo to work for extmarks.
+ u_save_cursor();
+ }
if (!global_busy) {
sub_nsubs = 0;
@@ -3418,6 +3620,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
// Check for a match on each line.
// If preview: limit to max('cmdwinheight', viewport).
linenr_T line2 = eap->line2;
+
for (linenr_T lnum = eap->line1;
lnum <= line2 && !got_quit && !aborting()
&& (!preview || preview_lines.lines_needed <= (linenr_T)p_cwh
@@ -3524,6 +3727,11 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
// Note: If not first match on a line, column can't be known here
current_match.start.lnum = sub_firstlnum;
+ // Match might be after the last line for "\n\zs" matching at
+ // the end of the last line.
+ if (lnum > curbuf->b_ml.ml_line_count) {
+ break;
+ }
if (sub_firstline == NULL) {
sub_firstline = vim_strsave(ml_get(sub_firstlnum));
}
@@ -3871,6 +4079,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
ADJUST_SUB_FIRSTLNUM();
+
// Now the trick is to replace CTRL-M chars with a real line
// break. This would make it impossible to insert a CTRL-M in
// the text. The line break can be avoided by preceding the
@@ -3885,7 +4094,9 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
*p1 = NUL; // truncate up to the CR
ml_append(lnum - 1, new_start,
(colnr_T)(p1 - new_start + 1), false);
- mark_adjust(lnum + 1, (linenr_T)MAXLNUM, 1L, 0L, false);
+ mark_adjust(lnum + 1, (linenr_T)MAXLNUM, 1L, 0L, false,
+ kExtmarkNOOP);
+
if (subflags.do_ask) {
appended_lines(lnum - 1, 1L);
} else {
@@ -3912,6 +4123,44 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
current_match.end.lnum = lnum;
}
+ // Adjust extmarks, by delete and then insert
+ if (!preview) {
+ newline_in_pat = (regmatch.endpos[0].lnum
+ - regmatch.startpos[0].lnum);
+ newline_in_sub = current_match.end.lnum - current_match.start.lnum;
+ if (newline_in_pat || newline_in_sub) {
+ ExtmarkSubMulti sub_multi;
+ no_of_lines_changed = newline_in_sub - newline_in_pat;
+
+ sub_multi.newline_in_pat = newline_in_pat;
+ sub_multi.newline_in_sub = newline_in_sub;
+ sub_multi.lnum = lnum;
+ sub_multi.lnum_added = no_of_lines_changed;
+ sub_multi.cm_start = current_match.start;
+ sub_multi.cm_end = current_match.end;
+
+ sub_multi.startpos = regmatch.startpos[0];
+ sub_multi.endpos = regmatch.endpos[0];
+ sub_multi.eol = extmark_eol_col(curbuf, lnum);
+
+ kv_push(extmark_sub_multi, sub_multi);
+ // Collect information required for moving extmarks WITHOUT \n, \r
+ } else {
+ no_of_lines_changed = 0;
+
+ if (regmatch.startpos[0].col != -1) {
+ ExtmarkSubSingle sub_single;
+ sub_single.sublen = sublen;
+ sub_single.lnum = lnum;
+ sub_single.startpos = regmatch.startpos[0];
+ sub_single.endpos = regmatch.endpos[0];
+
+ kv_push(extmark_sub_single, sub_single);
+ }
+ }
+ }
+
+
// 4. If subflags.do_all is set, find next match.
// Prevent endless loop with patterns that match empty
// strings, e.g. :s/$/pat/g or :s/[a-z]* /(&)/g.
@@ -3978,7 +4227,7 @@ skip:
ml_delete(lnum, false);
}
mark_adjust(lnum, lnum + nmatch_tl - 1,
- (long)MAXLNUM, -nmatch_tl, false);
+ (long)MAXLNUM, -nmatch_tl, false, kExtmarkNOOP);
if (subflags.do_ask) {
deleted_lines(lnum, nmatch_tl);
}
@@ -4154,6 +4403,35 @@ skip:
}
}
}
+ if (newline_in_pat || newline_in_sub) {
+ long n = (long)kv_size(extmark_sub_multi);
+ ExtmarkSubMulti sub_multi;
+ if (no_of_lines_changed < 0) {
+ for (i = 0; i < n; i++) {
+ sub_multi = kv_A(extmark_sub_multi, i);
+ extmark_move_regmatch_multi(sub_multi, i);
+ }
+ } else {
+ // Move extmarks in reverse order to avoid moving marks we just moved...
+ for (i = 0; i < n; i++) {
+ sub_multi = kv_Z(extmark_sub_multi, i);
+ extmark_move_regmatch_multi(sub_multi, n - i);
+ }
+ }
+ kv_destroy(extmark_sub_multi);
+ } else {
+ long n = (long)kv_size(extmark_sub_single);
+ ExtmarkSubSingle sub_single;
+ for (i = 0; i < n; i++) {
+ sub_single = kv_Z(extmark_sub_single, i);
+ extmark_move_regmatch_single(sub_single.startpos,
+ sub_single.endpos,
+ sub_single.lnum,
+ sub_single.sublen);
+ }
+
+ kv_destroy(extmark_sub_single);
+ }
kv_destroy(preview_lines.subresults);
@@ -5535,6 +5813,7 @@ static buf_T *show_sub(exarg_T *eap, pos_T old_cusr,
// We keep a special-purpose buffer around, but don't assume it exists.
buf_T *preview_buf = bufnr ? buflist_findnr(bufnr) : 0;
+ cmdmod.split = 0; // disable :leftabove/botright modifiers
cmdmod.tab = 0; // disable :tab modifier
cmdmod.noswapfile = true; // disable swap for preview buffer
// disable file info message
@@ -5581,6 +5860,9 @@ static buf_T *show_sub(exarg_T *eap, pos_T old_cusr,
highest_num_line = kv_last(lines.subresults).end.lnum;
col_width = log10(highest_num_line) + 1 + 3;
}
+ } else {
+ // Failed to split the window, don't show 'inccommand' preview.
+ preview_buf = NULL;
}
char *str = NULL; // construct the line to show in here
@@ -5593,7 +5875,7 @@ static buf_T *show_sub(exarg_T *eap, pos_T old_cusr,
for (size_t matchidx = 0; matchidx < lines.subresults.size; matchidx++) {
SubResult match = lines.subresults.items[matchidx];
- if (split && preview_buf) {
+ if (preview_buf) {
lpos_T p_start = { 0, match.start.col }; // match starts here in preview
lpos_T p_end = { 0, match.end.col }; // ... and ends here
diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua
index 6317ec77ff..f7aa8a994a 100644
--- a/src/nvim/ex_cmds.lua
+++ b/src/nvim/ex_cmds.lua
@@ -323,6 +323,12 @@ return {
func='ex_abclear',
},
{
+ command='cabove',
+ flags=bit.bor(RANGE, TRLBAR),
+ addr_type=ADDR_OTHER ,
+ func='ex_cbelow',
+ },
+ {
command='caddbuffer',
flags=bit.bor(RANGE, NOTADR, WORD1, TRLBAR),
addr_type=ADDR_LINES,
@@ -359,6 +365,12 @@ return {
func='ex_cbuffer',
},
{
+ command='cbelow',
+ flags=bit.bor(RANGE, TRLBAR),
+ addr_type=ADDR_OTHER ,
+ func='ex_cbelow',
+ },
+ {
command='cbottom',
flags=bit.bor(TRLBAR),
addr_type=ADDR_LINES,
@@ -1273,6 +1285,12 @@ return {
func='ex_last',
},
{
+ command='labove',
+ flags=bit.bor(RANGE, TRLBAR),
+ addr_type=ADDR_OTHER ,
+ func='ex_cbelow',
+ },
+ {
command='language',
flags=bit.bor(EXTRA, TRLBAR, CMDWIN),
addr_type=ADDR_LINES,
@@ -1309,6 +1327,12 @@ return {
func='ex_cbuffer',
},
{
+ command='lbelow',
+ flags=bit.bor(RANGE, TRLBAR),
+ addr_type=ADDR_OTHER ,
+ func='ex_cbelow',
+ },
+ {
command='lbottom',
flags=bit.bor(TRLBAR),
addr_type=ADDR_LINES,
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index ae3fb4fbfb..641edf4610 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -140,6 +140,31 @@ struct dbg_stuff {
except_T *current_exception;
};
+typedef struct {
+ // parsed results
+ exarg_T *eap;
+ char_u *parsed_upto; // local we've parsed up to so far
+ char_u *cmd; // start of command
+ char_u *after_modifier;
+
+ // errors
+ char_u *errormsg;
+
+ // globals that need to be updated
+ cmdmod_T cmdmod;
+ int sandbox;
+ int msg_silent;
+ int emsg_silent;
+ bool ex_pressedreturn;
+ long p_verbose;
+
+ // other side-effects
+ bool set_eventignore;
+ long verbose_save;
+ int save_msg_silent;
+ int did_esilent;
+ bool did_sandbox;
+} parse_state_T;
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ex_docmd.c.generated.h"
@@ -1201,69 +1226,74 @@ static void get_wincmd_addr_type(char_u *arg, exarg_T *eap)
}
}
-/*
- * Execute one Ex command.
- *
- * If 'sourcing' is TRUE, the command will be included in the error message.
- *
- * 1. skip comment lines and leading space
- * 2. handle command modifiers
- * 3. skip over the range to find the command
- * 4. parse the range
- * 5. parse the command
- * 6. parse arguments
- * 7. switch on command name
- *
- * Note: "fgetline" can be NULL.
- *
- * This function may be called recursively!
- */
-static char_u * do_one_cmd(char_u **cmdlinep,
- int flags,
- struct condstack *cstack,
- LineGetter fgetline,
- void *cookie /* argument for fgetline() */
- )
+/// Skip colons and trailing whitespace, returning a pointer to the first
+/// non-colon, non-whitespace character.
+//
+/// @param skipleadingwhite Skip leading whitespace too
+static char_u *skip_colon_white(const char_u *p, bool skipleadingwhite)
{
- char_u *p;
- linenr_T lnum;
- long n;
- char_u *errormsg = NULL; /* error message */
- exarg_T ea; /* Ex command arguments */
- long verbose_save = -1;
- int save_msg_scroll = msg_scroll;
- int save_msg_silent = -1;
- int did_esilent = 0;
- int did_sandbox = FALSE;
- cmdmod_T save_cmdmod;
- const int save_reg_executing = reg_executing;
- char_u *cmd;
- int address_count = 1;
+ if (skipleadingwhite) {
+ p = skipwhite(p);
+ }
- memset(&ea, 0, sizeof(ea));
- ea.line1 = 1;
- ea.line2 = 1;
- ex_nesting_level++;
+ while (*p == ':') {
+ p = skipwhite(p + 1);
+ }
- /* When the last file has not been edited :q has to be typed twice. */
- if (quitmore
- /* avoid that a function call in 'statusline' does this */
- && !getline_equal(fgetline, cookie, get_func_line)
- /* avoid that an autocommand, e.g. QuitPre, does this */
- && !getline_equal(fgetline, cookie, getnextac)
- )
- --quitmore;
+ return (char_u *)p;
+}
- /*
- * Reset browse, confirm, etc.. They are restored when returning, for
- * recursive calls.
- */
- save_cmdmod = cmdmod;
- memset(&cmdmod, 0, sizeof(cmdmod));
+static void parse_state_to_global(const parse_state_T *parse_state)
+{
+ cmdmod = parse_state->cmdmod;
+ sandbox = parse_state->sandbox;
+ msg_silent = parse_state->msg_silent;
+ emsg_silent = parse_state->emsg_silent;
+ ex_pressedreturn = parse_state->ex_pressedreturn;
+ p_verbose = parse_state->p_verbose;
- /* "#!anything" is handled like a comment. */
- if ((*cmdlinep)[0] == '#' && (*cmdlinep)[1] == '!')
- goto doend;
+ if (parse_state->set_eventignore) {
+ set_string_option_direct(
+ (char_u *)"ei", -1, (char_u *)"all", OPT_FREE, SID_NONE);
+ }
+}
+
+static void parse_state_from_global(parse_state_T *parse_state)
+{
+ memset(parse_state, 0, sizeof(*parse_state));
+ parse_state->cmdmod = cmdmod;
+ parse_state->sandbox = sandbox;
+ parse_state->msg_silent = msg_silent;
+ parse_state->emsg_silent = emsg_silent;
+ parse_state->ex_pressedreturn = ex_pressedreturn;
+ parse_state->p_verbose = p_verbose;
+}
+
+//
+// Parse one Ex command.
+//
+// This has no side-effects, except for modifying parameters
+// passed in by pointer.
+//
+// The `out` should be zeroed, and its `ea` member initialised,
+// before calling this function.
+//
+static bool parse_one_cmd(
+ char_u **cmdlinep,
+ parse_state_T *const out,
+ LineGetter fgetline,
+ void *fgetline_cookie)
+{
+ exarg_T ea = {
+ .line1 = 1,
+ .line2 = 1,
+ };
+ *out->eap = ea;
+
+ // "#!anything" is handled like a comment.
+ if ((*cmdlinep)[0] == '#' && (*cmdlinep)[1] == '!') {
+ return false;
+ }
/*
* Repeat until no more command modifiers are found.
@@ -1273,70 +1303,76 @@ static char_u * do_one_cmd(char_u **cmdlinep,
/*
* 1. Skip comment lines and leading white space and colons.
*/
- while (*ea.cmd == ' ' || *ea.cmd == '\t' || *ea.cmd == ':')
- ++ea.cmd;
+ while (*ea.cmd == ' '
+ || *ea.cmd == '\t'
+ || *ea.cmd == ':') {
+ ea.cmd++;
+ }
- /* in ex mode, an empty line works like :+ */
+ // in ex mode, an empty line works like :+
if (*ea.cmd == NUL && exmode_active
- && (getline_equal(fgetline, cookie, getexmodeline)
- || getline_equal(fgetline, cookie, getexline))
+ && (getline_equal(fgetline, fgetline_cookie, getexmodeline)
+ || getline_equal(fgetline, fgetline_cookie, getexline))
&& curwin->w_cursor.lnum < curbuf->b_ml.ml_line_count) {
ea.cmd = (char_u *)"+";
- ex_pressedreturn = true;
+ out->ex_pressedreturn = true;
}
- /* ignore comment and empty lines */
- if (*ea.cmd == '"')
- goto doend;
+ // ignore comment and empty lines
+ if (*ea.cmd == '"') {
+ return false;
+ }
if (*ea.cmd == NUL) {
- ex_pressedreturn = true;
- goto doend;
+ out->ex_pressedreturn = true;
+ return false;
}
/*
* 2. Handle command modifiers.
*/
- p = skip_range(ea.cmd, NULL);
+ char_u *p = skip_range(ea.cmd, NULL);
switch (*p) {
- /* When adding an entry, also modify cmd_exists(). */
+ // When adding an entry, also modify cmd_exists().
case 'a': if (!checkforcmd(&ea.cmd, "aboveleft", 3))
break;
- cmdmod.split |= WSP_ABOVE;
+ out->cmdmod.split |= WSP_ABOVE;
continue;
case 'b': if (checkforcmd(&ea.cmd, "belowright", 3)) {
- cmdmod.split |= WSP_BELOW;
+ out->cmdmod.split |= WSP_BELOW;
continue;
}
if (checkforcmd(&ea.cmd, "browse", 3)) {
- cmdmod.browse = true;
+ out->cmdmod.browse = true;
continue;
}
- if (!checkforcmd(&ea.cmd, "botright", 2))
+ if (!checkforcmd(&ea.cmd, "botright", 2)) {
break;
- cmdmod.split |= WSP_BOT;
+ }
+ out->cmdmod.split |= WSP_BOT;
continue;
case 'c': if (!checkforcmd(&ea.cmd, "confirm", 4))
break;
- cmdmod.confirm = true;
+ out->cmdmod.confirm = true;
continue;
case 'k': if (checkforcmd(&ea.cmd, "keepmarks", 3)) {
- cmdmod.keepmarks = true;
+ out->cmdmod.keepmarks = true;
continue;
}
if (checkforcmd(&ea.cmd, "keepalt", 5)) {
- cmdmod.keepalt = true;
+ out->cmdmod.keepalt = true;
continue;
}
if (checkforcmd(&ea.cmd, "keeppatterns", 5)) {
- cmdmod.keeppatterns = true;
+ out->cmdmod.keeppatterns = true;
continue;
}
- if (!checkforcmd(&ea.cmd, "keepjumps", 5))
+ if (!checkforcmd(&ea.cmd, "keepjumps", 5)) {
break;
- cmdmod.keepjumps = true;
+ }
+ out->cmdmod.keepjumps = true;
continue;
case 'f': { // only accept ":filter {pat} cmd"
@@ -1346,7 +1382,7 @@ static char_u * do_one_cmd(char_u **cmdlinep,
break;
}
if (*p == '!') {
- cmdmod.filter_force = true;
+ out->cmdmod.filter_force = true;
p = skipwhite(p + 1);
if (*p == NUL || ends_excmd(*p)) {
break;
@@ -1356,134 +1392,217 @@ static char_u * do_one_cmd(char_u **cmdlinep,
if (p == NULL || *p == NUL) {
break;
}
- cmdmod.filter_regmatch.regprog = vim_regcomp(reg_pat, RE_MAGIC);
- if (cmdmod.filter_regmatch.regprog == NULL) {
+ out->cmdmod.filter_regmatch.regprog = vim_regcomp(reg_pat, RE_MAGIC);
+ if (out->cmdmod.filter_regmatch.regprog == NULL) {
break;
}
ea.cmd = p;
continue;
}
- /* ":hide" and ":hide | cmd" are not modifiers */
+ // ":hide" and ":hide | cmd" are not modifiers
case 'h': if (p != ea.cmd || !checkforcmd(&p, "hide", 3)
|| *p == NUL || ends_excmd(*p))
break;
ea.cmd = p;
- cmdmod.hide = true;
+ out->cmdmod.hide = true;
continue;
case 'l': if (checkforcmd(&ea.cmd, "lockmarks", 3)) {
- cmdmod.lockmarks = true;
+ out->cmdmod.lockmarks = true;
continue;
}
- if (!checkforcmd(&ea.cmd, "leftabove", 5))
+ if (!checkforcmd(&ea.cmd, "leftabove", 5)) {
break;
- cmdmod.split |= WSP_ABOVE;
+ }
+ out->cmdmod.split |= WSP_ABOVE;
continue;
case 'n':
if (checkforcmd(&ea.cmd, "noautocmd", 3)) {
- if (cmdmod.save_ei == NULL) {
- /* Set 'eventignore' to "all". Restore the
- * existing option value later. */
- cmdmod.save_ei = vim_strsave(p_ei);
- set_string_option_direct(
- (char_u *)"ei", -1, (char_u *)"all", OPT_FREE, SID_NONE);
+ if (out->cmdmod.save_ei == NULL) {
+ // Set 'eventignore' to "all". Restore the
+ // existing option value later.
+ out->cmdmod.save_ei = vim_strsave(p_ei);
+ out->set_eventignore = true;
}
continue;
}
if (!checkforcmd(&ea.cmd, "noswapfile", 3)) {
break;
}
- cmdmod.noswapfile = true;
+ out->cmdmod.noswapfile = true;
continue;
case 'r': if (!checkforcmd(&ea.cmd, "rightbelow", 6))
break;
- cmdmod.split |= WSP_BELOW;
+ out->cmdmod.split |= WSP_BELOW;
continue;
case 's': if (checkforcmd(&ea.cmd, "sandbox", 3)) {
- if (!did_sandbox)
- ++sandbox;
- did_sandbox = TRUE;
+ if (!out->did_sandbox) {
+ out->sandbox++;
+ }
+ out->did_sandbox = true;
continue;
}
- if (!checkforcmd(&ea.cmd, "silent", 3))
+ if (!checkforcmd(&ea.cmd, "silent", 3)) {
break;
- if (save_msg_silent == -1)
- save_msg_silent = msg_silent;
- ++msg_silent;
+ }
+ if (out->save_msg_silent == -1) {
+ out->save_msg_silent = out->msg_silent;
+ }
+ out->msg_silent++;
if (*ea.cmd == '!' && !ascii_iswhite(ea.cmd[-1])) {
- /* ":silent!", but not "silent !cmd" */
+ // ":silent!", but not "silent !cmd"
ea.cmd = skipwhite(ea.cmd + 1);
- ++emsg_silent;
- ++did_esilent;
+ out->emsg_silent++;
+ out->did_esilent++;
}
continue;
case 't': if (checkforcmd(&p, "tab", 3)) {
- long tabnr = get_address(&ea, &ea.cmd, ADDR_TABS, ea.skip, false, 1);
+ long tabnr = get_address(
+ &ea, &ea.cmd, ADDR_TABS, ea.skip, false, 1);
+
if (tabnr == MAXLNUM) {
- cmdmod.tab = tabpage_index(curtab) + 1;
+ out->cmdmod.tab = tabpage_index(curtab) + 1;
} else {
if (tabnr < 0 || tabnr > LAST_TAB_NR) {
- errormsg = (char_u *)_(e_invrange);
- goto doend;
+ out->errormsg = (char_u *)_(e_invrange);
+ return false;
}
- cmdmod.tab = tabnr + 1;
+ out->cmdmod.tab = tabnr + 1;
}
ea.cmd = p;
continue;
}
- if (!checkforcmd(&ea.cmd, "topleft", 2))
+ if (!checkforcmd(&ea.cmd, "topleft", 2)) {
break;
- cmdmod.split |= WSP_TOP;
+ }
+ out->cmdmod.split |= WSP_TOP;
continue;
case 'u': if (!checkforcmd(&ea.cmd, "unsilent", 3))
break;
- if (save_msg_silent == -1)
- save_msg_silent = msg_silent;
- msg_silent = 0;
+ if (out->save_msg_silent == -1) {
+ out->save_msg_silent = out->msg_silent;
+ }
+ out->msg_silent = 0;
continue;
case 'v': if (checkforcmd(&ea.cmd, "vertical", 4)) {
- cmdmod.split |= WSP_VERT;
+ out->cmdmod.split |= WSP_VERT;
continue;
}
if (!checkforcmd(&p, "verbose", 4))
break;
- if (verbose_save < 0)
- verbose_save = p_verbose;
- if (ascii_isdigit(*ea.cmd))
- p_verbose = atoi((char *)ea.cmd);
- else
- p_verbose = 1;
+ if (out->verbose_save < 0) {
+ out->verbose_save = out->p_verbose;
+ }
+ if (ascii_isdigit(*ea.cmd)) {
+ out->p_verbose = atoi((char *)ea.cmd);
+ } else {
+ out->p_verbose = 1;
+ }
ea.cmd = p;
continue;
}
break;
}
- char_u *after_modifier = ea.cmd;
-
- ea.skip = (did_emsg
- || got_int
- || current_exception
- || (cstack->cs_idx >= 0
- && !(cstack->cs_flags[cstack->cs_idx] & CSF_ACTIVE)));
+ out->after_modifier = ea.cmd;
// 3. Skip over the range to find the command. Let "p" point to after it.
//
// We need the command to know what kind of range it uses.
- cmd = ea.cmd;
+ out->cmd = ea.cmd;
ea.cmd = skip_range(ea.cmd, NULL);
if (*ea.cmd == '*') {
ea.cmd = skipwhite(ea.cmd + 1);
}
- p = find_command(&ea, NULL);
+ out->parsed_upto = find_command(&ea, NULL);
+
+ *out->eap = ea;
+
+ return true;
+}
+
+/*
+ * Execute one Ex command.
+ *
+ * If 'sourcing' is TRUE, the command will be included in the error message.
+ *
+ * 1. skip comment lines and leading space
+ * 2. handle command modifiers
+ * 3. skip over the range to find the command
+ * 4. parse the range
+ * 5. parse the command
+ * 6. parse arguments
+ * 7. switch on command name
+ *
+ * Note: "fgetline" can be NULL.
+ *
+ * This function may be called recursively!
+ */
+static char_u * do_one_cmd(char_u **cmdlinep,
+ int flags,
+ struct condstack *cstack,
+ LineGetter fgetline,
+ void *cookie /* argument for fgetline() */
+ )
+{
+ char_u *p;
+ linenr_T lnum;
+ long n;
+ char_u *errormsg = NULL; // error message
+ exarg_T ea;
+ int save_msg_scroll = msg_scroll;
+ parse_state_T parsed;
+ cmdmod_T save_cmdmod;
+ const int save_reg_executing = reg_executing;
+
+ ex_nesting_level++;
+
+ /* When the last file has not been edited :q has to be typed twice. */
+ if (quitmore
+ /* avoid that a function call in 'statusline' does this */
+ && !getline_equal(fgetline, cookie, get_func_line)
+ /* avoid that an autocommand, e.g. QuitPre, does this */
+ && !getline_equal(fgetline, cookie, getnextac)
+ )
+ --quitmore;
+
+ /*
+ * Reset browse, confirm, etc.. They are restored when returning, for
+ * recursive calls.
+ */
+ save_cmdmod = cmdmod;
+ memset(&cmdmod, 0, sizeof(cmdmod));
+
+ parse_state_from_global(&parsed);
+ parsed.eap = &ea;
+ parsed.verbose_save = -1;
+ parsed.save_msg_silent = -1;
+ parsed.did_esilent = 0;
+ parsed.did_sandbox = false;
+ bool parse_success = parse_one_cmd(cmdlinep, &parsed, fgetline, cookie);
+ parse_state_to_global(&parsed);
+
+ // Update locals from parse_one_cmd()
+ errormsg = parsed.errormsg;
+ p = parsed.parsed_upto;
+
+ if (!parse_success) {
+ goto doend;
+ }
+
+ ea.skip = (did_emsg
+ || got_int
+ || current_exception
+ || (cstack->cs_idx >= 0
+ && !(cstack->cs_flags[cstack->cs_idx] & CSF_ACTIVE)));
// Count this line for profiling if skip is TRUE.
if (do_profiling == PROF_YES
@@ -1554,148 +1673,9 @@ static char_u * do_one_cmd(char_u **cmdlinep,
}
}
- /* repeat for all ',' or ';' separated addresses */
- ea.cmd = cmd;
- for (;; ) {
- ea.line1 = ea.line2;
- switch (ea.addr_type) {
- case ADDR_LINES:
- // default is current line number
- ea.line2 = curwin->w_cursor.lnum;
- break;
- case ADDR_WINDOWS:
- ea.line2 = CURRENT_WIN_NR;
- break;
- case ADDR_ARGUMENTS:
- ea.line2 = curwin->w_arg_idx + 1;
- if (ea.line2 > ARGCOUNT) {
- ea.line2 = ARGCOUNT;
- }
- break;
- case ADDR_LOADED_BUFFERS:
- case ADDR_BUFFERS:
- ea.line2 = curbuf->b_fnum;
- break;
- case ADDR_TABS:
- ea.line2 = CURRENT_TAB_NR;
- break;
- case ADDR_TABS_RELATIVE:
- ea.line2 = 1;
- break;
- case ADDR_QUICKFIX:
- ea.line2 = qf_get_cur_valid_idx(&ea);
- break;
- }
- ea.cmd = skipwhite(ea.cmd);
- lnum = get_address(&ea, &ea.cmd, ea.addr_type, ea.skip,
- ea.addr_count == 0, address_count++);
- if (ea.cmd == NULL) { // error detected
- goto doend;
- }
- if (lnum == MAXLNUM) {
- if (*ea.cmd == '%') { /* '%' - all lines */
- ++ea.cmd;
- switch (ea.addr_type) {
- case ADDR_LINES:
- ea.line1 = 1;
- ea.line2 = curbuf->b_ml.ml_line_count;
- break;
- case ADDR_LOADED_BUFFERS: {
- buf_T *buf = firstbuf;
- while (buf->b_next != NULL && buf->b_ml.ml_mfp == NULL) {
- buf = buf->b_next;
- }
- ea.line1 = buf->b_fnum;
- buf = lastbuf;
- while (buf->b_prev != NULL && buf->b_ml.ml_mfp == NULL) {
- buf = buf->b_prev;
- }
- ea.line2 = buf->b_fnum;
- break;
- }
- case ADDR_BUFFERS:
- ea.line1 = firstbuf->b_fnum;
- ea.line2 = lastbuf->b_fnum;
- break;
- case ADDR_WINDOWS:
- case ADDR_TABS:
- if (IS_USER_CMDIDX(ea.cmdidx)) {
- ea.line1 = 1;
- ea.line2 =
- ea.addr_type == ADDR_WINDOWS ? LAST_WIN_NR : LAST_TAB_NR;
- } else {
- // there is no Vim command which uses '%' and
- // ADDR_WINDOWS or ADDR_TABS
- errormsg = (char_u *)_(e_invrange);
- goto doend;
- }
- break;
- case ADDR_TABS_RELATIVE:
- errormsg = (char_u *)_(e_invrange);
- goto doend;
- break;
- case ADDR_ARGUMENTS:
- if (ARGCOUNT == 0) {
- ea.line1 = ea.line2 = 0;
- } else {
- ea.line1 = 1;
- ea.line2 = ARGCOUNT;
- }
- break;
- case ADDR_QUICKFIX:
- ea.line1 = 1;
- ea.line2 = qf_get_size(&ea);
- if (ea.line2 == 0) {
- ea.line2 = 1;
- }
- break;
- }
- ++ea.addr_count;
- }
- /* '*' - visual area */
- else if (*ea.cmd == '*') {
- pos_T *fp;
-
- if (ea.addr_type != ADDR_LINES) {
- errormsg = (char_u *)_(e_invrange);
- goto doend;
- }
-
- ++ea.cmd;
- if (!ea.skip) {
- fp = getmark('<', FALSE);
- if (check_mark(fp) == FAIL)
- goto doend;
- ea.line1 = fp->lnum;
- fp = getmark('>', FALSE);
- if (check_mark(fp) == FAIL)
- goto doend;
- ea.line2 = fp->lnum;
- ++ea.addr_count;
- }
- }
- } else
- ea.line2 = lnum;
- ea.addr_count++;
-
- if (*ea.cmd == ';') {
- if (!ea.skip) {
- curwin->w_cursor.lnum = ea.line2;
- // don't leave the cursor on an illegal line or column
- check_cursor();
- }
- } else if (*ea.cmd != ',') {
- break;
- }
- ea.cmd++;
- }
-
- /* One address given: set start and end lines */
- if (ea.addr_count == 1) {
- ea.line1 = ea.line2;
- /* ... but only implicit: really no address given */
- if (lnum == MAXLNUM)
- ea.addr_count = 0;
+ ea.cmd = parsed.cmd;
+ if (parse_cmd_address(&ea, &errormsg) == FAIL) {
+ goto doend;
}
/*
@@ -1705,9 +1685,7 @@ static char_u * do_one_cmd(char_u **cmdlinep,
/*
* Skip ':' and any white space
*/
- ea.cmd = skipwhite(ea.cmd);
- while (*ea.cmd == ':')
- ea.cmd = skipwhite(ea.cmd + 1);
+ ea.cmd = skip_colon_white(ea.cmd, true);
/*
* If we got a line, but no command, then go to the line.
@@ -1776,8 +1754,8 @@ static char_u * do_one_cmd(char_u **cmdlinep,
if (!(flags & DOCMD_VERBOSE)) {
// If the modifier was parsed OK the error must be in the following
// command
- if (after_modifier != NULL) {
- append_command(after_modifier);
+ if (parsed.after_modifier != NULL) {
+ append_command(parsed.after_modifier);
} else {
append_command(*cmdlinep);
}
@@ -2245,12 +2223,12 @@ static char_u * do_one_cmd(char_u **cmdlinep,
// The :try command saves the emsg_silent flag, reset it here when
// ":silent! try" was used, it should only apply to :try itself.
- if (ea.cmdidx == CMD_try && did_esilent > 0) {
- emsg_silent -= did_esilent;
+ if (ea.cmdidx == CMD_try && parsed.did_esilent > 0) {
+ emsg_silent -= parsed.did_esilent;
if (emsg_silent < 0) {
emsg_silent = 0;
}
- did_esilent = 0;
+ parsed.did_esilent = 0;
}
// 7. Execute the command.
@@ -2316,8 +2294,9 @@ doend:
? cmdnames[(int)ea.cmdidx].cmd_name
: (char_u *)NULL);
- if (verbose_save >= 0)
- p_verbose = verbose_save;
+ if (parsed.verbose_save >= 0) {
+ p_verbose = parsed.verbose_save;
+ }
if (cmdmod.save_ei != NULL) {
/* Restore 'eventignore' to the value before ":noautocmd". */
set_string_option_direct((char_u *)"ei", -1, cmdmod.save_ei,
@@ -2332,16 +2311,18 @@ doend:
cmdmod = save_cmdmod;
reg_executing = save_reg_executing;
- if (save_msg_silent != -1) {
- /* messages could be enabled for a serious error, need to check if the
- * counters don't become negative */
- if (!did_emsg || msg_silent > save_msg_silent)
- msg_silent = save_msg_silent;
- emsg_silent -= did_esilent;
- if (emsg_silent < 0)
+ if (parsed.save_msg_silent != -1) {
+ // messages could be enabled for a serious error, need to check if the
+ // counters don't become negative
+ if (!did_emsg || msg_silent > parsed.save_msg_silent) {
+ msg_silent = parsed.save_msg_silent;
+ }
+ emsg_silent -= parsed.did_esilent;
+ if (emsg_silent < 0) {
emsg_silent = 0;
- /* Restore msg_scroll, it's set by file I/O commands, even when no
- * message is actually displayed. */
+ }
+ // Restore msg_scroll, it's set by file I/O commands, even when no
+ // message is actually displayed.
msg_scroll = save_msg_scroll;
/* "silent reg" or "silent echo x" inside "redir" leaves msg_col
@@ -2350,8 +2331,9 @@ doend:
msg_col = 0;
}
- if (did_sandbox)
- --sandbox;
+ if (parsed.did_sandbox) {
+ sandbox--;
+ }
if (ea.nextcmd && *ea.nextcmd == NUL) /* not really a next command */
ea.nextcmd = NULL;
@@ -2361,6 +2343,160 @@ doend:
return ea.nextcmd;
}
+// Parse the address range, if any, in "eap".
+// Return FAIL and set "errormsg" or return OK.
+int parse_cmd_address(exarg_T *eap, char_u **errormsg)
+ FUNC_ATTR_NONNULL_ALL
+{
+ int address_count = 1;
+ linenr_T lnum;
+
+ // Repeat for all ',' or ';' separated addresses.
+ for (;;) {
+ eap->line1 = eap->line2;
+ switch (eap->addr_type) {
+ case ADDR_LINES:
+ // default is current line number
+ eap->line2 = curwin->w_cursor.lnum;
+ break;
+ case ADDR_WINDOWS:
+ eap->line2 = CURRENT_WIN_NR;
+ break;
+ case ADDR_ARGUMENTS:
+ eap->line2 = curwin->w_arg_idx + 1;
+ if (eap->line2 > ARGCOUNT) {
+ eap->line2 = ARGCOUNT;
+ }
+ break;
+ case ADDR_LOADED_BUFFERS:
+ case ADDR_BUFFERS:
+ eap->line2 = curbuf->b_fnum;
+ break;
+ case ADDR_TABS:
+ eap->line2 = CURRENT_TAB_NR;
+ break;
+ case ADDR_TABS_RELATIVE:
+ eap->line2 = 1;
+ break;
+ case ADDR_QUICKFIX:
+ eap->line2 = qf_get_cur_valid_idx(eap);
+ break;
+ }
+ eap->cmd = skipwhite(eap->cmd);
+ lnum = get_address(eap, &eap->cmd, eap->addr_type, eap->skip,
+ eap->addr_count == 0, address_count++);
+ if (eap->cmd == NULL) { // error detected
+ return FAIL;
+ }
+ if (lnum == MAXLNUM) {
+ if (*eap->cmd == '%') { // '%' - all lines
+ eap->cmd++;
+ switch (eap->addr_type) {
+ case ADDR_LINES:
+ eap->line1 = 1;
+ eap->line2 = curbuf->b_ml.ml_line_count;
+ break;
+ case ADDR_LOADED_BUFFERS: {
+ buf_T *buf = firstbuf;
+
+ while (buf->b_next != NULL && buf->b_ml.ml_mfp == NULL) {
+ buf = buf->b_next;
+ }
+ eap->line1 = buf->b_fnum;
+ buf = lastbuf;
+ while (buf->b_prev != NULL && buf->b_ml.ml_mfp == NULL) {
+ buf = buf->b_prev;
+ }
+ eap->line2 = buf->b_fnum;
+ break;
+ }
+ case ADDR_BUFFERS:
+ eap->line1 = firstbuf->b_fnum;
+ eap->line2 = lastbuf->b_fnum;
+ break;
+ case ADDR_WINDOWS:
+ case ADDR_TABS:
+ if (IS_USER_CMDIDX(eap->cmdidx)) {
+ eap->line1 = 1;
+ eap->line2 = eap->addr_type == ADDR_WINDOWS
+ ? LAST_WIN_NR : LAST_TAB_NR;
+ } else {
+ // there is no Vim command which uses '%' and
+ // ADDR_WINDOWS or ADDR_TABS
+ *errormsg = (char_u *)_(e_invrange);
+ return FAIL;
+ }
+ break;
+ case ADDR_TABS_RELATIVE:
+ *errormsg = (char_u *)_(e_invrange);
+ return FAIL;
+ case ADDR_ARGUMENTS:
+ if (ARGCOUNT == 0) {
+ eap->line1 = eap->line2 = 0;
+ } else {
+ eap->line1 = 1;
+ eap->line2 = ARGCOUNT;
+ }
+ break;
+ case ADDR_QUICKFIX:
+ eap->line1 = 1;
+ eap->line2 = qf_get_size(eap);
+ if (eap->line2 == 0) {
+ eap->line2 = 1;
+ }
+ break;
+ }
+ eap->addr_count++;
+ } else if (*eap->cmd == '*') {
+ // '*' - visual area
+ if (eap->addr_type != ADDR_LINES) {
+ *errormsg = (char_u *)_(e_invrange);
+ return FAIL;
+ }
+
+ eap->cmd++;
+ if (!eap->skip) {
+ pos_T *fp = getmark('<', false);
+ if (check_mark(fp) == FAIL) {
+ return FAIL;
+ }
+ eap->line1 = fp->lnum;
+ fp = getmark('>', false);
+ if (check_mark(fp) == FAIL) {
+ return FAIL;
+ }
+ eap->line2 = fp->lnum;
+ eap->addr_count++;
+ }
+ }
+ } else {
+ eap->line2 = lnum;
+ }
+ eap->addr_count++;
+
+ if (*eap->cmd == ';') {
+ if (!eap->skip) {
+ curwin->w_cursor.lnum = eap->line2;
+ // don't leave the cursor on an illegal line or column
+ check_cursor();
+ }
+ } else if (*eap->cmd != ',') {
+ break;
+ }
+ eap->cmd++;
+ }
+
+ // One address given: set start and end lines.
+ if (eap->addr_count == 1) {
+ eap->line1 = eap->line2;
+ // ... but only implicit: really no address given
+ if (lnum == MAXLNUM) {
+ eap->addr_count = 0;
+ }
+ }
+ return OK;
+}
+
/*
* Check for an Ex command with optional tail.
* If there is a match advance "pp" to the argument and return TRUE.
@@ -3541,15 +3677,13 @@ const char * set_one_cmd_context(
return NULL;
}
-/*
- * skip a range specifier of the form: addr [,addr] [;addr] ..
- *
- * Backslashed delimiters after / or ? will be skipped, and commands will
- * not be expanded between /'s and ?'s or after "'".
- *
- * Also skip white space and ":" characters.
- * Returns the "cmd" pointer advanced to beyond the range.
- */
+// Skip a range specifier of the form: addr [,addr] [;addr] ..
+//
+// Backslashed delimiters after / or ? will be skipped, and commands will
+// not be expanded between /'s and ?'s or after "'".
+//
+// Also skip white space and ":" characters.
+// Returns the "cmd" pointer advanced to beyond the range.
char_u *skip_range(
const char_u *cmd,
int *ctx // pointer to xp_context or NULL
@@ -3580,9 +3714,8 @@ char_u *skip_range(
++cmd;
}
- /* Skip ":" and white space. */
- while (*cmd == ':')
- cmd = skipwhite(cmd + 1);
+ // Skip ":" and white space.
+ cmd = skip_colon_white(cmd, false);
return (char_u *)cmd;
}
@@ -3750,8 +3883,7 @@ static linenr_T get_address(exarg_T *eap,
curwin->w_cursor.col = 0;
}
searchcmdlen = 0;
- if (!do_search(NULL, c, cmd, 1L,
- SEARCH_HIS | SEARCH_MSG, NULL, NULL)) {
+ if (!do_search(NULL, c, cmd, 1L, SEARCH_HIS | SEARCH_MSG, NULL)) {
curwin->w_cursor = pos;
cmd = NULL;
goto error;
@@ -3788,8 +3920,7 @@ static linenr_T get_address(exarg_T *eap,
pos.coladd = 0;
if (searchit(curwin, curbuf, &pos, NULL,
*cmd == '?' ? BACKWARD : FORWARD,
- (char_u *)"", 1L, SEARCH_MSG,
- i, (linenr_T)0, NULL, NULL) != FAIL) {
+ (char_u *)"", 1L, SEARCH_MSG, i, NULL) != FAIL) {
lnum = pos.lnum;
} else {
cmd = NULL;
@@ -8195,6 +8326,7 @@ static void ex_normal(exarg_T *eap)
int save_insertmode = p_im;
int save_finish_op = finish_op;
long save_opcount = opcount;
+ const int save_reg_executing = reg_executing;
char_u *arg = NULL;
int l;
char_u *p;
@@ -8289,7 +8421,8 @@ static void ex_normal(exarg_T *eap)
p_im = save_insertmode;
finish_op = save_finish_op;
opcount = save_opcount;
- msg_didout |= save_msg_didout; /* don't reset msg_didout now */
+ reg_executing = save_reg_executing;
+ msg_didout |= save_msg_didout; // don't reset msg_didout now
/* Restore the state (needed when called from a function executed for
* 'indentexpr'). Update the mouse and cursor, they may have changed. */
@@ -10139,6 +10272,17 @@ static void ex_folddo(exarg_T *eap)
ml_clearmarked(); // clear rest of the marks
}
+// Returns true if the supplied Ex cmdidx is for a location list command
+// instead of a quickfix command.
+bool is_loclist_cmd(int cmdidx)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ if (cmdidx < 0 || cmdidx >= CMD_SIZE) {
+ return false;
+ }
+ return cmdnames[cmdidx].cmd_name[0] == 'l';
+}
+
bool get_pressedreturn(void)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
@@ -10197,10 +10341,13 @@ bool cmd_can_preview(char_u *cmd)
return false;
}
+ // Ignore additional colons at the start...
+ cmd = skip_colon_white(cmd, true);
+
// Ignore any leading modifiers (:keeppatterns, :verbose, etc.)
for (int len = modifier_len(cmd); len != 0; len = modifier_len(cmd)) {
cmd += len;
- cmd = skipwhite(cmd);
+ cmd = skip_colon_white(cmd, true);
}
exarg_T ea;
diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c
index 5235b9e648..9e2671ca5e 100644
--- a/src/nvim/ex_getln.c
+++ b/src/nvim/ex_getln.c
@@ -1075,7 +1075,7 @@ static void command_line_next_incsearch(CommandLineState *s, bool next_match)
int found = searchit(curwin, curbuf, &t, NULL,
next_match ? FORWARD : BACKWARD,
pat, s->count, search_flags,
- RE_SEARCH, 0, NULL, NULL);
+ RE_SEARCH, NULL);
emsg_off--;
ui_busy_stop();
if (found) {
@@ -1818,6 +1818,7 @@ static int command_line_changed(CommandLineState *s)
if (p_is && !cmd_silent && (s->firstc == '/' || s->firstc == '?')) {
pos_T end_pos;
proftime_T tm;
+ searchit_arg_T sia;
// if there is a character waiting, search and redraw later
if (char_avail()) {
@@ -1844,8 +1845,10 @@ static int command_line_changed(CommandLineState *s)
if (!p_hls) {
search_flags += SEARCH_KEEP;
}
+ memset(&sia, 0, sizeof(sia));
+ sia.sa_tm = &tm;
i = do_search(NULL, s->firstc, ccline.cmdbuff, s->count,
- search_flags, &tm, NULL);
+ search_flags, &sia);
emsg_off--;
// if interrupted while searching, behave like it failed
if (got_int) {
@@ -1924,7 +1927,9 @@ static int command_line_changed(CommandLineState *s)
// - Immediately undo the effects.
State |= CMDPREVIEW;
emsg_silent++; // Block error reporting as the command may be incomplete
+ msg_silent++; // Block messages, namely ones that prompt
do_cmdline(ccline.cmdbuff, NULL, NULL, DOCMD_KEEPLINE|DOCMD_NOWAIT);
+ msg_silent--; // Unblock messages
emsg_silent--; // Unblock error reporting
// Restore the window "view".
diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c
index 58e6b2ae92..fcf15638c7 100644
--- a/src/nvim/fileio.c
+++ b/src/nvim/fileio.c
@@ -2650,6 +2650,7 @@ buf_write(
*/
if (!(append && *p_pm == NUL) && !filtering && perm >= 0 && dobackup) {
FileInfo file_info;
+ const bool no_prepend_dot = false;
if ((bkc & BKC_YES) || append) { /* "yes" */
backup_copy = TRUE;
@@ -2737,6 +2738,7 @@ buf_write(
int some_error = false;
char_u *dirp;
char_u *rootname;
+ char_u *p;
/*
* Try to make the backup in each directory in the 'bdir' option.
@@ -2756,6 +2758,17 @@ buf_write(
* Isolate one directory name, using an entry in 'bdir'.
*/
(void)copy_option_part(&dirp, IObuff, IOSIZE, ",");
+ p = IObuff + STRLEN(IObuff);
+ if (after_pathsep((char *)IObuff, (char *)p) && p[-1] == p[-2]) {
+ // Ends with '//', Use Full path
+ if ((p = (char_u *)make_percent_swname((char *)IObuff, (char *)fname))
+ != NULL) {
+ backup = (char_u *)modname((char *)p, (char *)backup_ext,
+ no_prepend_dot);
+ xfree(p);
+ }
+ }
+
rootname = get_file_in_dir(fname, IObuff);
if (rootname == NULL) {
some_error = TRUE; /* out of memory */
@@ -2764,10 +2777,14 @@ buf_write(
FileInfo file_info_new;
{
- /*
- * Make backup file name.
- */
- backup = (char_u *)modname((char *)rootname, (char *)backup_ext, FALSE);
+ //
+ // Make the backup file name.
+ //
+ if (backup == NULL) {
+ backup = (char_u *)modname((char *)rootname, (char *)backup_ext,
+ no_prepend_dot);
+ }
+
if (backup == NULL) {
xfree(rootname);
some_error = TRUE; /* out of memory */
@@ -2893,12 +2910,26 @@ nobackup:
* Isolate one directory name and make the backup file name.
*/
(void)copy_option_part(&dirp, IObuff, IOSIZE, ",");
- rootname = get_file_in_dir(fname, IObuff);
- if (rootname == NULL)
- backup = NULL;
- else {
- backup = (char_u *)modname((char *)rootname, (char *)backup_ext, FALSE);
- xfree(rootname);
+ p = IObuff + STRLEN(IObuff);
+ if (after_pathsep((char *)IObuff, (char *)p) && p[-1] == p[-2]) {
+ // path ends with '//', use full path
+ if ((p = (char_u *)make_percent_swname((char *)IObuff, (char *)fname))
+ != NULL) {
+ backup = (char_u *)modname((char *)p, (char *)backup_ext,
+ no_prepend_dot);
+ xfree(p);
+ }
+ }
+
+ if (backup == NULL) {
+ rootname = get_file_in_dir(fname, IObuff);
+ if (rootname == NULL) {
+ backup = NULL;
+ } else {
+ backup = (char_u *)modname((char *)rootname, (char *)backup_ext,
+ no_prepend_dot);
+ xfree(rootname);
+ }
}
if (backup != NULL) {
diff --git a/src/nvim/fold.c b/src/nvim/fold.c
index 5ce953e626..b193b4005c 100644
--- a/src/nvim/fold.c
+++ b/src/nvim/fold.c
@@ -771,6 +771,11 @@ void foldUpdate(win_T *wp, linenr_T top, linenr_T bot)
return;
}
+ if (need_diff_redraw) {
+ // will update later
+ return;
+ }
+
// Mark all folds from top to bot as maybe-small.
fold_T *fp;
(void)foldFind(&wp->w_folds, top, &fp);
diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c
index 399f0671b4..c038977127 100644
--- a/src/nvim/getchar.c
+++ b/src/nvim/getchar.c
@@ -2409,7 +2409,6 @@ int inchar(
did_outofmem_msg = FALSE; /* display out of memory message (again) */
did_swapwrite_msg = FALSE; /* display swap file write error again */
}
- undo_off = FALSE; /* restart undo now */
// Get a character from a script file if there is one.
// If interrupted: Stop reading script files, close them all.
diff --git a/src/nvim/globals.h b/src/nvim/globals.h
index c3d1a4d40b..15ad6d8767 100644
--- a/src/nvim/globals.h
+++ b/src/nvim/globals.h
@@ -400,11 +400,6 @@ EXTERN bool mouse_past_eol INIT(= false); /* mouse right of line */
EXTERN int mouse_dragging INIT(= 0); /* extending Visual area with
mouse dragging */
-/* Value set from 'diffopt'. */
-EXTERN int diff_context INIT(= 6); /* context for folds */
-EXTERN int diff_foldcolumn INIT(= 2); /* 'foldcolumn' for diff mode */
-EXTERN int diff_need_scrollbind INIT(= FALSE);
-
/* The root of the menu hierarchy. */
EXTERN vimmenu_T *root_menu INIT(= NULL);
/*
@@ -768,7 +763,6 @@ EXTERN int did_outofmem_msg INIT(= false);
// set after out of memory msg
EXTERN int did_swapwrite_msg INIT(= false);
// set after swap write error msg
-EXTERN int undo_off INIT(= false); // undo switched off for now
EXTERN int global_busy INIT(= 0); // set when :global is executing
EXTERN int listcmd_busy INIT(= false); // set when :argdo, :windo or
// :bufdo is executing
diff --git a/src/nvim/highlight.c b/src/nvim/highlight.c
index 83ee89b2a1..c96f07ed89 100644
--- a/src/nvim/highlight.c
+++ b/src/nvim/highlight.c
@@ -321,18 +321,26 @@ int hl_combine_attr(int char_attr, int prim_attr)
if (spell_aep.cterm_fg_color > 0) {
new_en.cterm_fg_color = spell_aep.cterm_fg_color;
+ new_en.rgb_ae_attr &= ((~HL_FG_INDEXED)
+ | (spell_aep.rgb_ae_attr & HL_FG_INDEXED));
}
if (spell_aep.cterm_bg_color > 0) {
new_en.cterm_bg_color = spell_aep.cterm_bg_color;
+ new_en.rgb_ae_attr &= ((~HL_BG_INDEXED)
+ | (spell_aep.rgb_ae_attr & HL_BG_INDEXED));
}
if (spell_aep.rgb_fg_color >= 0) {
new_en.rgb_fg_color = spell_aep.rgb_fg_color;
+ new_en.rgb_ae_attr &= ((~HL_FG_INDEXED)
+ | (spell_aep.rgb_ae_attr & HL_FG_INDEXED));
}
if (spell_aep.rgb_bg_color >= 0) {
new_en.rgb_bg_color = spell_aep.rgb_bg_color;
+ new_en.rgb_ae_attr &= ((~HL_BG_INDEXED)
+ | (spell_aep.rgb_ae_attr & HL_BG_INDEXED));
}
if (spell_aep.rgb_sp_color >= 0) {
@@ -422,6 +430,7 @@ int hl_blend_attrs(int back_attr, int front_attr, bool *through)
cattrs.cterm_bg_color = fattrs.cterm_bg_color;
cattrs.cterm_fg_color = cterm_blend(ratio, battrs.cterm_fg_color,
fattrs.cterm_bg_color);
+ cattrs.rgb_ae_attr &= ~(HL_FG_INDEXED | HL_BG_INDEXED);
} else {
cattrs = fattrs;
if (ratio >= 50) {
@@ -435,6 +444,8 @@ int hl_blend_attrs(int back_attr, int front_attr, bool *through)
} else {
cattrs.rgb_sp_color = -1;
}
+
+ cattrs.rgb_ae_attr &= ~HL_BG_INDEXED;
}
cattrs.rgb_bg_color = rgb_blend(ratio, battrs.rgb_bg_color,
fattrs.rgb_bg_color);
@@ -611,6 +622,14 @@ Dictionary hlattrs2dict(HlAttrs ae, bool use_rgb)
}
if (use_rgb) {
+ if (mask & HL_FG_INDEXED) {
+ PUT(hl, "fg_indexed", BOOLEAN_OBJ(true));
+ }
+
+ if (mask & HL_BG_INDEXED) {
+ PUT(hl, "bg_indexed", BOOLEAN_OBJ(true));
+ }
+
if (ae.rgb_fg_color != -1) {
PUT(hl, "foreground", INTEGER_OBJ(ae.rgb_fg_color));
}
diff --git a/src/nvim/highlight_defs.h b/src/nvim/highlight_defs.h
index 255699c8e0..36f3181674 100644
--- a/src/nvim/highlight_defs.h
+++ b/src/nvim/highlight_defs.h
@@ -19,6 +19,8 @@ typedef enum {
HL_STANDOUT = 0x20,
HL_STRIKETHROUGH = 0x40,
HL_NOCOMBINE = 0x80,
+ HL_BG_INDEXED = 0x0100,
+ HL_FG_INDEXED = 0x0200,
} HlAttrFlags;
/// Stores a complete highlighting entry, including colors and attributes
diff --git a/src/nvim/lib/kbtree.h b/src/nvim/lib/kbtree.h
index 33aeff1d89..bef37f8ba9 100644
--- a/src/nvim/lib/kbtree.h
+++ b/src/nvim/lib/kbtree.h
@@ -25,6 +25,12 @@
* SUCH DAMAGE.
*/
+// Gotchas
+// -------
+//
+// if you delete from a kbtree while iterating over it you must use
+// kb_del_itr and not kb_del otherwise the iterator might point to freed memory.
+
#ifndef NVIM_LIB_KBTREE_H
#define NVIM_LIB_KBTREE_H
diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c
index 9665655e74..44fe60e9c8 100644
--- a/src/nvim/lua/converter.c
+++ b/src/nvim/lua/converter.c
@@ -377,6 +377,19 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv)
nlua_pop_typval_table_processing_end:
break;
}
+ case LUA_TUSERDATA: {
+ nlua_pushref(lstate, nlua_nil_ref);
+ bool is_nil = lua_rawequal(lstate, -2, -1);
+ lua_pop(lstate, 1);
+ if (is_nil) {
+ cur.tv->v_type = VAR_SPECIAL;
+ cur.tv->vval.v_special = kSpecialVarNull;
+ } else {
+ EMSG(_("E5101: Cannot convert given lua type"));
+ ret = false;
+ }
+ break;
+ }
default: {
EMSG(_("E5101: Cannot convert given lua type"));
ret = false;
@@ -401,10 +414,18 @@ nlua_pop_typval_table_processing_end:
return ret;
}
+static bool typval_conv_special = false;
+
#define TYPVAL_ENCODE_ALLOW_SPECIALS true
#define TYPVAL_ENCODE_CONV_NIL(tv) \
- lua_pushnil(lstate)
+ do { \
+ if (typval_conv_special) { \
+ lua_pushnil(lstate); \
+ } else { \
+ nlua_pushref(lstate, nlua_nil_ref); \
+ } \
+ } while (0)
#define TYPVAL_ENCODE_CONV_BOOL(tv, num) \
lua_pushboolean(lstate, (bool)(num))
@@ -439,7 +460,13 @@ nlua_pop_typval_table_processing_end:
lua_createtable(lstate, 0, 0)
#define TYPVAL_ENCODE_CONV_EMPTY_DICT(tv, dict) \
- nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary)
+ do { \
+ if (typval_conv_special) { \
+ nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary); \
+ } else { \
+ lua_createtable(lstate, 0, 0); \
+ } \
+ } while (0)
#define TYPVAL_ENCODE_CONV_LIST_START(tv, len) \
do { \
@@ -548,9 +575,11 @@ nlua_pop_typval_table_processing_end:
/// @param[in] tv typval_T to convert.
///
/// @return true in case of success, false otherwise.
-bool nlua_push_typval(lua_State *lstate, typval_T *const tv)
+bool nlua_push_typval(lua_State *lstate, typval_T *const tv, bool special)
{
+ typval_conv_special = special;
const int initial_size = lua_gettop(lstate);
+
if (!lua_checkstack(lstate, initial_size + 2)) {
emsgf(_("E1502: Lua failed to grow stack to %i"), initial_size + 4);
return false;
@@ -708,7 +737,11 @@ void nlua_push_Object(lua_State *lstate, const Object obj, bool special)
{
switch (obj.type) {
case kObjectTypeNil: {
- lua_pushnil(lstate);
+ if (special) {
+ lua_pushnil(lstate);
+ } else {
+ nlua_pushref(lstate, nlua_nil_ref);
+ }
break;
}
case kObjectTypeLuaRef: {
@@ -1142,6 +1175,19 @@ Object nlua_pop_Object(lua_State *const lstate, bool ref, Error *const err)
break;
}
+ case LUA_TUSERDATA: {
+ nlua_pushref(lstate, nlua_nil_ref);
+ bool is_nil = lua_rawequal(lstate, -2, -1);
+ lua_pop(lstate, 1);
+ if (is_nil) {
+ *cur.obj = NIL;
+ } else {
+ api_set_error(err, kErrorTypeValidation,
+ "Cannot convert userdata");
+ }
+ break;
+ }
+
default: {
type_error:
api_set_error(err, kErrorTypeValidation,
diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c
index 127458fe39..093c130c5f 100644
--- a/src/nvim/lua/executor.c
+++ b/src/nvim/lua/executor.c
@@ -12,6 +12,7 @@
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/vim.h"
+#include "nvim/msgpack_rpc/channel.h"
#include "nvim/vim.h"
#include "nvim/ex_getln.h"
#include "nvim/ex_cmds2.h"
@@ -47,9 +48,6 @@ typedef struct {
# include "lua/executor.c.generated.h"
#endif
-/// Name of the run code for use in messages
-#define NLUA_EVAL_NAME "<VimL compiled string>"
-
/// Convert lua error into a Vim error message
///
/// @param lstate Lua interpreter state.
@@ -295,6 +293,17 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
// in_fast_event
lua_pushcfunction(lstate, &nlua_in_fast_event);
lua_setfield(lstate, -2, "in_fast_event");
+ // call
+ lua_pushcfunction(lstate, &nlua_call);
+ lua_setfield(lstate, -2, "call");
+
+ // rpcrequest
+ lua_pushcfunction(lstate, &nlua_rpcrequest);
+ lua_setfield(lstate, -2, "rpcrequest");
+
+ // rpcnotify
+ lua_pushcfunction(lstate, &nlua_rpcnotify);
+ lua_setfield(lstate, -2, "rpcnotify");
// vim.loop
luv_set_loop(lstate, &main_loop.uv);
@@ -311,6 +320,15 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
lua_setfield(lstate, -2, "luv");
lua_pop(lstate, 3);
+ // vim.NIL
+ lua_newuserdata(lstate, 0);
+ lua_createtable(lstate, 0, 0);
+ lua_pushcfunction(lstate, &nlua_nil_tostring);
+ lua_setfield(lstate, -2, "__tostring");
+ lua_setmetatable(lstate, -2);
+ nlua_nil_ref = nlua_ref(lstate, -1);
+ lua_setfield(lstate, -2, "NIL");
+
// internal vim._treesitter... API
nlua_add_treesitter(lstate);
@@ -376,29 +394,6 @@ static lua_State *nlua_enter(void)
return lstate;
}
-/// Execute lua string
-///
-/// @param[in] str String to execute.
-/// @param[out] ret_tv Location where result will be saved.
-///
-/// @return Result of the execution.
-void executor_exec_lua(const String str, typval_T *const ret_tv)
- FUNC_ATTR_NONNULL_ALL
-{
- lua_State *const lstate = nlua_enter();
-
- if (luaL_loadbuffer(lstate, str.data, str.size, NLUA_EVAL_NAME)) {
- nlua_error(lstate, _("E5104: Error while creating lua chunk: %.*s"));
- return;
- }
- if (lua_pcall(lstate, 0, 1, 0)) {
- nlua_error(lstate, _("E5105: Error while calling lua chunk: %.*s"));
- return;
- }
-
- nlua_pop_typval(lstate, ret_tv);
-}
-
static void nlua_print_event(void **argv)
{
char *str = argv[0];
@@ -539,6 +534,125 @@ int nlua_in_fast_event(lua_State *lstate)
return 1;
}
+int nlua_call(lua_State *lstate)
+{
+ Error err = ERROR_INIT;
+ size_t name_len;
+ const char_u *name = (const char_u *)luaL_checklstring(lstate, 1, &name_len);
+ if (!nlua_is_deferred_safe(lstate)) {
+ return luaL_error(lstate, e_luv_api_disabled, "vimL function");
+ }
+
+ int nargs = lua_gettop(lstate)-1;
+ if (nargs > MAX_FUNC_ARGS) {
+ return luaL_error(lstate, "Function called with too many arguments");
+ }
+
+ typval_T vim_args[MAX_FUNC_ARGS + 1];
+ int i = 0; // also used for freeing the variables
+ for (; i < nargs; i++) {
+ lua_pushvalue(lstate, (int)i+2);
+ if (!nlua_pop_typval(lstate, &vim_args[i])) {
+ api_set_error(&err, kErrorTypeException,
+ "error converting argument %d", i+1);
+ goto free_vim_args;
+ }
+ }
+
+ TRY_WRAP({
+ // TODO(bfredl): this should be simplified in error handling refactor
+ force_abort = false;
+ suppress_errthrow = false;
+ current_exception = NULL;
+ did_emsg = false;
+
+ try_start();
+ typval_T rettv;
+ int dummy;
+ // call_func() retval is deceptive, ignore it. Instead we set `msg_list`
+ // (TRY_WRAP) to capture abort-causing non-exception errors.
+ (void)call_func(name, (int)name_len, &rettv, nargs,
+ vim_args, NULL, curwin->w_cursor.lnum, curwin->w_cursor.lnum,
+ &dummy, true, NULL, NULL);
+ if (!try_end(&err)) {
+ nlua_push_typval(lstate, &rettv, false);
+ }
+ tv_clear(&rettv);
+ });
+
+free_vim_args:
+ while (i > 0) {
+ tv_clear(&vim_args[--i]);
+ }
+ if (ERROR_SET(&err)) {
+ lua_pushstring(lstate, err.msg);
+ api_clear_error(&err);
+ return lua_error(lstate);
+ }
+ return 1;
+}
+
+static int nlua_rpcrequest(lua_State *lstate)
+{
+ if (!nlua_is_deferred_safe(lstate)) {
+ return luaL_error(lstate, e_luv_api_disabled, "rpcrequest");
+ }
+ return nlua_rpc(lstate, true);
+}
+
+static int nlua_rpcnotify(lua_State *lstate)
+{
+ return nlua_rpc(lstate, false);
+}
+
+static int nlua_rpc(lua_State *lstate, bool request)
+{
+ size_t name_len;
+ uint64_t chan_id = (uint64_t)luaL_checkinteger(lstate, 1);
+ const char *name = luaL_checklstring(lstate, 2, &name_len);
+ int nargs = lua_gettop(lstate)-2;
+ Error err = ERROR_INIT;
+ Array args = ARRAY_DICT_INIT;
+
+ for (int i = 0; i < nargs; i++) {
+ lua_pushvalue(lstate, (int)i+3);
+ ADD(args, nlua_pop_Object(lstate, false, &err));
+ if (ERROR_SET(&err)) {
+ api_free_array(args);
+ goto check_err;
+ }
+ }
+
+ if (request) {
+ Object result = rpc_send_call(chan_id, name, args, &err);
+ if (!ERROR_SET(&err)) {
+ nlua_push_Object(lstate, result, false);
+ api_free_object(result);
+ }
+ } else {
+ if (!rpc_send_event(chan_id, name, args)) {
+ api_set_error(&err, kErrorTypeValidation,
+ "Invalid channel: %"PRIu64, chan_id);
+ }
+ }
+
+check_err:
+ if (ERROR_SET(&err)) {
+ lua_pushstring(lstate, err.msg);
+ api_clear_error(&err);
+ return lua_error(lstate);
+ }
+
+ return request ? 1 : 0;
+}
+
+static int nlua_nil_tostring(lua_State *lstate)
+{
+ lua_pushstring(lstate, "vim.NIL");
+ return 1;
+}
+
+
#ifdef WIN32
/// os.getenv: override os.getenv to maintain coherency. #9681
///
@@ -592,10 +706,6 @@ void executor_eval_lua(const String str, typval_T *const arg,
typval_T *const ret_tv)
FUNC_ATTR_NONNULL_ALL
{
- lua_State *const lstate = nlua_enter();
-
- garray_T str_ga;
- ga_init(&str_ga, 1, 80);
#define EVALHEADER "local _A=select(1,...) return ("
const size_t lcmd_len = sizeof(EVALHEADER) - 1 + str.size + 1;
char *lcmd;
@@ -608,30 +718,71 @@ void executor_eval_lua(const String str, typval_T *const arg,
memcpy(lcmd + sizeof(EVALHEADER) - 1, str.data, str.size);
lcmd[lcmd_len - 1] = ')';
#undef EVALHEADER
- if (luaL_loadbuffer(lstate, lcmd, lcmd_len, NLUA_EVAL_NAME)) {
- nlua_error(lstate,
- _("E5107: Error while creating lua chunk for luaeval(): %.*s"));
- if (lcmd != (char *)IObuff) {
- xfree(lcmd);
- }
- return;
- }
+ typval_exec_lua(lcmd, lcmd_len, "luaeval()", arg, 1, true, ret_tv);
+
if (lcmd != (char *)IObuff) {
xfree(lcmd);
}
+}
- if (arg->v_type == VAR_UNKNOWN) {
- lua_pushnil(lstate);
+void executor_call_lua(const char *str, size_t len, typval_T *const args,
+ int argcount, typval_T *ret_tv)
+ FUNC_ATTR_NONNULL_ALL
+{
+#define CALLHEADER "return "
+#define CALLSUFFIX "(...)"
+ const size_t lcmd_len = sizeof(CALLHEADER) - 1 + len + sizeof(CALLSUFFIX) - 1;
+ char *lcmd;
+ if (lcmd_len < IOSIZE) {
+ lcmd = (char *)IObuff;
} else {
- nlua_push_typval(lstate, arg);
+ lcmd = xmalloc(lcmd_len);
+ }
+ memcpy(lcmd, CALLHEADER, sizeof(CALLHEADER) - 1);
+ memcpy(lcmd + sizeof(CALLHEADER) - 1, str, len);
+ memcpy(lcmd + sizeof(CALLHEADER) - 1 + len, CALLSUFFIX,
+ sizeof(CALLSUFFIX) - 1);
+#undef CALLHEADER
+#undef CALLSUFFIX
+
+ typval_exec_lua(lcmd, lcmd_len, "v:lua", args, argcount, false, ret_tv);
+
+ if (lcmd != (char *)IObuff) {
+ xfree(lcmd);
+ }
+}
+
+static void typval_exec_lua(const char *lcmd, size_t lcmd_len, const char *name,
+ typval_T *const args, int argcount, bool special,
+ typval_T *ret_tv)
+{
+ if (check_restricted() || check_secure()) {
+ ret_tv->v_type = VAR_NUMBER;
+ ret_tv->vval.v_number = 0;
+ return;
+ }
+
+ lua_State *const lstate = nlua_enter();
+ if (luaL_loadbuffer(lstate, lcmd, lcmd_len, name)) {
+ nlua_error(lstate, _("E5107: Error loading lua %.*s"));
+ return;
}
- if (lua_pcall(lstate, 1, 1, 0)) {
- nlua_error(lstate,
- _("E5108: Error while calling lua chunk for luaeval(): %.*s"));
+
+ for (int i = 0; i < argcount; i++) {
+ if (args[i].v_type == VAR_UNKNOWN) {
+ lua_pushnil(lstate);
+ } else {
+ nlua_push_typval(lstate, &args[i], special);
+ }
+ }
+ if (lua_pcall(lstate, argcount, ret_tv ? 1 : 0, 0)) {
+ nlua_error(lstate, _("E5108: Error executing lua %.*s"));
return;
}
- nlua_pop_typval(lstate, ret_tv);
+ if (ret_tv) {
+ nlua_pop_typval(lstate, ret_tv);
+ }
}
/// Execute lua string
@@ -717,9 +868,8 @@ void ex_lua(exarg_T *const eap)
xfree(code);
return;
}
- typval_T tv = { .v_type = VAR_UNKNOWN };
- executor_exec_lua((String) { .data = code, .size = len }, &tv);
- tv_clear(&tv);
+ typval_exec_lua(code, len, ":lua", NULL, 0, false, NULL);
+
xfree(code);
}
@@ -757,8 +907,8 @@ void ex_luado(exarg_T *const eap)
#undef DOSTART
#undef DOEND
- if (luaL_loadbuffer(lstate, lcmd, lcmd_len, NLUA_EVAL_NAME)) {
- nlua_error(lstate, _("E5109: Error while creating lua chunk: %.*s"));
+ if (luaL_loadbuffer(lstate, lcmd, lcmd_len, ":luado")) {
+ nlua_error(lstate, _("E5109: Error loading lua: %.*s"));
if (lcmd_len >= IOSIZE) {
xfree(lcmd);
}
@@ -768,7 +918,7 @@ void ex_luado(exarg_T *const eap)
xfree(lcmd);
}
if (lua_pcall(lstate, 0, 1, 0)) {
- nlua_error(lstate, _("E5110: Error while creating lua function: %.*s"));
+ nlua_error(lstate, _("E5110: Error executing lua: %.*s"));
return;
}
for (linenr_T l = eap->line1; l <= eap->line2; l++) {
@@ -779,7 +929,7 @@ void ex_luado(exarg_T *const eap)
lua_pushstring(lstate, (const char *)ml_get_buf(curbuf, l, false));
lua_pushnumber(lstate, (lua_Number)l);
if (lua_pcall(lstate, 2, 1, 0)) {
- nlua_error(lstate, _("E5111: Error while calling lua function: %.*s"));
+ nlua_error(lstate, _("E5111: Error calling lua: %.*s"));
break;
}
if (lua_isstring(lstate, -1)) {
diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h
index 8d356a5600..32f66b629c 100644
--- a/src/nvim/lua/executor.h
+++ b/src/nvim/lua/executor.h
@@ -12,6 +12,8 @@
// Generated by msgpack-gen.lua
void nlua_add_api_functions(lua_State *lstate) REAL_FATTR_NONNULL_ALL;
+EXTERN LuaRef nlua_nil_ref INIT(= LUA_NOREF);
+
#define set_api_error(s, err) \
do { \
Error *err_ = (err); \
diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua
index b67762e48e..1665a55aff 100644
--- a/src/nvim/lua/vim.lua
+++ b/src/nvim/lua/vim.lua
@@ -165,6 +165,19 @@ end
--- Paste handler, invoked by |nvim_paste()| when a conforming UI
--- (such as the |TUI|) pastes text into the editor.
---
+--- Example: To remove ANSI color codes when pasting:
+--- <pre>
+--- vim.paste = (function(overridden)
+--- return function(lines, phase)
+--- for i,line in ipairs(lines) do
+--- -- Scrub ANSI color codes from paste input.
+--- lines[i] = line:gsub('\27%[[0-9;mK]+', '')
+--- end
+--- overridden(lines, phase)
+--- end
+--- end)(vim.paste)
+--- </pre>
+---
--@see |paste|
---
--@param lines |readfile()|-style list of lines to paste. |channel-lines|
@@ -192,8 +205,11 @@ paste = (function()
local line1 = lines[1]:gsub('<', '<lt>'):gsub('[\r\n\012\027]', ' ') -- Scrub.
vim.api.nvim_input(line1)
vim.api.nvim_set_option('paste', false)
- elseif mode ~= 'c' then -- Else: discard remaining cmdline-mode chunks.
- if phase < 2 and mode ~= 'i' and mode ~= 'R' and mode ~= 't' then
+ elseif mode ~= 'c' then
+ if phase < 2 and mode:find('^[vV\22sS\19]') then
+ vim.api.nvim_command([[exe "normal! \<Del>"]])
+ vim.api.nvim_put(lines, 'c', false, true)
+ elseif phase < 2 and not mode:find('^[iRt]') then
vim.api.nvim_put(lines, 'c', true, true)
-- XXX: Normal-mode: workaround bad cursor-placement after first chunk.
vim.api.nvim_command('normal! a')
@@ -239,8 +255,26 @@ local function __index(t, key)
-- Expose all `vim.shared` functions on the `vim` module.
t[key] = require('vim.shared')[key]
return t[key]
+ elseif require('vim.uri')[key] ~= nil then
+ -- Expose all `vim.uri` functions on the `vim` module.
+ t[key] = require('vim.uri')[key]
+ return t[key]
+ elseif key == 'lsp' then
+ t.lsp = require('vim.lsp')
+ return t.lsp
+ end
+end
+
+
+-- vim.fn.{func}(...)
+local function _fn_index(t, key)
+ local function _fn(...)
+ return vim.call(key, ...)
end
+ t[key] = _fn
+ return _fn
end
+local fn = setmetatable({}, {__index=_fn_index})
local module = {
_update_package_paths = _update_package_paths,
@@ -249,6 +283,7 @@ local module = {
_system = _system,
paste = paste,
schedule_wrap = schedule_wrap,
+ fn=fn,
}
setmetatable(module, {
diff --git a/src/nvim/main.c b/src/nvim/main.c
index e0a1e60fc0..e39eec4038 100644
--- a/src/nvim/main.c
+++ b/src/nvim/main.c
@@ -27,6 +27,7 @@
#include "nvim/highlight.h"
#include "nvim/iconv.h"
#include "nvim/if_cscope.h"
+#include "nvim/lua/executor.h"
#ifdef HAVE_LOCALE_H
# include <locale.h>
#endif
diff --git a/src/nvim/mark.c b/src/nvim/mark.c
index e8f1651a6e..e5070f23ff 100644
--- a/src/nvim/mark.c
+++ b/src/nvim/mark.c
@@ -296,17 +296,17 @@ pos_T *movechangelist(int count)
* - NULL if there is no mark called 'c'.
* - -1 if mark is in other file and jumped there (only if changefile is TRUE)
*/
-pos_T *getmark_buf(buf_T *buf, int c, int changefile)
+pos_T *getmark_buf(buf_T *buf, int c, bool changefile)
{
return getmark_buf_fnum(buf, c, changefile, NULL);
}
-pos_T *getmark(int c, int changefile)
+pos_T *getmark(int c, bool changefile)
{
return getmark_buf_fnum(curbuf, c, changefile, NULL);
}
-pos_T *getmark_buf_fnum(buf_T *buf, int c, int changefile, int *fnum)
+pos_T *getmark_buf_fnum(buf_T *buf, int c, bool changefile, int *fnum)
{
pos_T *posp;
pos_T *startp, *endp;
@@ -905,9 +905,10 @@ void mark_adjust(linenr_T line1,
linenr_T line2,
long amount,
long amount_after,
- bool end_temp)
+ bool end_temp,
+ ExtmarkOp op)
{
- mark_adjust_internal(line1, line2, amount, amount_after, true, end_temp);
+ mark_adjust_internal(line1, line2, amount, amount_after, true, end_temp, op);
}
// mark_adjust_nofold() does the same as mark_adjust() but without adjusting
@@ -916,14 +917,16 @@ void mark_adjust(linenr_T line1,
// calling foldMarkAdjust() with arguments line1, line2, amount, amount_after,
// for an example of why this may be necessary, see do_move().
void mark_adjust_nofold(linenr_T line1, linenr_T line2, long amount,
- long amount_after, bool end_temp)
+ long amount_after, bool end_temp,
+ ExtmarkOp op)
{
- mark_adjust_internal(line1, line2, amount, amount_after, false, end_temp);
+ mark_adjust_internal(line1, line2, amount, amount_after, false, end_temp, op);
}
static void mark_adjust_internal(linenr_T line1, linenr_T line2,
long amount, long amount_after,
- bool adjust_folds, bool end_temp)
+ bool adjust_folds, bool end_temp,
+ ExtmarkOp op)
{
int i;
int fnum = curbuf->b_fnum;
@@ -979,6 +982,9 @@ static void mark_adjust_internal(linenr_T line1, linenr_T line2,
sign_mark_adjust(line1, line2, amount, amount_after);
bufhl_mark_adjust(curbuf, line1, line2, amount, amount_after, end_temp);
+ if (op != kExtmarkNOOP) {
+ extmark_adjust(curbuf, line1, line2, amount, amount_after, op, end_temp);
+ }
}
/* previous context mark */
@@ -1090,7 +1096,7 @@ static void mark_adjust_internal(linenr_T line1, linenr_T line2,
// cursor is inside them.
void mark_col_adjust(
linenr_T lnum, colnr_T mincol, long lnum_amount, long col_amount,
- int spaces_removed)
+ int spaces_removed, ExtmarkOp op)
{
int i;
int fnum = curbuf->b_fnum;
@@ -1110,6 +1116,13 @@ void mark_col_adjust(
col_adjust(&(namedfm[i].fmark.mark));
}
+ // Extmarks
+ if (op != kExtmarkNOOP) {
+ // TODO(timeyyy): consider spaces_removed? (behave like a delete)
+ extmark_col_adjust(curbuf, lnum, mincol, lnum_amount, col_amount,
+ kExtmarkUndo);
+ }
+
/* last Insert position */
col_adjust(&(curbuf->b_last_insert.mark));
diff --git a/src/nvim/mark_extended.c b/src/nvim/mark_extended.c
new file mode 100644
index 0000000000..01745f484d
--- /dev/null
+++ b/src/nvim/mark_extended.c
@@ -0,0 +1,1135 @@
+// This is an open source non-commercial project. Dear PVS-Studio, please check
+// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+
+// Implements extended marks for plugins. Each mark exists in a btree of
+// lines containing btrees of columns.
+//
+// The btree provides efficent range lookups.
+// A map of pointers to the marks is used for fast lookup by mark id.
+//
+// Marks are moved by calls to: extmark_col_adjust, extmark_adjust, or
+// extmark_col_adjust_delete which are based on col_adjust and mark_adjust from
+// mark.c
+//
+// Undo/Redo of marks is implemented by storing the call arguments to
+// extmark_col_adjust or extmark_adjust. The list of arguments
+// is applied in extmark_apply_undo. The only case where we have to
+// copy extmarks is for the area being effected by a delete.
+//
+// Marks live in namespaces that allow plugins/users to segregate marks
+// from other users.
+//
+// For possible ideas for efficency improvements see:
+// http://blog.atom.io/2015/06/16/optimizing-an-important-atom-primitive.html
+// TODO(bfredl): These ideas could be used for an enhanced btree, which
+// wouldn't need separate line and column layers.
+// Other implementations exist in gtk and tk toolkits.
+//
+// Deleting marks only happens when explicitly calling extmark_del, deleteing
+// over a range of marks will only move the marks. Deleting on a mark will
+// leave it in same position unless it is on the EOL of a line.
+
+#include <assert.h>
+#include "nvim/vim.h"
+#include "charset.h"
+#include "nvim/mark_extended.h"
+#include "nvim/memline.h"
+#include "nvim/pos.h"
+#include "nvim/globals.h"
+#include "nvim/map.h"
+#include "nvim/lib/kbtree.h"
+#include "nvim/undo.h"
+#include "nvim/buffer.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "mark_extended.c.generated.h"
+#endif
+
+
+/// Create or update an extmark
+///
+/// must not be used during iteration!
+/// @returns whether a new mark was created
+int extmark_set(buf_T *buf, uint64_t ns, uint64_t id,
+ linenr_T lnum, colnr_T col, ExtmarkOp op)
+{
+ Extmark *extmark = extmark_from_id(buf, ns, id);
+ if (!extmark) {
+ extmark_create(buf, ns, id, lnum, col, op);
+ return true;
+ } else {
+ ExtmarkLine *extmarkline = extmark->line;
+ extmark_update(extmark, buf, ns, id, lnum, col, op, NULL);
+ if (kb_size(&extmarkline->items) == 0) {
+ kb_del(extmarklines, &buf->b_extlines, extmarkline);
+ extmarkline_free(extmarkline);
+ }
+ return false;
+ }
+}
+
+// Remove an extmark
+// Returns 0 on missing id
+int extmark_del(buf_T *buf, uint64_t ns, uint64_t id, ExtmarkOp op)
+{
+ Extmark *extmark = extmark_from_id(buf, ns, id);
+ if (!extmark) {
+ return 0;
+ }
+ return extmark_delete(extmark, buf, ns, id, op);
+}
+
+// Free extmarks in a ns between lines
+// if ns = 0, it means clear all namespaces
+void extmark_clear(buf_T *buf, uint64_t ns,
+ linenr_T l_lnum, linenr_T u_lnum, ExtmarkOp undo)
+{
+ if (!buf->b_extmark_ns) {
+ return;
+ }
+
+ bool marks_cleared = false;
+ if (undo == kExtmarkUndo) {
+ // Copy marks that would be effected by clear
+ u_extmark_copy(buf, ns, l_lnum, 0, u_lnum, MAXCOL);
+ }
+
+ bool all_ns = ns == 0 ? true : false;
+ ExtmarkNs *ns_obj;
+ if (!all_ns) {
+ ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ if (!ns_obj) {
+ // nothing to do
+ return;
+ }
+ }
+
+ FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, {
+ FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, 0, MAXCOL, {
+ if (extmark->ns_id == ns || all_ns) {
+ marks_cleared = true;
+ if (all_ns) {
+ ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, extmark->ns_id);
+ } else {
+ ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ }
+ pmap_del(uint64_t)(ns_obj->map, extmark->mark_id);
+ kb_del_itr(markitems, &extmarkline->items, &mitr);
+ }
+ });
+ if (kb_size(&extmarkline->items) == 0) {
+ kb_del_itr(extmarklines, &buf->b_extlines, &itr);
+ extmarkline_free(extmarkline);
+ }
+ });
+
+ // Record the undo for the actual move
+ if (marks_cleared && undo == kExtmarkUndo) {
+ u_extmark_clear(buf, ns, l_lnum, u_lnum);
+ }
+}
+
+// Returns the position of marks between a range,
+// marks found at the start or end index will be included,
+// if upper_lnum or upper_col are negative the buffer
+// will be searched to the start, or end
+// dir can be set to control the order of the array
+// amount = amount of marks to find or -1 for all
+ExtmarkArray extmark_get(buf_T *buf, uint64_t ns,
+ linenr_T l_lnum, colnr_T l_col,
+ linenr_T u_lnum, colnr_T u_col,
+ int64_t amount, bool reverse)
+{
+ ExtmarkArray array = KV_INITIAL_VALUE;
+ // Find all the marks
+ if (!reverse) {
+ FOR_ALL_EXTMARKS(buf, ns, l_lnum, l_col, u_lnum, u_col, {
+ if (extmark->ns_id == ns) {
+ kv_push(array, extmark);
+ if (kv_size(array) == (size_t)amount) {
+ return array;
+ }
+ }
+ })
+ } else {
+ FOR_ALL_EXTMARKS_PREV(buf, ns, l_lnum, l_col, u_lnum, u_col, {
+ if (extmark->ns_id == ns) {
+ kv_push(array, extmark);
+ if (kv_size(array) == (size_t)amount) {
+ return array;
+ }
+ }
+ })
+ }
+ return array;
+}
+
+static void extmark_create(buf_T *buf, uint64_t ns, uint64_t id,
+ linenr_T lnum, colnr_T col, ExtmarkOp op)
+{
+ if (!buf->b_extmark_ns) {
+ buf->b_extmark_ns = pmap_new(uint64_t)();
+ }
+ ExtmarkNs *ns_obj = NULL;
+ ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ // Initialize a new namespace for this buffer
+ if (!ns_obj) {
+ ns_obj = xmalloc(sizeof(ExtmarkNs));
+ ns_obj->map = pmap_new(uint64_t)();
+ pmap_put(uint64_t)(buf->b_extmark_ns, ns, ns_obj);
+ }
+
+ // Create or get a line
+ ExtmarkLine *extmarkline = extmarkline_ref(buf, lnum, true);
+ // Create and put mark on the line
+ extmark_put(col, id, extmarkline, ns);
+
+ // Marks do not have stable address so we have to look them up
+ // by using the line instead of the mark
+ pmap_put(uint64_t)(ns_obj->map, id, extmarkline);
+ if (op != kExtmarkNoUndo) {
+ u_extmark_set(buf, ns, id, lnum, col, kExtmarkSet);
+ }
+
+ // Set a free id so extmark_free_id_get works
+ extmark_free_id_set(ns_obj, id);
+}
+
+// update the position of an extmark
+// to update while iterating pass the markitems itr
+static void extmark_update(Extmark *extmark, buf_T *buf,
+ uint64_t ns, uint64_t id,
+ linenr_T lnum, colnr_T col,
+ ExtmarkOp op, kbitr_t(markitems) *mitr)
+{
+ assert(op != kExtmarkNOOP);
+ if (op != kExtmarkNoUndo) {
+ u_extmark_update(buf, ns, id, extmark->line->lnum, extmark->col,
+ lnum, col);
+ }
+ ExtmarkLine *old_line = extmark->line;
+ // Move the mark to a new line and update column
+ if (old_line->lnum != lnum) {
+ ExtmarkLine *ref_line = extmarkline_ref(buf, lnum, true);
+ extmark_put(col, id, ref_line, ns);
+ // Update the hashmap
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ pmap_put(uint64_t)(ns_obj->map, id, ref_line);
+ // Delete old mark
+ if (mitr != NULL) {
+ kb_del_itr(markitems, &(old_line->items), mitr);
+ } else {
+ kb_del(markitems, &old_line->items, *extmark);
+ }
+ // Just update the column
+ } else {
+ if (mitr != NULL) {
+ // The btree stays organized during iteration with kbitr_t
+ extmark->col = col;
+ } else {
+ // Keep the btree in order
+ kb_del(markitems, &old_line->items, *extmark);
+ extmark_put(col, id, old_line, ns);
+ }
+ }
+}
+
+static int extmark_delete(Extmark *extmark,
+ buf_T *buf,
+ uint64_t ns,
+ uint64_t id,
+ ExtmarkOp op)
+{
+ if (op != kExtmarkNoUndo) {
+ u_extmark_set(buf, ns, id, extmark->line->lnum, extmark->col,
+ kExtmarkDel);
+ }
+
+ // Remove our key from the namespace
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ pmap_del(uint64_t)(ns_obj->map, id);
+
+ // Remove the mark mark from the line
+ ExtmarkLine *extmarkline = extmark->line;
+ kb_del(markitems, &extmarkline->items, *extmark);
+ // Remove the line if there are no more marks in the line
+ if (kb_size(&extmarkline->items) == 0) {
+ kb_del(extmarklines, &buf->b_extlines, extmarkline);
+ extmarkline_free(extmarkline);
+ }
+ return true;
+}
+
+// Lookup an extmark by id
+Extmark *extmark_from_id(buf_T *buf, uint64_t ns, uint64_t id)
+{
+ if (!buf->b_extmark_ns) {
+ return NULL;
+ }
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ if (!ns_obj || !kh_size(ns_obj->map->table)) {
+ return NULL;
+ }
+ ExtmarkLine *extmarkline = pmap_get(uint64_t)(ns_obj->map, id);
+ if (!extmarkline) {
+ return NULL;
+ }
+
+ FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, 0, MAXCOL, {
+ if (extmark->ns_id == ns
+ && extmark->mark_id == id) {
+ return extmark;
+ }
+ })
+ return NULL;
+}
+
+// Lookup an extmark by position
+Extmark *extmark_from_pos(buf_T *buf, uint64_t ns, linenr_T lnum, colnr_T col)
+{
+ if (!buf->b_extmark_ns) {
+ return NULL;
+ }
+ FOR_ALL_EXTMARKS(buf, ns, lnum, col, lnum, col, {
+ if (extmark->ns_id == ns) {
+ if (extmark->col == col) {
+ return extmark;
+ }
+ }
+ })
+ return NULL;
+}
+
+// Returns an avaliable id in a namespace
+uint64_t extmark_free_id_get(buf_T *buf, uint64_t ns)
+{
+ if (!buf->b_extmark_ns) {
+ return 1;
+ }
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ if (!ns_obj) {
+ return 1;
+ }
+ return ns_obj->free_id;
+}
+
+// Set the next free id in a namesapce
+static void extmark_free_id_set(ExtmarkNs *ns_obj, uint64_t id)
+{
+ // Simply Heurstic, the largest id + 1
+ ns_obj->free_id = id + 1;
+}
+
+// free extmarks from the buffer
+void extmark_free_all(buf_T *buf)
+{
+ if (!buf->b_extmark_ns) {
+ return;
+ }
+
+ uint64_t ns;
+ ExtmarkNs *ns_obj;
+
+ FOR_ALL_EXTMARKLINES(buf, 1, MAXLNUM, {
+ kb_del_itr(extmarklines, &buf->b_extlines, &itr);
+ extmarkline_free(extmarkline);
+ })
+
+ map_foreach(buf->b_extmark_ns, ns, ns_obj, {
+ (void)ns;
+ pmap_free(uint64_t)(ns_obj->map);
+ xfree(ns_obj);
+ });
+
+ pmap_free(uint64_t)(buf->b_extmark_ns);
+ buf->b_extmark_ns = NULL;
+
+ // k?_init called to set pointers to NULL
+ kb_destroy(extmarklines, (&buf->b_extlines));
+ kb_init(&buf->b_extlines);
+
+ kv_destroy(buf->b_extmark_move_space);
+ kv_init(buf->b_extmark_move_space);
+}
+
+
+// Save info for undo/redo of set marks
+static void u_extmark_set(buf_T *buf, uint64_t ns, uint64_t id,
+ linenr_T lnum, colnr_T col, UndoObjectType undo_type)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ExtmarkSet set;
+ set.ns_id = ns;
+ set.mark_id = id;
+ set.lnum = lnum;
+ set.col = col;
+
+ ExtmarkUndoObject undo = { .type = undo_type,
+ .data.set = set };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// Save info for undo/redo of deleted marks
+static void u_extmark_update(buf_T *buf, uint64_t ns, uint64_t id,
+ linenr_T old_lnum, colnr_T old_col,
+ linenr_T lnum, colnr_T col)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ExtmarkUpdate update;
+ update.ns_id = ns;
+ update.mark_id = id;
+ update.old_lnum = old_lnum;
+ update.old_col = old_col;
+ update.lnum = lnum;
+ update.col = col;
+
+ ExtmarkUndoObject undo = { .type = kExtmarkUpdate,
+ .data.update = update };
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// Hueristic works only for when the user is typing in insert mode
+// - Instead of 1 undo object for each char inserted,
+// we create 1 undo objet for all text inserted before the user hits esc
+// Return True if we compacted else False
+static bool u_compact_col_adjust(buf_T *buf, linenr_T lnum, colnr_T mincol,
+ long lnum_amount, long col_amount)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return false;
+ }
+
+ if (kv_size(uhp->uh_extmark) < 1) {
+ return false;
+ }
+ // Check the last action
+ ExtmarkUndoObject object = kv_last(uhp->uh_extmark);
+
+ if (object.type != kColAdjust) {
+ return false;
+ }
+ ColAdjust undo = object.data.col_adjust;
+ bool compactable = false;
+
+ if (!undo.lnum_amount && !lnum_amount) {
+ if (undo.lnum == lnum) {
+ if ((undo.mincol + undo.col_amount) >= mincol) {
+ compactable = true;
+ } } }
+
+ if (!compactable) {
+ return false;
+ }
+
+ undo.col_amount = undo.col_amount + col_amount;
+ ExtmarkUndoObject new_undo = { .type = kColAdjust,
+ .data.col_adjust = undo };
+ kv_last(uhp->uh_extmark) = new_undo;
+ return true;
+}
+
+// Save col_adjust info so we can undo/redo
+void u_extmark_col_adjust(buf_T *buf, linenr_T lnum, colnr_T mincol,
+ long lnum_amount, long col_amount)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ if (!u_compact_col_adjust(buf, lnum, mincol, lnum_amount, col_amount)) {
+ ColAdjust col_adjust;
+ col_adjust.lnum = lnum;
+ col_adjust.mincol = mincol;
+ col_adjust.lnum_amount = lnum_amount;
+ col_adjust.col_amount = col_amount;
+
+ ExtmarkUndoObject undo = { .type = kColAdjust,
+ .data.col_adjust = col_adjust };
+
+ kv_push(uhp->uh_extmark, undo);
+ }
+}
+
+// Save col_adjust_delete info so we can undo/redo
+void u_extmark_col_adjust_delete(buf_T *buf, linenr_T lnum,
+ colnr_T mincol, colnr_T endcol, int eol)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ColAdjustDelete col_adjust_delete;
+ col_adjust_delete.lnum = lnum;
+ col_adjust_delete.mincol = mincol;
+ col_adjust_delete.endcol = endcol;
+ col_adjust_delete.eol = eol;
+
+ ExtmarkUndoObject undo = { .type = kColAdjustDelete,
+ .data.col_adjust_delete = col_adjust_delete };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// Save adjust info so we can undo/redo
+static void u_extmark_adjust(buf_T * buf, linenr_T line1, linenr_T line2,
+ long amount, long amount_after)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ Adjust adjust;
+ adjust.line1 = line1;
+ adjust.line2 = line2;
+ adjust.amount = amount;
+ adjust.amount_after = amount_after;
+
+ ExtmarkUndoObject undo = { .type = kLineAdjust,
+ .data.adjust = adjust };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// save info to undo/redo a :move
+void u_extmark_move(buf_T *buf, linenr_T line1, linenr_T line2,
+ linenr_T last_line, linenr_T dest, linenr_T num_lines,
+ linenr_T extra)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ AdjustMove move;
+ move.line1 = line1;
+ move.line2 = line2;
+ move.last_line = last_line;
+ move.dest = dest;
+ move.num_lines = num_lines;
+ move.extra = extra;
+
+ ExtmarkUndoObject undo = { .type = kAdjustMove,
+ .data.move = move };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// copy extmarks data between range, useful when we cannot simply reverse
+// the operation. This will do nothing on redo, enforces correct position when
+// undo.
+// if ns = 0, it means copy all namespaces
+void u_extmark_copy(buf_T *buf, uint64_t ns,
+ linenr_T l_lnum, colnr_T l_col,
+ linenr_T u_lnum, colnr_T u_col)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ bool all_ns = ns == 0 ? true : false;
+
+ ExtmarkCopy copy;
+ ExtmarkUndoObject undo;
+ FOR_ALL_EXTMARKS(buf, 1, l_lnum, l_col, u_lnum, u_col, {
+ if (all_ns || extmark->ns_id == ns) {
+ copy.ns_id = extmark->ns_id;
+ copy.mark_id = extmark->mark_id;
+ copy.lnum = extmark->line->lnum;
+ copy.col = extmark->col;
+
+ undo.data.copy = copy;
+ undo.type = kExtmarkCopy;
+ kv_push(uhp->uh_extmark, undo);
+ }
+ });
+}
+
+void u_extmark_copy_place(buf_T *buf,
+ linenr_T l_lnum, colnr_T l_col,
+ linenr_T u_lnum, colnr_T u_col,
+ linenr_T p_lnum, colnr_T p_col)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ExtmarkCopyPlace copy_place;
+ copy_place.l_lnum = l_lnum;
+ copy_place.l_col = l_col;
+ copy_place.u_lnum = u_lnum;
+ copy_place.u_col = u_col;
+ copy_place.p_lnum = p_lnum;
+ copy_place.p_col = p_col;
+
+ ExtmarkUndoObject undo = { .type = kExtmarkCopyPlace,
+ .data.copy_place = copy_place };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// Save info for undo/redo of extmark_clear
+static void u_extmark_clear(buf_T *buf, uint64_t ns,
+ linenr_T l_lnum, linenr_T u_lnum)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ExtmarkClear clear;
+ clear.ns_id = ns;
+ clear.l_lnum = l_lnum;
+ clear.u_lnum = u_lnum;
+
+ ExtmarkUndoObject undo = { .type = kExtmarkClear,
+ .data.clear = clear };
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// undo or redo an extmark operation
+void extmark_apply_undo(ExtmarkUndoObject undo_info, bool undo)
+{
+ linenr_T lnum;
+ colnr_T mincol;
+ long lnum_amount;
+ long col_amount;
+ linenr_T line1;
+ linenr_T line2;
+ long amount;
+ long amount_after;
+
+ // use extmark_col_adjust
+ if (undo_info.type == kColAdjust) {
+ // Undo
+ if (undo) {
+ lnum = (undo_info.data.col_adjust.lnum
+ + undo_info.data.col_adjust.lnum_amount);
+ lnum_amount = -undo_info.data.col_adjust.lnum_amount;
+ col_amount = -undo_info.data.col_adjust.col_amount;
+ mincol = (undo_info.data.col_adjust.mincol
+ + (colnr_T)undo_info.data.col_adjust.col_amount);
+ // Redo
+ } else {
+ lnum = undo_info.data.col_adjust.lnum;
+ col_amount = undo_info.data.col_adjust.col_amount;
+ lnum_amount = undo_info.data.col_adjust.lnum_amount;
+ mincol = undo_info.data.col_adjust.mincol;
+ }
+ extmark_col_adjust(curbuf,
+ lnum, mincol, lnum_amount, col_amount, kExtmarkNoUndo);
+ // use extmark_col_adjust_delete
+ } else if (undo_info.type == kColAdjustDelete) {
+ if (undo) {
+ mincol = undo_info.data.col_adjust_delete.mincol;
+ col_amount = (undo_info.data.col_adjust_delete.endcol
+ - undo_info.data.col_adjust_delete.mincol) + 1;
+ extmark_col_adjust(curbuf,
+ undo_info.data.col_adjust_delete.lnum,
+ mincol,
+ 0,
+ col_amount,
+ kExtmarkNoUndo);
+ // Redo
+ } else {
+ extmark_col_adjust_delete(curbuf,
+ undo_info.data.col_adjust_delete.lnum,
+ undo_info.data.col_adjust_delete.mincol,
+ undo_info.data.col_adjust_delete.endcol,
+ kExtmarkNoUndo,
+ undo_info.data.col_adjust_delete.eol);
+ }
+ // use extmark_adjust
+ } else if (undo_info.type == kLineAdjust) {
+ if (undo) {
+ // Undo - call signature type one - insert now
+ if (undo_info.data.adjust.amount == MAXLNUM) {
+ line1 = undo_info.data.adjust.line1;
+ line2 = MAXLNUM;
+ amount = -undo_info.data.adjust.amount_after;
+ amount_after = 0;
+ // Undo - call singature type two - delete now
+ } else if (undo_info.data.adjust.line2 == MAXLNUM) {
+ line1 = undo_info.data.adjust.line1;
+ line2 = undo_info.data.adjust.line2;
+ amount = -undo_info.data.adjust.amount;
+ amount_after = undo_info.data.adjust.amount_after;
+ // Undo - call signature three - move lines
+ } else {
+ line1 = (undo_info.data.adjust.line1
+ + undo_info.data.adjust.amount);
+ line2 = (undo_info.data.adjust.line2
+ + undo_info.data.adjust.amount);
+ amount = -undo_info.data.adjust.amount;
+ amount_after = -undo_info.data.adjust.amount_after;
+ }
+ // redo
+ } else {
+ line1 = undo_info.data.adjust.line1;
+ line2 = undo_info.data.adjust.line2;
+ amount = undo_info.data.adjust.amount;
+ amount_after = undo_info.data.adjust.amount_after;
+ }
+ extmark_adjust(curbuf,
+ line1, line2, amount, amount_after, kExtmarkNoUndo, false);
+ // kExtmarkCopy
+ } else if (undo_info.type == kExtmarkCopy) {
+ // Redo should be handled by kColAdjustDelete or kExtmarkCopyPlace
+ if (undo) {
+ extmark_set(curbuf,
+ undo_info.data.copy.ns_id,
+ undo_info.data.copy.mark_id,
+ undo_info.data.copy.lnum,
+ undo_info.data.copy.col,
+ kExtmarkNoUndo);
+ }
+ // uses extmark_copy_and_place
+ } else if (undo_info.type == kExtmarkCopyPlace) {
+ // Redo, undo is handle by kExtmarkCopy
+ if (!undo) {
+ extmark_copy_and_place(curbuf,
+ undo_info.data.copy_place.l_lnum,
+ undo_info.data.copy_place.l_col,
+ undo_info.data.copy_place.u_lnum,
+ undo_info.data.copy_place.u_col,
+ undo_info.data.copy_place.p_lnum,
+ undo_info.data.copy_place.p_col,
+ kExtmarkNoUndo, true, NULL);
+ }
+ // kExtmarkClear
+ } else if (undo_info.type == kExtmarkClear) {
+ // Redo, undo is handle by kExtmarkCopy
+ if (!undo) {
+ extmark_clear(curbuf,
+ undo_info.data.clear.ns_id,
+ undo_info.data.clear.l_lnum,
+ undo_info.data.clear.u_lnum,
+ kExtmarkNoUndo);
+ }
+ // kAdjustMove
+ } else if (undo_info.type == kAdjustMove) {
+ apply_undo_move(undo_info, undo);
+ // extmark_set
+ } else if (undo_info.type == kExtmarkSet) {
+ if (undo) {
+ extmark_del(curbuf,
+ undo_info.data.set.ns_id,
+ undo_info.data.set.mark_id,
+ kExtmarkNoUndo);
+ // Redo
+ } else {
+ extmark_set(curbuf,
+ undo_info.data.set.ns_id,
+ undo_info.data.set.mark_id,
+ undo_info.data.set.lnum,
+ undo_info.data.set.col,
+ kExtmarkNoUndo);
+ }
+ // extmark_set into update
+ } else if (undo_info.type == kExtmarkUpdate) {
+ if (undo) {
+ extmark_set(curbuf,
+ undo_info.data.update.ns_id,
+ undo_info.data.update.mark_id,
+ undo_info.data.update.old_lnum,
+ undo_info.data.update.old_col,
+ kExtmarkNoUndo);
+ // Redo
+ } else {
+ extmark_set(curbuf,
+ undo_info.data.update.ns_id,
+ undo_info.data.update.mark_id,
+ undo_info.data.update.lnum,
+ undo_info.data.update.col,
+ kExtmarkNoUndo);
+ }
+ // extmark_del
+ } else if (undo_info.type == kExtmarkDel) {
+ if (undo) {
+ extmark_set(curbuf,
+ undo_info.data.set.ns_id,
+ undo_info.data.set.mark_id,
+ undo_info.data.set.lnum,
+ undo_info.data.set.col,
+ kExtmarkNoUndo);
+ // Redo
+ } else {
+ extmark_del(curbuf,
+ undo_info.data.set.ns_id,
+ undo_info.data.set.mark_id,
+ kExtmarkNoUndo);
+ }
+ }
+}
+
+// undo/redo an kExtmarkMove operation
+static void apply_undo_move(ExtmarkUndoObject undo_info, bool undo)
+{
+ // 3 calls are required , see comment in function do_move (ex_cmds.c)
+ linenr_T line1 = undo_info.data.move.line1;
+ linenr_T line2 = undo_info.data.move.line2;
+ linenr_T last_line = undo_info.data.move.last_line;
+ linenr_T dest = undo_info.data.move.dest;
+ linenr_T num_lines = undo_info.data.move.num_lines;
+ linenr_T extra = undo_info.data.move.extra;
+
+ if (undo) {
+ if (dest >= line2) {
+ extmark_adjust(curbuf, dest - num_lines + 1, dest,
+ last_line - dest + num_lines - 1, 0L, kExtmarkNoUndo,
+ true);
+ extmark_adjust(curbuf, dest - line2, dest - line1,
+ dest - line2, 0L, kExtmarkNoUndo, false);
+ } else {
+ extmark_adjust(curbuf, line1-num_lines, line2-num_lines,
+ last_line - (line1-num_lines), 0L, kExtmarkNoUndo, true);
+ extmark_adjust(curbuf, (line1-num_lines) + 1, (line2-num_lines) + 1,
+ -num_lines, 0L, kExtmarkNoUndo, false);
+ }
+ extmark_adjust(curbuf, last_line, last_line + num_lines - 1,
+ line1 - last_line, 0L, kExtmarkNoUndo, true);
+ // redo
+ } else {
+ extmark_adjust(curbuf, line1, line2,
+ last_line - line2, 0L, kExtmarkNoUndo, true);
+ if (dest >= line2) {
+ extmark_adjust(curbuf, line2 + 1, dest,
+ -num_lines, 0L, kExtmarkNoUndo, false);
+ } else {
+ extmark_adjust(curbuf, dest + 1, line1 - 1,
+ num_lines, 0L, kExtmarkNoUndo, false);
+ }
+ extmark_adjust(curbuf, last_line - num_lines + 1, last_line,
+ -(last_line - dest - extra), 0L, kExtmarkNoUndo, true);
+ }
+}
+
+
+/// Get the column position for EOL on a line
+///
+/// If the lnum doesn't exist, returns 0
+colnr_T extmark_eol_col(buf_T *buf, linenr_T lnum)
+{
+ if (lnum > buf->b_ml.ml_line_count) {
+ return 0;
+ }
+ return (colnr_T)STRLEN(ml_get_buf(buf, lnum, false)) + 1;
+}
+
+
+// Adjust columns and rows for extmarks
+// based off mark_col_adjust in mark.c
+// returns true if something was moved otherwise false
+static bool extmark_col_adjust_impl(buf_T *buf, linenr_T lnum,
+ colnr_T mincol, long lnum_amount,
+ bool for_delete,
+ long update_col)
+{
+ bool marks_exist = false;
+
+ ExtmarkLine *extmarkline = extmarkline_ref(buf, lnum, false);
+ if (!extmarkline) {
+ return false;
+ }
+
+ FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, mincol, MAXCOL, {
+ marks_exist = true;
+
+ // Calculate desired col amount where the adjustment should take place
+ // (not taking) eol into account
+ long col_amount;
+ if (for_delete) {
+ if (extmark->col < update_col) {
+ // When mark inside range
+ colnr_T start_effected_range = mincol - 1;
+ col_amount = -(extmark->col - start_effected_range);
+ } else {
+ // Mark outside of range
+ // -1 because a delete of width 0 should still move marks
+ col_amount = -(update_col - mincol) - 1;
+ }
+ } else {
+ // for anything other than deletes
+ col_amount = update_col;
+ }
+
+ // No update required for this guy
+ if (col_amount == 0 && lnum_amount == 0) {
+ continue;
+ }
+
+ // Set mark to start of line
+ if (col_amount < 0
+ && extmark->col <= (colnr_T)-col_amount) {
+ extmark_update(extmark, buf, extmark->ns_id, extmark->mark_id,
+ extmarkline->lnum + lnum_amount,
+ 1, kExtmarkNoUndo, &mitr);
+ // Update the mark
+ } else {
+ // Note: The undo is handled by u_extmark_col_adjust, NoUndo here
+ extmark_update(extmark, buf, extmark->ns_id, extmark->mark_id,
+ extmarkline->lnum + lnum_amount,
+ extmark->col + (colnr_T)col_amount, kExtmarkNoUndo, &mitr);
+ }
+ })
+
+ if (kb_size(&extmarkline->items) == 0) {
+ kb_del(extmarklines, &buf->b_extlines, extmarkline);
+ extmarkline_free(extmarkline);
+ }
+
+ return marks_exist;
+}
+
+// Adjust columns and rows for extmarks
+//
+// based off mark_col_adjust in mark.c
+// use extmark_col_adjust_impl to move columns by inserting
+// Doesn't take the eol into consideration (possible to put marks in invalid
+// positions)
+void extmark_col_adjust(buf_T *buf, linenr_T lnum,
+ colnr_T mincol, long lnum_amount,
+ long col_amount, ExtmarkOp undo)
+{
+ assert(col_amount > INT_MIN && col_amount <= INT_MAX);
+
+ bool marks_moved = extmark_col_adjust_impl(buf, lnum, mincol, lnum_amount,
+ false, col_amount);
+
+ if (undo == kExtmarkUndo && marks_moved) {
+ u_extmark_col_adjust(buf, lnum, mincol, lnum_amount, col_amount);
+ }
+}
+
+// Adjust marks after a delete on a line
+//
+// Automatically readjusts to take the eol into account
+// TODO(timeyyy): change mincol to be for the mark to be copied, not moved
+//
+// @param mincol First column that needs to be moved (start of delete range) + 1
+// @param endcol Last column which needs to be copied (end of delete range + 1)
+void extmark_col_adjust_delete(buf_T *buf, linenr_T lnum,
+ colnr_T mincol, colnr_T endcol,
+ ExtmarkOp undo, int _eol)
+{
+ colnr_T start_effected_range = mincol;
+
+ bool marks_moved;
+ if (undo == kExtmarkUndo) {
+ // Copy marks that would be effected by delete
+ // -1 because we need to restore if a mark existed at the start pos
+ u_extmark_copy(buf, 0, lnum, start_effected_range, lnum, endcol);
+ }
+
+ marks_moved = extmark_col_adjust_impl(buf, lnum, mincol, 0,
+ true, (long)endcol);
+
+ // Deletes at the end of the line have different behaviour than the normal
+ // case when deleted.
+ // Cleanup any marks that are floating beyond the end of line.
+ // we allow this to be passed in as well because the buffer may have already
+ // been mutated.
+ int eol = _eol;
+ if (!eol) {
+ eol = extmark_eol_col(buf, lnum);
+ }
+ FOR_ALL_EXTMARKS(buf, 1, lnum, eol, lnum, -1, {
+ extmark_update(extmark, buf, extmark->ns_id, extmark->mark_id,
+ extmarkline->lnum, (colnr_T)eol, kExtmarkNoUndo, &mitr);
+ })
+
+ // Record the undo for the actual move
+ if (marks_moved && undo == kExtmarkUndo) {
+ u_extmark_col_adjust_delete(buf, lnum, mincol, endcol, eol);
+ }
+}
+
+// Adjust extmark row for inserted/deleted rows (columns stay fixed).
+void extmark_adjust(buf_T *buf,
+ linenr_T line1,
+ linenr_T line2,
+ long amount,
+ long amount_after,
+ ExtmarkOp undo,
+ bool end_temp)
+{
+ ExtmarkLine *_extline;
+
+ // btree needs to be kept ordered to work, so far only :move requires this
+ // 2nd call with end_temp = true unpack the lines from the temp position
+ if (end_temp && amount < 0) {
+ for (size_t i = 0; i < kv_size(buf->b_extmark_move_space); i++) {
+ _extline = kv_A(buf->b_extmark_move_space, i);
+ _extline->lnum += amount;
+ kb_put(extmarklines, &buf->b_extlines, _extline);
+ }
+ kv_size(buf->b_extmark_move_space) = 0;
+ return;
+ }
+
+ bool marks_exist = false;
+ linenr_T *lp;
+
+ linenr_T adj_start = line1;
+ if (amount == MAXLNUM) {
+ // Careful! marks from deleted region can end up on en extisting extmarkline
+ // that is goinig to be adjusted to the target position.
+ linenr_T join_num = line1 - amount_after;
+ ExtmarkLine *joinline = (join_num > line2
+ ? extmarkline_ref(buf, join_num, false) : NULL);
+
+ // extmark_adjust is already redoable, the copy should only be for undo
+ marks_exist = extmark_copy_and_place(curbuf,
+ line1, 1,
+ line2, MAXCOL,
+ line1, 1,
+ kExtmarkUndoNoRedo, true, joinline);
+ adj_start = line2+1;
+ }
+ FOR_ALL_EXTMARKLINES(buf, adj_start, MAXLNUM, {
+ marks_exist = true;
+ lp = &(extmarkline->lnum);
+ if (*lp <= line2) {
+ // 1st call with end_temp = true, store the lines in a temp position
+ if (end_temp && amount > 0) {
+ kb_del_itr_extmarklines(&buf->b_extlines, &itr);
+ kv_push(buf->b_extmark_move_space, extmarkline);
+ }
+
+ *lp += amount;
+ } else if (amount_after && *lp > line2) {
+ *lp += amount_after;
+ }
+ })
+
+ if (undo == kExtmarkUndo && marks_exist) {
+ u_extmark_adjust(buf, line1, line2, amount, amount_after);
+ }
+}
+
+/// Range points to copy
+///
+/// if part of a larger iteration we can't delete, then the caller
+/// must check for empty lines.
+bool extmark_copy_and_place(buf_T *buf,
+ linenr_T l_lnum, colnr_T l_col,
+ linenr_T u_lnum, colnr_T u_col,
+ linenr_T p_lnum, colnr_T p_col,
+ ExtmarkOp undo, bool delete,
+ ExtmarkLine *destline)
+
+{
+ bool marks_moved = false;
+ if (undo == kExtmarkUndo || undo == kExtmarkUndoNoRedo) {
+ // Copy marks that would be effected by delete
+ u_extmark_copy(buf, 0, l_lnum, l_col, u_lnum, u_col);
+ }
+
+ // Move extmarks to their final position
+ // Careful: if we move items within the same line, we might change order of
+ // marks within the same extmarkline. Too keep it simple, first delete all
+ // items from the extmarkline and put them back in the right order.
+ FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, {
+ kvec_t(Extmark) temp_space = KV_INITIAL_VALUE;
+ bool same_line = extmarkline == destline;
+ FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items,
+ (extmarkline->lnum > l_lnum) ? 0 : l_col,
+ (extmarkline->lnum < u_lnum) ? MAXCOL : u_col, {
+ if (!destline) {
+ destline = extmarkline_ref(buf, p_lnum, true);
+ same_line = extmarkline == destline;
+ }
+ marks_moved = true;
+ if (!same_line) {
+ extmark_put(p_col, extmark->mark_id, destline, extmark->ns_id);
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns,
+ extmark->ns_id);
+ pmap_put(uint64_t)(ns_obj->map, extmark->mark_id, destline);
+ } else {
+ kv_push(temp_space, *extmark);
+ }
+ // Delete old mark
+ kb_del_itr(markitems, &extmarkline->items, &mitr);
+ })
+ if (same_line) {
+ for (size_t i = 0; i < kv_size(temp_space); i++) {
+ Extmark mark = kv_A(temp_space, i);
+ extmark_put(p_col, mark.mark_id, extmarkline, mark.ns_id);
+ }
+ kv_destroy(temp_space);
+ } else if (delete && kb_size(&extmarkline->items) == 0) {
+ kb_del_itr(extmarklines, &buf->b_extlines, &itr);
+ extmarkline_free(extmarkline);
+ }
+ })
+
+ // Record the undo for the actual move
+ if (marks_moved && undo == kExtmarkUndo) {
+ u_extmark_copy_place(buf, l_lnum, l_col, u_lnum, u_col, p_lnum, p_col);
+ }
+
+ return marks_moved;
+}
+
+// Get reference to line in kbtree_t, allocating it if neccessary.
+ExtmarkLine *extmarkline_ref(buf_T *buf, linenr_T lnum, bool put)
+{
+ kbtree_t(extmarklines) *b = &buf->b_extlines;
+ ExtmarkLine t, **pp;
+ t.lnum = lnum;
+
+ pp = kb_get(extmarklines, b, &t);
+ if (!pp) {
+ if (!put) {
+ return NULL;
+ }
+ ExtmarkLine *p = xcalloc(sizeof(ExtmarkLine), 1);
+ p->lnum = lnum;
+ // p->items zero initialized
+ kb_put(extmarklines, b, p);
+ return p;
+ }
+ // Return existing
+ return *pp;
+}
+
+void extmarkline_free(ExtmarkLine *extmarkline)
+{
+ kb_destroy(markitems, (&extmarkline->items));
+ xfree(extmarkline);
+}
+
+/// Put an extmark into a line,
+///
+/// caller must ensure combination of id and ns_id isn't in use.
+void extmark_put(colnr_T col, uint64_t id,
+ ExtmarkLine *extmarkline, uint64_t ns)
+{
+ Extmark t;
+ t.col = col;
+ t.mark_id = id;
+ t.line = extmarkline;
+ t.ns_id = ns;
+
+ kbtree_t(markitems) *b = &(extmarkline->items);
+ // kb_put requries the key to not be there
+ assert(!kb_getp(markitems, b, &t));
+
+ kb_put(markitems, b, t);
+}
+
+
diff --git a/src/nvim/mark_extended.h b/src/nvim/mark_extended.h
new file mode 100644
index 0000000000..ee1da26875
--- /dev/null
+++ b/src/nvim/mark_extended.h
@@ -0,0 +1,282 @@
+#ifndef NVIM_MARK_EXTENDED_H
+#define NVIM_MARK_EXTENDED_H
+
+#include "nvim/mark_extended_defs.h"
+#include "nvim/buffer_defs.h" // for buf_T
+
+
+// Macro Documentation: FOR_ALL_?
+// Search exclusively using the range values given.
+// Use MAXCOL/MAXLNUM for the start and end of the line/col.
+// The ns parameter: Unless otherwise stated, this is only a starting point
+// for the btree to searched in, the results being itterated over will
+// still contain extmarks from other namespaces.
+
+// see FOR_ALL_? for documentation
+#define FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, code)\
+ kbitr_t(extmarklines) itr;\
+ ExtmarkLine t;\
+ t.lnum = l_lnum;\
+ if (!kb_itr_get(extmarklines, &buf->b_extlines, &t, &itr)) { \
+ kb_itr_next(extmarklines, &buf->b_extlines, &itr);\
+ }\
+ ExtmarkLine *extmarkline;\
+ for (; kb_itr_valid(&itr); kb_itr_next(extmarklines, \
+ &buf->b_extlines, &itr)) { \
+ extmarkline = kb_itr_key(&itr);\
+ if (extmarkline->lnum > u_lnum) { \
+ break;\
+ }\
+ code;\
+ }
+
+// see FOR_ALL_? for documentation
+#define FOR_ALL_EXTMARKLINES_PREV(buf, l_lnum, u_lnum, code)\
+ kbitr_t(extmarklines) itr;\
+ ExtmarkLine t;\
+ t.lnum = u_lnum;\
+ if (!kb_itr_get(extmarklines, &buf->b_extlines, &t, &itr)) { \
+ kb_itr_prev(extmarklines, &buf->b_extlines, &itr);\
+ }\
+ ExtmarkLine *extmarkline;\
+ for (; kb_itr_valid(&itr); kb_itr_prev(extmarklines, \
+ &buf->b_extlines, &itr)) { \
+ extmarkline = kb_itr_key(&itr);\
+ if (extmarkline->lnum < l_lnum) { \
+ break;\
+ }\
+ code;\
+ }
+
+// see FOR_ALL_? for documentation
+#define FOR_ALL_EXTMARKS(buf, ns, l_lnum, l_col, u_lnum, u_col, code)\
+ kbitr_t(markitems) mitr;\
+ Extmark mt;\
+ mt.ns_id = ns;\
+ mt.mark_id = 0;\
+ mt.line = NULL;\
+ FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, { \
+ mt.col = (extmarkline->lnum != l_lnum) ? MINCOL : l_col;\
+ if (!kb_itr_get(markitems, &extmarkline->items, mt, &mitr)) { \
+ kb_itr_next(markitems, &extmarkline->items, &mitr);\
+ } \
+ Extmark *extmark;\
+ for (; \
+ kb_itr_valid(&mitr); \
+ kb_itr_next(markitems, &extmarkline->items, &mitr)) { \
+ extmark = &kb_itr_key(&mitr);\
+ if (extmark->line->lnum == u_lnum \
+ && extmark->col > u_col) { \
+ break;\
+ }\
+ code;\
+ }\
+ })
+
+
+// see FOR_ALL_? for documentation
+#define FOR_ALL_EXTMARKS_PREV(buf, ns, l_lnum, l_col, u_lnum, u_col, code)\
+ kbitr_t(markitems) mitr;\
+ Extmark mt;\
+ mt.mark_id = sizeof(uint64_t);\
+ mt.ns_id = ns;\
+ FOR_ALL_EXTMARKLINES_PREV(buf, l_lnum, u_lnum, { \
+ mt.col = (extmarkline->lnum != u_lnum) ? MAXCOL : u_col;\
+ if (!kb_itr_get(markitems, &extmarkline->items, mt, &mitr)) { \
+ kb_itr_prev(markitems, &extmarkline->items, &mitr);\
+ } \
+ Extmark *extmark;\
+ for (; \
+ kb_itr_valid(&mitr); \
+ kb_itr_prev(markitems, &extmarkline->items, &mitr)) { \
+ extmark = &kb_itr_key(&mitr);\
+ if (extmark->line->lnum == l_lnum \
+ && extmark->col < l_col) { \
+ break;\
+ }\
+ code;\
+ }\
+ })
+
+
+#define FOR_ALL_EXTMARKS_IN_LINE(items, l_col, u_col, code)\
+ kbitr_t(markitems) mitr;\
+ Extmark mt;\
+ mt.ns_id = 0;\
+ mt.mark_id = 0;\
+ mt.line = NULL;\
+ mt.col = l_col;\
+ colnr_T extmarkline_u_col = u_col;\
+ if (!kb_itr_get(markitems, &items, mt, &mitr)) { \
+ kb_itr_next(markitems, &items, &mitr);\
+ } \
+ Extmark *extmark;\
+ for (; kb_itr_valid(&mitr); kb_itr_next(markitems, &items, &mitr)) { \
+ extmark = &kb_itr_key(&mitr);\
+ if (extmark->col > extmarkline_u_col) { \
+ break;\
+ }\
+ code;\
+ }
+
+
+typedef struct ExtmarkNs { // For namespacing extmarks
+ PMap(uint64_t) *map; // For fast lookup
+ uint64_t free_id; // For automatically assigning id's
+} ExtmarkNs;
+
+
+typedef kvec_t(Extmark *) ExtmarkArray;
+
+
+// Undo/redo extmarks
+
+typedef enum {
+ kExtmarkNOOP, // Extmarks shouldn't be moved
+ kExtmarkUndo, // Operation should be reversable/undoable
+ kExtmarkNoUndo, // Operation should not be reversable
+ kExtmarkUndoNoRedo, // Operation should be undoable, but not redoable
+} ExtmarkOp;
+
+
+// adjust line numbers only, corresponding to mark_adjust call
+typedef struct {
+ linenr_T line1;
+ linenr_T line2;
+ long amount;
+ long amount_after;
+} Adjust;
+
+// adjust columns after split/join line, like mark_col_adjust
+typedef struct {
+ linenr_T lnum;
+ colnr_T mincol;
+ long col_amount;
+ long lnum_amount;
+} ColAdjust;
+
+// delete the columns between mincol and endcol
+typedef struct {
+ linenr_T lnum;
+ colnr_T mincol;
+ colnr_T endcol;
+ int eol;
+} ColAdjustDelete;
+
+// adjust linenumbers after :move operation
+typedef struct {
+ linenr_T line1;
+ linenr_T line2;
+ linenr_T last_line;
+ linenr_T dest;
+ linenr_T num_lines;
+ linenr_T extra;
+} AdjustMove;
+
+// TODO(bfredl): reconsider if we really should track mark creation/updating
+// itself, these are not really "edit" operation.
+// extmark was created
+typedef struct {
+ uint64_t ns_id;
+ uint64_t mark_id;
+ linenr_T lnum;
+ colnr_T col;
+} ExtmarkSet;
+
+// extmark was updated
+typedef struct {
+ uint64_t ns_id;
+ uint64_t mark_id;
+ linenr_T old_lnum;
+ colnr_T old_col;
+ linenr_T lnum;
+ colnr_T col;
+} ExtmarkUpdate;
+
+// copied mark before deletion (as operation is destructive)
+typedef struct {
+ uint64_t ns_id;
+ uint64_t mark_id;
+ linenr_T lnum;
+ colnr_T col;
+} ExtmarkCopy;
+
+// also used as part of :move operation? probably can be simplified to one
+// event.
+typedef struct {
+ linenr_T l_lnum;
+ colnr_T l_col;
+ linenr_T u_lnum;
+ colnr_T u_col;
+ linenr_T p_lnum;
+ colnr_T p_col;
+} ExtmarkCopyPlace;
+
+// extmark was cleared.
+// TODO(bfredl): same reconsideration as for ExtmarkSet/ExtmarkUpdate
+typedef struct {
+ uint64_t ns_id;
+ linenr_T l_lnum;
+ linenr_T u_lnum;
+} ExtmarkClear;
+
+
+typedef enum {
+ kLineAdjust,
+ kColAdjust,
+ kColAdjustDelete,
+ kAdjustMove,
+ kExtmarkSet,
+ kExtmarkDel,
+ kExtmarkUpdate,
+ kExtmarkCopy,
+ kExtmarkCopyPlace,
+ kExtmarkClear,
+} UndoObjectType;
+
+// TODO(bfredl): reduce the number of undo action types
+struct undo_object {
+ UndoObjectType type;
+ union {
+ Adjust adjust;
+ ColAdjust col_adjust;
+ ColAdjustDelete col_adjust_delete;
+ AdjustMove move;
+ ExtmarkSet set;
+ ExtmarkUpdate update;
+ ExtmarkCopy copy;
+ ExtmarkCopyPlace copy_place;
+ ExtmarkClear clear;
+ } data;
+};
+
+
+// For doing move of extmarks in substitutions
+typedef struct {
+ lpos_T startpos;
+ lpos_T endpos;
+ linenr_T lnum;
+ int sublen;
+} ExtmarkSubSingle;
+
+// For doing move of extmarks in substitutions
+typedef struct {
+ lpos_T startpos;
+ lpos_T endpos;
+ linenr_T lnum;
+ linenr_T newline_in_pat;
+ linenr_T newline_in_sub;
+ linenr_T lnum_added;
+ lpos_T cm_start; // start of the match
+ lpos_T cm_end; // end of the match
+ int eol; // end of the match
+} ExtmarkSubMulti;
+
+typedef kvec_t(ExtmarkSubSingle) extmark_sub_single_vec_t;
+typedef kvec_t(ExtmarkSubMulti) extmark_sub_multi_vec_t;
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "mark_extended.h.generated.h"
+#endif
+
+#endif // NVIM_MARK_EXTENDED_H
diff --git a/src/nvim/mark_extended_defs.h b/src/nvim/mark_extended_defs.h
new file mode 100644
index 0000000000..565c599d06
--- /dev/null
+++ b/src/nvim/mark_extended_defs.h
@@ -0,0 +1,54 @@
+#ifndef NVIM_MARK_EXTENDED_DEFS_H
+#define NVIM_MARK_EXTENDED_DEFS_H
+
+#include "nvim/pos.h" // for colnr_T
+#include "nvim/map.h" // for uint64_t
+#include "nvim/lib/kbtree.h"
+#include "nvim/lib/kvec.h"
+
+struct ExtmarkLine;
+
+typedef struct Extmark
+{
+ uint64_t ns_id;
+ uint64_t mark_id;
+ struct ExtmarkLine *line;
+ colnr_T col;
+} Extmark;
+
+
+// We only need to compare columns as rows are stored in a different tree.
+// Marks are ordered by: position, namespace, mark_id
+// This improves moving marks but slows down all other use cases (searches)
+static inline int extmark_cmp(Extmark a, Extmark b)
+{
+ int cmp = kb_generic_cmp(a.col, b.col);
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = kb_generic_cmp(a.ns_id, b.ns_id);
+ if (cmp != 0) {
+ return cmp;
+ }
+ return kb_generic_cmp(a.mark_id, b.mark_id);
+}
+
+
+#define markitems_cmp(a, b) (extmark_cmp((a), (b)))
+KBTREE_INIT(markitems, Extmark, markitems_cmp, 10)
+
+typedef struct ExtmarkLine
+{
+ linenr_T lnum;
+ kbtree_t(markitems) items;
+} ExtmarkLine;
+
+#define EXTMARKLINE_CMP(a, b) (kb_generic_cmp((a)->lnum, (b)->lnum))
+KBTREE_INIT(extmarklines, ExtmarkLine *, EXTMARKLINE_CMP, 10)
+
+
+typedef struct undo_object ExtmarkUndoObject;
+typedef kvec_t(ExtmarkUndoObject) extmark_undo_vec_t;
+
+
+#endif // NVIM_MARK_EXTENDED_DEFS_H
diff --git a/src/nvim/memline.c b/src/nvim/memline.c
index b85c23e50f..2824d57f49 100644
--- a/src/nvim/memline.c
+++ b/src/nvim/memline.c
@@ -1437,7 +1437,7 @@ recover_names (
* Append the full path to name with path separators made into percent
* signs, to dir. An unnamed buffer is handled as "" (<currentdir>/"")
*/
-static char *make_percent_swname(const char *dir, char *name)
+char *make_percent_swname(const char *dir, char *name)
FUNC_ATTR_NONNULL_ARG(1)
{
char *d = NULL;
@@ -1929,6 +1929,7 @@ int ml_append_buf(
colnr_T len, // length of new line, including NUL, or 0
bool newfile // flag, see above
)
+ FUNC_ATTR_NONNULL_ARG(1)
{
if (buf->b_ml.ml_mfp == NULL)
return FAIL;
diff --git a/src/nvim/memory.c b/src/nvim/memory.c
index 64aae71433..9bc6b23ce3 100644
--- a/src/nvim/memory.c
+++ b/src/nvim/memory.c
@@ -693,6 +693,8 @@ void free_all_mem(void)
clear_hl_tables(false);
list_free_log();
+
+ check_quickfix_busy();
}
#endif
diff --git a/src/nvim/misc1.c b/src/nvim/misc1.c
index 1db8a1fa11..a871d424c6 100644
--- a/src/nvim/misc1.c
+++ b/src/nvim/misc1.c
@@ -30,7 +30,6 @@
#include "nvim/indent_c.h"
#include "nvim/buffer_updates.h"
#include "nvim/main.h"
-#include "nvim/mark.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
@@ -792,6 +791,8 @@ int prompt_for_number(int *mouse_used)
cmdline_row = msg_row - 1;
}
need_wait_return = false;
+ msg_didany = false;
+ msg_didout = false;
} else {
cmdline_row = save_cmdline_row;
}
diff --git a/src/nvim/normal.c b/src/nvim/normal.c
index f6222f9d3f..2ef2c3101f 100644
--- a/src/nvim/normal.c
+++ b/src/nvim/normal.c
@@ -3794,7 +3794,7 @@ find_decl (
valid = false;
(void)valid; // Avoid "dead assignment" warning.
t = searchit(curwin, curbuf, &curwin->w_cursor, NULL, FORWARD,
- pat, 1L, searchflags, RE_LAST, (linenr_T)0, NULL, NULL);
+ pat, 1L, searchflags, RE_LAST, NULL);
if (curwin->w_cursor.lnum >= old_pos.lnum) {
t = false; // match after start is failure too
}
@@ -4936,7 +4936,8 @@ static void nv_ident(cmdarg_T *cap)
/* put pattern in search history */
init_history();
add_to_history(HIST_SEARCH, (char_u *)buf, true, NUL);
- (void)normal_search(cap, cmdchar == '*' ? '/' : '?', (char_u *)buf, 0);
+ (void)normal_search(cap, cmdchar == '*' ? '/' : '?', (char_u *)buf, 0,
+ NULL);
} else {
g_tag_at_cursor = true;
do_cmdline_cmd(buf);
@@ -5363,7 +5364,7 @@ static void nv_search(cmdarg_T *cap)
(void)normal_search(cap, cap->cmdchar, cap->searchbuf,
(cap->arg || !equalpos(save_cursor, curwin->w_cursor))
- ? 0 : SEARCH_MARK);
+ ? 0 : SEARCH_MARK, NULL);
}
/*
@@ -5373,14 +5374,15 @@ static void nv_search(cmdarg_T *cap)
static void nv_next(cmdarg_T *cap)
{
pos_T old = curwin->w_cursor;
- int i = normal_search(cap, 0, NULL, SEARCH_MARK | cap->arg);
+ int wrapped = false;
+ int i = normal_search(cap, 0, NULL, SEARCH_MARK | cap->arg, &wrapped);
- if (i == 1 && equalpos(old, curwin->w_cursor)) {
+ if (i == 1 && !wrapped && equalpos(old, curwin->w_cursor)) {
// Avoid getting stuck on the current cursor position, which can happen when
// an offset is given and the cursor is on the last char in the buffer:
// Repeat with count + 1.
cap->count1 += 1;
- (void)normal_search(cap, 0, NULL, SEARCH_MARK | cap->arg);
+ (void)normal_search(cap, 0, NULL, SEARCH_MARK | cap->arg, NULL);
cap->count1 -= 1;
}
}
@@ -5394,18 +5396,24 @@ static int normal_search(
cmdarg_T *cap,
int dir,
char_u *pat,
- int opt /* extra flags for do_search() */
+ int opt, // extra flags for do_search()
+ int *wrapped
)
{
int i;
+ searchit_arg_T sia;
cap->oap->motion_type = kMTCharWise;
cap->oap->inclusive = false;
cap->oap->use_reg_one = true;
curwin->w_set_curswant = true;
+ memset(&sia, 0, sizeof(sia));
i = do_search(cap->oap, dir, pat, cap->count1,
- opt | SEARCH_OPT | SEARCH_ECHO | SEARCH_MSG, NULL, NULL);
+ opt | SEARCH_OPT | SEARCH_ECHO | SEARCH_MSG, &sia);
+ if (wrapped != NULL) {
+ *wrapped = sia.sa_wrapped;
+ }
if (i == 0) {
clearop(cap->oap);
} else {
@@ -6741,6 +6749,22 @@ static void nv_g_cmd(cmdarg_T *cap)
curwin->w_set_curswant = true;
break;
+ case 'M':
+ {
+ const char_u *const ptr = get_cursor_line_ptr();
+
+ oap->motion_type = kMTCharWise;
+ oap->inclusive = false;
+ i = (int)mb_string2cells_len(ptr, STRLEN(ptr));
+ if (cap->count0 > 0 && cap->count0 <= 100) {
+ coladvance((colnr_T)(i * cap->count0 / 100));
+ } else {
+ coladvance((colnr_T)(i / 2));
+ }
+ curwin->w_set_curswant = true;
+ }
+ break;
+
case '_':
/* "g_": to the last non-blank character in the line or <count> lines
* downward. */
diff --git a/src/nvim/ops.c b/src/nvim/ops.c
index 030782cbcc..2301b2159f 100644
--- a/src/nvim/ops.c
+++ b/src/nvim/ops.c
@@ -49,6 +49,7 @@
#include "nvim/undo.h"
#include "nvim/macros.h"
#include "nvim/window.h"
+#include "nvim/lib/kvec.h"
#include "nvim/os/input.h"
#include "nvim/os/time.h"
@@ -306,6 +307,15 @@ void shift_line(
change_indent(INDENT_SET, count, false, NUL, call_changed_bytes);
} else {
(void)set_indent(count, call_changed_bytes ? SIN_CHANGED : 0);
+
+ colnr_T mincol = (curwin->w_cursor.col + 1) -p_sw;
+ colnr_T col_amount = left ? -p_sw : p_sw;
+ extmark_col_adjust(curbuf,
+ curwin->w_cursor.lnum,
+ mincol,
+ 0,
+ col_amount,
+ kExtmarkUndo);
}
}
@@ -479,6 +489,10 @@ static void shift_block(oparg_T *oap, int amount)
State = oldstate;
curwin->w_cursor.col = oldcol;
p_ri = old_p_ri;
+
+ colnr_T col_amount = left ? -p_sw : p_sw;
+ extmark_col_adjust(curbuf, curwin->w_cursor.lnum,
+ curwin->w_cursor.col, 0, col_amount, kExtmarkUndo);
}
/*
@@ -623,10 +637,19 @@ void op_reindent(oparg_T *oap, Indenter how)
amount = how(); /* get the indent for this line */
if (amount >= 0 && set_indent(amount, SIN_UNDO)) {
- /* did change the indent, call changed_lines() later */
- if (first_changed == 0)
+ // did change the indent, call changed_lines() later
+ if (first_changed == 0) {
first_changed = curwin->w_cursor.lnum;
+ }
last_changed = curwin->w_cursor.lnum;
+
+ // Adjust extmarks
+ extmark_col_adjust(curbuf,
+ curwin->w_cursor.lnum,
+ 0, // mincol
+ 0, // lnum_amount
+ amount, // col_amount
+ kExtmarkUndo);
}
}
++curwin->w_cursor.lnum;
@@ -1562,6 +1585,7 @@ int op_delete(oparg_T *oap)
oap->end = curwin->w_cursor;
curwin->w_cursor = oap->start;
}
+ mb_adjust_opend(oap);
}
if (oap->line_count == 1) { /* delete characters within one line */
@@ -1620,6 +1644,8 @@ int op_delete(oparg_T *oap)
curwin->w_cursor.col = 0;
(void)del_bytes((colnr_T)n, !virtual_op,
oap->op_type == OP_DELETE && !oap->is_VIsual);
+ extmark_col_adjust(curbuf, curwin->w_cursor.lnum,
+ (colnr_T)0, 0L, (long)-n, kExtmarkUndo);
curwin->w_cursor = curpos; // restore curwin->w_cursor
(void)do_join(2, false, false, false, false);
}
@@ -1631,10 +1657,36 @@ setmarks:
if (oap->motion_type == kMTBlockWise) {
curbuf->b_op_end.lnum = oap->end.lnum;
curbuf->b_op_end.col = oap->start.col;
- } else
+ } else {
curbuf->b_op_end = oap->start;
+ }
curbuf->b_op_start = oap->start;
+ // TODO(timeyyy): refactor: Move extended marks
+ // + 1 to change to buf mode,
+ // and + 1 because we only move marks after the deleted col
+ colnr_T mincol = oap->start.col + 1 + 1;
+ colnr_T endcol;
+ if (oap->motion_type == kMTBlockWise) {
+ // TODO(timeyyy): refactor extmark_col_adjust to take lnumstart, lnum_end ?
+ endcol = bd.end_vcol + 1;
+ for (lnum = curwin->w_cursor.lnum; lnum <= oap->end.lnum; lnum++) {
+ extmark_col_adjust_delete(curbuf, lnum, mincol, endcol,
+ kExtmarkUndo, 0);
+ }
+
+ // Delete characters within one line,
+ // The case with multiple lines is handled by do_join
+ } else if (oap->motion_type == kMTCharWise && oap->line_count == 1) {
+ // + 1 to change to buf mode, then plus 1 to fit function requirements
+ endcol = oap->end.col + 1 + 1;
+
+ lnum = curwin->w_cursor.lnum;
+ if (oap->is_VIsual == false) {
+ endcol = MAX(endcol - 1, mincol);
+ }
+ extmark_col_adjust_delete(curbuf, lnum, mincol, endcol, kExtmarkUndo, 0);
+ }
return OK;
}
@@ -2030,8 +2082,8 @@ bool swapchar(int op_type, pos_T *pos)
pos_T sp = curwin->w_cursor;
curwin->w_cursor = *pos;
- /* don't use del_char(), it also removes composing chars */
- del_bytes(utf_ptr2len(get_cursor_pos_ptr()), FALSE, FALSE);
+ // don't use del_char(), it also removes composing chars
+ del_bytes(utf_ptr2len(get_cursor_pos_ptr()), false, false);
ins_char(nc);
curwin->w_cursor = sp;
} else {
@@ -2104,8 +2156,9 @@ void op_insert(oparg_T *oap, long count1)
* values in "bd". */
if (u_save_cursor() == FAIL)
return;
- for (i = 0; i < bd.endspaces; i++)
+ for (i = 0; i < bd.endspaces; i++) {
ins_char(' ');
+ }
bd.textlen += bd.endspaces;
}
} else {
@@ -2223,6 +2276,10 @@ void op_insert(oparg_T *oap, long count1)
xfree(ins_text);
}
}
+ colnr_T col = oap->start.col;
+ for (linenr_T lnum = oap->start.lnum; lnum <= oap->end.lnum; lnum++) {
+ extmark_col_adjust(curbuf, lnum, col, 0, 1, kExtmarkUndo);
+ }
}
/*
@@ -2693,6 +2750,27 @@ static void do_autocmd_textyankpost(oparg_T *oap, yankreg_T *reg)
}
+static void extmarks_do_put(int dir,
+ size_t totlen,
+ MotionType y_type,
+ linenr_T lnum,
+ colnr_T col)
+{
+ // adjust extmarks
+ colnr_T col_amount = (colnr_T)(dir == FORWARD ? totlen-1 : totlen);
+ // Move extmark with char put
+ if (y_type == kMTCharWise) {
+ extmark_col_adjust(curbuf, lnum, col, 0, col_amount, kExtmarkUndo);
+ // Move extmark with blockwise put
+ } else if (y_type == kMTBlockWise) {
+ for (lnum = curbuf->b_op_start.lnum;
+ lnum <= curbuf->b_op_end.lnum;
+ lnum++) {
+ extmark_col_adjust(curbuf, lnum, col, 0, col_amount, kExtmarkUndo);
+ }
+ }
+}
+
/*
* Put contents of register "regname" into the text.
* Caller must check "regname" to be valid!
@@ -2707,8 +2785,8 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags)
char_u *oldp;
int yanklen;
size_t totlen = 0; // init for gcc
- linenr_T lnum;
- colnr_T col;
+ linenr_T lnum = 0;
+ colnr_T col = 0;
size_t i; // index in y_array[]
MotionType y_type;
size_t y_size;
@@ -3285,11 +3363,11 @@ error:
curbuf->b_op_start.lnum++;
}
// Skip mark_adjust when adding lines after the last one, there
- // can't be marks there. But still needed in diff mode.
+ // can't be marks there.
if (curbuf->b_op_start.lnum + (y_type == kMTCharWise) - 1 + nr_lines
- < curbuf->b_ml.ml_line_count || curwin->w_p_diff) {
+ < curbuf->b_ml.ml_line_count) {
mark_adjust(curbuf->b_op_start.lnum + (y_type == kMTCharWise),
- (linenr_T)MAXLNUM, nr_lines, 0L, false);
+ (linenr_T)MAXLNUM, nr_lines, 0L, false, kExtmarkUndo);
}
// note changed text for displaying and folding
@@ -3351,6 +3429,8 @@ end:
/* If the cursor is past the end of the line put it at the end. */
adjust_cursor_eol();
+
+ extmarks_do_put(dir, totlen, y_type, lnum, col);
}
/*
@@ -3693,7 +3773,10 @@ int do_join(size_t count,
if (insert_space && t > 0) {
curr = skipwhite(curr);
- if (*curr != ')' && currsize != 0 && endcurr1 != TAB
+ if (*curr != NUL
+ && *curr != ')'
+ && currsize != 0
+ && endcurr1 != TAB
&& (!has_format_option(FO_MBYTE_JOIN)
|| (utf_ptr2char(curr) < 0x100 && endcurr1 < 0x100))
&& (!has_format_option(FO_MBYTE_JOIN2)
@@ -3744,6 +3827,7 @@ int do_join(size_t count,
* column. This is not Vi compatible, but Vi deletes the marks, thus that
* should not really be a problem.
*/
+
for (t = (linenr_T)count - 1;; t--) {
cend -= currsize;
memmove(cend, curr, (size_t)currsize);
@@ -3755,12 +3839,18 @@ int do_join(size_t count,
// If deleting more spaces than adding, the cursor moves no more than
// what is added if it is inside these spaces.
const int spaces_removed = (int)((curr - curr_start) - spaces[t]);
+ linenr_T lnum = curwin->w_cursor.lnum + t;
+ colnr_T mincol = (colnr_T)0;
+ long lnum_amount = (linenr_T)-t;
+ long col_amount = (long)(cend - newp - spaces_removed);
+
+ mark_col_adjust(lnum, mincol, lnum_amount, col_amount, spaces_removed,
+ kExtmarkUndo);
- mark_col_adjust(curwin->w_cursor.lnum + t, (colnr_T)0, (linenr_T)-t,
- (long)(cend - newp - spaces_removed), spaces_removed);
if (t == 0) {
break;
}
+
curr = curr_start = ml_get((linenr_T)(curwin->w_cursor.lnum + t - 1));
if (remove_comments)
curr += comments[t - 1];
@@ -3768,6 +3858,7 @@ int do_join(size_t count,
curr = skipwhite(curr);
currsize = (int)STRLEN(curr);
}
+
ml_replace(curwin->w_cursor.lnum, newp, false);
if (setmark) {
@@ -4188,14 +4279,14 @@ format_lines(
if (next_leader_len > 0) {
(void)del_bytes(next_leader_len, false, false);
mark_col_adjust(curwin->w_cursor.lnum, (colnr_T)0, 0L,
- (long)-next_leader_len, 0);
+ (long)-next_leader_len, 0, kExtmarkUndo);
} else if (second_indent > 0) { // the "leader" for FO_Q_SECOND
int indent = (int)getwhitecols_curline();
if (indent > 0) {
(void)del_bytes(indent, FALSE, FALSE);
mark_col_adjust(curwin->w_cursor.lnum,
- (colnr_T)0, 0L, (long)-indent, 0);
+ (colnr_T)0, 0L, (long)-indent, 0, kExtmarkUndo);
}
}
curwin->w_cursor.lnum--;
@@ -4538,7 +4629,7 @@ void op_addsub(oparg_T *oap, linenr_T Prenum1, bool g_cmd)
int do_addsub(int op_type, pos_T *pos, int length, linenr_T Prenum1)
{
int col;
- char_u *buf1;
+ char_u *buf1 = NULL;
char_u buf2[NUMBUFLEN];
int pre; // 'X' or 'x': hex; '0': octal; 'B' or 'b': bin
static bool hexupper = false; // 0xABC
@@ -4847,7 +4938,6 @@ int do_addsub(int op_type, pos_T *pos, int length, linenr_T Prenum1)
*ptr = NUL;
STRCAT(buf1, buf2);
ins_str(buf1); // insert the new number
- xfree(buf1);
endpos = curwin->w_cursor;
if (curwin->w_cursor.col) {
curwin->w_cursor.col--;
@@ -4861,7 +4951,25 @@ int do_addsub(int op_type, pos_T *pos, int length, linenr_T Prenum1)
curbuf->b_op_end.col--;
}
+ // if buf1 wasn't allocated, only a singe ASCII char was changed in-place.
+ if (did_change && buf1 != NULL) {
+ extmark_col_adjust_delete(curbuf,
+ pos->lnum,
+ startpos.col + 2,
+ endpos.col + 1 + length,
+ kExtmarkUndo,
+ 0);
+ long col_amount = (long)STRLEN(buf1);
+ extmark_col_adjust(curbuf,
+ pos->lnum,
+ startpos.col + 1,
+ 0,
+ col_amount,
+ kExtmarkUndo);
+ }
+
theend:
+ xfree(buf1);
if (visual) {
curwin->w_cursor = save_cursor;
} else if (did_change) {
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
index e96b3f8e02..d20174466d 100644
--- a/src/nvim/options.lua
+++ b/src/nvim/options.lua
@@ -612,7 +612,7 @@ return {
alloced=true,
redraw={'current_window'},
varname='p_dip',
- defaults={if_true={vi="internal,filler"}}
+ defaults={if_true={vi="internal,filler,closeoff"}}
},
{
full_name='digraph', abbreviation='dg',
diff --git a/src/nvim/os/tty.c b/src/nvim/os/tty.c
index bd5b9b4506..4f525bed9a 100644
--- a/src/nvim/os/tty.c
+++ b/src/nvim/os/tty.c
@@ -6,6 +6,7 @@
//
#include "nvim/os/os.h"
+#include "nvim/os/tty.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "os/tty.c.generated.h"
diff --git a/src/nvim/po/check.vim b/src/nvim/po/check.vim
index eae27ef74d..650c6155e2 100644
--- a/src/nvim/po/check.vim
+++ b/src/nvim/po/check.vim
@@ -47,6 +47,17 @@ let wsv = winsaveview()
let error = 0
while 1
+ let lnum = line('.')
+ if getline(lnum) =~ 'msgid "Text;.*;"'
+ if getline(lnum + 1) !~ '^msgstr "\([^;]\+;\)\+"'
+ echomsg 'Mismatching ; in line ' . (lnum + 1)
+ echomsg 'Did you forget the trailing semicolon?'
+ if error == 0
+ let error = lnum + 1
+ endif
+ endif
+ endif
+
if getline(line('.') - 1) !~ "no-c-format"
" go over the "msgid" and "msgid_plural" lines
let prevfromline = 'foobar'
diff --git a/src/nvim/pos.h b/src/nvim/pos.h
index 47d253e083..8e86ea08c5 100644
--- a/src/nvim/pos.h
+++ b/src/nvim/pos.h
@@ -14,6 +14,10 @@ typedef int colnr_T;
enum { MAXLNUM = 0x7fffffff };
/// Maximal column number, 31 bits
enum { MAXCOL = 0x7fffffff };
+// Minimum line number
+enum { MINLNUM = 1 };
+// minimum column number
+enum { MINCOL = 1 };
/*
* position in file or buffer
diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c
index c5e8d4b490..ed57b28029 100644
--- a/src/nvim/quickfix.c
+++ b/src/nvim/quickfix.c
@@ -80,6 +80,15 @@ struct qfline_S {
#define LISTCOUNT 10
#define INVALID_QFIDX (-1)
+/// Quickfix list type.
+typedef enum
+{
+ QFLT_QUICKFIX, ///< Quickfix list - global list
+ QFLT_LOCATION, ///< Location list - per window list
+ QFLT_INTERNAL ///< Internal - Temporary list used by
+ // getqflist()/getloclist()
+} qfltype_T;
+
/// Quickfix/Location list definition
///
/// Usually the list contains one or more entries. But an empty list can be
@@ -87,6 +96,7 @@ struct qfline_S {
/// information and entries can be added later using setqflist()/setloclist().
typedef struct qf_list_S {
unsigned qf_id; ///< Unique identifier for this list
+ qfltype_T qfl_type;
qfline_T *qf_start; ///< pointer to the first error
qfline_T *qf_last; ///< pointer to the last error
qfline_T *qf_ptr; ///< pointer to the current error
@@ -120,6 +130,7 @@ struct qf_info_S {
int qf_listcount; /* current number of lists */
int qf_curlist; /* current error list */
qf_list_T qf_lists[LISTCOUNT];
+ qfltype_T qfl_type; // type of list
};
static qf_info_T ql_info; // global quickfix list
@@ -154,6 +165,13 @@ struct efm_S {
int conthere; /* %> used */
};
+/// List of location lists to be deleted.
+/// Used to delay the deletion of locations lists by autocmds.
+typedef struct qf_delq_S {
+ struct qf_delq_S *next;
+ qf_info_T *qi;
+} qf_delq_T;
+
enum {
QF_FAIL = 0,
QF_OK = 1,
@@ -163,6 +181,8 @@ enum {
QF_MULTISCAN = 5,
};
+/// State information used to parse lines and add entries to a quickfix/location
+/// list.
typedef struct {
char_u *linebuf;
size_t linelen;
@@ -196,14 +216,19 @@ typedef struct {
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "quickfix.c.generated.h"
#endif
-/* Quickfix window check helper macro */
+
+static char_u *e_no_more_items = (char_u *)N_("E553: No more items");
+
+// Quickfix window check helper macro
#define IS_QF_WINDOW(wp) (bt_quickfix(wp->w_buffer) && wp->w_llist_ref == NULL)
/* Location list window check helper macro */
#define IS_LL_WINDOW(wp) (bt_quickfix(wp->w_buffer) && wp->w_llist_ref != NULL)
// Quickfix and location list stack check helper macros
-#define IS_QF_STACK(qi) (qi == &ql_info)
-#define IS_LL_STACK(qi) (qi != &ql_info)
+#define IS_QF_STACK(qi) (qi->qfl_type == QFLT_QUICKFIX)
+#define IS_LL_STACK(qi) (qi->qfl_type == QFLT_LOCATION)
+#define IS_QF_LIST(qfl) (qfl->qfl_type == QFLT_QUICKFIX)
+#define IS_LL_LIST(qfl) (qfl->qfl_type == QFLT_LOCATION)
//
// Return location list for window 'wp'
@@ -211,6 +236,14 @@ typedef struct {
//
#define GET_LOC_LIST(wp) (IS_LL_WINDOW(wp) ? wp->w_llist_ref : wp->w_llist)
+// Macro to loop through all the items in a quickfix list
+// Quickfix item index starts from 1, so i below starts at 1
+#define FOR_ALL_QFL_ITEMS(qfl, qfp, i) \
+ for (i = 1, qfp = qfl->qf_start; /* NOLINT(readability/braces) */ \
+ !got_int && i <= qfl->qf_count && qfp != NULL; \
+ i++, qfp = qfp->qf_next)
+
+
// Looking up a buffer can be slow if there are many. Remember the last one
// to make this a lot faster if there are multiple matches in the same file.
static char_u *qf_last_bufname = NULL;
@@ -218,6 +251,50 @@ static bufref_T qf_last_bufref = { NULL, 0, 0 };
static char *e_loc_list_changed = N_("E926: Current location list was changed");
+// Counter to prevent autocmds from freeing up location lists when they are
+// still being used.
+static int quickfix_busy = 0;
+static qf_delq_T *qf_delq_head = NULL;
+
+/// Process the next line from a file/buffer/list/string and add it
+/// to the quickfix list 'qfl'.
+static int qf_init_process_nextline(qf_list_T *qfl,
+ efm_T *fmt_first,
+ qfstate_T *state,
+ qffields_T *fields)
+{
+ int status;
+
+ // Get the next line from a file/buffer/list/string
+ status = qf_get_nextline(state);
+ if (status != QF_OK) {
+ return status;
+ }
+
+ status = qf_parse_line(qfl, state->linebuf, state->linelen,
+ fmt_first, fields);
+ if (status != QF_OK) {
+ return status;
+ }
+
+ return qf_add_entry(qfl,
+ qfl->qf_directory,
+ (*fields->namebuf || qfl->qf_directory != NULL)
+ ? fields->namebuf
+ : ((qfl->qf_currfile != NULL && fields->valid)
+ ? qfl->qf_currfile : (char_u *)NULL),
+ fields->module,
+ 0,
+ fields->errmsg,
+ fields->lnum,
+ fields->col,
+ fields->use_viscol,
+ fields->pattern,
+ fields->enr,
+ fields->type,
+ fields->valid);
+}
+
/// Read the errorfile "efile" into memory, line by line, building the error
/// list. Set the error list's title to qf_title.
///
@@ -229,8 +306,9 @@ static char *e_loc_list_changed = N_("E926: Current location list was changed");
/// @params enc If non-NULL, encoding used to parse errors
///
/// @returns -1 for error, number of errors for success.
-int qf_init(win_T *wp, char_u *efile, char_u *errorformat, int newlist,
- char_u *qf_title, char_u *enc)
+int qf_init(win_T *wp, const char_u *restrict efile,
+ char_u *restrict errorformat, int newlist,
+ const char_u *restrict qf_title, char_u *restrict enc)
{
qf_info_T *qi = &ql_info;
@@ -264,93 +342,96 @@ static struct fmtpattern
{ 'o', ".\\+" }
};
-// Convert an errorformat pattern to a regular expression pattern.
-// See fmt_pat definition above for the list of supported patterns.
-static char_u *fmtpat_to_regpat(
- const char_u *efmp,
- efm_T *fmt_ptr,
+/// Convert an errorformat pattern to a regular expression pattern.
+/// See fmt_pat definition above for the list of supported patterns. The
+/// pattern specifier is supplied in "efmpat". The converted pattern is stored
+/// in "regpat". Returns a pointer to the location after the pattern.
+static char_u * efmpat_to_regpat(
+ const char_u *efmpat,
+ char_u *regpat,
+ efm_T *efminfo,
int idx,
int round,
- char_u *ptr,
char_u *errmsg,
size_t errmsglen)
FUNC_ATTR_NONNULL_ALL
{
- if (fmt_ptr->addr[idx]) {
+ if (efminfo->addr[idx]) {
// Each errorformat pattern can occur only once
snprintf((char *)errmsg, errmsglen,
- _("E372: Too many %%%c in format string"), *efmp);
+ _("E372: Too many %%%c in format string"), *efmpat);
EMSG(errmsg);
return NULL;
}
if ((idx && idx < 6
- && vim_strchr((char_u *)"DXOPQ", fmt_ptr->prefix) != NULL)
+ && vim_strchr((char_u *)"DXOPQ", efminfo->prefix) != NULL)
|| (idx == 6
- && vim_strchr((char_u *)"OPQ", fmt_ptr->prefix) == NULL)) {
+ && vim_strchr((char_u *)"OPQ", efminfo->prefix) == NULL)) {
snprintf((char *)errmsg, errmsglen,
- _("E373: Unexpected %%%c in format string"), *efmp);
+ _("E373: Unexpected %%%c in format string"), *efmpat);
EMSG(errmsg);
return NULL;
}
- fmt_ptr->addr[idx] = (char_u)++round;
- *ptr++ = '\\';
- *ptr++ = '(';
+ efminfo->addr[idx] = (char_u)++round;
+ *regpat++ = '\\';
+ *regpat++ = '(';
#ifdef BACKSLASH_IN_FILENAME
- if (*efmp == 'f') {
+ if (*efmpat == 'f') {
// Also match "c:" in the file name, even when
// checking for a colon next: "%f:".
// "\%(\a:\)\="
- STRCPY(ptr, "\\%(\\a:\\)\\=");
- ptr += 10;
+ STRCPY(regpat, "\\%(\\a:\\)\\=");
+ regpat += 10;
}
#endif
- if (*efmp == 'f' && efmp[1] != NUL) {
- if (efmp[1] != '\\' && efmp[1] != '%') {
+ if (*efmpat == 'f' && efmpat[1] != NUL) {
+ if (efmpat[1] != '\\' && efmpat[1] != '%') {
// A file name may contain spaces, but this isn't
// in "\f". For "%f:%l:%m" there may be a ":" in
// the file name. Use ".\{-1,}x" instead (x is
// the next character), the requirement that :999:
// follows should work.
- STRCPY(ptr, ".\\{-1,}");
- ptr += 7;
+ STRCPY(regpat, ".\\{-1,}");
+ regpat += 7;
} else {
// File name followed by '\\' or '%': include as
// many file name chars as possible.
- STRCPY(ptr, "\\f\\+");
- ptr += 4;
+ STRCPY(regpat, "\\f\\+");
+ regpat += 4;
}
} else {
char_u *srcptr = (char_u *)fmt_pat[idx].pattern;
- while ((*ptr = *srcptr++) != NUL) {
- ptr++;
+ while ((*regpat = *srcptr++) != NUL) {
+ regpat++;
}
}
- *ptr++ = '\\';
- *ptr++ = ')';
+ *regpat++ = '\\';
+ *regpat++ = ')';
- return ptr;
+ return regpat;
}
-// Convert a scanf like format in 'errorformat' to a regular expression.
-static char_u *scanf_fmt_to_regpat(
+/// Convert a scanf like format in 'errorformat' to a regular expression.
+/// Returns a pointer to the location after the pattern.
+static char_u * scanf_fmt_to_regpat(
+ const char_u **pefmp,
const char_u *efm,
int len,
- const char_u **pefmp,
- char_u *ptr,
+ char_u *regpat,
char_u *errmsg,
size_t errmsglen)
FUNC_ATTR_NONNULL_ALL
{
const char_u *efmp = *pefmp;
- if (*++efmp == '[' || *efmp == '\\') {
- if ((*ptr++ = *efmp) == '[') { // %*[^a-z0-9] etc.
+ if (*efmp == '[' || *efmp == '\\') {
+ if ((*regpat++ = *efmp) == '[') { // %*[^a-z0-9] etc.
if (efmp[1] == '^') {
- *ptr++ = *++efmp;
+ *regpat++ = *++efmp;
}
if (efmp < efm + len) {
- *ptr++ = *++efmp; // could be ']'
- while (efmp < efm + len && (*ptr++ = *++efmp) != ']') {
+ *regpat++ = *++efmp; // could be ']'
+ while (efmp < efm + len && (*regpat++ = *++efmp) != ']') {
}
if (efmp == efm + len) {
EMSG(_("E374: Missing ] in format string"));
@@ -358,10 +439,10 @@ static char_u *scanf_fmt_to_regpat(
}
}
} else if (efmp < efm + len) { // %*\D, %*\s etc.
- *ptr++ = *++efmp;
+ *regpat++ = *++efmp;
}
- *ptr++ = '\\';
- *ptr++ = '+';
+ *regpat++ = '\\';
+ *regpat++ = '+';
} else {
// TODO(vim): scanf()-like: %*ud, %*3c, %*f, ... ?
snprintf((char *)errmsg, errmsglen,
@@ -372,31 +453,27 @@ static char_u *scanf_fmt_to_regpat(
*pefmp = efmp;
- return ptr;
+ return regpat;
}
-// Analyze/parse an errorformat prefix.
-static int efm_analyze_prefix(const char_u **pefmp, efm_T *fmt_ptr,
- char_u *errmsg, size_t errmsglen)
+/// Analyze/parse an errorformat prefix.
+static const char_u *efm_analyze_prefix(const char_u *efmp, efm_T *efminfo,
+ char_u *errmsg, size_t errmsglen)
FUNC_ATTR_NONNULL_ALL
{
- const char_u *efmp = *pefmp;
-
if (vim_strchr((char_u *)"+-", *efmp) != NULL) {
- fmt_ptr->flags = *efmp++;
+ efminfo->flags = *efmp++;
}
if (vim_strchr((char_u *)"DXAEWICZGOPQ", *efmp) != NULL) {
- fmt_ptr->prefix = *efmp;
+ efminfo->prefix = *efmp;
} else {
snprintf((char *)errmsg, errmsglen,
_("E376: Invalid %%%c in format string prefix"), *efmp);
EMSG(errmsg);
- return FAIL;
+ return NULL;
}
- *pefmp = efmp;
-
- return OK;
+ return efmp;
}
@@ -419,16 +496,17 @@ static int efm_to_regpat(const char_u *efm, int len, efm_T *fmt_ptr,
}
}
if (idx < FMT_PATTERNS) {
- ptr = fmtpat_to_regpat(efmp, fmt_ptr, idx, round, ptr,
- errmsg, errmsglen);
+ ptr = efmpat_to_regpat(efmp, ptr, fmt_ptr, idx, round, errmsg,
+ errmsglen);
if (ptr == NULL) {
- return -1;
+ return FAIL;
}
round++;
} else if (*efmp == '*') {
- ptr = scanf_fmt_to_regpat(efm, len, &efmp, ptr, errmsg, errmsglen);
+ efmp++;
+ ptr = scanf_fmt_to_regpat(&efmp, efm, len, ptr, errmsg, errmsglen);
if (ptr == NULL) {
- return -1;
+ return FAIL;
}
} else if (vim_strchr((char_u *)"%\\.^$~[", *efmp) != NULL) {
*ptr++ = *efmp; // regexp magic characters
@@ -437,14 +515,17 @@ static int efm_to_regpat(const char_u *efm, int len, efm_T *fmt_ptr,
} else if (*efmp == '>') {
fmt_ptr->conthere = true;
} else if (efmp == efm + 1) { // analyse prefix
- if (efm_analyze_prefix(&efmp, fmt_ptr, errmsg, errmsglen) == FAIL) {
- return -1;
+ // prefix is allowed only at the beginning of the errorformat
+ // option part
+ efmp = efm_analyze_prefix(efmp, fmt_ptr, errmsg, errmsglen);
+ if (efmp == NULL) {
+ return FAIL;
}
} else {
snprintf((char *)errmsg, CMDBUFFSIZE + 1,
_("E377: Invalid %%%c in format string"), *efmp);
EMSG(errmsg);
- return -1;
+ return FAIL;
}
} else { // copy normal character
if (*efmp == '\\' && efmp + 1 < efm + len) {
@@ -460,7 +541,7 @@ static int efm_to_regpat(const char_u *efm, int len, efm_T *fmt_ptr,
*ptr++ = '$';
*ptr = NUL;
- return 0;
+ return OK;
}
static efm_T *fmt_start = NULL; // cached across qf_parse_line() calls
@@ -476,7 +557,42 @@ static void free_efm_list(efm_T **efm_first)
fmt_start = NULL;
}
-// Parse 'errorformat' option
+/// Compute the size of the buffer used to convert a 'errorformat' pattern into
+/// a regular expression pattern.
+static size_t efm_regpat_bufsz(char_u *efm)
+{
+ size_t sz;
+
+ sz = (FMT_PATTERNS * 3) + (STRLEN(efm) << 2);
+ for (int i = FMT_PATTERNS - 1; i >= 0; ) {
+ sz += STRLEN(fmt_pat[i--].pattern);
+ }
+#ifdef BACKSLASH_IN_FILENAME
+ sz += 12; // "%f" can become twelve chars longer (see efm_to_regpat)
+#else
+ sz += 2; // "%f" can become two chars longer
+#endif
+
+ return sz;
+}
+
+/// Return the length of a 'errorformat' option part (separated by ",").
+static int efm_option_part_len(char_u *efm)
+{
+ int len;
+
+ for (len = 0; efm[len] != NUL && efm[len] != ','; len++) {
+ if (efm[len] == '\\' && efm[len + 1] != NUL) {
+ len++;
+ }
+ }
+
+ return len;
+}
+
+/// Parse the 'errorformat' option. Multiple parts in the 'errorformat' option
+/// are parsed and converted to regular expressions. Returns information about
+/// the parsed 'errorformat' option.
static efm_T * parse_efm_option(char_u *efm)
{
efm_T *fmt_ptr = NULL;
@@ -488,16 +604,8 @@ static efm_T * parse_efm_option(char_u *efm)
char_u *errmsg = xmalloc(errmsglen);
// Get some space to modify the format string into.
- size_t i = (FMT_PATTERNS * 3) + (STRLEN(efm) << 2);
- for (int round = FMT_PATTERNS - 1; round >= 0; ) {
- i += STRLEN(fmt_pat[round--].pattern);
- }
-#ifdef BACKSLASH_IN_FILENAME
- i += 12; // "%f" can become twelve chars longer (see efm_to_regpat)
-#else
- i += 2; // "%f" can become two chars longer
-#endif
- char_u *fmtstr = xmalloc(i);
+ size_t sz = efm_regpat_bufsz(efm);
+ char_u *fmtstr = xmalloc(sz);
while (efm[0] != NUL) {
// Allocate a new eformat structure and put it at the end of the list
@@ -510,13 +618,9 @@ static efm_T * parse_efm_option(char_u *efm)
fmt_last = fmt_ptr;
// Isolate one part in the 'errorformat' option
- for (len = 0; efm[len] != NUL && efm[len] != ','; len++) {
- if (efm[len] == '\\' && efm[len + 1] != NUL) {
- len++;
- }
- }
+ len = efm_option_part_len(efm);
- if (efm_to_regpat(efm, len, fmt_ptr, fmtstr, errmsg, errmsglen) == -1) {
+ if (efm_to_regpat(efm, len, fmt_ptr, fmtstr, errmsg, errmsglen) == FAIL) {
goto parse_efm_error;
}
if ((fmt_ptr->prog = vim_regcomp(fmtstr, RE_MAGIC + RE_STRING)) == NULL) {
@@ -542,6 +646,7 @@ parse_efm_end:
return fmt_first;
}
+/// Allocate more memory for the line buffer used for parsing lines.
static char_u *qf_grow_linebuf(qfstate_T *state, size_t newsz)
{
// If the line exceeds LINE_MAXLEN exclude the last
@@ -783,25 +888,41 @@ static int qf_get_nextline(qfstate_T *state)
return QF_OK;
}
-// Returns true if the specified quickfix/location list is empty.
-static bool qf_list_empty(const qf_info_T *qi, int qf_idx)
+/// Returns true if the specified quickfix/location stack is empty
+static bool qf_stack_empty(const qf_info_T *qi)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
- if (qi == NULL || qf_idx < 0 || qf_idx >= LISTCOUNT) {
- return true;
- }
- return qi->qf_lists[qf_idx].qf_count <= 0;
+ return qi == NULL || qi->qf_listcount <= 0;
+}
+
+/// Returns true if the specified quickfix/location list is empty.
+static bool qf_list_empty(qf_list_T *qfl)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ return qfl == NULL || qfl->qf_count <= 0;
+}
+
+/// Returns true if the specified quickfix/location list is not empty and
+/// has valid entries.
+static bool qf_list_has_valid_entries(qf_list_T *qfl)
+{
+ return !qf_list_empty(qfl) && !qfl->qf_nonevalid;
+}
+
+/// Return a pointer to a list in the specified quickfix stack
+static qf_list_T * qf_get_list(qf_info_T *qi, int idx)
+{
+ return &qi->qf_lists[idx];
}
/// Parse a line and get the quickfix fields.
/// Return the QF_ status.
-static int qf_parse_line(qf_info_T *qi, int qf_idx, char_u *linebuf,
+static int qf_parse_line(qf_list_T *qfl, char_u *linebuf,
size_t linelen, efm_T *fmt_first, qffields_T *fields)
{
efm_T *fmt_ptr;
int idx = 0;
char_u *tail = NULL;
- qf_list_T *qfl = &qi->qf_lists[qf_idx];
int status;
restofline:
@@ -857,7 +978,7 @@ restofline:
qfl->qf_multiignore = false; // reset continuation
} else if (vim_strchr((char_u *)"CZ", idx) != NULL) {
// continuation of multi-line msg
- status = qf_parse_multiline_pfx(qi, qf_idx, idx, qfl, fields);
+ status = qf_parse_multiline_pfx(idx, qfl, fields);
if (status != QF_OK) {
return status;
}
@@ -880,6 +1001,79 @@ restofline:
return QF_OK;
}
+// Allocate the fields used for parsing lines and populating a quickfix list.
+static void qf_alloc_fields(qffields_T *pfields)
+ FUNC_ATTR_NONNULL_ALL
+{
+ pfields->namebuf = xmalloc(CMDBUFFSIZE + 1);
+ pfields->module = xmalloc(CMDBUFFSIZE + 1);
+ pfields->errmsglen = CMDBUFFSIZE + 1;
+ pfields->errmsg = xmalloc(pfields->errmsglen);
+ pfields->pattern = xmalloc(CMDBUFFSIZE + 1);
+}
+
+// Free the fields used for parsing lines and populating a quickfix list.
+static void qf_free_fields(qffields_T *pfields)
+ FUNC_ATTR_NONNULL_ALL
+{
+ xfree(pfields->namebuf);
+ xfree(pfields->module);
+ xfree(pfields->errmsg);
+ xfree(pfields->pattern);
+}
+
+// Setup the state information used for parsing lines and populating a
+// quickfix list.
+static int qf_setup_state(
+ qfstate_T *pstate,
+ char_u *restrict enc,
+ const char_u *restrict efile,
+ typval_T *tv,
+ buf_T *buf,
+ linenr_T lnumfirst,
+ linenr_T lnumlast)
+ FUNC_ATTR_NONNULL_ARG(1)
+{
+ pstate->vc.vc_type = CONV_NONE;
+ if (enc != NULL && *enc != NUL) {
+ convert_setup(&pstate->vc, enc, p_enc);
+ }
+
+ if (efile != NULL
+ && (pstate->fd = os_fopen((const char *)efile, "r")) == NULL) {
+ EMSG2(_(e_openerrf), efile);
+ return FAIL;
+ }
+
+ if (tv != NULL) {
+ if (tv->v_type == VAR_STRING) {
+ pstate->p_str = tv->vval.v_string;
+ } else if (tv->v_type == VAR_LIST) {
+ pstate->p_li = tv_list_first(tv->vval.v_list);
+ }
+ pstate->tv = tv;
+ }
+ pstate->buf = buf;
+ pstate->buflnum = lnumfirst;
+ pstate->lnumlast = lnumlast;
+
+ return OK;
+}
+
+// Cleanup the state information used for parsing lines and populating a
+// quickfix list.
+static void qf_cleanup_state(qfstate_T *pstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (pstate->fd != NULL) {
+ fclose(pstate->fd);
+ }
+ xfree(pstate->growbuf);
+ if (pstate->vc.vc_type != CONV_NONE) {
+ convert_setup(&pstate->vc, NULL, NULL);
+ }
+}
+
// Read the errorfile "efile" into memory, line by line, building the error
// list.
// Alternative: when "efile" is NULL read errors from buffer "buf".
@@ -892,19 +1086,20 @@ static int
qf_init_ext(
qf_info_T *qi,
int qf_idx,
- char_u *efile,
+ const char_u *restrict efile,
buf_T *buf,
typval_T *tv,
- char_u *errorformat,
- int newlist, // TRUE: start a new error list
+ char_u *restrict errorformat,
+ bool newlist, // true: start a new error list
linenr_T lnumfirst, // first line number to use
linenr_T lnumlast, // last line number to use
- char_u *qf_title,
- char_u *enc
+ const char_u *restrict qf_title,
+ char_u *restrict enc
)
+ FUNC_ATTR_NONNULL_ARG(1)
{
- qfstate_T state;
- qffields_T fields;
+ qfstate_T state = { 0 };
+ qffields_T fields = { 0 };
qfline_T *old_last = NULL;
bool adding = false;
static efm_T *fmt_first = NULL;
@@ -916,21 +1111,9 @@ qf_init_ext(
// Do not used the cached buffer, it may have been wiped out.
XFREE_CLEAR(qf_last_bufname);
- memset(&state, 0, sizeof(state));
- memset(&fields, 0, sizeof(fields));
- state.vc.vc_type = CONV_NONE;
- if (enc != NULL && *enc != NUL) {
- convert_setup(&state.vc, enc, p_enc);
- }
-
- fields.namebuf = xmalloc(CMDBUFFSIZE + 1);
- fields.module = xmalloc(CMDBUFFSIZE + 1);
- fields.errmsglen = CMDBUFFSIZE + 1;
- fields.errmsg = xmalloc(fields.errmsglen);
- fields.pattern = xmalloc(CMDBUFFSIZE + 1);
-
- if (efile != NULL && (state.fd = os_fopen((char *)efile, "r")) == NULL) {
- EMSG2(_(e_openerrf), efile);
+ qf_alloc_fields(&fields);
+ if (qf_setup_state(&state, enc, efile, tv, buf,
+ lnumfirst, lnumlast) == FAIL) {
goto qf_init_end;
}
@@ -941,12 +1124,12 @@ qf_init_ext(
} else {
// Adding to existing list, use last entry.
adding = true;
- if (qi->qf_lists[qf_idx].qf_count > 0) {
+ if (!qf_list_empty(qf_get_list(qi, qf_idx) )) {
old_last = qi->qf_lists[qf_idx].qf_last;
}
}
- qf_list_T *qfl = &qi->qf_lists[qf_idx];
+ qf_list_T *qfl = qf_get_list(qi, qf_idx);
// Use the local value of 'errorformat' if it's set.
if (errorformat == p_efm && tv == NULL && buf && *buf->b_p_efm != NUL) {
@@ -979,57 +1162,19 @@ qf_init_ext(
*/
got_int = FALSE;
- if (tv != NULL) {
- if (tv->v_type == VAR_STRING) {
- state.p_str = tv->vval.v_string;
- } else if (tv->v_type == VAR_LIST) {
- state.p_list = tv->vval.v_list;
- state.p_li = tv_list_first(tv->vval.v_list);
- }
- state.tv = tv;
- }
- state.buf = buf;
- state.buflnum = lnumfirst;
- state.lnumlast = lnumlast;
-
/*
* Read the lines in the error file one by one.
* Try to recognize one of the error formats in each line.
*/
while (!got_int) {
- // Get the next line from a file/buffer/list/string
- status = qf_get_nextline(&state);
+ status = qf_init_process_nextline(qfl, fmt_first, &state, &fields);
if (status == QF_END_OF_INPUT) { // end of input
break;
}
-
- status = qf_parse_line(qi, qf_idx, state.linebuf, state.linelen,
- fmt_first, &fields);
if (status == QF_FAIL) {
goto error2;
}
- if (status == QF_IGNORE_LINE) {
- continue;
- }
- if (qf_add_entry(qi,
- qf_idx,
- qfl->qf_directory,
- (*fields.namebuf || qfl->qf_directory)
- ? fields.namebuf : ((qfl->qf_currfile && fields.valid)
- ? qfl->qf_currfile : (char_u *)NULL),
- fields.module,
- 0,
- fields.errmsg,
- fields.lnum,
- fields.col,
- fields.use_viscol,
- fields.pattern,
- fields.enr,
- fields.type,
- fields.valid) == FAIL) {
- goto error2;
- }
line_breakcheck();
}
if (state.fd == NULL || !ferror(state.fd)) {
@@ -1052,45 +1197,34 @@ qf_init_ext(
error2:
if (!adding) {
// Error when creating a new list. Free the new list
- qf_free(qi, qi->qf_curlist);
+ qf_free(qfl);
qi->qf_listcount--;
if (qi->qf_curlist > 0) {
qi->qf_curlist--;
}
}
qf_init_end:
- if (state.fd != NULL) {
- fclose(state.fd);
- }
- xfree(fields.namebuf);
- xfree(fields.module);
- xfree(fields.errmsg);
- xfree(fields.pattern);
- xfree(state.growbuf);
-
if (qf_idx == qi->qf_curlist) {
qf_update_buffer(qi, old_last);
}
-
- if (state.vc.vc_type != CONV_NONE) {
- convert_setup(&state.vc, NULL, NULL);
- }
+ qf_cleanup_state(&state);
+ qf_free_fields(&fields);
return retval;
}
/// Set the title of the specified quickfix list. Frees the previous title.
/// Prepends ':' to the title.
-static void qf_store_title(qf_info_T *qi, int qf_idx, const char_u *title)
+static void qf_store_title(qf_list_T *qfl, const char_u *title)
FUNC_ATTR_NONNULL_ARG(1)
{
- XFREE_CLEAR(qi->qf_lists[qf_idx].qf_title);
+ XFREE_CLEAR(qfl->qf_title);
if (title != NULL) {
size_t len = STRLEN(title) + 1;
char_u *p = xmallocz(len);
- qi->qf_lists[qf_idx].qf_title = p;
+ qfl->qf_title = p;
xstrlcpy((char *)p, (const char *)title, len + 1);
}
}
@@ -1108,35 +1242,256 @@ static char_u * qf_cmdtitle(char_u *cmd)
return qftitle_str;
}
-// Prepare for adding a new quickfix list. If the current list is in the
-// middle of the stack, then all the following lists are freed and then
-// the new list is added.
-static void qf_new_list(qf_info_T *qi, char_u *qf_title)
+/// Return a pointer to the current list in the specified quickfix stack
+static qf_list_T * qf_get_curlist(qf_info_T *qi)
+{
+ return qf_get_list(qi, qi->qf_curlist);
+}
+
+/// Prepare for adding a new quickfix list. If the current list is in the
+/// middle of the stack, then all the following lists are freed and then
+/// the new list is added.
+static void qf_new_list(qf_info_T *qi, const char_u *qf_title)
{
int i;
+ qf_list_T *qfl;
// If the current entry is not the last entry, delete entries beyond
// the current entry. This makes it possible to browse in a tree-like
// way with ":grep'.
- while (qi->qf_listcount > qi->qf_curlist + 1)
- qf_free(qi, --qi->qf_listcount);
+ while (qi->qf_listcount > qi->qf_curlist + 1) {
+ qf_free(&qi->qf_lists[--qi->qf_listcount]);
+ }
/*
* When the stack is full, remove to oldest entry
* Otherwise, add a new entry.
*/
if (qi->qf_listcount == LISTCOUNT) {
- qf_free(qi, 0);
- for (i = 1; i < LISTCOUNT; ++i)
+ qf_free(&qi->qf_lists[0]);
+ for (i = 1; i < LISTCOUNT; i++) {
qi->qf_lists[i - 1] = qi->qf_lists[i];
+ }
qi->qf_curlist = LISTCOUNT - 1;
} else
qi->qf_curlist = qi->qf_listcount++;
- memset(&qi->qf_lists[qi->qf_curlist], 0, (size_t)(sizeof(qf_list_T)));
- qf_store_title(qi, qi->qf_curlist, qf_title);
- qi->qf_lists[qi->qf_curlist].qf_id = ++last_qf_id;
+ qfl = qf_get_curlist(qi);
+ memset(qfl, 0, sizeof(qf_list_T));
+ qf_store_title(qfl, qf_title);
+ qfl->qfl_type = qi->qfl_type;
+ qfl->qf_id = ++last_qf_id;
+}
+
+/// Parse the match for filename ('%f') pattern in regmatch.
+/// Return the matched value in "fields->namebuf".
+static int qf_parse_fmt_f(regmatch_T *rmp,
+ int midx,
+ qffields_T *fields,
+ int prefix)
+{
+ char_u c;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+
+ // Expand ~/file and $HOME/file to full path.
+ c = *rmp->endp[midx];
+ *rmp->endp[midx] = NUL;
+ expand_env(rmp->startp[midx], fields->namebuf, CMDBUFFSIZE);
+ *rmp->endp[midx] = c;
+
+ // For separate filename patterns (%O, %P and %Q), the specified file
+ // should exist.
+ if (vim_strchr((char_u *)"OPQ", prefix) != NULL
+ && !os_path_exists(fields->namebuf)) {
+ return QF_FAIL;
+ }
+
+ return QF_OK;
+}
+
+/// Parse the match for error number ('%n') pattern in regmatch.
+/// Return the matched value in "fields->enr".
+static int qf_parse_fmt_n(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->enr = (int)atol((char *)rmp->startp[midx]);
+ return QF_OK;
+}
+
+/// Parse the match for line number (%l') pattern in regmatch.
+/// Return the matched value in "fields->lnum".
+static int qf_parse_fmt_l(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->lnum = atol((char *)rmp->startp[midx]);
+ return QF_OK;
+}
+
+/// Parse the match for column number ('%c') pattern in regmatch.
+/// Return the matched value in "fields->col".
+static int qf_parse_fmt_c(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->col = (int)atol((char *)rmp->startp[midx]);
+ return QF_OK;
+}
+
+/// Parse the match for error type ('%t') pattern in regmatch.
+/// Return the matched value in "fields->type".
+static int qf_parse_fmt_t(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->type = *rmp->startp[midx];
+ return QF_OK;
+}
+
+/// Parse the match for '%+' format pattern. The whole matching line is included
+/// in the error string. Return the matched line in "fields->errmsg".
+static int qf_parse_fmt_plus(char_u *linebuf,
+ size_t linelen,
+ qffields_T *fields)
+{
+ if (linelen >= fields->errmsglen) {
+ // linelen + null terminator
+ fields->errmsg = xrealloc(fields->errmsg, linelen + 1);
+ fields->errmsglen = linelen + 1;
+ }
+ STRLCPY(fields->errmsg, linebuf, linelen + 1);
+ return QF_OK;
+}
+
+/// Parse the match for error message ('%m') pattern in regmatch.
+/// Return the matched value in "fields->errmsg".
+static int qf_parse_fmt_m(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ size_t len;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ len = (size_t)(rmp->endp[midx] - rmp->startp[midx]);
+ if (len >= fields->errmsglen) {
+ // len + null terminator
+ fields->errmsg = xrealloc(fields->errmsg, len + 1);
+ fields->errmsglen = len + 1;
+ }
+ STRLCPY(fields->errmsg, rmp->startp[midx], len + 1);
+ return QF_OK;
+}
+
+/// Parse the match for rest of a single-line file message ('%r') pattern.
+/// Return the matched value in "tail".
+static int qf_parse_fmt_r(regmatch_T *rmp, int midx, char_u **tail)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ *tail = rmp->startp[midx];
+ return QF_OK;
+}
+
+/// Parse the match for the pointer line ('%p') pattern in regmatch.
+/// Return the matched value in "fields->col".
+static int qf_parse_fmt_p(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ char_u *match_ptr;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->col = 0;
+ for (match_ptr = rmp->startp[midx]; match_ptr != rmp->endp[midx];
+ match_ptr++) {
+ fields->col++;
+ if (*match_ptr == TAB) {
+ fields->col += 7;
+ fields->col -= fields->col % 8;
+ }
+ }
+ fields->col++;
+ fields->use_viscol = true;
+ return QF_OK;
+}
+
+/// Parse the match for the virtual column number ('%v') pattern in regmatch.
+/// Return the matched value in "fields->col".
+static int qf_parse_fmt_v(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->col = (int)atol((char *)rmp->startp[midx]);
+ fields->use_viscol = true;
+ return QF_OK;
+}
+
+/// Parse the match for the search text ('%s') pattern in regmatch.
+/// Return the matched value in "fields->pattern".
+static int qf_parse_fmt_s(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ size_t len;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ len = (size_t)(rmp->endp[midx] - rmp->startp[midx]);
+ if (len > CMDBUFFSIZE - 5) {
+ len = CMDBUFFSIZE - 5;
+ }
+ STRCPY(fields->pattern, "^\\V");
+ xstrlcat((char *)fields->pattern, (char *)rmp->startp[midx], len + 4);
+ fields->pattern[len + 3] = '\\';
+ fields->pattern[len + 4] = '$';
+ fields->pattern[len + 5] = NUL;
+ return QF_OK;
+}
+
+/// Parse the match for the module ('%o') pattern in regmatch.
+/// Return the matched value in "fields->module".
+static int qf_parse_fmt_o(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ size_t len;
+ size_t dsize;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ len = (size_t)(rmp->endp[midx] - rmp->startp[midx]);
+ dsize = STRLEN(fields->module) + len + 1;
+ if (dsize > CMDBUFFSIZE) {
+ dsize = CMDBUFFSIZE;
+ }
+ xstrlcat((char *)fields->module, (char *)rmp->startp[midx], dsize);
+ return QF_OK;
}
+/// 'errorformat' format pattern parser functions.
+/// The '%f' and '%r' formats are parsed differently from other formats.
+/// See qf_parse_match() for details.
+static int (*qf_parse_fmt[FMT_PATTERNS])(regmatch_T *, int, qffields_T *) = {
+ NULL,
+ qf_parse_fmt_n,
+ qf_parse_fmt_l,
+ qf_parse_fmt_c,
+ qf_parse_fmt_t,
+ qf_parse_fmt_m,
+ NULL,
+ qf_parse_fmt_p,
+ qf_parse_fmt_v,
+ qf_parse_fmt_s,
+ qf_parse_fmt_o
+};
+
/// Parse the error format matches in 'regmatch' and set the values in 'fields'.
/// fmt_ptr contains the 'efm' format specifiers/prefixes that have a match.
/// Returns QF_OK if all the matches are successfully parsed. On failure,
@@ -1147,7 +1502,8 @@ static int qf_parse_match(char_u *linebuf, size_t linelen, efm_T *fmt_ptr,
{
char_u idx = fmt_ptr->prefix;
int i;
- size_t len;
+ int midx;
+ int status;
if ((idx == 'C' || idx == 'Z') && !qf_multiline) {
return QF_FAIL;
@@ -1161,118 +1517,26 @@ static int qf_parse_match(char_u *linebuf, size_t linelen, efm_T *fmt_ptr,
// Extract error message data from matched line.
// We check for an actual submatch, because "\[" and "\]" in
// the 'errorformat' may cause the wrong submatch to be used.
- if ((i = (int)fmt_ptr->addr[0]) > 0) { // %f
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
-
- // Expand ~/file and $HOME/file to full path.
- char_u c = *regmatch->endp[i];
- *regmatch->endp[i] = NUL;
- expand_env(regmatch->startp[i], fields->namebuf, CMDBUFFSIZE);
- *regmatch->endp[i] = c;
-
- if (vim_strchr((char_u *)"OPQ", idx) != NULL
- && !os_path_exists(fields->namebuf)) {
- return QF_FAIL;
- }
- }
- if ((i = (int)fmt_ptr->addr[1]) > 0) { // %n
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->enr = (int)atol((char *)regmatch->startp[i]);
- }
- if ((i = (int)fmt_ptr->addr[2]) > 0) { // %l
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->lnum = atol((char *)regmatch->startp[i]);
- }
- if ((i = (int)fmt_ptr->addr[3]) > 0) { // %c
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->col = (int)atol((char *)regmatch->startp[i]);
- }
- if ((i = (int)fmt_ptr->addr[4]) > 0) { // %t
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->type = *regmatch->startp[i];
- }
- if (fmt_ptr->flags == '+' && !qf_multiscan) { // %+
- if (linelen >= fields->errmsglen) {
- // linelen + null terminator
- fields->errmsg = xrealloc(fields->errmsg, linelen + 1);
- fields->errmsglen = linelen + 1;
- }
- STRLCPY(fields->errmsg, linebuf, linelen + 1);
- } else if ((i = (int)fmt_ptr->addr[5]) > 0) { // %m
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
- len = (size_t)(regmatch->endp[i] - regmatch->startp[i]);
- if (len >= fields->errmsglen) {
- // len + null terminator
- fields->errmsg = xrealloc(fields->errmsg, len + 1);
- fields->errmsglen = len + 1;
- }
- STRLCPY(fields->errmsg, regmatch->startp[i], len + 1);
- }
- if ((i = (int)fmt_ptr->addr[6]) > 0) { // %r
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- *tail = regmatch->startp[i];
- }
- if ((i = (int)fmt_ptr->addr[7]) > 0) { // %p
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
- fields->col = 0;
- char_u *match_ptr;
- for (match_ptr = regmatch->startp[i]; match_ptr != regmatch->endp[i];
- match_ptr++) {
- fields->col++;
- if (*match_ptr == TAB) {
- fields->col += 7;
- fields->col -= fields->col % 8;
+ for (i = 0; i < FMT_PATTERNS; i++) {
+ status = QF_OK;
+ midx = (int)fmt_ptr->addr[i];
+ if (i == 0 && midx > 0) { // %f
+ status = qf_parse_fmt_f(regmatch, midx, fields, idx);
+ } else if (i == 5) {
+ if (fmt_ptr->flags == '+' && !qf_multiscan) { // %+
+ status = qf_parse_fmt_plus(linebuf, linelen, fields);
+ } else if (midx > 0) { // %m
+ status = qf_parse_fmt_m(regmatch, midx, fields);
}
+ } else if (i == 6 && midx > 0) { // %r
+ status = qf_parse_fmt_r(regmatch, midx, tail);
+ } else if (midx > 0) { // others
+ status = (qf_parse_fmt[i])(regmatch, midx, fields);
}
- fields->col++;
- fields->use_viscol = true;
- }
- if ((i = (int)fmt_ptr->addr[8]) > 0) { // %v
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->col = (int)atol((char *)regmatch->startp[i]);
- fields->use_viscol = true;
- }
- if ((i = (int)fmt_ptr->addr[9]) > 0) { // %s
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
- len = (size_t)(regmatch->endp[i] - regmatch->startp[i]);
- if (len > CMDBUFFSIZE - 5) {
- len = CMDBUFFSIZE - 5;
- }
- STRCPY(fields->pattern, "^\\V");
- STRNCAT(fields->pattern, regmatch->startp[i], len);
- fields->pattern[len + 3] = '\\';
- fields->pattern[len + 4] = '$';
- fields->pattern[len + 5] = NUL;
- }
- if ((i = (int)fmt_ptr->addr[10]) > 0) { // %o
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
- len = (size_t)(regmatch->endp[i] - regmatch->startp[i]);
- if (len > CMDBUFFSIZE) {
- len = CMDBUFFSIZE;
+
+ if (status != QF_OK) {
+ return status;
}
- STRNCAT(fields->module, regmatch->startp[i], len);
}
return QF_OK;
@@ -1384,8 +1648,7 @@ static int qf_parse_line_nomatch(char_u *linebuf, size_t linelen,
}
/// Parse multi-line error format prefixes (%C and %Z)
-static int qf_parse_multiline_pfx(qf_info_T *qi, int qf_idx, int idx,
- qf_list_T *qfl, qffields_T *fields)
+static int qf_parse_multiline_pfx(int idx, qf_list_T *qfl, qffields_T *fields)
{
if (!qfl->qf_multiignore) {
qfline_T *qfprev = qfl->qf_last;
@@ -1416,7 +1679,7 @@ static int qf_parse_multiline_pfx(qf_info_T *qi, int qf_idx, int idx,
}
qfprev->qf_viscol = fields->use_viscol;
if (!qfprev->qf_fnum) {
- qfprev->qf_fnum = qf_get_fnum(qi, qf_idx, qfl->qf_directory,
+ qfprev->qf_fnum = qf_get_fnum(qfl, qfl->qf_directory,
*fields->namebuf || qfl->qf_directory
? fields->namebuf
: qfl->qf_currfile && fields->valid
@@ -1431,7 +1694,18 @@ static int qf_parse_multiline_pfx(qf_info_T *qi, int qf_idx, int idx,
return QF_IGNORE_LINE;
}
-/// Free a location list.
+/// Queue location list stack delete request.
+static void locstack_queue_delreq(qf_info_T *qi)
+{
+ qf_delq_T *q;
+
+ q = xmalloc(sizeof(qf_delq_T));
+ q->qi = qi;
+ q->next = qf_delq_head;
+ qf_delq_head = q;
+}
+
+/// Free a location list stack
static void ll_free_all(qf_info_T **pqi)
{
int i;
@@ -1444,10 +1718,17 @@ static void ll_free_all(qf_info_T **pqi)
qi->qf_refcount--;
if (qi->qf_refcount < 1) {
- /* No references to this location list */
- for (i = 0; i < qi->qf_listcount; ++i)
- qf_free(qi, i);
- xfree(qi);
+ // No references to this location list.
+ // If the location list is still in use, then queue the delete request
+ // to be processed later.
+ if (quickfix_busy > 0) {
+ locstack_queue_delreq(qi);
+ } else {
+ for (i = 0; i < qi->qf_listcount; i++) {
+ qf_free(qf_get_list(qi, i));
+ }
+ xfree(qi);
+ }
}
}
@@ -1461,16 +1742,61 @@ void qf_free_all(win_T *wp)
/* location list */
ll_free_all(&wp->w_llist);
ll_free_all(&wp->w_llist_ref);
- } else
- /* quickfix list */
- for (i = 0; i < qi->qf_listcount; ++i)
- qf_free(qi, i);
+ } else {
+ // quickfix list
+ for (i = 0; i < qi->qf_listcount; i++) {
+ qf_free(qf_get_list(qi, i));
+ }
+ }
+}
+
+/// Delay freeing of location list stacks when the quickfix code is running.
+/// Used to avoid problems with autocmds freeing location list stacks when the
+/// quickfix code is still referencing the stack.
+/// Must always call decr_quickfix_busy() exactly once after this.
+static void incr_quickfix_busy(void)
+{
+ quickfix_busy++;
+}
+
+/// Safe to free location list stacks. Process any delayed delete requests.
+static void decr_quickfix_busy(void)
+{
+ quickfix_busy--;
+ if (quickfix_busy == 0) {
+ // No longer referencing the location lists. Process all the pending
+ // delete requests.
+ while (qf_delq_head != NULL) {
+ qf_delq_T *q = qf_delq_head;
+
+ qf_delq_head = q->next;
+ ll_free_all(&q->qi);
+ xfree(q);
+ }
+ }
+#ifdef ABORT_ON_INTERNAL_ERROR
+ if (quickfix_busy < 0) {
+ EMSG("quickfix_busy has become negative");
+ abort();
+ }
+#endif
+}
+
+#if defined(EXITFREE)
+void check_quickfix_busy(void)
+{
+ if (quickfix_busy != 0) {
+ EMSGN("quickfix_busy not zero on exit: %ld", (long)quickfix_busy);
+# ifdef ABORT_ON_INTERNAL_ERROR
+ abort();
+# endif
+ }
}
+#endif
/// Add an entry to the end of the list of errors.
///
-/// @param qi quickfix list
-/// @param qf_idx list index
+/// @param qfl quickfix list entry
/// @param dir optional directory name
/// @param fname file name or NULL
/// @param module module name or NULL
@@ -1484,8 +1810,8 @@ void qf_free_all(win_T *wp)
/// @param type type character
/// @param valid valid entry
///
-/// @returns OK or FAIL.
-static int qf_add_entry(qf_info_T *qi, int qf_idx, char_u *dir, char_u *fname,
+/// @returns QF_OK or QF_FAIL.
+static int qf_add_entry(qf_list_T *qfl, char_u *dir, char_u *fname,
char_u *module, int bufnum, char_u *mesg, long lnum,
int col, char_u vis_col, char_u *pattern, int nr,
char_u type, char_u valid)
@@ -1499,10 +1825,10 @@ static int qf_add_entry(qf_info_T *qi, int qf_idx, char_u *dir, char_u *fname,
qfp->qf_fnum = bufnum;
if (buf != NULL) {
buf->b_has_qf_entry |=
- IS_QF_STACK(qi) ? BUF_HAS_QF_ENTRY : BUF_HAS_LL_ENTRY;
+ IS_QF_LIST(qfl) ? BUF_HAS_QF_ENTRY : BUF_HAS_LL_ENTRY;
}
} else {
- qfp->qf_fnum = qf_get_fnum(qi, qf_idx, dir, fname);
+ qfp->qf_fnum = qf_get_fnum(qfl, dir, fname);
}
qfp->qf_text = vim_strsave(mesg);
qfp->qf_lnum = lnum;
@@ -1525,12 +1851,12 @@ static int qf_add_entry(qf_info_T *qi, int qf_idx, char_u *dir, char_u *fname,
qfp->qf_type = (char_u)type;
qfp->qf_valid = valid;
- lastp = &qi->qf_lists[qf_idx].qf_last;
- if (qi->qf_lists[qf_idx].qf_count == 0) {
+ lastp = &qfl->qf_last;
+ if (qf_list_empty(qfl)) {
// first element in the list
- qi->qf_lists[qf_idx].qf_start = qfp;
- qi->qf_lists[qf_idx].qf_ptr = qfp;
- qi->qf_lists[qf_idx].qf_index = 0;
+ qfl->qf_start = qfp;
+ qfl->qf_ptr = qfp;
+ qfl->qf_index = 0;
qfp->qf_prev = NULL;
} else {
assert(*lastp);
@@ -1540,33 +1866,31 @@ static int qf_add_entry(qf_info_T *qi, int qf_idx, char_u *dir, char_u *fname,
qfp->qf_next = NULL;
qfp->qf_cleared = false;
*lastp = qfp;
- qi->qf_lists[qf_idx].qf_count++;
- if (qi->qf_lists[qf_idx].qf_index == 0 && qfp->qf_valid) {
+ qfl->qf_count++;
+ if (qfl->qf_index == 0 && qfp->qf_valid) {
// first valid entry
- qi->qf_lists[qf_idx].qf_index = qi->qf_lists[qf_idx].qf_count;
- qi->qf_lists[qf_idx].qf_ptr = qfp;
+ qfl->qf_index = qfl->qf_count;
+ qfl->qf_ptr = qfp;
}
- return OK;
+ return QF_OK;
}
-/*
- * Allocate a new location list
- */
-static qf_info_T *ll_new_list(void)
+/// Allocate a new quickfix/location list stack
+static qf_info_T *qf_alloc_stack(qfltype_T qfltype)
FUNC_ATTR_NONNULL_RET
{
qf_info_T *qi = xcalloc(1, sizeof(qf_info_T));
qi->qf_refcount++;
+ qi->qfl_type = qfltype;
return qi;
}
-/*
- * Return the location list for window 'wp'.
- * If not present, allocate a location list
- */
+/// Return the location list stack for window 'wp'.
+/// If not present, allocate a location list stack
static qf_info_T *ll_get_or_alloc_list(win_T *wp)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET
{
if (IS_LL_WINDOW(wp))
/* For a location list window, use the referenced location list */
@@ -1578,127 +1902,176 @@ static qf_info_T *ll_get_or_alloc_list(win_T *wp)
*/
ll_free_all(&wp->w_llist_ref);
- if (wp->w_llist == NULL)
- wp->w_llist = ll_new_list(); /* new location list */
+ if (wp->w_llist == NULL) {
+ wp->w_llist = qf_alloc_stack(QFLT_LOCATION); // new location list
+ }
return wp->w_llist;
}
-/*
- * Copy the location list from window "from" to window "to".
- */
-void copy_loclist(win_T *from, win_T *to)
+/// Get the quickfix/location list stack to use for the specified Ex command.
+/// For a location list command, returns the stack for the current window. If
+/// the location list is not found, then returns NULL and prints an error
+/// message if 'print_emsg' is TRUE.
+static qf_info_T * qf_cmd_get_stack(exarg_T *eap, int print_emsg)
+{
+ qf_info_T *qi = &ql_info;
+
+ if (is_loclist_cmd(eap->cmdidx)) {
+ qi = GET_LOC_LIST(curwin);
+ if (qi == NULL) {
+ if (print_emsg) {
+ EMSG(_(e_loclist));
+ }
+ return NULL;
+ }
+ }
+
+ return qi;
+}
+
+/// Get the quickfix/location list stack to use for the specified Ex command.
+/// For a location list command, returns the stack for the current window.
+/// If the location list is not present, then allocates a new one.
+/// For a location list command, sets 'pwinp' to curwin.
+static qf_info_T *qf_cmd_get_or_alloc_stack(const exarg_T *eap, win_T **pwinp)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET
+{
+ qf_info_T *qi = &ql_info;
+
+ if (is_loclist_cmd(eap->cmdidx)) {
+ qi = ll_get_or_alloc_list(curwin);
+ *pwinp = curwin;
+ }
+
+ return qi;
+}
+
+/// Copy location list entries from 'from_qfl' to 'to_qfl'.
+static int copy_loclist_entries(const qf_list_T *from_qfl, qf_list_T *to_qfl)
+ FUNC_ATTR_NONNULL_ALL
{
- qf_info_T *qi;
- int idx;
int i;
+ qfline_T *from_qfp;
+
+ // copy all the location entries in this list
+ FOR_ALL_QFL_ITEMS(from_qfl, from_qfp, i) {
+ if (qf_add_entry(to_qfl,
+ NULL,
+ NULL,
+ from_qfp->qf_module,
+ 0,
+ from_qfp->qf_text,
+ from_qfp->qf_lnum,
+ from_qfp->qf_col,
+ from_qfp->qf_viscol,
+ from_qfp->qf_pattern,
+ from_qfp->qf_nr,
+ 0,
+ from_qfp->qf_valid) == QF_FAIL) {
+ return FAIL;
+ }
- /*
- * When copying from a location list window, copy the referenced
- * location list. For other windows, copy the location list for
- * that window.
- */
- if (IS_LL_WINDOW(from))
+ // qf_add_entry() will not set the qf_num field, as the
+ // directory and file names are not supplied. So the qf_fnum
+ // field is copied here.
+ qfline_T *const prevp = to_qfl->qf_last;
+ prevp->qf_fnum = from_qfp->qf_fnum; // file number
+ prevp->qf_type = from_qfp->qf_type; // error type
+ if (from_qfl->qf_ptr == from_qfp) {
+ to_qfl->qf_ptr = prevp; // current location
+ }
+ }
+
+ return OK;
+}
+
+/// Copy the specified location list 'from_qfl' to 'to_qfl'.
+static int copy_loclist(const qf_list_T *from_qfl, qf_list_T *to_qfl)
+ FUNC_ATTR_NONNULL_ALL
+{
+ // Some of the fields are populated by qf_add_entry()
+ to_qfl->qfl_type = from_qfl->qfl_type;
+ to_qfl->qf_nonevalid = from_qfl->qf_nonevalid;
+ to_qfl->qf_count = 0;
+ to_qfl->qf_index = 0;
+ to_qfl->qf_start = NULL;
+ to_qfl->qf_last = NULL;
+ to_qfl->qf_ptr = NULL;
+ if (from_qfl->qf_title != NULL) {
+ to_qfl->qf_title = vim_strsave(from_qfl->qf_title);
+ } else {
+ to_qfl->qf_title = NULL;
+ }
+ if (from_qfl->qf_ctx != NULL) {
+ to_qfl->qf_ctx = xcalloc(1, sizeof(*to_qfl->qf_ctx));
+ tv_copy(from_qfl->qf_ctx, to_qfl->qf_ctx);
+ } else {
+ to_qfl->qf_ctx = NULL;
+ }
+
+ if (from_qfl->qf_count) {
+ if (copy_loclist_entries(from_qfl, to_qfl) == FAIL) {
+ return FAIL;
+ }
+ }
+
+ to_qfl->qf_index = from_qfl->qf_index; // current index in the list
+
+ // Assign a new ID for the location list
+ to_qfl->qf_id = ++last_qf_id;
+ to_qfl->qf_changedtick = 0L;
+
+ // When no valid entries are present in the list, qf_ptr points to
+ // the first item in the list
+ if (to_qfl->qf_nonevalid) {
+ to_qfl->qf_ptr = to_qfl->qf_start;
+ to_qfl->qf_index = 1;
+ }
+
+ return OK;
+}
+
+// Copy the location list stack 'from' window to 'to' window.
+void copy_loclist_stack(win_T *from, win_T *to)
+ FUNC_ATTR_NONNULL_ALL
+{
+ qf_info_T *qi;
+
+ // When copying from a location list window, copy the referenced
+ // location list. For other windows, copy the location list for
+ // that window.
+ if (IS_LL_WINDOW(from)) {
qi = from->w_llist_ref;
- else
+ } else {
qi = from->w_llist;
+ }
- if (qi == NULL) /* no location list to copy */
+ if (qi == NULL) { // no location list to copy
return;
+ }
- /* allocate a new location list */
- to->w_llist = ll_new_list();
+ // allocate a new location list
+ to->w_llist = qf_alloc_stack(QFLT_LOCATION);
to->w_llist->qf_listcount = qi->qf_listcount;
- /* Copy the location lists one at a time */
- for (idx = 0; idx < qi->qf_listcount; idx++) {
- qf_list_T *from_qfl;
- qf_list_T *to_qfl;
-
+ // Copy the location lists one at a time
+ for (int idx = 0; idx < qi->qf_listcount; idx++) {
to->w_llist->qf_curlist = idx;
- from_qfl = &qi->qf_lists[idx];
- to_qfl = &to->w_llist->qf_lists[idx];
-
- /* Some of the fields are populated by qf_add_entry() */
- to_qfl->qf_nonevalid = from_qfl->qf_nonevalid;
- to_qfl->qf_count = 0;
- to_qfl->qf_index = 0;
- to_qfl->qf_start = NULL;
- to_qfl->qf_last = NULL;
- to_qfl->qf_ptr = NULL;
- if (from_qfl->qf_title != NULL)
- to_qfl->qf_title = vim_strsave(from_qfl->qf_title);
- else
- to_qfl->qf_title = NULL;
-
- if (from_qfl->qf_ctx != NULL) {
- to_qfl->qf_ctx = xcalloc(1, sizeof(typval_T));
- tv_copy(from_qfl->qf_ctx, to_qfl->qf_ctx);
- } else {
- to_qfl->qf_ctx = NULL;
- }
-
- if (from_qfl->qf_count) {
- qfline_T *from_qfp;
- qfline_T *prevp;
-
- // copy all the location entries in this list
- for (i = 0, from_qfp = from_qfl->qf_start;
- i < from_qfl->qf_count && from_qfp != NULL;
- i++, from_qfp = from_qfp->qf_next) {
- if (qf_add_entry(to->w_llist,
- to->w_llist->qf_curlist,
- NULL,
- NULL,
- from_qfp->qf_module,
- 0,
- from_qfp->qf_text,
- from_qfp->qf_lnum,
- from_qfp->qf_col,
- from_qfp->qf_viscol,
- from_qfp->qf_pattern,
- from_qfp->qf_nr,
- 0,
- from_qfp->qf_valid) == FAIL) {
- qf_free_all(to);
- return;
- }
- /*
- * qf_add_entry() will not set the qf_num field, as the
- * directory and file names are not supplied. So the qf_fnum
- * field is copied here.
- */
- prevp = to->w_llist->qf_lists[to->w_llist->qf_curlist].qf_last;
- prevp->qf_fnum = from_qfp->qf_fnum; // file number
- prevp->qf_type = from_qfp->qf_type; // error type
- if (from_qfl->qf_ptr == from_qfp) {
- to_qfl->qf_ptr = prevp; // current location
- }
- }
- }
-
- to_qfl->qf_index = from_qfl->qf_index; /* current index in the list */
-
- // Assign a new ID for the location list
- to_qfl->qf_id = ++last_qf_id;
- to_qfl->qf_changedtick = 0L;
-
- /* When no valid entries are present in the list, qf_ptr points to
- * the first item in the list */
- if (to_qfl->qf_nonevalid) {
- to_qfl->qf_ptr = to_qfl->qf_start;
- to_qfl->qf_index = 1;
+ if (copy_loclist(qf_get_list(qi, idx),
+ qf_get_list(to->w_llist, idx)) == FAIL) {
+ qf_free_all(to);
+ return;
}
}
- to->w_llist->qf_curlist = qi->qf_curlist; /* current list */
+ to->w_llist->qf_curlist = qi->qf_curlist; // current list
}
-// Get buffer number for file "directory/fname".
-// Also sets the b_has_qf_entry flag.
-static int qf_get_fnum(qf_info_T *qi, int qf_idx, char_u *directory,
- char_u *fname)
+/// Get buffer number for file "directory/fname".
+/// Also sets the b_has_qf_entry flag.
+static int qf_get_fnum(qf_list_T *qfl, char_u *directory, char_u *fname )
{
char_u *ptr = NULL;
char_u *bufname;
@@ -1721,7 +2094,7 @@ static int qf_get_fnum(qf_info_T *qi, int qf_idx, char_u *directory,
// directory change.
if (!os_path_exists(ptr)) {
xfree(ptr);
- directory = qf_guess_filepath(qi, qf_idx, fname);
+ directory = qf_guess_filepath(qfl, fname);
if (directory) {
ptr = (char_u *)concat_fnames((char *)directory, (char *)fname, true);
} else {
@@ -1749,7 +2122,7 @@ static int qf_get_fnum(qf_info_T *qi, int qf_idx, char_u *directory,
return 0;
}
buf->b_has_qf_entry =
- IS_QF_STACK(qi) ? BUF_HAS_QF_ENTRY : BUF_HAS_LL_ENTRY;
+ IS_QF_LIST(qfl) ? BUF_HAS_QF_ENTRY : BUF_HAS_LL_ENTRY;
return buf->b_fnum;
}
@@ -1851,30 +2224,29 @@ static void qf_clean_dir_stack(struct dir_stack_T **stackptr)
}
}
-// Check in which directory of the directory stack the given file can be
-// found.
-// Returns a pointer to the directory name or NULL if not found.
-// Cleans up intermediate directory entries.
-//
-// TODO(vim): How to solve the following problem?
-// If we have this directory tree:
-// ./
-// ./aa
-// ./aa/bb
-// ./bb
-// ./bb/x.c
-// and make says:
-// making all in aa
-// making all in bb
-// x.c:9: Error
-// Then qf_push_dir thinks we are in ./aa/bb, but we are in ./bb.
-// qf_guess_filepath will return NULL.
-static char_u *qf_guess_filepath(qf_info_T *qi, int qf_idx, char_u *filename)
+/// Check in which directory of the directory stack the given file can be
+/// found.
+/// Returns a pointer to the directory name or NULL if not found.
+/// Cleans up intermediate directory entries.
+///
+/// TODO(vim): How to solve the following problem?
+/// If we have this directory tree:
+/// ./
+/// ./aa
+/// ./aa/bb
+/// ./bb
+/// ./bb/x.c
+/// and make says:
+/// making all in aa
+/// making all in bb
+/// x.c:9: Error
+/// Then qf_push_dir thinks we are in ./aa/bb, but we are in ./bb.
+/// qf_guess_filepath will return NULL.
+static char_u *qf_guess_filepath(qf_list_T *qfl, char_u *filename)
{
struct dir_stack_T *ds_ptr;
struct dir_stack_T *ds_tmp;
char_u *fullname;
- qf_list_T *qfl = &qi->qf_lists[qf_idx];
// no dirs on the stack - there's nothing we can do
if (qfl->qf_dir_stack == NULL) {
@@ -1932,22 +2304,19 @@ static bool qflist_valid(win_T *wp, unsigned int qf_id)
/// This may invalidate the current quickfix entry. This function checks
/// whether an entry is still present in the quickfix list.
/// Similar to location list.
-static bool is_qf_entry_present(qf_info_T *qi, qfline_T *qf_ptr)
+static bool is_qf_entry_present(qf_list_T *qfl, qfline_T *qf_ptr)
{
- qf_list_T *qfl;
qfline_T *qfp;
int i;
- qfl = &qi->qf_lists[qi->qf_curlist];
-
// Search for the entry in the current list
- for (i = 0, qfp = qfl->qf_start; i < qfl->qf_count; i++, qfp = qfp->qf_next) {
- if (qfp == NULL || qfp == qf_ptr) {
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
+ if (qfp == qf_ptr) {
break;
}
}
- if (i == qfl->qf_count) { // Entry is not found
+ if (i > qfl->qf_count) { // Entry is not found
return false;
}
@@ -1956,20 +2325,19 @@ static bool is_qf_entry_present(qf_info_T *qi, qfline_T *qf_ptr)
/// Get the next valid entry in the current quickfix/location list. The search
/// starts from the current entry. Returns NULL on failure.
-static qfline_T *get_next_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
+static qfline_T *get_next_valid_entry(qf_list_T *qfl, qfline_T *qf_ptr,
int *qf_index, int dir)
{
int idx = *qf_index;
int old_qf_fnum = qf_ptr->qf_fnum;
do {
- if (idx == qi->qf_lists[qi->qf_curlist].qf_count
- || qf_ptr->qf_next == NULL) {
+ if (idx == qfl->qf_count || qf_ptr->qf_next == NULL) {
return NULL;
}
idx++;
qf_ptr = qf_ptr->qf_next;
- } while ((!qi->qf_lists[qi->qf_curlist].qf_nonevalid && !qf_ptr->qf_valid)
+ } while ((!qfl->qf_nonevalid && !qf_ptr->qf_valid)
|| (dir == FORWARD_FILE && qf_ptr->qf_fnum == old_qf_fnum));
*qf_index = idx;
@@ -1978,7 +2346,7 @@ static qfline_T *get_next_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
/// Get the previous valid entry in the current quickfix/location list. The
/// search starts from the current entry. Returns NULL on failure.
-static qfline_T *get_prev_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
+static qfline_T *get_prev_valid_entry(qf_list_T *qfl, qfline_T *qf_ptr,
int *qf_index, int dir)
{
int idx = *qf_index;
@@ -1990,7 +2358,7 @@ static qfline_T *get_prev_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
}
idx--;
qf_ptr = qf_ptr->qf_prev;
- } while ((!qi->qf_lists[qi->qf_curlist].qf_nonevalid && !qf_ptr->qf_valid)
+ } while ((!qfl->qf_nonevalid && !qf_ptr->qf_valid)
|| (dir == BACKWARD_FILE && qf_ptr->qf_fnum == old_qf_fnum));
*qf_index = idx;
@@ -2001,12 +2369,11 @@ static qfline_T *get_prev_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
/// the quickfix list.
/// dir == FORWARD or FORWARD_FILE: next valid entry
/// dir == BACKWARD or BACKWARD_FILE: previous valid entry
-static qfline_T *get_nth_valid_entry(qf_info_T *qi, int errornr,
+static qfline_T *get_nth_valid_entry(qf_list_T *qfl, int errornr,
qfline_T *qf_ptr, int *qf_index, int dir)
{
qfline_T *prev_qf_ptr;
int prev_index;
- static char_u *e_no_more_items = (char_u *)N_("E553: No more items");
char_u *err = e_no_more_items;
while (errornr--) {
@@ -2014,9 +2381,9 @@ static qfline_T *get_nth_valid_entry(qf_info_T *qi, int errornr,
prev_index = *qf_index;
if (dir == FORWARD || dir == FORWARD_FILE) {
- qf_ptr = get_next_valid_entry(qi, qf_ptr, qf_index, dir);
+ qf_ptr = get_next_valid_entry(qfl, qf_ptr, qf_index, dir);
} else {
- qf_ptr = get_prev_valid_entry(qi, qf_ptr, qf_index, dir);
+ qf_ptr = get_prev_valid_entry(qfl, qf_ptr, qf_index, dir);
}
if (qf_ptr == NULL) {
@@ -2036,7 +2403,7 @@ static qfline_T *get_nth_valid_entry(qf_info_T *qi, int errornr,
}
/// Get n'th (errornr) quickfix entry
-static qfline_T *get_nth_entry(qf_info_T *qi, int errornr, qfline_T *qf_ptr,
+static qfline_T *get_nth_entry(qf_list_T *qfl, int errornr, qfline_T *qf_ptr,
int *cur_qfidx)
{
int qf_idx = *cur_qfidx;
@@ -2049,7 +2416,7 @@ static qfline_T *get_nth_entry(qf_info_T *qi, int errornr, qfline_T *qf_ptr,
// New error number is greater than the current error number
while (errornr > qf_idx
- && qf_idx < qi->qf_lists[qi->qf_curlist].qf_count
+ && qf_idx < qfl->qf_count
&& qf_ptr->qf_next != NULL) {
qf_idx++;
qf_ptr = qf_ptr->qf_next;
@@ -2071,6 +2438,13 @@ static win_T *qf_find_help_win(void)
return NULL;
}
+/// Set the location list for the specified window to 'qi'.
+static void win_set_loclist(win_T *wp, qf_info_T *qi)
+{
+ wp->w_llist = qi;
+ qi->qf_refcount++;
+}
+
/// Find a help window or open one.
static int jump_to_help_window(qf_info_T *qi, int *opened_window)
{
@@ -2110,8 +2484,7 @@ static int jump_to_help_window(qf_info_T *qi, int *opened_window)
if (IS_LL_STACK(qi)) { // not a quickfix list
// The new window should use the supplied location list
- curwin->w_llist = qi;
- qi->qf_refcount++;
+ win_set_loclist(curwin, qi);
}
}
@@ -2147,7 +2520,7 @@ static win_T *qf_find_win_with_normal_buf(void)
return NULL;
}
-// Go to a window in any tabpage containing the specified file. Returns TRUE
+// Go to a window in any tabpage containing the specified file. Returns true
// if successfully jumped to the window. Otherwise returns FALSE.
static bool qf_goto_tabwin_with_file(int fnum)
{
@@ -2177,8 +2550,7 @@ static int qf_open_new_file_win(qf_info_T *ll_ref)
if (ll_ref != NULL) {
// The new window should use the location list from the
// location list window
- curwin->w_llist = ll_ref;
- ll_ref->qf_refcount++;
+ win_set_loclist(curwin, ll_ref);
}
return OK;
}
@@ -2219,9 +2591,10 @@ static void qf_goto_win_with_ll_file(win_T *use_win, int qf_fnum,
// If the location list for the window is not set, then set it
// to the location list from the location window
- if (win->w_llist == NULL) {
- win->w_llist = ll_ref;
- ll_ref->qf_refcount++;
+ if (win->w_llist == NULL && ll_ref != NULL) {
+ // The new window should use the location list from the
+ // location list window
+ win_set_loclist(win, ll_ref);
}
}
@@ -2320,6 +2693,8 @@ static int qf_jump_to_usable_window(int qf_fnum, int *opened_window)
static int qf_jump_edit_buffer(qf_info_T *qi, qfline_T *qf_ptr, int forceit,
win_T *oldwin, int *opened_window, int *abort)
{
+ qf_list_T *qfl = qf_get_curlist(qi);
+ qfltype_T qfl_type = qfl->qfl_type;
int retval = OK;
if (qf_ptr->qf_type == 1) {
@@ -2334,12 +2709,12 @@ static int qf_jump_edit_buffer(qf_info_T *qi, qfline_T *qf_ptr, int forceit,
oldwin == curwin ? curwin : NULL);
}
} else {
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qfl->qf_id;
retval = buflist_getfile(qf_ptr->qf_fnum, (linenr_T)1,
GETF_SETMARK | GETF_SWITCH, forceit);
- if (IS_LL_STACK(qi)) {
+ if (qfl_type == QFLT_LOCATION) {
// Location list. Check whether the associated window is still
// present and the list is still valid.
if (!win_valid_any_tab(oldwin)) {
@@ -2350,8 +2725,8 @@ static int qf_jump_edit_buffer(qf_info_T *qi, qfline_T *qf_ptr, int forceit,
EMSG(_(e_loc_list_changed));
*abort = true;
}
- } else if (!is_qf_entry_present(qi, qf_ptr)) {
- if (IS_QF_STACK(qi)) {
+ } else if (!is_qf_entry_present(qfl, qf_ptr)) {
+ if (qfl_type == QFLT_QUICKFIX) {
EMSG(_("E925: Current quickfix was changed"));
} else {
EMSG(_(e_loc_list_changed));
@@ -2373,9 +2748,6 @@ static void qf_jump_goto_line(linenr_T qf_lnum, int qf_col, char_u qf_viscol,
char_u *qf_pattern)
{
linenr_T i;
- char_u *line;
- colnr_T screen_col;
- colnr_T char_col;
if (qf_pattern == NULL) {
// Go to line with error, unless qf_lnum is 0.
@@ -2387,26 +2759,11 @@ static void qf_jump_goto_line(linenr_T qf_lnum, int qf_col, char_u qf_viscol,
curwin->w_cursor.lnum = i;
}
if (qf_col > 0) {
- curwin->w_cursor.col = qf_col - 1;
curwin->w_cursor.coladd = 0;
if (qf_viscol == true) {
- // Check each character from the beginning of the error
- // line up to the error column. For each tab character
- // found, reduce the error column value by the length of
- // a tab character.
- line = get_cursor_line_ptr();
- screen_col = 0;
- for (char_col = 0; char_col < curwin->w_cursor.col; char_col++) {
- if (*line == NUL) {
- break;
- }
- if (*line++ == '\t') {
- curwin->w_cursor.col -= 7 - (screen_col % 8);
- screen_col += 8 - (screen_col % 8);
- } else {
- screen_col++;
- }
- }
+ coladvance(qf_col - 1);
+ } else {
+ curwin->w_cursor.col = qf_col - 1;
}
curwin->w_set_curswant = true;
check_cursor();
@@ -2417,7 +2774,7 @@ static void qf_jump_goto_line(linenr_T qf_lnum, int qf_col, char_u qf_viscol,
// Move the cursor to the first line in the buffer
pos_T save_cursor = curwin->w_cursor;
curwin->w_cursor.lnum = 0;
- if (!do_search(NULL, '/', qf_pattern, (long)1, SEARCH_KEEP, NULL, NULL)) {
+ if (!do_search(NULL, '/', qf_pattern, (long)1, SEARCH_KEEP, NULL)) {
curwin->w_cursor = save_cursor;
}
}
@@ -2433,7 +2790,7 @@ static void qf_jump_print_msg(qf_info_T *qi, int qf_index, qfline_T *qf_ptr,
update_topline_redraw();
}
snprintf((char *)IObuff, IOSIZE, _("(%d of %d)%s%s: "), qf_index,
- qi->qf_lists[qi->qf_curlist].qf_count,
+ qf_get_curlist(qi)->qf_count,
qf_ptr->qf_cleared ? _(" (line deleted)") : "",
(char *)qf_types(qf_ptr->qf_type, qf_ptr->qf_nr));
// Add the message, skipping leading whitespace and newlines.
@@ -2463,6 +2820,7 @@ static void qf_jump_print_msg(qf_info_T *qi, int qf_index, qfline_T *qf_ptr,
/// else go to entry "errornr"
void qf_jump(qf_info_T *qi, int dir, int errornr, int forceit)
{
+ qf_list_T *qfl;
qfline_T *qf_ptr;
qfline_T *old_qf_ptr;
int qf_index;
@@ -2480,32 +2838,34 @@ void qf_jump(qf_info_T *qi, int dir, int errornr, int forceit)
if (qi == NULL)
qi = &ql_info;
- if (qi->qf_curlist >= qi->qf_listcount
- || qi->qf_lists[qi->qf_curlist].qf_count == 0) {
+ if (qf_stack_empty(qi) || qf_list_empty(qf_get_curlist(qi))) {
EMSG(_(e_quickfix));
return;
}
- qf_ptr = qi->qf_lists[qi->qf_curlist].qf_ptr;
+ qfl = qf_get_curlist(qi);
+
+ qf_ptr = qfl->qf_ptr;
old_qf_ptr = qf_ptr;
- qf_index = qi->qf_lists[qi->qf_curlist].qf_index;
+ qf_index = qfl->qf_index;
old_qf_index = qf_index;
if (dir != 0) { // next/prev valid entry
- qf_ptr = get_nth_valid_entry(qi, errornr, qf_ptr, &qf_index, dir);
+ qf_ptr = get_nth_valid_entry(qfl, errornr, qf_ptr, &qf_index, dir);
if (qf_ptr == NULL) {
qf_ptr = old_qf_ptr;
qf_index = old_qf_index;
goto theend; // The horror... the horror...
}
} else if (errornr != 0) { // go to specified number
- qf_ptr = get_nth_entry(qi, errornr, qf_ptr, &qf_index);
+ qf_ptr = get_nth_entry(qfl, errornr, qf_ptr, &qf_index);
}
- qi->qf_lists[qi->qf_curlist].qf_index = qf_index;
- if (qf_win_pos_update(qi, old_qf_index))
- /* No need to print the error message if it's visible in the error
- * window */
- print_message = FALSE;
+ qfl->qf_index = qf_index;
+ if (qf_win_pos_update(qi, old_qf_index)) {
+ // No need to print the error message if it's visible in the error
+ // window
+ print_message = false;
+ }
// For ":helpgrep" find a help window or open one.
if (qf_ptr->qf_type == 1 && (!bt_help(curwin->w_buffer) || cmdmod.tab != 0)) {
@@ -2574,8 +2934,8 @@ failed:
}
theend:
if (qi != NULL) {
- qi->qf_lists[qi->qf_curlist].qf_ptr = qf_ptr;
- qi->qf_lists[qi->qf_curlist].qf_index = qf_index;
+ qfl->qf_ptr = qf_ptr;
+ qfl->qf_index = qf_index;
}
if (p_swb != old_swb && opened_window) {
/* Restore old 'switchbuf' value, but not when an autocommand or
@@ -2588,36 +2948,117 @@ theend:
}
}
+
+// Highlight attributes used for displaying entries from the quickfix list.
+static int qfFileAttr;
+static int qfSepAttr;
+static int qfLineAttr;
+
+/// Display information about a single entry from the quickfix/location list.
+/// Used by ":clist/:llist" commands.
+/// 'cursel' will be set to true for the currently selected entry in the
+/// quickfix list.
+static void qf_list_entry(qfline_T *qfp, int qf_idx, bool cursel)
+{
+ char_u *fname;
+ buf_T *buf;
+
+ fname = NULL;
+ if (qfp->qf_module != NULL && *qfp->qf_module != NUL) {
+ vim_snprintf((char *)IObuff, IOSIZE, "%2d %s", qf_idx,
+ (char *)qfp->qf_module);
+ } else {
+ if (qfp->qf_fnum != 0
+ && (buf = buflist_findnr(qfp->qf_fnum)) != NULL) {
+ fname = buf->b_fname;
+ if (qfp->qf_type == 1) { // :helpgrep
+ fname = path_tail(fname);
+ }
+ }
+ if (fname == NULL) {
+ snprintf((char *)IObuff, IOSIZE, "%2d", qf_idx);
+ } else {
+ vim_snprintf((char *)IObuff, IOSIZE, "%2d %s",
+ qf_idx, (char *)fname);
+ }
+ }
+
+ // Support for filtering entries using :filter /pat/ clist
+ // Match against the module name, file name, search pattern and
+ // text of the entry.
+ bool filter_entry = true;
+ if (qfp->qf_module != NULL && *qfp->qf_module != NUL) {
+ filter_entry &= message_filtered(qfp->qf_module);
+ }
+ if (filter_entry && fname != NULL) {
+ filter_entry &= message_filtered(fname);
+ }
+ if (filter_entry && qfp->qf_pattern != NULL) {
+ filter_entry &= message_filtered(qfp->qf_pattern);
+ }
+ if (filter_entry) {
+ filter_entry &= message_filtered(qfp->qf_text);
+ }
+ if (filter_entry) {
+ return;
+ }
+
+ msg_putchar('\n');
+ msg_outtrans_attr(IObuff, cursel ? HL_ATTR(HLF_QFL) : qfFileAttr);
+
+ if (qfp->qf_lnum != 0) {
+ msg_puts_attr(":", qfSepAttr);
+ }
+ if (qfp->qf_lnum == 0) {
+ IObuff[0] = NUL;
+ } else if (qfp->qf_col == 0) {
+ vim_snprintf((char *)IObuff, IOSIZE, "%" PRIdLINENR, qfp->qf_lnum);
+ } else {
+ vim_snprintf((char *)IObuff, IOSIZE, "%" PRIdLINENR " col %d",
+ qfp->qf_lnum, qfp->qf_col);
+ }
+ vim_snprintf((char *)IObuff + STRLEN(IObuff), IOSIZE, "%s",
+ (char *)qf_types(qfp->qf_type, qfp->qf_nr));
+ msg_puts_attr((const char *)IObuff, qfLineAttr);
+ msg_puts_attr(":", qfSepAttr);
+ if (qfp->qf_pattern != NULL) {
+ qf_fmt_text(qfp->qf_pattern, IObuff, IOSIZE);
+ msg_puts((const char *)IObuff);
+ msg_puts_attr(":", qfSepAttr);
+ }
+ msg_puts(" ");
+
+ // Remove newlines and leading whitespace from the text. For an
+ // unrecognized line keep the indent, the compiler may mark a word
+ // with ^^^^. */
+ qf_fmt_text((fname != NULL || qfp->qf_lnum != 0)
+ ? skipwhite(qfp->qf_text) : qfp->qf_text,
+ IObuff, IOSIZE);
+ msg_prt_line(IObuff, false);
+ ui_flush(); // show one line at a time
+}
+
/*
* ":clist": list all errors
* ":llist": list all locations
*/
void qf_list(exarg_T *eap)
{
- buf_T *buf;
- char_u *fname;
- qfline_T *qfp;
+ qf_list_T *qfl;
+ qfline_T *qfp;
int i;
int idx1 = 1;
int idx2 = -1;
char_u *arg = eap->arg;
- int qfFileAttr;
- int qfSepAttr;
- int qfLineAttr;
int all = eap->forceit; // if not :cl!, only show
// recognised errors
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_llist) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
- if (qi->qf_curlist >= qi->qf_listcount
- || qi->qf_lists[qi->qf_curlist].qf_count == 0) {
+ if (qf_stack_empty(qi) || qf_list_empty(qf_get_curlist(qi))) {
EMSG(_(e_quickfix));
return;
}
@@ -2631,12 +3072,13 @@ void qf_list(exarg_T *eap)
EMSG(_(e_trailing));
return;
}
+ qfl = qf_get_curlist(qi);
if (plus) {
- i = qi->qf_lists[qi->qf_curlist].qf_index;
+ i = qfl->qf_index;
idx2 = i + idx1;
idx1 = i;
} else {
- i = qi->qf_lists[qi->qf_curlist].qf_count;
+ i = qfl->qf_count;
if (idx1 < 0) {
idx1 = (-idx1 > i) ? 0 : idx1 + i + 1;
}
@@ -2663,95 +3105,13 @@ void qf_list(exarg_T *eap)
qfLineAttr = HL_ATTR(HLF_N);
}
- if (qi->qf_lists[qi->qf_curlist].qf_nonevalid) {
+ if (qfl->qf_nonevalid) {
all = true;
}
- qfp = qi->qf_lists[qi->qf_curlist].qf_start;
- for (i = 1; !got_int && i <= qi->qf_lists[qi->qf_curlist].qf_count; ) {
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
if ((qfp->qf_valid || all) && idx1 <= i && i <= idx2) {
- if (got_int) {
- break;
- }
-
- fname = NULL;
- if (qfp->qf_module != NULL && *qfp->qf_module != NUL) {
- vim_snprintf((char *)IObuff, IOSIZE, "%2d %s", i,
- (char *)qfp->qf_module);
- } else {
- if (qfp->qf_fnum != 0 && (buf = buflist_findnr(qfp->qf_fnum)) != NULL) {
- fname = buf->b_fname;
- if (qfp->qf_type == 1) { // :helpgrep
- fname = path_tail(fname);
- }
- }
- if (fname == NULL) {
- snprintf((char *)IObuff, IOSIZE, "%2d", i);
- } else {
- vim_snprintf((char *)IObuff, IOSIZE, "%2d %s", i, (char *)fname);
- }
- }
-
- // Support for filtering entries using :filter /pat/ clist
- // Match against the module name, file name, search pattern and
- // text of the entry.
- bool filter_entry = true;
- if (qfp->qf_module != NULL && *qfp->qf_module != NUL) {
- filter_entry &= message_filtered(qfp->qf_module);
- }
- if (filter_entry && fname != NULL) {
- filter_entry &= message_filtered(fname);
- }
- if (filter_entry && qfp->qf_pattern != NULL) {
- filter_entry &= message_filtered(qfp->qf_pattern);
- }
- if (filter_entry) {
- filter_entry &= message_filtered(qfp->qf_text);
- }
- if (filter_entry) {
- goto next_entry;
- }
- msg_putchar('\n');
- msg_outtrans_attr(IObuff, i == qi->qf_lists[qi->qf_curlist].qf_index
- ? HL_ATTR(HLF_QFL) : qfFileAttr);
-
- if (qfp->qf_lnum != 0) {
- msg_puts_attr(":", qfSepAttr);
- }
- if (qfp->qf_lnum == 0) {
- IObuff[0] = NUL;
- } else if (qfp->qf_col == 0) {
- vim_snprintf((char *)IObuff, IOSIZE, "%" PRIdLINENR, qfp->qf_lnum);
- } else {
- vim_snprintf((char *)IObuff, IOSIZE, "%" PRIdLINENR " col %d",
- qfp->qf_lnum, qfp->qf_col);
- }
- vim_snprintf((char *)IObuff + STRLEN(IObuff), IOSIZE, "%s",
- (char *)qf_types(qfp->qf_type, qfp->qf_nr));
- msg_puts_attr((const char *)IObuff, qfLineAttr);
- msg_puts_attr(":", qfSepAttr);
- if (qfp->qf_pattern != NULL) {
- qf_fmt_text(qfp->qf_pattern, IObuff, IOSIZE);
- msg_puts((const char *)IObuff);
- msg_puts_attr(":", qfSepAttr);
- }
- msg_puts(" ");
-
- /* Remove newlines and leading whitespace from the text. For an
- * unrecognized line keep the indent, the compiler may mark a word
- * with ^^^^. */
- qf_fmt_text((fname != NULL || qfp->qf_lnum != 0)
- ? skipwhite(qfp->qf_text) : qfp->qf_text,
- IObuff, IOSIZE);
- msg_prt_line(IObuff, FALSE);
- ui_flush(); /* show one line at a time */
- }
-
-next_entry:
- qfp = qfp->qf_next;
- if (qfp == NULL) {
- break;
+ qf_list_entry(qfp, i, i == qfl->qf_index);
}
- i++;
os_breakcheck();
}
}
@@ -2760,10 +3120,12 @@ next_entry:
* Remove newlines and leading whitespace from an error message.
* Put the result in "buf[bufsize]".
*/
-static void qf_fmt_text(char_u *text, char_u *buf, int bufsize)
+static void qf_fmt_text(const char_u *restrict text, char_u *restrict buf,
+ int bufsize)
+ FUNC_ATTR_NONNULL_ALL
{
int i;
- char_u *p = text;
+ const char_u *p = text;
for (i = 0; *p != NUL && i < bufsize - 1; ++i) {
if (*p == '\n') {
@@ -2804,23 +3166,17 @@ static void qf_msg(qf_info_T *qi, int which, char *lead)
msg(buf);
}
-/*
- * ":colder [count]": Up in the quickfix stack.
- * ":cnewer [count]": Down in the quickfix stack.
- * ":lolder [count]": Up in the location list stack.
- * ":lnewer [count]": Down in the location list stack.
- */
+/// ":colder [count]": Up in the quickfix stack.
+/// ":cnewer [count]": Down in the quickfix stack.
+/// ":lolder [count]": Up in the location list stack.
+/// ":lnewer [count]": Down in the location list stack.
void qf_age(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
int count;
- if (eap->cmdidx == CMD_lolder || eap->cmdidx == CMD_lnewer) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
if (eap->addr_count != 0) {
@@ -2851,14 +3207,10 @@ void qf_age(exarg_T *eap)
/// Display the information about all the quickfix/location lists in the stack.
void qf_history(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi = qf_cmd_get_stack(eap, false);
int i;
- if (eap->cmdidx == CMD_lhistory) {
- qi = GET_LOC_LIST(curwin);
- }
- if (qi == NULL || (qi->qf_listcount == 0
- && qi->qf_lists[qi->qf_curlist].qf_count == 0)) {
+ if (qf_stack_empty(qi) || qf_list_empty(qf_get_curlist(qi))) {
MSG(_("No entries"));
} else {
for (i = 0; i < qi->qf_listcount; i++) {
@@ -2869,12 +3221,11 @@ void qf_history(exarg_T *eap)
/// Free all the entries in the error list "idx". Note that other information
/// associated with the list like context and title are not freed.
-static void qf_free_items(qf_info_T *qi, int idx)
+static void qf_free_items(qf_list_T *qfl)
{
qfline_T *qfp;
qfline_T *qfpnext;
bool stop = false;
- qf_list_T *qfl = &qi->qf_lists[idx];
while (qfl->qf_count && qfl->qf_start != NULL) {
qfp = qfl->qf_start;
@@ -2915,10 +3266,9 @@ static void qf_free_items(qf_info_T *qi, int idx)
/// Free error list "idx". Frees all the entries in the quickfix list,
/// associated context information and the title.
-static void qf_free(qf_info_T *qi, int idx)
+static void qf_free(qf_list_T *qfl)
{
- qf_list_T *qfl = &qi->qf_lists[idx];
- qf_free_items(qi, idx);
+ qf_free_items(qfl);
XFREE_CLEAR(qfl->qf_title);
tv_free(qfl->qf_ctx);
@@ -2950,11 +3300,10 @@ bool qf_mark_adjust(win_T *wp, linenr_T line1, linenr_T line2, long amount,
qi = wp->w_llist;
}
- for (idx = 0; idx < qi->qf_listcount; ++idx)
- if (qi->qf_lists[idx].qf_count)
- for (i = 0, qfp = qi->qf_lists[idx].qf_start;
- i < qi->qf_lists[idx].qf_count && qfp != NULL;
- i++, qfp = qfp->qf_next) {
+ for (idx = 0; idx < qi->qf_listcount; idx++) {
+ qf_list_T *qfl = qf_get_list(qi, idx);
+ if (!qf_list_empty(qfl)) {
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
if (qfp->qf_fnum == curbuf->b_fnum) {
found_one = true;
if (qfp->qf_lnum >= line1 && qfp->qf_lnum <= line2) {
@@ -2966,6 +3315,8 @@ bool qf_mark_adjust(win_T *wp, linenr_T line1, linenr_T line2, long amount,
qfp->qf_lnum += amount_after;
}
}
+ }
+ }
return found_one;
}
@@ -3025,7 +3376,7 @@ void qf_view_result(bool split)
if (IS_LL_WINDOW(curwin)) {
qi = GET_LOC_LIST(curwin);
}
- if (qf_list_empty(qi, qi->qf_curlist)) {
+ if (qf_list_empty(qf_get_curlist(qi))) {
EMSG(_(e_quickfix));
return;
}
@@ -3053,15 +3404,16 @@ void qf_view_result(bool split)
*/
void ex_cwindow(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
+ qf_list_T *qfl;
win_T *win;
- if (eap->cmdidx == CMD_lwindow) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL)
- return;
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
+ qfl = qf_get_curlist(qi);
+
/* Look for an existing quickfix window. */
win = qf_find_win(qi);
@@ -3070,13 +3422,15 @@ void ex_cwindow(exarg_T *eap)
* close the window. If a quickfix window is not open, then open
* it if we have errors; otherwise, leave it closed.
*/
- if (qi->qf_lists[qi->qf_curlist].qf_nonevalid
- || qi->qf_lists[qi->qf_curlist].qf_count == 0
- || qi->qf_curlist >= qi->qf_listcount) {
- if (win != NULL)
+ if (qf_stack_empty(qi)
+ || qfl->qf_nonevalid
+ || qf_list_empty(qf_get_curlist(qi))) {
+ if (win != NULL) {
ex_cclose(eap);
- } else if (win == NULL)
+ }
+ } else if (win == NULL) {
ex_copen(eap);
+ }
}
/*
@@ -3085,13 +3439,11 @@ void ex_cwindow(exarg_T *eap)
*/
void ex_cclose(exarg_T *eap)
{
- win_T *win = NULL;
- qf_info_T *qi = &ql_info;
+ win_T *win = NULL;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_lclose || eap->cmdidx == CMD_lwindow) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL)
- return;
+ if ((qi = qf_cmd_get_stack(eap, false)) == NULL) {
+ return;
}
/* Find existing quickfix window and close it. */
@@ -3101,27 +3453,124 @@ void ex_cclose(exarg_T *eap)
}
}
-/*
- * ":copen": open a window that shows the list of errors.
- * ":lopen": open a window that shows the location list.
- */
+// Goto a quickfix or location list window (if present).
+// Returns OK if the window is found, FAIL otherwise.
+static int qf_goto_cwindow(const qf_info_T *qi, bool resize, int sz,
+ bool vertsplit)
+{
+ win_T *const win = qf_find_win(qi);
+ if (win == NULL) {
+ return FAIL;
+ }
+
+ win_goto(win);
+ if (resize) {
+ if (vertsplit) {
+ if (sz != win->w_width) {
+ win_setwidth(sz);
+ }
+ } else if (sz != win->w_height
+ && (win->w_height + win->w_status_height + tabline_height()
+ < cmdline_row)) {
+ win_setheight(sz);
+ }
+ }
+
+ return OK;
+}
+
+// Open a new quickfix or location list window, load the quickfix buffer and
+// set the appropriate options for the window.
+// Returns FAIL if the window could not be opened.
+static int qf_open_new_cwindow(const qf_info_T *qi, int height)
+{
+ win_T *oldwin = curwin;
+ const tabpage_T *const prevtab = curtab;
+ int flags = 0;
+
+ const buf_T *const qf_buf = qf_find_buf(qi);
+
+ // The current window becomes the previous window afterwards.
+ win_T *const win = curwin;
+
+ if (IS_QF_STACK(qi) && cmdmod.split == 0) {
+ // Create the new quickfix window at the very bottom, except when
+ // :belowright or :aboveleft is used.
+ win_goto(lastwin);
+ }
+ // Default is to open the window below the current window
+ if (cmdmod.split == 0) {
+ flags = WSP_BELOW;
+ }
+ flags |= WSP_NEWLOC;
+ if (win_split(height, flags) == FAIL) {
+ return FAIL; // not enough room for window
+ }
+ RESET_BINDING(curwin);
+
+ if (IS_LL_STACK(qi)) {
+ // For the location list window, create a reference to the
+ // location list from the window 'win'.
+ curwin->w_llist_ref = win->w_llist;
+ win->w_llist->qf_refcount++;
+ }
+
+ if (oldwin != curwin) {
+ oldwin = NULL; // don't store info when in another window
+ }
+ if (qf_buf != NULL) {
+ // Use the existing quickfix buffer
+ (void)do_ecmd(qf_buf->b_fnum, NULL, NULL, NULL, ECMD_ONE,
+ ECMD_HIDE + ECMD_OLDBUF, oldwin);
+ } else {
+ // Create a new quickfix buffer
+ (void)do_ecmd(0, NULL, NULL, NULL, ECMD_ONE, ECMD_HIDE, oldwin);
+
+ // switch off 'swapfile'
+ set_option_value("swf", 0L, NULL, OPT_LOCAL);
+ set_option_value("bt", 0L, "quickfix", OPT_LOCAL);
+ set_option_value("bh", 0L, "wipe", OPT_LOCAL);
+ RESET_BINDING(curwin);
+ curwin->w_p_diff = false;
+ set_option_value("fdm", 0L, "manual", OPT_LOCAL);
+ }
+
+ // Only set the height when still in the same tab page and there is no
+ // window to the side.
+ if (curtab == prevtab && curwin->w_width == Columns) {
+ win_setheight(height);
+ }
+ curwin->w_p_wfh = true; // set 'winfixheight'
+ if (win_valid(win)) {
+ prevwin = win;
+ }
+ return OK;
+}
+
+/// Set "w:quickfix_title" if "qi" has a title.
+static void qf_set_title_var(qf_list_T *qfl)
+{
+ if (qfl->qf_title != NULL) {
+ set_internal_string_var((char_u *)"w:quickfix_title", qfl->qf_title);
+ }
+}
+
+/// ":copen": open a window that shows the list of errors.
+/// ":lopen": open a window that shows the location list.
void ex_copen(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
+ qf_list_T *qfl;
int height;
- win_T *win;
- tabpage_T *prevtab = curtab;
- buf_T *qf_buf;
- win_T *oldwin = curwin;
+ int status = FAIL;
+ int lnum;
- if (eap->cmdidx == CMD_lopen || eap->cmdidx == CMD_lwindow) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
+ incr_quickfix_busy();
+
if (eap->addr_count != 0) {
assert(eap->line2 <= INT_MAX);
height = (int)eap->line2;
@@ -3130,94 +3579,33 @@ void ex_copen(exarg_T *eap)
}
reset_VIsual_and_resel(); // stop Visual mode
- /*
- * Find existing quickfix window, or open a new one.
- */
- win = qf_find_win(qi);
-
- if (win != NULL && cmdmod.tab == 0) {
- win_goto(win);
- if (eap->addr_count != 0) {
- if (cmdmod.split & WSP_VERT) {
- if (height != win->w_width) {
- win_setwidth(height);
- }
- } else {
- if (height != win->w_height) {
- win_setheight(height);
- }
- }
- }
- } else {
- int flags = 0;
-
- qf_buf = qf_find_buf(qi);
-
- /* The current window becomes the previous window afterwards. */
- win = curwin;
-
- if ((eap->cmdidx == CMD_copen || eap->cmdidx == CMD_cwindow)
- && cmdmod.split == 0)
- // Create the new quickfix window at the very bottom, except when
- // :belowright or :aboveleft is used.
- win_goto(lastwin);
- // Default is to open the window below the current window
- if (cmdmod.split == 0) {
- flags = WSP_BELOW;
- }
- flags |= WSP_NEWLOC;
- if (win_split(height, flags) == FAIL) {
- return; // not enough room for window
+ // Find an existing quickfix window, or open a new one.
+ if (cmdmod.tab == 0) {
+ status = qf_goto_cwindow(qi, eap->addr_count != 0, height,
+ cmdmod.split & WSP_VERT);
+ }
+ if (status == FAIL) {
+ if (qf_open_new_cwindow(qi, height) == FAIL) {
+ decr_quickfix_busy();
+ return;
}
- RESET_BINDING(curwin);
+ }
- if (eap->cmdidx == CMD_lopen || eap->cmdidx == CMD_lwindow) {
- /*
- * For the location list window, create a reference to the
- * location list from the window 'win'.
- */
- curwin->w_llist_ref = win->w_llist;
- win->w_llist->qf_refcount++;
- }
-
- if (oldwin != curwin)
- oldwin = NULL; /* don't store info when in another window */
- if (qf_buf != NULL)
- /* Use the existing quickfix buffer */
- (void)do_ecmd(qf_buf->b_fnum, NULL, NULL, NULL, ECMD_ONE,
- ECMD_HIDE + ECMD_OLDBUF, oldwin);
- else {
- /* Create a new quickfix buffer */
- (void)do_ecmd(0, NULL, NULL, NULL, ECMD_ONE, ECMD_HIDE, oldwin);
- // Switch off 'swapfile'.
- set_option_value("swf", 0L, NULL, OPT_LOCAL);
- set_option_value("bt", 0L, "quickfix", OPT_LOCAL);
- set_option_value("bh", 0L, "wipe", OPT_LOCAL);
- RESET_BINDING(curwin);
- curwin->w_p_diff = false;
- set_option_value("fdm", 0L, "manual", OPT_LOCAL);
- }
-
- /* Only set the height when still in the same tab page and there is no
- * window to the side. */
- if (curtab == prevtab
- && curwin->w_width == Columns
- )
- win_setheight(height);
- curwin->w_p_wfh = TRUE; /* set 'winfixheight' */
- if (win_valid(win))
- prevwin = win;
- }
-
- qf_set_title_var(qi);
+ qfl = qf_get_curlist(qi);
+ qf_set_title_var(qfl);
+ // Save the current index here, as updating the quickfix buffer may free
+ // the quickfix list
+ lnum = qfl->qf_index;
// Fill the buffer with the quickfix list.
- qf_fill_buffer(qi, curbuf, NULL);
+ qf_fill_buffer(qfl, curbuf, NULL);
+
+ decr_quickfix_busy();
- curwin->w_cursor.lnum = qi->qf_lists[qi->qf_curlist].qf_index;
+ curwin->w_cursor.lnum = lnum;
curwin->w_cursor.col = 0;
check_cursor();
- update_topline(); /* scroll to show the line */
+ update_topline(); // scroll to show the line
}
// Move the cursor in the quickfix window to "lnum".
@@ -3238,17 +3626,13 @@ static void qf_win_goto(win_T *win, linenr_T lnum)
curbuf = curwin->w_buffer;
}
-// :cbottom/:lbottom command.
+/// :cbottom/:lbottom command.
void ex_cbottom(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_lbottom) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
win_T *win = qf_find_win(qi);
@@ -3270,7 +3654,7 @@ linenr_T qf_current_entry(win_T *wp)
/* In the location list window, use the referenced location list */
qi = wp->w_llist_ref;
- return qi->qf_lists[qi->qf_curlist].qf_index;
+ return qf_get_curlist(qi)->qf_index;
}
/*
@@ -3284,7 +3668,7 @@ qf_win_pos_update (
)
{
win_T *win;
- int qf_index = qi->qf_lists[qi->qf_curlist].qf_index;
+ int qf_index = qf_get_curlist(qi)->qf_index;
/*
* Put the cursor on the current error in the quickfix window, so that
@@ -3307,8 +3691,9 @@ qf_win_pos_update (
}
/// Checks whether the given window is displaying the specified
-/// quickfix/location list buffer.
-static int is_qf_win(win_T *win, qf_info_T *qi)
+/// quickfix/location stack.
+static int is_qf_win(const win_T *win, const qf_info_T *qi)
+ FUNC_ATTR_NONNULL_ARG(2) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
//
// A window displaying the quickfix buffer will have the w_llist_ref field
@@ -3326,9 +3711,10 @@ static int is_qf_win(win_T *win, qf_info_T *qi)
return false;
}
-/// Find a window displaying the quickfix/location list 'qi'
+/// Find a window displaying the quickfix/location stack 'qi'
/// Only searches in the current tabpage.
-static win_T *qf_find_win(qf_info_T *qi)
+static win_T *qf_find_win(const qf_info_T *qi)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
FOR_ALL_WINDOWS_IN_TAB(win, curtab) {
if (is_qf_win(win, qi)) {
@@ -3343,7 +3729,8 @@ static win_T *qf_find_win(qf_info_T *qi)
* Find a quickfix buffer.
* Searches in windows opened in all the tabs.
*/
-static buf_T *qf_find_buf(qf_info_T *qi)
+static buf_T *qf_find_buf(const qf_info_T *qi)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
FOR_ALL_TAB_WINDOWS(tp, win) {
if (is_qf_win(win, qi)) {
@@ -3362,7 +3749,7 @@ static void qf_update_win_titlevar(qf_info_T *qi)
if ((win = qf_find_win(qi)) != NULL) {
win_T *curwin_save = curwin;
curwin = win;
- qf_set_title_var(qi);
+ qf_set_title_var(qf_get_curlist(qi));
curwin = curwin_save;
}
}
@@ -3388,7 +3775,7 @@ static void qf_update_buffer(qf_info_T *qi, qfline_T *old_last)
qf_update_win_titlevar(qi);
- qf_fill_buffer(qi, buf, old_last);
+ qf_fill_buffer(qf_get_curlist(qi), buf, old_last);
buf_inc_changedtick(buf);
if (old_last == NULL) {
@@ -3406,26 +3793,82 @@ static void qf_update_buffer(qf_info_T *qi, qfline_T *old_last)
}
}
-// Set "w:quickfix_title" if "qi" has a title.
-static void qf_set_title_var(qf_info_T *qi)
+// Add an error line to the quickfix buffer.
+static int qf_buf_add_line(buf_T *buf, linenr_T lnum, const qfline_T *qfp,
+ char_u *dirname)
+ FUNC_ATTR_NONNULL_ALL
{
- if (qi->qf_lists[qi->qf_curlist].qf_title != NULL) {
- set_internal_string_var((char_u *)"w:quickfix_title",
- qi->qf_lists[qi->qf_curlist].qf_title);
+ int len;
+ buf_T *errbuf;
+
+ if (qfp->qf_module != NULL) {
+ STRCPY(IObuff, qfp->qf_module);
+ len = (int)STRLEN(IObuff);
+ } else if (qfp->qf_fnum != 0
+ && (errbuf = buflist_findnr(qfp->qf_fnum)) != NULL
+ && errbuf->b_fname != NULL) {
+ if (qfp->qf_type == 1) { // :helpgrep
+ STRLCPY(IObuff, path_tail(errbuf->b_fname), sizeof(IObuff));
+ } else {
+ // shorten the file name if not done already
+ if (errbuf->b_sfname == NULL
+ || path_is_absolute(errbuf->b_sfname)) {
+ if (*dirname == NUL) {
+ os_dirname(dirname, MAXPATHL);
+ }
+ shorten_buf_fname(errbuf, dirname, false);
+ }
+ STRLCPY(IObuff, errbuf->b_fname, sizeof(IObuff));
+ }
+ len = (int)STRLEN(IObuff);
+ } else {
+ len = 0;
+ }
+ IObuff[len++] = '|';
+
+ if (qfp->qf_lnum > 0) {
+ snprintf((char *)IObuff + len, sizeof(IObuff), "%" PRId64,
+ (int64_t)qfp->qf_lnum);
+ len += (int)STRLEN(IObuff + len);
+
+ if (qfp->qf_col > 0) {
+ snprintf((char *)IObuff + len, sizeof(IObuff), " col %d", qfp->qf_col);
+ len += (int)STRLEN(IObuff + len);
+ }
+
+ snprintf((char *)IObuff + len, sizeof(IObuff), "%s",
+ (char *)qf_types(qfp->qf_type, qfp->qf_nr));
+ len += (int)STRLEN(IObuff + len);
+ } else if (qfp->qf_pattern != NULL) {
+ qf_fmt_text(qfp->qf_pattern, IObuff + len, IOSIZE - len);
+ len += (int)STRLEN(IObuff + len);
+ }
+ IObuff[len++] = '|';
+ IObuff[len++] = ' ';
+
+ // Remove newlines and leading whitespace from the text.
+ // For an unrecognized line keep the indent, the compiler may
+ // mark a word with ^^^^.
+ qf_fmt_text(len > 3 ? skipwhite(qfp->qf_text) : qfp->qf_text,
+ IObuff + len, IOSIZE - len);
+
+ if (ml_append_buf(buf, lnum, IObuff,
+ (colnr_T)STRLEN(IObuff) + 1, false) == FAIL) {
+ return FAIL;
}
+ return OK;
}
-// Fill current buffer with quickfix errors, replacing any previous contents.
-// curbuf must be the quickfix buffer!
-// If "old_last" is not NULL append the items after this one.
-// When "old_last" is NULL then "buf" must equal "curbuf"! Because ml_delete()
-// is used and autocommands will be triggered.
-static void qf_fill_buffer(qf_info_T *qi, buf_T *buf, qfline_T *old_last)
+/// Fill current buffer with quickfix errors, replacing any previous contents.
+/// curbuf must be the quickfix buffer!
+/// If "old_last" is not NULL append the items after this one.
+/// When "old_last" is NULL then "buf" must equal "curbuf"! Because ml_delete()
+/// is used and autocommands will be triggered.
+static void qf_fill_buffer(qf_list_T *qfl, buf_T *buf, qfline_T *old_last)
+ FUNC_ATTR_NONNULL_ARG(2)
{
linenr_T lnum;
qfline_T *qfp;
- buf_T *errbuf;
- int len;
const bool old_KeyTyped = KeyTyped;
if (old_last == NULL) {
@@ -3440,73 +3883,22 @@ static void qf_fill_buffer(qf_info_T *qi, buf_T *buf, qfline_T *old_last)
}
}
- /* Check if there is anything to display */
- if (qi->qf_curlist < qi->qf_listcount) {
+ // Check if there is anything to display
+ if (qfl != NULL) {
char_u dirname[MAXPATHL];
*dirname = NUL;
// Add one line for each error
if (old_last == NULL) {
- qfp = qi->qf_lists[qi->qf_curlist].qf_start;
+ qfp = qfl->qf_start;
lnum = 0;
} else {
qfp = old_last->qf_next;
lnum = buf->b_ml.ml_line_count;
}
- while (lnum < qi->qf_lists[qi->qf_curlist].qf_count) {
- if (qfp->qf_module != NULL) {
- STRCPY(IObuff, qfp->qf_module);
- len = (int)STRLEN(IObuff);
- } else if (qfp->qf_fnum != 0
- && (errbuf = buflist_findnr(qfp->qf_fnum)) != NULL
- && errbuf->b_fname != NULL) {
- if (qfp->qf_type == 1) { // :helpgrep
- STRLCPY(IObuff, path_tail(errbuf->b_fname), sizeof(IObuff));
- } else {
- // shorten the file name if not done already
- if (errbuf->b_sfname == NULL
- || path_is_absolute(errbuf->b_sfname)) {
- if (*dirname == NUL) {
- os_dirname(dirname, MAXPATHL);
- }
- shorten_buf_fname(errbuf, dirname, false);
- }
- STRLCPY(IObuff, errbuf->b_fname, sizeof(IObuff));
- }
- len = (int)STRLEN(IObuff);
- } else {
- len = 0;
- }
- IObuff[len++] = '|';
-
- if (qfp->qf_lnum > 0) {
- sprintf((char *)IObuff + len, "%" PRId64, (int64_t)qfp->qf_lnum);
- len += (int)STRLEN(IObuff + len);
-
- if (qfp->qf_col > 0) {
- sprintf((char *)IObuff + len, " col %d", qfp->qf_col);
- len += (int)STRLEN(IObuff + len);
- }
-
- sprintf((char *)IObuff + len, "%s",
- (char *)qf_types(qfp->qf_type, qfp->qf_nr));
- len += (int)STRLEN(IObuff + len);
- } else if (qfp->qf_pattern != NULL) {
- qf_fmt_text(qfp->qf_pattern, IObuff + len, IOSIZE - len);
- len += (int)STRLEN(IObuff + len);
- }
- IObuff[len++] = '|';
- IObuff[len++] = ' ';
-
- /* Remove newlines and leading whitespace from the text.
- * For an unrecognized line keep the indent, the compiler may
- * mark a word with ^^^^. */
- qf_fmt_text(len > 3 ? skipwhite(qfp->qf_text) : qfp->qf_text,
- IObuff + len, IOSIZE - len);
-
- if (ml_append_buf(buf, lnum, IObuff, (colnr_T)STRLEN(IObuff) + 1, false)
- == FAIL) {
+ while (lnum < qfl->qf_count) {
+ if (qf_buf_add_line(buf, lnum, qfp, dirname) == FAIL) {
break;
}
lnum++;
@@ -3548,9 +3940,9 @@ static void qf_fill_buffer(qf_info_T *qi, buf_T *buf, qfline_T *old_last)
KeyTyped = old_KeyTyped;
}
-static void qf_list_changed(qf_info_T *qi, int qf_idx)
+static void qf_list_changed(qf_list_T *qfl)
{
- qi->qf_lists[qf_idx].qf_changedtick++;
+ qfl->qf_changedtick++;
}
/// Return the quickfix/location list number with the given identifier.
@@ -3566,15 +3958,15 @@ static int qf_id2nr(const qf_info_T *const qi, const unsigned qfid)
return INVALID_QFIDX;
}
-// If the current list is not "save_qfid" and we can find the list with that ID
-// then make it the current list.
-// This is used when autocommands may have changed the current list.
-// Returns OK if successfully restored the list. Returns FAIL if the list with
-// the specified identifier (save_qfid) is not found in the stack.
+/// If the current list is not "save_qfid" and we can find the list with that ID
+/// then make it the current list.
+/// This is used when autocommands may have changed the current list.
+/// Returns OK if successfully restored the list. Returns FAIL if the list with
+/// the specified identifier (save_qfid) is not found in the stack.
static int qf_restore_list(qf_info_T *qi, unsigned save_qfid)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
- if (qi->qf_lists[qi->qf_curlist].qf_id != save_qfid) {
+ if (qf_get_curlist(qi)->qf_id != save_qfid) {
const int curlist = qf_id2nr(qi, save_qfid);
if (curlist < 0) {
// list is not present
@@ -3593,7 +3985,7 @@ static void qf_jump_first(qf_info_T *qi, unsigned save_qfid, int forceit)
return;
}
// Autocommands might have cleared the list, check for that
- if (!qf_list_empty(qi, qi->qf_curlist)) {
+ if (!qf_list_empty(qf_get_curlist(qi))) {
qf_jump(qi, 0, 0, forceit);
}
}
@@ -3611,6 +4003,58 @@ int grep_internal(cmdidx_T cmdidx)
*curbuf->b_p_gp == NUL ? p_gp : curbuf->b_p_gp) == 0;
}
+// Return the make/grep autocmd name.
+static char_u *make_get_auname(cmdidx_T cmdidx)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ switch (cmdidx) {
+ case CMD_make:
+ return (char_u *)"make";
+ case CMD_lmake:
+ return (char_u *)"lmake";
+ case CMD_grep:
+ return (char_u *)"grep";
+ case CMD_lgrep:
+ return (char_u *)"lgrep";
+ case CMD_grepadd:
+ return (char_u *)"grepadd";
+ case CMD_lgrepadd:
+ return (char_u *)"lgrepadd";
+ default:
+ return NULL;
+ }
+}
+
+// Form the complete command line to invoke 'make'/'grep'. Quote the command
+// using 'shellquote' and append 'shellpipe'. Echo the fully formed command.
+static char *make_get_fullcmd(const char_u *makecmd, const char_u *fname)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET
+{
+ size_t len = STRLEN(p_shq) * 2 + STRLEN(makecmd) + 1;
+ if (*p_sp != NUL) {
+ len += STRLEN(p_sp) + STRLEN(fname) + 3;
+ }
+ char *const cmd = xmalloc(len);
+ snprintf(cmd, len, "%s%s%s", (char *)p_shq, (char *)makecmd, (char *)p_shq);
+
+ // If 'shellpipe' empty: don't redirect to 'errorfile'.
+ if (*p_sp != NUL) {
+ append_redir(cmd, len, (char *)p_sp, (char *)fname);
+ }
+
+ // Display the fully formed command. Output a newline if there's something
+ // else than the :make command that was typed (in which case the cursor is
+ // in column 0).
+ if (msg_col == 0) {
+ msg_didout = false;
+ }
+ msg_start();
+ MSG_PUTS(":!");
+ msg_outtrans((char_u *)cmd); // show what we are doing
+
+ return cmd;
+}
+
/*
* Used for ":make", ":lmake", ":grep", ":lgrep", ":grepadd", and ":lgrepadd"
*/
@@ -3620,24 +4064,15 @@ void ex_make(exarg_T *eap)
win_T *wp = NULL;
qf_info_T *qi = &ql_info;
int res;
- char_u *au_name = NULL;
char_u *enc = (*curbuf->b_p_menc != NUL) ? curbuf->b_p_menc : p_menc;
- /* Redirect ":grep" to ":vimgrep" if 'grepprg' is "internal". */
+ // Redirect ":grep" to ":vimgrep" if 'grepprg' is "internal".
if (grep_internal(eap->cmdidx)) {
ex_vimgrep(eap);
return;
}
- switch (eap->cmdidx) {
- case CMD_make: au_name = (char_u *)"make"; break;
- case CMD_lmake: au_name = (char_u *)"lmake"; break;
- case CMD_grep: au_name = (char_u *)"grep"; break;
- case CMD_lgrep: au_name = (char_u *)"lgrep"; break;
- case CMD_grepadd: au_name = (char_u *)"grepadd"; break;
- case CMD_lgrepadd: au_name = (char_u *)"lgrepadd"; break;
- default: break;
- }
+ char_u *const au_name = make_get_auname(eap->cmdidx);
if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name,
curbuf->b_fname, true, curbuf)) {
if (aborting()) {
@@ -3645,9 +4080,9 @@ void ex_make(exarg_T *eap)
}
}
- if (eap->cmdidx == CMD_lmake || eap->cmdidx == CMD_lgrep
- || eap->cmdidx == CMD_lgrepadd)
+ if (is_loclist_cmd(eap->cmdidx)) {
wp = curwin;
+ }
autowrite_all();
fname = get_mef_name();
@@ -3655,28 +4090,11 @@ void ex_make(exarg_T *eap)
return;
os_remove((char *)fname); // in case it's not unique
- // If 'shellpipe' empty: don't redirect to 'errorfile'.
- const size_t len = (STRLEN(p_shq) * 2 + STRLEN(eap->arg) + 1
- + (*p_sp == NUL
- ? 0
- : STRLEN(p_sp) + STRLEN(fname) + 3));
- char *const cmd = xmalloc(len);
- snprintf(cmd, len, "%s%s%s", (char *)p_shq, (char *)eap->arg,
- (char *)p_shq);
- if (*p_sp != NUL) {
- append_redir(cmd, len, (char *) p_sp, (char *) fname);
- }
- // Output a newline if there's something else than the :make command that
- // was typed (in which case the cursor is in column 0).
- if (msg_col == 0) {
- msg_didout = false;
- }
- msg_start();
- MSG_PUTS(":!");
- msg_outtrans((char_u *)cmd); // show what we are doing
+ char *const cmd = make_get_fullcmd(eap->arg, fname);
do_shell((char_u *)cmd, 0);
+ incr_quickfix_busy();
res = qf_init(wp, fname, (eap->cmdidx != CMD_make
&& eap->cmdidx != CMD_lmake) ? p_gefm : p_efm,
@@ -3689,11 +4107,11 @@ void ex_make(exarg_T *eap)
}
}
if (res >= 0) {
- qf_list_changed(qi, qi->qf_curlist);
+ qf_list_changed(qf_get_curlist(qi));
}
// Remember the current quickfix list identifier, so that we can
// check for autocommands changing the current quickfix list.
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
if (au_name != NULL) {
apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name, curbuf->b_fname, true,
curbuf);
@@ -3704,6 +4122,7 @@ void ex_make(exarg_T *eap)
}
cleanup:
+ decr_quickfix_busy();
os_remove((char *)fname);
xfree(fname);
xfree(cmd);
@@ -3761,23 +4180,20 @@ static char_u *get_mef_name(void)
size_t qf_get_size(exarg_T *eap)
FUNC_ATTR_NONNULL_ALL
{
- qf_info_T *qi = &ql_info;
- if (eap->cmdidx == CMD_ldo || eap->cmdidx == CMD_lfdo) {
- // Location list.
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- return 0;
- }
+ qf_info_T *qi;
+ qf_list_T *qfl;
+
+ if ((qi = qf_cmd_get_stack(eap, false)) == NULL) {
+ return 0;
}
int prev_fnum = 0;
size_t sz = 0;
qfline_T *qfp;
- size_t i;
- assert(qi->qf_lists[qi->qf_curlist].qf_count >= 0);
- for (i = 0, qfp = qi->qf_lists[qi->qf_curlist].qf_start;
- i < (size_t)qi->qf_lists[qi->qf_curlist].qf_count && qfp != NULL;
- i++, qfp = qfp->qf_next) {
+ int i;
+ assert(qf_get_curlist(qi)->qf_count >= 0);
+ qfl = qf_get_curlist(qi);
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
if (!qfp->qf_valid) {
continue;
}
@@ -3800,18 +4216,14 @@ size_t qf_get_size(exarg_T *eap)
size_t qf_get_cur_idx(exarg_T *eap)
FUNC_ATTR_NONNULL_ALL
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_ldo || eap->cmdidx == CMD_lfdo) {
- // Location list.
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- return 0;
- }
+ if ((qi = qf_cmd_get_stack(eap, false)) == NULL) {
+ return 0;
}
- assert(qi->qf_lists[qi->qf_curlist].qf_index >= 0);
- return (size_t)qi->qf_lists[qi->qf_curlist].qf_index;
+ assert(qf_get_curlist(qi)->qf_index >= 0);
+ return (size_t)qf_get_curlist(qi)->qf_index;
}
/// Returns the current index in the quickfix/location list,
@@ -3820,20 +4232,16 @@ size_t qf_get_cur_idx(exarg_T *eap)
int qf_get_cur_valid_idx(exarg_T *eap)
FUNC_ATTR_NONNULL_ALL
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_ldo || eap->cmdidx == CMD_lfdo) {
- // Location list.
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- return 1;
- }
+ if ((qi = qf_cmd_get_stack(eap, false)) == NULL) {
+ return 1;
}
- qf_list_T *qfl = &qi->qf_lists[qi->qf_curlist];
+ qf_list_T *qfl = qf_get_curlist(qi);
// Check if the list has valid errors.
- if (qfl->qf_count <= 0 || qfl->qf_nonevalid) {
+ if (!qf_list_has_valid_entries(qfl)) {
return 1;
}
@@ -3868,24 +4276,20 @@ int qf_get_cur_valid_idx(exarg_T *eap)
/// Used by :cdo, :ldo, :cfdo and :lfdo commands.
/// For :cdo and :ldo, returns the 'n'th valid error entry.
/// For :cfdo and :lfdo, returns the 'n'th valid file entry.
-static size_t qf_get_nth_valid_entry(qf_info_T *qi, size_t n, bool fdo)
+static size_t qf_get_nth_valid_entry(qf_list_T *qfl, size_t n, int fdo)
FUNC_ATTR_NONNULL_ALL
{
- qf_list_T *qfl = &qi->qf_lists[qi->qf_curlist];
-
// Check if the list has valid errors.
- if (qfl->qf_count <= 0 || qfl->qf_nonevalid) {
+ if (!qf_list_has_valid_entries(qfl)) {
return 1;
}
int prev_fnum = 0;
size_t eidx = 0;
- size_t i;
+ int i;
qfline_T *qfp;
assert(qfl->qf_count >= 0);
- for (i = 1, qfp = qfl->qf_start;
- i <= (size_t)qfl->qf_count && qfp != NULL;
- i++, qfp = qfp->qf_next) {
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
if (qfp->qf_valid) {
if (fdo) {
if (qfp->qf_fnum > 0 && qfp->qf_fnum != prev_fnum) {
@@ -3903,41 +4307,39 @@ static size_t qf_get_nth_valid_entry(qf_info_T *qi, size_t n, bool fdo)
}
}
- return i <= (size_t)qfl->qf_count ? i : 1;
+ return i <= qfl->qf_count ? (size_t)i : 1;
}
-/*
- * ":cc", ":crewind", ":cfirst" and ":clast".
- * ":ll", ":lrewind", ":lfirst" and ":llast".
- * ":cdo", ":ldo", ":cfdo" and ":lfdo".
- */
+/// ":cc", ":crewind", ":cfirst" and ":clast".
+/// ":ll", ":lrewind", ":lfirst" and ":llast".
+/// ":cdo", ":ldo", ":cfdo" and ":lfdo".
void ex_cc(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_ll
- || eap->cmdidx == CMD_lrewind
- || eap->cmdidx == CMD_lfirst
- || eap->cmdidx == CMD_llast
- || eap->cmdidx == CMD_ldo
- || eap->cmdidx == CMD_lfdo) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
int errornr;
if (eap->addr_count > 0) {
errornr = (int)eap->line2;
- } else if (eap->cmdidx == CMD_cc || eap->cmdidx == CMD_ll) {
- errornr = 0;
- } else if (eap->cmdidx == CMD_crewind || eap->cmdidx == CMD_lrewind
- || eap->cmdidx == CMD_cfirst || eap->cmdidx == CMD_lfirst) {
- errornr = 1;
} else {
- errornr = 32767;
+ switch (eap->cmdidx) {
+ case CMD_cc:
+ case CMD_ll:
+ errornr = 0;
+ break;
+ case CMD_crewind:
+ case CMD_lrewind:
+ case CMD_cfirst:
+ case CMD_lfirst:
+ errornr = 1;
+ break;
+ default:
+ errornr = 32767;
+ break;
+ }
}
// For cdo and ldo commands, jump to the nth valid error.
@@ -3951,8 +4353,9 @@ void ex_cc(exarg_T *eap)
} else {
n = 1;
}
- size_t valid_entry = qf_get_nth_valid_entry(qi, n,
- eap->cmdidx == CMD_cfdo || eap->cmdidx == CMD_lfdo);
+ size_t valid_entry = qf_get_nth_valid_entry(
+ qf_get_curlist(qi), n,
+ eap->cmdidx == CMD_cfdo || eap->cmdidx == CMD_lfdo);
assert(valid_entry <= INT_MAX);
errornr = (int)valid_entry;
}
@@ -3960,28 +4363,15 @@ void ex_cc(exarg_T *eap)
qf_jump(qi, 0, errornr, eap->forceit);
}
-/*
- * ":cnext", ":cnfile", ":cNext" and ":cprevious".
- * ":lnext", ":lNext", ":lprevious", ":lnfile", ":lNfile" and ":lpfile".
- * ":cdo", ":ldo", ":cfdo" and ":lfdo".
- */
+/// ":cnext", ":cnfile", ":cNext" and ":cprevious".
+/// ":lnext", ":lNext", ":lprevious", ":lnfile", ":lNfile" and ":lpfile".
+/// ":cdo", ":ldo", ":cfdo" and ":lfdo".
void ex_cnext(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_lnext
- || eap->cmdidx == CMD_lNext
- || eap->cmdidx == CMD_lprevious
- || eap->cmdidx == CMD_lnfile
- || eap->cmdidx == CMD_lNfile
- || eap->cmdidx == CMD_lpfile
- || eap->cmdidx == CMD_ldo
- || eap->cmdidx == CMD_lfdo) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
int errornr;
@@ -3993,19 +4383,326 @@ void ex_cnext(exarg_T *eap)
errornr = 1;
}
- qf_jump(qi, (eap->cmdidx == CMD_cnext || eap->cmdidx == CMD_lnext
- || eap->cmdidx == CMD_cdo || eap->cmdidx == CMD_ldo)
- ? FORWARD
- : (eap->cmdidx == CMD_cnfile || eap->cmdidx == CMD_lnfile
- || eap->cmdidx == CMD_cfdo || eap->cmdidx == CMD_lfdo)
- ? FORWARD_FILE
- : (eap->cmdidx == CMD_cpfile || eap->cmdidx == CMD_lpfile
- || eap->cmdidx == CMD_cNfile || eap->cmdidx == CMD_lNfile)
- ? BACKWARD_FILE
- : BACKWARD,
- errornr, eap->forceit);
+ // Depending on the command jump to either next or previous entry/file.
+ Direction dir;
+ switch (eap->cmdidx) {
+ case CMD_cprevious:
+ case CMD_lprevious:
+ case CMD_cNext:
+ case CMD_lNext:
+ dir = BACKWARD;
+ break;
+ case CMD_cnfile:
+ case CMD_lnfile:
+ case CMD_cfdo:
+ case CMD_lfdo:
+ dir = FORWARD_FILE;
+ break;
+ case CMD_cpfile:
+ case CMD_lpfile:
+ case CMD_cNfile:
+ case CMD_lNfile:
+ dir = BACKWARD_FILE;
+ break;
+ case CMD_cnext:
+ case CMD_lnext:
+ case CMD_cdo:
+ case CMD_ldo:
+ default:
+ dir = FORWARD;
+ break;
+ }
+
+ qf_jump(qi, dir, errornr, eap->forceit);
+}
+
+/// Find the first entry in the quickfix list 'qfl' from buffer 'bnr'.
+/// The index of the entry is stored in 'errornr'.
+/// Returns NULL if an entry is not found.
+static qfline_T *qf_find_first_entry_in_buf(qf_list_T *qfl,
+ int bnr,
+ int *errornr)
+{
+ qfline_T *qfp = NULL;
+ int idx = 0;
+
+ // Find the first entry in this file
+ FOR_ALL_QFL_ITEMS(qfl, qfp, idx) {
+ if (qfp->qf_fnum == bnr) {
+ break;
+ }
+ }
+
+ *errornr = idx;
+ return qfp;
+}
+
+/// Find the first quickfix entry on the same line as 'entry'. Updates 'errornr'
+/// with the error number for the first entry. Assumes the entries are sorted in
+/// the quickfix list by line number.
+static qfline_T * qf_find_first_entry_on_line(qfline_T *entry, int *errornr)
+{
+ while (!got_int
+ && entry->qf_prev != NULL
+ && entry->qf_fnum == entry->qf_prev->qf_fnum
+ && entry->qf_lnum == entry->qf_prev->qf_lnum) {
+ entry = entry->qf_prev;
+ (*errornr)--;
+ }
+
+ return entry;
+}
+
+/// Find the last quickfix entry on the same line as 'entry'. Updates 'errornr'
+/// with the error number for the last entry. Assumes the entries are sorted in
+/// the quickfix list by line number.
+static qfline_T * qf_find_last_entry_on_line(qfline_T *entry, int *errornr)
+{
+ while (!got_int
+ && entry->qf_next != NULL
+ && entry->qf_fnum == entry->qf_next->qf_fnum
+ && entry->qf_lnum == entry->qf_next->qf_lnum) {
+ entry = entry->qf_next;
+ (*errornr)++;
+ }
+
+ return entry;
+}
+
+/// Find the first quickfix entry below line 'lnum' in buffer 'bnr'.
+/// 'qfp' points to the very first entry in the buffer and 'errornr' is the
+/// index of the very first entry in the quickfix list.
+/// Returns NULL if an entry is not found after 'lnum'.
+static qfline_T *qf_find_entry_on_next_line(int bnr,
+ linenr_T lnum,
+ qfline_T *qfp,
+ int *errornr)
+{
+ if (qfp->qf_lnum > lnum) {
+ // First entry is after line 'lnum'
+ return qfp;
+ }
+
+ // Find the entry just before or at the line 'lnum'
+ while (qfp->qf_next != NULL
+ && qfp->qf_next->qf_fnum == bnr
+ && qfp->qf_next->qf_lnum <= lnum) {
+ qfp = qfp->qf_next;
+ (*errornr)++;
+ }
+
+ if (qfp->qf_next == NULL || qfp->qf_next->qf_fnum != bnr) {
+ // No entries found after 'lnum'
+ return NULL;
+ }
+
+ // Use the entry just after line 'lnum'
+ qfp = qfp->qf_next;
+ (*errornr)++;
+
+ return qfp;
+}
+
+/// Find the first quickfix entry before line 'lnum' in buffer 'bnr'.
+/// 'qfp' points to the very first entry in the buffer and 'errornr' is the
+/// index of the very first entry in the quickfix list.
+/// Returns NULL if an entry is not found before 'lnum'.
+static qfline_T *qf_find_entry_on_prev_line(int bnr,
+ linenr_T lnum,
+ qfline_T *qfp,
+ int *errornr)
+{
+ // Find the entry just before the line 'lnum'
+ while (qfp->qf_next != NULL
+ && qfp->qf_next->qf_fnum == bnr
+ && qfp->qf_next->qf_lnum < lnum) {
+ qfp = qfp->qf_next;
+ (*errornr)++;
+ }
+
+ if (qfp->qf_lnum >= lnum) { // entry is after 'lnum'
+ return NULL;
+ }
+
+ // If multiple entries are on the same line, then use the first entry
+ qfp = qf_find_first_entry_on_line(qfp, errornr);
+
+ return qfp;
+}
+
+/// Find a quickfix entry in 'qfl' closest to line 'lnum' in buffer 'bnr' in
+/// the direction 'dir'.
+static qfline_T *qf_find_closest_entry(qf_list_T *qfl,
+ int bnr,
+ linenr_T lnum,
+ int dir,
+ int *errornr)
+{
+ qfline_T *qfp;
+
+ *errornr = 0;
+
+ // Find the first entry in this file
+ qfp = qf_find_first_entry_in_buf(qfl, bnr, errornr);
+ if (qfp == NULL) {
+ return NULL; // no entry in this file
+ }
+
+ if (dir == FORWARD) {
+ qfp = qf_find_entry_on_next_line(bnr, lnum, qfp, errornr);
+ } else {
+ qfp = qf_find_entry_on_prev_line(bnr, lnum, qfp, errornr);
+ }
+
+ return qfp;
+}
+
+/// Get the nth quickfix entry below the specified entry treating multiple
+/// entries on a single line as one. Searches forward in the list.
+static void qf_get_nth_below_entry(qfline_T *entry,
+ int *errornr,
+ linenr_T n)
+{
+ while (n-- > 0 && !got_int) {
+ qfline_T *first_entry = entry;
+ int first_errornr = *errornr;
+
+ // Treat all the entries on the same line in this file as one
+ entry = qf_find_last_entry_on_line(entry, errornr);
+
+ if (entry->qf_next == NULL
+ || entry->qf_next->qf_fnum != entry->qf_fnum) {
+ // If multiple entries are on the same line, then use the first
+ // entry
+ entry = first_entry;
+ *errornr = first_errornr;
+ break;
+ }
+
+ entry = entry->qf_next;
+ (*errornr)++;
+ }
+}
+
+/// Get the nth quickfix entry above the specified entry treating multiple
+/// entries on a single line as one. Searches backwards in the list.
+static void qf_get_nth_above_entry(qfline_T *entry,
+ int *errornr,
+ linenr_T n)
+{
+ while (n-- > 0 && !got_int) {
+ if (entry->qf_prev == NULL
+ || entry->qf_prev->qf_fnum != entry->qf_fnum) {
+ break;
+ }
+
+ entry = entry->qf_prev;
+ (*errornr)--;
+
+ // If multiple entries are on the same line, then use the first entry
+ entry = qf_find_first_entry_on_line(entry, errornr);
+ }
+}
+
+/// Find the n'th quickfix entry adjacent to line 'lnum' in buffer 'bnr' in the
+/// specified direction.
+/// Returns the error number in the quickfix list or 0 if an entry is not found.
+static int qf_find_nth_adj_entry(qf_list_T *qfl,
+ int bnr,
+ linenr_T lnum,
+ linenr_T n,
+ int dir)
+{
+ qfline_T *adj_entry;
+ int errornr;
+
+ // Find an entry closest to the specified line
+ adj_entry = qf_find_closest_entry(qfl, bnr, lnum, dir, &errornr);
+ if (adj_entry == NULL) {
+ return 0;
+ }
+
+ if (--n > 0) {
+ // Go to the n'th entry in the current buffer
+ if (dir == FORWARD) {
+ qf_get_nth_below_entry(adj_entry, &errornr, n);
+ } else {
+ qf_get_nth_above_entry(adj_entry, &errornr, n);
+ }
+ }
+
+ return errornr;
+}
+
+/// Jump to a quickfix entry in the current file nearest to the current line.
+/// ":cabove", ":cbelow", ":labove" and ":lbelow" commands
+void ex_cbelow(exarg_T *eap)
+{
+ qf_info_T *qi;
+ qf_list_T *qfl;
+ int dir;
+ int buf_has_flag;
+ int errornr = 0;
+
+ if (eap->addr_count > 0 && eap->line2 <= 0) {
+ EMSG(_(e_invrange));
+ return;
+ }
+
+ // Check whether the current buffer has any quickfix entries
+ if (eap->cmdidx == CMD_cabove || eap->cmdidx == CMD_cbelow) {
+ buf_has_flag = BUF_HAS_QF_ENTRY;
+ } else {
+ buf_has_flag = BUF_HAS_LL_ENTRY;
+ }
+ if (!(curbuf->b_has_qf_entry & buf_has_flag)) {
+ EMSG(_(e_quickfix));
+ return;
+ }
+
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
+ }
+
+ qfl = qf_get_curlist(qi);
+ // check if the list has valid errors
+ if (!qf_list_has_valid_entries(qfl)) {
+ EMSG(_(e_quickfix));
+ return;
+ }
+
+ if (eap->cmdidx == CMD_cbelow || eap->cmdidx == CMD_lbelow) {
+ dir = FORWARD;
+ } else {
+ dir = BACKWARD;
+ }
+
+ errornr = qf_find_nth_adj_entry(qfl, curbuf->b_fnum, curwin->w_cursor.lnum,
+ eap->addr_count > 0 ? eap->line2 : 0, dir);
+
+ if (errornr > 0) {
+ qf_jump(qi, 0, errornr, false);
+ } else {
+ EMSG(_(e_no_more_items));
+ }
+}
+
+
+/// Return the autocmd name for the :cfile Ex commands
+static char_u * cfile_get_auname(cmdidx_T cmdidx)
+{
+ switch (cmdidx) {
+ case CMD_cfile: return (char_u *)"cfile";
+ case CMD_cgetfile: return (char_u *)"cgetfile";
+ case CMD_caddfile: return (char_u *)"caddfile";
+ case CMD_lfile: return (char_u *)"lfile";
+ case CMD_lgetfile: return (char_u *)"lgetfile";
+ case CMD_laddfile: return (char_u *)"laddfile";
+ default: return NULL;
+ }
}
+
/*
* ":cfile"/":cgetfile"/":caddfile" commands.
* ":lfile"/":lgetfile"/":laddfile" commands.
@@ -4016,28 +4713,25 @@ void ex_cfile(exarg_T *eap)
qf_info_T *qi = &ql_info;
char_u *au_name = NULL;
- switch (eap->cmdidx) {
- case CMD_cfile: au_name = (char_u *)"cfile"; break;
- case CMD_cgetfile: au_name = (char_u *)"cgetfile"; break;
- case CMD_caddfile: au_name = (char_u *)"caddfile"; break;
- case CMD_lfile: au_name = (char_u *)"lfile"; break;
- case CMD_lgetfile: au_name = (char_u *)"lgetfile"; break;
- case CMD_laddfile: au_name = (char_u *)"laddfile"; break;
- default: break;
+ au_name = cfile_get_auname(eap->cmdidx);
+ if (au_name != NULL
+ && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name, NULL, false, curbuf)) {
+ if (aborting()) {
+ return;
+ }
}
- if (au_name != NULL)
- apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name, NULL, FALSE, curbuf);
- if (*eap->arg != NUL)
+ if (*eap->arg != NUL) {
set_string_option_direct((char_u *)"ef", -1, eap->arg, OPT_FREE, 0);
+ }
char_u *enc = (*curbuf->b_p_menc != NUL) ? curbuf->b_p_menc : p_menc;
- if (eap->cmdidx == CMD_lfile
- || eap->cmdidx == CMD_lgetfile
- || eap->cmdidx == CMD_laddfile) {
+ if (is_loclist_cmd(eap->cmdidx)) {
wp = curwin;
}
+ incr_quickfix_busy();
+
// This function is used by the :cfile, :cgetfile and :caddfile
// commands.
// :cfile always creates a new quickfix list and jumps to the
@@ -4052,13 +4746,14 @@ void ex_cfile(exarg_T *eap)
if (wp != NULL) {
qi = GET_LOC_LIST(wp);
if (qi == NULL) {
+ decr_quickfix_busy();
return;
}
}
if (res >= 0) {
- qf_list_changed(qi, qi->qf_curlist);
+ qf_list_changed(qf_get_curlist(qi));
}
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
if (au_name != NULL) {
apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name, NULL, false, curbuf);
}
@@ -4069,6 +4764,8 @@ void ex_cfile(exarg_T *eap)
// display the first error
qf_jump_first(qi, save_qfid, eap->forceit);
}
+
+ decr_quickfix_busy();
}
/// Return the vimgrep autocmd name.
@@ -4189,8 +4886,7 @@ static bool vgr_match_buflines(qf_info_T *qi, char_u *fname, buf_T *buf,
// Pass the buffer number so that it gets used even for a
// dummy buffer, unless duplicate_name is set, then the
// buffer will be wiped out below.
- if (qf_add_entry(qi,
- qi->qf_curlist,
+ if (qf_add_entry(qf_get_curlist(qi),
NULL, // dir
fname,
NULL,
@@ -4203,8 +4899,8 @@ static bool vgr_match_buflines(qf_info_T *qi, char_u *fname, buf_T *buf,
NULL, // search pattern
0, // nr
0, // type
- true // valid
- ) == FAIL) {
+ true) // valid
+ == QF_FAIL) {
got_int = true;
break;
}
@@ -4266,7 +4962,7 @@ void ex_vimgrep(exarg_T *eap)
char_u *s;
char_u *p;
int fi;
- qf_info_T *qi = &ql_info;
+ qf_list_T *qfl;
win_T *wp = NULL;
buf_T *buf;
int duplicate_name = FALSE;
@@ -4291,13 +4987,7 @@ void ex_vimgrep(exarg_T *eap)
}
}
- if (eap->cmdidx == CMD_lgrep
- || eap->cmdidx == CMD_lvimgrep
- || eap->cmdidx == CMD_lgrepadd
- || eap->cmdidx == CMD_lvimgrepadd) {
- qi = ll_get_or_alloc_list(curwin);
- wp = curwin;
- }
+ qf_info_T *qi = qf_cmd_get_or_alloc_stack(eap, &wp);
if (eap->addr_count > 0)
tomatch = eap->line2;
@@ -4326,7 +5016,7 @@ void ex_vimgrep(exarg_T *eap)
if ((eap->cmdidx != CMD_grepadd && eap->cmdidx != CMD_lgrepadd
&& eap->cmdidx != CMD_vimgrepadd && eap->cmdidx != CMD_lvimgrepadd)
- || qi->qf_curlist == qi->qf_listcount) {
+ || qf_stack_empty(qi)) {
// make place for a new list
qf_new_list(qi, title);
}
@@ -4346,9 +5036,11 @@ void ex_vimgrep(exarg_T *eap)
* ":lcd %:p:h" changes the meaning of short path names. */
os_dirname(dirname_start, MAXPATHL);
+ incr_quickfix_busy();
+
// Remember the current quickfix list identifier, so that we can check for
// autocommands changing the current quickfix list.
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
seconds = (time_t)0;
for (fi = 0; fi < fcount && !got_int && tomatch > 0; fi++) {
@@ -4377,9 +5069,10 @@ void ex_vimgrep(exarg_T *eap)
// buffer above, autocommands might have changed the quickfix list.
if (!vgr_qflist_valid(wp, qi, save_qfid, *eap->cmdlinep)) {
FreeWild(fcount, fnames);
+ decr_quickfix_busy();
goto theend;
}
- save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ save_qfid = qf_get_curlist(qi)->qf_id;
if (buf == NULL) {
if (!got_int)
@@ -4447,10 +5140,11 @@ void ex_vimgrep(exarg_T *eap)
FreeWild(fcount, fnames);
- qi->qf_lists[qi->qf_curlist].qf_nonevalid = FALSE;
- qi->qf_lists[qi->qf_curlist].qf_ptr = qi->qf_lists[qi->qf_curlist].qf_start;
- qi->qf_lists[qi->qf_curlist].qf_index = 1;
- qf_list_changed(qi, qi->qf_curlist);
+ qfl = qf_get_curlist(qi);
+ qfl->qf_nonevalid = false;
+ qfl->qf_ptr = qfl->qf_start;
+ qfl->qf_index = 1;
+ qf_list_changed(qfl);
qf_update_buffer(qi, NULL);
@@ -4460,16 +5154,14 @@ void ex_vimgrep(exarg_T *eap)
// The QuickFixCmdPost autocmd may free the quickfix list. Check the list
// is still valid.
- if (!qflist_valid(wp, save_qfid)) {
- goto theend;
- }
-
- if (qf_restore_list(qi, save_qfid) == FAIL) {
+ if (!qflist_valid(wp, save_qfid)
+ || qf_restore_list(qi, save_qfid) == FAIL) {
+ decr_quickfix_busy();
goto theend;
}
// Jump to first match.
- if (qi->qf_lists[qi->qf_curlist].qf_count > 0) {
+ if (!qf_list_empty(qf_get_curlist(qi))) {
if ((flags & VGR_NOJUMP) == 0) {
vgr_jump_to_match(qi, eap->forceit, &redraw_for_dummy, first_match_buf,
target_dir);
@@ -4477,6 +5169,8 @@ void ex_vimgrep(exarg_T *eap)
} else
EMSG2(_(e_nomatch2), s);
+ decr_quickfix_busy();
+
/* If we loaded a dummy buffer into the current window, the autocommands
* may have messed up things, need to redraw and recompute folds. */
if (redraw_for_dummy) {
@@ -4653,15 +5347,62 @@ static void unload_dummy_buffer(buf_T *buf, char_u *dirname_start)
}
}
+/// Copy the specified quickfix entry items into a new dict and appened the dict
+/// to 'list'. Returns OK on success.
+static int get_qfline_items(qfline_T *qfp, list_T *list)
+{
+ char_u buf[2];
+ int bufnum;
+
+ // Handle entries with a non-existing buffer number.
+ bufnum = qfp->qf_fnum;
+ if (bufnum != 0 && (buflist_findnr(bufnum) == NULL)) {
+ bufnum = 0;
+ }
+
+ dict_T *const dict = tv_dict_alloc();
+ tv_list_append_dict(list, dict);
+
+ buf[0] = qfp->qf_type;
+ buf[1] = NUL;
+ if (tv_dict_add_nr(dict, S_LEN("bufnr"), (varnumber_T)bufnum) == FAIL
+ || (tv_dict_add_nr(dict, S_LEN("lnum"), (varnumber_T)qfp->qf_lnum)
+ == FAIL)
+ || (tv_dict_add_nr(dict, S_LEN("col"), (varnumber_T)qfp->qf_col) == FAIL)
+ || (tv_dict_add_nr(dict, S_LEN("vcol"), (varnumber_T)qfp->qf_viscol)
+ == FAIL)
+ || (tv_dict_add_nr(dict, S_LEN("nr"), (varnumber_T)qfp->qf_nr) == FAIL)
+ || (tv_dict_add_str(
+ dict, S_LEN("module"),
+ (qfp->qf_module == NULL ? "" : (const char *)qfp->qf_module))
+ == FAIL)
+ || (tv_dict_add_str(
+ dict, S_LEN("pattern"),
+ (qfp->qf_pattern == NULL ? "" : (const char *)qfp->qf_pattern))
+ == FAIL)
+ || (tv_dict_add_str(
+ dict, S_LEN("text"),
+ (qfp->qf_text == NULL ? "" : (const char *)qfp->qf_text))
+ == FAIL)
+ || (tv_dict_add_str(dict, S_LEN("type"), (const char *)buf) == FAIL)
+ || (tv_dict_add_nr(dict, S_LEN("valid"), (varnumber_T)qfp->qf_valid)
+ == FAIL)) {
+ // tv_dict_add* fail only if key already exist, but this is a newly
+ // allocated dictionary which is thus guaranteed to have no existing keys.
+ assert(false);
+ }
+
+ return OK;
+}
+
/// Add each quickfix error to list "list" as a dictionary.
/// If qf_idx is -1, use the current list. Otherwise, use the specified list.
-int get_errorlist(const qf_info_T *qi_arg, win_T *wp, int qf_idx, list_T *list)
+int get_errorlist(qf_info_T *qi_arg, win_T *wp, int qf_idx, list_T *list)
{
- const qf_info_T *qi = qi_arg;
- char_u buf[2];
+ qf_info_T *qi = qi_arg;
+ qf_list_T *qfl;
qfline_T *qfp;
int i;
- int bufnum;
if (qi == NULL) {
qi = &ql_info;
@@ -4677,56 +5418,19 @@ int get_errorlist(const qf_info_T *qi_arg, win_T *wp, int qf_idx, list_T *list)
qf_idx = qi->qf_curlist;
}
- if (qf_idx >= qi->qf_listcount
- || qi->qf_lists[qf_idx].qf_count == 0) {
+ if (qf_idx >= qi->qf_listcount) {
return FAIL;
}
- qfp = qi->qf_lists[qf_idx].qf_start;
- for (i = 1; !got_int && i <= qi->qf_lists[qf_idx].qf_count; i++) {
- // Handle entries with a non-existing buffer number.
- bufnum = qfp->qf_fnum;
- if (bufnum != 0 && (buflist_findnr(bufnum) == NULL))
- bufnum = 0;
-
- dict_T *const dict = tv_dict_alloc();
- tv_list_append_dict(list, dict);
-
- buf[0] = qfp->qf_type;
- buf[1] = NUL;
- if (tv_dict_add_nr(dict, S_LEN("bufnr"), (varnumber_T)bufnum) == FAIL
- || (tv_dict_add_nr(dict, S_LEN("lnum"), (varnumber_T)qfp->qf_lnum)
- == FAIL)
- || (tv_dict_add_nr(dict, S_LEN("col"), (varnumber_T)qfp->qf_col)
- == FAIL)
- || (tv_dict_add_nr(dict, S_LEN("vcol"), (varnumber_T)qfp->qf_viscol)
- == FAIL)
- || (tv_dict_add_nr(dict, S_LEN("nr"), (varnumber_T)qfp->qf_nr) == FAIL)
- || tv_dict_add_str(dict, S_LEN("module"),
- (qfp->qf_module == NULL
- ? ""
- : (const char *)qfp->qf_module)) == FAIL
- || tv_dict_add_str(dict, S_LEN("pattern"),
- (qfp->qf_pattern == NULL
- ? ""
- : (const char *)qfp->qf_pattern)) == FAIL
- || tv_dict_add_str(dict, S_LEN("text"),
- (qfp->qf_text == NULL
- ? ""
- : (const char *)qfp->qf_text)) == FAIL
- || tv_dict_add_str(dict, S_LEN("type"), (const char *)buf) == FAIL
- || (tv_dict_add_nr(dict, S_LEN("valid"), (varnumber_T)qfp->qf_valid)
- == FAIL)) {
- // tv_dict_add* fail only if key already exist, but this is a newly
- // allocated dictionary which is thus guaranteed to have no existing keys.
- assert(false);
- }
+ qfl = qf_get_list(qi, qf_idx);
+ if (qf_list_empty(qfl)) {
+ return FAIL;
+ }
- qfp = qfp->qf_next;
- if (qfp == NULL) {
- break;
- }
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
+ get_qfline_items(qfp, list);
}
+
return OK;
}
@@ -4742,7 +5446,8 @@ enum {
QF_GETLIST_IDX = 0x40,
QF_GETLIST_SIZE = 0x80,
QF_GETLIST_TICK = 0x100,
- QF_GETLIST_ALL = 0x1FF
+ QF_GETLIST_FILEWINID = 0x200,
+ QF_GETLIST_ALL = 0x3FF,
};
/// Parse text from 'di' and return the quickfix list items.
@@ -4766,12 +5471,12 @@ static int qf_get_list_from_lines(dict_T *what, dictitem_T *di, dict_T *retdict)
}
list_T *l = tv_list_alloc(kListLenMayKnow);
- qf_info_T *const qi = ll_new_list();
+ qf_info_T *const qi = qf_alloc_stack(QFLT_INTERNAL);
if (qf_init_ext(qi, 0, NULL, NULL, &di->di_tv, errorformat,
true, (linenr_T)0, (linenr_T)0, NULL, NULL) > 0) {
(void)get_errorlist(qi, NULL, 0, l);
- qf_free(qi, 0);
+ qf_free(&qi->qf_lists[0]);
}
xfree(qi);
@@ -4798,12 +5503,17 @@ static int qf_winid(qf_info_T *qi)
}
/// Convert the keys in 'what' to quickfix list property flags.
-static int qf_getprop_keys2flags(dict_T *what)
+static int qf_getprop_keys2flags(const dict_T *what, bool loclist)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
int flags = QF_GETLIST_NONE;
if (tv_dict_find(what, S_LEN("all")) != NULL) {
flags |= QF_GETLIST_ALL;
+ if (!loclist) {
+ // File window ID is applicable only to location list windows
+ flags &= ~QF_GETLIST_FILEWINID;
+ }
}
if (tv_dict_find(what, S_LEN("title")) != NULL) {
flags |= QF_GETLIST_TITLE;
@@ -4832,6 +5542,9 @@ static int qf_getprop_keys2flags(dict_T *what)
if (tv_dict_find(what, S_LEN("changedtick")) != NULL) {
flags |= QF_GETLIST_TICK;
}
+ if (loclist && tv_dict_find(what, S_LEN("filewinid")) != NULL) {
+ flags |= QF_GETLIST_FILEWINID;
+ }
return flags;
}
@@ -4885,7 +5598,10 @@ static int qf_getprop_qfidx(qf_info_T *qi, dict_T *what)
}
/// Return default values for quickfix list properties in retdict.
-static int qf_getprop_defaults(qf_info_T *qi, int flags, dict_T *retdict)
+static int qf_getprop_defaults(qf_info_T *qi,
+ int flags,
+ int locstack,
+ dict_T *retdict)
{
int status = OK;
@@ -4917,15 +5633,37 @@ static int qf_getprop_defaults(qf_info_T *qi, int flags, dict_T *retdict)
if ((status == OK) && (flags & QF_GETLIST_TICK)) {
status = tv_dict_add_nr(retdict, S_LEN("changedtick"), 0);
}
+ if ((status == OK) && locstack && (flags & QF_GETLIST_FILEWINID)) {
+ status = tv_dict_add_nr(retdict, S_LEN("filewinid"), 0);
+ }
return status;
}
/// Return the quickfix list title as 'title' in retdict
-static int qf_getprop_title(qf_info_T *qi, int qf_idx, dict_T *retdict)
+static int qf_getprop_title(qf_list_T *qfl, dict_T *retdict)
{
return tv_dict_add_str(retdict, S_LEN("title"),
- (const char *)qi->qf_lists[qf_idx].qf_title);
+ (const char *)qfl->qf_title);
+}
+
+// Returns the identifier of the window used to display files from a location
+// list. If there is no associated window, then returns 0. Useful only when
+// called from a location list window.
+static int qf_getprop_filewinid(const win_T *wp, const qf_info_T *qi,
+ dict_T *retdict)
+ FUNC_ATTR_NONNULL_ARG(3)
+{
+ handle_T winid = 0;
+
+ if (wp != NULL && IS_LL_WINDOW(wp)) {
+ win_T *ll_wp = qf_find_win_with_loclist(qi);
+ if (ll_wp != NULL) {
+ winid = ll_wp->handle;
+ }
+ }
+
+ return tv_dict_add_nr(retdict, S_LEN("filewinid"), winid);
}
/// Return the quickfix list items/entries as 'items' in retdict
@@ -4939,13 +5677,13 @@ static int qf_getprop_items(qf_info_T *qi, int qf_idx, dict_T *retdict)
}
/// Return the quickfix list context (if any) as 'context' in retdict.
-static int qf_getprop_ctx(qf_info_T *qi, int qf_idx, dict_T *retdict)
+static int qf_getprop_ctx(qf_list_T *qfl, dict_T *retdict)
{
int status;
- if (qi->qf_lists[qf_idx].qf_ctx != NULL) {
+ if (qfl->qf_ctx != NULL) {
dictitem_T *di = tv_dict_item_alloc_len(S_LEN("context"));
- tv_copy(qi->qf_lists[qf_idx].qf_ctx, &di->di_tv);
+ tv_copy(qfl->qf_ctx, &di->di_tv);
status = tv_dict_add(retdict, di);
if (status == FAIL) {
tv_dict_item_free(di);
@@ -4957,15 +5695,15 @@ static int qf_getprop_ctx(qf_info_T *qi, int qf_idx, dict_T *retdict)
return status;
}
-/// Return the quickfix list index as 'idx' in retdict
-static int qf_getprop_idx(qf_info_T *qi, int qf_idx, dict_T *retdict)
+/// Return the current quickfix list index as 'idx' in retdict
+static int qf_getprop_idx(qf_list_T *qfl, dict_T *retdict)
{
- int idx = qi->qf_lists[qf_idx].qf_index;
- if (qi->qf_lists[qf_idx].qf_count == 0) {
- // For empty lists, qf_index is set to 1
- idx = 0;
+ int curidx = qfl->qf_index;
+ if (qf_list_empty(qfl)) {
+ // For empty lists, current index is set to 0
+ curidx = 0;
}
- return tv_dict_add_nr(retdict, S_LEN("idx"), idx);
+ return tv_dict_add_nr(retdict, S_LEN("idx"), curidx);
}
/// Return quickfix/location list details (title) as a dictionary.
@@ -4974,9 +5712,10 @@ static int qf_getprop_idx(qf_info_T *qi, int qf_idx, dict_T *retdict)
int qf_get_properties(win_T *wp, dict_T *what, dict_T *retdict)
{
qf_info_T *qi = &ql_info;
+ qf_list_T *qfl;
dictitem_T *di = NULL;
int status = OK;
- int qf_idx;
+ int qf_idx = INVALID_QFIDX;
if ((di = tv_dict_find(what, S_LEN("lines"))) != NULL) {
return qf_get_list_from_lines(what, di, retdict);
@@ -4986,19 +5725,21 @@ int qf_get_properties(win_T *wp, dict_T *what, dict_T *retdict)
qi = GET_LOC_LIST(wp);
}
- int flags = qf_getprop_keys2flags(what);
+ const int flags = qf_getprop_keys2flags(what, wp != NULL);
- if (qi != NULL && qi->qf_listcount != 0) {
+ if (!qf_stack_empty(qi)) {
qf_idx = qf_getprop_qfidx(qi, what);
}
// List is not present or is empty
- if (qi == NULL || qi->qf_listcount == 0 || qf_idx == INVALID_QFIDX) {
- return qf_getprop_defaults(qi, flags, retdict);
+ if (qf_stack_empty(qi) || qf_idx == INVALID_QFIDX) {
+ return qf_getprop_defaults(qi, flags, wp != NULL, retdict);
}
+ qfl = qf_get_list(qi, qf_idx);
+
if (flags & QF_GETLIST_TITLE) {
- status = qf_getprop_title(qi, qf_idx, retdict);
+ status = qf_getprop_title(qfl, retdict);
}
if ((status == OK) && (flags & QF_GETLIST_NR)) {
status = tv_dict_add_nr(retdict, S_LEN("nr"), qf_idx + 1);
@@ -5010,33 +5751,37 @@ int qf_get_properties(win_T *wp, dict_T *what, dict_T *retdict)
status = qf_getprop_items(qi, qf_idx, retdict);
}
if ((status == OK) && (flags & QF_GETLIST_CONTEXT)) {
- status = qf_getprop_ctx(qi, qf_idx, retdict);
+ status = qf_getprop_ctx(qfl, retdict);
}
if ((status == OK) && (flags & QF_GETLIST_ID)) {
- status = tv_dict_add_nr(retdict, S_LEN("id"), qi->qf_lists[qf_idx].qf_id);
+ status = tv_dict_add_nr(retdict, S_LEN("id"), qfl->qf_id);
}
if ((status == OK) && (flags & QF_GETLIST_IDX)) {
- status = qf_getprop_idx(qi, qf_idx, retdict);
+ status = qf_getprop_idx(qfl, retdict);
}
if ((status == OK) && (flags & QF_GETLIST_SIZE)) {
status = tv_dict_add_nr(retdict, S_LEN("size"),
- qi->qf_lists[qf_idx].qf_count);
+ qfl->qf_count);
}
if ((status == OK) && (flags & QF_GETLIST_TICK)) {
status = tv_dict_add_nr(retdict, S_LEN("changedtick"),
- qi->qf_lists[qf_idx].qf_changedtick);
+ qfl->qf_changedtick);
+ }
+ if ((status == OK) && (wp != NULL) && (flags & QF_GETLIST_FILEWINID)) {
+ status = qf_getprop_filewinid(wp, qi, retdict);
}
return status;
}
-// Add a new quickfix entry to list at 'qf_idx' in the stack 'qi' from the
-// items in the dict 'd'.
+/// Add a new quickfix entry to list at 'qf_idx' in the stack 'qi' from the
+/// items in the dict 'd'. If it is a valid error entry, then set 'valid_entry'
+/// to true.
static int qf_add_entry_from_dict(
- qf_info_T *qi,
- int qf_idx,
+ qf_list_T *qfl,
const dict_T *d,
- bool first_entry)
+ bool first_entry,
+ bool *valid_entry)
FUNC_ATTR_NONNULL_ALL
{
static bool did_bufnr_emsg;
@@ -5080,17 +5825,16 @@ static int qf_add_entry_from_dict(
valid = tv_dict_get_number(d, "valid");
}
- const int status = qf_add_entry(qi,
- qf_idx,
- NULL, // dir
+ const int status = qf_add_entry(qfl,
+ NULL, // dir
(char_u *)filename,
(char_u *)module,
bufnum,
(char_u *)text,
lnum,
col,
- vcol, // vis_col
- (char_u *)pattern, // search pattern
+ vcol, // vis_col
+ (char_u *)pattern, // search pattern
nr,
(char_u)(type == NULL ? NUL : *type),
valid);
@@ -5100,6 +5844,10 @@ static int qf_add_entry_from_dict(
xfree(pattern);
xfree(text);
+ if (valid) {
+ *valid_entry = true;
+ }
+
return status;
}
@@ -5108,19 +5856,22 @@ static int qf_add_entry_from_dict(
static int qf_add_entries(qf_info_T *qi, int qf_idx, list_T *list,
char_u *title, int action)
{
+ qf_list_T *qfl = qf_get_list(qi, qf_idx);
qfline_T *old_last = NULL;
int retval = OK;
+ bool valid_entry = false;
if (action == ' ' || qf_idx == qi->qf_listcount) {
// make place for a new list
qf_new_list(qi, title);
qf_idx = qi->qf_curlist;
- } else if (action == 'a' && qi->qf_lists[qf_idx].qf_count > 0) {
+ qfl = qf_get_list(qi, qf_idx);
+ } else if (action == 'a' && !qf_list_empty(qfl)) {
// Adding to existing list, use last entry.
- old_last = qi->qf_lists[qf_idx].qf_last;
+ old_last = qfl->qf_last;
} else if (action == 'r') {
- qf_free_items(qi, qf_idx);
- qf_store_title(qi, qf_idx, title);
+ qf_free_items(qfl);
+ qf_store_title(qfl, title);
}
TV_LIST_ITER_CONST(list, li, {
@@ -5133,23 +5884,30 @@ static int qf_add_entries(qf_info_T *qi, int qf_idx, list_T *list,
continue;
}
- retval = qf_add_entry_from_dict(qi, qf_idx, d, li == tv_list_first(list));
- if (retval == FAIL) {
+ retval = qf_add_entry_from_dict(qfl, d, li == tv_list_first(list),
+ &valid_entry);
+ if (retval == QF_FAIL) {
break;
}
});
- if (qi->qf_lists[qf_idx].qf_index == 0) {
- // no valid entry
- qi->qf_lists[qf_idx].qf_nonevalid = true;
- } else {
- qi->qf_lists[qf_idx].qf_nonevalid = false;
+ // Check if any valid error entries are added to the list.
+ if (valid_entry) {
+ qfl->qf_nonevalid = false;
+ } else if (qfl->qf_index == 0) {
+ qfl->qf_nonevalid = true;
}
+
+ // If not appending to the list, set the current error to the first entry
if (action != 'a') {
- qi->qf_lists[qf_idx].qf_ptr = qi->qf_lists[qf_idx].qf_start;
- if (qi->qf_lists[qf_idx].qf_count > 0) {
- qi->qf_lists[qf_idx].qf_index = 1;
- }
+ qfl->qf_ptr = qfl->qf_start;
+ }
+
+ // Update the current error index if not appending to the list or if the
+ // list was empty before and it is not empty now.
+ if ((action != 'a' || qfl->qf_index == 0)
+ && !qf_list_empty(qfl)) {
+ qfl->qf_index = 1;
}
// Don't update the cursor in quickfix window when appending entries
@@ -5158,7 +5916,7 @@ static int qf_add_entries(qf_info_T *qi, int qf_idx, list_T *list,
return retval;
}
-// Get the quickfix list index from 'nr' or 'id'
+/// Get the quickfix list index from 'nr' or 'id'
static int qf_setprop_get_qfidx(
const qf_info_T *qi,
const dict_T *what,
@@ -5182,7 +5940,7 @@ static int qf_setprop_get_qfidx(
// non-available list and add the new list at the end of the
// stack.
*newlist = true;
- qf_idx = qi->qf_listcount > 0 ? qi->qf_listcount - 1 : 0;
+ qf_idx = qf_stack_empty(qi) ? 0 : qi->qf_listcount - 1;
} else if (qf_idx < 0 || qf_idx >= qi->qf_listcount) {
return INVALID_QFIDX;
} else if (action != ' ') {
@@ -5190,7 +5948,7 @@ static int qf_setprop_get_qfidx(
}
} else if (di->di_tv.v_type == VAR_STRING
&& strequal((const char *)di->di_tv.vval.v_string, "$")) {
- if (qi->qf_listcount > 0) {
+ if (!qf_stack_empty(qi)) {
qf_idx = qi->qf_listcount - 1;
} else if (*newlist) {
qf_idx = 0;
@@ -5218,13 +5976,13 @@ static int qf_setprop_title(qf_info_T *qi, int qf_idx, const dict_T *what,
const dictitem_T *di)
FUNC_ATTR_NONNULL_ALL
{
+ qf_list_T *qfl = qf_get_list(qi, qf_idx);
if (di->di_tv.v_type != VAR_STRING) {
return FAIL;
}
- xfree(qi->qf_lists[qf_idx].qf_title);
- qi->qf_lists[qf_idx].qf_title =
- (char_u *)tv_dict_get_string(what, "title", true);
+ xfree(qfl->qf_title);
+ qfl->qf_title = (char_u *)tv_dict_get_string(what, "title", true);
if (qf_idx == qi->qf_curlist) {
qf_update_win_titlevar(qi);
}
@@ -5278,7 +6036,7 @@ static int qf_setprop_items_from_lines(
}
if (action == 'r') {
- qf_free_items(qi, qf_idx);
+ qf_free_items(&qi->qf_lists[qf_idx]);
}
if (qf_init_ext(qi, qf_idx, NULL, NULL, &di->di_tv, errorformat,
false, (linenr_T)0, (linenr_T)0, NULL, NULL) > 0) {
@@ -5289,27 +6047,28 @@ static int qf_setprop_items_from_lines(
}
// Set quickfix list context.
-static int qf_setprop_context(qf_info_T *qi, int qf_idx, dictitem_T *di)
+static int qf_setprop_context(qf_list_T *qfl, dictitem_T *di)
FUNC_ATTR_NONNULL_ALL
{
- tv_free(qi->qf_lists[qf_idx].qf_ctx);
+ tv_free(qfl->qf_ctx);
typval_T *ctx = xcalloc(1, sizeof(typval_T));
tv_copy(&di->di_tv, ctx);
- qi->qf_lists[qf_idx].qf_ctx = ctx;
+ qfl->qf_ctx = ctx;
return OK;
}
-// Set quickfix/location list properties (title, items, context).
-// Also used to add items from parsing a list of lines.
-// Used by the setqflist() and setloclist() Vim script functions.
+/// Set quickfix/location list properties (title, items, context).
+/// Also used to add items from parsing a list of lines.
+/// Used by the setqflist() and setloclist() Vim script functions.
static int qf_set_properties(qf_info_T *qi, const dict_T *what, int action,
char_u *title)
FUNC_ATTR_NONNULL_ALL
{
+ qf_list_T *qfl;
dictitem_T *di;
int retval = FAIL;
- bool newlist = action == ' ' || qi->qf_curlist == qi->qf_listcount;
+ bool newlist = action == ' ' || qf_stack_empty(qi);
int qf_idx = qf_setprop_get_qfidx(qi, what, action, &newlist);
if (qf_idx == INVALID_QFIDX) { // List not found
return FAIL;
@@ -5321,6 +6080,7 @@ static int qf_set_properties(qf_info_T *qi, const dict_T *what, int action,
qf_idx = qi->qf_curlist;
}
+ qfl = qf_get_list(qi, qf_idx);
if ((di = tv_dict_find(what, S_LEN("title"))) != NULL) {
retval = qf_setprop_title(qi, qf_idx, what, di);
}
@@ -5331,17 +6091,18 @@ static int qf_set_properties(qf_info_T *qi, const dict_T *what, int action,
retval = qf_setprop_items_from_lines(qi, qf_idx, what, di, action);
}
if ((di = tv_dict_find(what, S_LEN("context"))) != NULL) {
- retval = qf_setprop_context(qi, qf_idx, di);
+ retval = qf_setprop_context(qfl, di);
}
if (retval == OK) {
- qf_list_changed(qi, qf_idx);
+ qf_list_changed(qfl);
}
return retval;
}
-// Find the non-location list window with the specified location list.
+/// Find the non-location list window with the specified location list stack in
+/// the current tabpage.
static win_T * find_win_with_ll(qf_info_T *qi)
{
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
@@ -5362,7 +6123,7 @@ static void qf_free_stack(win_T *wp, qf_info_T *qi)
if (qfwin != NULL) {
// If the quickfix/location list window is open, then clear it
if (qi->qf_curlist < qi->qf_listcount) {
- qf_free(qi, qi->qf_curlist);
+ qf_free(qf_get_curlist(qi));
}
qf_update_buffer(qi, NULL);
}
@@ -5386,15 +6147,14 @@ static void qf_free_stack(win_T *wp, qf_info_T *qi)
} else if (IS_LL_WINDOW(orig_wp)) {
// If the location list window is open, then create a new empty location
// list
- qf_info_T *new_ll = ll_new_list();
+ qf_info_T *new_ll = qf_alloc_stack(QFLT_LOCATION);
// first free the list reference in the location list window
ll_free_all(&orig_wp->w_llist_ref);
orig_wp->w_llist_ref = new_ll;
if (llwin != NULL) {
- llwin->w_llist = new_ll;
- new_ll->qf_refcount++;
+ win_set_loclist(wp, new_ll);
}
}
}
@@ -5415,15 +6175,22 @@ int set_errorlist(win_T *wp, list_T *list, int action, char_u *title,
if (action == 'f') {
// Free the entire quickfix or location list stack
qf_free_stack(wp, qi);
- } else if (what != NULL) {
+ return OK;
+ }
+
+ incr_quickfix_busy();
+
+ if (what != NULL) {
retval = qf_set_properties(qi, what, action, title);
} else {
retval = qf_add_entries(qi, qi->qf_curlist, list, title, action);
if (retval == OK) {
- qf_list_changed(qi, qi->qf_curlist);
+ qf_list_changed(qf_get_curlist(qi));
}
}
+ decr_quickfix_busy();
+
return retval;
}
@@ -5474,6 +6241,62 @@ bool set_ref_in_quickfix(int copyID)
return abort;
}
+/// Return the autocmd name for the :cbuffer Ex commands
+static char_u * cbuffer_get_auname(cmdidx_T cmdidx)
+{
+ switch (cmdidx) {
+ case CMD_cbuffer: return (char_u *)"cbuffer";
+ case CMD_cgetbuffer: return (char_u *)"cgetbuffer";
+ case CMD_caddbuffer: return (char_u *)"caddbuffer";
+ case CMD_lbuffer: return (char_u *)"lbuffer";
+ case CMD_lgetbuffer: return (char_u *)"lgetbuffer";
+ case CMD_laddbuffer: return (char_u *)"laddbuffer";
+ default: return NULL;
+ }
+}
+
+/// Process and validate the arguments passed to the :cbuffer, :caddbuffer,
+/// :cgetbuffer, :lbuffer, :laddbuffer, :lgetbuffer Ex commands.
+static int cbuffer_process_args(exarg_T *eap,
+ buf_T **bufp,
+ linenr_T *line1,
+ linenr_T *line2)
+{
+ buf_T *buf = NULL;
+
+ if (*eap->arg == NUL)
+ buf = curbuf;
+ else if (*skipwhite(skipdigits(eap->arg)) == NUL)
+ buf = buflist_findnr(atoi((char *)eap->arg));
+
+ if (buf == NULL) {
+ EMSG(_(e_invarg));
+ return FAIL;
+ }
+
+ if (buf->b_ml.ml_mfp == NULL) {
+ EMSG(_("E681: Buffer is not loaded"));
+ return FAIL;
+ }
+
+ if (eap->addr_count == 0) {
+ eap->line1 = 1;
+ eap->line2 = buf->b_ml.ml_line_count;
+ }
+
+ if (eap->line1 < 1 || eap->line1 > buf->b_ml.ml_line_count
+ || eap->line2 < 1 || eap->line2 > buf->b_ml.ml_line_count) {
+ EMSG(_(e_invrange));
+ return FAIL;
+ }
+
+ *line1 = eap->line1;
+ *line2 = eap->line2;
+ *bufp = buf;
+
+ return OK;
+}
+
/*
* ":[range]cbuffer [bufnr]" command.
* ":[range]caddbuffer [bufnr]" command.
@@ -5485,34 +6308,14 @@ bool set_ref_in_quickfix(int copyID)
void ex_cbuffer(exarg_T *eap)
{
buf_T *buf = NULL;
- qf_info_T *qi = &ql_info;
- const char *au_name = NULL;
+ char_u *au_name = NULL;
win_T *wp = NULL;
+ char_u *qf_title;
+ linenr_T line1;
+ linenr_T line2;
- switch (eap->cmdidx) {
- case CMD_cbuffer:
- au_name = "cbuffer";
- break;
- case CMD_cgetbuffer:
- au_name = "cgetbuffer";
- break;
- case CMD_caddbuffer:
- au_name = "caddbuffer";
- break;
- case CMD_lbuffer:
- au_name = "lbuffer";
- break;
- case CMD_lgetbuffer:
- au_name = "lgetbuffer";
- break;
- case CMD_laddbuffer:
- au_name = "laddbuffer";
- break;
- default:
- break;
- }
-
- if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, (char_u *)au_name,
+ au_name = cbuffer_get_auname(eap->cmdidx);
+ if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name,
curbuf->b_fname, true, curbuf)) {
if (aborting()) {
return;
@@ -5520,114 +6323,87 @@ void ex_cbuffer(exarg_T *eap)
}
// Must come after autocommands.
- if (eap->cmdidx == CMD_lbuffer
- || eap->cmdidx == CMD_lgetbuffer
- || eap->cmdidx == CMD_laddbuffer) {
- qi = ll_get_or_alloc_list(curwin);
- wp = curwin;
+ qf_info_T *qi = qf_cmd_get_or_alloc_stack(eap, &wp);
+
+ if (cbuffer_process_args(eap, &buf, &line1, &line2) == FAIL) {
+ return;
}
- if (*eap->arg == NUL)
- buf = curbuf;
- else if (*skipwhite(skipdigits(eap->arg)) == NUL)
- buf = buflist_findnr(atoi((char *)eap->arg));
- if (buf == NULL)
- EMSG(_(e_invarg));
- else if (buf->b_ml.ml_mfp == NULL)
- EMSG(_("E681: Buffer is not loaded"));
- else {
- if (eap->addr_count == 0) {
- eap->line1 = 1;
- eap->line2 = buf->b_ml.ml_line_count;
- }
- if (eap->line1 < 1 || eap->line1 > buf->b_ml.ml_line_count
- || eap->line2 < 1 || eap->line2 > buf->b_ml.ml_line_count) {
- EMSG(_(e_invrange));
- } else {
- char_u *qf_title = qf_cmdtitle(*eap->cmdlinep);
+ qf_title = qf_cmdtitle(*eap->cmdlinep);
- if (buf->b_sfname) {
- vim_snprintf((char *)IObuff, IOSIZE, "%s (%s)",
- (char *)qf_title, (char *)buf->b_sfname);
- qf_title = IObuff;
- }
+ if (buf->b_sfname) {
+ vim_snprintf((char *)IObuff, IOSIZE, "%s (%s)",
+ (char *)qf_title, (char *)buf->b_sfname);
+ qf_title = IObuff;
+ }
- int res = qf_init_ext(qi, qi->qf_curlist, NULL, buf, NULL, p_efm,
- (eap->cmdidx != CMD_caddbuffer
- && eap->cmdidx != CMD_laddbuffer),
- eap->line1, eap->line2, qf_title, NULL);
- if (res >= 0) {
- qf_list_changed(qi, qi->qf_curlist);
- }
- // Remember the current quickfix list identifier, so that we can
- // check for autocommands changing the current quickfix list.
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
- if (au_name != NULL) {
- const buf_T *const curbuf_old = curbuf;
- apply_autocmds(EVENT_QUICKFIXCMDPOST, (char_u *)au_name,
- curbuf->b_fname, true, curbuf);
- if (curbuf != curbuf_old) {
- // Autocommands changed buffer, don't jump now, "qi" may
- // be invalid.
- res = 0;
- }
- }
- // Jump to the first error for new list and if autocmds didn't
- // free the list.
- if (res > 0 && (eap->cmdidx == CMD_cbuffer || eap->cmdidx == CMD_lbuffer)
- && qflist_valid(wp, save_qfid)) {
- // display the first error
- qf_jump_first(qi, save_qfid, eap->forceit);
- }
+ incr_quickfix_busy();
+
+ int res = qf_init_ext(qi, qi->qf_curlist, NULL, buf, NULL, p_efm,
+ (eap->cmdidx != CMD_caddbuffer
+ && eap->cmdidx != CMD_laddbuffer),
+ eap->line1, eap->line2, qf_title, NULL);
+ if (qf_stack_empty(qi)) {
+ decr_quickfix_busy();
+ return;
+ }
+ if (res >= 0) {
+ qf_list_changed(qf_get_curlist(qi));
+ }
+ // Remember the current quickfix list identifier, so that we can
+ // check for autocommands changing the current quickfix list.
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
+ if (au_name != NULL) {
+ const buf_T *const curbuf_old = curbuf;
+ apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name,
+ curbuf->b_fname, true, curbuf);
+ if (curbuf != curbuf_old) {
+ // Autocommands changed buffer, don't jump now, "qi" may
+ // be invalid.
+ res = 0;
}
}
+ // Jump to the first error for new list and if autocmds didn't
+ // free the list.
+ if (res > 0 && (eap->cmdidx == CMD_cbuffer || eap->cmdidx == CMD_lbuffer)
+ && qflist_valid(wp, save_qfid)) {
+ // display the first error
+ qf_jump_first(qi, save_qfid, eap->forceit);
+ }
+
+ decr_quickfix_busy();
}
-/*
- * ":cexpr {expr}", ":cgetexpr {expr}", ":caddexpr {expr}" command.
- * ":lexpr {expr}", ":lgetexpr {expr}", ":laddexpr {expr}" command.
- */
+/// Return the autocmd name for the :cexpr Ex commands.
+static char_u * cexpr_get_auname(cmdidx_T cmdidx)
+{
+ switch (cmdidx) {
+ case CMD_cexpr: return (char_u *)"cexpr";
+ case CMD_cgetexpr: return (char_u *)"cgetexpr";
+ case CMD_caddexpr: return (char_u *)"caddexpr";
+ case CMD_lexpr: return (char_u *)"lexpr";
+ case CMD_lgetexpr: return (char_u *)"lgetexpr";
+ case CMD_laddexpr: return (char_u *)"laddexpr";
+ default: return NULL;
+ }
+}
+
+/// ":cexpr {expr}", ":cgetexpr {expr}", ":caddexpr {expr}" command.
+/// ":lexpr {expr}", ":lgetexpr {expr}", ":laddexpr {expr}" command.
void ex_cexpr(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
- const char *au_name = NULL;
+ char_u *au_name = NULL;
win_T *wp = NULL;
- switch (eap->cmdidx) {
- case CMD_cexpr:
- au_name = "cexpr";
- break;
- case CMD_cgetexpr:
- au_name = "cgetexpr";
- break;
- case CMD_caddexpr:
- au_name = "caddexpr";
- break;
- case CMD_lexpr:
- au_name = "lexpr";
- break;
- case CMD_lgetexpr:
- au_name = "lgetexpr";
- break;
- case CMD_laddexpr:
- au_name = "laddexpr";
- break;
- default:
- break;
- }
- if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, (char_u *)au_name,
+ au_name = cexpr_get_auname(eap->cmdidx);
+ if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name,
curbuf->b_fname, true, curbuf)) {
if (aborting()) {
return;
}
}
- if (eap->cmdidx == CMD_lexpr
- || eap->cmdidx == CMD_lgetexpr
- || eap->cmdidx == CMD_laddexpr) {
- qi = ll_get_or_alloc_list(curwin);
- wp = curwin;
- }
+ qf_info_T *qi = qf_cmd_get_or_alloc_stack(eap, &wp);
/* Evaluate the expression. When the result is a string or a list we can
* use it to fill the errorlist. */
@@ -5635,19 +6411,24 @@ void ex_cexpr(exarg_T *eap)
if (eval0(eap->arg, &tv, NULL, true) != FAIL) {
if ((tv.v_type == VAR_STRING && tv.vval.v_string != NULL)
|| tv.v_type == VAR_LIST) {
+ incr_quickfix_busy();
int res = qf_init_ext(qi, qi->qf_curlist, NULL, NULL, &tv, p_efm,
(eap->cmdidx != CMD_caddexpr
&& eap->cmdidx != CMD_laddexpr),
(linenr_T)0, (linenr_T)0,
qf_cmdtitle(*eap->cmdlinep), NULL);
+ if (qf_stack_empty(qi)) {
+ decr_quickfix_busy();
+ goto cleanup;
+ }
if (res >= 0) {
- qf_list_changed(qi, qi->qf_curlist);
+ qf_list_changed(qf_get_curlist(qi));
}
// Remember the current quickfix list identifier, so that we can
// check for autocommands changing the current quickfix list.
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
if (au_name != NULL) {
- apply_autocmds(EVENT_QUICKFIXCMDPOST, (char_u *)au_name,
+ apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name,
curbuf->b_fname, true, curbuf);
}
// Jump to the first error for a new list and if autocmds didn't
@@ -5658,9 +6439,11 @@ void ex_cexpr(exarg_T *eap)
// display the first error
qf_jump_first(qi, save_qfid, eap->forceit);
}
+ decr_quickfix_busy();
} else {
EMSG(_("E777: String or List expected"));
}
+cleanup:
tv_clear(&tv);
}
}
@@ -5687,7 +6470,7 @@ static qf_info_T *hgr_get_ll(bool *new_ll)
}
if (qi == NULL) {
// Allocate a new location list for help text matches
- qi = ll_new_list();
+ qi = qf_alloc_stack(QFLT_LOCATION);
*new_ll = true;
}
@@ -5718,8 +6501,7 @@ static void hgr_search_file(
line[--l] = NUL;
}
- if (qf_add_entry(qi,
- qi->qf_curlist,
+ if (qf_add_entry(qf_get_curlist(qi),
NULL, // dir
fname,
NULL,
@@ -5731,8 +6513,8 @@ static void hgr_search_file(
NULL, // search pattern
0, // nr
1, // type
- true // valid
- ) == FAIL) {
+ true) // valid
+ == QF_FAIL) {
got_int = true;
if (line != IObuff) {
xfree(line);
@@ -5823,10 +6605,12 @@ void ex_helpgrep(exarg_T *eap)
char_u *const save_cpo = p_cpo;
p_cpo = empty_option;
- if (eap->cmdidx == CMD_lhelpgrep) {
+ if (is_loclist_cmd(eap->cmdidx)) {
qi = hgr_get_ll(&new_qi);
}
+ incr_quickfix_busy();
+
// Check for a specified language
char_u *const lang = check_help_lang(eap->arg);
regmatch_T regmatch = {
@@ -5841,10 +6625,12 @@ void ex_helpgrep(exarg_T *eap)
vim_regfree(regmatch.regprog);
- qi->qf_lists[qi->qf_curlist].qf_nonevalid = FALSE;
- qi->qf_lists[qi->qf_curlist].qf_ptr =
- qi->qf_lists[qi->qf_curlist].qf_start;
- qi->qf_lists[qi->qf_curlist].qf_index = 1;
+ qf_list_T *qfl = qf_get_curlist(qi);
+ qfl->qf_nonevalid = false;
+ qfl->qf_ptr = qfl->qf_start;
+ qfl->qf_index = 1;
+ qf_list_changed(qfl);
+ qf_update_buffer(qi, NULL);
}
if (p_cpo == empty_option) {
@@ -5854,23 +6640,24 @@ void ex_helpgrep(exarg_T *eap)
free_string_option(save_cpo);
}
- qf_list_changed(qi, qi->qf_curlist);
- qf_update_buffer(qi, NULL);
-
if (au_name != NULL) {
apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name,
curbuf->b_fname, true, curbuf);
if (!new_qi && IS_LL_STACK(qi) && qf_find_buf(qi) == NULL) {
// autocommands made "qi" invalid
+ decr_quickfix_busy();
return;
}
}
- /* Jump to first match. */
- if (qi->qf_lists[qi->qf_curlist].qf_count > 0)
- qf_jump(qi, 0, 0, FALSE);
- else
+ // Jump to first match.
+ if (!qf_list_empty(qf_get_curlist(qi))) {
+ qf_jump(qi, 0, 0, false);
+ } else {
EMSG2(_(e_nomatch2), eap->arg);
+ }
+
+ decr_quickfix_busy();
if (eap->cmdidx == CMD_lhelpgrep) {
// If the help window is not opened or if it already points to the
@@ -5885,3 +6672,4 @@ void ex_helpgrep(exarg_T *eap)
}
}
+
diff --git a/src/nvim/screen.c b/src/nvim/screen.c
index 1ce0b5217e..7b9601a5a6 100644
--- a/src/nvim/screen.c
+++ b/src/nvim/screen.c
@@ -286,6 +286,11 @@ int update_screen(int type)
return FAIL;
}
+ // May have postponed updating diffs.
+ if (need_diff_redraw) {
+ diff_redraw(true);
+ }
+
if (must_redraw) {
if (type < must_redraw) /* use maximal type */
type = must_redraw;
diff --git a/src/nvim/search.c b/src/nvim/search.c
index fb31e76986..a298f7333e 100644
--- a/src/nvim/search.c
+++ b/src/nvim/search.c
@@ -516,19 +516,17 @@ void last_pat_prog(regmmatch_T *regmatch)
/// the index of the first matching
/// subpattern plus one; one if there was none.
int searchit(
- win_T *win, /* window to search in, can be NULL for a
- buffer without a window! */
+ win_T *win, // window to search in; can be NULL for a
+ // buffer without a window!
buf_T *buf,
pos_T *pos,
- pos_T *end_pos, // set to end of the match, unless NULL
+ pos_T *end_pos, // set to end of the match, unless NULL
Direction dir,
char_u *pat,
long count,
int options,
- int pat_use, // which pattern to use when "pat" is empty
- linenr_T stop_lnum, // stop after this line number when != 0
- proftime_T *tm, // timeout limit or NULL
- int *timed_out // set when timed out or NULL
+ int pat_use, // which pattern to use when "pat" is empty
+ searchit_arg_T *extra_arg // optional extra arguments, can be NULL
)
{
int found;
@@ -548,7 +546,16 @@ int searchit(
int submatch = 0;
bool first_match = true;
int save_called_emsg = called_emsg;
- int break_loop = FALSE;
+ int break_loop = false;
+ linenr_T stop_lnum = 0; // stop after this line number when != 0
+ proftime_T *tm = NULL; // timeout limit or NULL
+ int *timed_out = NULL; // set when timed out or NULL
+
+ if (extra_arg != NULL) {
+ stop_lnum = extra_arg->sa_stop_lnum;
+ tm = extra_arg->sa_tm;
+ timed_out = &extra_arg->sa_timed_out;
+ }
if (search_regcomp(pat, RE_SEARCH, pat_use,
(options & (SEARCH_HIS + SEARCH_KEEP)), &regmatch) == FAIL) {
@@ -889,6 +896,9 @@ int searchit(
give_warning((char_u *)_(dir == BACKWARD
? top_bot_msg : bot_top_msg), true);
}
+ if (extra_arg != NULL) {
+ extra_arg->sa_wrapped = true;
+ }
}
if (got_int || called_emsg
|| (timed_out != NULL && *timed_out)
@@ -983,8 +993,7 @@ int do_search(
char_u *pat,
long count,
int options,
- proftime_T *tm, // timeout limit or NULL
- int *timed_out // flag set on timeout or NULL
+ searchit_arg_T *sia // optional arguments or NULL
)
{
pos_T pos; /* position of the last match */
@@ -1271,7 +1280,7 @@ int do_search(
& (SEARCH_KEEP + SEARCH_PEEK + SEARCH_HIS + SEARCH_MSG
+ SEARCH_START
+ ((pat != NULL && *pat == ';') ? 0 : SEARCH_NOOF)))),
- RE_LAST, (linenr_T)0, tm, timed_out);
+ RE_LAST, sia);
if (dircp != NULL) {
*dircp = dirc; // restore second '/' or '?' for normal_cmd()
@@ -3799,8 +3808,9 @@ current_quote(
}
vis_bef_curs = lt(VIsual, curwin->w_cursor);
+ vis_empty = equalpos(VIsual, curwin->w_cursor);
if (*p_sel == 'e') {
- if (!vis_bef_curs) {
+ if (!vis_bef_curs && !vis_empty) {
// VIsual needs to be start of Visual selection.
pos_T t = curwin->w_cursor;
@@ -3810,8 +3820,8 @@ current_quote(
restore_vis_bef = true;
}
dec_cursor();
+ vis_empty = equalpos(VIsual, curwin->w_cursor);
}
- vis_empty = equalpos(VIsual, curwin->w_cursor);
}
if (!vis_empty) {
@@ -4028,9 +4038,6 @@ current_search(
bool old_p_ws = p_ws;
pos_T save_VIsual = VIsual;
- /* wrapping should not occur */
- p_ws = false;
-
/* Correct cursor when 'selection' is exclusive */
if (VIsual_active && *p_sel == 'e' && lt(VIsual, curwin->w_cursor))
dec_cursor();
@@ -4040,25 +4047,21 @@ current_search(
pos_T pos; // position after the pattern
int result; // result of various function calls
+ orig_pos = pos = curwin->w_cursor;
if (VIsual_active) {
- orig_pos = pos = curwin->w_cursor;
-
// Searching further will extend the match.
if (forward) {
incl(&pos);
} else {
decl(&pos);
}
- } else {
- orig_pos = pos = curwin->w_cursor;
}
// Is the pattern is zero-width?, this time, don't care about the direction
- int one_char = is_one_char(spats[last_idx].pat, true, &curwin->w_cursor,
- FORWARD);
- if (one_char == -1) {
- p_ws = old_p_ws;
- return FAIL; /* pattern not found */
+ int zero_width = is_zero_width(spats[last_idx].pat, true, &curwin->w_cursor,
+ FORWARD);
+ if (zero_width == -1) {
+ return FAIL; // pattern not found
}
/*
@@ -4070,15 +4073,22 @@ current_search(
int dir = forward ? i : !i;
int flags = 0;
- if (!dir && !one_char) {
+ if (!dir && !zero_width) {
flags = SEARCH_END;
}
end_pos = pos;
+ // wrapping should not occur in the first round
+ if (i == 0) {
+ p_ws = false;
+ }
+
result = searchit(curwin, curbuf, &pos, &end_pos,
(dir ? FORWARD : BACKWARD),
spats[last_idx].pat, i ? count : 1,
- SEARCH_KEEP | flags, RE_SEARCH, 0, NULL, NULL);
+ SEARCH_KEEP | flags, RE_SEARCH, NULL);
+
+ p_ws = old_p_ws;
// First search may fail, but then start searching from the
// beginning of the file (cursor might be on the search match)
@@ -4088,7 +4098,6 @@ current_search(
curwin->w_cursor = orig_pos;
if (VIsual_active)
VIsual = save_VIsual;
- p_ws = old_p_ws;
return FAIL;
} else if (i == 0 && !result) {
if (forward) { // try again from start of buffer
@@ -4100,26 +4109,24 @@ current_search(
ml_get(curwin->w_buffer->b_ml.ml_line_count));
}
}
- p_ws = old_p_ws;
}
pos_T start_pos = pos;
- p_ws = old_p_ws;
-
if (!VIsual_active) {
VIsual = start_pos;
}
// put cursor on last character of match
curwin->w_cursor = end_pos;
- if (lt(VIsual, end_pos)) {
+ if (lt(VIsual, end_pos) && forward) {
dec_cursor();
+ } else if (VIsual_active && lt(curwin->w_cursor, VIsual)) {
+ curwin->w_cursor = pos; // put the cursor on the start of the match
}
VIsual_active = true;
VIsual_mode = 'v';
- redraw_curbuf_later(INVERTED); // Update the inversion.
if (*p_sel == 'e') {
// Correction for exclusive selection depends on the direction.
if (forward && ltoreq(VIsual, curwin->w_cursor)) {
@@ -4141,13 +4148,13 @@ current_search(
return OK;
}
-/// Check if the pattern is one character long or zero-width.
+/// Check if the pattern is zero-width.
/// If move is true, check from the beginning of the buffer,
/// else from position "cur".
/// "direction" is FORWARD or BACKWARD.
/// Returns TRUE, FALSE or -1 for failure.
-static int is_one_char(char_u *pattern, bool move, pos_T *cur,
- Direction direction)
+static int
+is_zero_width(char_u *pattern, int move, pos_T *cur, Direction direction)
{
regmmatch_T regmatch;
int nmatched = 0;
@@ -4175,7 +4182,7 @@ static int is_one_char(char_u *pattern, bool move, pos_T *cur,
flag = SEARCH_START;
}
if (searchit(curwin, curbuf, &pos, NULL, direction, pattern, 1,
- SEARCH_KEEP + flag, RE_SEARCH, 0, NULL, NULL) != FAIL) {
+ SEARCH_KEEP + flag, RE_SEARCH, NULL) != FAIL) {
// Zero-width pattern should match somewhere, then we can check if
// start and end are in the same position.
called_emsg = false;
@@ -4195,13 +4202,6 @@ static int is_one_char(char_u *pattern, bool move, pos_T *cur,
result = (nmatched != 0
&& regmatch.startpos[0].lnum == regmatch.endpos[0].lnum
&& regmatch.startpos[0].col == regmatch.endpos[0].col);
- // one char width
- if (!result
- && nmatched != 0
- && inc(&pos) >= 0
- && pos.col == regmatch.endpos[0].col) {
- result = true;
- }
}
}
@@ -4265,7 +4265,7 @@ static void search_stat(int dirc, pos_T *pos,
start = profile_setlimit(20L);
while (!got_int && searchit(curwin, curbuf, &lastpos, NULL,
FORWARD, NULL, 1, SEARCH_KEEP, RE_LAST,
- (linenr_T)0, NULL, NULL) != FAIL) {
+ NULL) != FAIL) {
// Stop after passing the time limit.
if (profile_passed_limit(start)) {
cnt = OUT_OF_TIME;
diff --git a/src/nvim/search.h b/src/nvim/search.h
index cb094aab8c..0366aee8a1 100644
--- a/src/nvim/search.h
+++ b/src/nvim/search.h
@@ -70,6 +70,15 @@ typedef struct spat {
dict_T *additional_data; ///< Additional data from ShaDa file.
} SearchPattern;
+/// Optional extra arguments for searchit().
+typedef struct {
+ linenr_T sa_stop_lnum; ///< stop after this line number when != 0
+ proftime_T *sa_tm; ///< timeout limit or NULL
+ int sa_timed_out; ///< set when timed out
+ int sa_wrapped; ///< search wrapped around
+} searchit_arg_T;
+
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "search.h.generated.h"
#endif
diff --git a/src/nvim/spell.c b/src/nvim/spell.c
index a3c1746383..5feb7efda9 100644
--- a/src/nvim/spell.c
+++ b/src/nvim/spell.c
@@ -1910,11 +1910,11 @@ int init_syl_tab(slang_T *slang)
// Count the number of syllables in "word".
// When "word" contains spaces the syllables after the last space are counted.
// Returns zero if syllables are not defines.
-static int count_syllables(slang_T *slang, char_u *word)
+static int count_syllables(slang_T *slang, const char_u *word)
+ FUNC_ATTR_NONNULL_ALL
{
int cnt = 0;
bool skip = false;
- char_u *p;
int len;
syl_item_T *syl;
int c;
@@ -1922,7 +1922,7 @@ static int count_syllables(slang_T *slang, char_u *word)
if (slang->sl_syllable == NULL)
return 0;
- for (p = word; *p != NUL; p += len) {
+ for (const char_u *p = word; *p != NUL; p += len) {
// When running into a space reset counter.
if (*p == ' ') {
len = 1;
@@ -2625,9 +2625,10 @@ static bool spell_mb_isword_class(int cl, const win_T *wp)
// Returns true if "p" points to a word character.
// Wide version of spell_iswordp().
-static bool spell_iswordp_w(int *p, win_T *wp)
+static bool spell_iswordp_w(const int *p, const win_T *wp)
+ FUNC_ATTR_NONNULL_ALL
{
- int *s;
+ const int *s;
if (*p < 256 ? wp->w_s->b_spell_ismw[*p]
: (wp->w_s->b_spell_ismw_mb != NULL
@@ -3031,7 +3032,7 @@ void ex_spellrepall(exarg_T *eap)
sub_nlines = 0;
curwin->w_cursor.lnum = 0;
while (!got_int) {
- if (do_search(NULL, '/', frompat, 1L, SEARCH_KEEP, NULL, NULL) == 0
+ if (do_search(NULL, '/', frompat, 1L, SEARCH_KEEP, NULL) == 0
|| u_save_cursor() == FAIL) {
break;
}
diff --git a/src/nvim/spellfile.c b/src/nvim/spellfile.c
index eeec5be120..4fac001bc5 100644
--- a/src/nvim/spellfile.c
+++ b/src/nvim/spellfile.c
@@ -265,6 +265,8 @@
// follow; never used in prefix tree
#define BY_SPECIAL BY_FLAGS2 // highest special byte value
+#define ZERO_FLAG 65009 // used when flag is zero: "0"
+
// Flags used in .spl file for soundsalike flags.
#define SAL_F0LLOWUP 1
#define SAL_COLLAPSE 2
@@ -2783,6 +2785,7 @@ static unsigned affitem2flag(int flagtype, char_u *item, char_u *fname, int lnum
}
// Get one affix name from "*pp" and advance the pointer.
+// Returns ZERO_FLAG for "0".
// Returns zero for an error, still advances the pointer then.
static unsigned get_affitem(int flagtype, char_u **pp)
{
@@ -2794,6 +2797,9 @@ static unsigned get_affitem(int flagtype, char_u **pp)
return 0;
}
res = getdigits_int(pp, true, 0);
+ if (res == 0) {
+ res = ZERO_FLAG;
+ }
} else {
res = mb_ptr2char_adv((const char_u **)pp);
if (flagtype == AFT_LONG || (flagtype == AFT_CAPLONG
@@ -2915,10 +2921,15 @@ static bool flag_in_afflist(int flagtype, char_u *afflist, unsigned flag)
int digits = getdigits_int(&p, true, 0);
assert(digits >= 0);
n = (unsigned int)digits;
- if (n == flag)
+ if (n == 0) {
+ n = ZERO_FLAG;
+ }
+ if (n == flag) {
return true;
- if (*p != NUL) // skip over comma
- ++p;
+ }
+ if (*p != NUL) { // skip over comma
+ p++;
+ }
}
break;
}
diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c
index ac8ace9fff..bdbc09a87a 100644
--- a/src/nvim/syntax.c
+++ b/src/nvim/syntax.c
@@ -2460,11 +2460,8 @@ update_si_end(
int force /* when TRUE overrule a previous end */
)
{
- lpos_T startpos;
- lpos_T endpos;
lpos_T hl_endpos;
lpos_T end_endpos;
- int end_idx;
/* return quickly for a keyword */
if (sip->si_idx < 0)
@@ -2480,9 +2477,12 @@ update_si_end(
* We need to find the end of the region. It may continue in the next
* line.
*/
- end_idx = 0;
- startpos.lnum = current_lnum;
- startpos.col = startcol;
+ int end_idx = 0;
+ lpos_T startpos = {
+ .lnum = current_lnum,
+ .col = startcol,
+ };
+ lpos_T endpos = { 0 };
find_endpos(sip->si_idx, &startpos, &endpos, &hl_endpos,
&(sip->si_flags), &end_endpos, &end_idx, sip->si_extmatch);
diff --git a/src/nvim/tag.c b/src/nvim/tag.c
index 0d42deed2b..9e8c05fb1e 100644
--- a/src/nvim/tag.c
+++ b/src/nvim/tag.c
@@ -65,6 +65,7 @@ typedef struct tag_pointers {
char_u *tagkind_end; // end of tagkind
char_u *user_data; // user_data string
char_u *user_data_end; // end of user_data
+ linenr_T tagline; // "line:" value
} tagptrs_T;
/*
@@ -988,9 +989,7 @@ add_llist_tags(
cmd[len] = NUL;
}
- if ((dict = tv_dict_alloc()) == NULL) {
- continue;
- }
+ dict = tv_dict_alloc();
tv_list_append_dict(list, dict);
tv_dict_add_str(dict, S_LEN("text"), (const char *)tag_name);
@@ -2547,6 +2546,7 @@ parse_match(
tagp->tagkind = NULL;
tagp->user_data = NULL;
+ tagp->tagline = 0;
tagp->command_end = NULL;
if (retval == OK) {
@@ -2566,6 +2566,8 @@ parse_match(
tagp->tagkind = p + 5;
} else if (STRNCMP(p, "user_data:", 10) == 0) {
tagp->user_data = p + 10;
+ } else if (STRNCMP(p, "line:", 5) == 0) {
+ tagp->tagline = atoi((char *)p + 5);
}
if (tagp->tagkind != NULL && tagp->user_data != NULL) {
break;
@@ -2813,9 +2815,15 @@ static int jumpto_tag(
p_ic = FALSE; /* don't ignore case now */
p_scs = FALSE;
save_lnum = curwin->w_cursor.lnum;
- curwin->w_cursor.lnum = 0; /* start search before first line */
+ if (tagp.tagline > 0) {
+ // start search before line from "line:" field
+ curwin->w_cursor.lnum = tagp.tagline - 1;
+ } else {
+ // start search before first line
+ curwin->w_cursor.lnum = 0;
+ }
if (do_search(NULL, pbuf[0], pbuf + 1, (long)1,
- search_options, NULL, NULL)) {
+ search_options, NULL)) {
retval = OK;
} else {
int found = 1;
@@ -2826,20 +2834,18 @@ static int jumpto_tag(
*/
p_ic = TRUE;
if (!do_search(NULL, pbuf[0], pbuf + 1, (long)1,
- search_options, NULL, NULL)) {
+ search_options, NULL)) {
// Failed to find pattern, take a guess: "^func ("
found = 2;
(void)test_for_static(&tagp);
cc = *tagp.tagname_end;
*tagp.tagname_end = NUL;
snprintf((char *)pbuf, LSIZE, "^%s\\s\\*(", tagp.tagname);
- if (!do_search(NULL, '/', pbuf, (long)1,
- search_options, NULL, NULL)) {
+ if (!do_search(NULL, '/', pbuf, (long)1, search_options, NULL)) {
// Guess again: "^char * \<func ("
snprintf((char *)pbuf, LSIZE, "^\\[#a-zA-Z_]\\.\\*\\<%s\\s\\*(",
tagp.tagname);
- if (!do_search(NULL, '/', pbuf, (long)1,
- search_options, NULL, NULL)) {
+ if (!do_search(NULL, '/', pbuf, (long)1, search_options, NULL)) {
found = 0;
}
}
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
index 7609006906..c5e756905a 100644
--- a/src/nvim/terminal.c
+++ b/src/nvim/terminal.c
@@ -138,6 +138,8 @@ struct terminal {
int pressed_button; // which mouse button is pressed
bool pending_resize; // pending width/height
+ bool color_set[16];
+
size_t refcount; // reference count
};
@@ -241,6 +243,7 @@ Terminal *terminal_open(TerminalOptions opts)
(uint8_t)((color_val >> 8) & 0xFF),
(uint8_t)((color_val >> 0) & 0xFF));
vterm_state_set_palette_color(state, i, &color);
+ rv->color_set[i] = true;
}
}
}
@@ -598,16 +601,22 @@ void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr,
int vt_fg = fg_default ? -1 : get_rgb(state, cell.fg);
int vt_bg = bg_default ? -1 : get_rgb(state, cell.bg);
- int vt_fg_idx = ((!fg_default && VTERM_COLOR_IS_INDEXED(&cell.fg))
- ? cell.fg.indexed.idx + 1 : 0);
- int vt_bg_idx = ((!bg_default && VTERM_COLOR_IS_INDEXED(&cell.bg))
- ? cell.bg.indexed.idx + 1 : 0);
+ bool fg_indexed = VTERM_COLOR_IS_INDEXED(&cell.fg);
+ bool bg_indexed = VTERM_COLOR_IS_INDEXED(&cell.bg);
+
+ int vt_fg_idx = ((!fg_default && fg_indexed) ? cell.fg.indexed.idx + 1 : 0);
+ int vt_bg_idx = ((!bg_default && bg_indexed) ? cell.bg.indexed.idx + 1 : 0);
+
+ bool fg_set = vt_fg_idx && vt_fg_idx <= 16 && term->color_set[vt_fg_idx-1];
+ bool bg_set = vt_bg_idx && vt_bg_idx <= 16 && term->color_set[vt_bg_idx-1];
int hl_attrs = (cell.attrs.bold ? HL_BOLD : 0)
| (cell.attrs.italic ? HL_ITALIC : 0)
| (cell.attrs.reverse ? HL_INVERSE : 0)
| (cell.attrs.underline ? HL_UNDERLINE : 0)
- | (cell.attrs.strike ? HL_STRIKETHROUGH: 0);
+ | (cell.attrs.strike ? HL_STRIKETHROUGH: 0)
+ | ((fg_indexed && !fg_set) ? HL_FG_INDEXED : 0)
+ | ((bg_indexed && !bg_set) ? HL_BG_INDEXED : 0);
int attr_id = 0;
diff --git a/src/nvim/testdir/shared.vim b/src/nvim/testdir/shared.vim
index 84f636077d..a5d83d6a25 100644
--- a/src/nvim/testdir/shared.vim
+++ b/src/nvim/testdir/shared.vim
@@ -252,6 +252,8 @@ func GetVimProg()
endif
endfunc
+let g:valgrind_cnt = 1
+
" Get the command to run Vim, with -u NONE and --headless arguments.
" If there is an argument use it instead of "NONE".
func GetVimCommand(...)
@@ -267,6 +269,13 @@ func GetVimCommand(...)
endif
let cmd .= ' --headless -i NONE'
let cmd = substitute(cmd, 'VIMRUNTIME=.*VIMRUNTIME;', '', '')
+
+ " If using valgrind, make sure every run uses a different log file.
+ if cmd =~ 'valgrind.*--log-file='
+ let cmd = substitute(cmd, '--log-file=\(^\s*\)', '--log-file=\1.' . g:valgrind_cnt, '')
+ let g:valgrind_cnt += 1
+ endif
+
return cmd
endfunc
@@ -290,9 +299,6 @@ endfunc
func RunVimPiped(before, after, arguments, pipecmd)
let $NVIM_LOG_FILE = exists($NVIM_LOG_FILE) ? $NVIM_LOG_FILE : 'Xnvim.log'
let cmd = GetVimCommand()
- if cmd == ''
- return 0
- endif
let args = ''
if len(a:before) > 0
call writefile(a:before, 'Xbefore.vim')
diff --git a/src/nvim/testdir/test_alot.vim b/src/nvim/testdir/test_alot.vim
index f1274b01c8..5668f45dea 100644
--- a/src/nvim/testdir/test_alot.vim
+++ b/src/nvim/testdir/test_alot.vim
@@ -2,6 +2,7 @@
" This makes testing go faster, since Vim doesn't need to restart.
source test_assign.vim
+source test_backup.vim
source test_behave.vim
source test_cd.vim
source test_changedtick.vim
diff --git a/src/nvim/testdir/test_backup.vim b/src/nvim/testdir/test_backup.vim
new file mode 100644
index 0000000000..fa10430613
--- /dev/null
+++ b/src/nvim/testdir/test_backup.vim
@@ -0,0 +1,58 @@
+" Tests for the backup function
+
+func Test_backup()
+ set backup backupdir=.
+ new
+ call setline(1, ['line1', 'line2'])
+ :f Xbackup.txt
+ :w! Xbackup.txt
+ " backup file is only created after
+ " writing a second time (before overwriting)
+ :w! Xbackup.txt
+ let l = readfile('Xbackup.txt~')
+ call assert_equal(['line1', 'line2'], l)
+ bw!
+ set backup&vim backupdir&vim
+ call delete('Xbackup.txt')
+ call delete('Xbackup.txt~')
+endfunc
+
+func Test_backup2()
+ set backup backupdir=.//
+ new
+ call setline(1, ['line1', 'line2', 'line3'])
+ :f Xbackup.txt
+ :w! Xbackup.txt
+ " backup file is only created after
+ " writing a second time (before overwriting)
+ :w! Xbackup.txt
+ sp *Xbackup.txt~
+ call assert_equal(['line1', 'line2', 'line3'], getline(1,'$'))
+ let f=expand('%')
+ call assert_match('src%nvim%testdir%Xbackup.txt\~', f)
+ bw!
+ bw!
+ call delete('Xbackup.txt')
+ call delete(f)
+ set backup&vim backupdir&vim
+endfunc
+
+func Test_backup2_backupcopy()
+ set backup backupdir=.// backupcopy=yes
+ new
+ call setline(1, ['line1', 'line2', 'line3'])
+ :f Xbackup.txt
+ :w! Xbackup.txt
+ " backup file is only created after
+ " writing a second time (before overwriting)
+ :w! Xbackup.txt
+ sp *Xbackup.txt~
+ call assert_equal(['line1', 'line2', 'line3'], getline(1,'$'))
+ let f=expand('%')
+ call assert_match('src%nvim%testdir%Xbackup.txt\~', f)
+ bw!
+ bw!
+ call delete('Xbackup.txt')
+ call delete(f)
+ set backup&vim backupdir&vim backupcopy&vim
+endfunc
diff --git a/src/nvim/testdir/test_diffmode.vim b/src/nvim/testdir/test_diffmode.vim
index 57b19aa817..21e0271bda 100644
--- a/src/nvim/testdir/test_diffmode.vim
+++ b/src/nvim/testdir/test_diffmode.vim
@@ -773,3 +773,28 @@ func Test_diff_of_diff()
call StopVimInTerminal(buf)
call delete('Xtest_diff_diff')
endfunc
+
+func CloseoffSetup()
+ enew
+ call setline(1, ['one', 'two', 'three'])
+ diffthis
+ new
+ call setline(1, ['one', 'tow', 'three'])
+ diffthis
+ call assert_equal(1, &diff)
+ only!
+endfunc
+
+func Test_diff_closeoff()
+ " "closeoff" included by default: last diff win gets 'diff' reset'
+ call CloseoffSetup()
+ call assert_equal(0, &diff)
+ enew!
+
+ " "closeoff" excluded: last diff win keeps 'diff' set'
+ set diffopt-=closeoff
+ call CloseoffSetup()
+ call assert_equal(1, &diff)
+ diffoff!
+ enew!
+endfunc
diff --git a/src/nvim/testdir/test_functions.vim b/src/nvim/testdir/test_functions.vim
index a36c51f56f..7822507f86 100644
--- a/src/nvim/testdir/test_functions.vim
+++ b/src/nvim/testdir/test_functions.vim
@@ -1132,6 +1132,13 @@ func Test_reg_executing_and_recording()
" :normal command saves and restores reg_executing
let s:reg_stat = ''
+ let @q = ":call TestFunc()\<CR>:call s:save_reg_stat()\<CR>"
+ func TestFunc() abort
+ normal! ia
+ endfunc
+ call feedkeys("@q", 'xt')
+ call assert_equal(':q', s:reg_stat)
+ delfunc TestFunc
" getchar() command saves and restores reg_executing
map W :call TestFunc()<CR>
diff --git a/src/nvim/testdir/test_gf.vim b/src/nvim/testdir/test_gf.vim
index accd21e9a3..d301874891 100644
--- a/src/nvim/testdir/test_gf.vim
+++ b/src/nvim/testdir/test_gf.vim
@@ -99,3 +99,28 @@ func Test_gf()
call delete('Xtest1')
call delete('Xtestgf')
endfunc
+
+func Test_gf_visual()
+ call writefile([], "Xtest_gf_visual")
+ new
+ call setline(1, 'XXXtest_gf_visualXXX')
+ set hidden
+
+ " Visually select Xtest_gf_visual and use gf to go to that file
+ norm! ttvtXgf
+ call assert_equal('Xtest_gf_visual', bufname('%'))
+
+ bwipe!
+ call delete('Xtest_gf_visual')
+ set hidden&
+endfunc
+
+func Test_gf_error()
+ new
+ call assert_fails('normal gf', 'E446:')
+ call assert_fails('normal gF', 'E446:')
+ call setline(1, '/doesnotexist')
+ call assert_fails('normal gf', 'E447:')
+ call assert_fails('normal gF', 'E447:')
+ bwipe!
+endfunc
diff --git a/src/nvim/testdir/test_gn.vim b/src/nvim/testdir/test_gn.vim
index 5e74289b00..d41675be0c 100644
--- a/src/nvim/testdir/test_gn.vim
+++ b/src/nvim/testdir/test_gn.vim
@@ -129,6 +129,33 @@ func Test_gn_command()
call assert_equal([' nnoremap', '', 'match'], getline(1,'$'))
sil! %d_
+ " make sure it works correctly for one-char wide search items
+ call setline('.', ['abcdefghi'])
+ let @/ = 'a'
+ exe "norm! 0fhvhhgNgU"
+ call assert_equal(['ABCDEFGHi'], getline(1,'$'))
+ call setline('.', ['abcdefghi'])
+ let @/ = 'b'
+ " this gn wraps around the end of the file
+ exe "norm! 0fhvhhgngU"
+ call assert_equal(['aBCDEFGHi'], getline(1,'$'))
+ sil! %d _
+ call setline('.', ['abcdefghi'])
+ let @/ = 'f'
+ exe "norm! 0vllgngU"
+ call assert_equal(['ABCDEFghi'], getline(1,'$'))
+ sil! %d _
+ call setline('.', ['12345678'])
+ let @/ = '5'
+ norm! gg0f7vhhhhgnd
+ call assert_equal(['12348'], getline(1,'$'))
+ sil! %d _
+ call setline('.', ['12345678'])
+ let @/ = '5'
+ norm! gg0f2vf7gNd
+ call assert_equal(['1678'], getline(1,'$'))
+ sil! %d _
+
set wrapscan&vim
set belloff&vim
endfu
diff --git a/src/nvim/testdir/test_join.vim b/src/nvim/testdir/test_join.vim
index 1c97414164..ecb55c9af6 100644
--- a/src/nvim/testdir/test_join.vim
+++ b/src/nvim/testdir/test_join.vim
@@ -9,6 +9,27 @@ func Test_join_with_count()
call setline(1, ['one', 'two', 'three', 'four'])
normal 10J
call assert_equal('one two three four', getline(1))
+
+ call setline(1, ['one', '', 'two'])
+ normal J
+ call assert_equal('one', getline(1))
+
+ call setline(1, ['one', ' ', 'two'])
+ normal J
+ call assert_equal('one', getline(1))
+
+ call setline(1, ['one', '', '', 'two'])
+ normal JJ
+ call assert_equal('one', getline(1))
+
+ call setline(1, ['one', ' ', ' ', 'two'])
+ normal JJ
+ call assert_equal('one', getline(1))
+
+ call setline(1, ['one', '', '', 'two'])
+ normal 2J
+ call assert_equal('one', getline(1))
+
quit!
endfunc
diff --git a/src/nvim/testdir/test_let.vim b/src/nvim/testdir/test_let.vim
index 1fce3d6937..3c0fefbd25 100644
--- a/src/nvim/testdir/test_let.vim
+++ b/src/nvim/testdir/test_let.vim
@@ -141,6 +141,11 @@ func Test_let_varg_fail()
call s:set_varg8([0])
endfunction
+func Test_let_utf8_environment()
+ let $a = 'ĀĒĪŌŪあいうえお'
+ call assert_equal('ĀĒĪŌŪあいうえお', $a)
+endfunc
+
func Test_let_heredoc_fails()
call assert_fails('let v =<< marker', 'E991:')
@@ -284,4 +289,12 @@ E
END
endif
call assert_equal([], check)
+
+ " unpack assignment
+ let [a, b, c] =<< END
+ x
+ \y
+ z
+END
+ call assert_equal([' x', ' \y', ' z'], [a, b, c])
endfunc
diff --git a/src/nvim/testdir/test_normal.vim b/src/nvim/testdir/test_normal.vim
index 07d250cace..eab638d19a 100644
--- a/src/nvim/testdir/test_normal.vim
+++ b/src/nvim/testdir/test_normal.vim
@@ -1895,6 +1895,7 @@ fun! Test_normal33_g_cmd2()
set wrap listchars= sbr=
let lineA='abcdefghijklmnopqrstuvwxyz'
let lineB='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ let lineC='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
$put =lineA
$put =lineB
@@ -1928,9 +1929,30 @@ fun! Test_normal33_g_cmd2()
call assert_equal(15, col('.'))
call assert_equal('l', getreg(0))
+ norm! 2ggdd
+ $put =lineC
+
+ " Test for gM
+ norm! gMyl
+ call assert_equal(73, col('.'))
+ call assert_equal('0', getreg(0))
+ " Test for 20gM
+ norm! 20gMyl
+ call assert_equal(29, col('.'))
+ call assert_equal('S', getreg(0))
+ " Test for 60gM
+ norm! 60gMyl
+ call assert_equal(87, col('.'))
+ call assert_equal('E', getreg(0))
+
+ " Test for g Ctrl-G
+ set ff=unix
+ let a=execute(":norm! g\<c-g>")
+ call assert_match('Col 87 of 144; Line 2 of 2; Word 1 of 1; Byte 88 of 146', a)
+
" Test for gI
norm! gIfoo
- call assert_equal(['', 'fooabcdefghijk lmno0123456789AMNOPQRSTUVWXYZ'], getline(1,'$'))
+ call assert_equal(['', 'foo0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'], getline(1,'$'))
" Test for gi
wincmd c
diff --git a/src/nvim/testdir/test_quickfix.vim b/src/nvim/testdir/test_quickfix.vim
index fc514fc9e6..15cbf52cb5 100644
--- a/src/nvim/testdir/test_quickfix.vim
+++ b/src/nvim/testdir/test_quickfix.vim
@@ -37,6 +37,8 @@ func s:setup_commands(cchar)
command! -nargs=* Xgrepadd <mods> grepadd <args>
command! -nargs=* Xhelpgrep helpgrep <args>
command! -nargs=0 -count Xcc <count>cc
+ command! -count=1 -nargs=0 Xbelow <mods><count>cbelow
+ command! -count=1 -nargs=0 Xabove <mods><count>cabove
let g:Xgetlist = function('getqflist')
let g:Xsetlist = function('setqflist')
call setqflist([], 'f')
@@ -70,6 +72,8 @@ func s:setup_commands(cchar)
command! -nargs=* Xgrepadd <mods> lgrepadd <args>
command! -nargs=* Xhelpgrep lhelpgrep <args>
command! -nargs=0 -count Xcc <count>ll
+ command! -count=1 -nargs=0 Xbelow <mods><count>lbelow
+ command! -count=1 -nargs=0 Xabove <mods><count>labove
let g:Xgetlist = function('getloclist', [0])
let g:Xsetlist = function('setloclist', [0])
call setloclist(0, [], 'f')
@@ -163,6 +167,12 @@ endfunc
func XageTests(cchar)
call s:setup_commands(a:cchar)
+ if a:cchar == 'l'
+ " No location list for the current window
+ call assert_fails('lolder', 'E776:')
+ call assert_fails('lnewer', 'E776:')
+ endif
+
let list = [{'bufnr': bufnr('%'), 'lnum': 1}]
call g:Xsetlist(list)
@@ -273,6 +283,27 @@ func Test_cwindow()
call XwindowTests('l')
endfunc
+func Test_copenHeight()
+ copen
+ wincmd H
+ let height = winheight(0)
+ copen 10
+ call assert_equal(height, winheight(0))
+ quit
+endfunc
+
+func Test_copenHeight_tabline()
+ set tabline=foo showtabline=2
+ copen
+ wincmd H
+ let height = winheight(0)
+ copen 10
+ call assert_equal(height, winheight(0))
+ quit
+ set tabline& showtabline&
+endfunc
+
+
" Tests for the :cfile, :lfile, :caddfile, :laddfile, :cgetfile and :lgetfile
" commands.
func XfileTests(cchar)
@@ -540,6 +571,8 @@ func s:test_xhelpgrep(cchar)
" Search for non existing help string
call assert_fails('Xhelpgrep a1b2c3', 'E480:')
+ " Invalid regular expression
+ call assert_fails('Xhelpgrep \@<!', 'E480:')
endfunc
func Test_helpgrep()
@@ -1045,8 +1078,8 @@ func Test_efm2()
set efm=%f:%s
cexpr 'Xtestfile:Line search text'
let l = getqflist()
- call assert_equal(l[0].pattern, '^\VLine search text\$')
- call assert_equal(l[0].lnum, 0)
+ call assert_equal('^\VLine search text\$', l[0].pattern)
+ call assert_equal(0, l[0].lnum)
let l = split(execute('clist', ''), "\n")
call assert_equal([' 1 Xtestfile:^\VLine search text\$: '], l)
@@ -1287,6 +1320,28 @@ func SetXlistTests(cchar, bnum)
let l = g:Xgetlist()
call g:Xsetlist(l)
call assert_equal(0, g:Xgetlist()[0].valid)
+ " Adding a non-valid entry should not mark the list as having valid entries
+ call g:Xsetlist([{'bufnr':a:bnum, 'lnum':5, 'valid':0}], 'a')
+ Xwindow
+ call assert_equal(1, winnr('$'))
+
+ " :cnext/:cprev should still work even with invalid entries in the list
+ let l = [{'bufnr' : a:bnum, 'lnum' : 1, 'text' : '1', 'valid' : 0},
+ \ {'bufnr' : a:bnum, 'lnum' : 2, 'text' : '2', 'valid' : 0}]
+ call g:Xsetlist(l)
+ Xnext
+ call assert_equal(2, g:Xgetlist({'idx' : 0}).idx)
+ Xprev
+ call assert_equal(1, g:Xgetlist({'idx' : 0}).idx)
+ " :cnext/:cprev should still work after appending invalid entries to an
+ " empty list
+ call g:Xsetlist([])
+ call g:Xsetlist(l, 'a')
+ Xnext
+ call assert_equal(2, g:Xgetlist({'idx' : 0}).idx)
+ Xprev
+ call assert_equal(1, g:Xgetlist({'idx' : 0}).idx)
+
call g:Xsetlist([{'text':'Text1', 'valid':1}])
Xwindow
call assert_equal(2, winnr('$'))
@@ -1963,6 +2018,18 @@ func Xproperty_tests(cchar)
call g:Xsetlist([], 'r', {'items' : [{'filename' : 'F1', 'lnum' : 10, 'text' : 'L10'}]})
call assert_equal('TestTitle', g:Xgetlist({'title' : 1}).title)
+ " Test for getting id of window associated with a location list window
+ if a:cchar == 'l'
+ only
+ call assert_equal(0, g:Xgetlist({'all' : 1}).filewinid)
+ let wid = win_getid()
+ Xopen
+ call assert_equal(wid, g:Xgetlist({'filewinid' : 1}).filewinid)
+ wincmd w
+ call assert_equal(0, g:Xgetlist({'filewinid' : 1}).filewinid)
+ only
+ endif
+
" The following used to crash Vim with address sanitizer
call g:Xsetlist([], 'f')
call g:Xsetlist([], 'a', {'items' : [{'filename':'F1', 'lnum':10}]})
@@ -2503,7 +2570,7 @@ func Test_file_from_copen()
cclose
augroup! QF_Test
-endfunction
+endfunc
func Test_resize_from_copen()
augroup QF_Test
@@ -2522,6 +2589,94 @@ func Test_resize_from_copen()
endtry
endfunc
+" Test for aborting quickfix commands using QuickFixCmdPre
+func Xtest_qfcmd_abort(cchar)
+ call s:setup_commands(a:cchar)
+
+ call g:Xsetlist([], 'f')
+
+ " cexpr/lexpr
+ let e = ''
+ try
+ Xexpr ["F1:10:Line10", "F2:20:Line20"]
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+
+ " cfile/lfile
+ call writefile(["F1:10:Line10", "F2:20:Line20"], 'Xfile1')
+ let e = ''
+ try
+ Xfile Xfile1
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+ call delete('Xfile1')
+
+ " cgetbuffer/lgetbuffer
+ enew!
+ call append(0, ["F1:10:Line10", "F2:20:Line20"])
+ let e = ''
+ try
+ Xgetbuffer
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+ enew!
+
+ " vimgrep/lvimgrep
+ let e = ''
+ try
+ Xvimgrep /func/ test_quickfix.vim
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+
+ " helpgrep/lhelpgrep
+ let e = ''
+ try
+ Xhelpgrep quickfix
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+
+ " grep/lgrep
+ if has('unix')
+ let e = ''
+ try
+ silent Xgrep func test_quickfix.vim
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+ endif
+endfunc
+
+func Test_qfcmd_abort()
+ augroup QF_Test
+ au!
+ autocmd QuickFixCmdPre * throw "AbortCmd"
+ augroup END
+
+ call Xtest_qfcmd_abort('c')
+ call Xtest_qfcmd_abort('l')
+
+ augroup QF_Test
+ au!
+ augroup END
+endfunc
+
" Tests for the quickfix buffer b:changedtick variable
func Xchangedtick_tests(cchar)
call s:setup_commands(a:cchar)
@@ -3075,7 +3230,17 @@ func Xgetlist_empty_tests(cchar)
call assert_equal('', g:Xgetlist({'title' : 0}).title)
call assert_equal(0, g:Xgetlist({'winid' : 0}).winid)
call assert_equal(0, g:Xgetlist({'changedtick' : 0}).changedtick)
- call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [], 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0, 'changedtick': 0}, g:Xgetlist({'all' : 0}))
+ if a:cchar == 'c'
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0,
+ \ 'items' : [], 'nr' : 0, 'size' : 0,
+ \ 'title' : '', 'winid' : 0, 'changedtick': 0},
+ \ g:Xgetlist({'all' : 0}))
+ else
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0,
+ \ 'items' : [], 'nr' : 0, 'size' : 0, 'title' : '',
+ \ 'winid' : 0, 'changedtick': 0, 'filewinid' : 0},
+ \ g:Xgetlist({'all' : 0}))
+ endif
" Quickfix window with empty stack
silent! Xopen
@@ -3108,7 +3273,16 @@ func Xgetlist_empty_tests(cchar)
call assert_equal('', g:Xgetlist({'id' : qfid, 'title' : 0}).title)
call assert_equal(0, g:Xgetlist({'id' : qfid, 'winid' : 0}).winid)
call assert_equal(0, g:Xgetlist({'id' : qfid, 'changedtick' : 0}).changedtick)
- call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [], 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0, 'changedtick' : 0}, g:Xgetlist({'id' : qfid, 'all' : 0}))
+ if a:cchar == 'c'
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [],
+ \ 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0,
+ \ 'changedtick' : 0}, g:Xgetlist({'id' : qfid, 'all' : 0}))
+ else
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [],
+ \ 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0,
+ \ 'changedtick' : 0, 'filewinid' : 0},
+ \ g:Xgetlist({'id' : qfid, 'all' : 0}))
+ endif
" Non-existing quickfix list number
call assert_equal('', g:Xgetlist({'nr' : 5, 'context' : 0}).context)
@@ -3120,7 +3294,16 @@ func Xgetlist_empty_tests(cchar)
call assert_equal('', g:Xgetlist({'nr' : 5, 'title' : 0}).title)
call assert_equal(0, g:Xgetlist({'nr' : 5, 'winid' : 0}).winid)
call assert_equal(0, g:Xgetlist({'nr' : 5, 'changedtick' : 0}).changedtick)
- call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [], 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0, 'changedtick' : 0}, g:Xgetlist({'nr' : 5, 'all' : 0}))
+ if a:cchar == 'c'
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [],
+ \ 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0,
+ \ 'changedtick' : 0}, g:Xgetlist({'nr' : 5, 'all' : 0}))
+ else
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [],
+ \ 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0,
+ \ 'changedtick' : 0, 'filewinid' : 0},
+ \ g:Xgetlist({'nr' : 5, 'all' : 0}))
+ endif
endfunc
func Test_getqflist()
@@ -3209,7 +3392,28 @@ func Test_lexpr_crash()
augroup QF_Test
au!
augroup END
+
enew | only
+ augroup QF_Test
+ au!
+ au BufNew * call setloclist(0, [], 'f')
+ augroup END
+ lexpr 'x:1:x'
+ augroup QF_Test
+ au!
+ augroup END
+
+ enew | only
+ lexpr ''
+ lopen
+ augroup QF_Test
+ au!
+ au FileType * call setloclist(0, [], 'f')
+ augroup END
+ lexpr ''
+ augroup QF_Test
+ au!
+ augroup END
endfunc
" The following test used to crash Vim
@@ -3603,3 +3807,167 @@ func Test_curswant()
call assert_equal(getcurpos()[4], virtcol('.'))
cclose | helpclose
endfunc
+
+" Test for parsing entries using visual screen column
+func Test_viscol()
+ enew
+ call writefile(["Col1\tCol2\tCol3"], 'Xfile1')
+ edit Xfile1
+
+ " Use byte offset for column number
+ set efm&
+ cexpr "Xfile1:1:5:XX\nXfile1:1:9:YY\nXfile1:1:20:ZZ"
+ call assert_equal([5, 8], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([9, 12], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([14, 20], [col('.'), virtcol('.')])
+
+ " Use screen column offset for column number
+ set efm=%f:%l:%v:%m
+ cexpr "Xfile1:1:8:XX\nXfile1:1:12:YY\nXfile1:1:20:ZZ"
+ call assert_equal([5, 8], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([9, 12], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([14, 20], [col('.'), virtcol('.')])
+ cexpr "Xfile1:1:6:XX\nXfile1:1:15:YY\nXfile1:1:24:ZZ"
+ call assert_equal([5, 8], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([10, 16], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([14, 20], [col('.'), virtcol('.')])
+
+ enew
+ call writefile(["Col1\täü\töß\tCol4"], 'Xfile1')
+
+ " Use byte offset for column number
+ set efm&
+ cexpr "Xfile1:1:8:XX\nXfile1:1:11:YY\nXfile1:1:16:ZZ"
+ call assert_equal([8, 10], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([11, 17], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([16, 25], [col('.'), virtcol('.')])
+
+ " Use screen column offset for column number
+ set efm=%f:%l:%v:%m
+ cexpr "Xfile1:1:10:XX\nXfile1:1:17:YY\nXfile1:1:25:ZZ"
+ call assert_equal([8, 10], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([11, 17], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([16, 25], [col('.'), virtcol('.')])
+
+ enew | only
+ set efm&
+ call delete('Xfile1')
+endfunc
+
+" Test for the :cbelow, :cabove, :lbelow and :labove commands.
+func Xtest_below(cchar)
+ call s:setup_commands(a:cchar)
+
+ " No quickfix/location list
+ call assert_fails('Xbelow', 'E42:')
+ call assert_fails('Xabove', 'E42:')
+
+ " Empty quickfix/location list
+ call g:Xsetlist([])
+ call assert_fails('Xbelow', 'E42:')
+ call assert_fails('Xabove', 'E42:')
+
+ call s:create_test_file('X1')
+ call s:create_test_file('X2')
+ call s:create_test_file('X3')
+ call s:create_test_file('X4')
+
+ " Invalid entries
+ edit X1
+ call g:Xsetlist(["E1", "E2"])
+ call assert_fails('Xbelow', 'E42:')
+ call assert_fails('Xabove', 'E42:')
+ call assert_fails('3Xbelow', 'E42:')
+ call assert_fails('4Xabove', 'E42:')
+
+ " Test the commands with various arguments
+ Xexpr ["X1:5:L5", "X2:5:L5", "X2:10:L10", "X2:15:L15", "X3:3:L3"]
+ edit +7 X2
+ Xabove
+ call assert_equal(['X2', 5], [bufname(''), line('.')])
+ call assert_fails('Xabove', 'E553:')
+ normal 2j
+ Xbelow
+ call assert_equal(['X2', 10], [bufname(''), line('.')])
+ " Last error in this file
+ Xbelow 99
+ call assert_equal(['X2', 15], [bufname(''), line('.')])
+ call assert_fails('Xbelow', 'E553:')
+ " First error in this file
+ Xabove 99
+ call assert_equal(['X2', 5], [bufname(''), line('.')])
+ call assert_fails('Xabove', 'E553:')
+ normal gg
+ Xbelow 2
+ call assert_equal(['X2', 10], [bufname(''), line('.')])
+ normal G
+ Xabove 2
+ call assert_equal(['X2', 10], [bufname(''), line('.')])
+ edit X4
+ call assert_fails('Xabove', 'E42:')
+ call assert_fails('Xbelow', 'E42:')
+ if a:cchar == 'l'
+ " If a buffer has location list entries from some other window but not
+ " from the current window, then the commands should fail.
+ edit X1 | split | call setloclist(0, [], 'f')
+ call assert_fails('Xabove', 'E776:')
+ call assert_fails('Xbelow', 'E776:')
+ close
+ endif
+
+ " Test for lines with multiple quickfix entries
+ Xexpr ["X1:5:L5", "X2:5:1:L5_1", "X2:5:2:L5_2", "X2:5:3:L5_3",
+ \ "X2:10:1:L10_1", "X2:10:2:L10_2", "X2:10:3:L10_3",
+ \ "X2:15:1:L15_1", "X2:15:2:L15_2", "X2:15:3:L15_3", "X3:3:L3"]
+ edit +1 X2
+ Xbelow 2
+ call assert_equal(['X2', 10, 1], [bufname(''), line('.'), col('.')])
+ normal gg
+ Xbelow 99
+ call assert_equal(['X2', 15, 1], [bufname(''), line('.'), col('.')])
+ normal G
+ Xabove 2
+ call assert_equal(['X2', 10, 1], [bufname(''), line('.'), col('.')])
+ normal G
+ Xabove 99
+ call assert_equal(['X2', 5, 1], [bufname(''), line('.'), col('.')])
+ normal 10G
+ Xabove
+ call assert_equal(['X2', 5, 1], [bufname(''), line('.'), col('.')])
+ normal 10G
+ Xbelow
+ call assert_equal(['X2', 15, 1], [bufname(''), line('.'), col('.')])
+
+ " Invalid range
+ if a:cchar == 'c'
+ call assert_fails('-2cbelow', 'E553:')
+ " TODO: should go to first error in the current line?
+ 0cabove
+ else
+ call assert_fails('-2lbelow', 'E553:')
+ " TODO: should go to first error in the current line?
+ 0labove
+ endif
+
+ call delete('X1')
+ call delete('X2')
+ call delete('X3')
+ call delete('X4')
+endfunc
+
+func Test_cbelow()
+ call Xtest_below('c')
+ call Xtest_below('l')
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/test_spell.vim b/src/nvim/testdir/test_spell.vim
index e49b5542fa..e2016d7927 100644
--- a/src/nvim/testdir/test_spell.vim
+++ b/src/nvim/testdir/test_spell.vim
@@ -283,9 +283,9 @@ func Test_zz_affix()
\ ])
call LoadAffAndDic(g:test_data_aff7, g:test_data_dic7)
- call RunGoodBad("meea1 meea\xE9 bar prebar barmeat prebarmeat leadprebar lead tail leadtail leadmiddletail",
+ call RunGoodBad("meea1 meezero meea\xE9 bar prebar barmeat prebarmeat leadprebar lead tail leadtail leadmiddletail",
\ "bad: mee meea2 prabar probarmaat middle leadmiddle middletail taillead leadprobar",
- \ ["bar", "barmeat", "lead", "meea1", "meea\xE9", "prebar", "prebarmeat", "tail"],
+ \ ["bar", "barmeat", "lead", "meea1", "meea\xE9", "meezero", "prebar", "prebarmeat", "tail"],
\ [
\ ["bad", ["bar", "lead", "tail"]],
\ ["mee", ["meea1", "meea\xE9", "bar"]],
@@ -320,6 +320,19 @@ func Test_zz_Numbers()
\ ])
endfunc
+" Affix flags
+func Test_zz_affix_flags()
+ call LoadAffAndDic(g:test_data_aff10, g:test_data_dic10)
+ call RunGoodBad("drink drinkable drinkables drinktable drinkabletable",
+ \ "bad: drinks drinkstable drinkablestable",
+ \ ["drink", "drinkable", "drinkables", "table"],
+ \ [['bad', []],
+ \ ['drinks', ['drink']],
+ \ ['drinkstable', ['drinktable', 'drinkable', 'drink table']],
+ \ ['drinkablestable', ['drinkabletable', 'drinkables table', 'drinkable table']],
+ \ ])
+endfunc
+
function FirstSpellWord()
call feedkeys("/^start:\n", 'tx')
normal ]smm
@@ -713,6 +726,9 @@ let g:test_data_aff7 = [
\"SFX 61003 Y 1",
\"SFX 61003 0 meat .",
\"",
+ \"SFX 0 Y 1",
+ \"SFX 0 0 zero .",
+ \"",
\"SFX 391 Y 1",
\"SFX 391 0 a1 .",
\"",
@@ -724,7 +740,7 @@ let g:test_data_aff7 = [
\ ]
let g:test_data_dic7 = [
\"1234",
- \"mee/391,111,9999",
+ \"mee/0,391,111,9999",
\"bar/17,61003,123",
\"lead/2",
\"tail/123",
@@ -748,6 +764,21 @@ let g:test_data_dic9 = [
\"foo",
\"bar",
\ ]
+let g:test_data_aff10 = [
+ \"COMPOUNDRULE se",
+ \"COMPOUNDPERMITFLAG p",
+ \"",
+ \"SFX A Y 1",
+ \"SFX A 0 able/Mp .",
+ \"",
+ \"SFX M Y 1",
+ \"SFX M 0 s .",
+ \ ]
+let g:test_data_dic10 = [
+ \"1234",
+ \"drink/As",
+ \"table/e",
+ \ ]
let g:test_data_aff_sal = [
\"SET ISO8859-1",
\"TRY esianrtolcdugmphbyfvkwjkqxz-\xEB\xE9\xE8\xEA\xEF\xEE\xE4\xE0\xE2\xF6\xFC\xFB'ESIANRTOLCDUGMPHBYFVKWJKQXZ",
diff --git a/src/nvim/testdir/test_substitute.vim b/src/nvim/testdir/test_substitute.vim
index b29b678129..e209310a05 100644
--- a/src/nvim/testdir/test_substitute.vim
+++ b/src/nvim/testdir/test_substitute.vim
@@ -149,6 +149,7 @@ func Run_SubCmd_Tests(tests)
for t in a:tests
let start = line('.') + 1
let end = start + len(t[2]) - 1
+ " TODO: why is there a one second delay the first time we get here?
exe "normal o" . t[0]
call cursor(start, 1)
exe t[1]
@@ -717,3 +718,12 @@ one two
close!
endfunc
+
+func Test_sub_beyond_end()
+ new
+ call setline(1, '#')
+ let @/ = '^#\n\zs'
+ s///e
+ call assert_equal('#', getline(1))
+ bwipe!
+endfunc
diff --git a/src/nvim/testdir/test_tagjump.vim b/src/nvim/testdir/test_tagjump.vim
index ce527a5e1d..f93af76f17 100644
--- a/src/nvim/testdir/test_tagjump.vim
+++ b/src/nvim/testdir/test_tagjump.vim
@@ -466,4 +466,28 @@ func Test_tag_line_toolong()
let &verbose = old_vbs
endfunc
+func Test_tagline()
+ call writefile([
+ \ 'provision Xtest.py /^ def provision(self, **kwargs):$/;" m line:1 language:Python class:Foo',
+ \ 'provision Xtest.py /^ def provision(self, **kwargs):$/;" m line:3 language:Python class:Bar',
+ \], 'Xtags')
+ call writefile([
+ \ ' def provision(self, **kwargs):',
+ \ ' pass',
+ \ ' def provision(self, **kwargs):',
+ \ ' pass',
+ \], 'Xtest.py')
+
+ set tags=Xtags
+
+ 1tag provision
+ call assert_equal(line('.'), 1)
+ 2tag provision
+ call assert_equal(line('.'), 3)
+
+ call delete('Xtags')
+ call delete('Xtest.py')
+ set tags&
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/test_textobjects.vim b/src/nvim/testdir/test_textobjects.vim
index 9194e0014d..448b2dc51c 100644
--- a/src/nvim/testdir/test_textobjects.vim
+++ b/src/nvim/testdir/test_textobjects.vim
@@ -48,6 +48,9 @@ func Test_quote_selection_selection_exclusive()
set selection=exclusive
exe "norm! fdvhi'y"
call assert_equal('bcde', @")
+ let @"='dummy'
+ exe "norm! $gevi'y"
+ call assert_equal('bcde', @")
set selection&vim
bw!
endfunc
diff --git a/src/nvim/testdir/test_vimscript.vim b/src/nvim/testdir/test_vimscript.vim
index 3fcba4134e..d2f13ff072 100644
--- a/src/nvim/testdir/test_vimscript.vim
+++ b/src/nvim/testdir/test_vimscript.vim
@@ -1284,7 +1284,7 @@ func s:DoNothing()
endfunc
func Test_script_local_func()
- set nocp viminfo+=nviminfo
+ set nocp nomore viminfo+=nviminfo
new
nnoremap <buffer> _x :call <SID>DoNothing()<bar>call <SID>DoLast()<bar>delfunc <SID>DoNothing<bar>delfunc <SID>DoLast<cr>
diff --git a/src/nvim/testdir/test_virtualedit.vim b/src/nvim/testdir/test_virtualedit.vim
index 67adede8d7..1e6b26a057 100644
--- a/src/nvim/testdir/test_virtualedit.vim
+++ b/src/nvim/testdir/test_virtualedit.vim
@@ -73,3 +73,12 @@ func Test_edit_CTRL_G()
bwipe!
set virtualedit=
endfunc
+
+func Test_edit_change()
+ new
+ set virtualedit=all
+ call setline(1, "\t⒌")
+ normal Cx
+ call assert_equal('x', getline(1))
+ bwipe!
+endfunc
diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c
index 844bc0db40..c71378463f 100644
--- a/src/nvim/tui/input.c
+++ b/src/nvim/tui/input.c
@@ -26,7 +26,7 @@ void tinput_init(TermInput *input, Loop *loop)
{
input->loop = loop;
input->paste = 0;
- input->in_fd = 0;
+ input->in_fd = STDIN_FILENO;
input->waiting_for_bg_response = 0;
input->key_buffer = rbuffer_new(KEY_BUFFER_SIZE);
uv_mutex_init(&input->key_buffer_mutex);
@@ -36,7 +36,7 @@ void tinput_init(TermInput *input, Loop *loop)
// echo q | nvim -es
// ls *.md | xargs nvim
#ifdef WIN32
- if (!os_isatty(0)) {
+ if (!os_isatty(input->in_fd)) {
const HANDLE conin_handle = CreateFile("CONIN$",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
@@ -46,8 +46,8 @@ void tinput_init(TermInput *input, Loop *loop)
assert(input->in_fd != -1);
}
#else
- if (!os_isatty(0) && os_isatty(2)) {
- input->in_fd = 2;
+ if (!os_isatty(input->in_fd) && os_isatty(STDERR_FILENO)) {
+ input->in_fd = STDERR_FILENO;
}
#endif
input_global_fd_init(input->in_fd);
diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c
index 945b093f32..60e1353000 100644
--- a/src/nvim/tui/tui.c
+++ b/src/nvim/tui/tui.c
@@ -220,7 +220,7 @@ static void terminfo_start(UI *ui)
data->unibi_ext.reset_cursor_style = -1;
data->unibi_ext.get_bg = -1;
data->unibi_ext.set_underline_color = -1;
- data->out_fd = 1;
+ data->out_fd = STDOUT_FILENO;
data->out_isatty = os_isatty(data->out_fd);
const char *term = os_getenv("TERM");
@@ -515,20 +515,8 @@ static void update_attrs(UI *ui, int attr_id)
}
data->print_attr_id = attr_id;
HlAttrs attrs = kv_A(data->attrs, (size_t)attr_id);
-
- int fg = ui->rgb ? attrs.rgb_fg_color : (attrs.cterm_fg_color - 1);
- if (fg == -1) {
- fg = ui->rgb ? data->clear_attrs.rgb_fg_color
- : (data->clear_attrs.cterm_fg_color - 1);
- }
-
- int bg = ui->rgb ? attrs.rgb_bg_color : (attrs.cterm_bg_color - 1);
- if (bg == -1) {
- bg = ui->rgb ? data->clear_attrs.rgb_bg_color
- : (data->clear_attrs.cterm_bg_color - 1);
- }
-
int attr = ui->rgb ? attrs.rgb_ae_attr : attrs.cterm_ae_attr;
+
bool bold = attr & HL_BOLD;
bool italic = attr & HL_ITALIC;
bool reverse = attr & HL_INVERSE;
@@ -596,14 +584,29 @@ static void update_attrs(UI *ui, int attr_id)
unibi_out_ext(ui, data->unibi_ext.set_underline_color);
}
}
- if (ui->rgb) {
+
+ int fg, bg;
+ if (ui->rgb && !(attr & HL_FG_INDEXED)) {
+ fg = ((attrs.rgb_fg_color != -1)
+ ? attrs.rgb_fg_color : data->clear_attrs.rgb_fg_color);
if (fg != -1) {
UNIBI_SET_NUM_VAR(data->params[0], (fg >> 16) & 0xff); // red
UNIBI_SET_NUM_VAR(data->params[1], (fg >> 8) & 0xff); // green
UNIBI_SET_NUM_VAR(data->params[2], fg & 0xff); // blue
unibi_out_ext(ui, data->unibi_ext.set_rgb_foreground);
}
+ } else {
+ fg = (attrs.cterm_fg_color
+ ? attrs.cterm_fg_color - 1 : (data->clear_attrs.cterm_fg_color - 1));
+ if (fg != -1) {
+ UNIBI_SET_NUM_VAR(data->params[0], fg);
+ unibi_out(ui, unibi_set_a_foreground);
+ }
+ }
+ if (ui->rgb && !(attr & HL_BG_INDEXED)) {
+ bg = ((attrs.rgb_bg_color != -1)
+ ? attrs.rgb_bg_color : data->clear_attrs.rgb_bg_color);
if (bg != -1) {
UNIBI_SET_NUM_VAR(data->params[0], (bg >> 16) & 0xff); // red
UNIBI_SET_NUM_VAR(data->params[1], (bg >> 8) & 0xff); // green
@@ -611,17 +614,15 @@ static void update_attrs(UI *ui, int attr_id)
unibi_out_ext(ui, data->unibi_ext.set_rgb_background);
}
} else {
- if (fg != -1) {
- UNIBI_SET_NUM_VAR(data->params[0], fg);
- unibi_out(ui, unibi_set_a_foreground);
- }
-
+ bg = (attrs.cterm_bg_color
+ ? attrs.cterm_bg_color - 1 : (data->clear_attrs.cterm_bg_color - 1));
if (bg != -1) {
UNIBI_SET_NUM_VAR(data->params[0], bg);
unibi_out(ui, unibi_set_a_background);
}
}
+
data->default_attr = fg == -1 && bg == -1
&& !bold && !italic && !underline && !undercurl && !reverse && !standout
&& !strikethrough;
diff --git a/src/nvim/undo.c b/src/nvim/undo.c
index 035613c7fd..539d42765d 100644
--- a/src/nvim/undo.c
+++ b/src/nvim/undo.c
@@ -91,7 +91,9 @@
#include "nvim/fileio.h"
#include "nvim/fold.h"
#include "nvim/buffer_updates.h"
+#include "nvim/pos.h" // MAXLNUM
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/memline.h"
#include "nvim/message.h"
#include "nvim/misc1.h"
@@ -106,6 +108,7 @@
#include "nvim/types.h"
#include "nvim/os/os.h"
#include "nvim/os/time.h"
+#include "nvim/lib/kvec.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "undo.c.generated.h"
@@ -222,9 +225,6 @@ int u_save_cursor(void)
*/
int u_save(linenr_T top, linenr_T bot)
{
- if (undo_off)
- return OK;
-
if (top >= bot || bot > (curbuf->b_ml.ml_line_count + 1)) {
return FAIL; /* rely on caller to do error messages */
}
@@ -243,10 +243,7 @@ int u_save(linenr_T top, linenr_T bot)
*/
int u_savesub(linenr_T lnum)
{
- if (undo_off)
- return OK;
-
- return u_savecommon(lnum - 1, lnum + 1, lnum + 1, FALSE);
+ return u_savecommon(lnum - 1, lnum + 1, lnum + 1, false);
}
/*
@@ -257,10 +254,7 @@ int u_savesub(linenr_T lnum)
*/
int u_inssub(linenr_T lnum)
{
- if (undo_off)
- return OK;
-
- return u_savecommon(lnum - 1, lnum, lnum + 1, FALSE);
+ return u_savecommon(lnum - 1, lnum, lnum + 1, false);
}
/*
@@ -272,9 +266,6 @@ int u_inssub(linenr_T lnum)
*/
int u_savedel(linenr_T lnum, long nlines)
{
- if (undo_off)
- return OK;
-
return u_savecommon(lnum - 1, lnum + nlines,
nlines == curbuf->b_ml.ml_line_count ? 2 : lnum, FALSE);
}
@@ -384,6 +375,7 @@ int u_savecommon(linenr_T top, linenr_T bot, linenr_T newbot, int reload)
* up the undo info when out of memory.
*/
uhp = xmalloc(sizeof(u_header_T));
+ kv_init(uhp->uh_extmark);
#ifdef U_DEBUG
uhp->uh_magic = UH_MAGIC;
#endif
@@ -2249,10 +2241,10 @@ static void u_undoredo(int undo, bool do_buf_event)
xfree((char_u *)uep->ue_array);
}
- /* adjust marks */
+ // Adjust marks
if (oldsize != newsize) {
mark_adjust(top + 1, top + oldsize, (long)MAXLNUM,
- (long)newsize - (long)oldsize, false);
+ (long)newsize - (long)oldsize, false, kExtmarkNOOP);
if (curbuf->b_op_start.lnum > top + oldsize) {
curbuf->b_op_start.lnum += newsize - oldsize;
}
@@ -2285,6 +2277,23 @@ static void u_undoredo(int undo, bool do_buf_event)
newlist = uep;
}
+ // Adjust Extmarks
+ ExtmarkUndoObject undo_info;
+ if (undo) {
+ for (i = (int)kv_size(curhead->uh_extmark) - 1; i > -1; i--) {
+ undo_info = kv_A(curhead->uh_extmark, i);
+ extmark_apply_undo(undo_info, undo);
+ }
+ // redo
+ } else {
+ for (i = 0; i < (int)kv_size(curhead->uh_extmark); i++) {
+ undo_info = kv_A(curhead->uh_extmark, i);
+ extmark_apply_undo(undo_info, undo);
+ }
+ }
+ // finish Adjusting extmarks
+
+
curhead->uh_entry = newlist;
curhead->uh_flags = new_flags;
if ((old_flags & UH_EMPTYBUF) && BUFEMPTY()) {
@@ -2828,6 +2837,8 @@ u_freeentries(
u_freeentry(uep, uep->ue_size);
}
+ kv_destroy(uhp->uh_extmark);
+
#ifdef U_DEBUG
uhp->uh_magic = 0;
#endif
@@ -2902,9 +2913,6 @@ void u_undoline(void)
colnr_T t;
char_u *oldp;
- if (undo_off)
- return;
-
if (curbuf->b_u_line_ptr == NULL
|| curbuf->b_u_line_lnum > curbuf->b_ml.ml_line_count) {
beep_flush();
@@ -3022,3 +3030,34 @@ list_T *u_eval_tree(const u_header_T *const first_uhp)
return list;
}
+
+// Given the buffer, Return the undo header. If none is set, set one first.
+// NULL will be returned if e.g undolevels = -1 (undo disabled)
+u_header_T *u_force_get_undo_header(buf_T *buf)
+{
+ u_header_T *uhp = NULL;
+ if (buf->b_u_curhead != NULL) {
+ uhp = buf->b_u_curhead;
+ } else if (buf->b_u_newhead) {
+ uhp = buf->b_u_newhead;
+ }
+ // Create the first undo header for the buffer
+ if (!uhp) {
+ // Undo is normally invoked in change code, which already has swapped
+ // curbuf.
+ buf_T *save_curbuf = curbuf;
+ curbuf = buf;
+ // Args are tricky: this means replace empty range by empty range..
+ u_savecommon(0, 1, 1, true);
+ curbuf = save_curbuf;
+
+ uhp = buf->b_u_curhead;
+ if (!uhp) {
+ uhp = buf->b_u_newhead;
+ if (get_undolevel() > 0 && !uhp) {
+ abort();
+ }
+ }
+ }
+ return uhp;
+}
diff --git a/src/nvim/undo_defs.h b/src/nvim/undo_defs.h
index 6c7e2bba41..0fa3b415ec 100644
--- a/src/nvim/undo_defs.h
+++ b/src/nvim/undo_defs.h
@@ -4,6 +4,7 @@
#include <time.h> // for time_t
#include "nvim/pos.h"
+#include "nvim/mark_extended_defs.h"
#include "nvim/mark_defs.h"
typedef struct u_header u_header_T;
@@ -56,14 +57,15 @@ struct u_header {
u_entry_T *uh_getbot_entry; /* pointer to where ue_bot must be set */
pos_T uh_cursor; /* cursor position before saving */
long uh_cursor_vcol;
- int uh_flags; /* see below */
- fmark_T uh_namedm[NMARKS]; /* marks before undo/after redo */
- visualinfo_T uh_visual; /* Visual areas before undo/after redo */
- time_t uh_time; /* timestamp when the change was made */
- long uh_save_nr; /* set when the file was saved after the
- changes in this block */
+ int uh_flags; // see below
+ fmark_T uh_namedm[NMARKS]; // marks before undo/after redo
+ extmark_undo_vec_t uh_extmark; // info to move extmarks
+ visualinfo_T uh_visual; // Visual areas before undo/after redo
+ time_t uh_time; // timestamp when the change was made
+ long uh_save_nr; // set when the file was saved after the
+ // changes in this block
#ifdef U_DEBUG
- int uh_magic; /* magic number to check allocation */
+ int uh_magic; // magic number to check allocation
#endif
};
diff --git a/src/nvim/version.c b/src/nvim/version.c
index b6122f6463..f678b743c2 100644
--- a/src/nvim/version.c
+++ b/src/nvim/version.c
@@ -83,7 +83,7 @@ static const int included_patches[] = {
1838,
1837,
1836,
- // 1835,
+ 1835,
1834,
1833,
1832,
@@ -120,7 +120,7 @@ static const int included_patches[] = {
1801,
1800,
1799,
- // 1798,
+ 1798,
1797,
1796,
1795,
@@ -176,7 +176,7 @@ static const int included_patches[] = {
// 1745,
// 1744,
// 1743,
- // 1742,
+ 1742,
1741,
1740,
1739,
@@ -193,10 +193,10 @@ static const int included_patches[] = {
1728,
1727,
1726,
- // 1725,
+ 1725,
1724,
1723,
- // 1722,
+ 1722,
1721,
1720,
1719,
@@ -207,7 +207,7 @@ static const int included_patches[] = {
1714,
1713,
// 1712,
- // 1711,
+ 1711,
1710,
1709,
1708,
@@ -233,7 +233,7 @@ static const int included_patches[] = {
1688,
1687,
1686,
- // 1685,
+ 1685,
1684,
1683,
1682,
@@ -252,8 +252,8 @@ static const int included_patches[] = {
1669,
// 1668,
1667,
- // 1666,
- // 1665,
+ 1666,
+ 1665,
1664,
1663,
1662,
@@ -294,12 +294,12 @@ static const int included_patches[] = {
1627,
1626,
1625,
- // 1624,
+ 1624,
1623,
1622,
1621,
1620,
- // 1619,
+ 1619,
1618,
// 1617,
// 1616,
@@ -336,7 +336,7 @@ static const int included_patches[] = {
1585,
1584,
1583,
- // 1582,
+ 1582,
1581,
1580,
1579,
@@ -426,7 +426,7 @@ static const int included_patches[] = {
// 1495,
1494,
1493,
- // 1492,
+ 1492,
// 1491,
1490,
1489,
@@ -467,7 +467,7 @@ static const int included_patches[] = {
// 1454,
1453,
1452,
- // 1451,
+ 1451,
1450,
// 1449,
1448,
@@ -742,7 +742,7 @@ static const int included_patches[] = {
1179,
1178,
1177,
- // 1176,
+ 1176,
1175,
1174,
1173,
@@ -984,7 +984,7 @@ static const int included_patches[] = {
937,
936,
935,
- // 934,
+ 934,
933,
932,
931,
diff --git a/src/nvim/window.c b/src/nvim/window.c
index ce5be8e904..2a7578e33c 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -1530,8 +1530,9 @@ static void win_init(win_T *newp, win_T *oldp, int flags)
/* Don't copy the location list. */
newp->w_llist = NULL;
newp->w_llist_ref = NULL;
- } else
- copy_loclist(oldp, newp);
+ } else {
+ copy_loclist_stack(oldp, newp);
+ }
newp->w_localdir = (oldp->w_localdir == NULL)
? NULL : vim_strsave(oldp->w_localdir);
@@ -1574,7 +1575,7 @@ static void win_init_some(win_T *newp, win_T *oldp)
/// Check if "win" is a pointer to an existing window in the current tabpage.
///
/// @param win window to check
-bool win_valid(win_T *win) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+bool win_valid(const win_T *win) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
if (win == NULL) {
return false;
@@ -2417,6 +2418,7 @@ int win_close(win_T *win, bool free_buf)
bool help_window = false;
tabpage_T *prev_curtab = curtab;
frame_T *win_frame = win->w_floating ? NULL : win->w_frame->fr_parent;
+ const bool had_diffmode = win->w_p_diff;
if (last_window() && !win->w_floating) {
EMSG(_("E444: Cannot close last window"));
@@ -2641,6 +2643,22 @@ int win_close(win_T *win, bool free_buf)
if (help_window)
restore_snapshot(SNAP_HELP_IDX, close_curwin);
+ // If the window had 'diff' set and now there is only one window left in
+ // the tab page with 'diff' set, and "closeoff" is in 'diffopt', then
+ // execute ":diffoff!".
+ if (diffopt_closeoff() && had_diffmode && curtab == prev_curtab) {
+ int diffcount = 0;
+
+ FOR_ALL_WINDOWS_IN_TAB(dwin, curtab) {
+ if (dwin->w_p_diff) {
+ diffcount++;
+ }
+ }
+ if (diffcount == 1) {
+ do_cmdline_cmd("diffoff!");
+ }
+ }
+
curwin->w_pos_changed = true;
redraw_all_later(NOT_VALID);
return OK;
diff --git a/test/functional/api/mark_extended_spec.lua b/test/functional/api/mark_extended_spec.lua
new file mode 100644
index 0000000000..76db9f9d81
--- /dev/null
+++ b/test/functional/api/mark_extended_spec.lua
@@ -0,0 +1,1375 @@
+local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
+
+local request = helpers.request
+local eq = helpers.eq
+local ok = helpers.ok
+local curbufmeths = helpers.curbufmeths
+local pcall_err = helpers.pcall_err
+local insert = helpers.insert
+local feed = helpers.feed
+local clear = helpers.clear
+local command = helpers.command
+
+local function check_undo_redo(ns, mark, sr, sc, er, ec) --s = start, e = end
+ local rv = curbufmeths.get_extmark_by_id(ns, mark)
+ eq({er, ec}, rv)
+ feed("u")
+ rv = curbufmeths.get_extmark_by_id(ns, mark)
+ eq({sr, sc}, rv)
+ feed("<c-r>")
+ rv = curbufmeths.get_extmark_by_id(ns, mark)
+ eq({er, ec}, rv)
+end
+
+local function set_extmark(ns_id, id, line, col, opts)
+ if opts == nil then
+ opts = {}
+ end
+ return curbufmeths.set_extmark(ns_id, id, line, col, opts)
+end
+
+local function get_extmarks(ns_id, start, end_, opts)
+ if opts == nil then
+ opts = {}
+ end
+ return curbufmeths.get_extmarks(ns_id, start, end_, opts)
+end
+
+describe('Extmarks buffer api', function()
+ local screen
+ local marks, positions, ns_string2, ns_string, init_text, row, col
+ local ns, ns2
+
+ before_each(function()
+ -- Initialize some namespaces and insert 12345 into a buffer
+ marks = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
+ positions = {{0, 0,}, {0, 2}, {0, 3}}
+
+ ns_string = "my-fancy-plugin"
+ ns_string2 = "my-fancy-plugin2"
+ init_text = "12345"
+ row = 0
+ col = 2
+
+ clear()
+ screen = Screen.new(15, 10)
+ screen:attach()
+
+ insert(init_text)
+ ns = request('nvim_create_namespace', ns_string)
+ ns2 = request('nvim_create_namespace', ns_string2)
+ end)
+
+ it('adds, updates and deletes marks #extmarks', function()
+ local rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ eq(marks[1], rv)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({positions[1][1], positions[1][2]}, rv)
+ -- Test adding a second mark on same row works
+ rv = set_extmark(ns, marks[2], positions[2][1], positions[2][2])
+ eq(marks[2], rv)
+
+ -- Test an update, (same pos)
+ rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ eq(marks[1], rv)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[2])
+ eq({positions[2][1], positions[2][2]}, rv)
+ -- Test an update, (new pos)
+ row = positions[1][1]
+ col = positions[1][2] + 1
+ rv = set_extmark(ns, marks[1], row, col)
+ eq(marks[1], rv)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({row, col}, rv)
+
+ -- remove the test marks
+ eq(true, curbufmeths.del_extmark(ns, marks[1]))
+ eq(false, curbufmeths.del_extmark(ns, marks[1]))
+ eq(true, curbufmeths.del_extmark(ns, marks[2]))
+ eq(false, curbufmeths.del_extmark(ns, marks[3]))
+ eq(false, curbufmeths.del_extmark(ns, 1000))
+ end)
+
+ it('can clear a specific namespace range #extmarks', function()
+ set_extmark(ns, 1, 0, 1)
+ set_extmark(ns2, 1, 0, 1)
+ -- force a new undo buffer
+ feed('o<esc>')
+ curbufmeths.clear_namespace(ns2, 0, -1)
+ eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ feed('u')
+ eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({{1, 0, 1}}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ feed('<c-r>')
+ eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ end)
+
+ it('can clear a namespace range using 0,-1 #extmarks', function()
+ set_extmark(ns, 1, 0, 1)
+ set_extmark(ns2, 1, 0, 1)
+ -- force a new undo buffer
+ feed('o<esc>')
+ curbufmeths.clear_namespace(-1, 0, -1)
+ eq({}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ feed('u')
+ eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({{1, 0, 1}}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ feed('<c-r>')
+ eq({}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ end)
+
+ it('querying for information and ranges #extmarks', function()
+ -- add some more marks
+ for i, m in ipairs(marks) do
+ if positions[i] ~= nil then
+ local rv = set_extmark(ns, m, positions[i][1], positions[i][2])
+ eq(m, rv)
+ end
+ end
+
+ -- {0, 0} and {-1, -1} work as extreme values
+ eq({{1, 0, 0}}, get_extmarks(ns, {0, 0}, {0, 0}))
+ eq({}, get_extmarks(ns, {-1, -1}, {-1, -1}))
+ local rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ for i, m in ipairs(marks) do
+ if positions[i] ~= nil then
+ eq({m, positions[i][1], positions[i][2]}, rv[i])
+ end
+ end
+
+ -- 0 and -1 works as short hand extreme values
+ eq({{1, 0, 0}}, get_extmarks(ns, 0, 0))
+ eq({}, get_extmarks(ns, -1, -1))
+ rv = get_extmarks(ns, 0, -1)
+ for i, m in ipairs(marks) do
+ if positions[i] ~= nil then
+ eq({m, positions[i][1], positions[i][2]}, rv[i])
+ end
+ end
+
+ -- next with mark id
+ rv = get_extmarks(ns, marks[1], {-1, -1}, {amount=1})
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+ rv = get_extmarks(ns, marks[2], {-1, -1}, {amount=1})
+ eq({{marks[2], positions[2][1], positions[2][2]}}, rv)
+ -- next with positional when mark exists at position
+ rv = get_extmarks(ns, positions[1], {-1, -1}, {amount=1})
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+ -- next with positional index (no mark at position)
+ rv = get_extmarks(ns, {positions[1][1], positions[1][2] +1}, {-1, -1}, {amount=1})
+ eq({{marks[2], positions[2][1], positions[2][2]}}, rv)
+ -- next with Extremity index
+ rv = get_extmarks(ns, {0,0}, {-1, -1}, {amount=1})
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+
+ -- nextrange with mark id
+ rv = get_extmarks(ns, marks[1], marks[3])
+ eq({marks[1], positions[1][1], positions[1][2]}, rv[1])
+ eq({marks[2], positions[2][1], positions[2][2]}, rv[2])
+ -- nextrange with amount
+ rv = get_extmarks(ns, marks[1], marks[3], {amount=2})
+ eq(2, table.getn(rv))
+ -- nextrange with positional when mark exists at position
+ rv = get_extmarks(ns, positions[1], positions[3])
+ eq({marks[1], positions[1][1], positions[1][2]}, rv[1])
+ eq({marks[2], positions[2][1], positions[2][2]}, rv[2])
+ rv = get_extmarks(ns, positions[2], positions[3])
+ eq(2, table.getn(rv))
+ -- nextrange with positional index (no mark at position)
+ local lower = {positions[1][1], positions[2][2] -1}
+ local upper = {positions[2][1], positions[3][2] - 1}
+ rv = get_extmarks(ns, lower, upper)
+ eq({{marks[2], positions[2][1], positions[2][2]}}, rv)
+ lower = {positions[3][1], positions[3][2] + 1}
+ upper = {positions[3][1], positions[3][2] + 2}
+ rv = get_extmarks(ns, lower, upper)
+ eq({}, rv)
+ -- nextrange with extremity index
+ lower = {positions[2][1], positions[2][2]+1}
+ upper = {-1, -1}
+ rv = get_extmarks(ns, lower, upper)
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+
+ -- prev with mark id
+ rv = get_extmarks(ns, marks[3], {0, 0}, {amount=1})
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+ rv = get_extmarks(ns, marks[2], {0, 0}, {amount=1})
+ eq({{marks[2], positions[2][1], positions[2][2]}}, rv)
+ -- prev with positional when mark exists at position
+ rv = get_extmarks(ns, positions[3], {0, 0}, {amount=1})
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+ -- prev with positional index (no mark at position)
+ rv = get_extmarks(ns, {positions[1][1], positions[1][2] +1}, {0, 0}, {amount=1})
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+ -- prev with Extremity index
+ rv = get_extmarks(ns, {-1,-1}, {0,0}, {amount=1})
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+
+ -- prevrange with mark id
+ rv = get_extmarks(ns, marks[3], marks[1])
+ eq({marks[3], positions[3][1], positions[3][2]}, rv[1])
+ eq({marks[2], positions[2][1], positions[2][2]}, rv[2])
+ eq({marks[1], positions[1][1], positions[1][2]}, rv[3])
+ -- prevrange with amount
+ rv = get_extmarks(ns, marks[3], marks[1], {amount=2})
+ eq(2, table.getn(rv))
+ -- prevrange with positional when mark exists at position
+ rv = get_extmarks(ns, positions[3], positions[1])
+ eq({{marks[3], positions[3][1], positions[3][2]},
+ {marks[2], positions[2][1], positions[2][2]},
+ {marks[1], positions[1][1], positions[1][2]}}, rv)
+ rv = get_extmarks(ns, positions[2], positions[1])
+ eq(2, table.getn(rv))
+ -- prevrange with positional index (no mark at position)
+ lower = {positions[2][1], positions[2][2] + 1}
+ upper = {positions[3][1], positions[3][2] + 1}
+ rv = get_extmarks(ns, upper, lower)
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+ lower = {positions[3][1], positions[3][2] + 1}
+ upper = {positions[3][1], positions[3][2] + 2}
+ rv = get_extmarks(ns, upper, lower)
+ eq({}, rv)
+ -- prevrange with extremity index
+ lower = {0,0}
+ upper = {positions[2][1], positions[2][2] - 1}
+ rv = get_extmarks(ns, upper, lower)
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+ end)
+
+ it('querying for information with amount #extmarks', function()
+ -- add some more marks
+ for i, m in ipairs(marks) do
+ if positions[i] ~= nil then
+ local rv = set_extmark(ns, m, positions[i][1], positions[i][2])
+ eq(m, rv)
+ end
+ end
+
+ local rv = get_extmarks(ns, {0, 0}, {-1, -1}, {amount=1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {amount=2})
+ eq(2, table.getn(rv))
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {amount=3})
+ eq(3, table.getn(rv))
+
+ -- now in reverse
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {amount=1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {amount=2})
+ eq(2, table.getn(rv))
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {amount=3})
+ eq(3, table.getn(rv))
+ end)
+
+ it('get_marks works when mark col > upper col #extmarks', function()
+ feed('A<cr>12345<esc>')
+ feed('A<cr>12345<esc>')
+ set_extmark(ns, 10, 0, 2) -- this shouldn't be found
+ set_extmark(ns, 11, 2, 1) -- this shouldn't be found
+ set_extmark(ns, marks[1], 0, 4) -- check col > our upper bound
+ set_extmark(ns, marks[2], 1, 1) -- check col < lower bound
+ set_extmark(ns, marks[3], 2, 0) -- check is inclusive
+ eq({{marks[1], 0, 4},
+ {marks[2], 1, 1},
+ {marks[3], 2, 0}},
+ get_extmarks(ns, {0, 3}, {2, 0}))
+ end)
+
+ it('get_marks works in reverse when mark col < lower col #extmarks', function()
+ feed('A<cr>12345<esc>')
+ feed('A<cr>12345<esc>')
+ set_extmark(ns, 10, 0, 1) -- this shouldn't be found
+ set_extmark(ns, 11, 2, 4) -- this shouldn't be found
+ set_extmark(ns, marks[1], 2, 1) -- check col < our lower bound
+ set_extmark(ns, marks[2], 1, 4) -- check col > upper bound
+ set_extmark(ns, marks[3], 0, 2) -- check is inclusive
+ local rv = get_extmarks(ns, {2, 3}, {0, 2})
+ eq({{marks[1], 2, 1},
+ {marks[2], 1, 4},
+ {marks[3], 0, 2}},
+ rv)
+ end)
+
+ it('get_marks amount 0 returns nothing #extmarks', function()
+ set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ local rv = get_extmarks(ns, {-1, -1}, {-1, -1}, {amount=0})
+ eq({}, rv)
+ end)
+
+
+ it('marks move with line insertations #extmarks', function()
+ set_extmark(ns, marks[1], 0, 0)
+ feed("yyP")
+ check_undo_redo(ns, marks[1], 0, 0, 1, 0)
+ end)
+
+ it('marks move with multiline insertations #extmarks', function()
+ feed("a<cr>22<cr>33<esc>")
+ set_extmark(ns, marks[1], 1, 1)
+ feed('ggVGyP')
+ check_undo_redo(ns, marks[1], 1, 1, 4, 1)
+ end)
+
+ it('marks move with line join #extmarks', function()
+ -- do_join in ops.c
+ feed("a<cr>222<esc>")
+ set_extmark(ns, marks[1], 1, 0)
+ feed('ggJ')
+ check_undo_redo(ns, marks[1], 1, 0, 0, 6)
+ end)
+
+ it('join works when no marks are present #extmarks', function()
+ feed("a<cr>1<esc>")
+ feed('kJ')
+ -- This shouldn't seg fault
+ screen:expect([[
+ 12345^ 1 |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ |
+ ]])
+ end)
+
+ it('marks move with multiline join #extmarks', function()
+ -- do_join in ops.c
+ feed("a<cr>222<cr>333<cr>444<esc>")
+ set_extmark(ns, marks[1], 3, 0)
+ feed('2GVGJ')
+ check_undo_redo(ns, marks[1], 3, 0, 1, 8)
+ end)
+
+ it('marks move with line deletes #extmarks', function()
+ feed("a<cr>222<cr>333<cr>444<esc>")
+ set_extmark(ns, marks[1], 2, 1)
+ feed('ggjdd')
+ check_undo_redo(ns, marks[1], 2, 1, 1, 1)
+ end)
+
+ it('marks move with multiline deletes #extmarks', function()
+ feed("a<cr>222<cr>333<cr>444<esc>")
+ set_extmark(ns, marks[1], 3, 0)
+ feed('gg2dd')
+ check_undo_redo(ns, marks[1], 3, 0, 1, 0)
+ -- regression test, undoing multiline delete when mark is on row 1
+ feed('ugg3dd')
+ check_undo_redo(ns, marks[1], 3, 0, 0, 0)
+ end)
+
+ it('marks move with open line #extmarks', function()
+ -- open_line in misc1.c
+ -- testing marks below are also moved
+ feed("yyP")
+ set_extmark(ns, marks[1], 0, 4)
+ set_extmark(ns, marks[2], 1, 4)
+ feed('1G<s-o><esc>')
+ check_undo_redo(ns, marks[1], 0, 4, 1, 4)
+ check_undo_redo(ns, marks[2], 1, 4, 2, 4)
+ feed('2Go<esc>')
+ check_undo_redo(ns, marks[1], 1, 4, 1, 4)
+ check_undo_redo(ns, marks[2], 2, 4, 3, 4)
+ end)
+
+ it('marks move with char inserts #extmarks', function()
+ -- insertchar in edit.c (the ins_str branch)
+ set_extmark(ns, marks[1], 0, 3)
+ feed('0')
+ insert('abc')
+ screen:expect([[
+ ab^c12345 |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ |
+ ]])
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, 6}, rv)
+ -- check_undo_redo(ns, marks[1], 0, 2, 0, 5)
+ end)
+
+ -- gravity right as definted in tk library
+ it('marks have gravity right #extmarks', function()
+ -- insertchar in edit.c (the ins_str branch)
+ set_extmark(ns, marks[1], 0, 2)
+ feed('03l')
+ insert("X")
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+
+ -- check multibyte chars
+ feed('03l<esc>')
+ insert("~~")
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ end)
+
+ it('we can insert multibyte chars #extmarks', function()
+ -- insertchar in edit.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 2)
+ -- Insert a fullwidth (two col) tilde, NICE
+ feed('0i~<esc>')
+ check_undo_redo(ns, marks[1], 1, 2, 1, 3)
+ end)
+
+ it('marks move with blockwise inserts #extmarks', function()
+ -- op_insert in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 2)
+ feed('0<c-v>lkI9<esc>')
+ check_undo_redo(ns, marks[1], 1, 2, 1, 3)
+ end)
+
+ it('marks move with line splits (using enter) #extmarks', function()
+ -- open_line in misc1.c
+ -- testing marks below are also moved
+ feed("yyP")
+ set_extmark(ns, marks[1], 0, 4)
+ set_extmark(ns, marks[2], 1, 4)
+ feed('1Gla<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 4, 1, 2)
+ check_undo_redo(ns, marks[2], 1, 4, 2, 4)
+ end)
+
+ it('marks at last line move on insert new line #extmarks', function()
+ -- open_line in misc1.c
+ set_extmark(ns, marks[1], 0, 4)
+ feed('0i<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 4, 1, 4)
+ end)
+
+ it('yet again marks move with line splits #extmarks', function()
+ -- the first test above wasn't catching all errors..
+ feed("A67890<esc>")
+ set_extmark(ns, marks[1], 0, 4)
+ feed("04li<cr><esc>")
+ check_undo_redo(ns, marks[1], 0, 4, 1, 0)
+ end)
+
+ it('and one last time line splits... #extmarks', function()
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 2)
+ feed("02li<cr><esc>")
+ check_undo_redo(ns, marks[1], 0, 1, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 2, 1, 0)
+ end)
+
+ it('multiple marks move with mark splits #extmarks', function()
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 3)
+ feed("0li<cr><esc>")
+ check_undo_redo(ns, marks[1], 0, 1, 1, 0)
+ check_undo_redo(ns, marks[2], 0, 3, 1, 2)
+ end)
+
+ it('deleting on a mark works #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 2)
+ feed('02lx')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ end)
+
+ it('marks move with char deletes #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 2)
+ feed('02dl')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 0)
+ -- from the other side (nothing should happen)
+ feed('$x')
+ check_undo_redo(ns, marks[1], 0, 0, 0, 0)
+ end)
+
+ it('marks move with char deletes over a range #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ feed('0l3dl<esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 1)
+ -- delete 1, nothing should happend to our marks
+ feed('u')
+ feed('$x')
+ check_undo_redo(ns, marks[2], 0, 3, 0, 3)
+ end)
+
+ it('deleting marks at end of line works #extmarks', function()
+ -- mark_extended.c/extmark_col_adjust_delete
+ set_extmark(ns, marks[1], 0, 4)
+ feed('$x')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 4)
+ -- check the copy happened correctly on delete at eol
+ feed('$x')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 3)
+ feed('u')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 4)
+ end)
+
+ it('marks move with blockwise deletes #extmarks', function()
+ -- op_delete in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 4)
+ feed('h<c-v>hhkd')
+ check_undo_redo(ns, marks[1], 1, 4, 1, 1)
+ end)
+
+ it('marks move with blockwise deletes over a range #extmarks', function()
+ -- op_delete in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 1, 2)
+ feed('0<c-v>k3lx')
+ check_undo_redo(ns, marks[1], 0, 1, 0, 0)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 0)
+ check_undo_redo(ns, marks[3], 1, 2, 1, 0)
+ -- delete 1, nothing should happend to our marks
+ feed('u')
+ feed('$<c-v>jx')
+ check_undo_redo(ns, marks[2], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[3], 1, 2, 1, 2)
+ end)
+
+ it('works with char deletes over multilines #extmarks', function()
+ feed('a<cr>12345<cr>test-me<esc>')
+ set_extmark(ns, marks[1], 2, 5)
+ feed('gg')
+ feed('dv?-m?<cr>')
+ check_undo_redo(ns, marks[1], 2, 5, 0, 0)
+ end)
+
+ it('marks outside of deleted range move with visual char deletes #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 3)
+ feed('0vx<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 2)
+
+ feed("u")
+ feed('0vlx<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 1)
+
+ feed("u")
+ feed('0v2lx<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 0)
+
+ -- from the other side (nothing should happen)
+ feed('$vx')
+ check_undo_redo(ns, marks[1], 0, 0, 0, 0)
+ end)
+
+ it('marks outside of deleted range move with char deletes #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 3)
+ feed('0x<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 2)
+
+ feed("u")
+ feed('02x<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 1)
+
+ feed("u")
+ feed('0v3lx<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 0)
+
+ -- from the other side (nothing should happen)
+ feed("u")
+ feed('$vx')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ end)
+
+ it('marks move with P(backward) paste #extmarks', function()
+ -- do_put in ops.c
+ feed('0iabc<esc>')
+ set_extmark(ns, marks[1], 0, 7)
+ feed('0veyP')
+ check_undo_redo(ns, marks[1], 0, 7, 0, 15)
+ end)
+
+ it('marks move with p(forward) paste #extmarks', function()
+ -- do_put in ops.c
+ feed('0iabc<esc>')
+ set_extmark(ns, marks[1], 0, 7)
+ feed('0veyp')
+ check_undo_redo(ns, marks[1], 0, 7, 0, 14)
+ end)
+
+ it('marks move with blockwise P(backward) paste #extmarks', function()
+ -- do_put in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 4)
+ feed('<c-v>hhkyP<esc>')
+ check_undo_redo(ns, marks[1], 1, 4, 1, 7)
+ end)
+
+ it('marks move with blockwise p(forward) paste #extmarks', function()
+ -- do_put in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 4)
+ feed('<c-v>hhkyp<esc>')
+ check_undo_redo(ns, marks[1], 1, 4, 1, 6)
+ end)
+
+ it('replace works #extmarks', function()
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0r2')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ end)
+
+ it('blockwise replace works #extmarks', function()
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0<c-v>llkr1<esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ end)
+
+ it('shift line #extmarks', function()
+ -- shift_line in ops.c
+ feed(':set shiftwidth=4<cr><esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0>>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 6)
+
+ feed('>>')
+ check_undo_redo(ns, marks[1], 0, 6, 0, 10)
+
+ feed('<LT><LT>') -- have to escape, same as <<
+ check_undo_redo(ns, marks[1], 0, 10, 0, 6)
+ end)
+
+ it('blockwise shift #extmarks', function()
+ -- shift_block in ops.c
+ feed(':set shiftwidth=4<cr><esc>')
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 2)
+ feed('0<c-v>k>')
+ check_undo_redo(ns, marks[1], 1, 2, 1, 6)
+ feed('<c-v>j>')
+ check_undo_redo(ns, marks[1], 1, 6, 1, 10)
+
+ feed('<c-v>j<LT>')
+ check_undo_redo(ns, marks[1], 1, 10, 1, 6)
+ end)
+
+ it('tab works with expandtab #extmarks', function()
+ -- ins_tab in edit.c
+ feed(':set expandtab<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0i<tab><tab><esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 6)
+ end)
+
+ it('tabs work #extmarks', function()
+ -- ins_tab in edit.c
+ feed(':set noexpandtab<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ feed(':set softtabstop=2<cr><esc>')
+ feed(':set tabstop=8<cr><esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0i<tab><esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 4)
+ feed('0iX<tab><esc>')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 6)
+ end)
+
+ it('marks move when using :move #extmarks', function()
+ set_extmark(ns, marks[1], 0, 0)
+ feed('A<cr>2<esc>:1move 2<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 0, 1, 0)
+ -- test codepath when moving lines up
+ feed(':2move 0<cr><esc>')
+ check_undo_redo(ns, marks[1], 1, 0, 0, 0)
+ end)
+
+ it('marks move when using :move part 2 #extmarks', function()
+ -- make sure we didn't get lucky with the math...
+ feed('A<cr>2<cr>3<cr>4<cr>5<cr>6<esc>')
+ set_extmark(ns, marks[1], 1, 0)
+ feed(':2,3move 5<cr><esc>')
+ check_undo_redo(ns, marks[1], 1, 0, 3, 0)
+ -- test codepath when moving lines up
+ feed(':4,5move 1<cr><esc>')
+ check_undo_redo(ns, marks[1], 3, 0, 1, 0)
+ end)
+
+ it('undo and redo of set and unset marks #extmarks', function()
+ -- Force a new undo head
+ feed('o<esc>')
+ set_extmark(ns, marks[1], 0, 1)
+ feed('o<esc>')
+ set_extmark(ns, marks[2], 0, -1)
+ set_extmark(ns, marks[3], 0, -1)
+
+ feed("u")
+ local rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(1, table.getn(rv))
+
+ feed("<c-r>")
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(3, table.getn(rv))
+
+ -- Test updates
+ feed('o<esc>')
+ set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ rv = get_extmarks(ns, marks[1], marks[1], {amount=1})
+ eq(1, table.getn(rv))
+ feed("u")
+ feed("<c-r>")
+ check_undo_redo(ns, marks[1], 0, 1, positions[1][1], positions[1][2])
+
+ -- Test unset
+ feed('o<esc>')
+ curbufmeths.del_extmark(ns, marks[3])
+ feed("u")
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(3, table.getn(rv))
+ feed("<c-r>")
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(2, table.getn(rv))
+ end)
+
+ it('undo and redo of marks deleted during edits #extmarks', function()
+ -- test extmark_adjust
+ feed('A<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 2)
+ feed('dd')
+ check_undo_redo(ns, marks[1], 1, 2, 1, 0)
+ end)
+
+ it('namespaces work properly #extmarks', function()
+ local rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ eq(1, rv)
+ rv = set_extmark(ns2, marks[1], positions[1][1], positions[1][2])
+ eq(1, rv)
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns2, {0, 0}, {-1, -1})
+ eq(1, table.getn(rv))
+
+ -- Set more marks for testing the ranges
+ set_extmark(ns, marks[2], positions[2][1], positions[2][2])
+ set_extmark(ns, marks[3], positions[3][1], positions[3][2])
+ set_extmark(ns2, marks[2], positions[2][1], positions[2][2])
+ set_extmark(ns2, marks[3], positions[3][1], positions[3][2])
+
+ -- get_next (amount set)
+ rv = get_extmarks(ns, {0, 0}, positions[2], {amount=1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns2, {0, 0}, positions[2], {amount=1})
+ eq(1, table.getn(rv))
+ -- get_prev (amount set)
+ rv = get_extmarks(ns, positions[1], {0, 0}, {amount=1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns2, positions[1], {0, 0}, {amount=1})
+ eq(1, table.getn(rv))
+
+ -- get_next (amount not set)
+ rv = get_extmarks(ns, positions[1], positions[2])
+ eq(2, table.getn(rv))
+ rv = get_extmarks(ns2, positions[1], positions[2])
+ eq(2, table.getn(rv))
+ -- get_prev (amount not set)
+ rv = get_extmarks(ns, positions[2], positions[1])
+ eq(2, table.getn(rv))
+ rv = get_extmarks(ns2, positions[2], positions[1])
+ eq(2, table.getn(rv))
+
+ curbufmeths.del_extmark(ns, marks[1])
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(2, table.getn(rv))
+ curbufmeths.del_extmark(ns2, marks[1])
+ rv = get_extmarks(ns2, {0, 0}, {-1, -1})
+ eq(2, table.getn(rv))
+ end)
+
+ it('mark set can create unique identifiers #extmarks', function()
+ -- create mark with id 1
+ eq(1, set_extmark(ns, 1, positions[1][1], positions[1][2]))
+ -- ask for unique id, it should be the next one, i e 2
+ eq(2, set_extmark(ns, 0, positions[1][1], positions[1][2]))
+ eq(3, set_extmark(ns, 3, positions[2][1], positions[2][2]))
+ eq(4, set_extmark(ns, 0, positions[1][1], positions[1][2]))
+
+ -- mixing manual and allocated id:s are not recommened, but it should
+ -- do something reasonable
+ eq(6, set_extmark(ns, 6, positions[2][1], positions[2][2]))
+ eq(7, set_extmark(ns, 0, positions[1][1], positions[1][2]))
+ eq(8, set_extmark(ns, 0, positions[1][1], positions[1][2]))
+ end)
+
+ it('auto indenting with enter works #extmarks', function()
+ -- op_reindent in ops.c
+ feed(':set cindent<cr><esc>')
+ feed(':set autoindent<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ feed("0iint <esc>A {1M1<esc>b<esc>")
+ -- Set the mark on the M, should move..
+ set_extmark(ns, marks[1], 0, 12)
+ -- Set the mark before the cursor, should stay there
+ set_extmark(ns, marks[2], 0, 10)
+ feed("i<cr><esc>")
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({1, 3}, rv)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[2])
+ eq({0, 10}, rv)
+ check_undo_redo(ns, marks[1], 0, 12, 1, 3)
+ end)
+
+ it('auto indenting entire line works #extmarks', function()
+ feed(':set cindent<cr><esc>')
+ feed(':set autoindent<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ -- <c-f> will force an indent of 2
+ feed("0iint <esc>A {<cr><esc>0i1M1<esc>")
+ set_extmark(ns, marks[1], 1, 1)
+ feed("0i<c-f><esc>")
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({1, 3}, rv)
+ check_undo_redo(ns, marks[1], 1, 1, 1, 3)
+ -- now check when cursor at eol
+ feed("uA<c-f><esc>")
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({1, 3}, rv)
+ end)
+
+ it('removing auto indenting with <C-D> works #extmarks', function()
+ feed(':set cindent<cr><esc>')
+ feed(':set autoindent<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ feed("0i<tab><esc>")
+ set_extmark(ns, marks[1], 0, 3)
+ feed("bi<c-d><esc>")
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, 1}, rv)
+ check_undo_redo(ns, marks[1], 0, 3, 0, 1)
+ -- check when cursor at eol
+ feed("uA<c-d><esc>")
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, 1}, rv)
+ end)
+
+ it('indenting multiple lines with = works #extmarks', function()
+ feed(':set cindent<cr><esc>')
+ feed(':set autoindent<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ feed("0iint <esc>A {<cr><bs>1M1<cr><bs>2M2<esc>")
+ set_extmark(ns, marks[1], 1, 1)
+ set_extmark(ns, marks[2], 2, 1)
+ feed('=gg')
+ check_undo_redo(ns, marks[1], 1, 1, 1, 3)
+ check_undo_redo(ns, marks[2], 2, 1, 2, 5)
+ end)
+
+ it('substitutes by deleting inside the replace matches #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ feed(':s/34/xx<cr>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 4)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 4)
+ end)
+
+ it('substitutes when insert text > deleted #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ feed(':s/34/xxx<cr>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 5)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 5)
+ end)
+
+ it('substitutes when marks around eol #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ set_extmark(ns, marks[1], 0, 4)
+ set_extmark(ns, marks[2], 0, 5)
+ feed(':s/5/xxx<cr>')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 7)
+ check_undo_redo(ns, marks[2], 0, 5, 0, 7)
+ end)
+
+ it('substitutes over range insert text > deleted #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ feed('A<cr>x34xx<esc>')
+ feed('A<cr>xxx34<esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 1, 1)
+ set_extmark(ns, marks[3], 2, 4)
+ feed(':1,3s/34/xxx<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 5)
+ check_undo_redo(ns, marks[2], 1, 1, 1, 4)
+ check_undo_redo(ns, marks[3], 2, 4, 2, 6)
+ end)
+
+ it('substitutes multiple matches in a line #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ feed('ddi3x3x3<esc>')
+ set_extmark(ns, marks[1], 0, 0)
+ set_extmark(ns, marks[2], 0, 2)
+ set_extmark(ns, marks[3], 0, 4)
+ feed(':s/3/yy/g<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 0, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 2, 0, 5)
+ check_undo_redo(ns, marks[3], 0, 4, 0, 8)
+ end)
+
+ it('substitions over multiple lines with newline in pattern #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 1, 0)
+ set_extmark(ns, marks[4], 1, 5)
+ set_extmark(ns, marks[5], 2, 0)
+ feed([[:1,2s:5\n:5 <cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 0, 6)
+ check_undo_redo(ns, marks[3], 1, 0, 0, 6)
+ check_undo_redo(ns, marks[4], 1, 5, 0, 11)
+ check_undo_redo(ns, marks[5], 2, 0, 1, 0)
+ end)
+
+ it('inserting #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 1, 0)
+ set_extmark(ns, marks[4], 1, 5)
+ set_extmark(ns, marks[5], 2, 0)
+ set_extmark(ns, marks[6], 1, 2)
+ feed([[:1,2s:5\n67:X<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 0, 5)
+ check_undo_redo(ns, marks[3], 1, 0, 0, 5)
+ check_undo_redo(ns, marks[4], 1, 5, 0, 8)
+ check_undo_redo(ns, marks[5], 2, 0, 1, 0)
+ check_undo_redo(ns, marks[6], 1, 2, 0, 5)
+ end)
+
+ it('substitions with multiple newlines in pattern #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 4)
+ set_extmark(ns, marks[2], 0, 5)
+ set_extmark(ns, marks[3], 1, 0)
+ set_extmark(ns, marks[4], 1, 5)
+ set_extmark(ns, marks[5], 2, 0)
+ feed([[:1,2s:\n.*\n:X<cr>]])
+ check_undo_redo(ns, marks[1], 0, 4, 0, 4)
+ check_undo_redo(ns, marks[2], 0, 5, 0, 6)
+ check_undo_redo(ns, marks[3], 1, 0, 0, 6)
+ check_undo_redo(ns, marks[4], 1, 5, 0, 6)
+ check_undo_redo(ns, marks[5], 2, 0, 0, 6)
+ end)
+
+ it('substitions over multiple lines with replace in substition #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 2)
+ set_extmark(ns, marks[3], 0, 4)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 2, 0)
+ feed([[:1,2s:3:\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 1, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 2, 1, 0)
+ check_undo_redo(ns, marks[3], 0, 4, 1, 1)
+ check_undo_redo(ns, marks[4], 1, 0, 2, 0)
+ check_undo_redo(ns, marks[5], 2, 0, 3, 0)
+ feed('u')
+ feed([[:1,2s:3:\rxx<cr>]])
+ eq({1, 3}, curbufmeths.get_extmark_by_id(ns, marks[3]))
+ end)
+
+ it('substitions over multiple lines with replace in substition #extmarks_sub', function()
+ feed('A<cr>x3<cr>xx<esc>')
+ set_extmark(ns, marks[1], 1, 0)
+ set_extmark(ns, marks[2], 1, 1)
+ set_extmark(ns, marks[3], 1, 2)
+ feed([[:2,2s:3:\r<cr>]])
+ check_undo_redo(ns, marks[1], 1, 0, 1, 0)
+ check_undo_redo(ns, marks[2], 1, 1, 2, 0)
+ check_undo_redo(ns, marks[3], 1, 2, 2, 0)
+ end)
+
+ it('substitions over multiple lines with replace in substition #extmarks_sub', function()
+ feed('A<cr>x3<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 2)
+ set_extmark(ns, marks[3], 0, 4)
+ set_extmark(ns, marks[4], 1, 1)
+ set_extmark(ns, marks[5], 2, 0)
+ feed([[:1,2s:3:\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 1, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 2, 1, 0)
+ check_undo_redo(ns, marks[3], 0, 4, 1, 1)
+ check_undo_redo(ns, marks[4], 1, 1, 3, 0)
+ check_undo_redo(ns, marks[5], 2, 0, 4, 0)
+ feed('u')
+ feed([[:1,2s:3:\rxx<cr>]])
+ check_undo_redo(ns, marks[3], 0, 4, 1, 3)
+ end)
+
+ it('substitions with newline in match and sub, delta is 0 #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 1, 5)
+ set_extmark(ns, marks[6], 2, 0)
+ feed([[:1,1s:5\n:\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 1, 0)
+ check_undo_redo(ns, marks[3], 0, 5, 1, 0)
+ check_undo_redo(ns, marks[4], 1, 0, 1, 0)
+ check_undo_redo(ns, marks[5], 1, 5, 1, 5)
+ check_undo_redo(ns, marks[6], 2, 0, 2, 0)
+ end)
+
+ it('substitions with newline in match and sub, delta > 0 #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 1, 5)
+ set_extmark(ns, marks[6], 2, 0)
+ feed([[:1,1s:5\n:\r\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 2, 0)
+ check_undo_redo(ns, marks[3], 0, 5, 2, 0)
+ check_undo_redo(ns, marks[4], 1, 0, 2, 0)
+ check_undo_redo(ns, marks[5], 1, 5, 2, 5)
+ check_undo_redo(ns, marks[6], 2, 0, 3, 0)
+ end)
+
+ it('substitions with newline in match and sub, delta < 0 #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 1, 5)
+ set_extmark(ns, marks[6], 2, 1)
+ set_extmark(ns, marks[7], 3, 0)
+ feed([[:1,2s:5\n.*\n:\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 1, 0)
+ check_undo_redo(ns, marks[3], 0, 5, 1, 0)
+ check_undo_redo(ns, marks[4], 1, 0, 1, 0)
+ check_undo_redo(ns, marks[5], 1, 5, 1, 0)
+ check_undo_redo(ns, marks[6], 2, 1, 1, 1)
+ check_undo_redo(ns, marks[7], 3, 0, 2, 0)
+ end)
+
+ it('substitions with backrefs, newline inserted into sub #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 1, 5)
+ set_extmark(ns, marks[6], 2, 0)
+ feed([[:1,1s:5\(\n\):\0\1<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 2, 0)
+ check_undo_redo(ns, marks[3], 0, 5, 2, 0)
+ check_undo_redo(ns, marks[4], 1, 0, 2, 0)
+ check_undo_redo(ns, marks[5], 1, 5, 2, 5)
+ check_undo_redo(ns, marks[6], 2, 0, 3, 0)
+ end)
+
+ it('substitions a ^ #extmarks_sub', function()
+ set_extmark(ns, marks[1], 0, 0)
+ set_extmark(ns, marks[2], 0, 1)
+ feed([[:s:^:x<cr>]])
+ check_undo_redo(ns, marks[1], 0, 0, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 1, 0, 2)
+ end)
+
+ it('using <c-a> without increase in order of magnitude #extmarks_inc_dec', function()
+ -- do_addsub in ops.c
+ feed('ddiabc998xxx<esc>Tc')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 0, 6)
+ set_extmark(ns, marks[5], 0, 7)
+ feed('<c-a>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 6)
+ check_undo_redo(ns, marks[3], 0, 5, 0, 6)
+ check_undo_redo(ns, marks[4], 0, 6, 0, 6)
+ check_undo_redo(ns, marks[5], 0, 7, 0, 7)
+ end)
+
+ it('using <c-a> when increase in order of magnitude #extmarks_inc_dec', function()
+ -- do_addsub in ops.c
+ feed('ddiabc999xxx<esc>Tc')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 0, 6)
+ set_extmark(ns, marks[5], 0, 7)
+ feed('<c-a>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 7)
+ check_undo_redo(ns, marks[3], 0, 5, 0, 7)
+ check_undo_redo(ns, marks[4], 0, 6, 0, 7)
+ check_undo_redo(ns, marks[5], 0, 7, 0, 8)
+ end)
+
+ it('using <c-a> when negative and without decrease in order of magnitude #extmarks_inc_dec', function()
+ feed('ddiabc-999xxx<esc>T-')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 6)
+ set_extmark(ns, marks[4], 0, 7)
+ set_extmark(ns, marks[5], 0, 8)
+ feed('<c-a>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 7)
+ check_undo_redo(ns, marks[3], 0, 6, 0, 7)
+ check_undo_redo(ns, marks[4], 0, 7, 0, 7)
+ check_undo_redo(ns, marks[5], 0, 8, 0, 8)
+ end)
+
+ it('using <c-a> when negative and decrease in order of magnitude #extmarks_inc_dec', function()
+ feed('ddiabc-1000xxx<esc>T-')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 7)
+ set_extmark(ns, marks[4], 0, 8)
+ set_extmark(ns, marks[5], 0, 9)
+ feed('<c-a>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 7)
+ check_undo_redo(ns, marks[3], 0, 7, 0, 7)
+ check_undo_redo(ns, marks[4], 0, 8, 0, 7)
+ check_undo_redo(ns, marks[5], 0, 9, 0, 8)
+ end)
+
+ it('using <c-x> without decrease in order of magnitude #extmarks_inc_dec', function()
+ -- do_addsub in ops.c
+ feed('ddiabc999xxx<esc>Tc')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 0, 6)
+ set_extmark(ns, marks[5], 0, 7)
+ feed('<c-x>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 6)
+ check_undo_redo(ns, marks[3], 0, 5, 0, 6)
+ check_undo_redo(ns, marks[4], 0, 6, 0, 6)
+ check_undo_redo(ns, marks[5], 0, 7, 0, 7)
+ end)
+
+ it('using <c-x> when decrease in order of magnitude #extmarks_inc_dec', function()
+ -- do_addsub in ops.c
+ feed('ddiabc1000xxx<esc>Tc')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 6)
+ set_extmark(ns, marks[4], 0, 7)
+ set_extmark(ns, marks[5], 0, 8)
+ feed('<c-x>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 6)
+ check_undo_redo(ns, marks[3], 0, 6, 0, 6)
+ check_undo_redo(ns, marks[4], 0, 7, 0, 6)
+ check_undo_redo(ns, marks[5], 0, 8, 0, 7)
+ end)
+
+ it('using <c-x> when negative and without increase in order of magnitude #extmarks_inc_dec', function()
+ feed('ddiabc-998xxx<esc>T-')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 6)
+ set_extmark(ns, marks[4], 0, 7)
+ set_extmark(ns, marks[5], 0, 8)
+ feed('<c-x>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 7)
+ check_undo_redo(ns, marks[3], 0, 6, 0, 7)
+ check_undo_redo(ns, marks[4], 0, 7, 0, 7)
+ check_undo_redo(ns, marks[5], 0, 8, 0, 8)
+ end)
+
+ it('using <c-x> when negative and increase in order of magnitude #extmarks_inc_dec', function()
+ feed('ddiabc-999xxx<esc>T-')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 6)
+ set_extmark(ns, marks[4], 0, 7)
+ set_extmark(ns, marks[5], 0, 8)
+ feed('<c-x>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 8)
+ check_undo_redo(ns, marks[3], 0, 6, 0, 8)
+ check_undo_redo(ns, marks[4], 0, 7, 0, 8)
+ check_undo_redo(ns, marks[5], 0, 8, 0, 9)
+ end)
+
+ it('throws consistent error codes', function()
+ local ns_invalid = ns2 + 1
+ eq("Invalid ns_id", pcall_err(set_extmark, ns_invalid, marks[1], positions[1][1], positions[1][2]))
+ eq("Invalid ns_id", pcall_err(curbufmeths.del_extmark, ns_invalid, marks[1]))
+ eq("Invalid ns_id", pcall_err(get_extmarks, ns_invalid, positions[1], positions[2]))
+ eq("Invalid ns_id", pcall_err(curbufmeths.get_extmark_by_id, ns_invalid, marks[1]))
+ end)
+
+ it('when col = line-length, set the mark on eol #extmarks', function()
+ set_extmark(ns, marks[1], 0, -1)
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, init_text:len()}, rv)
+ -- Test another
+ set_extmark(ns, marks[1], 0, -1)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, init_text:len()}, rv)
+ end)
+
+ it('when col = line-length, set the mark on eol #extmarks', function()
+ local invalid_col = init_text:len() + 1
+ eq("col value outside range", pcall_err(set_extmark, ns, marks[1], 0, invalid_col))
+ end)
+
+ it('when line > line_count, throw error #extmarks', function()
+ local invalid_col = init_text:len() + 1
+ local invalid_lnum = 3
+ eq('line value outside range', pcall_err(set_extmark, ns, marks[1], invalid_lnum, invalid_col))
+ eq({}, curbufmeths.get_extmark_by_id(ns, marks[1]))
+ end)
+
+ it('bug from check_col in extmark_set #extmarks_sub', function()
+ -- This bug was caused by extmark_set always using
+ -- check_col. check_col always uses the current buffer.
+ -- This wasn't working during undo so we now use
+ -- check_col and check_lnum only when they are required.
+ feed('A<cr>67890<cr>xx<esc>')
+ feed('A<cr>12345<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 3, 4)
+ feed([[:1,5s:5\n:5 <cr>]])
+ check_undo_redo(ns, marks[1], 3, 4, 2, 6)
+ end)
+
+ it('in read-only buffer', function()
+ command("view! runtime/doc/help.txt")
+ eq(true, curbufmeths.get_option('ro'))
+ local id = set_extmark(ns, 0, 0, 2)
+ eq({{id, 0, 2}}, get_extmarks(ns,0, -1))
+ end)
+end)
+
+describe('Extmarks buffer api with many marks', function()
+ local ns1
+ local ns2
+ local ns_marks = {}
+ before_each(function()
+ clear()
+ ns1 = request('nvim_create_namespace', "ns1")
+ ns2 = request('nvim_create_namespace', "ns2")
+ ns_marks = {[ns1]={}, [ns2]={}}
+ local lines = {}
+ for i = 1,30 do
+ lines[#lines+1] = string.rep("x ",i)
+ end
+ curbufmeths.set_lines(0, -1, true, lines)
+ local ns = ns1
+ local q = 0
+ for i = 0,29 do
+ for j = 0,i do
+ local id = set_extmark(ns,0, i,j)
+ eq(nil, ns_marks[ns][id])
+ ok(id > 0)
+ ns_marks[ns][id] = {i,j}
+ ns = ns1+ns2-ns
+ q = q + 1
+ end
+ end
+ eq(233, #ns_marks[ns1])
+ eq(232, #ns_marks[ns2])
+
+ end)
+
+ local function get_marks(ns)
+ local mark_list = get_extmarks(ns, 0, -1)
+ local marks = {}
+ for _, mark in ipairs(mark_list) do
+ local id, row, col = unpack(mark)
+ eq(nil, marks[id], "duplicate mark")
+ marks[id] = {row,col}
+ end
+ return marks
+ end
+
+ it("can get marks #extmarks", function()
+ eq(ns_marks[ns1], get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ end)
+
+ it("can clear all marks in ns #extmarks", function()
+ curbufmeths.clear_namespace(ns1, 0, -1)
+ eq({}, get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ curbufmeths.clear_namespace(ns2, 0, -1)
+ eq({}, get_marks(ns1))
+ eq({}, get_marks(ns2))
+ end)
+
+ it("can clear line range #extmarks", function()
+ curbufmeths.clear_namespace(ns1, 10, 20)
+ for id, mark in pairs(ns_marks[ns1]) do
+ if 10 <= mark[1] and mark[1] < 20 then
+ ns_marks[ns1][id] = nil
+ end
+ end
+ eq(ns_marks[ns1], get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ end)
+
+ it("can delete line #extmarks", function()
+ feed('10Gdd')
+ for _, marks in pairs(ns_marks) do
+ for id, mark in pairs(marks) do
+ if mark[1] == 9 then
+ marks[id] = {9,0}
+ elseif mark[1] >= 10 then
+ mark[1] = mark[1] - 1
+ end
+ end
+ end
+ eq(ns_marks[ns1], get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ end)
+
+ it("can delete lines #extmarks", function()
+ feed('10G10dd')
+ for _, marks in pairs(ns_marks) do
+ for id, mark in pairs(marks) do
+ if 9 <= mark[1] and mark[1] < 19 then
+ marks[id] = {9,0}
+ elseif mark[1] >= 19 then
+ mark[1] = mark[1] - 10
+ end
+ end
+ end
+ eq(ns_marks[ns1], get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ end)
+
+ it("can wipe buffer #extmarks", function()
+ command('bwipe!')
+ eq({}, get_marks(ns1))
+ eq({}, get_marks(ns2))
+ end)
+end)
diff --git a/test/functional/core/fileio_spec.lua b/test/functional/core/fileio_spec.lua
index e6bce85b8a..f4c476560d 100644
--- a/test/functional/core/fileio_spec.lua
+++ b/test/functional/core/fileio_spec.lua
@@ -9,9 +9,12 @@ local nvim_prog = helpers.nvim_prog
local request = helpers.request
local retry = helpers.retry
local rmdir = helpers.rmdir
+local mkdir = helpers.mkdir
local sleep = helpers.sleep
local read_file = helpers.read_file
local trim = helpers.trim
+local currentdir = helpers.funcs.getcwd
+local iswin = helpers.iswin
describe('fileio', function()
before_each(function()
@@ -24,6 +27,7 @@ describe('fileio', function()
os.remove('Xtest_startup_file2')
os.remove('Xtest_тест.md')
rmdir('Xtest_startup_swapdir')
+ rmdir('Xtest_backupdir')
end)
it('fsync() codepaths #8304', function()
@@ -88,6 +92,27 @@ describe('fileio', function()
eq('foo', bar_contents);
end)
+ it('backup with full path #11214', function()
+ clear()
+ mkdir('Xtest_backupdir')
+ command('set backup')
+ command('set backupdir=Xtest_backupdir//')
+ command('write Xtest_startup_file1')
+ feed('ifoo<esc>')
+ command('write')
+ feed('Abar<esc>')
+ command('write')
+
+ -- Backup filename = fullpath, separators replaced with "%".
+ local backup_file_name = string.gsub(currentdir()..'/Xtest_startup_file1',
+ iswin() and '[:/\\]' or '/', '%%') .. '~'
+ local foo_contents = trim(read_file('Xtest_backupdir/'..backup_file_name))
+ local foobar_contents = trim(read_file('Xtest_startup_file1'))
+
+ eq('foobar', foobar_contents);
+ eq('foo', foo_contents);
+ end)
+
it('readfile() on multibyte filename #10586', function()
clear()
local text = {
diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua
index b793e531c9..37a9f0b836 100644
--- a/test/functional/core/main_spec.lua
+++ b/test/functional/core/main_spec.lua
@@ -67,7 +67,7 @@ describe('Command-line option', function()
|
|
]], {
- [1] = {foreground = 4210943},
+ [1] = {foreground = tonumber('0x4040ff'), fg_indexed=true},
[2] = {bold = true, reverse = true}
})
feed('i:cq<CR>')
diff --git a/test/functional/fixtures/lsp-test-rpc-server.lua b/test/functional/fixtures/lsp-test-rpc-server.lua
new file mode 100644
index 0000000000..971e61b072
--- /dev/null
+++ b/test/functional/fixtures/lsp-test-rpc-server.lua
@@ -0,0 +1,424 @@
+local protocol = require 'vim.lsp.protocol'
+
+-- Internal utility methods.
+
+-- TODO replace with a better implementation.
+local function json_encode(data)
+ local status, result = pcall(vim.fn.json_encode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+local function json_decode(data)
+ local status, result = pcall(vim.fn.json_decode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+
+local function message_parts(sep, ...)
+ local parts = {}
+ for i = 1, select("#", ...) do
+ local arg = select(i, ...)
+ if arg ~= nil then
+ table.insert(parts, arg)
+ end
+ end
+ return table.concat(parts, sep)
+end
+
+-- Assert utility methods
+
+local function assert_eq(a, b, ...)
+ if not vim.deep_equal(a, b) then
+ error(message_parts(": ",
+ ..., "assert_eq failed",
+ string.format("left == %q, right == %q", vim.inspect(a), vim.inspect(b))
+ ))
+ end
+end
+
+local function format_message_with_content_length(encoded_message)
+ return table.concat {
+ 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n';
+ encoded_message;
+ }
+end
+
+-- Server utility methods.
+
+local function read_message()
+ local line = io.read("*l")
+ local length = line:lower():match("content%-length:%s*(%d+)")
+ return assert(json_decode(io.read(2 + length):sub(2)), "read_message.json_decode")
+end
+
+local function send(payload)
+ io.stdout:write(format_message_with_content_length(json_encode(payload)))
+end
+
+local function respond(id, err, result)
+ assert(type(id) == 'number', "id must be a number")
+ send { jsonrpc = "2.0"; id = id, error = err, result = result }
+end
+
+local function notify(method, params)
+ assert(type(method) == 'string', "method must be a string")
+ send { method = method, params = params or {} }
+end
+
+local function expect_notification(method, params, ...)
+ local message = read_message()
+ assert_eq(method, message.method,
+ ..., "expect_notification", "method")
+ assert_eq(params, message.params,
+ ..., "expect_notification", method, "params")
+ assert_eq({jsonrpc = "2.0"; method=method, params=params}, message,
+ ..., "expect_notification", "message")
+end
+
+local function expect_request(method, callback, ...)
+ local req = read_message()
+ assert_eq(method, req.method,
+ ..., "expect_request", "method")
+ local err, result = callback(req.params)
+ respond(req.id, err, result)
+end
+
+io.stderr:setvbuf("no")
+
+local function skeleton(config)
+ local on_init = assert(config.on_init)
+ local body = assert(config.body)
+ expect_request("initialize", function(params)
+ return nil, on_init(params)
+ end)
+ expect_notification("initialized", {})
+ body()
+ expect_request("shutdown", function()
+ return nil, {}
+ end)
+ expect_notification("exit", nil)
+end
+
+-- The actual tests.
+
+local tests = {}
+
+function tests.basic_init()
+ skeleton {
+ on_init = function(_params)
+ return { capabilities = {} }
+ end;
+ body = function()
+ notify('test')
+ end;
+ }
+end
+
+function tests.basic_check_capabilities()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ end;
+ }
+end
+
+function tests.basic_finish()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n");
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n");
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "boop"}, "\n"); };
+ }
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change_multi()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n");
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "321"}, "\n"); };
+ }
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 4;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "boop"}, "\n"); };
+ }
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change_multi_and_close()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n");
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "321"}, "\n"); };
+ }
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 4;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "boop"}, "\n"); };
+ }
+ })
+ expect_notification('textDocument/didClose', {
+ textDocument = {
+ uri = "file://";
+ };
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change_incremental()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Incremental;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n");
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ {
+ range = {
+ start = { line = 1; character = 0; };
+ ["end"] = { line = 2; character = 0; };
+ };
+ rangeLength = 4;
+ text = "boop\n";
+ };
+ }
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change_incremental_editting()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Incremental;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n");
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ {
+ range = {
+ start = { line = 0; character = 0; };
+ ["end"] = { line = 1; character = 0; };
+ };
+ rangeLength = 4;
+ text = "testing\n\n";
+ };
+ }
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.invalid_header()
+ io.stdout:write("Content-length: \r\n")
+end
+
+-- Tests will be indexed by TEST_NAME
+
+local kill_timer = vim.loop.new_timer()
+kill_timer:start(_G.TIMEOUT or 1e3, 0, function()
+ kill_timer:stop()
+ kill_timer:close()
+ io.stderr:write("TIMEOUT")
+ os.exit(100)
+end)
+
+local test_name = _G.TEST_NAME -- lualint workaround
+assert(type(test_name) == 'string', 'TEST_NAME must be specified.')
+local status, err = pcall(assert(tests[test_name], "Test not found"))
+kill_timer:stop()
+kill_timer:close()
+if not status then
+ io.stderr:write(err)
+ os.exit(1)
+end
+os.exit(0)
diff --git a/test/functional/legacy/022_line_ending_spec.lua b/test/functional/legacy/022_line_ending_spec.lua
deleted file mode 100644
index fb4b782011..0000000000
--- a/test/functional/legacy/022_line_ending_spec.lua
+++ /dev/null
@@ -1,25 +0,0 @@
--- Tests for file with some lines ending in CTRL-M, some not
-
-local helpers = require('test.functional.helpers')(after_each)
-local clear, feed = helpers.clear, helpers.feed
-local feed_command, expect = helpers.feed_command, helpers.expect
-
-describe('line ending', function()
- setup(clear)
-
- it('is working', function()
- feed('i', [[
- this lines ends in a<C-V><C-M>
- this one doesn't
- this one does<C-V><C-M>
- and the last one doesn't]], '<ESC>')
-
- feed_command('set ta tx')
- feed_command('e!')
-
- expect("this lines ends in a\r\n"..
- "this one doesn't\n"..
- "this one does\r\n"..
- "and the last one doesn't")
- end)
-end)
diff --git a/test/functional/legacy/041_writing_and_reading_hundred_kbyte_spec.lua b/test/functional/legacy/041_writing_and_reading_hundred_kbyte_spec.lua
deleted file mode 100644
index b526d82519..0000000000
--- a/test/functional/legacy/041_writing_and_reading_hundred_kbyte_spec.lua
+++ /dev/null
@@ -1,43 +0,0 @@
--- Test for writing and reading a file of over 100 Kbyte
-
-local helpers = require('test.functional.helpers')(after_each)
-
-local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert
-local command, expect = helpers.command, helpers.expect
-local wait = helpers.wait
-
-describe('writing and reading a file of over 100 Kbyte', function()
- setup(clear)
-
- it('is working', function()
- insert([[
- This is the start
- This is the leader
- This is the middle
- This is the trailer
- This is the end]])
-
- feed('kY3000p2GY3000p')
- wait()
-
- command('w! test.out')
- command('%d')
- command('e! test.out')
- command('yank A')
- command('3003yank A')
- command('6005yank A')
- command('%d')
- command('0put a')
- command('$d')
- command('w!')
-
- expect([[
- This is the start
- This is the middle
- This is the end]])
- end)
-
- teardown(function()
- os.remove('test.out')
- end)
-end)
diff --git a/test/functional/legacy/077_mf_hash_grow_spec.lua b/test/functional/legacy/077_mf_hash_grow_spec.lua
deleted file mode 100644
index 4719a3ecbf..0000000000
--- a/test/functional/legacy/077_mf_hash_grow_spec.lua
+++ /dev/null
@@ -1,52 +0,0 @@
--- Inserts 2 million lines with consecutive integers starting from 1
--- (essentially, the output of GNU's seq 1 2000000), writes them to Xtest
--- and calculates its cksum.
--- We need 2 million lines to trigger a call to mf_hash_grow(). If it would mess
--- up the lines the checksum would differ.
--- cksum is part of POSIX and so should be available on most Unixes.
--- If it isn't available then the test will be skipped.
-
-local helpers = require('test.functional.helpers')(after_each)
-
-local feed = helpers.feed
-local wait = helpers.wait
-local clear = helpers.clear
-local expect = helpers.expect
-local command = helpers.command
-
-describe('mf_hash_grow()', function()
- setup(clear)
-
- -- Check to see if cksum exists, otherwise skip the test
- local null = helpers.iswin() and 'nul' or '/dev/null'
- if os.execute('cksum --help >' .. null .. ' 2>&1') ~= 0 then
- pending('was not tested because cksum was not found', function() end)
- else
- it('is working', function()
- command('set fileformat=unix undolevels=-1')
-
- -- Fill the buffer with numbers 1 - 2000000
- command('let i = 1')
- command('while i <= 2000000 | call append(i, range(i, i + 99)) | let i += 100 | endwhile')
-
- -- Delete empty first line, save to Xtest, and clear buffer
- feed('ggdd<cr>')
- wait()
- command('w! Xtest')
- feed('ggdG<cr>')
- wait()
-
- -- Calculate the cksum of Xtest and delete first line
- command('r !cksum Xtest')
- feed('ggdd<cr>')
-
- -- Assert correct output of cksum.
- expect([[
- 3678979763 14888896 Xtest]])
- end)
- end
-
- teardown(function()
- os.remove('Xtest')
- end)
-end)
diff --git a/test/functional/legacy/084_curswant_spec.lua b/test/functional/legacy/084_curswant_spec.lua
deleted file mode 100644
index 42cb2fc56d..0000000000
--- a/test/functional/legacy/084_curswant_spec.lua
+++ /dev/null
@@ -1,49 +0,0 @@
--- Tests for curswant not changing when setting an option.
-
-local helpers = require('test.functional.helpers')(after_each)
-local insert, source = helpers.insert, helpers.source
-local clear, expect = helpers.clear, helpers.expect
-
-describe('curswant', function()
- setup(clear)
-
- -- luacheck: ignore 621 (Indentation)
- it('is working', function()
- insert([[
- start target options
- tabstop
- timeoutlen
- ttimeoutlen
- end target options]])
-
- source([[
- /^start target options$/+1,/^end target options$/-1 yank
- let target_option_names = split(@0)
- function TestCurswant(option_name)
- normal! ggf8j
- let curswant_before = winsaveview().curswant
- execute 'let' '&'.a:option_name '=' '&'.a:option_name
- let curswant_after = winsaveview().curswant
- return [a:option_name, curswant_before, curswant_after]
- endfunction
-
- new
- put =['1234567890', '12345']
- 1 delete _
- let result = []
- for option_name in target_option_names
- call add(result, TestCurswant(option_name))
- endfor
-
- new
- put =map(copy(result), 'join(v:val, '' '')')
- 1 delete _
- ]])
-
- -- Assert buffer contents.
- expect([[
- tabstop 7 4
- timeoutlen 7 7
- ttimeoutlen 7 7]])
- end)
-end)
diff --git a/test/functional/legacy/098_scrollbind_spec.lua b/test/functional/legacy/098_scrollbind_spec.lua
deleted file mode 100644
index d22aefdcbc..0000000000
--- a/test/functional/legacy/098_scrollbind_spec.lua
+++ /dev/null
@@ -1,48 +0,0 @@
--- Test for 'scrollbind' causing an unexpected scroll of one of the windows.
-
-local helpers = require('test.functional.helpers')(after_each)
-local source = helpers.source
-local clear, expect = helpers.clear, helpers.expect
-
-describe('scrollbind', function()
- setup(clear)
-
- it('is working', function()
- source([[
- set laststatus=0
- let g:totalLines = &lines * 20
- let middle = g:totalLines / 2
- wincmd n
- wincmd o
- for i in range(1, g:totalLines)
- call setline(i, 'LINE ' . i)
- endfor
- exe string(middle)
- normal zt
- normal M
- aboveleft vert new
- for i in range(1, g:totalLines)
- call setline(i, 'line ' . i)
- endfor
- exe string(middle)
- normal zt
- normal M
- setl scb | wincmd p
- setl scb
- wincmd w
- let topLineLeft = line('w0')
- wincmd p
- let topLineRight = line('w0')
- setl noscrollbind
- wincmd p
- setl noscrollbind
- q!
- %del _
- call setline(1, 'Difference between the top lines (left - right): ' . string(topLineLeft - topLineRight))
- brewind
- ]])
-
- -- Assert buffer contents.
- expect("Difference between the top lines (left - right): 0")
- end)
-end)
diff --git a/test/functional/legacy/104_let_assignment_spec.lua b/test/functional/legacy/104_let_assignment_spec.lua
deleted file mode 100644
index a03bb026f6..0000000000
--- a/test/functional/legacy/104_let_assignment_spec.lua
+++ /dev/null
@@ -1,54 +0,0 @@
--- Tests for :let.
-
-local helpers = require('test.functional.helpers')(after_each)
-local clear, source = helpers.clear, helpers.source
-local command, expect = helpers.command, helpers.expect
-
-describe(':let', function()
- setup(clear)
-
- it('is working', function()
- command('set runtimepath+=test/functional/fixtures')
-
- -- Test to not autoload when assigning. It causes internal error.
- source([[
- try
- let Test104#numvar = function('tr')
- $put ='OK: ' . string(Test104#numvar)
- catch
- $put ='FAIL: ' . v:exception
- endtry
- let a = 1
- let b = 2
- for letargs in ['a b', '{0 == 1 ? "a" : "b"}', '{0 == 1 ? "a" : "b"} a', 'a {0 == 1 ? "a" : "b"}']
- try
- redir => messages
- execute 'let' letargs
- redir END
- $put ='OK:'
- $put =split(substitute(messages, '\n', '\0 ', 'g'), '\n')
- catch
- $put ='FAIL: ' . v:exception
- redir END
- endtry
- endfor]])
-
- -- Remove empty line
- command('1d')
-
- -- Assert buffer contents.
- expect([[
- OK: function('tr')
- OK:
- a #1
- b #2
- OK:
- b #2
- OK:
- b #2
- a #1
- OK:
- a #1
- b #2]])
- end)
-end)
diff --git a/test/functional/lua/api_spec.lua b/test/functional/lua/api_spec.lua
index b1dc5c07fd..23167d3ed9 100644
--- a/test/functional/lua/api_spec.lua
+++ b/test/functional/lua/api_spec.lua
@@ -155,41 +155,41 @@ describe('luaeval(vim.api.…)', function()
it('errors out correctly when working with API', function()
-- Conversion errors
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Cannot convert given lua type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua type',
exc_exec([[call luaeval("vim.api.nvim__id(vim.api.nvim__id)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Cannot convert given lua table',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua table',
exc_exec([[call luaeval("vim.api.nvim__id({1, foo=42})")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Cannot convert given lua type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua type',
exc_exec([[call luaeval("vim.api.nvim__id({42, vim.api.nvim__id})")]]))
-- Errors in number of arguments
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected 1 argument',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected 1 argument',
exc_exec([[call luaeval("vim.api.nvim__id()")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected 1 argument',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected 1 argument',
exc_exec([[call luaeval("vim.api.nvim__id(1, 2)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected 2 arguments',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected 2 arguments',
exc_exec([[call luaeval("vim.api.nvim_set_var(1, 2, 3)")]]))
-- Error in argument types
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua string',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua string',
exc_exec([[call luaeval("vim.api.nvim_set_var(1, 2)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua number',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua number',
exc_exec([[call luaeval("vim.api.nvim_buf_get_lines(0, 'test', 1, false)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Number is not integral',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Number is not integral',
exc_exec([[call luaeval("vim.api.nvim_buf_get_lines(0, 1.5, 1, false)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua table',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua table',
exc_exec([[call luaeval("vim.api.nvim__id_float('test')")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Unexpected type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Unexpected type',
exc_exec([[call luaeval("vim.api.nvim__id_float({[vim.type_idx]=vim.types.dictionary})")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua table',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua table',
exc_exec([[call luaeval("vim.api.nvim__id_array(1)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Unexpected type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Unexpected type',
exc_exec([[call luaeval("vim.api.nvim__id_array({[vim.type_idx]=vim.types.dictionary})")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua table',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua table',
exc_exec([[call luaeval("vim.api.nvim__id_dictionary(1)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Unexpected type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Unexpected type',
exc_exec([[call luaeval("vim.api.nvim__id_dictionary({[vim.type_idx]=vim.types.array})")]]))
-- TODO: check for errors with Tabpage argument
-- TODO: check for errors with Window argument
diff --git a/test/functional/lua/commands_spec.lua b/test/functional/lua/commands_spec.lua
index 26dcbe0534..96eaa7991b 100644
--- a/test/functional/lua/commands_spec.lua
+++ b/test/functional/lua/commands_spec.lua
@@ -13,6 +13,7 @@ local source = helpers.source
local dedent = helpers.dedent
local command = helpers.command
local exc_exec = helpers.exc_exec
+local pcall_err = helpers.pcall_err
local write_file = helpers.write_file
local redir_exec = helpers.redir_exec
local curbufmeths = helpers.curbufmeths
@@ -42,16 +43,16 @@ describe(':lua command', function()
eq({'', 'ETTS', 'TTSE', 'STTE'}, curbufmeths.get_lines(0, 100, false))
end)
it('throws catchable errors', function()
- eq([[Vim(lua):E5104: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unexpected symbol near ')']],
- exc_exec('lua ()'))
- eq([[Vim(lua):E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: TEST]],
+ eq([[Vim(lua):E5107: Error loading lua [string ":lua"]:1: unexpected symbol near ')']],
+ pcall_err(command, 'lua ()'))
+ eq([[Vim(lua):E5108: Error executing lua [string ":lua"]:1: TEST]],
exc_exec('lua error("TEST")'))
- eq([[Vim(lua):E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: Invalid buffer id]],
+ eq([[Vim(lua):E5108: Error executing lua [string ":lua"]:1: Invalid buffer id]],
exc_exec('lua vim.api.nvim_buf_set_lines(-10, 1, 1, false, {"TEST"})'))
eq({''}, curbufmeths.get_lines(0, 100, false))
end)
it('works with NULL errors', function()
- eq([=[Vim(lua):E5105: Error while calling lua chunk: [NULL]]=],
+ eq([=[Vim(lua):E5108: Error executing lua [NULL]]=],
exc_exec('lua error(nil)'))
end)
it('accepts embedded NLs without heredoc', function()
@@ -74,7 +75,7 @@ describe(':lua command', function()
it('works with long strings', function()
local s = ('x'):rep(100500)
- eq('\nE5104: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unfinished string near \'<eof>\'', redir_exec(('lua vim.api.nvim_buf_set_lines(1, 1, 2, false, {"%s})'):format(s)))
+ eq('\nE5107: Error loading lua [string ":lua"]:1: unfinished string near \'<eof>\'', redir_exec(('lua vim.api.nvim_buf_set_lines(1, 1, 2, false, {"%s})'):format(s)))
eq({''}, curbufmeths.get_lines(0, -1, false))
eq('', redir_exec(('lua vim.api.nvim_buf_set_lines(1, 1, 2, false, {"%s"})'):format(s)))
@@ -82,7 +83,7 @@ describe(':lua command', function()
end)
it('can show multiline error messages', function()
- local screen = Screen.new(50,10)
+ local screen = Screen.new(40,10)
screen:attach()
screen:set_default_attr_ids({
[1] = {bold = true, foreground = Screen.colors.Blue1},
@@ -92,51 +93,51 @@ describe(':lua command', function()
})
feed(':lua error("fail\\nmuch error\\nsuch details")<cr>')
- screen:expect([[
- |
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {2: }|
- {3:E5105: Error while calling lua chunk: [string "<Vi}|
- {3:mL compiled string>"]:1: fail} |
- {3:much error} |
- {3:such details} |
- {4:Press ENTER or type command to continue}^ |
- ]])
+ screen:expect{grid=[[
+ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {2: }|
+ {3:E5108: Error executing lua [string ":lua}|
+ {3:"]:1: fail} |
+ {3:much error} |
+ {3:such details} |
+ {4:Press ENTER or type command to continue}^ |
+ ]]}
feed('<cr>')
- screen:expect([[
- ^ |
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- |
- ]])
- eq('E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: fail\nmuch error\nsuch details', eval('v:errmsg'))
+ screen:expect{grid=[[
+ ^ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]]}
+ eq('E5108: Error executing lua [string ":lua"]:1: fail\nmuch error\nsuch details', eval('v:errmsg'))
local status, err = pcall(command,'lua error("some error\\nin a\\nAPI command")')
- local expected = 'Vim(lua):E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: some error\nin a\nAPI command'
+ local expected = 'Vim(lua):E5108: Error executing lua [string ":lua"]:1: some error\nin a\nAPI command'
eq(false, status)
eq(expected, string.sub(err, -string.len(expected)))
feed(':messages<cr>')
- screen:expect([[
- |
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {2: }|
- {3:E5105: Error while calling lua chunk: [string "<Vi}|
- {3:mL compiled string>"]:1: fail} |
- {3:much error} |
- {3:such details} |
- {4:Press ENTER or type command to continue}^ |
- ]])
+ screen:expect{grid=[[
+ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {2: }|
+ {3:E5108: Error executing lua [string ":lua}|
+ {3:"]:1: fail} |
+ {3:much error} |
+ {3:such details} |
+ {4:Press ENTER or type command to continue}^ |
+ ]]}
end)
end)
@@ -167,13 +168,13 @@ describe(':luado command', function()
eq({''}, curbufmeths.get_lines(0, -1, false))
end)
it('fails on errors', function()
- eq([[Vim(luado):E5109: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unexpected symbol near ')']],
+ eq([[Vim(luado):E5109: Error loading lua: [string ":luado"]:1: unexpected symbol near ')']],
exc_exec('luado ()'))
- eq([[Vim(luado):E5111: Error while calling lua function: [string "<VimL compiled string>"]:1: attempt to perform arithmetic on global 'liness' (a nil value)]],
+ eq([[Vim(luado):E5111: Error calling lua: [string ":luado"]:1: attempt to perform arithmetic on global 'liness' (a nil value)]],
exc_exec('luado return liness + 1'))
end)
it('works with NULL errors', function()
- eq([=[Vim(luado):E5111: Error while calling lua function: [NULL]]=],
+ eq([=[Vim(luado):E5111: Error calling lua: [NULL]]=],
exc_exec('luado error(nil)'))
end)
it('fails in sandbox when needed', function()
@@ -185,7 +186,7 @@ describe(':luado command', function()
it('works with long strings', function()
local s = ('x'):rep(100500)
- eq('\nE5109: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unfinished string near \'<eof>\'', redir_exec(('luado return "%s'):format(s)))
+ eq('\nE5109: Error loading lua: [string ":luado"]:1: unfinished string near \'<eof>\'', redir_exec(('luado return "%s'):format(s)))
eq({''}, curbufmeths.get_lines(0, -1, false))
eq('', redir_exec(('luado return "%s"'):format(s)))
diff --git a/test/functional/lua/luaeval_spec.lua b/test/functional/lua/luaeval_spec.lua
index 760105df6b..61c8e5c02e 100644
--- a/test/functional/lua/luaeval_spec.lua
+++ b/test/functional/lua/luaeval_spec.lua
@@ -1,13 +1,17 @@
-- Test suite for testing luaeval() function
local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
local redir_exec = helpers.redir_exec
+local pcall_err = helpers.pcall_err
local exc_exec = helpers.exc_exec
+local exec_lua = helpers.exec_lua
local command = helpers.command
local meths = helpers.meths
local funcs = helpers.funcs
local clear = helpers.clear
local eval = helpers.eval
+local feed = helpers.feed
local NIL = helpers.NIL
local eq = helpers.eq
@@ -186,9 +190,9 @@ describe('luaeval()', function()
exc_exec('call luaeval("{1, foo=2}")'))
eq("Vim(call):E5101: Cannot convert given lua type",
exc_exec('call luaeval("vim.api.nvim_buf_get_lines")'))
- startswith("Vim(call):E5107: Error while creating lua chunk for luaeval(): ",
+ startswith("Vim(call):E5107: Error loading lua [string \"luaeval()\"]:",
exc_exec('call luaeval("1, 2, 3")'))
- startswith("Vim(call):E5108: Error while calling lua chunk for luaeval(): ",
+ startswith("Vim(call):E5108: Error executing lua [string \"luaeval()\"]:",
exc_exec('call luaeval("(nil)()")'))
eq("Vim(call):E5101: Cannot convert given lua type",
exc_exec('call luaeval("{42, vim.api}")'))
@@ -237,19 +241,99 @@ describe('luaeval()', function()
it('errors out correctly when doing incorrect things in lua', function()
-- Conversion errors
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: attempt to call field \'xxx_nonexistent_key_xxx\' (a nil value)',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: attempt to call field \'xxx_nonexistent_key_xxx\' (a nil value)',
exc_exec([[call luaeval("vim.xxx_nonexistent_key_xxx()")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: ERROR',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: ERROR',
exc_exec([[call luaeval("error('ERROR')")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [NULL]',
+ eq('Vim(call):E5108: Error executing lua [NULL]',
exc_exec([[call luaeval("error(nil)")]]))
end)
it('does not leak memory when called with too long line',
function()
local s = ('x'):rep(65536)
- eq('Vim(call):E5107: Error while creating lua chunk for luaeval(): [string "<VimL compiled string>"]:1: unexpected symbol near \')\'',
+ eq('Vim(call):E5107: Error loading lua [string "luaeval()"]:1: unexpected symbol near \')\'',
exc_exec([[call luaeval("(']] .. s ..[[' + )")]]))
eq(s, funcs.luaeval('"' .. s .. '"'))
end)
end)
+
+describe('v:lua', function()
+ before_each(function()
+ exec_lua([[
+ function _G.foo(a,b,n)
+ _G.val = n
+ return a+b
+ end
+ mymod = {}
+ function mymod.noisy(name)
+ vim.api.nvim_set_current_line("hey "..name)
+ end
+ function mymod.crashy()
+ nonexistent()
+ end
+ function mymod.omni(findstart, base)
+ if findstart == 1 then
+ return 5
+ else
+ if base == 'st' then
+ return {'stuff', 'steam', 'strange things'}
+ end
+ end
+ end
+ vim.api.nvim_buf_set_option(0, 'omnifunc', 'v:lua.mymod.omni')
+ ]])
+ end)
+
+ it('works in expressions', function()
+ eq(7, eval('v:lua.foo(3,4,v:null)'))
+ eq(true, exec_lua([[return _G.val == vim.NIL]]))
+ eq(NIL, eval('v:lua.mymod.noisy("eval")'))
+ eq("hey eval", meths.get_current_line())
+
+ eq("Vim:E5108: Error executing lua [string \"<nvim>\"]:10: attempt to call global 'nonexistent' (a nil value)",
+ pcall_err(eval, 'v:lua.mymod.crashy()'))
+ end)
+
+ it('works in :call', function()
+ command(":call v:lua.mymod.noisy('command')")
+ eq("hey command", meths.get_current_line())
+ eq("Vim(call):E5108: Error executing lua [string \"<nvim>\"]:10: attempt to call global 'nonexistent' (a nil value)",
+ pcall_err(command, 'call v:lua.mymod.crashy()'))
+ end)
+
+ it('works in func options', function()
+ local screen = Screen.new(60, 8)
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {background = Screen.colors.WebGray},
+ [3] = {background = Screen.colors.LightMagenta},
+ [4] = {bold = true},
+ [5] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ })
+ screen:attach()
+ feed('isome st<c-x><c-o>')
+ screen:expect{grid=[[
+ some stuff^ |
+ {1:~ }{2: stuff }{1: }|
+ {1:~ }{3: steam }{1: }|
+ {1:~ }{3: strange things }{1: }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {4:-- Omni completion (^O^N^P) }{5:match 1 of 3} |
+ ]]}
+ end)
+
+ it('throw errors for invalid use', function()
+ eq('Vim(let):E15: Invalid expression: v:lua.func', pcall_err(command, "let g:Func = v:lua.func"))
+ eq('Vim(let):E15: Invalid expression: v:lua', pcall_err(command, "let g:Func = v:lua"))
+ eq("Vim(let):E15: Invalid expression: v:['lua']", pcall_err(command, "let g:Func = v:['lua']"))
+
+ eq("Vim:E15: Invalid expression: v:['lua'].foo()", pcall_err(eval, "v:['lua'].foo()"))
+ eq("Vim(call):E461: Illegal variable name: v:['lua']", pcall_err(command, "call v:['lua'].baar()"))
+
+ eq("Vim(let):E46: Cannot change read-only variable \"v:['lua']\"", pcall_err(command, "let v:['lua'] = 'xx'"))
+ eq("Vim(let):E46: Cannot change read-only variable \"v:lua\"", pcall_err(command, "let v:lua = 'xx'"))
+ end)
+end)
diff --git a/test/functional/lua/overrides_spec.lua b/test/functional/lua/overrides_spec.lua
index f6439001ac..8c260632d9 100644
--- a/test/functional/lua/overrides_spec.lua
+++ b/test/functional/lua/overrides_spec.lua
@@ -54,11 +54,12 @@ describe('print', function()
v_tblout = setmetatable({}, meta_tblout)
]])
eq('', redir_exec('luafile ' .. fname))
- eq('\nE5105: Error while calling lua chunk: E5114: Error while converting print argument #2: [NULL]',
+ -- TODO(bfredl): these look weird, print() should not use "E5114:" style errors..
+ eq('\nE5108: Error executing lua E5114: Error while converting print argument #2: [NULL]',
redir_exec('lua print("foo", v_nilerr, "bar")'))
- eq('\nE5105: Error while calling lua chunk: E5114: Error while converting print argument #2: Xtest-functional-lua-overrides-luafile:2: abc',
+ eq('\nE5108: Error executing lua E5114: Error while converting print argument #2: Xtest-functional-lua-overrides-luafile:2: abc',
redir_exec('lua print("foo", v_abcerr, "bar")'))
- eq('\nE5105: Error while calling lua chunk: E5114: Error while converting print argument #2: <Unknown error: lua_tolstring returned NULL for tostring result>',
+ eq('\nE5108: Error executing lua E5114: Error while converting print argument #2: <Unknown error: lua_tolstring returned NULL for tostring result>',
redir_exec('lua print("foo", v_tblout, "bar")'))
end)
it('prints strings with NULs and NLs correctly', function()
@@ -156,7 +157,8 @@ describe('debug.debug', function()
lua_debug> ^ |
]])
feed('<C-c>')
- screen:expect([[
+ screen:expect{grid=[[
+ {0:~ }|
{0:~ }|
{0:~ }|
{0:~ }|
@@ -167,11 +169,10 @@ describe('debug.debug', function()
lua_debug> print("TEST") |
TEST |
|
- {E:E5105: Error while calling lua chunk: [string "<VimL }|
- {E:compiled string>"]:5: attempt to perform arithmetic o}|
- {E:n local 'a' (a nil value)} |
+ {E:E5108: Error executing lua [string ":lua"]:5: attempt}|
+ {E: to perform arithmetic on local 'a' (a nil value)} |
Interrupt: {cr:Press ENTER or type command to continue}^ |
- ]])
+ ]]}
feed('<C-l>:lua Test()\n')
screen:expect([[
{0:~ }|
@@ -190,7 +191,8 @@ describe('debug.debug', function()
lua_debug> ^ |
]])
feed('\n')
- screen:expect([[
+ screen:expect{grid=[[
+ {0:~ }|
{0:~ }|
{0:~ }|
{0:~ }|
@@ -201,11 +203,10 @@ describe('debug.debug', function()
{0:~ }|
nil |
lua_debug> |
- {E:E5105: Error while calling lua chunk: [string "<VimL }|
- {E:compiled string>"]:5: attempt to perform arithmetic o}|
- {E:n local 'a' (a nil value)} |
+ {E:E5108: Error executing lua [string ":lua"]:5: attempt}|
+ {E: to perform arithmetic on local 'a' (a nil value)} |
{cr:Press ENTER or type command to continue}^ |
- ]])
+ ]]}
end)
it("can be safely exited with 'cont'", function()
diff --git a/test/functional/lua/treesitter_spec.lua b/test/functional/lua/treesitter_spec.lua
index 700e4599f2..5a53ca1425 100644
--- a/test/functional/lua/treesitter_spec.lua
+++ b/test/functional/lua/treesitter_spec.lua
@@ -15,9 +15,7 @@ before_each(clear)
describe('treesitter API', function()
-- error tests not requiring a parser library
it('handles missing language', function()
- local path_pat = 'Error executing lua: '..(iswin() and '.+\\vim\\' or '.+/vim/')
-
- matches(path_pat..'treesitter.lua:39: no such language: borklang',
+ eq('Error executing lua: .../treesitter.lua: no such language: borklang',
pcall_err(exec_lua, "parser = vim.treesitter.create_parser(0, 'borklang')"))
-- actual message depends on platform
diff --git a/test/functional/lua/uri_spec.lua b/test/functional/lua/uri_spec.lua
new file mode 100644
index 0000000000..19b1eb1f61
--- /dev/null
+++ b/test/functional/lua/uri_spec.lua
@@ -0,0 +1,107 @@
+local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
+local exec_lua = helpers.exec_lua
+local eq = helpers.eq
+
+describe('URI methods', function()
+ before_each(function()
+ clear()
+ end)
+
+ describe('file path to uri', function()
+ describe('encode Unix file path', function()
+ it('file path includes only ascii charactors', function()
+ exec_lua("filepath = '/Foo/Bar/Baz.txt'")
+
+ eq('file:///Foo/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+
+ it('file path including white space', function()
+ exec_lua("filepath = '/Foo /Bar/Baz.txt'")
+
+ eq('file:///Foo%20/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+
+ it('file path including Unicode charactors', function()
+ exec_lua("filepath = '/xy/åäö/ɧ/汉语/↥/🤦/🦄/å/بِيَّ.txt'")
+
+ -- The URI encoding should be case-insensitive
+ eq('file:///xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+ end)
+
+ describe('encode Windows filepath', function()
+ it('file path includes only ascii charactors', function()
+ exec_lua([[filepath = 'C:\\Foo\\Bar\\Baz.txt']])
+
+ eq('file:///C:/Foo/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+
+ it('file path including white space', function()
+ exec_lua([[filepath = 'C:\\Foo \\Bar\\Baz.txt']])
+
+ eq('file:///C:/Foo%20/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+
+ it('file path including Unicode charactors', function()
+ exec_lua([[filepath = 'C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\å\\بِيَّ.txt']])
+
+ eq('file:///C:/xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+ end)
+ end)
+
+ describe('uri to filepath', function()
+ describe('decode Unix file path', function()
+ it('file path includes only ascii charactors', function()
+ exec_lua("uri = 'file:///Foo/Bar/Baz.txt'")
+
+ eq('/Foo/Bar/Baz.txt', exec_lua("return vim.uri_to_fname(uri)"))
+ end)
+
+ it('file path including white space', function()
+ exec_lua("uri = 'file:///Foo%20/Bar/Baz.txt'")
+
+ eq('/Foo /Bar/Baz.txt', exec_lua("return vim.uri_to_fname(uri)"))
+ end)
+
+ it('file path including Unicode charactors', function()
+ local test_case = [[
+ local uri = 'file:///xy/%C3%A5%C3%A4%C3%B6/%C9%A7/%E6%B1%89%E8%AF%AD/%E2%86%A5/%F0%9F%A4%A6/%F0%9F%A6%84/a%CC%8A/%D8%A8%D9%90%D9%8A%D9%8E%D9%91.txt'
+ return vim.uri_to_fname(uri)
+ ]]
+
+ eq('/xy/åäö/ɧ/汉语/↥/🤦/🦄/å/بِيَّ.txt', exec_lua(test_case))
+ end)
+ end)
+
+ describe('decode Windows filepath', function()
+ it('file path includes only ascii charactors', function()
+ local test_case = [[
+ local uri = 'file:///C:/Foo/Bar/Baz.txt'
+ return vim.uri_to_fname(uri)
+ ]]
+
+ eq('C:\\Foo\\Bar\\Baz.txt', exec_lua(test_case))
+ end)
+
+ it('file path including white space', function()
+ local test_case = [[
+ local uri = 'file:///C:/Foo%20/Bar/Baz.txt'
+ return vim.uri_to_fname(uri)
+ ]]
+
+ eq('C:\\Foo \\Bar\\Baz.txt', exec_lua(test_case))
+ end)
+
+ it('file path including Unicode charactors', function()
+ local test_case = [[
+ local uri = 'file:///C:/xy/%C3%A5%C3%A4%C3%B6/%C9%A7/%E6%B1%89%E8%AF%AD/%E2%86%A5/%F0%9F%A4%A6/%F0%9F%A6%84/a%CC%8A/%D8%A8%D9%90%D9%8A%D9%8E%D9%91.txt'
+ return vim.uri_to_fname(uri)
+ ]]
+
+ eq('C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\å\\بِيَّ.txt', exec_lua(test_case))
+ end)
+ end)
+ end)
+end)
diff --git a/test/functional/lua/utility_functions_spec.lua b/test/functional/lua/utility_functions_spec.lua
deleted file mode 100644
index ea2b1fc8a9..0000000000
--- a/test/functional/lua/utility_functions_spec.lua
+++ /dev/null
@@ -1,291 +0,0 @@
--- Test suite for testing interactions with API bindings
-local helpers = require('test.functional.helpers')(after_each)
-local Screen = require('test.functional.ui.screen')
-
-local funcs = helpers.funcs
-local clear = helpers.clear
-local eq = helpers.eq
-local eval = helpers.eval
-local feed = helpers.feed
-local pcall_err = helpers.pcall_err
-local exec_lua = helpers.exec_lua
-
-before_each(clear)
-
-describe('lua stdlib', function()
- -- İ: `tolower("İ")` is `i` which has length 1 while `İ` itself has
- -- length 2 (in bytes).
- -- Ⱥ: `tolower("Ⱥ")` is `ⱥ` which has length 2 while `Ⱥ` itself has
- -- length 3 (in bytes).
- --
- -- Note: 'i' !=? 'İ' and 'ⱥ' !=? 'Ⱥ' on some systems.
- -- Note: Built-in Nvim comparison (on systems lacking `strcasecmp`) works
- -- only on ASCII characters.
- it('vim.stricmp', function()
- eq(0, funcs.luaeval('vim.stricmp("a", "A")'))
- eq(0, funcs.luaeval('vim.stricmp("A", "a")'))
- eq(0, funcs.luaeval('vim.stricmp("a", "a")'))
- eq(0, funcs.luaeval('vim.stricmp("A", "A")'))
-
- eq(0, funcs.luaeval('vim.stricmp("", "")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0", "\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0", "\\0\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0", "\\0\\0\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0A", "\\0\\0\\0a")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0A")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0a")'))
-
- eq(0, funcs.luaeval('vim.stricmp("a\\0", "A\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("A\\0", "a\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("a\\0", "a\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("A\\0", "A\\0")'))
-
- eq(0, funcs.luaeval('vim.stricmp("\\0a", "\\0A")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0A", "\\0a")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0a", "\\0a")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0A", "\\0A")'))
-
- eq(0, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0A\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0a\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0a\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0A\\0")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("a", "B")'))
- eq(-1, funcs.luaeval('vim.stricmp("A", "b")'))
- eq(-1, funcs.luaeval('vim.stricmp("a", "b")'))
- eq(-1, funcs.luaeval('vim.stricmp("A", "B")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("", "\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0", "\\0\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0\\0", "\\0\\0\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0A", "\\0\\0\\0b")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0B")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0b")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("a\\0", "B\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("A\\0", "b\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("a\\0", "b\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("A\\0", "B\\0")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("\\0a", "\\0B")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0A", "\\0b")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0a", "\\0b")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0A", "\\0B")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0B\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0b\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0b\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0B\\0")'))
-
- eq(1, funcs.luaeval('vim.stricmp("c", "B")'))
- eq(1, funcs.luaeval('vim.stricmp("C", "b")'))
- eq(1, funcs.luaeval('vim.stricmp("c", "b")'))
- eq(1, funcs.luaeval('vim.stricmp("C", "B")'))
-
- eq(1, funcs.luaeval('vim.stricmp("\\0", "")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0", "\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0", "\\0\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0\\0", "\\0\\0\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0C", "\\0\\0\\0b")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0c", "\\0\\0\\0B")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0c", "\\0\\0\\0b")'))
-
- eq(1, funcs.luaeval('vim.stricmp("c\\0", "B\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("C\\0", "b\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("c\\0", "b\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("C\\0", "B\\0")'))
-
- eq(1, funcs.luaeval('vim.stricmp("c\\0", "B")'))
- eq(1, funcs.luaeval('vim.stricmp("C\\0", "b")'))
- eq(1, funcs.luaeval('vim.stricmp("c\\0", "b")'))
- eq(1, funcs.luaeval('vim.stricmp("C\\0", "B")'))
-
- eq(1, funcs.luaeval('vim.stricmp("\\0c", "\\0B")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0C", "\\0b")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0c", "\\0b")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0C", "\\0B")'))
-
- eq(1, funcs.luaeval('vim.stricmp("\\0c\\0", "\\0B\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0C\\0", "\\0b\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0c\\0", "\\0b\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0C\\0", "\\0B\\0")'))
- end)
-
- it("vim.str_utfindex/str_byteindex", function()
- exec_lua([[_G.test_text = "xy åäö ɧ 汉语 ↥ 🤦x🦄 å بِيَّ"]])
- local indicies32 = {[0]=0,1,2,3,5,7,9,10,12,13,16,19,20,23,24,28,29,33,34,35,37,38,40,42,44,46,48}
- local indicies16 = {[0]=0,1,2,3,5,7,9,10,12,13,16,19,20,23,24,28,28,29,33,33,34,35,37,38,40,42,44,46,48}
- for i,k in pairs(indicies32) do
- eq(k, exec_lua("return vim.str_byteindex(_G.test_text, ...)", i), i)
- end
- for i,k in pairs(indicies16) do
- eq(k, exec_lua("return vim.str_byteindex(_G.test_text, ..., true)", i), i)
- end
- local i32, i16 = 0, 0
- for k = 0,48 do
- if indicies32[i32] < k then
- i32 = i32 + 1
- end
- if indicies16[i16] < k then
- i16 = i16 + 1
- if indicies16[i16+1] == indicies16[i16] then
- i16 = i16 + 1
- end
- end
- eq({i32, i16}, exec_lua("return {vim.str_utfindex(_G.test_text, ...)}", k), k)
- end
- end)
-
- it("vim.schedule", function()
- exec_lua([[
- test_table = {}
- vim.schedule(function()
- table.insert(test_table, "xx")
- end)
- table.insert(test_table, "yy")
- ]])
- eq({"yy","xx"}, exec_lua("return test_table"))
-
- -- type checked args
- eq('Error executing lua: vim.schedule: expected function',
- pcall_err(exec_lua, "vim.schedule('stringly')"))
-
- eq('Error executing lua: vim.schedule: expected function',
- pcall_err(exec_lua, "vim.schedule()"))
-
- exec_lua([[
- vim.schedule(function()
- error("big failure\nvery async")
- end)
- ]])
-
- feed("<cr>")
- eq('Error executing vim.schedule lua callback: [string "<nvim>"]:2: big failure\nvery async', eval("v:errmsg"))
-
- local screen = Screen.new(60,5)
- screen:set_default_attr_ids({
- [1] = {bold = true, foreground = Screen.colors.Blue1},
- [2] = {bold = true, reverse = true},
- [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
- [4] = {bold = true, foreground = Screen.colors.SeaGreen4},
- })
- screen:attach()
- screen:expect{grid=[[
- ^ |
- {1:~ }|
- {1:~ }|
- {1:~ }|
- |
- ]]}
-
- -- nvim_command causes a vimL exception, check that it is properly caught
- -- and propagated as an error message in async contexts.. #10809
- exec_lua([[
- vim.schedule(function()
- vim.api.nvim_command(":echo 'err")
- end)
- ]])
- screen:expect{grid=[[
- |
- {2: }|
- {3:Error executing vim.schedule lua callback: [string "<nvim>"]}|
- {3::2: Vim(echo):E115: Missing quote: 'err} |
- {4:Press ENTER or type command to continue}^ |
- ]]}
- end)
-
- it("vim.split", function()
- local split = function(str, sep)
- return exec_lua('return vim.split(...)', str, sep)
- end
-
- local tests = {
- { "a,b", ",", false, { 'a', 'b' } },
- { ":aa::bb:", ":", false, { '', 'aa', '', 'bb', '' } },
- { "::ee::ff:", ":", false, { '', '', 'ee', '', 'ff', '' } },
- { "ab", ".", false, { '', '', '' } },
- { "a1b2c", "[0-9]", false, { 'a', 'b', 'c' } },
- { "xy", "", false, { 'x', 'y' } },
- { "here be dragons", " ", false, { "here", "be", "dragons"} },
- { "axaby", "ab?", false, { '', 'x', 'y' } },
- { "f v2v v3v w2w ", "([vw])2%1", false, { 'f ', ' v3v ', ' ' } },
- { "x*yz*oo*l", "*", true, { 'x', 'yz', 'oo', 'l' } },
- }
-
- for _, t in ipairs(tests) do
- eq(t[4], split(t[1], t[2], t[3]))
- end
-
- local loops = {
- { "abc", ".-" },
- }
-
- for _, t in ipairs(loops) do
- local status, err = pcall(split, t[1], t[2])
- eq(false, status)
- assert(string.match(err, "Infinite loop detected"))
- end
- end)
-
- it('vim.trim', function()
- local trim = function(s)
- return exec_lua('return vim.trim(...)', s)
- end
-
- local trims = {
- { " a", "a" },
- { " b ", "b" },
- { "\tc" , "c" },
- { "r\n", "r" },
- }
-
- for _, t in ipairs(trims) do
- assert(t[2], trim(t[1]))
- end
-
- local status, err = pcall(trim, 2)
- eq(false, status)
- assert(string.match(err, "Only strings can be trimmed"))
- end)
-
- it('vim.inspect', function()
- -- just make sure it basically works, it has its own test suite
- local inspect = function(t, opts)
- return exec_lua('return vim.inspect(...)', t, opts)
- end
-
- eq('2', inspect(2))
- eq('{+a = {+b = 1+}+}',
- inspect({ a = { b = 1 } }, { newline = '+', indent = '' }))
-
- -- special value vim.inspect.KEY works
- eq('{ KEY_a = "x", KEY_b = "y"}', exec_lua([[
- return vim.inspect({a="x", b="y"}, {newline = '', process = function(item, path)
- if path[#path] == vim.inspect.KEY then
- return 'KEY_'..item
- end
- return item
- end})
- ]]))
- end)
-
- it("vim.deepcopy", function()
- local is_dc = exec_lua([[
- local a = { x = { 1, 2 }, y = 5}
- local b = vim.deepcopy(a)
-
- local count = 0
- for _ in pairs(b) do count = count + 1 end
-
- return b.x[1] == 1 and b.x[2] == 2 and b.y == 5 and count == 2
- and tostring(a) ~= tostring(b)
- ]])
-
- assert(is_dc)
- end)
-
- it('vim.pesc', function()
- eq('foo%-bar', exec_lua([[return vim.pesc('foo-bar')]]))
- eq('foo%%%-bar', exec_lua([[return vim.pesc(vim.pesc('foo-bar'))]]))
- end)
-end)
diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua
new file mode 100644
index 0000000000..028f2dcd52
--- /dev/null
+++ b/test/functional/lua/vim_spec.lua
@@ -0,0 +1,552 @@
+-- Test suite for testing interactions with API bindings
+local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
+
+local funcs = helpers.funcs
+local meths = helpers.meths
+local command = helpers.command
+local clear = helpers.clear
+local eq = helpers.eq
+local eval = helpers.eval
+local feed = helpers.feed
+local pcall_err = helpers.pcall_err
+local exec_lua = helpers.exec_lua
+local matches = helpers.matches
+local source = helpers.source
+local NIL = helpers.NIL
+local retry = helpers.retry
+
+before_each(clear)
+
+describe('lua stdlib', function()
+ -- İ: `tolower("İ")` is `i` which has length 1 while `İ` itself has
+ -- length 2 (in bytes).
+ -- Ⱥ: `tolower("Ⱥ")` is `ⱥ` which has length 2 while `Ⱥ` itself has
+ -- length 3 (in bytes).
+ --
+ -- Note: 'i' !=? 'İ' and 'ⱥ' !=? 'Ⱥ' on some systems.
+ -- Note: Built-in Nvim comparison (on systems lacking `strcasecmp`) works
+ -- only on ASCII characters.
+ it('vim.stricmp', function()
+ eq(0, funcs.luaeval('vim.stricmp("a", "A")'))
+ eq(0, funcs.luaeval('vim.stricmp("A", "a")'))
+ eq(0, funcs.luaeval('vim.stricmp("a", "a")'))
+ eq(0, funcs.luaeval('vim.stricmp("A", "A")'))
+
+ eq(0, funcs.luaeval('vim.stricmp("", "")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0", "\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0", "\\0\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0", "\\0\\0\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0A", "\\0\\0\\0a")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0A")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0a")'))
+
+ eq(0, funcs.luaeval('vim.stricmp("a\\0", "A\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("A\\0", "a\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("a\\0", "a\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("A\\0", "A\\0")'))
+
+ eq(0, funcs.luaeval('vim.stricmp("\\0a", "\\0A")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0A", "\\0a")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0a", "\\0a")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0A", "\\0A")'))
+
+ eq(0, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0A\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0a\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0a\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0A\\0")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("a", "B")'))
+ eq(-1, funcs.luaeval('vim.stricmp("A", "b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("a", "b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("A", "B")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("", "\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0", "\\0\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0\\0", "\\0\\0\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0A", "\\0\\0\\0b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0B")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0b")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("a\\0", "B\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("A\\0", "b\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("a\\0", "b\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("A\\0", "B\\0")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("\\0a", "\\0B")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0A", "\\0b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0a", "\\0b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0A", "\\0B")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0B\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0b\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0b\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0B\\0")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("c", "B")'))
+ eq(1, funcs.luaeval('vim.stricmp("C", "b")'))
+ eq(1, funcs.luaeval('vim.stricmp("c", "b")'))
+ eq(1, funcs.luaeval('vim.stricmp("C", "B")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("\\0", "")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0", "\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0", "\\0\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0\\0", "\\0\\0\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0C", "\\0\\0\\0b")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0c", "\\0\\0\\0B")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0c", "\\0\\0\\0b")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("c\\0", "B\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("C\\0", "b\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("c\\0", "b\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("C\\0", "B\\0")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("c\\0", "B")'))
+ eq(1, funcs.luaeval('vim.stricmp("C\\0", "b")'))
+ eq(1, funcs.luaeval('vim.stricmp("c\\0", "b")'))
+ eq(1, funcs.luaeval('vim.stricmp("C\\0", "B")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("\\0c", "\\0B")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0C", "\\0b")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0c", "\\0b")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0C", "\\0B")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("\\0c\\0", "\\0B\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0C\\0", "\\0b\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0c\\0", "\\0b\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0C\\0", "\\0B\\0")'))
+ end)
+
+ it("vim.str_utfindex/str_byteindex", function()
+ exec_lua([[_G.test_text = "xy åäö ɧ 汉语 ↥ 🤦x🦄 å بِيَّ"]])
+ local indicies32 = {[0]=0,1,2,3,5,7,9,10,12,13,16,19,20,23,24,28,29,33,34,35,37,38,40,42,44,46,48}
+ local indicies16 = {[0]=0,1,2,3,5,7,9,10,12,13,16,19,20,23,24,28,28,29,33,33,34,35,37,38,40,42,44,46,48}
+ for i,k in pairs(indicies32) do
+ eq(k, exec_lua("return vim.str_byteindex(_G.test_text, ...)", i), i)
+ end
+ for i,k in pairs(indicies16) do
+ eq(k, exec_lua("return vim.str_byteindex(_G.test_text, ..., true)", i), i)
+ end
+ local i32, i16 = 0, 0
+ for k = 0,48 do
+ if indicies32[i32] < k then
+ i32 = i32 + 1
+ end
+ if indicies16[i16] < k then
+ i16 = i16 + 1
+ if indicies16[i16+1] == indicies16[i16] then
+ i16 = i16 + 1
+ end
+ end
+ eq({i32, i16}, exec_lua("return {vim.str_utfindex(_G.test_text, ...)}", k), k)
+ end
+ end)
+
+ it("vim.schedule", function()
+ exec_lua([[
+ test_table = {}
+ vim.schedule(function()
+ table.insert(test_table, "xx")
+ end)
+ table.insert(test_table, "yy")
+ ]])
+ eq({"yy","xx"}, exec_lua("return test_table"))
+
+ -- Validates args.
+ eq('Error executing lua: vim.schedule: expected function',
+ pcall_err(exec_lua, "vim.schedule('stringly')"))
+ eq('Error executing lua: vim.schedule: expected function',
+ pcall_err(exec_lua, "vim.schedule()"))
+
+ exec_lua([[
+ vim.schedule(function()
+ error("big failure\nvery async")
+ end)
+ ]])
+
+ feed("<cr>")
+ eq('Error executing vim.schedule lua callback: [string "<nvim>"]:2: big failure\nvery async', eval("v:errmsg"))
+
+ local screen = Screen.new(60,5)
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {bold = true, reverse = true},
+ [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ [4] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ })
+ screen:attach()
+ screen:expect{grid=[[
+ ^ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]]}
+
+ -- nvim_command causes a vimL exception, check that it is properly caught
+ -- and propagated as an error message in async contexts.. #10809
+ exec_lua([[
+ vim.schedule(function()
+ vim.api.nvim_command(":echo 'err")
+ end)
+ ]])
+ screen:expect{grid=[[
+ |
+ {2: }|
+ {3:Error executing vim.schedule lua callback: [string "<nvim>"]}|
+ {3::2: Vim(echo):E115: Missing quote: 'err} |
+ {4:Press ENTER or type command to continue}^ |
+ ]]}
+ end)
+
+ it("vim.split", function()
+ local split = function(str, sep, plain)
+ return exec_lua('return vim.split(...)', str, sep, plain)
+ end
+
+ local tests = {
+ { "a,b", ",", false, { 'a', 'b' } },
+ { ":aa::bb:", ":", false, { '', 'aa', '', 'bb', '' } },
+ { "::ee::ff:", ":", false, { '', '', 'ee', '', 'ff', '' } },
+ { "ab", ".", false, { '', '', '' } },
+ { "a1b2c", "[0-9]", false, { 'a', 'b', 'c' } },
+ { "xy", "", false, { 'x', 'y' } },
+ { "here be dragons", " ", false, { "here", "be", "dragons"} },
+ { "axaby", "ab?", false, { '', 'x', 'y' } },
+ { "f v2v v3v w2w ", "([vw])2%1", false, { 'f ', ' v3v ', ' ' } },
+ { "x*yz*oo*l", "*", true, { 'x', 'yz', 'oo', 'l' } },
+ }
+
+ for _, t in ipairs(tests) do
+ eq(t[4], split(t[1], t[2], t[3]))
+ end
+
+ local loops = {
+ { "abc", ".-" },
+ }
+
+ for _, t in ipairs(loops) do
+ matches(".*Infinite loop detected", pcall_err(split, t[1], t[2]))
+ end
+
+ -- Validates args.
+ eq(true, pcall(split, 'string', 'string'))
+ eq('Error executing lua: .../shared.lua: s: expected string, got number',
+ pcall_err(split, 1, 'string'))
+ eq('Error executing lua: .../shared.lua: sep: expected string, got number',
+ pcall_err(split, 'string', 1))
+ eq('Error executing lua: .../shared.lua: plain: expected boolean, got number',
+ pcall_err(split, 'string', 'string', 1))
+ end)
+
+ it('vim.trim', function()
+ local trim = function(s)
+ return exec_lua('return vim.trim(...)', s)
+ end
+
+ local trims = {
+ { " a", "a" },
+ { " b ", "b" },
+ { "\tc" , "c" },
+ { "r\n", "r" },
+ }
+
+ for _, t in ipairs(trims) do
+ assert(t[2], trim(t[1]))
+ end
+
+ -- Validates args.
+ eq('Error executing lua: .../shared.lua: s: expected string, got number',
+ pcall_err(trim, 2))
+ end)
+
+ it('vim.inspect', function()
+ -- just make sure it basically works, it has its own test suite
+ local inspect = function(t, opts)
+ return exec_lua('return vim.inspect(...)', t, opts)
+ end
+
+ eq('2', inspect(2))
+ eq('{+a = {+b = 1+}+}',
+ inspect({ a = { b = 1 } }, { newline = '+', indent = '' }))
+
+ -- special value vim.inspect.KEY works
+ eq('{ KEY_a = "x", KEY_b = "y"}', exec_lua([[
+ return vim.inspect({a="x", b="y"}, {newline = '', process = function(item, path)
+ if path[#path] == vim.inspect.KEY then
+ return 'KEY_'..item
+ end
+ return item
+ end})
+ ]]))
+ end)
+
+ it("vim.deepcopy", function()
+ local is_dc = exec_lua([[
+ local a = { x = { 1, 2 }, y = 5}
+ local b = vim.deepcopy(a)
+
+ local count = 0
+ for _ in pairs(b) do count = count + 1 end
+
+ return b.x[1] == 1 and b.x[2] == 2 and b.y == 5 and count == 2
+ and tostring(a) ~= tostring(b)
+ ]])
+
+ assert(is_dc)
+ end)
+
+ it('vim.pesc', function()
+ eq('foo%-bar', exec_lua([[return vim.pesc('foo-bar')]]))
+ eq('foo%%%-bar', exec_lua([[return vim.pesc(vim.pesc('foo-bar'))]]))
+
+ -- Validates args.
+ eq('Error executing lua: .../shared.lua: s: expected string, got number',
+ pcall_err(exec_lua, [[return vim.pesc(2)]]))
+ end)
+
+ it('vim.tbl_keys', function()
+ eq({}, exec_lua("return vim.tbl_keys({})"))
+ for _, v in pairs(exec_lua("return vim.tbl_keys({'a', 'b', 'c'})")) do
+ eq(true, exec_lua("return vim.tbl_contains({ 1, 2, 3 }, ...)", v))
+ end
+ for _, v in pairs(exec_lua("return vim.tbl_keys({a=1, b=2, c=3})")) do
+ eq(true, exec_lua("return vim.tbl_contains({ 'a', 'b', 'c' }, ...)", v))
+ end
+ end)
+
+ it('vim.tbl_values', function()
+ eq({}, exec_lua("return vim.tbl_values({})"))
+ for _, v in pairs(exec_lua("return vim.tbl_values({'a', 'b', 'c'})")) do
+ eq(true, exec_lua("return vim.tbl_contains({ 'a', 'b', 'c' }, ...)", v))
+ end
+ for _, v in pairs(exec_lua("return vim.tbl_values({a=1, b=2, c=3})")) do
+ eq(true, exec_lua("return vim.tbl_contains({ 1, 2, 3 }, ...)", v))
+ end
+ end)
+
+ it('vim.tbl_islist', function()
+ eq(NIL, exec_lua("return vim.tbl_islist({})"))
+ eq(true, exec_lua("return vim.tbl_islist({'a', 'b', 'c'})"))
+ eq(false, exec_lua("return vim.tbl_islist({'a', '32', a='hello', b='baz'})"))
+ eq(false, exec_lua("return vim.tbl_islist({1, a='hello', b='baz'})"))
+ eq(false, exec_lua("return vim.tbl_islist({a='hello', b='baz', 1})"))
+ eq(false, exec_lua("return vim.tbl_islist({1, 2, nil, a='hello'})"))
+ end)
+
+ it('vim.tbl_isempty', function()
+ eq(true, exec_lua("return vim.tbl_isempty({})"))
+ eq(false, exec_lua("return vim.tbl_isempty({ 1, 2, 3 })"))
+ eq(false, exec_lua("return vim.tbl_isempty({a=1, b=2, c=3})"))
+ end)
+
+ it('vim.deep_equal', function()
+ eq(true, exec_lua [[ return vim.deep_equal({a=1}, {a=1}) ]])
+ eq(true, exec_lua [[ return vim.deep_equal({a={b=1}}, {a={b=1}}) ]])
+ eq(true, exec_lua [[ return vim.deep_equal({a={b={nil}}}, {a={b={}}}) ]])
+ eq(true, exec_lua [[ return vim.deep_equal({a=1, [5]=5}, {nil,nil,nil,nil,5,a=1}) ]])
+ eq(false, exec_lua [[ return vim.deep_equal(1, {nil,nil,nil,nil,5,a=1}) ]])
+ eq(false, exec_lua [[ return vim.deep_equal(1, 3) ]])
+ eq(false, exec_lua [[ return vim.deep_equal(nil, 3) ]])
+ eq(false, exec_lua [[ return vim.deep_equal({a=1}, {a=2}) ]])
+ end)
+
+ it('vim.list_extend', function()
+ eq({1,2,3}, exec_lua [[ return vim.list_extend({1}, {2,3}) ]])
+ eq('Error executing lua: .../shared.lua: src must be a table',
+ pcall_err(exec_lua, [[ return vim.list_extend({1}, nil) ]]))
+ eq({1,2}, exec_lua [[ return vim.list_extend({1}, {2;a=1}) ]])
+ eq(true, exec_lua [[ local a = {1} return vim.list_extend(a, {2;a=1}) == a ]])
+ end)
+
+ it('vim.tbl_add_reverse_lookup', function()
+ eq(true, exec_lua [[
+ local a = { A = 1 }
+ vim.tbl_add_reverse_lookup(a)
+ return vim.deep_equal(a, { A = 1; [1] = 'A'; })
+ ]])
+ -- Throw an error for trying to do it twice (run into an existing key)
+ local code = [[
+ local res = {}
+ local a = { A = 1 }
+ vim.tbl_add_reverse_lookup(a)
+ assert(vim.deep_equal(a, { A = 1; [1] = 'A'; }))
+ vim.tbl_add_reverse_lookup(a)
+ ]]
+ matches('Error executing lua: .../shared.lua: The reverse lookup found an existing value for "[1A]" while processing key "[1A]"',
+ pcall_err(exec_lua, code))
+ end)
+
+ it('vim.call, vim.fn', function()
+ eq(true, exec_lua([[return vim.call('sin', 0.0) == 0.0 ]]))
+ eq(true, exec_lua([[return vim.fn.sin(0.0) == 0.0 ]]))
+ -- compat: nvim_call_function uses "special" value for vimL float
+ eq(false, exec_lua([[return vim.api.nvim_call_function('sin', {0.0}) == 0.0 ]]))
+
+ source([[
+ func! FooFunc(test)
+ let g:test = a:test
+ return {}
+ endfunc
+ func! VarArg(...)
+ return a:000
+ endfunc
+ func! Nilly()
+ return [v:null, v:null]
+ endfunc
+ ]])
+ eq(true, exec_lua([[return next(vim.fn.FooFunc(3)) == nil ]]))
+ eq(3, eval("g:test"))
+ -- compat: nvim_call_function uses "special" value for empty dict
+ eq(true, exec_lua([[return next(vim.api.nvim_call_function("FooFunc", {5})) == true ]]))
+ eq(5, eval("g:test"))
+
+ eq({2, "foo", true}, exec_lua([[return vim.fn.VarArg(2, "foo", true)]]))
+
+ eq(true, exec_lua([[
+ local x = vim.fn.Nilly()
+ return #x == 2 and x[1] == vim.NIL and x[2] == vim.NIL
+ ]]))
+ eq({NIL, NIL}, exec_lua([[return vim.fn.Nilly()]]))
+
+ -- error handling
+ eq({false, 'Vim:E714: List required'}, exec_lua([[return {pcall(vim.fn.add, "aa", "bb")}]]))
+ end)
+
+ it('vim.rpcrequest and vim.rpcnotify', function()
+ exec_lua([[
+ chan = vim.fn.jobstart({'cat'}, {rpc=true})
+ vim.rpcrequest(chan, 'nvim_set_current_line', 'meow')
+ ]])
+ eq('meow', meths.get_current_line())
+ command("let x = [3, 'aa', v:true, v:null]")
+ eq(true, exec_lua([[
+ ret = vim.rpcrequest(chan, 'nvim_get_var', 'x')
+ return #ret == 4 and ret[1] == 3 and ret[2] == 'aa' and ret[3] == true and ret[4] == vim.NIL
+ ]]))
+ eq({3, 'aa', true, NIL}, exec_lua([[return ret]]))
+
+ -- error handling
+ eq({false, 'Invalid channel: 23'},
+ exec_lua([[return {pcall(vim.rpcrequest, 23, 'foo')}]]))
+ eq({false, 'Invalid channel: 23'},
+ exec_lua([[return {pcall(vim.rpcnotify, 23, 'foo')}]]))
+
+ eq({false, 'Vim:E121: Undefined variable: foobar'},
+ exec_lua([[return {pcall(vim.rpcrequest, chan, 'nvim_eval', "foobar")}]]))
+
+
+ -- rpcnotify doesn't wait on request
+ eq('meow', exec_lua([[
+ vim.rpcnotify(chan, 'nvim_set_current_line', 'foo')
+ return vim.api.nvim_get_current_line()
+ ]]))
+ retry(10, nil, function()
+ eq('foo', meths.get_current_line())
+ end)
+
+ local screen = Screen.new(50,7)
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {bold = true, reverse = true},
+ [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ [4] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ })
+ screen:attach()
+ exec_lua([[
+ local timer = vim.loop.new_timer()
+ timer:start(20, 0, function ()
+ -- notify ok (executed later when safe)
+ vim.rpcnotify(chan, 'nvim_set_var', 'yy', {3, vim.NIL})
+ -- rpcrequest an error
+ vim.rpcrequest(chan, 'nvim_set_current_line', 'bork')
+ end)
+ ]])
+ screen:expect{grid=[[
+ foo |
+ {1:~ }|
+ {2: }|
+ {3:Error executing luv callback:} |
+ {3:[string "<nvim>"]:6: E5560: rpcrequest must not be}|
+ {3: called in a lua loop callback} |
+ {4:Press ENTER or type command to continue}^ |
+ ]]}
+ feed('<cr>')
+ eq({3, NIL}, meths.get_var('yy'))
+ end)
+
+ it('vim.validate', function()
+ exec_lua("vim.validate{arg1={{}, 'table' }}")
+ exec_lua("vim.validate{arg1={{}, 't' }}")
+ exec_lua("vim.validate{arg1={nil, 't', true }}")
+ exec_lua("vim.validate{arg1={{ foo='foo' }, 't' }}")
+ exec_lua("vim.validate{arg1={{ 'foo' }, 't' }}")
+ exec_lua("vim.validate{arg1={'foo', 'string' }}")
+ exec_lua("vim.validate{arg1={'foo', 's' }}")
+ exec_lua("vim.validate{arg1={'', 's' }}")
+ exec_lua("vim.validate{arg1={nil, 's', true }}")
+ exec_lua("vim.validate{arg1={1, 'number' }}")
+ exec_lua("vim.validate{arg1={1, 'n' }}")
+ exec_lua("vim.validate{arg1={0, 'n' }}")
+ exec_lua("vim.validate{arg1={0.1, 'n' }}")
+ exec_lua("vim.validate{arg1={nil, 'n', true }}")
+ exec_lua("vim.validate{arg1={true, 'boolean' }}")
+ exec_lua("vim.validate{arg1={true, 'b' }}")
+ exec_lua("vim.validate{arg1={false, 'b' }}")
+ exec_lua("vim.validate{arg1={nil, 'b', true }}")
+ exec_lua("vim.validate{arg1={function()end, 'function' }}")
+ exec_lua("vim.validate{arg1={function()end, 'f' }}")
+ exec_lua("vim.validate{arg1={nil, 'f', true }}")
+ exec_lua("vim.validate{arg1={nil, 'nil' }}")
+ exec_lua("vim.validate{arg1={nil, 'nil', true }}")
+ exec_lua("vim.validate{arg1={coroutine.create(function()end), 'thread' }}")
+ exec_lua("vim.validate{arg1={nil, 'thread', true }}")
+ exec_lua("vim.validate{arg1={{}, 't' }, arg2={ 'foo', 's' }}")
+ exec_lua("vim.validate{arg1={2, function(a) return (a % 2) == 0 end, 'even number' }}")
+
+ eq("Error executing lua: .../shared.lua: 1: expected table, got number",
+ pcall_err(exec_lua, "vim.validate{ 1, 'x' }"))
+ eq("Error executing lua: .../shared.lua: invalid type name: x",
+ pcall_err(exec_lua, "vim.validate{ arg1={ 1, 'x' }}"))
+ eq("Error executing lua: .../shared.lua: invalid type name: 1",
+ pcall_err(exec_lua, "vim.validate{ arg1={ 1, 1 }}"))
+ eq("Error executing lua: .../shared.lua: invalid type name: nil",
+ pcall_err(exec_lua, "vim.validate{ arg1={ 1 }}"))
+
+ -- Validated parameters are required by default.
+ eq("Error executing lua: .../shared.lua: arg1: expected string, got nil",
+ pcall_err(exec_lua, "vim.validate{ arg1={ nil, 's' }}"))
+ -- Explicitly required.
+ eq("Error executing lua: .../shared.lua: arg1: expected string, got nil",
+ pcall_err(exec_lua, "vim.validate{ arg1={ nil, 's', false }}"))
+
+ eq("Error executing lua: .../shared.lua: arg1: expected table, got number",
+ pcall_err(exec_lua, "vim.validate{arg1={1, 't'}}"))
+ eq("Error executing lua: .../shared.lua: arg2: expected string, got number",
+ pcall_err(exec_lua, "vim.validate{arg1={{}, 't'}, arg2={1, 's'}}"))
+ eq("Error executing lua: .../shared.lua: arg2: expected string, got nil",
+ pcall_err(exec_lua, "vim.validate{arg1={{}, 't'}, arg2={nil, 's'}}"))
+ eq("Error executing lua: .../shared.lua: arg2: expected string, got nil",
+ pcall_err(exec_lua, "vim.validate{arg1={{}, 't'}, arg2={nil, 's'}}"))
+ eq("Error executing lua: .../shared.lua: arg1: expected even number, got 3",
+ pcall_err(exec_lua, "vim.validate{arg1={3, function(a) return a == 1 end, 'even number'}}"))
+ eq("Error executing lua: .../shared.lua: arg1: expected ?, got 3",
+ pcall_err(exec_lua, "vim.validate{arg1={3, function(a) return a == 1 end}}"))
+ end)
+
+ it('vim.is_callable', function()
+ eq(true, exec_lua("return vim.is_callable(function()end)"))
+ eq(true, exec_lua([[
+ local meta = { __call = function()end }
+ local function new_callable()
+ return setmetatable({}, meta)
+ end
+ local callable = new_callable()
+ return vim.is_callable(callable)
+ ]]))
+
+ eq(false, exec_lua("return vim.is_callable(1)"))
+ eq(false, exec_lua("return vim.is_callable('foo')"))
+ eq(false, exec_lua("return vim.is_callable({})"))
+ end)
+end)
diff --git a/test/functional/plugin/lsp/lsp_spec.lua b/test/functional/plugin/lsp/lsp_spec.lua
new file mode 100644
index 0000000000..cd0974b81c
--- /dev/null
+++ b/test/functional/plugin/lsp/lsp_spec.lua
@@ -0,0 +1,634 @@
+local helpers = require('test.functional.helpers')(after_each)
+
+local clear = helpers.clear
+local exec_lua = helpers.exec_lua
+local eq = helpers.eq
+local NIL = helpers.NIL
+
+-- Use these to get access to a coroutine so that I can run async tests and use
+-- yield.
+local run, stop = helpers.run, helpers.stop
+
+if helpers.pending_win32(pending) then return end
+
+local is_windows = require'luv'.os_uname().sysname == "Windows"
+local lsp_test_rpc_server_file = "test/functional/fixtures/lsp-test-rpc-server.lua"
+if is_windows then
+ lsp_test_rpc_server_file = lsp_test_rpc_server_file:gsub("/", "\\")
+end
+
+local function test_rpc_server_setup(test_name, timeout_ms)
+ exec_lua([=[
+ lsp = require('vim.lsp')
+ local test_name, fixture_filename, timeout = ...
+ TEST_RPC_CLIENT_ID = lsp.start_client {
+ cmd = {
+ vim.api.nvim_get_vvar("progpath"), '-Es', '-u', 'NONE', '--headless',
+ "-c", string.format("lua TEST_NAME = %q", test_name),
+ "-c", string.format("lua TIMEOUT = %d", timeout),
+ "-c", "luafile "..fixture_filename,
+ };
+ callbacks = setmetatable({}, {
+ __index = function(t, method)
+ return function(...)
+ return vim.rpcrequest(1, 'callback', ...)
+ end
+ end;
+ });
+ root_dir = vim.loop.cwd();
+ on_init = function(client, result)
+ TEST_RPC_CLIENT = client
+ vim.rpcrequest(1, "init", result)
+ end;
+ on_exit = function(...)
+ vim.rpcnotify(1, "exit", ...)
+ end;
+ }
+ ]=], test_name, lsp_test_rpc_server_file, timeout_ms or 1e3)
+end
+
+local function test_rpc_server(config)
+ if config.test_name then
+ clear()
+ test_rpc_server_setup(config.test_name, config.timeout_ms or 1e3)
+ end
+ local client = setmetatable({}, {
+ __index = function(_, name)
+ -- Workaround for not being able to yield() inside __index for Lua 5.1 :(
+ -- Otherwise I would just return the value here.
+ return function(...)
+ return exec_lua([=[
+ local name = ...
+ if type(TEST_RPC_CLIENT[name]) == 'function' then
+ return TEST_RPC_CLIENT[name](select(2, ...))
+ else
+ return TEST_RPC_CLIENT[name]
+ end
+ ]=], name, ...)
+ end
+ end;
+ })
+ local code, signal
+ local function on_request(method, args)
+ if method == "init" then
+ if config.on_init then
+ config.on_init(client, unpack(args))
+ end
+ return NIL
+ end
+ if method == 'callback' then
+ if config.on_callback then
+ config.on_callback(unpack(args))
+ end
+ end
+ return NIL
+ end
+ local function on_notify(method, args)
+ if method == 'exit' then
+ code, signal = unpack(args)
+ return stop()
+ end
+ end
+ -- TODO specify timeout?
+ -- run(on_request, on_notify, config.on_setup, 1000)
+ run(on_request, on_notify, config.on_setup)
+ if config.on_exit then
+ config.on_exit(code, signal)
+ end
+ stop()
+ if config.test_name then
+ exec_lua("lsp._vim_exit_handler()")
+ end
+end
+
+describe('Language Client API', function()
+ describe('server_name is specified', function()
+ before_each(function()
+ clear()
+ -- Run an instance of nvim on the file which contains our "scripts".
+ -- Pass TEST_NAME to pick the script.
+ local test_name = "basic_init"
+ exec_lua([=[
+ lsp = require('vim.lsp')
+ local test_name, fixture_filename = ...
+ TEST_RPC_CLIENT_ID = lsp.start_client {
+ cmd = {
+ vim.api.nvim_get_vvar("progpath"), '-Es', '-u', 'NONE', '--headless',
+ "-c", string.format("lua TEST_NAME = %q", test_name),
+ "-c", "luafile "..fixture_filename;
+ };
+ root_dir = vim.loop.cwd();
+ }
+ ]=], test_name, lsp_test_rpc_server_file)
+ end)
+
+ after_each(function()
+ exec_lua("lsp._vim_exit_handler()")
+ -- exec_lua("lsp.stop_all_clients(true)")
+ end)
+
+ describe('start_client and stop_client', function()
+ it('should return true', function()
+ for _ = 1, 20 do
+ helpers.sleep(10)
+ if exec_lua("return #lsp.get_active_clients()") > 0 then
+ break
+ end
+ end
+ eq(1, exec_lua("return #lsp.get_active_clients()"))
+ eq(false, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID) == nil"))
+ eq(false, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID).is_stopped()"))
+ exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID).stop()")
+ eq(false, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID).is_stopped()"))
+ for _ = 1, 20 do
+ helpers.sleep(10)
+ if exec_lua("return #lsp.get_active_clients()") == 0 then
+ break
+ end
+ end
+ eq(true, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID) == nil"))
+ end)
+ end)
+ end)
+
+ describe('basic_init test', function()
+ it('should run correctly', function()
+ local expected_callbacks = {
+ {NIL, "test", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "basic_init";
+ on_init = function(client, _init_result)
+ -- client is a dummy object which will queue up commands to be run
+ -- once the server initializes. It can't accept lua callbacks or
+ -- other types that may be unserializable for now.
+ client.stop()
+ end;
+ -- If the program timed out, then code will be nil.
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ -- Note that NIL must be used here.
+ -- on_callback(err, method, result, client_id)
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...})
+ end;
+ }
+ end)
+
+ it('should fail', function()
+ local expected_callbacks = {
+ {NIL, "test", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "basic_init";
+ on_init = function(client)
+ client.notify('test')
+ client.stop()
+ end;
+ on_exit = function(code, signal)
+ eq(1, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...}, "expected callback")
+ end;
+ }
+ end)
+
+ it('should succeed with manual shutdown', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "test", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "basic_init";
+ on_init = function(client)
+ eq(0, client.resolved_capabilities().text_document_did_change)
+ client.request('shutdown')
+ client.notify('exit')
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...}, "expected callback")
+ end;
+ }
+ end)
+
+ it('should verify capabilities sent', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "basic_check_capabilities";
+ on_init = function(client)
+ client.stop()
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...}, "expected callback")
+ end;
+ }
+ end)
+
+ it('should not send didOpen if the buffer closes before init', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_finish";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ eq(1, exec_lua("return TEST_RPC_CLIENT_ID"))
+ eq(true, exec_lua("return lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)"))
+ eq(true, exec_lua("return lsp.buf_is_attached(BUFFER, TEST_RPC_CLIENT_ID)"))
+ exec_lua [[
+ vim.api.nvim_command(BUFFER.."bwipeout")
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ client.notify('finish')
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body sent attaching before init', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(not lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID), "Shouldn't attach twice")
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body sent attaching after init', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body and didChange full', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ exec_lua [[
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "boop";
+ })
+ ]]
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ -- TODO(askhan) we don't support full for now, so we can disable these tests.
+ pending('should check the body and didChange incremental', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change_incremental";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Incremental")
+ eq(sync_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ exec_lua [[
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "boop";
+ })
+ ]]
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ -- 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 editting', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change_incremental_editting";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Incremental")
+ eq(sync_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ helpers.command("normal! 1Go")
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body and didChange full with 2 changes', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change_multi";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(sync_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ exec_lua [[
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "321";
+ })
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "boop";
+ })
+ ]]
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body and didChange full lifecycle', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change_multi_and_close";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(sync_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ exec_lua [[
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "321";
+ })
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "boop";
+ })
+ vim.api.nvim_command(BUFFER.."bwipeout")
+ ]]
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ end)
+
+ describe("parsing tests", function()
+ it('should handle invalid content-length correctly', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "invalid_header";
+ on_setup = function()
+ end;
+ on_init = function(_client)
+ client = _client
+ client.stop(true)
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ end;
+ }
+ end)
+
+ end)
+end)
diff --git a/test/functional/terminal/highlight_spec.lua b/test/functional/terminal/highlight_spec.lua
index 06a6fd6f2b..8d3f0218af 100644
--- a/test/functional/terminal/highlight_spec.lua
+++ b/test/functional/terminal/highlight_spec.lua
@@ -121,13 +121,12 @@ it(':terminal highlight has lower precedence than editor #9964', function()
local screen = Screen.new(30, 4)
screen:set_default_attr_ids({
-- "Normal" highlight emitted by the child nvim process.
- N_child = {foreground = tonumber('0x4040ff'), background = tonumber('0xffff40')},
- -- "Search" highlight emitted by the child nvim process.
- S_child = {background = tonumber('0xffff40'), italic = true, foreground = tonumber('0x4040ff')},
+ N_child = {foreground = tonumber('0x4040ff'), background = tonumber('0xffff40'), fg_indexed=true, bg_indexed=true},
-- "Search" highlight in the parent nvim process.
S = {background = Screen.colors.Green, italic = true, foreground = Screen.colors.Red},
-- "Question" highlight in the parent nvim process.
- Q = {background = tonumber('0xffff40'), bold = true, foreground = Screen.colors.SeaGreen4},
+ -- note: bg is indexed as it comes from the (cterm) child, while fg isn't as it comes from (rgb) parent
+ Q = {background = tonumber('0xffff40'), bold = true, foreground = Screen.colors.SeaGreen4, bg_indexed=true},
})
screen:attach({rgb=true})
-- Child nvim process in :terminal (with cterm colors).
@@ -160,6 +159,54 @@ it(':terminal highlight has lower precedence than editor #9964', function()
]])
end)
+describe(':terminal highlight forwarding', function()
+ local screen
+
+ before_each(function()
+ clear()
+ screen = Screen.new(50, 7)
+ screen:set_rgb_cterm(true)
+ screen:set_default_attr_ids({
+ [1] = {{reverse = true}, {reverse = true}},
+ [2] = {{bold = true}, {bold = true}},
+ [3] = {{fg_indexed = true, foreground = tonumber('0xe0e000')}, {foreground = 3}},
+ [4] = {{foreground = tonumber('0xff8000')}, {}},
+ })
+ screen:attach()
+ command('enew | call termopen(["'..nvim_dir..'/tty-test"])')
+ feed('i')
+ screen:expect([[
+ tty ready |
+ {1: } |
+ |
+ |
+ |
+ |
+ {2:-- TERMINAL --} |
+ ]])
+ end)
+
+ it('will handle cterm and rgb attributes', function()
+ if helpers.pending_win32(pending) then return end
+ thelpers.set_fg(3)
+ thelpers.feed_data('text')
+ thelpers.feed_termcode('[38:2:255:128:0m')
+ thelpers.feed_data('color')
+ thelpers.clear_attrs()
+ thelpers.feed_data('text')
+ screen:expect{grid=[[
+ tty ready |
+ {3:text}{4:color}text{1: } |
+ |
+ |
+ |
+ |
+ {2:-- TERMINAL --} |
+ ]]}
+ end)
+end)
+
+
describe(':terminal highlight with custom palette', function()
local screen
@@ -167,7 +214,7 @@ describe(':terminal highlight with custom palette', function()
clear()
screen = Screen.new(50, 7)
screen:set_default_attr_ids({
- [1] = {foreground = tonumber('0x123456')},
+ [1] = {foreground = tonumber('0x123456')}, -- no fg_indexed when overriden
[2] = {foreground = 12},
[3] = {bold = true, reverse = true},
[5] = {background = 11},
diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua
index 060f065bfc..1df8df6f6e 100644
--- a/test/functional/terminal/scrollback_spec.lua
+++ b/test/functional/terminal/scrollback_spec.lua
@@ -449,7 +449,7 @@ describe("'scrollback' option", function()
38: line |
39: line |
40: line |
- {IGNORE}|
+ {MATCH:.*}|
{3:-- TERMINAL --} |
]]}
end
diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua
index bc83660c19..676d6ef76d 100644
--- a/test/functional/terminal/tui_spec.lua
+++ b/test/functional/terminal/tui_spec.lua
@@ -302,6 +302,49 @@ describe('TUI', function()
expect_child_buf_lines({''})
end)
+ it('paste: select-mode', function()
+ feed_data('ithis is line 1\nthis is line 2\nline 3 is here\n\027')
+ wait_for_mode('n')
+ screen:expect{grid=[[
+ this is line 1 |
+ this is line 2 |
+ line 3 is here |
+ {1: } |
+ {5:[No Name] [+] }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
+ -- Select-mode. Use <C-n> to move down.
+ feed_data('gg04lgh\14\14')
+ wait_for_mode('s')
+ feed_data('\027[200~')
+ feed_data('just paste it™')
+ feed_data('\027[201~')
+ screen:expect{grid=[[
+ thisjust paste it™{1:3} is here |
+ |
+ {4:~ }|
+ {4:~ }|
+ {5:[No Name] [+] }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
+ -- Undo.
+ feed_data('u')
+ expect_child_buf_lines{
+ 'this is line 1',
+ 'this is line 2',
+ 'line 3 is here',
+ '',
+ }
+ -- Redo.
+ feed_data('\18') -- <C-r>
+ expect_child_buf_lines{
+ 'thisjust paste it™3 is here',
+ '',
+ }
+ end)
+
it('paste: terminal mode', function()
feed_data(':set statusline=^^^^^^^\n')
feed_data(':terminal '..nvim_dir..'/tty-test\n')
@@ -535,7 +578,7 @@ describe('TUI', function()
|
{4:~ }|
{5: }|
- {8:paste: Error executing lua: vim.lua:197: Vim:E21: }|
+ {MATCH:paste: Error executing lua: vim.lua:%d+: Vim:E21: }|
{8:Cannot make changes, 'modifiable' is off} |
{10:Press ENTER or type command to continue}{1: } |
{3:-- TERMINAL --} |
@@ -680,11 +723,11 @@ describe('TUI', function()
screen:set_option('rgb', true)
screen:set_default_attr_ids({
[1] = {reverse = true},
- [2] = {foreground = tonumber('0x4040ff')},
+ [2] = {foreground = tonumber('0x4040ff'), fg_indexed=true},
[3] = {bold = true, reverse = true},
[4] = {bold = true},
- [5] = {reverse = true, foreground = tonumber('0xe0e000')},
- [6] = {foreground = tonumber('0xe0e000')},
+ [5] = {reverse = true, foreground = tonumber('0xe0e000'), fg_indexed=true},
+ [6] = {foreground = tonumber('0xe0e000'), fg_indexed=true},
[7] = {reverse = true, foreground = Screen.colors.SeaGreen4},
[8] = {foreground = Screen.colors.SeaGreen4},
[9] = {bold = true, foreground = Screen.colors.Blue1},
@@ -728,6 +771,54 @@ describe('TUI', function()
]])
end)
+ it('forwards :term palette colors with termguicolors', function()
+ screen:set_rgb_cterm(true)
+ screen:set_default_attr_ids({
+ [1] = {{reverse = true}, {reverse = true}},
+ [2] = {{bold = true, reverse = true}, {bold = true, reverse = true}},
+ [3] = {{bold = true}, {bold = true}},
+ [4] = {{fg_indexed = true, foreground = tonumber('0xe0e000')}, {foreground = 3}},
+ [5] = {{foreground = tonumber('0xff8000')}, {}},
+ })
+
+ feed_data(':set statusline=^^^^^^^\n')
+ feed_data(':set termguicolors\n')
+ feed_data(':terminal '..nvim_dir..'/tty-test\n')
+ -- Depending on platform the above might or might not fit in the cmdline
+ -- so clear it for consistent behavior.
+ feed_data(':\027')
+ screen:expect{grid=[[
+ {1:t}ty ready |
+ |
+ |
+ |
+ {2:^^^^^^^ }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
+ feed_data(':call chansend(&channel, "\\033[38;5;3mtext\\033[38:2:255:128:0mcolor\\033[0;10mtext")\n')
+ screen:expect{grid=[[
+ {1:t}ty ready |
+ {4:text}{5:color}text |
+ |
+ |
+ {2:^^^^^^^ }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
+
+ feed_data(':set notermguicolors\n')
+ screen:expect{grid=[[
+ {1:t}ty ready |
+ {4:text}colortext |
+ |
+ |
+ {2:^^^^^^^ }|
+ :set notermguicolors |
+ {3:-- TERMINAL --} |
+ ]]}
+ end)
+
it('is included in nvim_list_uis()', function()
feed_data(':echo map(nvim_list_uis(), {k,v -> sort(items(filter(v, {k,v -> k[:3] !=# "ext_" })))})\r')
screen:expect([=[
diff --git a/test/functional/ui/bufhl_spec.lua b/test/functional/ui/bufhl_spec.lua
index 5df909f79c..65c5f67726 100644
--- a/test/functional/ui/bufhl_spec.lua
+++ b/test/functional/ui/bufhl_spec.lua
@@ -386,6 +386,22 @@ describe('Buffer highlighting', function()
]])
end)
+ it('can be retrieved', function()
+ local get_virtual_text = curbufmeths.get_virtual_text
+ local line_count = curbufmeths.line_count
+
+ local s1 = {{'Köttbullar', 'Comment'}, {'Kräuterbutter'}}
+ local s2 = {{'こんにちは', 'Comment'}}
+
+ set_virtual_text(-1, 0, s1, {})
+ eq(s1, get_virtual_text(0))
+
+ set_virtual_text(-1, line_count(), s2, {})
+ eq(s2, get_virtual_text(line_count()))
+
+ eq({}, get_virtual_text(line_count() + 9000))
+ end)
+
it('is not highlighted by visual selection', function()
feed("ggVG")
screen:expect([[
diff --git a/test/functional/ui/hlstate_spec.lua b/test/functional/ui/hlstate_spec.lua
index 1e18df835a..2a567b28ee 100644
--- a/test/functional/ui/hlstate_spec.lua
+++ b/test/functional/ui/hlstate_spec.lua
@@ -181,11 +181,11 @@ describe('ext_hlstate detailed highlights', function()
it("work with :terminal", function()
screen:set_default_attr_ids({
[1] = {{}, {{hi_name = "TermCursorNC", ui_name = "TermCursorNC", kind = "ui"}}},
- [2] = {{foreground = 52479}, {{kind = "term"}}},
- [3] = {{bold = true, foreground = 52479}, {{kind = "term"}}},
- [4] = {{foreground = 52479}, {2, 1}},
- [5] = {{foreground = 4259839}, {{kind = "term"}}},
- [6] = {{foreground = 4259839}, {5, 1}},
+ [2] = {{foreground = tonumber('0x00ccff'), fg_indexed=true}, {{kind = "term"}}},
+ [3] = {{bold = true, foreground = tonumber('0x00ccff'), fg_indexed=true}, {{kind = "term"}}},
+ [4] = {{foreground = tonumber('0x00ccff'), fg_indexed=true}, {2, 1}},
+ [5] = {{foreground = tonumber('0x40ffff'), fg_indexed=true}, {{kind = "term"}}},
+ [6] = {{foreground = tonumber('0x40ffff'), fg_indexed=true}, {5, 1}},
[7] = {{}, {{hi_name = "MsgArea", ui_name = "MsgArea", kind = "ui"}}},
})
command('enew | call termopen(["'..nvim_dir..'/tty-test"])')
diff --git a/test/functional/ui/inccommand_spec.lua b/test/functional/ui/inccommand_spec.lua
index d60cd08fb0..b841574643 100644
--- a/test/functional/ui/inccommand_spec.lua
+++ b/test/functional/ui/inccommand_spec.lua
@@ -88,14 +88,14 @@ local function common_setup(screen, inccommand, text)
})
end
- command("set inccommand=" .. (inccommand and inccommand or ""))
+ command("set inccommand=" .. (inccommand or ""))
if text then
insert(text)
end
end
-describe(":substitute, inccommand=split", function()
+describe(":substitute, inccommand=split interactivity", function()
before_each(function()
clear()
common_setup(nil, "split", default_text)
@@ -779,6 +779,59 @@ describe(":substitute, inccommand=split", function()
{15:~ }|
:silent tabedit %s/tw/to^ |
]])
+ feed('<Esc>')
+
+ -- leading colons
+ feed(':::%s/tw/to')
+ screen:expect{any=[[{12:to}o lines]]}
+ feed('<Esc>')
+ screen:expect{any=[[two lines]]}
+ end)
+
+ it("ignores new-window modifiers when splitting the preview window", function()
+ -- one modifier
+ feed(':topleft %s/tw/to')
+ screen:expect([[
+ Inc substitution on |
+ {12:to}o lines |
+ Inc substitution on |
+ {12:to}o lines |
+ |
+ {11:[No Name] [+] }|
+ |2| {12:to}o lines |
+ |4| {12:to}o lines |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {10:[Preview] }|
+ :topleft %s/tw/to^ |
+ ]])
+ feed('<Esc>')
+ screen:expect{any=[[two lines]]}
+
+ -- multiple modifiers
+ feed(':topleft vert %s/tw/to')
+ screen:expect([[
+ Inc substitution on |
+ {12:to}o lines |
+ Inc substitution on |
+ {12:to}o lines |
+ |
+ {11:[No Name] [+] }|
+ |2| {12:to}o lines |
+ |4| {12:to}o lines |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {10:[Preview] }|
+ :topleft vert %s/tw/to^ |
+ ]])
+ feed('<Esc>')
+ screen:expect{any=[[two lines]]}
end)
it('shows split window when typing the pattern', function()
@@ -2529,6 +2582,49 @@ describe(":substitute", function()
:%s/some\(thing\)\@!/every/^ |
]])
end)
+
+ it("doesn't prompt to swap cmd range", function()
+ screen = Screen.new(50, 8) -- wide to avoid hit-enter prompt
+ common_setup(screen, "split", default_text)
+ feed(':2,1s/tw/MO/g')
+
+ -- substitution preview should have been made, without prompting
+ screen:expect([[
+ {12:MO}o lines |
+ {11:[No Name] [+] }|
+ |2| {12:MO}o lines |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {10:[Preview] }|
+ :2,1s/tw/MO/g^ |
+ ]])
+
+ -- but should be prompted on hitting enter
+ feed('<CR>')
+ screen:expect([[
+ {12:MO}o lines |
+ {11:[No Name] [+] }|
+ |2| {12:MO}o lines |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {10:[Preview] }|
+ {13:Backwards range given, OK to swap (y/n)?}^ |
+ ]])
+
+ feed('y')
+ screen:expect([[
+ Inc substitution on |
+ ^MOo lines |
+ |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {13:Backwards range given, OK to swap (y/n)?}y |
+ ]])
+ end)
end)
it(':substitute with inccommand during :terminal activity', function()
diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua
index d16559bab2..40ea030f73 100644
--- a/test/functional/ui/messages_spec.lua
+++ b/test/functional/ui/messages_spec.lua
@@ -122,7 +122,7 @@ describe('ui/ext_messages', function()
feed('G$x')
screen:expect{grid=[[
line 1 |
- {IGNORE}|
+ {MATCH:.*}|
{1:~ }|
{1:~ }|
{1:~ }|
@@ -747,7 +747,7 @@ describe('ui/ext_messages', function()
{1:~ }|
{1:~ }|
]], messages={{
- content = {{'E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: such\nmultiline\nerror', 2}},
+ content = {{'E5108: Error executing lua [string ":lua"]:1: such\nmultiline\nerror', 2}},
kind = "lua_error"
}}}
end)
@@ -966,7 +966,7 @@ describe('ui/ext_messages', function()
{1:~ }|
{1:~ }|
{1:~ }|
- {IGNORE}|
+ {MATCH:.*}|
{1:~ }|
{1:~ }Nvim is open source and freely distributable{1: }|
{1:~ }https://neovim.io/#chat{1: }|
@@ -976,8 +976,8 @@ describe('ui/ext_messages', function()
{1:~ }type :q{5:<Enter>} to exit {1: }|
{1:~ }type :help{5:<Enter>} for help {1: }|
{1:~ }|
- {IGNORE}|
- {IGNORE}|
+ {MATCH:.*}|
+ {MATCH:.*}|
{1:~ }|
{1:~ }|
{1:~ }|
@@ -1022,7 +1022,7 @@ describe('ui/ext_messages', function()
|
|
|
- {IGNORE}|
+ {MATCH:.*}|
|
Nvim is open source and freely distributable |
https://neovim.io/#chat |
@@ -1032,8 +1032,8 @@ describe('ui/ext_messages', function()
type :q{5:<Enter>} to exit |
type :help{5:<Enter>} for help |
|
- {IGNORE}|
- {IGNORE}|
+ {MATCH:.*}|
+ {MATCH:.*}|
|
|
|
@@ -1146,97 +1146,96 @@ aliquip ex ea commodo consequat.]])
it('handles wrapped lines with line scroll', function()
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
feed('j')
screen:expect{grid=[[
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
{2:a aliqua.} |
+ {2:Ut enim ad minim veniam, quis nostr}|
{4:-- More --}^ |
]]}
feed('k')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
feed('j')
screen:expect{grid=[[
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
{2:a aliqua.} |
+ {2:Ut enim ad minim veniam, quis nostr}|
{4:-- More --}^ |
]]}
-
end)
it('handles wrapped lines with page scroll', function()
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
feed('d')
screen:expect{grid=[[
- {2:adipisicing elit, sed do eiusmod te}|
- {2:mpor} |
{2:incididunt ut labore et dolore magn}|
{2:a aliqua.} |
{2:Ut enim ad minim veniam, quis nostr}|
{2:ud xercitation} |
{2:ullamco laboris nisi ut} |
- {4:-- More --}^ |
+ {2:aliquip ex ea commodo consequat.} |
+ {4:Press ENTER or type command to cont}|
+ {4:inue}^ |
]]}
feed('u')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
feed('d')
screen:expect{grid=[[
- {2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
{2:a aliqua.} |
{2:Ut enim ad minim veniam, quis nostr}|
{2:ud xercitation} |
{2:ullamco laboris nisi ut} |
+ {2:aliquip ex ea commodo consequat.} |
{4:-- More --}^ |
]]}
end)
@@ -1246,49 +1245,49 @@ aliquip ex ea commodo consequat.]])
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {3:E5105: Error while calling lua chun}|
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:E5108: Error executing lua [string }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
+ {3:a aliqua.}{5: }|
{6:-- More --}{5:^ }|
]]}
feed('j')
screen:expect{grid=[[
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
{3:a aliqua.}{5: }|
+ {3:Ut enim ad minim veniam, quis nostr}|
{6:-- More --}{5:^ }|
]]}
feed('k')
screen:expect{grid=[[
- {3:E5105: Error while calling lua chun}|
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:E5108: Error executing lua [string }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
+ {3:a aliqua.}{5: }|
{6:-- More --}{5:^ }|
]]}
feed('j')
screen:expect{grid=[[
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
{3:a aliqua.}{5: }|
+ {3:Ut enim ad minim veniam, quis nostr}|
{6:-- More --}{5:^ }|
]]}
end)
@@ -1297,46 +1296,46 @@ aliquip ex ea commodo consequat.]])
command("hi MsgArea guisp=Yellow")
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {3:E5105: Error while calling lua chun}|
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:E5108: Error executing lua [string }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
+ {3:a aliqua.}{5: }|
{6:-- More --}{5:^ }|
]]}
feed('d')
screen:expect{grid=[[
- {3:adipisicing elit, sed do eiusmod te}|
- {3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
{3:a aliqua.}{5: }|
{3:Ut enim ad minim veniam, quis nostr}|
{3:ud xercitation}{5: }|
{3:ullamco laboris nisi ut}{5: }|
- {6:-- More --}{5:^ }|
+ {3:aliquip ex ea commodo consequat.}{5: }|
+ {6:Press ENTER or type command to cont}|
+ {6:inue}{5:^ }|
]]}
feed('u')
screen:expect{grid=[[
- {3:E5105: Error while calling lua chun}|
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:E5108: Error executing lua [string }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
+ {3:a aliqua.}{5: }|
{6:-- More --}{5:^ }|
]]}
feed('d')
screen:expect{grid=[[
- {3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
{3:a aliqua.}{5: }|
{3:Ut enim ad minim veniam, quis nostr}|
{3:ud xercitation}{5: }|
{3:ullamco laboris nisi ut}{5: }|
+ {3:aliquip ex ea commodo consequat.}{5: }|
{6:-- More --}{5:^ }|
]]}
end)
@@ -1473,23 +1472,23 @@ aliquip ex ea commodo consequat.]])
it('can be resized', function()
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
-- responds to resize, but text is not reflown
screen:try_resize(45, 5)
screen:expect{grid=[[
- {2:nsectetur} |
{2:adipisicing elit, sed do eiusmod te} |
{2:mpor} |
{2:incididunt ut labore et dolore magn} |
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
@@ -1497,14 +1496,14 @@ aliquip ex ea commodo consequat.]])
-- text is not reflown; existing lines get cut
screen:try_resize(30, 12)
screen:expect{grid=[[
- {2:E5105: Error while calling lua}|
- {2:k: [string "<VimL compiled str}|
- {2:]:1: Lorem ipsum dolor sit ame}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [st}|
+ {2:":lua"]:1: Lorem ipsum dolor s}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusm}|
{2:mpore} |
{2:incididunt ut labore et dolore}|
- {2: magn} |
+ {2:a aliqua.} |
+ |
|
|
|
@@ -1515,18 +1514,18 @@ aliquip ex ea commodo consequat.]])
-- wrapped at the new screen size.
feed('<cr>')
screen:expect{grid=[[
- {2:k: [string "<VimL compiled str}|
- {2:]:1: Lorem ipsum dolor sit ame}|
- {2:nsectetur} |
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusm}|
{2:mpore} |
{2:incididunt ut labore et dolore}|
- {2: magna aliqua.} |
+ {2:a aliqua.} |
{2:Ut enim ad minim veniam, quis }|
{2:nostrud xercitation} |
{2:ullamco laboris nisi ut} |
{2:aliquip ex ea commodo consequa}|
- {4:-- More --}^ |
+ {2:t.} |
+ {4:Press ENTER or type command to}|
+ {4: continue}^ |
]]}
feed('q')
diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua
index b57e13fea1..d3f78bf77b 100644
--- a/test/functional/ui/screen.lua
+++ b/test/functional/ui/screen.lua
@@ -269,7 +269,7 @@ local ext_keys = {
-- grid: Expected screen state (string). Each line represents a screen
-- row. Last character of each row (typically "|") is stripped.
-- Common indentation is stripped.
--- Lines containing only "{IGNORE}|" are skipped.
+-- "{MATCH:x}|" lines are matched against Lua pattern `x`.
-- attr_ids: Expected text attributes. Screen rows are transformed according
-- to this table, as follows: each substring S composed of
-- characters having the same attributes will be substituted by
@@ -390,9 +390,10 @@ function Screen:expect(expected, attr_ids, ...)
err_msg = "Expected screen height " .. #expected_rows
.. ' differs from actual height ' .. #actual_rows .. '.'
end
- for i = 1, #expected_rows do
- msg_expected_rows[i] = expected_rows[i]
- if expected_rows[i] ~= actual_rows[i] and expected_rows[i] ~= "{IGNORE}|" then
+ for i, row in ipairs(expected_rows) do
+ msg_expected_rows[i] = row
+ local m = (row ~= actual_rows[i] and row:match('{MATCH:(.*)}') or nil)
+ if row ~= actual_rows[i] and (not m or not actual_rows[i]:match(m)) then
msg_expected_rows[i] = '*' .. msg_expected_rows[i]
if i <= #actual_rows then
actual_rows[i] = '*' .. actual_rows[i]
@@ -1362,6 +1363,7 @@ function Screen:linegrid_check_attrs(attrs)
if self._rgb_cterm then
attr_rgb, attr_cterm, info = unpack(v)
attr = {attr_rgb, attr_cterm}
+ info = info or {}
elseif self._options.ext_hlstate then
attr, info = unpack(v)
else
@@ -1400,11 +1402,12 @@ end
function Screen:_pprint_hlitem(item)
-- print(inspect(item))
local multi = self._rgb_cterm or self._options.ext_hlstate
- local attrdict = "{"..self:_pprint_attrs(multi and item[1] or item).."}"
+ local cterm = (not self._rgb_cterm and not self._options.rgb)
+ local attrdict = "{"..self:_pprint_attrs(multi and item[1] or item, cterm).."}"
local attrdict2, hlinfo
local descdict = ""
if self._rgb_cterm then
- attrdict2 = ", {"..self:_pprint_attrs(item[2]).."}"
+ attrdict2 = ", {"..self:_pprint_attrs(item[2], true).."}"
hlinfo = item[3]
else
attrdict2 = ""
@@ -1433,13 +1436,15 @@ function Screen:_pprint_hlinfo(states)
end
-function Screen:_pprint_attrs(attrs)
+function Screen:_pprint_attrs(attrs, cterm)
local items = {}
for f, v in pairs(attrs) do
local desc = tostring(v)
if f == "foreground" or f == "background" or f == "special" then
if Screen.colornames[v] ~= nil then
desc = "Screen.colors."..Screen.colornames[v]
+ elseif cterm then
+ desc = tostring(v)
else
desc = string.format("tonumber('0x%06x')",v)
end
@@ -1511,7 +1516,8 @@ function Screen:_equal_attrs(a, b)
a.italic == b.italic and a.reverse == b.reverse and
a.foreground == b.foreground and a.background == b.background and
a.special == b.special and a.blend == b.blend and
- a.strikethrough == b.strikethrough
+ a.strikethrough == b.strikethrough and
+ a.fg_indexed == b.fg_indexed and a.bg_indexed == b.bg_indexed
end
function Screen:_equal_info(a, b)
diff --git a/test/helpers.lua b/test/helpers.lua
index 4c526d217f..3f29a28c0d 100644
--- a/test/helpers.lua
+++ b/test/helpers.lua
@@ -74,7 +74,8 @@ function module.matches(pat, actual)
error(string.format('Pattern does not match.\nPattern:\n%s\nActual:\n%s', pat, actual))
end
--- Invokes `fn` and returns the error string, or raises an error if `fn` succeeds.
+-- Invokes `fn` and returns the error string (may truncate full paths), or
+-- raises an error if `fn` succeeds.
--
-- Usage:
-- -- Match exact string.
@@ -88,7 +89,17 @@ function module.pcall_err(fn, ...)
if status == true then
error('expected failure, but got success')
end
+ -- From this:
+ -- /home/foo/neovim/runtime/lua/vim/shared.lua:186: Expected string, got number
+ -- to this:
+ -- Expected string, got number
local errmsg = tostring(rv):gsub('^[^:]+:%d+: ', '')
+ -- From this:
+ -- Error executing lua: /very/long/foo.lua:186: Expected string, got number
+ -- to this:
+ -- Error executing lua: .../foo.lua:186: Expected string, got number
+ errmsg = errmsg:gsub([[lua: [a-zA-Z]?:?[^:]-[/\]([^:/\]+):%d+: ]], 'lua: .../%1: ')
+ -- ^ Windows drive-letter (C:)
return errmsg
end
diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt
index dbb113aa0f..7465d037ee 100644
--- a/third-party/CMakeLists.txt
+++ b/third-party/CMakeLists.txt
@@ -153,8 +153,8 @@ set(LUAJIT_SHA256 ad5077bd861241bf5e50ae4bf543d291c5fcffab95ccc3218401131f503e45
set(LUA_URL https://www.lua.org/ftp/lua-5.1.5.tar.gz)
set(LUA_SHA256 2640fc56a795f29d28ef15e13c34a47e223960b0240e8cb0a82d9b0738695333)
-set(LUAROCKS_URL https://github.com/luarocks/luarocks/archive/v2.4.4.tar.gz)
-set(LUAROCKS_SHA256 9eb3d0738fd02ad8bf39bcedccac4e83e9b5fff2bcca247c3584b925b2075d9c)
+set(LUAROCKS_URL https://github.com/luarocks/luarocks/archive/v3.2.1.tar.gz)
+set(LUAROCKS_SHA256 0cab9f79311083f33e4d8f5a76021604f1d3f7141ce9a2ef1d8b717d92058370)
set(UNIBILIUM_URL https://github.com/neovim/unibilium/archive/92d929f.tar.gz)
set(UNIBILIUM_SHA256 29815283c654277ef77a3adcc8840db79ddbb20a0f0b0c8f648bd8cd49a02e4b)
@@ -179,8 +179,8 @@ set(GPERF_SHA256 588546b945bba4b70b6a3a616e80b4ab466e3f33024a352fc2198112cdbb3ae
set(WINTOOLS_URL https://github.com/neovim/deps/raw/2f9acbecf06365c10baa3c0087f34a54c9c6f949/opt/win32tools.zip)
set(WINTOOLS_SHA256 8bfce7e3a365721a027ce842f2ec1cf878f1726233c215c05964aac07300798c)
-set(WINGUI_URL https://github.com/equalsraf/neovim-qt/releases/download/v0.2.14/neovim-qt.zip)
-set(WINGUI_SHA256 dfcb1f7d25d4907dc1d4f20edd71ff9eb4762196225106bec01274dd668fb04c)
+set(WINGUI_URL https://github.com/equalsraf/neovim-qt/releases/download/v0.2.15/neovim-qt.zip)
+set(WINGUI_SHA256 b519ecb80b60522d25043f2d076a55656f5fbe5adf7f7e2943e5d8b161043987)
set(WIN32YANK_X86_URL https://github.com/equalsraf/win32yank/releases/download/v0.0.4/win32yank-x86.zip)
set(WIN32YANK_X86_SHA256 62f34e5a46c5d4a7b3f3b512e1ff7b77fedd432f42581cbe825233a996eed62c)
diff --git a/third-party/cmake/BuildLuajit.cmake b/third-party/cmake/BuildLuajit.cmake
index 458cfeafda..c0b24fb2a5 100644
--- a/third-party/cmake/BuildLuajit.cmake
+++ b/third-party/cmake/BuildLuajit.cmake
@@ -42,6 +42,12 @@ function(BuildLuajit)
endif()
endfunction()
+check_c_compiler_flag(-fno-stack-check HAS_NO_STACK_CHECK)
+if(CMAKE_SYSTEM_NAME MATCHES "Darwin" AND HAS_NO_STACK_CHECK)
+ set(NO_STACK_CHECK "CFLAGS+=-fno-stack-check")
+else()
+ set(NO_STACK_CHECK "")
+endif()
if(CMAKE_SYSTEM_NAME MATCHES "OpenBSD")
set(AMD64_ABI "LDFLAGS=-lpthread -lc++abi")
else()
@@ -50,6 +56,7 @@ endif()
set(INSTALLCMD_UNIX ${MAKE_PRG} CFLAGS=-fPIC
CFLAGS+=-DLUA_USE_APICHECK
CFLAGS+=-DLUA_USE_ASSERT
+ ${NO_STACK_CHECK}
${AMD64_ABI}
CCDEBUG+=-g
Q=
diff --git a/third-party/cmake/BuildLuarocks.cmake b/third-party/cmake/BuildLuarocks.cmake
index 7551c52ecc..4b1b94a46b 100644
--- a/third-party/cmake/BuildLuarocks.cmake
+++ b/third-party/cmake/BuildLuarocks.cmake
@@ -52,13 +52,17 @@ if(NOT MSVC)
set(LUAROCKS_BUILDARGS CC=${HOSTDEPS_C_COMPILER} LD=${HOSTDEPS_C_COMPILER})
endif()
+# Lua version, used with rocks directories.
+# Defaults to 5.1 for bundled LuaJIT/Lua.
+set(LUA_VERSION "5.1")
+
if(UNIX OR (MINGW AND CMAKE_CROSSCOMPILING))
if(USE_BUNDLED_LUAJIT)
list(APPEND LUAROCKS_OPTS
--with-lua=${HOSTDEPS_INSTALL_DIR}
--with-lua-include=${HOSTDEPS_INSTALL_DIR}/include/luajit-2.1
- --lua-suffix=jit)
+ --with-lua-interpreter=luajit)
elseif(USE_BUNDLED_LUA)
list(APPEND LUAROCKS_OPTS
--with-lua=${HOSTDEPS_INSTALL_DIR})
@@ -66,9 +70,23 @@ if(UNIX OR (MINGW AND CMAKE_CROSSCOMPILING))
find_package(LuaJit)
if(LUAJIT_FOUND)
list(APPEND LUAROCKS_OPTS
- --lua-version=5.1
--with-lua-include=${LUAJIT_INCLUDE_DIRS}
- --lua-suffix=jit)
+ --with-lua-interpreter=luajit)
+ endif()
+
+ # Get LUA_VERSION used with rocks output.
+ if(LUAJIT_FOUND)
+ set(LUA_EXE "luajit")
+ else()
+ set(LUA_EXE "lua")
+ endif()
+ execute_process(
+ COMMAND ${LUA_EXE} -e "print(string.sub(_VERSION, 5))"
+ OUTPUT_VARIABLE LUA_VERSION
+ ERROR_VARIABLE ERR
+ RESULT_VARIABLE RES)
+ if(NOT RES EQUAL 0)
+ message(FATAL_ERROR "Could not get LUA_VERSION with ${LUA_EXE}: ${ERR}")
endif()
endif()
@@ -111,7 +129,7 @@ if(USE_BUNDLED_LUAJIT)
elseif(USE_BUNDLED_LUA)
add_dependencies(luarocks lua)
endif()
-set(ROCKS_DIR ${HOSTDEPS_LIB_DIR}/luarocks/rocks)
+set(ROCKS_DIR ${HOSTDEPS_LIB_DIR}/luarocks/rocks-${LUA_VERSION})
# mpack
add_custom_command(OUTPUT ${ROCKS_DIR}/mpack