diff options
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? @@ -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() @@ -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)), ®match) == 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 |