diff options
106 files changed, 3553 insertions, 1086 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd90aeb932..2f2b3f102f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: matrix: include: - flavor: asan - cc: clang-11 + cc: clang-12 runner: ubuntu-20.04 os: linux - flavor: lint @@ -38,12 +38,6 @@ jobs: - name: Setup commom environment variables run: ./.github/workflows/env.sh ${{ matrix.flavor }} - - name: Setup clang repository - if: matrix.flavor == 'asan' || matrix.flavor == 'tsan' - run: | - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add - - sudo add-apt-repository 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-11 main' - - name: Install apt packages if: matrix.os == 'linux' run: | @@ -52,7 +46,11 @@ jobs: - name: Install new clang if: matrix.flavor == 'asan' || matrix.flavor == 'tsan' - run: sudo apt-get install -y clang-11 + run: | + wget https://apt.llvm.org/llvm.sh + chmod a+x llvm.sh + sudo ./llvm.sh 12 + rm llvm.sh - name: Install brew packages if: matrix.os == 'osx' @@ -91,8 +89,8 @@ jobs: runs-on: windows-2016 if: github.event.pull_request.draft == false env: - DEPS_BUILD_DIR: "C:/projects/nvim-deps" - DEPS_PREFIX: "C:/projects/nvim-deps/usr" + DEPS_BUILD_DIR: ${{ format('{0}/nvim-deps', github.workspace) }} + DEPS_PREFIX: ${{ format('{0}/nvim-deps/usr', github.workspace) }} strategy: fail-fast: false @@ -104,7 +102,7 @@ jobs: - uses: actions/cache@v2 with: - path: C:\projects\nvim-deps + path: ${{ env.DEPS_BUILD_DIR }} key: ${{ matrix.config }}-${{ hashFiles('third-party\**') }} - name: Run CI diff --git a/.github/workflows/env.sh b/.github/workflows/env.sh index cc1cef5cc4..459ed669eb 100755 --- a/.github/workflows/env.sh +++ b/.github/workflows/env.sh @@ -34,7 +34,7 @@ case "$FLAVOR" in BUILD_FLAGS="$BUILD_FLAGS -DPREFER_LUA=ON" cat <<EOF >> "$GITHUB_ENV" CLANG_SANITIZER=ASAN_UBSAN -SYMBOLIZER=asan_symbolize-11 +SYMBOLIZER=asan_symbolize-12 ASAN_OPTIONS=detect_leaks=1:check_initialization_order=1:log_path=$GITHUB_WORKSPACE/build/log/asan UBSAN_OPTIONS=print_stacktrace=1 log_path=$GITHUB_WORKSPACE/build/log/ubsan EOF diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43fe1d5101..e5064760d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,8 +110,8 @@ jobs: windows: runs-on: windows-2016 env: - DEPS_BUILD_DIR: "C:/projects/nvim-deps" - DEPS_PREFIX: "C:/projects/nvim-deps/usr" + DEPS_BUILD_DIR: ${{ format('{0}/nvim-deps', github.workspace) }} + DEPS_PREFIX: ${{ format('{0}/nvim-deps/usr', github.workspace) }} strategy: matrix: include: @@ -137,6 +137,8 @@ jobs: publish: needs: [linux, appimage, macOS, windows] runs-on: ubuntu-20.04 + permissions: + contents: write steps: - uses: actions/download-artifact@v2 - if: github.event_name == 'workflow_dispatch' diff --git a/.github/workflows/nightly.yaml b/.github/workflows/vim-patches.yml index 431ccd8b61..5742b51158 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/vim-patches.yml @@ -1,4 +1,4 @@ -name: Nightly +name: vim-patches on: schedule: - cron: '3 3 * * *' @@ -6,6 +6,9 @@ on: jobs: update-vim-patches: runs-on: ubuntu-20.04 + permissions: + contents: write + pull-requests: write env: VIM_SOURCE_DIR: ${{ format('{0}/vim-src', github.workspace) }} VERSION_BRANCH: marvim/ci-version-update @@ -46,4 +49,4 @@ jobs: git add -u git commit -m 'version.c: update [skip ci]' git push --force https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY} ${VERSION_BRANCH} - gh pr create --fill --label vim-patch --base master --head ${VERSION_BRANCH} || true + gh pr create --fill --label vim-patch --base ${GITHUB_REF#refs/heads/} --head ${VERSION_BRANCH} || true diff --git a/.gitignore b/.gitignore index 670340a519..c1726ede14 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,7 @@ compile_commands.json /.nvimlog # Generated by scripts/vim-patch.sh -/.vim-src/ +/.vim-src # Generated by old (Vim) tests. /src/nvim/testdir/del @@ -66,3 +66,6 @@ tags # Generated by gen_vimdoc.py: /runtime/doc/*.mpack /tmp-*-doc + +# vim patches +/vim-*.patch @@ -52,7 +52,10 @@ Pre-built packages for Windows, macOS, and Linux are found on the Install from source ------------------- +See the [Building Neovim](https://github.com/neovim/neovim/wiki/Building-Neovim) wiki page for details. + The build is CMake-based, but a Makefile is provided as a convenience. +After installing the dependencies, run the following command. make CMAKE_BUILD_TYPE=RelWithDebInfo sudo make install @@ -62,29 +65,12 @@ To install to a non-default location: make CMAKE_INSTALL_PREFIX=/full/path/ make install -To skip bundled (`third-party/*`) dependencies: - -1. Install the dependencies using a package manager. - ``` - sudo apt install gperf luajit luarocks libuv1-dev libluajit-5.1-dev libunibilium-dev libmsgpack-dev libtermkey-dev libvterm-dev libutf8proc-dev - sudo luarocks build mpack - sudo luarocks build lpeg - sudo luarocks build inspect - ``` -2. Build with `USE_BUNDLED=OFF`: - ``` - make CMAKE_BUILD_TYPE=RelWithDebInfo USE_BUNDLED=OFF - sudo make install - ``` - To inspect the build, these CMake features are useful: - `cmake --build build --target help` lists all build targets. - `build/CMakeCache.txt` (or `cmake -LAH build/`) contains the resolved values of all CMake variables. - `build/compile_commands.json` shows the full compiler invocations for each translation unit. -See the [Building Neovim](https://github.com/neovim/neovim/wiki/Building-Neovim) wiki page for details. - Transitioning from Vim -------------------- diff --git a/ci/common/test.sh b/ci/common/test.sh index 118e181dfa..92c15c8ba1 100644 --- a/ci/common/test.sh +++ b/ci/common/test.sh @@ -34,7 +34,7 @@ check_core_dumps() { cores="$(find /cores/ -type f -print)" local _sudo='sudo' else - cores="$(find ./ -type f -name 'core.*' -print)" + cores="$(find ./ -type f \( -name 'core.*' -o -name core -o -name nvim.core \) -print)" local _sudo= fi diff --git a/contrib/flake.nix b/contrib/flake.nix index c86bba6809..e75ff0356b 100644 --- a/contrib/flake.nix +++ b/contrib/flake.nix @@ -72,6 +72,7 @@ jq # jq for scripts/vim-patch.sh -r shellcheck # for `make shlint` doxygen # for script/gen_vimdoc.py + clang-tools # for clangd to find the correct headers ]); shellHook = oa.shellHook + '' diff --git a/runtime/autoload/health/provider.vim b/runtime/autoload/health/provider.vim index 112dd4354f..de540405e6 100644 --- a/runtime/autoload/health/provider.vim +++ b/runtime/autoload/health/provider.vim @@ -400,8 +400,6 @@ function! s:check_python(version) abort endfor endif - let pip = 'pip' . (a:version == 2 ? '' : '3') - if empty(python_exe) " No Python executable can import 'neovim'. Check if any Python executable " can import 'pynvim'. If so, that Python failed to import 'neovim' as @@ -413,9 +411,9 @@ function! s:check_python(version) abort \ 'Detected pip upgrade failure: Python executable can import "pynvim" but ' \ . 'not "neovim": '. pynvim_exe, \ "Use that Python version to reinstall \"pynvim\" and optionally \"neovim\".\n" - \ . pip ." uninstall pynvim neovim\n" - \ . pip ." install pynvim\n" - \ . pip ." install neovim # only if needed by third-party software") + \ . pynvim_exe ." -m pip uninstall pynvim neovim\n" + \ . pynvim_exe ." -m pip install pynvim\n" + \ . pynvim_exe ." -m pip install neovim # only if needed by third-party software") endif else let [pyversion, current, latest, status] = s:version_info(python_exe) @@ -440,7 +438,7 @@ function! s:check_python(version) abort if s:is_bad_response(current) call health#report_error( \ "pynvim is not installed.\nError: ".current, - \ ['Run in shell: '. pip .' install pynvim']) + \ ['Run in shell: '. python_exe .' -m pip install pynvim']) endif if s:is_bad_response(latest) diff --git a/runtime/autoload/tutor.vim b/runtime/autoload/tutor.vim index 6afe64de84..abf5c5e2c8 100644 --- a/runtime/autoload/tutor.vim +++ b/runtime/autoload/tutor.vim @@ -104,6 +104,10 @@ function! tutor#CheckLine(line) if exists('b:tutor_metadata') && has_key(b:tutor_metadata, 'expect') let bufn = bufnr('%') let ctext = getline(a:line) + let signs = sign_getplaced('.', {'lnum': a:line})[0].signs + if !empty(signs) + call sign_unplace('', {'id': signs[0].id}) + endif if b:tutor_metadata['expect'][string(a:line)] == -1 || ctext ==# b:tutor_metadata['expect'][string(a:line)] exe "sign place ".b:tutor_sign_id." line=".a:line." name=tutorok buffer=".bufn else diff --git a/runtime/delmenu.vim b/runtime/delmenu.vim index 81df87d346..5c20290152 100644 --- a/runtime/delmenu.vim +++ b/runtime/delmenu.vim @@ -2,24 +2,30 @@ " Warning: This also deletes all menus defined by the user! " " Maintainer: Bram Moolenaar <Bram@vim.org> -" Last Change: 2001 May 27 +" Last Change: 2019 Dec 10 aunmenu * -silent! unlet did_install_default_menus -silent! unlet did_install_syntax_menu -if exists("did_menu_trans") +unlet! g:did_install_default_menus +unlet! g:did_install_syntax_menu + +if exists('g:did_menu_trans') menutrans clear - unlet did_menu_trans + unlet g:did_menu_trans endif -silent! unlet find_help_dialog +unlet! g:find_help_dialog -silent! unlet menutrans_help_dialog -silent! unlet menutrans_path_dialog -silent! unlet menutrans_tags_dialog -silent! unlet menutrans_textwidth_dialog -silent! unlet menutrans_fileformat_dialog -silent! unlet menutrans_no_file +unlet! g:menutrans_fileformat_choices +unlet! g:menutrans_fileformat_dialog +unlet! g:menutrans_help_dialog +unlet! g:menutrans_no_file +unlet! g:menutrans_path_dialog +unlet! g:menutrans_set_lang_to +unlet! g:menutrans_spell_add_ARG_to_word_list +unlet! g:menutrans_spell_change_ARG_to +unlet! g:menutrans_spell_ignore_ARG +unlet! g:menutrans_tags_dialog +unlet! g:menutrans_textwidth_dialog " vim: set sw=2 : diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 0c17fa1669..0d85d6b539 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -1212,6 +1212,9 @@ nvim_open_term({buffer}, {opts}) *nvim_open_term()* {buffer} the buffer to use (expected to be empty) {opts} Optional parameters. Reserved for future use. + Return: ~ + Channel id, or 0 on error + nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()* Open a new window. @@ -2280,6 +2283,10 @@ nvim_buf_set_extmark({buffer}, {ns_id}, {line}, {col}, {opts}) color • "blend": blend with background text color. + • hl_eol : when true, for a multiline highlight + covering the EOL of a line, continue the + highlight for the rest of the screen line + (just like for diff and cursorline highlight). • ephemeral : for use with |nvim_set_decoration_provider| callbacks. The mark will only be used for the current redraw diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt index 967f4b26f2..656bb10c45 100644 --- a/runtime/doc/channel.txt +++ b/runtime/doc/channel.txt @@ -174,4 +174,81 @@ Put this in `uppercase.vim` and run: > nvim --headless --cmd "source uppercase.vim" ============================================================================== +5. Using a prompt buffer *prompt-buffer* + +If you want to type input for the job in a Vim window you have a few options: +- Use a normal buffer and handle all possible commands yourself. + This will be complicated, since there are so many possible commands. +- Use a terminal window. This works well if what you type goes directly to + the job and the job output is directly displayed in the window. + See |terminal|. +- Use a window with a prompt buffer. This works well when entering a line for + the job in Vim while displaying (possibly filtered) output from the job. + +A prompt buffer is created by setting 'buftype' to "prompt". You would +normally only do that in a newly created buffer. + +The user can edit and enter one line of text at the very last line of the +buffer. When pressing Enter in the prompt line the callback set with +|prompt_setcallback()| is invoked. It would normally send the line to a job. +Another callback would receive the output from the job and display it in the +buffer, below the prompt (and above the next prompt). + +Only the text in the last line, after the prompt, is editable. The rest of the +buffer is not modifiable with Normal mode commands. It can be modified by +calling functions, such as |append()|. Using other commands may mess up the +buffer. + +After setting 'buftype' to "prompt" Vim does not automatically start Insert +mode, use `:startinsert` if you want to enter Insert mode, so that the user +can start typing a line. + +The text of the prompt can be set with the |prompt_setprompt()| function. If +no prompt is set with |prompt_setprompt()|, "% " is used. You can get the +effective prompt text for a buffer, with |prompt_getprompt()|. + +The user can go to Normal mode and navigate through the buffer. This can be +useful to see older output or copy text. + +Any command that starts Insert mode, such as "a", "i", "A" and "I", will move +the cursor to the last line. "A" will move to the end of the line, "I" to the +start of the line. + +Here is an example for Unix. It starts a shell in the background and prompts +for the next shell command. Output from the shell is displayed above the +prompt. > + + " Function handling a line of text that has been typed. + func TextEntered(text) + " Send the text to a shell with Enter appended. + call chansend(g:shell_job, [a:text, '']) + endfunc + + " Function handling output from the shell: Added above the prompt. + func GotOutput(channel, msg, name) + call append(line("$") - 1, a:msg) + endfunc + + " Function handling the shell exit: close the window. + func JobExit(job, status, event) + quit! + endfunc + + " Start a shell in the background. + let shell_job = jobstart(["/bin/sh"], #{ + \ on_stdout: function('GotOutput'), + \ on_stderr: function('GotOutput'), + \ on_exit: function('JobExit'), + \ }) + + new + set buftype=prompt + let buf = bufnr('') + call prompt_setcallback(buf, function("TextEntered")) + call prompt_setprompt(buf, "shell command: ") + + " start accepting shell commands + startinsert +< + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index c3736d9a3e..b8dcfd0ff4 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2309,6 +2309,7 @@ perleval({expr}) any evaluate |perl| expression pow({x}, {y}) Float {x} to the power of {y} prevnonblank({lnum}) Number line nr of non-blank line <= {lnum} printf({fmt}, {expr1}...) String format text +prompt_getprompt({buf}) String get prompt text prompt_setcallback({buf}, {expr}) none set prompt callback function prompt_setinterrupt({buf}, {text}) none set prompt interrupt function prompt_setprompt({buf}, {text}) none set prompt text @@ -5361,9 +5362,8 @@ input({opts}) prompt "" Same as {prompt} in the first form. default "" Same as {text} in the first form. completion nothing Same as {completion} in the first form. - cancelreturn "" Same as {cancelreturn} from - |inputdialog()|. Also works with - input(). + cancelreturn "" The value returned when the dialog is + cancelled. highlight nothing Highlight handler: |Funcref|. The highlighting set with |:echohl| is used for the prompt. @@ -6747,6 +6747,13 @@ printf({fmt}, {expr1} ...) *printf()* of "%" items. If there are not sufficient or too many arguments an error is given. Up to 18 arguments can be used. +prompt_getprompt({buf}) *prompt_getprompt()* + Returns the effective prompt text for buffer {buf}. {buf} can + be a buffer name or number. See |prompt-buffer|. + + If the buffer doesn't exist or isn't a prompt buffer, an empty + string is returned. + prompt_setcallback({buf}, {expr}) *prompt_setcallback()* Set prompt callback for buffer {buf} to {expr}. When {expr} is an empty string the callback is removed. This has only @@ -10099,6 +10106,8 @@ This function can then be called with: > The recursiveness of user functions is restricted with the |'maxfuncdepth'| option. +It is also possible to use `:eval`. It does not support a range. + AUTOMATICALLY LOADING FUNCTIONS ~ *autoload-functions* @@ -10532,6 +10541,20 @@ text... Unlock the internal variable {name}. Does the opposite of |:lockvar|. + *:eval* +:eval {expr} Evaluate {expr} and discard the result. Example: > + :eval append(Filter(Getlist()), '$') + +< The expression is supposed to have a side effect, + since the resulting value is not used. In the example + the `append()` call appends the List with text to the + buffer. This is similar to `:call` but works with any + expression. + + The command can be shortened to `:ev` or `:eva`, but + these are hard to recognize and therefore not to be + used. + :if {expr1} *:if* *:end* *:endif* *:en* *E171* *E579* *E580* :en[dif] Execute the commands until the next matching ":else" diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 3c0dbf96c5..5c2ee568c5 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -592,14 +592,34 @@ buf_request({bufnr}, {method}, {params}, {handler}) You could instead iterate all clients and call their `cancel_request()` methods. + *vim.lsp.buf_request_all()* +buf_request_all({bufnr}, {method}, {params}, {callback}) + Sends an async request for all active clients attached to the + buffer. Executes the callback on the combined result. + Parameters are the same as |vim.lsp.buf_request()| but the + return result and callback are different. + + Parameters: ~ + {bufnr} (number) Buffer handle, or 0 for current. + {method} (string) LSP method name + {params} (optional, table) Parameters to send to the + server + {callback} (function) The callback to call when all + requests are finished. + + Return: ~ + (function) A function that will cancel all requests which + is the same as the one returned from `buf_request` . + *vim.lsp.buf_request_sync()* buf_request_sync({bufnr}, {method}, {params}, {timeout_ms}) - Sends a request to a server and waits for the response. + Sends a request to all server and waits for the response of + all of them. - Calls |vim.lsp.buf_request()| but blocks Nvim while awaiting - the result. Parameters are the same as |vim.lsp.buf_request()| - but the return result is different. Wait maximum of - {timeout_ms} (default 100) ms. + Calls |vim.lsp.buf_request_all()| but blocks Nvim while + awaiting the result. Parameters are the same as + |vim.lsp.buf_request()| but the return result is different. + Wait maximum of {timeout_ms} (default 100) ms. Parameters: ~ {bufnr} (number) Buffer handle, or 0 for current. @@ -678,6 +698,9 @@ client_is_stopped({client_id}) *vim.lsp.client_is_stopped()* Return: ~ true if client is stopped, false otherwise. +flush({client}) *vim.lsp.flush()* + TODO: Documentation + get_active_clients() *vim.lsp.get_active_clients()* Gets all active clients. @@ -708,6 +731,15 @@ get_log_path() *vim.lsp.get_log_path()* Return: ~ (String) Path to logfile. +init({client}, {bufnr}) *vim.lsp.init()* + client_id → state + + state pending_change?: function that the timer starts to + trigger didChange pending_changes: list of tables with the + pending changesets; for incremental_sync only + use_incremental_sync: bool buffers?: table (bufnr → lines); + for incremental sync only timer?: uv_timer + omnifunc({findstart}, {base}) *vim.lsp.omnifunc()* Implements 'omnifunc' compatible LSP completion. @@ -727,6 +759,16 @@ omnifunc({findstart}, {base}) *vim.lsp.omnifunc()* |complete-items| |CompleteDone| + *vim.lsp.prepare()* +prepare({bufnr}, {firstline}, {new_lastline}, {changedtick}) + TODO: Documentation + +reset({client_id}) *vim.lsp.reset()* + TODO: Documentation + +reset_buf({client}, {bufnr}) *vim.lsp.reset_buf()* + TODO: Documentation + set_log_level({level}) *vim.lsp.set_log_level()* Sets the global log level for LSP logging. @@ -849,6 +891,11 @@ start_client({config}) *vim.lsp.start_client()* • allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits + • debounce_text_changes (number, + default nil): Debounce didChange + notifications to the server by the + given number in milliseconds. No + debounce occurs if nil Return: ~ Client id. |vim.lsp.get_client_by_id()| Note: client may @@ -1311,6 +1358,10 @@ on_publish_diagnostics({_}, {_}, {params}, {client_id}, {_}, {config}) • Update diagnostics in InsertMode or wait until InsertLeave + • severity_sort: (default=false) + • Sort diagnostics (and thus signs and virtual + text) + reset({client_id}, {buffer_client_map}) *vim.lsp.diagnostic.reset()* Clear diagnotics and diagnostic cache diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index c2fc25431c..be01966d42 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -18,7 +18,8 @@ an idea of what lurks beneath: > 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. +be used from Lua code. A good overview of using Lua in neovim is given by +https://github.com/nanotee/nvim-lua-guide. Module conflicts are resolved by "last wins". For example if both of these are on 'runtimepath': @@ -831,6 +832,7 @@ LUA-VIMSCRIPT BRIDGE *lua-vimscript* Nvim Lua provides an interface to Vimscript variables and functions, and editor commands and options. +See also https://github.com/nanotee/nvim-lua-guide. vim.call({func}, {...}) *vim.call()* Invokes |vim-function| or |user-function| {func} with arguments {...}. @@ -839,10 +841,18 @@ vim.call({func}, {...}) *vim.call()* vim.fn[func]({...}) vim.cmd({cmd}) *vim.cmd()* - Invokes an Ex command (the ":" commands, Vimscript statements). + Executes multiple lines of Vimscript at once. It is an alias to + |nvim_exec()|, where `output` is set to false. Thus it works identical + to |:source|. See also |ex-cmd-index|. Example: > vim.cmd('echo 42') + vim.cmd([[ + augroup My_group + autocmd! + autocmd FileType c setlocal cindent + augroup END + ]]) vim.fn.{func}({...}) *vim.fn* Invokes |vim-function| or |user-function| {func} with arguments {...}. diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 04310ca8d4..63a9db9d0d 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -4818,7 +4818,7 @@ A jump table for the options with a short description can be found at |Q_op|. |xdg| ($XDG_CONFIG_DIRS, defaults to /etc/xdg). This also contains preferences from system administrator. 3. Data home directory, for plugins installed by user. - Given by `stdpath("data")`. |$XDG_DATA_HOME| + Given by `stdpath("data")/site`. |$XDG_DATA_HOME| 4. nvim/site subdirectories for each directory in $XDG_DATA_DIRS. This is for plugins which were installed by system administrator, but are not part of the Nvim distribution. XDG_DATA_DIRS defaults @@ -5555,6 +5555,12 @@ A jump table for the options with a short description can be found at |Q_op|. "number" display signs in the 'number' column. If the number column is not present, then behaves like 'auto'. + Note regarding 'orphaned signs': with signcolumn numbers higher than + 1, deleting lines will also remove the associated signs automatically, + in contrast to the default Vim behavior of keeping and grouping them. + This is done in order for the signcolumn appearence not appear weird + during line deletion. + *'smartcase'* *'scs'* *'nosmartcase'* *'noscs'* 'smartcase' 'scs' boolean (default off) diff --git a/runtime/doc/provider.txt b/runtime/doc/provider.txt index f944689d0b..be895f9e4e 100644 --- a/runtime/doc/provider.txt +++ b/runtime/doc/provider.txt @@ -88,7 +88,7 @@ Example using pyenv: > pyenv install 3.4.4 pyenv virtualenv 3.4.4 py3nvim pyenv activate py3nvim - pip install pynvim + python3 -m pip install pynvim pyenv which python # Note the path The last command reports the interpreter path, add it to your init.vim: > let g:python3_host_prog = '/path/to/py3nvim/bin/python' diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 343f4a62c2..1f4b5d3097 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -102,14 +102,14 @@ tsnode:field({name}) *tsnode:field()* tsnode:child_count() *tsnode:child_count()* Get the node's number of children. -tsnode:child({index}) *tsnode:child()* +tsnode:child({index}) *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()* +tsnode:named_child_count() *tsnode:named_child_count()* Get the node's number of named children. -tsnode:named_child({index}) *tsnode:named_child()* +tsnode:named_child({index}) *tsnode:named_child()* Get the node's named child at the given {index}, where zero represents the first named child. @@ -146,7 +146,7 @@ tsnode:has_error() *tsnode:has_error()* tsnode:sexpr() *tsnode:sexpr()* Get an S-expression representing the node as a string. -tsnode:id() *tsnode:id()* +tsnode:id() *tsnode:id()* Get an unique identier for the node inside its own tree. No guarantees are made about this identifer's internal representation, @@ -156,16 +156,16 @@ tsnode:id() *tsnode:id()* NB: the id is not guaranteed to be unique for nodes from different trees. tsnode:descendant_for_range({start_row}, {start_col}, {end_row}, {end_col}) - *tsnode:descendant_for_range()* + *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()* + *tsnode:named_descendant_for_range()* Get the smallest named node within this node that spans the given range of (row, column) positions -Query methods *lua-treesitter-query* +Query *lua-treesitter-query* Tree-sitter queries are supported, with some limitations. Currently, the only supported match predicate is `eq?` (both comparing a capture against a string @@ -178,65 +178,6 @@ and predicates. A `capture` allows you to associate names with a specific node in a pattern. A `predicate` adds arbitrary metadata and conditional data to a match. -vim.treesitter.parse_query({lang}, {query}) - *vim.treesitter.parse_query()* - Parse {query} as a string. (If the query is in a file, the caller - should read the contents into a string before calling). - - Returns a `Query` (see |lua-treesitter-query|) object which can be used to - search nodes in the syntax tree for the patterns defined in {query} - using `iter_*` methods below. Exposes `info` and `captures` with - additional information about the {query}. - - `captures` contains the list of unique capture names defined in - {query}. - -` info.captures` also points to `captures`. - - `info.patterns` contains information about predicates. - - -query:iter_captures({node}, {bufnr}, {start_row}, {end_row}) - *query:iter_captures()* - Iterate over all captures from all matches inside {node}. - {bufnr} is needed if the query contains predicates, then the caller - must ensure to use a freshly parsed tree consistent with the current - text of the buffer. {start_row} and {end_row} can be used to limit - matches inside a row range (this is typically used with root node - as the node, i e to get syntax highlight matches in the current - viewport). When omitted the start and end row values are used from - the given node. - - The iterator returns three values, a numeric id identifying the capture, - the captured node, and metadata from any directives processing the match. - The following example shows how to get captures by name: -> - for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do - local name = query.captures[id] -- name of the capture in the query - -- typically useful info about the node: - local type = node:type() -- type of the captured node - local row1, col1, row2, col2 = node:range() -- range of the capture - ... use the info here ... - end -< -query:iter_matches({node}, {bufnr}, {start_row}, {end_row}) - *query:iter_matches()* - Iterate over all matches within a node. The arguments are the same as - for |query:iter_captures()| but the iterated values are different: - an (1-based) index of the pattern in the query, a table mapping - capture indices to nodes, and metadata from any directives processing the match. - If the query has more than one pattern the capture table might be sparse, - and e.g. `pairs()` method should be used over `ipairs`. - Here an example iterating over all captures in every match: -> - for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do - for id, node in pairs(match) do - local name = query.captures[id] - -- `node` was captured by the `name` capture in the match - - local node_data = metadata[id] -- Node level metadata - - ... use the info here ... - end - end - Treesitter Query Predicates *lua-treesitter-predicates* When writing queries for treesitter, one might use `predicates`, that is, @@ -298,28 +239,6 @@ Here is a list of built-in directives: `({capture_id}, {start_row}, {start_col}, {end_row}, {end_col}, {key?})` The default key is "offset". - *vim.treesitter.query.add_predicate()* -vim.treesitter.query.add_predicate({name}, {handler}) - -This adds a predicate with the name {name} to be used in queries. -{handler} should be a function whose signature will be : > - handler(match, pattern, bufnr, predicate) -< - *vim.treesitter.query.list_predicates()* -vim.treesitter.query.list_predicates() - -This lists the currently available predicates to use in queries. - - *vim.treesitter.query.add_directive()* -vim.treesitter.query.add_directive({name}, {handler}) - -This adds a directive with the name {name} to be used in queries. -{handler} should be a function whose signature will be : > - handler(match, pattern, bufnr, predicate, metadata) -Handlers can set match level data by setting directly on the metadata object `metadata.key = value` -Handlers can set node level data by using the capture id on the metadata table -`metadata[capture_id].key = value` - Treesitter syntax highlighting (WIP) *lua-treesitter-highlight* NOTE: This is a partially implemented feature, and not usable as a default @@ -364,5 +283,434 @@ identical identifiers, highlighting both as |hl-WarningMsg|: > ((binary_expression left: (identifier) @WarningMsg.left right: (identifier) @WarningMsg.right) (eq? @WarningMsg.left @WarningMsg.right)) +< + +============================================================================== +Lua module: vim.treesitter *lua-treesitter-core* + +get_parser({bufnr}, {lang}, {opts}) *get_parser()* + Gets the parser for this bufnr / ft combination. + + If needed this will create the parser. Unconditionnally attach + the provided callback + + Parameters: ~ + {bufnr} The buffer the parser should be tied to + {lang} The filetype of this parser + {opts} Options object to pass to the created language + tree + + Return: ~ + The parser + +get_string_parser({str}, {lang}, {opts}) *get_string_parser()* + Gets a string parser + + Parameters: ~ + {str} The string to parse + {lang} The language of this string + {opts} Options to pass to the created language tree + + +============================================================================== +Lua module: vim.treesitter.language *treesitter-language* + +inspect_language({lang}) *inspect_language()* + Inspects the provided language. + + Inspecting provides some useful informations on the language + like node names, ... + + Parameters: ~ + {lang} The language. + +require_language({lang}, {path}, {silent}) *require_language()* + Asserts that the provided language is installed, and + optionally provide a path for the parser + + Parsers are searched in the `parser` runtime directory. + + Parameters: ~ + {lang} The language the parser should parse + {path} Optional path the parser is located at + {silent} Don't throw an error if language not found + + +============================================================================== +Lua module: vim.treesitter.query *treesitter-query* + +add_directive({name}, {handler}, {force}) *add_directive()* + Adds a new directive to be used in queries + + Parameters: ~ + {name} the name of the directive, without leading # + {handler} the handler function to be used signature will + be (match, pattern, bufnr, predicate) + +add_predicate({name}, {handler}, {force}) *add_predicate()* + Adds a new predicate to be used in queries + + Parameters: ~ + {name} the name of the predicate, without leading # + {handler} the handler function to be used signature will + be (match, pattern, bufnr, predicate) + +get_node_text({node}, {source}) *get_node_text()* + Gets the text corresponding to a given node + + Parameters: ~ + {node} the node + {bsource} The buffer or string from which the node is + extracted + +get_query({lang}, {query_name}) *get_query()* + Returns the runtime query {query_name} for {lang}. + + Parameters: ~ + {lang} The language to use for the query + {query_name} The name of the query (i.e. "highlights") + + Return: ~ + The corresponding query, parsed. + + *get_query_files()* +get_query_files({lang}, {query_name}, {is_included}) + Gets the list of files used to make up a query + + Parameters: ~ + {lang} The language + {query_name} The name of the query to load + {is_included} Internal parameter, most of the time left + as `nil` + +list_predicates() *list_predicates()* + TODO: Documentation + +parse_query({lang}, {query}) *parse_query()* + Parse {query} as a string. (If the query is in a file, the + caller should read the contents into a string before calling). + + Returns a `Query` (see |lua-treesitter-query|) object which + can be used to search nodes in the syntax tree for the + patterns defined in {query} using `iter_*` methods below. + + Exposes `info` and `captures` with additional information about the {query}. + • `captures` contains the list of unique capture names defined + in {query}. - `info.captures` also points to `captures` . + • `info.patterns` contains information about predicates. + + Parameters: ~ + {lang} The language + {query} A string containing the query (s-expr syntax) + + Return: ~ + The query + + *Query:iter_captures()* +Query:iter_captures({self}, {node}, {source}, {start}, {stop}) + Iterate over all captures from all matches inside {node} + + {source} is needed if the query contains predicates, then the + caller must ensure to use a freshly parsed tree consistent + with the current text of the buffer (if relevent). {start_row} + and {end_row} can be used to limit matches inside a row range + (this is typically used with root node as the node, i e to get + syntax highlight matches in the current viewport). When + omitted the start and end row values are used from the given + node. + + The iterator returns three values, a numeric id identifying + the capture, the captured node, and metadata from any + directives processing the match. The following example shows + how to get captures by name: +> + + for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do + local name = query.captures[id] -- name of the capture in the query + -- typically useful info about the node: + local type = node:type() -- type of the captured node + local row1, col1, row2, col2 = node:range() -- range of the capture + ... use the info here ... + end +< + + Parameters: ~ + {node} The node under which the search will occur + {source} The source buffer or string to exctract text + from + {start} The starting line of the search + {stop} The stopping line of the search (end-exclusive) + {self} + + Return: ~ + The matching capture id + The captured node + + *Query:iter_matches()* +Query:iter_matches({self}, {node}, {source}, {start}, {stop}) + Iterates the matches of self on a given range. + + Iterate over all matches within a node. The arguments are the + same as for |query:iter_captures()| but the iterated values + are different: an (1-based) index of the pattern in the query, + a table mapping capture indices to nodes, and metadata from + any directives processing the match. If the query has more + than one pattern the capture table might be sparse, and e.g. + `pairs()` method should be used over `ipairs` . Here an + example iterating over all captures in every match: +> + + for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do + for id, node in pairs(match) do + local name = query.captures[id] + -- `node` was captured by the `name` capture in the match + + local node_data = metadata[id] -- Node level metadata + + ... use the info here ... + end + end +< + + Parameters: ~ + {node} The node under which the search will occur + {source} The source buffer or string to search + {start} The starting line of the search + {stop} The stopping line of the search (end-exclusive) + {self} + + Return: ~ + The matching pattern id + The matching match + +set_query({lang}, {query_name}, {text}) *set_query()* + Sets the runtime query {query_name} for {lang} + + This allows users to override any runtime files and/or + configuration set by plugins. + + Parameters: ~ + {lang} string: The language to use for the query + {query_name} string: The name of the query (i.e. + "highlights") + {text} string: The query text (unparsed). + + +============================================================================== +Lua module: vim.treesitter.highlighter *treesitter-highlighter* + +new({tree}, {opts}) *highlighter.new()* + Creates a new highlighter using + + Parameters: ~ + {tree} The language tree to use for highlighting + {opts} Table used to configure the highlighter + • queries: Table to overwrite queries used by the + highlighter + +TSHighlighter:destroy({self}) *TSHighlighter:destroy()* + Removes all internal references to the highlighter + + Parameters: ~ + {self} + +TSHighlighter:get_query({self}, {lang}) *TSHighlighter:get_query()* + Gets the query used for + + Parameters: ~ + {lang} A language used by the highlighter. + {self} + + +============================================================================== +Lua module: vim.treesitter.languagetree *treesitter-languagetree* + +LanguageTree:add_child({self}, {lang}) *LanguageTree:add_child()* + Adds a child language to this tree. + + If the language already exists as a child, it will first be + removed. + + Parameters: ~ + {lang} The language to add. + {self} + +LanguageTree:children({self}) *LanguageTree:children()* + Returns a map of language to child tree. + + Parameters: ~ + {self} + +LanguageTree:contains({self}, {range}) *LanguageTree:contains()* + Determines wether This goes down the tree to recursively check childs. + + Parameters: ~ + {range} is contained in this language tree + + Parameters: ~ + {range} A range, that is a `{ start_line, start_col, + end_line, end_col }` table. + {self} + +LanguageTree:destroy({self}) *LanguageTree:destroy()* + Destroys this language tree and all its children. + + Any cleanup logic should be performed here. Note, this DOES + NOT remove this tree from a parent. `remove_child` must be called on the parent to remove it. + + Parameters: ~ + {self} + + *LanguageTree:for_each_child()* +LanguageTree:for_each_child({self}, {fn}, {include_self}) + Invokes the callback for each LanguageTree and it's children + recursively + + Parameters: ~ + {fn} The function to invoke. This is invoked + with arguments (tree: LanguageTree, lang: + string) + {include_self} Whether to include the invoking tree in + the results. + {self} + +LanguageTree:for_each_tree({self}, {fn}) *LanguageTree:for_each_tree()* + Invokes the callback for each treesitter trees recursively. + + Note, this includes the invoking language tree's trees as + well. + + Parameters: ~ + {fn} The callback to invoke. The callback is invoked + with arguments (tree: TSTree, languageTree: + LanguageTree) + {self} + +LanguageTree:included_regions({self}) *LanguageTree:included_regions()* + Gets the set of included regions + + Parameters: ~ + {self} + +LanguageTree:invalidate({self}, {reload}) *LanguageTree:invalidate()* + Invalidates this parser and all its children + + Parameters: ~ + {self} + +LanguageTree:is_valid({self}) *LanguageTree:is_valid()* + Determines whether this tree is valid. If the tree is invalid, `parse()` must be called to get the an updated tree. + + Parameters: ~ + {self} + +LanguageTree:lang({self}) *LanguageTree:lang()* + Gets the language of this tree node. + + Parameters: ~ + {self} + + *LanguageTree:language_for_range()* +LanguageTree:language_for_range({self}, {range}) + Gets the appropriate language that contains + + Parameters: ~ + {range} A text range, see |LanguageTree:contains| + {self} + +LanguageTree:parse({self}) *LanguageTree:parse()* + Parses all defined regions using a treesitter parser for the + language this tree represents. This will run the injection + query for this language to determine if any child languages + should be created. + + Parameters: ~ + {self} + +LanguageTree:register_cbs({self}, {cbs}) *LanguageTree:register_cbs()* + Registers callbacks for the parser + + Parameters: ~ + {cbs} An `nvim_buf_attach` -like table argument with the following keys : `on_bytes` : see `nvim_buf_attach` , but this will be called after the parsers callback. `on_changedtree` : a callback that will be called every time the + tree has syntactical changes. it will only be + passed one argument, that is a table of the ranges + (as node ranges) that changed. `on_child_added` : emitted when a child is added to the tree. `on_child_removed` : emitted when a child is removed from the tree. + {self} + +LanguageTree:remove_child({self}, {lang}) *LanguageTree:remove_child()* + Removes a child language from this tree. + + Parameters: ~ + {lang} The language to remove. + {self} + + *LanguageTree:set_included_regions()* +LanguageTree:set_included_regions({self}, {regions}) + Sets the included regions that should be parsed by this + parser. A region is a set of nodes and/or ranges that will be + parsed in the same context. + + For example, `{ { node1 }, { node2} }` is two separate + regions. This will be parsed by the parser in two different + contexts... thus resulting in two separate trees. + + `{ { node1, node2 } }` is a single region consisting of two + nodes. This will be parsed by the parser in a single + context... thus resulting in a single tree. + + This allows for embedded languages to be parsed together + across different nodes, which is useful for templating + languages like ERB and EJS. + + Note, this call invalidates the tree and requires it to be + parsed again. + + Parameters: ~ + {regions} A list of regions this tree should manage and + parse. + {self} + +LanguageTree:source({self}) *LanguageTree:source()* + Returns the source content of the language tree (bufnr or + string). + + Parameters: ~ + {self} + +LanguageTree:trees({self}) *LanguageTree:trees()* + Returns all trees this language tree contains. Does not + include child languages. + + Parameters: ~ + {self} + +new({source}, {lang}, {opts}) *languagetree.new()* + Represents a single treesitter parser for a language. The + language can contain child languages with in its range, hence + the tree. + + Parameters: ~ + {source} Can be a bufnr or a string of text to + parse + {lang} The language this tree represents + {opts} Options table + {opts.injections} A table of language to injection query + strings. This is useful for overriding + the built-in runtime file searching for + the injection language query per + language. + + +============================================================================== +Lua module: vim.treesitter.health *treesitter-health* + +check_health() *check_health()* + TODO: Documentation + +list_parsers() *list_parsers()* + Lists the parsers currently installed + + Return: ~ + A list of parsers vim:tw=78:ts=8:ft=help:norl: diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index 21f5dcc815..41948f577e 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -978,6 +978,7 @@ Tags: *tag-functions* settagstack() modify the tag stack of a window Prompt Buffer: *promptbuffer-functions* + prompt_getprompt() get the effective prompt text for a buffer prompt_setcallback() set prompt callback for a buffer prompt_setinterrupt() set interrupt callback for a buffer prompt_setprompt() set the prompt text for a buffer diff --git a/runtime/filetype.vim b/runtime/filetype.vim index b7157a14e7..724a96cb01 100644 --- a/runtime/filetype.vim +++ b/runtime/filetype.vim @@ -413,6 +413,10 @@ endif " Lynx config files au BufNewFile,BufRead lynx.cfg setf lynx +" Modula-3 configuration language (must be before *.cfg and *makefile) +au BufNewFile,BufRead *.quake,cm3.cfg setf m3quake +au BufNewFile,BufRead m3makefile,m3overrides setf m3build + " Quake au BufNewFile,BufRead *baseq[2-3]/*.cfg,*id1/*.cfg setf quake au BufNewFile,BufRead *quake[1-3]/*.cfg setf quake @@ -592,7 +596,7 @@ au BufNewFile,BufRead *.fan,*.fwt setf fan au BufNewFile,BufRead *.factor setf factor " Fennel -autocmd BufRead,BufNewFile *.fnl setf fennel +autocmd BufRead,BufNewFile *.fnl setf fennel " Fetchmail RC file au BufNewFile,BufRead .fetchmailrc setf fetchmail @@ -636,7 +640,7 @@ au BufNewFile,BufRead *.mo,*.gdmo setf gdmo au BufNewFile,BufRead *.ged,lltxxxxx.txt setf gedcom " Gift (Moodle) -autocmd BufRead,BufNewFile *.gift setf gift +autocmd BufRead,BufNewFile *.gift setf gift " Git au BufNewFile,BufRead COMMIT_EDITMSG,MERGE_MSG,TAG_EDITMSG setf gitcommit @@ -707,7 +711,7 @@ au BufNewFile,BufRead .gtkrc,gtkrc setf gtkrc au BufNewFile,BufRead *.haml setf haml " Hamster Classic | Playground files -au BufNewFile,BufRead *.hsm setf hamster +au BufNewFile,BufRead *.hsm setf hamster " Haskell au BufNewFile,BufRead *.hs,*.hsc,*.hs-boot setf haskell @@ -1041,10 +1045,10 @@ au BufNewFile,BufRead *.mod \ setf modsim3 | \ endif -" Modula 2 (.md removed in favor of Markdown) +" Modula-2 (.md removed in favor of Markdown) au BufNewFile,BufRead *.m2,*.DEF,*.MOD,*.mi setf modula2 -" Modula 3 (.m3, .i3, .mg, .ig) +" Modula-3 (.m3, .i3, .mg, .ig) au BufNewFile,BufRead *.[mi][3g] setf modula3 " Monk @@ -1334,12 +1338,15 @@ au BufNewFile,BufRead *.pdb setf prolog " Promela au BufNewFile,BufRead *.pml setf promela +" Property Specification Language (PSL) +au BufNewFile,BufRead *.psl setf psl + " Google protocol buffers au BufNewFile,BufRead *.proto setf proto au BufNewFile,BufRead *.pbtxt setf pbtxt " Poke -au BufNewFile,BufRead *.pk setf poke +au BufNewFile,BufRead *.pk setf poke " Protocols au BufNewFile,BufRead */etc/protocols setf protocols diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 563ffc479e..93ec9ed624 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -232,6 +232,12 @@ local function validate_client_config(config) flags = { config.flags, "t", true }; get_language_id = { config.get_language_id, "f", true }; } + assert( + (not config.flags + or not config.flags.debounce_text_changes + or type(config.flags.debounce_text_changes) == 'number'), + "flags.debounce_text_changes must be nil or a number with the debounce time in milliseconds" + ) local cmd, cmd_args = lsp._cmd_parts(config.cmd) local offset_encoding = valid_encodings.UTF16 @@ -260,21 +266,171 @@ local function buf_get_full_text(bufnr) end --@private +--- Memoizes a function. On first run, the function return value is saved and +--- immediately returned on subsequent runs. If the function returns a multival, +--- only the first returned value will be memoized and returned. The function will only be run once, +--- even if it has side-effects. +--- +--@param fn (function) Function to run +--@returns (function) Memoized function +local function once(fn) + local value + local ran = false + return function(...) + if not ran then + value = fn(...) + ran = true + end + return value + end +end + + +local changetracking = {} +do + --- client_id → state + --- + --- state + --- pending_change?: function that the timer starts to trigger didChange + --- pending_changes: list of tables with the pending changesets; for incremental_sync only + --- use_incremental_sync: bool + --- buffers?: table (bufnr → lines); for incremental sync only + --- timer?: uv_timer + local state_by_client = {} + + function changetracking.init(client, bufnr) + local state = state_by_client[client.id] + if not state then + state = { + pending_changes = {}; + use_incremental_sync = ( + if_nil(client.config.flags.allow_incremental_sync, true) + and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental + ); + } + state_by_client[client.id] = state + end + if not state.use_incremental_sync then + return + end + if not state.buffers then + state.buffers = {} + end + state.buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true) + end + + function changetracking.reset_buf(client, bufnr) + local state = state_by_client[client.id] + if state then + changetracking._reset_timer(state) + if state.buffers then + state.buffers[bufnr] = nil + end + end + end + + function changetracking.reset(client_id) + local state = state_by_client[client_id] + if state then + state_by_client[client_id] = nil + changetracking._reset_timer(state) + end + end + + function changetracking.prepare(bufnr, firstline, new_lastline, changedtick) + local incremental_changes = function(client) + local cached_buffers = state_by_client[client.id].buffers + local lines = nvim_buf_get_lines(bufnr, 0, -1, true) + local startline = math.min(firstline + 1, math.min(#cached_buffers[bufnr], #lines)) + local endline = math.min(-(#lines - new_lastline), -1) + local incremental_change = vim.lsp.util.compute_diff( + cached_buffers[bufnr], lines, startline, endline, client.offset_encoding or 'utf-16') + cached_buffers[bufnr] = lines + return incremental_change + end + local full_changes = once(function() + return { + text = buf_get_full_text(bufnr); + }; + end) + local uri = vim.uri_from_bufnr(bufnr) + return function(client) + if client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.None then + return + end + local state = state_by_client[client.id] + local debounce = client.config.flags.debounce_text_changes + if not debounce then + local changes = state.use_incremental_sync and incremental_changes(client) or full_changes() + client.notify("textDocument/didChange", { + textDocument = { + uri = uri; + version = changedtick; + }; + contentChanges = { changes, } + }) + return + end + changetracking._reset_timer(state) + if state.use_incremental_sync then + -- This must be done immediately and cannot be delayed + -- The contents would further change and startline/endline may no longer fit + table.insert(state.pending_changes, incremental_changes(client)) + end + state.pending_change = function() + state.pending_change = nil + if client.is_stopped() then + return + end + local contentChanges + if state.use_incremental_sync then + contentChanges = state.pending_changes + state.pending_changes = {} + else + contentChanges = { full_changes(), } + end + client.notify("textDocument/didChange", { + textDocument = { + uri = uri; + version = changedtick; + }; + contentChanges = contentChanges + }) + end + state.timer = vim.loop.new_timer() + -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines + state.timer:start(debounce, 0, vim.schedule_wrap(state.pending_change)) + end + end + + function changetracking._reset_timer(state) + if state.timer then + state.timer:stop() + state.timer:close() + state.timer = nil + end + end + + --- Flushes any outstanding change notification. + function changetracking.flush(client) + local state = state_by_client[client.id] + if state then + changetracking._reset_timer(state) + if state.pending_change then + state.pending_change() + end + end + end +end + + +--@private --- Default handler for the 'textDocument/didOpen' LSP notification. --- --@param bufnr (Number) Number of the buffer, or 0 for current --@param client Client object local function text_document_did_open_handler(bufnr, client) - local use_incremental_sync = ( - if_nil(client.config.flags.allow_incremental_sync, true) - and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental - ) - if use_incremental_sync then - if not client._cached_buffers then - client._cached_buffers = {} - end - client._cached_buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true) - end + changetracking.init(client, bufnr) if not client.resolved_capabilities.text_document_open_close then return end @@ -327,6 +483,13 @@ end --- result. You can use this with `client.cancel_request(request_id)` --- to cancel the request. --- +--- - request_sync(method, params, timeout_ms, bufnr) +--- Sends a request to the server and synchronously waits for the response. +--- This is a wrapper around {client.request} +--- Returns: { err=err, result=result }, a dictionary, where `err` and `result` come from +--- the |lsp-handler|. On timeout, cancel or error, returns `(nil, err)` where `err` is a +--- string describing the failure reason. If the request was unsuccessful returns `nil`. +--- --- - notify(method, params) --- Sends a notification to an LSP server. --- Returns: a boolean to indicate if the notification was successful. If @@ -469,6 +632,9 @@ end --- server in the initialize request. Invalid/empty values will default to "off" --@param flags: A table with flags for the client. The current (experimental) flags are: --- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits +--- - debounce_text_changes (number, default nil): Debounce didChange +--- notifications to the server by the given number in milliseconds. No debounce +--- occurs if nil --- --@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be --- fully initialized. Use `on_init` to do any actions once @@ -563,6 +729,7 @@ function lsp.start_client(config) uninitialized_clients[client_id] = nil lsp.diagnostic.reset(client_id, all_buffer_active_clients) + changetracking.reset(client_id) all_client_active_buffers[client_id] = nil for _, client_ids in pairs(all_buffer_active_clients) do client_ids[client_id] = nil @@ -721,6 +888,9 @@ function lsp.start_client(config) handler = resolve_handler(method) or error(string.format("not found: %q request handler for client %q.", method, client.name)) end + -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state + changetracking.flush(client) + local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr) return rpc.request(method, params, function(err, result) handler(err, method, result, client_id, bufnr) @@ -728,6 +898,42 @@ function lsp.start_client(config) end --@private + --- Sends a request to the server and synchronously waits for the response. + --- + --- This is a wrapper around {client.request} + --- + --@param method (string) LSP method name. + --@param params (table) LSP request params. + --@param timeout_ms (number, optional, default=1000) Maximum time in + ---milliseconds to wait for a result. + --@param bufnr (number) Buffer handle (0 for current). + --@returns { err=err, result=result }, a dictionary, where `err` and `result` come from the |lsp-handler|. + ---On timeout, cancel or error, returns `(nil, err)` where `err` is a + ---string describing the failure reason. If the request was unsuccessful + ---returns `nil`. + --@see |vim.lsp.buf_request_sync()| + function client.request_sync(method, params, timeout_ms, bufnr) + local request_result = nil + local function _sync_handler(err, _, result) + request_result = { err = err, result = result } + end + + local success, request_id = client.request(method, params, _sync_handler, + bufnr) + if not success then return nil end + + local wait_result, reason = vim.wait(timeout_ms or 1000, function() + return request_result ~= nil + end, 10) + + if not wait_result then + client.cancel_request(request_id) + return nil, wait_result_reason[reason] + end + return request_result + end + + --@private --- Sends a notification to an LSP server. --- --@param method (string) LSP method name. @@ -753,7 +959,7 @@ function lsp.start_client(config) -- Track this so that we can escalate automatically if we've alredy tried a -- graceful shutdown - local tried_graceful_shutdown = false + local graceful_shutdown_failed = false --@private --- Stops a client, optionally with force. --- @@ -765,6 +971,7 @@ function lsp.start_client(config) function client.stop(force) lsp.diagnostic.reset(client_id, all_buffer_active_clients) + changetracking.reset(client_id) all_client_active_buffers[client_id] = nil for _, client_ids in pairs(all_buffer_active_clients) do client_ids[client_id] = nil @@ -774,11 +981,10 @@ function lsp.start_client(config) if handle:is_closing() then return end - if force or (not client.initialized) or tried_graceful_shutdown then + if force or (not client.initialized) or graceful_shutdown_failed 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 @@ -786,6 +992,7 @@ function lsp.start_client(config) else -- If there was an error in the shutdown request, then term to be safe. handle:kill(15) + graceful_shutdown_failed = true end end) end @@ -816,20 +1023,6 @@ function lsp.start_client(config) end --@private ---- Memoizes a function. On first run, the function return value is saved and ---- immediately returned on subsequent runs. ---- ---@param fn (function) Function to run ---@returns (function) Memoized function -local function once(fn) - local value - return function(...) - if not value then value = fn(...) end - return value - end -end - ---@private --@fn text_document_did_change_handler(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size) --- Notify all attached clients that a buffer has changed. local text_document_did_change_handler @@ -848,45 +1041,9 @@ do if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then return end - util.buf_versions[bufnr] = changedtick - - local incremental_changes = function(client) - local lines = nvim_buf_get_lines(bufnr, 0, -1, true) - local startline = math.min(firstline + 1, math.min(#client._cached_buffers[bufnr], #lines)) - local endline = math.min(-(#lines - new_lastline), -1) - local incremental_change = vim.lsp.util.compute_diff( - client._cached_buffers[bufnr], lines, startline, endline, client.offset_encoding or "utf-16") - client._cached_buffers[bufnr] = lines - return incremental_change - end - - local full_changes = once(function() - return { - text = buf_get_full_text(bufnr); - }; - end) - - local uri = vim.uri_from_bufnr(bufnr) - for_each_buffer_client(bufnr, function(client) - local allow_incremental_sync = if_nil(client.config.flags.allow_incremental_sync, true) - local text_document_did_change = client.resolved_capabilities.text_document_did_change - local changes - if text_document_did_change == protocol.TextDocumentSyncKind.None then - return - elseif not allow_incremental_sync 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) + local compute_change_and_notify = changetracking.prepare(bufnr, firstline, new_lastline, changedtick) + for_each_buffer_client(bufnr, compute_change_and_notify) end end @@ -956,9 +1113,7 @@ function lsp.buf_attach_client(bufnr, client_id) if client.resolved_capabilities.text_document_open_close then client.notify('textDocument/didClose', params) end - if client._cached_buffers then - client._cached_buffers[bufnr] = nil - end + changetracking.reset_buf(client, bufnr) end) util.buf_versions[bufnr] = nil all_buffer_active_clients[bufnr] = nil @@ -1133,42 +1288,77 @@ function lsp.buf_request(bufnr, method, params, handler) return client_request_ids, _cancel_all_requests end ---- Sends a request to a server and waits for the response. +---Sends an async request for all active clients attached to the buffer. +---Executes the callback on the combined result. +---Parameters are the same as |vim.lsp.buf_request()| but the return result and callback are +---different. --- ---- Calls |vim.lsp.buf_request()| but blocks Nvim while awaiting the result. +--@param bufnr (number) Buffer handle, or 0 for current. +--@param method (string) LSP method name +--@param params (optional, table) Parameters to send to the server +--@param callback (function) The callback to call when all requests are finished. +-- Unlike `buf_request`, this will collect all the responses from each server instead of handling them. +-- A map of client_id:request_result will be provided to the callback +-- +--@returns (function) A function that will cancel all requests which is the same as the one returned from `buf_request`. +function lsp.buf_request_all(bufnr, method, params, callback) + local request_results = {} + local result_count = 0 + local expected_result_count = 0 + local cancel, client_request_ids + + local set_expected_result_count = once(function() + for _ in pairs(client_request_ids) do + expected_result_count = expected_result_count + 1 + end + end) + + local function _sync_handler(err, _, result, client_id) + request_results[client_id] = { error = err, result = result } + result_count = result_count + 1 + set_expected_result_count() + + if result_count >= expected_result_count then + callback(request_results) + end + end + + client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _sync_handler) + + return cancel +end + +--- Sends a request to all server and waits for the response of all of them. +--- +--- Calls |vim.lsp.buf_request_all()| but blocks Nvim while awaiting the result. --- Parameters are the same as |vim.lsp.buf_request()| but the return result is ---- different. Wait maximum of {timeout_ms} (default 100) ms. +--- different. Wait maximum of {timeout_ms} (default 1000) ms. --- --@param bufnr (number) Buffer handle, or 0 for current. --@param method (string) LSP method name --@param params (optional, table) Parameters to send to the server ---@param timeout_ms (optional, number, default=100) Maximum time in +--@param timeout_ms (optional, number, default=1000) Maximum time in --- milliseconds to wait for a result. --- --@returns Map of client_id:request_result. On timeout, cancel or error, --- returns `(nil, err)` where `err` is a string describing the failure --- reason. function lsp.buf_request_sync(bufnr, method, params, timeout_ms) - local request_results = {} - local result_count = 0 - local function _sync_handler(err, _, 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, _sync_handler) - local expected_result_count = 0 - for _ in pairs(client_request_ids) do - expected_result_count = expected_result_count + 1 - end + local request_results + + local cancel = lsp.buf_request_all(bufnr, method, params, function(it) + request_results = it + end) - local wait_result, reason = vim.wait(timeout_ms or 100, function() - return result_count >= expected_result_count + local wait_result, reason = vim.wait(timeout_ms or 1000, function() + return request_results ~= nil end, 10) if not wait_result then cancel() return nil, wait_result_reason[reason] end + return request_results end diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 31116985e2..341a3e82fc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -111,6 +111,39 @@ function M.completion(context) return request('textDocument/completion', params) end +--@private +--- If there is more than one client that supports the given method, +--- asks the user to select one. +-- +--@returns The client that the user selected or nil +local function select_client(method) + local clients = vim.tbl_values(vim.lsp.buf_get_clients()); + clients = vim.tbl_filter(function (client) + return client.supports_method(method) + end, clients) + -- better UX when choices are always in the same order (between restarts) + table.sort(clients, function (a, b) return a.name < b.name end) + + if #clients > 1 then + local choices = {} + for k,v in ipairs(clients) do + table.insert(choices, string.format("%d %s", k, v.name)) + end + local user_choice = vim.fn.confirm( + "Select a language server:", + table.concat(choices, "\n"), + 0, + "Question" + ) + if user_choice == 0 then return nil end + return clients[user_choice] + elseif #clients < 1 then + return nil + else + return clients[1] + end +end + --- Formats the current buffer. --- --@param options (optional, table) Can be used to specify FormattingOptions. @@ -119,8 +152,11 @@ end -- --@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting function M.formatting(options) + local client = select_client("textDocument/formatting") + if client == nil then return end + local params = util.make_formatting_params(options) - return request('textDocument/formatting', params) + return client.request("textDocument/formatting", params) end --- Performs |vim.lsp.buf.formatting()| synchronously. @@ -134,14 +170,62 @@ end --- --@param options Table with valid `FormattingOptions` entries --@param timeout_ms (number) Request timeout +--@see |vim.lsp.buf.formatting_seq_sync| function M.formatting_sync(options, timeout_ms) + local client = select_client("textDocument/formatting") + if client == nil then return end + local params = util.make_formatting_params(options) - local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms) - if not result or vim.tbl_isempty(result) then return end - local _, formatting_result = next(result) - result = formatting_result.result - if not result then return end - vim.lsp.util.apply_text_edits(result) + local result, err = client.request_sync("textDocument/formatting", params, timeout_ms) + if result and result.result then + util.apply_text_edits(result.result) + elseif err then + vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN) + end +end + +--- Formats the current buffer by sequentially requesting formatting from attached clients. +--- +--- Useful when multiple clients with formatting capability are attached. +--- +--- Since it's synchronous, can be used for running on save, to make sure buffer is formatted +--- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method. +--- Example: +--- <pre> +--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]] +--- </pre> +--- +--@param options (optional, table) `FormattingOptions` entries +--@param timeout_ms (optional, number) Request timeout +--@param order (optional, table) List of client names. Formatting is requested from clients +---in the following order: first all clients that are not in the `order` list, then +---the remaining clients in the order as they occur in the `order` list. +function M.formatting_seq_sync(options, timeout_ms, order) + local clients = vim.tbl_values(vim.lsp.buf_get_clients()); + + -- sort the clients according to `order` + for _, client_name in ipairs(order or {}) do + -- if the client exists, move to the end of the list + for i, client in ipairs(clients) do + if client.name == client_name then + table.insert(clients, table.remove(clients, i)) + break + end + end + end + + -- loop through the clients and make synchronous formatting requests + for _, client in ipairs(clients) do + if client.resolved_capabilities.document_formatting then + local params = util.make_formatting_params(options) + local result, err = client.request_sync("textDocument/formatting", params, timeout_ms) + if result and result.result then + util.apply_text_edits(result.result) + elseif err then + vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN) + end + end + end end --- Formats a given range. @@ -152,15 +236,12 @@ end --@param end_pos ({number, number}, optional) mark-indexed position. ---Defaults to the end of the last visual selection. function M.range_formatting(options, start_pos, end_pos) - validate { options = {options, 't', true} } - local sts = vim.bo.softtabstop; - options = vim.tbl_extend('keep', options or {}, { - tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop; - insertSpaces = vim.bo.expandtab; - }) + local client = select_client("textDocument/rangeFormatting") + if client == nil then return end + local params = util.make_given_range_params(start_pos, end_pos) - params.options = options - return request('textDocument/rangeFormatting', params) + params.options = util.make_formatting_params(options).options + return client.request("textDocument/rangeFormatting", params) end --- Renames all references to the symbol under the cursor. diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index bd7ef9cfdc..6f2f846a3b 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -406,9 +406,7 @@ function M.get_line_diagnostics(bufnr, line_nr, opts, client_id) line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics) end - if opts.severity_sort then - table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end) - end + table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end) return line_diagnostics end @@ -997,6 +995,8 @@ end --- - See |vim.lsp.diagnostic.set_signs()| --- - update_in_insert: (default=false) --- - Update diagnostics in InsertMode or wait until InsertLeave +--- - severity_sort: (default=false) +--- - Sort diagnostics (and thus signs and virtual text) function M.on_publish_diagnostics(_, _, params, client_id, _, config) local uri = params.uri local bufnr = vim.uri_to_bufnr(uri) @@ -1007,6 +1007,10 @@ function M.on_publish_diagnostics(_, _, params, client_id, _, config) local diagnostics = params.diagnostics + if config and if_nil(config.severity_sort, false) then + table.sort(diagnostics, function(a, b) return a.severity > b.severity end) + end + -- Always save the diagnostics, even if the buf is not loaded. -- Language servers may report compile or build errors via diagnostics -- Users should be able to find these, even if they're in files which @@ -1034,6 +1038,7 @@ function M.display(diagnostics, bufnr, client_id, config) underline = true, virtual_text = true, update_in_insert = false, + severity_sort = false, }, config) -- TODO(tjdevries): Consider how we can make this a "standardized" kind of thing for |lsp-handlers|. @@ -1116,7 +1121,6 @@ end ---@return table {popup_bufnr, win_id} function M.show_line_diagnostics(opts, bufnr, line_nr, client_id) opts = opts or {} - opts.severity_sort = if_nil(opts.severity_sort, true) local show_header = if_nil(opts.show_header, true) @@ -1140,7 +1144,7 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id) local message_lines = vim.split(diagnostic.message, '\n', true) table.insert(lines, prefix..message_lines[1]) - table.insert(highlights, {#prefix + 1, hiname}) + table.insert(highlights, {#prefix, hiname}) for j = 2, #message_lines do table.insert(lines, message_lines[j]) table.insert(highlights, {0, hiname}) diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 1aa8326514..0cabd1a0d4 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -518,7 +518,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) 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 + elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then -- Server Result decoded.error = convert_NIL(decoded.error) decoded.result = convert_NIL(decoded.result) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index a070cb5306..ce8468aa8a 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -18,6 +18,40 @@ end local M = {} +local default_border = { + {"", "NormalFloat"}, + {"", "NormalFloat"}, + {"", "NormalFloat"}, + {" ", "NormalFloat"}, + {"", "NormalFloat"}, + {"", "NormalFloat"}, + {"", "NormalFloat"}, + {" ", "NormalFloat"}, +} + +--@private +-- Check the border given by opts or the default border for the additional +-- size it adds to a float. +--@returns size of border in height and width +local function get_border_size(opts) + local border = opts and opts.border or default_border + local height = 0 + local width = 0 + + if type(border) == 'string' then + -- 'single', 'double', etc. + height = 2 + width = 2 + else + height = height + vim.fn.strdisplaywidth(border[2][1]) -- top + height = height + vim.fn.strdisplaywidth(border[6][1]) -- bottom + width = width + vim.fn.strdisplaywidth(border[4][1]) -- right + width = width + vim.fn.strdisplaywidth(border[8][1]) -- left + end + + return { height = height, width = width } +end + --@private local function split_lines(value) return split(value, '\n', true) @@ -436,6 +470,7 @@ function M.apply_text_document_edit(text_document_edit, index) -- `VersionedTextDocumentIdentifier`s version may be null -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier if should_check_version and (text_document.version + and text_document.version > 0 and M.buf_versions[bufnr] and M.buf_versions[bufnr] > text_document.version) then print("Buffer ", text_document.uri, " newer than edits.") @@ -856,7 +891,7 @@ function M.make_floating_popup_options(width, height, opts) else anchor = anchor..'S' height = math.min(lines_above, height) - row = 0 + row = -get_border_size(opts).height end if vim.fn.wincol() + width <= api.nvim_get_option('columns') then @@ -875,19 +910,27 @@ function M.make_floating_popup_options(width, height, opts) row = row + (opts.offset_y or 0), style = 'minimal', width = width, - border = opts.border or { - {"", "NormalFloat"}, - {"", "NormalFloat"}, - {"", "NormalFloat"}, - {" ", "NormalFloat"}, - {"", "NormalFloat"}, - {"", "NormalFloat"}, - {"", "NormalFloat"}, - {" ", "NormalFloat"} - }, + border = opts.border or default_border, } end +local function _should_add_to_tagstack(new_item) + local stack = vim.fn.gettagstack() + + -- Check if we're at the bottom of the tagstack. + if stack.curidx <= 1 then return true end + + local top_item = stack.items[stack.curidx-1] + + -- Check if the item at the top of the tagstack is exactly the + -- same as the one we want to push. + if top_item.tagname ~= new_item.tagname then return true end + for i, v in ipairs(top_item.from) do + if v ~= new_item.from[i] then return true end + end + return false +end + --- Jumps to a location. --- --@param location (`Location`|`LocationLink`) @@ -896,22 +939,36 @@ function M.jump_to_location(location) -- location may be Location or LocationLink local uri = location.uri or location.targetUri if uri == nil then return end - local bufnr = vim.uri_to_bufnr(uri) - -- Save position in jumplist - vim.cmd "normal! m'" - -- Push a new item into tagstack - local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0} - local items = {{tagname=vim.fn.expand('<cword>'), from=from}} - vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't') + local from_bufnr = vim.fn.bufnr('%') + local from = {from_bufnr, vim.fn.line('.'), vim.fn.col('.'), 0} + local item = {tagname=vim.fn.expand('<cword>'), from=from} + + -- Save position in jumplist + vim.cmd("mark '") --- Jump to new location (adjusting for UTF-16 encoding of characters) + local bufnr = vim.uri_to_bufnr(uri) api.nvim_set_current_buf(bufnr) api.nvim_buf_set_option(0, 'buflisted', true) local range = location.range or location.targetSelectionRange local row = range.start.line local col = get_line_byte_from_position(0, range.start) + -- This prevents the tagstack to be filled with items that provide + -- no motion when CTRL-T is pressed because they're both the source + -- and the destination. + local motionless = + bufnr == from_bufnr and + row+1 == from[2] and col+1 == from[3] + if not motionless and _should_add_to_tagstack(item) then + local winid = vim.fn.win_getid() + local items = {item} + vim.fn.settagstack(winid, {items=items}, 't') + end + + -- Jump to new location api.nvim_win_set_cursor(0, {row + 1, col}) + return true end @@ -1185,6 +1242,20 @@ function M._make_floating_popup_size(contents, opts) width = math.max(line_widths[i], width) end end + + local border_width = get_border_size(opts).width + local screen_width = api.nvim_win_get_width(0) + width = math.min(width, screen_width) + + -- make sure borders are always inside the screen + if width + border_width > screen_width then + width = width - (width + border_width - screen_width) + end + + if wrap_at and wrap_at > width then + wrap_at = width + end + if max_width then width = math.min(width, max_width) wrap_at = math.min(wrap_at or max_width, max_width) diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index f223c7b8c8..de997b2d86 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -25,12 +25,12 @@ setmetatable(M, { }) --- Creates a new parser. --- --- It is not recommended to use this, use vim.treesitter.get_parser() instead. --- --- @param bufnr The buffer the parser will be tied to --- @param lang The language of the parser --- @param opts Options to pass to the language tree +--- +--- It is not recommended to use this, use vim.treesitter.get_parser() instead. +--- +--- @param bufnr The buffer the parser will be tied to +--- @param lang The language of the parser +--- @param opts Options to pass to the created language tree function M._create_parser(bufnr, lang, opts) language.require_language(lang) if bufnr == 0 then @@ -41,10 +41,12 @@ function M._create_parser(bufnr, lang, opts) local self = LanguageTree.new(bufnr, lang, opts) + ---@private local function bytes_cb(_, ...) self:_on_bytes(...) end + ---@private local function detach_cb(_, ...) if parsers[bufnr] == self then parsers[bufnr] = nil @@ -52,6 +54,7 @@ function M._create_parser(bufnr, lang, opts) self:_on_detach(...) end + ---@private local function reload_cb(_, ...) self:_on_reload(...) end @@ -64,15 +67,15 @@ function M._create_parser(bufnr, lang, opts) end --- Gets the parser for this bufnr / ft combination. --- --- If needed this will create the parser. --- Unconditionnally attach the provided callback --- --- @param bufnr The buffer the parser should be tied to --- @param ft The filetype of this parser --- @param opts Options object to pass to the parser --- --- @returns The parser +--- +--- If needed this will create the parser. +--- Unconditionnally attach the provided callback +--- +--- @param bufnr The buffer the parser should be tied to +--- @param lang The filetype of this parser +--- @param opts Options object to pass to the created language tree +--- +--- @returns The parser function M.get_parser(bufnr, lang, opts) opts = opts or {} @@ -92,6 +95,11 @@ function M.get_parser(bufnr, lang, opts) return parsers[bufnr] end +--- Gets a string parser +--- +--- @param str The string to parse +--- @param lang The language of this string +--- @param opts Options to pass to the created language tree function M.get_string_parser(str, lang, opts) vim.validate { str = { str, 'string' }, diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua index dd0b11a6c7..e031ba1bd6 100644 --- a/runtime/lua/vim/treesitter/health.lua +++ b/runtime/lua/vim/treesitter/health.lua @@ -1,10 +1,14 @@ local M = {} local ts = vim.treesitter +--- Lists the parsers currently installed +--- +---@return A list of parsers function M.list_parsers() return vim.api.nvim_get_runtime_file('parser/*', true) end +--- Performs a healthcheck for treesitter integration function M.check_health() local report_info = vim.fn['health#report_info'] local report_ok = vim.fn['health#report_ok'] diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index fe7e1052c9..84b6a5f135 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -70,11 +70,13 @@ TSHighlighter.hl_map = { ["include"] = "Include", } +---@private local function is_highlight_name(capture_name) local firstc = string.sub(capture_name, 1, 1) return firstc ~= string.lower(firstc) end +---@private function TSHighlighterQuery.new(lang, query_string) local self = setmetatable({}, { __index = TSHighlighterQuery }) @@ -99,10 +101,12 @@ function TSHighlighterQuery.new(lang, query_string) return self end +---@private function TSHighlighterQuery:query() return self._query end +---@private --- Get the hl from capture. --- Returns a tuple { highlight_name: string, is_builtin: bool } function TSHighlighterQuery:_get_hl_from_capture(capture) @@ -116,6 +120,11 @@ function TSHighlighterQuery:_get_hl_from_capture(capture) end end +--- Creates a new highlighter using @param tree +--- +--- @param tree The language tree to use for highlighting +--- @param opts Table used to configure the highlighter +--- - queries: Table to overwrite queries used by the highlighter function TSHighlighter.new(tree, opts) local self = setmetatable({}, TSHighlighter) @@ -165,12 +174,14 @@ function TSHighlighter.new(tree, opts) return self end +--- Removes all internal references to the highlighter function TSHighlighter:destroy() if TSHighlighter.active[self.bufnr] then TSHighlighter.active[self.bufnr] = nil end end +---@private function TSHighlighter:get_highlight_state(tstree) if not self._highlight_states[tstree] then self._highlight_states[tstree] = { @@ -182,24 +193,31 @@ function TSHighlighter:get_highlight_state(tstree) return self._highlight_states[tstree] end +---@private function TSHighlighter:reset_highlight_state() self._highlight_states = {} end +---@private function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end) a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) end +---@private function TSHighlighter:on_detach() self:destroy() end +---@private function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes or {}) do a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3]+1) end end +--- Gets the query used for @param lang +--- +--- @param lang A language used by the highlighter. function TSHighlighter:get_query(lang) if not self._queries[lang] then self._queries[lang] = TSHighlighterQuery.new(lang) @@ -208,6 +226,7 @@ function TSHighlighter:get_query(lang) return self._queries[lang] end +---@private local function on_line_impl(self, buf, line) self.tree:for_each_tree(function(tstree, tree) if not tstree then return end @@ -251,6 +270,7 @@ local function on_line_impl(self, buf, line) end, true) end +---@private function TSHighlighter._on_line(_, _win, buf, line, _) local self = TSHighlighter.active[buf] if not self then return end @@ -258,6 +278,7 @@ function TSHighlighter._on_line(_, _win, buf, line, _) on_line_impl(self, buf, line) end +---@private function TSHighlighter._on_buf(_, buf) local self = TSHighlighter.active[buf] if self then @@ -265,6 +286,7 @@ function TSHighlighter._on_buf(_, buf) end end +---@private function TSHighlighter._on_win(_, _win, buf, _topline) local self = TSHighlighter.active[buf] if not self then diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index eed28e0e41..6dc37c7848 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -3,12 +3,12 @@ local a = vim.api local M = {} --- Asserts that the provided language is installed, and optionally provide a path for the parser --- --- Parsers are searched in the `parser` runtime directory. --- --- @param lang The language the parser should parse --- @param path Optional path the parser is located at --- @param silent Don't throw an error if language not found +--- +--- Parsers are searched in the `parser` runtime directory. +--- +--- @param lang The language the parser should parse +--- @param path Optional path the parser is located at +--- @param silent Don't throw an error if language not found function M.require_language(lang, path, silent) if vim._ts_has_language(lang) then return true @@ -37,10 +37,10 @@ function M.require_language(lang, path, silent) end --- Inspects the provided language. --- --- Inspecting provides some useful informations on the language like node names, ... --- --- @param lang The language. +--- +--- Inspecting provides some useful informations on the language like node names, ... +--- +--- @param lang The language. function M.inspect_language(lang) M.require_language(lang) return vim._ts_inspect_language(lang) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4168c1e365..899d90e464 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -5,21 +5,26 @@ local language = require'vim.treesitter.language' local LanguageTree = {} LanguageTree.__index = LanguageTree --- Represents a single treesitter parser for a language. --- The language can contain child languages with in its range, --- hence the tree. --- --- @param source Can be a bufnr or a string of text to parse --- @param lang The language this tree represents --- @param opts Options table --- @param opts.queries A table of language to injection query strings. --- This is useful for overriding the built-in runtime file --- searching for the injection language query per language. +--- Represents a single treesitter parser for a language. +--- The language can contain child languages with in its range, +--- hence the tree. +--- +--- @param source Can be a bufnr or a string of text to parse +--- @param lang The language this tree represents +--- @param opts Options table +--- @param opts.injections A table of language to injection query strings. +--- This is useful for overriding the built-in runtime file +--- searching for the injection language query per language. function LanguageTree.new(source, lang, opts) language.require_language(lang) opts = opts or {} - local custom_queries = opts.queries or {} + if opts.queries then + a.nvim_err_writeln("'queries' is no longer supported. Use 'injections' now") + opts.injections = opts.queries + end + + local injections = opts.injections or {} local self = setmetatable({ _source = source, _lang = lang, @@ -27,8 +32,8 @@ function LanguageTree.new(source, lang, opts) _regions = {}, _trees = {}, _opts = opts, - _injection_query = custom_queries[lang] - and query.parse_query(lang, custom_queries[lang]) + _injection_query = injections[lang] + and query.parse_query(lang, injections[lang]) or query.get_query(lang, "injections"), _valid = false, _parser = vim._create_ts_parser(lang), @@ -45,7 +50,7 @@ function LanguageTree.new(source, lang, opts) return self end --- Invalidates this parser and all its children +--- Invalidates this parser and all its children function LanguageTree:invalidate(reload) self._valid = false @@ -59,38 +64,38 @@ function LanguageTree:invalidate(reload) end end --- Returns all trees this language tree contains. --- Does not include child languages. +--- Returns all trees this language tree contains. +--- Does not include child languages. function LanguageTree:trees() return self._trees end --- Gets the language of this tree layer. +--- Gets the language of this tree node. function LanguageTree:lang() return self._lang end --- Determines whether this tree is valid. --- If the tree is invalid, `parse()` must be called --- to get the an updated tree. +--- Determines whether this tree is valid. +--- If the tree is invalid, `parse()` must be called +--- to get the an updated tree. function LanguageTree:is_valid() return self._valid end --- Returns a map of language to child tree. +--- Returns a map of language to child tree. function LanguageTree:children() return self._children end --- Returns the source content of the language tree (bufnr or string). +--- Returns the source content of the language tree (bufnr or string). function LanguageTree:source() return self._source end --- Parses all defined regions using a treesitter parser --- for the language this tree represents. --- This will run the injection query for this language to --- determine if any child languages should be created. +--- Parses all defined regions using a treesitter parser +--- for the language this tree represents. +--- This will run the injection query for this language to +--- determine if any child languages should be created. function LanguageTree:parse() if self._valid then return self._trees @@ -164,9 +169,10 @@ function LanguageTree:parse() return self._trees, changes end --- Invokes the callback for each LanguageTree and it's children recursively --- @param fn The function to invoke. This is invoked with arguments (tree: LanguageTree, lang: string) --- @param include_self Whether to include the invoking tree in the results. +--- Invokes the callback for each LanguageTree and it's children recursively +--- +--- @param fn The function to invoke. This is invoked with arguments (tree: LanguageTree, lang: string) +--- @param include_self Whether to include the invoking tree in the results. function LanguageTree:for_each_child(fn, include_self) if include_self then fn(self, self._lang) @@ -177,10 +183,12 @@ function LanguageTree:for_each_child(fn, include_self) end end --- Invokes the callback for each treesitter trees recursively. --- Note, this includes the invoking language tree's trees as well. --- @param fn The callback to invoke. The callback is invoked with arguments --- (tree: TSTree, languageTree: LanguageTree) +--- Invokes the callback for each treesitter trees recursively. +--- +--- Note, this includes the invoking language tree's trees as well. +--- +--- @param fn The callback to invoke. The callback is invoked with arguments +--- (tree: TSTree, languageTree: LanguageTree) function LanguageTree:for_each_tree(fn) for _, tree in ipairs(self._trees) do fn(tree, self) @@ -191,9 +199,11 @@ function LanguageTree:for_each_tree(fn) end end --- Adds a child language to this tree. --- If the language already exists as a child, it will first be removed. --- @param lang The language to add. +--- Adds a child language to this tree. +--- +--- If the language already exists as a child, it will first be removed. +--- +--- @param lang The language to add. function LanguageTree:add_child(lang) if self._children[lang] then self:remove_child(lang) @@ -207,8 +217,9 @@ function LanguageTree:add_child(lang) return self._children[lang] end --- Removes a child language from this tree. --- @param lang The language to remove. +--- Removes a child language from this tree. +--- +--- @param lang The language to remove. function LanguageTree:remove_child(lang) local child = self._children[lang] @@ -220,10 +231,11 @@ function LanguageTree:remove_child(lang) end end --- Destroys this language tree and all its children. --- Any cleanup logic should be performed here. --- Note, this DOES NOT remove this tree from a parent. --- `remove_child` must be called on the parent to remove it. +--- Destroys this language tree and all its children. +--- +--- Any cleanup logic should be performed here. +--- Note, this DOES NOT remove this tree from a parent. +--- `remove_child` must be called on the parent to remove it. function LanguageTree:destroy() -- Cleanup here for _, child in ipairs(self._children) do @@ -231,23 +243,23 @@ function LanguageTree:destroy() end end --- Sets the included regions that should be parsed by this parser. --- A region is a set of nodes and/or ranges that will be parsed in the same context. --- --- For example, `{ { node1 }, { node2} }` is two separate regions. --- This will be parsed by the parser in two different contexts... thus resulting --- in two separate trees. --- --- `{ { node1, node2 } }` is a single region consisting of two nodes. --- This will be parsed by the parser in a single context... thus resulting --- in a single tree. --- --- This allows for embedded languages to be parsed together across different --- nodes, which is useful for templating languages like ERB and EJS. --- --- Note, this call invalidates the tree and requires it to be parsed again. --- --- @param regions A list of regions this tree should manage and parse. +--- Sets the included regions that should be parsed by this parser. +--- A region is a set of nodes and/or ranges that will be parsed in the same context. +--- +--- For example, `{ { node1 }, { node2} }` is two separate regions. +--- This will be parsed by the parser in two different contexts... thus resulting +--- in two separate trees. +--- +--- `{ { node1, node2 } }` is a single region consisting of two nodes. +--- This will be parsed by the parser in a single context... thus resulting +--- in a single tree. +--- +--- This allows for embedded languages to be parsed together across different +--- nodes, which is useful for templating languages like ERB and EJS. +--- +--- Note, this call invalidates the tree and requires it to be parsed again. +--- +--- @param regions A list of regions this tree should manage and parse. function LanguageTree:set_included_regions(regions) -- TODO(vigoux): I don't think string parsers are useful for now if type(self._source) == "number" then @@ -276,16 +288,18 @@ function LanguageTree:set_included_regions(regions) self:invalidate() end --- Gets the set of included regions +--- Gets the set of included regions function LanguageTree:included_regions() return self._regions end --- Gets language injection points by language. --- This is where most of the injection processing occurs. --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. --- @private +--- Gets language injection points by language. +--- +--- This is where most of the injection processing occurs. +--- +--- TODO: Allow for an offset predicate to tailor the injection range +--- instead of using the entire nodes range. +--- @private function LanguageTree:_get_injections() if not self._injection_query then return {} end @@ -297,33 +311,50 @@ function LanguageTree:_get_injections() for pattern, match, metadata in self._injection_query:iter_matches(root_node, self._source, start_line, end_line+1) do local lang = nil - local injection_node = nil - local combined = false + local ranges = {} + local combined = metadata.combined + + -- Directives can configure how injections are captured as well as actual node captures. + -- This allows more advanced processing for determining ranges and language resolution. + if metadata.content then + local content = metadata.content + + -- Allow for captured nodes to be used + if type(content) == "number" then + content = {match[content]} + end + + if content then + vim.list_extend(ranges, content) + end + end + + if metadata.language then + lang = metadata.language + end -- You can specify the content and language together -- using a tag with the language, for example -- @javascript for id, node in pairs(match) do - local data = metadata[id] local name = self._injection_query.captures[id] - local offset_range = data and data.offset -- Lang should override any other language tag - if name == "language" then + if name == "language" and not lang then lang = query.get_node_text(node, self._source) elseif name == "combined" then combined = true - elseif name == "content" then - injection_node = offset_range or node + elseif name == "content" and #ranges == 0 then + table.insert(ranges, node) -- Ignore any tags that start with "_" -- Allows for other tags to be used in matches elseif string.sub(name, 1, 1) ~= "_" then - if lang == nil then + if not lang then lang = name end - if not injection_node then - injection_node = offset_range or node + if #ranges == 0 then + table.insert(ranges, node) end end end @@ -337,21 +368,21 @@ function LanguageTree:_get_injections() injections[tree_index][lang] = {} end - -- Key by pattern so we can either combine each node to parse in the same - -- context or treat each node independently. + -- Key this by pattern. If combined is set to true all captures of this pattern + -- will be parsed by treesitter as the same "source". + -- If combined is false, each "region" will be parsed as a single source. if not injections[tree_index][lang][pattern] then - injections[tree_index][lang][pattern] = { combined = combined, nodes = {} } + injections[tree_index][lang][pattern] = { combined = combined, regions = {} } end - table.insert(injections[tree_index][lang][pattern].nodes, injection_node) + table.insert(injections[tree_index][lang][pattern].regions, ranges) end end local result = {} -- Generate a map by lang of node lists. - -- Each list is a set of ranges that should be parsed - -- together. + -- Each list is a set of ranges that should be parsed together. for _, lang_map in ipairs(injections) do for lang, patterns in pairs(lang_map) do if not result[lang] then @@ -360,10 +391,10 @@ function LanguageTree:_get_injections() for _, entry in pairs(patterns) do if entry.combined then - table.insert(result[lang], entry.nodes) + table.insert(result[lang], vim.tbl_flatten(entry.regions)) else - for _, node in ipairs(entry.nodes) do - table.insert(result[lang], {node}) + for _, ranges in ipairs(entry.regions) do + table.insert(result[lang], ranges) end end end @@ -373,12 +404,14 @@ function LanguageTree:_get_injections() return result end +---@private function LanguageTree:_do_callback(cb_name, ...) for _, cb in ipairs(self._callbacks[cb_name]) do cb(...) end end +---@private function LanguageTree:_on_bytes(bufnr, changed_tick, start_row, start_col, start_byte, old_row, old_col, old_byte, @@ -403,24 +436,26 @@ function LanguageTree:_on_bytes(bufnr, changed_tick, new_row, new_col, new_byte) end +---@private function LanguageTree:_on_reload() self:invalidate(true) end +---@private function LanguageTree:_on_detach(...) self:invalidate(true) self:_do_callback('detach', ...) end --- Registers callbacks for the parser --- @param cbs An `nvim_buf_attach`-like table argument with the following keys : --- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback. --- `on_changedtree` : a callback that will be called every time the tree has syntactical changes. --- it will only be passed one argument, that is a table of the ranges (as node ranges) that --- changed. --- `on_child_added` : emitted when a child is added to the tree. --- `on_child_removed` : emitted when a child is removed from the tree. +--- @param cbs An `nvim_buf_attach`-like table argument with the following keys : +--- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback. +--- `on_changedtree` : a callback that will be called every time the tree has syntactical changes. +--- it will only be passed one argument, that is a table of the ranges (as node ranges) that +--- changed. +--- `on_child_added` : emitted when a child is added to the tree. +--- `on_child_removed` : emitted when a child is removed from the tree. function LanguageTree:register_cbs(cbs) if not cbs then return end @@ -445,6 +480,7 @@ function LanguageTree:register_cbs(cbs) end end +---@private local function tree_contains(tree, range) local start_row, start_col, end_row, end_col = tree:root():range() local start_fits = start_row < range[1] or (start_row == range[1] and start_col <= range[2]) @@ -457,6 +493,11 @@ local function tree_contains(tree, range) return false end +--- Determines wether @param range is contained in this language tree +--- +--- This goes down the tree to recursively check childs. +--- +--- @param range A range, that is a `{ start_line, start_col, end_line, end_col }` table. function LanguageTree:contains(range) for _, tree in pairs(self._trees) do if tree_contains(tree, range) then @@ -467,6 +508,9 @@ function LanguageTree:contains(range) return false end +--- Gets the appropriate language that contains @param range +--- +--- @param range A text range, see |LanguageTree:contains| function LanguageTree:language_for_range(range) for _, child in pairs(self._children) do if child:contains(range) then diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index f40e1d5294..db6d7e4dc0 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -8,6 +8,7 @@ Query.__index = Query local M = {} +---@private local function dedupe_files(files) local result = {} local seen = {} @@ -22,6 +23,7 @@ local function dedupe_files(files) return result end +---@private local function safe_read(filename, read_quantifier) local file, err = io.open(filename, 'r') if not file then @@ -32,6 +34,11 @@ local function safe_read(filename, read_quantifier) return content end +--- Gets the list of files used to make up a query +--- +--- @param lang The language +--- @param query_name The name of the query to load +--- @param is_included Internal parameter, most of the time left as `nil` function M.get_query_files(lang, query_name, is_included) local query_path = string.format('queries/%s/%s.scm', lang, query_name) local lang_files = dedupe_files(a.nvim_get_runtime_file(query_path, true)) @@ -79,6 +86,7 @@ function M.get_query_files(lang, query_name, is_included) return query_files end +---@private local function read_query_files(filenames) local contents = {} @@ -89,17 +97,6 @@ local function read_query_files(filenames) return table.concat(contents, '') end -local match_metatable = { - __index = function(tbl, key) - rawset(tbl, key, {}) - return tbl[key] - end -} - -local function new_match_metadata() - return setmetatable({}, match_metatable) -end - --- The explicitly set queries from |vim.treesitter.query.set_query()| local explicit_queries = setmetatable({}, { __index = function(t, k) @@ -114,19 +111,20 @@ local explicit_queries = setmetatable({}, { --- --- This allows users to override any runtime files and/or configuration --- set by plugins. ----@param lang string: The language to use for the query ----@param query_name string: The name of the query (i.e. "highlights") ----@param text string: The query text (unparsed). +--- +--- @param lang string: The language to use for the query +--- @param query_name string: The name of the query (i.e. "highlights") +--- @param text string: The query text (unparsed). function M.set_query(lang, query_name, text) explicit_queries[lang][query_name] = M.parse_query(lang, text) end --- Returns the runtime query {query_name} for {lang}. --- --- @param lang The language to use for the query --- @param query_name The name of the query (i.e. "highlights") --- --- @return The corresponding query, parsed. +--- +--- @param lang The language to use for the query +--- @param query_name The name of the query (i.e. "highlights") +--- +--- @return The corresponding query, parsed. function M.get_query(lang, query_name) if explicit_queries[lang][query_name] then return explicit_queries[lang][query_name] @@ -140,12 +138,23 @@ function M.get_query(lang, query_name) end end ---- Parses a query. --- --- @param language The language --- @param query A string containing the query (s-expr syntax) --- --- @returns The query +--- Parse {query} as a string. (If the query is in a file, the caller +--- should read the contents into a string before calling). +--- +--- Returns a `Query` (see |lua-treesitter-query|) object which can be used to +--- search nodes in the syntax tree for the patterns defined in {query} +--- using `iter_*` methods below. +--- +--- Exposes `info` and `captures` with additional information about the {query}. +--- - `captures` contains the list of unique capture names defined in +--- {query}. +--- -` info.captures` also points to `captures`. +--- - `info.patterns` contains information about predicates. +--- +--- @param lang The language +--- @param query A string containing the query (s-expr syntax) +--- +--- @returns The query function M.parse_query(lang, query) language.require_language(lang) local self = setmetatable({}, Query) @@ -158,8 +167,9 @@ end -- TODO(vigoux): support multiline nodes too --- Gets the text corresponding to a given node --- @param node the node --- @param bufnr the buffer from which the node is extracted. +--- +--- @param node the node +--- @param bsource The buffer or string from which the node is extracted function M.get_node_text(node, source) local start_row, start_col, start_byte = node:start() local end_row, end_col, end_byte = node:end_() @@ -211,6 +221,7 @@ local predicate_handlers = { ["match?"] = (function() local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true} + ---@private local function check_magic(str) if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then return str @@ -259,7 +270,7 @@ predicate_handlers["vim-match?"] = predicate_handlers["match?"] -- Directives store metadata or perform side effects against a match. -- Directives should always end with a `!`. -- Directive handler receive the following arguments --- (match, pattern, bufnr, predicate) +-- (match, pattern, bufnr, predicate, metadata) local directive_handlers = { ["set!"] = function(_, _, _, pred, metadata) if #pred == 4 then @@ -279,7 +290,6 @@ local directive_handlers = { local start_col_offset = pred[4] or 0 local end_row_offset = pred[5] or 0 local end_col_offset = pred[6] or 0 - local key = pred[7] or "offset" range[1] = range[1] + start_row_offset range[2] = range[2] + start_col_offset @@ -288,16 +298,16 @@ local directive_handlers = { -- If this produces an invalid range, we just skip it. if range[1] < range[3] or (range[1] == range[3] and range[2] <= range[4]) then - metadata[pred[2]][key] = range + metadata.content = {range} end end } --- Adds a new predicate to be used in queries --- --- @param name the name of the predicate, without leading # --- @param handler the handler function to be used --- signature will be (match, pattern, bufnr, predicate) +--- +--- @param name the name of the predicate, without leading # +--- @param handler the handler function to be used +--- signature will be (match, pattern, bufnr, predicate) function M.add_predicate(name, handler, force) if predicate_handlers[name] and not force then error(string.format("Overriding %s", name)) @@ -307,10 +317,10 @@ function M.add_predicate(name, handler, force) end --- Adds a new directive to be used in queries --- --- @param name the name of the directive, without leading # --- @param handler the handler function to be used --- signature will be (match, pattern, bufnr, predicate) +--- +--- @param name the name of the directive, without leading # +--- @param handler the handler function to be used +--- signature will be (match, pattern, bufnr, predicate) function M.add_directive(name, handler, force) if directive_handlers[name] and not force then error(string.format("Overriding %s", name)) @@ -324,14 +334,17 @@ function M.list_predicates() return vim.tbl_keys(predicate_handlers) end +---@private local function xor(x, y) return (x or y) and not (x and y) end +---@private local function is_directive(name) return string.sub(name, -1) == "!" end +---@private function Query:match_preds(match, pattern, source) local preds = self.info.patterns[pattern] @@ -370,7 +383,7 @@ function Query:match_preds(match, pattern, source) return true end ---- Applies directives against a match and pattern. +---@private function Query:apply_directives(match, pattern, source, metadata) local preds = self.info.patterns[pattern] @@ -392,6 +405,7 @@ end --- Returns the start and stop value if set else the node's range. -- When the node's range is used, the stop is incremented by 1 -- to make the search inclusive. +---@private local function value_or_node_range(start, stop, node) if start == nil and stop == nil then local node_start, _, node_stop, _ = node:range() @@ -401,15 +415,36 @@ local function value_or_node_range(start, stop, node) return start, stop end ---- Iterates of the captures of self on a given range. --- --- @param node The node under which the search will occur --- @param buffer The source buffer to search --- @param start The starting line of the search --- @param stop The stopping line of the search (end-exclusive) --- --- @returns The matching capture id --- @returns The captured node +--- Iterate over all captures from all matches inside {node} +--- +--- {source} is needed if the query contains predicates, then the caller +--- must ensure to use a freshly parsed tree consistent with the current +--- text of the buffer (if relevent). {start_row} and {end_row} can be used to limit +--- matches inside a row range (this is typically used with root node +--- as the node, i e to get syntax highlight matches in the current +--- viewport). When omitted the start and end row values are used from the given node. +--- +--- The iterator returns three values, a numeric id identifying the capture, +--- the captured node, and metadata from any directives processing the match. +--- The following example shows how to get captures by name: +--- +--- <pre> +--- for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do +--- local name = query.captures[id] -- name of the capture in the query +--- -- typically useful info about the node: +--- local type = node:type() -- type of the captured node +--- local row1, col1, row2, col2 = node:range() -- range of the capture +--- ... use the info here ... +--- end +--- </pre> +--- +--- @param node The node under which the search will occur +--- @param source The source buffer or string to exctract text from +--- @param start The starting line of the search +--- @param stop The stopping line of the search (end-exclusive) +--- +--- @returns The matching capture id +--- @returns The captured node function Query:iter_captures(node, source, start, stop) if type(source) == "number" and source == 0 then source = vim.api.nvim_get_current_buf() @@ -418,9 +453,10 @@ function Query:iter_captures(node, source, start, stop) start, stop = value_or_node_range(start, stop, node) local raw_iter = node:_rawquery(self.query, true, start, stop) + ---@private local function iter() local capture, captured_node, match = raw_iter() - local metadata = new_match_metadata() + local metadata = {} if match ~= nil then local active = self:match_preds(match, match.pattern, source) @@ -437,14 +473,35 @@ function Query:iter_captures(node, source, start, stop) end --- Iterates the matches of self on a given range. --- --- @param node The node under which the search will occur --- @param buffer The source buffer to search --- @param start The starting line of the search --- @param stop The stopping line of the search (end-exclusive) --- --- @returns The matching pattern id --- @returns The matching match +--- +--- Iterate over all matches within a node. The arguments are the same as +--- for |query:iter_captures()| but the iterated values are different: +--- an (1-based) index of the pattern in the query, a table mapping +--- capture indices to nodes, and metadata from any directives processing the match. +--- If the query has more than one pattern the capture table might be sparse, +--- and e.g. `pairs()` method should be used over `ipairs`. +--- Here an example iterating over all captures in every match: +--- +--- <pre> +--- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do +--- for id, node in pairs(match) do +--- local name = query.captures[id] +--- -- `node` was captured by the `name` capture in the match +--- +--- local node_data = metadata[id] -- Node level metadata +--- +--- ... use the info here ... +--- end +--- end +--- </pre> +--- +--- @param node The node under which the search will occur +--- @param source The source buffer or string to search +--- @param start The starting line of the search +--- @param stop The stopping line of the search (end-exclusive) +--- +--- @returns The matching pattern id +--- @returns The matching match function Query:iter_matches(node, source, start, stop) if type(source) == "number" and source == 0 then source = vim.api.nvim_get_current_buf() @@ -455,7 +512,7 @@ function Query:iter_matches(node, source, start, stop) local raw_iter = node:_rawquery(self.query, false, start, stop) local function iter() local pattern, match = raw_iter() - local metadata = new_match_metadata() + local metadata = {} if match ~= nil then local active = self:match_preds(match, pattern, source) diff --git a/runtime/menu.vim b/runtime/menu.vim index cd56eb5583..78306a57b8 100644 --- a/runtime/menu.vim +++ b/runtime/menu.vim @@ -2,7 +2,7 @@ " You can also use this as a start for your own set of menus. " " Maintainer: Bram Moolenaar <Bram@vim.org> -" Last Change: 2019 Jan 27 +" Last Change: 2019 Dec 10 " Note that ":an" (short for ":anoremenu") is often used to make a menu work " in all modes and avoid side effects from mappings defined by the user. @@ -690,11 +690,11 @@ func! s:BMShow(...) let g:bmenu_priority = a:1 endif - " remove old menu, if exists; keep one entry to avoid a torn off menu to - " disappear. - silent! unmenu &Buffers + " Remove old menu, if exists; keep one entry to avoid a torn off menu to + " disappear. Use try/catch to avoid setting v:errmsg + try | unmenu &Buffers | catch | endtry exe 'noremenu ' . g:bmenu_priority . ".1 &Buffers.Dummy l" - silent! unmenu! &Buffers + try | unmenu! &Buffers | catch | endtry " create new menu; set 'cpo' to include the <CR> let cpo_save = &cpo diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py index b4d896fecc..d46306d41a 100755 --- a/scripts/gen_vimdoc.py +++ b/scripts/gen_vimdoc.py @@ -186,6 +186,48 @@ CONFIG = { 'module_override': {}, 'append_only': [], }, + 'treesitter': { + 'mode': 'lua', + 'filename': 'treesitter.txt', + 'section_start_token': '*lua-treesitter-core*', + 'section_order': [ + 'treesitter.lua', + 'language.lua', + 'query.lua', + 'highlighter.lua', + 'languagetree.lua', + 'health.lua', + ], + 'files': ' '.join([ + os.path.join(base_dir, 'runtime/lua/vim/treesitter.lua'), + os.path.join(base_dir, 'runtime/lua/vim/treesitter/'), + ]), + 'file_patterns': '*.lua', + 'fn_name_prefix': '', + 'section_name': {}, + 'section_fmt': lambda name: ( + 'Lua module: vim.treesitter' + if name.lower() == 'treesitter' + else f'Lua module: vim.treesitter.{name.lower()}'), + 'helptag_fmt': lambda name: ( + '*lua-treesitter-core*' + if name.lower() == 'treesitter' + else f'*treesitter-{name.lower()}*'), + 'fn_helptag_fmt': lambda fstem, name: ( + f'*{name}()*' + if name != 'new' + else f'*{fstem}.{name}()*'), + # 'fn_helptag_fmt': lambda fstem, name: ( + # f'*vim.treesitter.{name}()*' + # if fstem == 'treesitter' + # else ( + # '*vim.lsp.client*' + # # HACK. TODO(justinmk): class/structure support in lua2dox + # if 'lsp.client' == f'{fstem}.{name}' + # else f'*vim.lsp.{fstem}.{name}()*')), + 'module_override': {}, + 'append_only': [], + } } param_exclude = ( @@ -666,15 +708,6 @@ def extract_from_xml(filename, target, width): annotations = filter(None, map(lambda x: annotation_map.get(x), annotations.split())) - if not fmt_vimhelp: - pass - else: - fstem = '?' - if '.' in compoundname: - fstem = compoundname.split('.')[0] - fstem = CONFIG[target]['module_override'].get(fstem, fstem) - vimtag = CONFIG[target]['fn_helptag_fmt'](fstem, name) - params = [] type_length = 0 @@ -695,17 +728,37 @@ def extract_from_xml(filename, target, width): if fmt_vimhelp and param_type.endswith('*'): param_type = param_type.strip('* ') param_name = '*' + param_name + type_length = max(type_length, len(param_type)) params.append((param_type, param_name)) + # Handle Object Oriented style functions here. + # We make sure they have "self" in the parameters, + # and a parent function + if return_type.startswith('function') \ + and len(return_type.split(' ')) >= 2 \ + and any(x[1] == 'self' for x in params): + split_return = return_type.split(' ') + name = f'{split_return[1]}:{name}' + c_args = [] for param_type, param_name in params: c_args.append((' ' if fmt_vimhelp else '') + ( '%s %s' % (param_type.ljust(type_length), param_name)).strip()) + if not fmt_vimhelp: + pass + else: + fstem = '?' + if '.' in compoundname: + fstem = compoundname.split('.')[0] + fstem = CONFIG[target]['module_override'].get(fstem, fstem) + vimtag = CONFIG[target]['fn_helptag_fmt'](fstem, name) + prefix = '%s(' % name suffix = '%s)' % ', '.join('{%s}' % a[1] for a in params if a[0] not in ('void', 'Error')) + if not fmt_vimhelp: c_decl = '%s %s(%s);' % (return_type, name, ', '.join(c_args)) signature = prefix + suffix @@ -774,7 +827,9 @@ def extract_from_xml(filename, target, width): xrefs.clear() - fns = collections.OrderedDict(sorted(fns.items())) + fns = collections.OrderedDict(sorted( + fns.items(), + key=lambda key_item_tuple: key_item_tuple[0].lower())) deprecated_fns = collections.OrderedDict(sorted(deprecated_fns.items())) return (fns, deprecated_fns) @@ -1002,6 +1057,7 @@ def main(config, args): title, helptag, section_doc = sections.pop(filename) except KeyError: msg(f'warning: empty docs, skipping (target={target}): {filename}') + msg(f' existing docs: {sections.keys()}') continue i += 1 if filename not in CONFIG[target]['append_only']: diff --git a/scripts/lua2dox.lua b/scripts/lua2dox.lua index 1dc4c0a5a0..0b36a1e061 100644 --- a/scripts/lua2dox.lua +++ b/scripts/lua2dox.lua @@ -491,6 +491,27 @@ function TLua2DoX_filter.readfile(this,AppStamp,Filename) end end + -- Big hax + if string.find(fn, ":") then + -- TODO: We need to add a first parameter of "SELF" here + -- local colon_place = string.find(fn, ":") + -- local name = string.sub(fn, 1, colon_place) + fn = fn:gsub(":", ".", 1) + outStream:writeln("/// @param self") + + local paren_start = string.find(fn, "(", 1, true) + local paren_finish = string.find(fn, ")", 1, true) + + -- Nothing in between the parens + local comma + if paren_finish == paren_start + 1 then + comma = "" + else + comma = ", " + end + fn = string.sub(fn, 1, paren_start) .. "self" .. comma .. string.sub(fn, paren_start + 1) + end + -- add vanilla function outStream:writeln(fn_type .. 'function ' .. fn .. '{}') end diff --git a/scripts/pvscheck.sh b/scripts/pvscheck.sh index f054f6e6fe..f3371b485e 100755 --- a/scripts/pvscheck.sh +++ b/scripts/pvscheck.sh @@ -389,7 +389,7 @@ run_analysis() {( detect_url() { local url="${1:-detect}" if test "$url" = detect ; then - curl --silent -L 'https://www.viva64.com/en/pvs-studio-download/' \ + curl --silent -L 'https://pvs-studio.com/en/pvs-studio/download-all/' \ | grep -o 'https\{0,1\}://[^"<>]\{1,\}/pvs-studio[^/"<>]*-x86_64\.tgz' \ || echo FAILED else diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 2c9d655a15..8b422b3abe 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -506,11 +506,9 @@ if(WIN32) "file(MAKE_DIRECTORY \"${PROJECT_BINARY_DIR}/windows_runtime_deps/platforms\")") foreach(DEP_FILE IN ITEMS ca-bundle.crt - cat.exe curl.exe diff.exe tee.exe - tidy.exe win32yank.exe winpty-agent.exe winpty.dll diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index c55dc39605..cc5a62a170 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -1426,6 +1426,10 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, /// - "eol": right after eol character (default) /// - "overlay": display over the specified column, without /// shifting the underlying text. +/// - "right_align": display right aligned in the window. +/// - virt_text_win_col : position the virtual text at a fixed +/// window column (starting from the first +/// text column) /// - virt_text_hide : hide the virtual text when the background /// text is selected or hidden due to /// horizontal scroll 'nowrap' @@ -1437,6 +1441,10 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, /// default /// - "combine": combine with background text color /// - "blend": blend with background text color. +/// - hl_eol : when true, for a multiline highlight covering the +/// EOL of a line, continue the highlight for the rest +/// of the screen line (just like for diff and +/// cursorline highlight). /// /// - ephemeral : for use with |nvim_set_decoration_provider| /// callbacks. The mark will only be used for the current @@ -1570,17 +1578,33 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, decor.virt_text_pos = kVTEndOfLine; } else if (strequal("overlay", str.data)) { decor.virt_text_pos = kVTOverlay; + } else if (strequal("right_align", str.data)) { + decor.virt_text_pos = kVTRightAlign; } else { api_set_error(err, kErrorTypeValidation, "virt_text_pos: invalid value"); goto error; } + } else if (strequal("virt_text_win_col", k.data)) { + if (v->type != kObjectTypeInteger) { + api_set_error(err, kErrorTypeValidation, + "virt_text_win_col is not a Number of the correct size"); + goto error; + } + + decor.col = (int)v->data.integer; + decor.virt_text_pos = kVTWinCol; } else if (strequal("virt_text_hide", k.data)) { decor.virt_text_hide = api_object_to_bool(*v, "virt_text_hide", false, err); if (ERROR_SET(err)) { goto error; } + } else if (strequal("hl_eol", k.data)) { + decor.hl_eol = api_object_to_bool(*v, "hl_eol", false, err); + if (ERROR_SET(err)) { + goto error; + } } else if (strequal("hl_mode", k.data)) { if (v->type != kObjectTypeString) { api_set_error(err, kErrorTypeValidation, @@ -1664,12 +1688,21 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, col2 = 0; } + if (decor.virt_text_pos == kVTRightAlign) { + decor.col = 0; + for (size_t i = 0; i < kv_size(decor.virt_text); i++) { + decor.col += mb_string2cells((char_u *)kv_A(decor.virt_text, i).text); + } + } + + Decoration *d = NULL; if (ephemeral) { d = &decor; } else if (kv_size(decor.virt_text) - || decor.priority != DECOR_PRIORITY_BASE) { + || decor.priority != DECOR_PRIORITY_BASE + || decor.hl_eol) { // TODO(bfredl): this is a bit sketchy. eventually we should // have predefined decorations for both marks/ephemerals d = xcalloc(1, sizeof(*d)); @@ -1680,7 +1713,7 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, // TODO(bfredl): synergize these two branches even more if (ephemeral && decor_state.buf == buf) { - decor_add_ephemeral((int)line, (int)col, line2, col2, &decor, 0); + decor_add_ephemeral((int)line, (int)col, line2, col2, &decor); } else { if (ephemeral) { api_set_error(err, kErrorTypeException, "not yet implemented"); diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index c73a9195c3..24ba6110c4 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -1765,6 +1765,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) { "double", { "╔", "═", "╗", "║", "╝", "═", "╚", "║" }, false }, { "single", { "┌", "─", "┐", "│", "┘", "─", "└", "│" }, false }, { "shadow", { "", "", " ", " ", " ", " ", " ", "" }, true }, + { "solid", { " ", " ", " ", " ", " ", " ", " ", " " }, false }, { NULL, { { NUL } } , false }, }; diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index b5e53beabe..c363c77afb 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -104,10 +104,14 @@ String nvim_exec(String src, Boolean output, Error *err) } try_start(); - msg_silent++; + if (output) { + msg_silent++; + } do_source_str(src.data, "nvim_exec()"); - capture_ga = save_capture_ga; - msg_silent = save_msg_silent; + if (output) { + capture_ga = save_capture_ga; + msg_silent = save_msg_silent; + } try_end(err); if (ERROR_SET(err)) { @@ -1263,6 +1267,7 @@ fail: /// @param buffer the buffer to use (expected to be empty) /// @param opts Optional parameters. Reserved for future use. /// @param[out] err Error details, if any +/// @return Channel id, or 0 on error Integer nvim_open_term(Buffer buffer, Dictionary opts, Error *err) FUNC_API_SINCE(7) { diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c index 89fa2f86fb..f942d6b19f 100644 --- a/src/nvim/api/window.c +++ b/src/nvim/api/window.c @@ -381,7 +381,7 @@ Integer nvim_win_get_number(Window window, Error *err) } int tabnr; - win_get_tabwin(window, &tabnr, &rv); + win_get_tabwin(win->handle, &tabnr, &rv); return rv; } diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index c98f2786c2..ce4163fccf 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -1844,7 +1844,7 @@ buf_T *buflist_new(char_u *ffname_arg, char_u *sfname_arg, linenr_T lnum, EMSG(_("W14: Warning: List of file names overflow")); if (emsg_silent == 0) { ui_flush(); - os_delay(3000L, true); // make sure it is noticed + os_delay(3001L, true); // make sure it is noticed } top_file_num = 1; } diff --git a/src/nvim/change.c b/src/nvim/change.c index 38bd591eca..74e27ca880 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -68,7 +68,7 @@ void change_warning(int col) (void)msg_end(); if (msg_silent == 0 && !silent_mode && ui_active()) { ui_flush(); - os_delay(1000L, true); // give the user time to think about it + os_delay(1002L, true); // give the user time to think about it } curbuf->b_did_warn = true; redraw_cmdline = false; // don't redraw and erase the message @@ -109,7 +109,7 @@ void changed(void) // and don't let the emsg() set msg_scroll. if (need_wait_return && emsg_silent == 0) { ui_flush(); - os_delay(2000L, true); + os_delay(2002L, true); wait_return(true); msg_scroll = save_msg_scroll; } else { diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index 52a48ae6fb..ca1d141dd8 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -144,9 +144,9 @@ bool decor_redraw_reset(buf_T *buf, DecorState *state) state->row = -1; state->buf = buf; for (size_t i = 0; i < kv_size(state->active); i++) { - HlRange item = kv_A(state->active, i); + DecorRange item = kv_A(state->active, i); if (item.virt_text_owned) { - clear_virttext(&item.virt_text); + clear_virttext(&item.decor.virt_text); } } kv_size(state->active) = 0; @@ -190,14 +190,14 @@ bool decor_redraw_start(buf_T *buf, int top_row, DecorState *state) if (mark.id&MARKTREE_END_FLAG) { decor_add(state, altpos.row, altpos.col, mark.row, mark.col, - decor, false, 0); + decor, false); } else { if (altpos.row == -1) { altpos.row = mark.row; altpos.col = mark.col; } decor_add(state, mark.row, mark.col, altpos.row, altpos.col, - decor, false, 0); + decor, false); } next_mark: @@ -222,22 +222,23 @@ bool decor_redraw_line(buf_T *buf, int row, DecorState *state) } static void decor_add(DecorState *state, int start_row, int start_col, - int end_row, int end_col, Decoration *decor, bool owned, - DecorPriority priority) + int end_row, int end_col, Decoration *decor, bool owned) { int attr_id = decor->hl_id > 0 ? syn_id2attr(decor->hl_id) : 0; - HlRange range = { start_row, start_col, end_row, end_col, - attr_id, MAX(priority, decor->priority), - decor->virt_text, - decor->virt_text_pos, decor->virt_text_hide, decor->hl_mode, + DecorRange range = { start_row, start_col, end_row, end_col, + *decor, attr_id, kv_size(decor->virt_text) && owned, -1 }; + if (decor->virt_text_pos == kVTEndOfLine) { + range.win_col = -2; // handled separately + } + kv_pushp(state->active); size_t index; for (index = kv_size(state->active)-1; index > 0; index--) { - HlRange item = kv_A(state->active, index-1); - if (item.priority <= range.priority) { + DecorRange item = kv_A(state->active, index-1); + if (item.decor.priority <= range.decor.priority) { break; } kv_A(state->active, index) = kv_A(state->active, index-1); @@ -245,7 +246,7 @@ static void decor_add(DecorState *state, int start_row, int start_col, kv_A(state->active, index) = range; } -int decor_redraw_col(buf_T *buf, int col, int virt_col, bool hidden, +int decor_redraw_col(buf_T *buf, int col, int win_col, bool hidden, DecorState *state) { if (col <= state->col_until) { @@ -291,7 +292,7 @@ int decor_redraw_col(buf_T *buf, int col, int virt_col, bool hidden, } decor_add(state, mark.row, mark.col, endpos.row, endpos.col, - decor, false, 0); + decor, false); next_mark: marktree_itr_next(buf->b_marktree, state->itr); @@ -300,11 +301,11 @@ next_mark: int attr = 0; size_t j = 0; for (size_t i = 0; i < kv_size(state->active); i++) { - HlRange item = kv_A(state->active, i); + DecorRange item = kv_A(state->active, i); bool active = false, keep = true; if (item.end_row < state->row || (item.end_row == state->row && item.end_col <= col)) { - if (!(item.start_row >= state->row && kv_size(item.virt_text))) { + if (!(item.start_row >= state->row && kv_size(item.decor.virt_text))) { keep = false; } } else { @@ -324,13 +325,14 @@ next_mark: attr = hl_combine_attr(attr, item.attr_id); } if ((item.start_row == state->row && item.start_col <= col) - && kv_size(item.virt_text) && item.virt_col == -1) { - item.virt_col = (item.virt_text_hide && hidden) ? -2 : virt_col; + && kv_size(item.decor.virt_text) + && item.decor.virt_text_pos == kVTOverlay && item.win_col == -1) { + item.win_col = (item.decor.virt_text_hide && hidden) ? -2 : win_col; } if (keep) { kv_A(state->active, j++) = item; } else if (item.virt_text_owned) { - clear_virttext(&item.virt_text); + clear_virttext(&item.decor.virt_text); } } kv_size(state->active) = j; @@ -343,28 +345,39 @@ void decor_redraw_end(DecorState *state) state->buf = NULL; } -VirtText decor_redraw_virt_text(buf_T *buf, DecorState *state) +VirtText decor_redraw_eol(buf_T *buf, DecorState *state, int *eol_attr, + bool *aligned) { decor_redraw_col(buf, MAXCOL, MAXCOL, false, state); + VirtText text = VIRTTEXT_EMPTY; for (size_t i = 0; i < kv_size(state->active); i++) { - HlRange item = kv_A(state->active, i); - if (item.start_row == state->row && kv_size(item.virt_text) - && item.virt_text_pos == kVTEndOfLine) { - return item.virt_text; + DecorRange item = kv_A(state->active, i); + if (item.start_row == state->row && kv_size(item.decor.virt_text)) { + if (!kv_size(text) && item.decor.virt_text_pos == kVTEndOfLine) { + text = item.decor.virt_text; + } else if (item.decor.virt_text_pos == kVTRightAlign + || item.decor.virt_text_pos == kVTWinCol) { + *aligned = true; + } + } + + + if (item.decor.hl_eol && item.start_row <= state->row) { + *eol_attr = hl_combine_attr(*eol_attr, item.attr_id); } } - return VIRTTEXT_EMPTY; + + return text; } void decor_add_ephemeral(int start_row, int start_col, int end_row, int end_col, - Decoration *decor, DecorPriority priority) + Decoration *decor) { if (end_row == -1) { end_row = start_row; end_col = start_col; } - decor_add(&decor_state, start_row, start_col, end_row, end_col, decor, true, - priority); + decor_add(&decor_state, start_row, start_col, end_row, end_col, decor, true); } diff --git a/src/nvim/decoration.h b/src/nvim/decoration.h index c5424a1642..4cebc0b731 100644 --- a/src/nvim/decoration.h +++ b/src/nvim/decoration.h @@ -21,6 +21,8 @@ typedef uint16_t DecorPriority; typedef enum { kVTEndOfLine, kVTOverlay, + kVTWinCol, + kVTRightAlign, } VirtTextPos; typedef enum { @@ -37,33 +39,29 @@ struct Decoration VirtTextPos virt_text_pos; bool virt_text_hide; HlMode hl_mode; + bool hl_eol; // TODO(bfredl): style, signs, etc DecorPriority priority; bool shared; // shared decoration, don't free + int col; // fixed col value, like win_col }; #define DECORATION_INIT { 0, KV_INITIAL_VALUE, kVTEndOfLine, false, \ - kHlModeUnknown, DECOR_PRIORITY_BASE, false } + kHlModeUnknown, false, DECOR_PRIORITY_BASE, false, 0 } typedef struct { int start_row; int start_col; int end_row; int end_col; - int attr_id; - // TODO(bfredl): embed decoration instead, perhaps using an arena - // for ephemerals? - DecorPriority priority; - VirtText virt_text; - VirtTextPos virt_text_pos; - bool virt_text_hide; - HlMode hl_mode; + Decoration decor; + int attr_id; // cached lookup of decor.hl_id bool virt_text_owned; - int virt_col; -} HlRange; + int win_col; +} DecorRange; typedef struct { MarkTreeIter itr[1]; - kvec_t(HlRange) active; + kvec_t(DecorRange) active; buf_T *buf; int top_row; int row; diff --git a/src/nvim/edit.c b/src/nvim/edit.c index ea13052f25..999cc74185 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -1604,13 +1604,20 @@ void edit_putchar(int c, bool highlight) } } +/// Return the effective prompt for the specified buffer. +char_u *buf_prompt_text(const buf_T *const buf) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (buf->b_prompt_text == NULL) { + return (char_u *)"% "; + } + return buf->b_prompt_text; +} + // Return the effective prompt for the current buffer. -char_u *prompt_text(void) +char_u *prompt_text(void) FUNC_ATTR_WARN_UNUSED_RESULT { - if (curbuf->b_prompt_text == NULL) { - return (char_u *)"% "; - } - return curbuf->b_prompt_text; + return buf_prompt_text(curbuf); } // Prepare for prompt mode: Make sure the last line has the prompt text. @@ -2058,7 +2065,7 @@ static bool check_compl_option(bool dict_opt) vim_beep(BO_COMPL); setcursor(); ui_flush(); - os_delay(2000L, false); + os_delay(2004L, false); } return false; } diff --git a/src/nvim/eval.c b/src/nvim/eval.c index b310fd49b0..05d429c7d5 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -3417,8 +3417,7 @@ static int eval4(char_u **arg, typval_T *rettv, int evaluate) { typval_T var2; char_u *p; - exptype_T type = TYPE_UNKNOWN; - bool type_is = false; // true for "is" and "isnot" + exprtype_T type = EXPR_UNKNOWN; int len = 2; bool ic; @@ -3430,35 +3429,42 @@ static int eval4(char_u **arg, typval_T *rettv, int evaluate) p = *arg; switch (p[0]) { - case '=': if (p[1] == '=') - type = TYPE_EQUAL; - else if (p[1] == '~') - type = TYPE_MATCH; + case '=': + if (p[1] == '=') { + type = EXPR_EQUAL; + } else if (p[1] == '~') { + type = EXPR_MATCH; + } break; - case '!': if (p[1] == '=') - type = TYPE_NEQUAL; - else if (p[1] == '~') - type = TYPE_NOMATCH; + case '!': + if (p[1] == '=') { + type = EXPR_NEQUAL; + } else if (p[1] == '~') { + type = EXPR_NOMATCH; + } break; - case '>': if (p[1] != '=') { - type = TYPE_GREATER; + case '>': + if (p[1] != '=') { + type = EXPR_GREATER; len = 1; - } else - type = TYPE_GEQUAL; + } else { + type = EXPR_GEQUAL; + } break; - case '<': if (p[1] != '=') { - type = TYPE_SMALLER; + case '<': + if (p[1] != '=') { + type = EXPR_SMALLER; len = 1; - } else - type = TYPE_SEQUAL; + } else { + type = EXPR_SEQUAL; + } break; case 'i': if (p[1] == 's') { if (p[2] == 'n' && p[3] == 'o' && p[4] == 't') { len = 5; } if (!isalnum(p[len]) && p[len] != '_') { - type = len == 2 ? TYPE_EQUAL : TYPE_NEQUAL; - type_is = true; + type = len == 2 ? EXPR_IS : EXPR_ISNOT; } } break; @@ -3467,7 +3473,7 @@ static int eval4(char_u **arg, typval_T *rettv, int evaluate) /* * If there is a comparative operator, use it. */ - if (type != TYPE_UNKNOWN) { + if (type != EXPR_UNKNOWN) { // extra question mark appended: ignore case if (p[len] == '?') { ic = true; @@ -3486,7 +3492,7 @@ static int eval4(char_u **arg, typval_T *rettv, int evaluate) return FAIL; } if (evaluate) { - const int ret = typval_compare(rettv, &var2, type, type_is, ic); + const int ret = typval_compare(rettv, &var2, type, ic); tv_clear(&var2); return ret; @@ -10582,27 +10588,27 @@ bool invoke_prompt_interrupt(void) int typval_compare( typval_T *typ1, // first operand typval_T *typ2, // second operand - exptype_T type, // operator - bool type_is, // true for "is" and "isnot" + exprtype_T type, // operator bool ic // ignore case ) FUNC_ATTR_NONNULL_ALL { varnumber_T n1, n2; + const bool type_is = type == EXPR_IS || type == EXPR_ISNOT; if (type_is && typ1->v_type != typ2->v_type) { // For "is" a different type always means false, for "notis" // it means true. - n1 = type == TYPE_NEQUAL; + n1 = type == EXPR_ISNOT; } else if (typ1->v_type == VAR_LIST || typ2->v_type == VAR_LIST) { if (type_is) { n1 = typ1->v_type == typ2->v_type && typ1->vval.v_list == typ2->vval.v_list; - if (type == TYPE_NEQUAL) { + if (type == EXPR_ISNOT) { n1 = !n1; } } else if (typ1->v_type != typ2->v_type - || (type != TYPE_EQUAL && type != TYPE_NEQUAL)) { + || (type != EXPR_EQUAL && type != EXPR_NEQUAL)) { if (typ1->v_type != typ2->v_type) { EMSG(_("E691: Can only compare List with List")); } else { @@ -10613,7 +10619,7 @@ int typval_compare( } else { // Compare two Lists for being equal or unequal. n1 = tv_list_equal(typ1->vval.v_list, typ2->vval.v_list, ic, false); - if (type == TYPE_NEQUAL) { + if (type == EXPR_NEQUAL) { n1 = !n1; } } @@ -10621,11 +10627,11 @@ int typval_compare( if (type_is) { n1 = typ1->v_type == typ2->v_type && typ1->vval.v_dict == typ2->vval.v_dict; - if (type == TYPE_NEQUAL) { + if (type == EXPR_ISNOT) { n1 = !n1; } } else if (typ1->v_type != typ2->v_type - || (type != TYPE_EQUAL && type != TYPE_NEQUAL)) { + || (type != EXPR_EQUAL && type != EXPR_NEQUAL)) { if (typ1->v_type != typ2->v_type) { EMSG(_("E735: Can only compare Dictionary with Dictionary")); } else { @@ -10636,12 +10642,13 @@ int typval_compare( } else { // Compare two Dictionaries for being equal or unequal. n1 = tv_dict_equal(typ1->vval.v_dict, typ2->vval.v_dict, ic, false); - if (type == TYPE_NEQUAL) { + if (type == EXPR_NEQUAL) { n1 = !n1; } } } else if (tv_is_func(*typ1) || tv_is_func(*typ2)) { - if (type != TYPE_EQUAL && type != TYPE_NEQUAL) { + if (type != EXPR_EQUAL && type != EXPR_NEQUAL + && type != EXPR_IS && type != EXPR_ISNOT) { EMSG(_("E694: Invalid operation for Funcrefs")); tv_clear(typ1); return FAIL; @@ -10663,43 +10670,47 @@ int typval_compare( } else { n1 = tv_equal(typ1, typ2, ic, false); } - if (type == TYPE_NEQUAL) { + if (type == EXPR_NEQUAL || type == EXPR_ISNOT) { n1 = !n1; } } else if ((typ1->v_type == VAR_FLOAT || typ2->v_type == VAR_FLOAT) - && type != TYPE_MATCH && type != TYPE_NOMATCH) { + && type != EXPR_MATCH && type != EXPR_NOMATCH) { // If one of the two variables is a float, compare as a float. // When using "=~" or "!~", always compare as string. const float_T f1 = tv_get_float(typ1); const float_T f2 = tv_get_float(typ2); n1 = false; switch (type) { - case TYPE_EQUAL: n1 = f1 == f2; break; - case TYPE_NEQUAL: n1 = f1 != f2; break; - case TYPE_GREATER: n1 = f1 > f2; break; - case TYPE_GEQUAL: n1 = f1 >= f2; break; - case TYPE_SMALLER: n1 = f1 < f2; break; - case TYPE_SEQUAL: n1 = f1 <= f2; break; - case TYPE_UNKNOWN: - case TYPE_MATCH: - case TYPE_NOMATCH: break; + case EXPR_IS: + case EXPR_EQUAL: n1 = f1 == f2; break; + case EXPR_ISNOT: + case EXPR_NEQUAL: n1 = f1 != f2; break; + case EXPR_GREATER: n1 = f1 > f2; break; + case EXPR_GEQUAL: n1 = f1 >= f2; break; + case EXPR_SMALLER: n1 = f1 < f2; break; + case EXPR_SEQUAL: n1 = f1 <= f2; break; + case EXPR_UNKNOWN: + case EXPR_MATCH: + case EXPR_NOMATCH: break; // avoid gcc warning } } else if ((typ1->v_type == VAR_NUMBER || typ2->v_type == VAR_NUMBER) - && type != TYPE_MATCH && type != TYPE_NOMATCH) { + && type != EXPR_MATCH && type != EXPR_NOMATCH) { // If one of the two variables is a number, compare as a number. // When using "=~" or "!~", always compare as string. n1 = tv_get_number(typ1); n2 = tv_get_number(typ2); switch (type) { - case TYPE_EQUAL: n1 = n1 == n2; break; - case TYPE_NEQUAL: n1 = n1 != n2; break; - case TYPE_GREATER: n1 = n1 > n2; break; - case TYPE_GEQUAL: n1 = n1 >= n2; break; - case TYPE_SMALLER: n1 = n1 < n2; break; - case TYPE_SEQUAL: n1 = n1 <= n2; break; - case TYPE_UNKNOWN: - case TYPE_MATCH: - case TYPE_NOMATCH: break; + case EXPR_IS: + case EXPR_EQUAL: n1 = n1 == n2; break; + case EXPR_ISNOT: + case EXPR_NEQUAL: n1 = n1 != n2; break; + case EXPR_GREATER: n1 = n1 > n2; break; + case EXPR_GEQUAL: n1 = n1 >= n2; break; + case EXPR_SMALLER: n1 = n1 < n2; break; + case EXPR_SEQUAL: n1 = n1 <= n2; break; + case EXPR_UNKNOWN: + case EXPR_MATCH: + case EXPR_NOMATCH: break; // avoid gcc warning } } else { char buf1[NUMBUFLEN]; @@ -10707,28 +10718,30 @@ int typval_compare( const char *const s1 = tv_get_string_buf(typ1, buf1); const char *const s2 = tv_get_string_buf(typ2, buf2); int i; - if (type != TYPE_MATCH && type != TYPE_NOMATCH) { + if (type != EXPR_MATCH && type != EXPR_NOMATCH) { i = mb_strcmp_ic(ic, s1, s2); } else { i = 0; } n1 = false; switch (type) { - case TYPE_EQUAL: n1 = i == 0; break; - case TYPE_NEQUAL: n1 = i != 0; break; - case TYPE_GREATER: n1 = i > 0; break; - case TYPE_GEQUAL: n1 = i >= 0; break; - case TYPE_SMALLER: n1 = i < 0; break; - case TYPE_SEQUAL: n1 = i <= 0; break; - - case TYPE_MATCH: - case TYPE_NOMATCH: + case EXPR_IS: + case EXPR_EQUAL: n1 = i == 0; break; + case EXPR_ISNOT: + case EXPR_NEQUAL: n1 = i != 0; break; + case EXPR_GREATER: n1 = i > 0; break; + case EXPR_GEQUAL: n1 = i >= 0; break; + case EXPR_SMALLER: n1 = i < 0; break; + case EXPR_SEQUAL: n1 = i <= 0; break; + + case EXPR_MATCH: + case EXPR_NOMATCH: n1 = pattern_match((char_u *)s2, (char_u *)s1, ic); - if (type == TYPE_NOMATCH) { + if (type == EXPR_NOMATCH) { n1 = !n1; } break; - case TYPE_UNKNOWN: break; // Avoid gcc warning. + case EXPR_UNKNOWN: break; // avoid gcc warning } } tv_clear(typ1); diff --git a/src/nvim/eval.h b/src/nvim/eval.h index a62d87fcc4..3da4bb8655 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -230,16 +230,18 @@ typedef enum /// types for expressions. typedef enum { - TYPE_UNKNOWN = 0, - TYPE_EQUAL, ///< == - TYPE_NEQUAL, ///< != - TYPE_GREATER, ///< > - TYPE_GEQUAL, ///< >= - TYPE_SMALLER, ///< < - TYPE_SEQUAL, ///< <= - TYPE_MATCH, ///< =~ - TYPE_NOMATCH, ///< !~ -} exptype_T; + EXPR_UNKNOWN = 0, + EXPR_EQUAL, ///< == + EXPR_NEQUAL, ///< != + EXPR_GREATER, ///< > + EXPR_GEQUAL, ///< >= + EXPR_SMALLER, ///< < + EXPR_SEQUAL, ///< <= + EXPR_MATCH, ///< =~ + EXPR_NOMATCH, ///< !~ + EXPR_IS, ///< is + EXPR_ISNOT, ///< isnot +} exprtype_T; /// Type for dict_list function typedef enum { diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index b10e99fc08..77e7c7b3a9 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -251,6 +251,7 @@ return { pow={args=2}, prevnonblank={args=1}, printf={args=varargs(1)}, + prompt_getprompt={args=1}, prompt_setcallback={args={2, 2}}, prompt_setinterrupt={args={2, 2}}, prompt_setprompt={args={2, 2}}, diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 0d288e2cc2..6d328953f6 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -602,12 +602,7 @@ static void f_bufname(typval_T *argvars, typval_T *rettv, FunPtr fptr) if (argvars[0].v_type == VAR_UNKNOWN) { buf = curbuf; } else { - if (!tv_check_str_or_nr(&argvars[0])) { - return; - } - emsg_off++; - buf = tv_get_buf(&argvars[0], false); - emsg_off--; + buf = tv_get_buf_from_arg(&argvars[0]); } if (buf != NULL && buf->b_fname != NULL) { rettv->vval.v_string = (char_u *)xstrdup((char *)buf->b_fname); @@ -627,6 +622,9 @@ static void f_bufnr(typval_T *argvars, typval_T *rettv, FunPtr fptr) if (argvars[0].v_type == VAR_UNKNOWN) { buf = curbuf; } else { + // Don't use tv_get_buf_from_arg(); we continue if the buffer wasn't found + // and the second argument isn't zero, but we want to return early if the + // first argument isn't a string or number so only one error is shown. if (!tv_check_str_or_nr(&argvars[0])) { return; } @@ -653,18 +651,12 @@ static void f_bufnr(typval_T *argvars, typval_T *rettv, FunPtr fptr) static void buf_win_common(typval_T *argvars, typval_T *rettv, bool get_nr) { - if (!tv_check_str_or_nr(&argvars[0])) { + const buf_T *const buf = tv_get_buf_from_arg(&argvars[0]); + if (buf == NULL) { // no need to search if invalid arg or buffer not found rettv->vval.v_number = -1; return; } - emsg_off++; - buf_T *buf = tv_get_buf(&argvars[0], true); - if (buf == NULL) { // no need to search if buffer was not found - rettv->vval.v_number = -1; - goto end; - } - int winnr = 0; int winid; bool found_buf = false; @@ -677,8 +669,6 @@ static void buf_win_common(typval_T *argvars, typval_T *rettv, bool get_nr) } } rettv->vval.v_number = (found_buf ? (get_nr ? winnr : winid) : -1); -end: - emsg_off--; } /// "bufwinid(nr)" function @@ -731,6 +721,18 @@ buf_T *tv_get_buf(typval_T *tv, int curtab_only) return buf; } +/// Like tv_get_buf() but give an error message if the type is wrong. +buf_T *tv_get_buf_from_arg(typval_T *const tv) FUNC_ATTR_NONNULL_ALL +{ + if (!tv_check_str_or_nr(tv)) { + return NULL; + } + emsg_off++; + buf_T *const buf = tv_get_buf(tv, false); + emsg_off--; + return buf; +} + /// Get the buffer from "arg" and give an error and return NULL if it is not /// valid. buf_T * get_buf_arg(typval_T *arg) @@ -2799,13 +2801,9 @@ static void f_getbufinfo(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } else if (argvars[0].v_type != VAR_UNKNOWN) { // Information about one buffer. Argument specifies the buffer - if (tv_check_num(&argvars[0])) { // issue errmsg if type error - emsg_off++; - argbuf = tv_get_buf(&argvars[0], false); - emsg_off--; - if (argbuf == NULL) { - return; - } + argbuf = tv_get_buf_from_arg(&argvars[0]); + if (argbuf == NULL) { + return; } } @@ -2875,13 +2873,7 @@ static void get_buffer_lines(buf_T *buf, */ static void f_getbufline(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - buf_T *buf = NULL; - - if (tv_check_str_or_nr(&argvars[0])) { - emsg_off++; - buf = tv_get_buf(&argvars[0], false); - emsg_off--; - } + buf_T *const buf = tv_get_buf_from_arg(&argvars[0]); const linenr_T lnum = tv_get_lnum_buf(&argvars[1], buf); const linenr_T end = (argvars[2].v_type == VAR_UNKNOWN @@ -6499,6 +6491,26 @@ static void f_prompt_setinterrupt(typval_T *argvars, buf->b_prompt_interrupt= interrupt_callback; } +/// "prompt_getprompt({buffer})" function +void f_prompt_getprompt(typval_T *argvars, typval_T *rettv, FunPtr fptr) + FUNC_ATTR_NONNULL_ALL +{ + // return an empty string by default, e.g. it's not a prompt buffer + rettv->v_type = VAR_STRING; + rettv->vval.v_string = NULL; + + buf_T *const buf = tv_get_buf_from_arg(&argvars[0]); + if (buf == NULL) { + return; + } + + if (!bt_prompt(buf)) { + return; + } + + rettv->vval.v_string = vim_strsave(buf_prompt_text(buf)); +} + // "prompt_setprompt({buffer}, {text})" function static void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, FunPtr fptr) diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index 689d05e079..00260bc3f7 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -833,6 +833,8 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, bool islambda = false; char_u numbuf[NUMBUFLEN]; char_u *name; + typval_T *tv_to_free[MAX_FUNC_ARGS]; + int tv_to_free_len = 0; proftime_T wait_start; proftime_T call_start; int started_profiling = false; @@ -985,6 +987,11 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, v->di_tv = isdefault ? def_rettv : argvars[i]; v->di_tv.v_lock = VAR_FIXED; + if (isdefault) { + // Need to free this later, no matter where it's stored. + tv_to_free[tv_to_free_len++] = &v->di_tv; + } + if (addlocal) { // Named arguments can be accessed without the "a:" prefix in lambda // expressions. Add to the l: dict. @@ -1209,7 +1216,9 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, did_emsg |= save_did_emsg; depth--; - + for (int i = 0; i < tv_to_free_len; i++) { + tv_clear(tv_to_free[i]); + } cleanup_function_call(fc); if (--fp->uf_calls <= 0 && fp->uf_refcount <= 0) { diff --git a/src/nvim/event/libuv_process.c b/src/nvim/event/libuv_process.c index 0b1ecb12e2..c02f730431 100644 --- a/src/nvim/event/libuv_process.c +++ b/src/nvim/event/libuv_process.c @@ -82,7 +82,7 @@ int libuv_process_spawn(LibuvProcess *uvproc) int status; if ((status = uv_spawn(&proc->loop->uv, &uvproc->uv, &uvproc->uvopts))) { - ELOG("uv_spawn failed: %s", uv_strerror(status)); + ELOG("uv_spawn(%s) failed: %s", uvproc->uvopts.file, uv_strerror(status)); if (uvproc->uvopts.env) { os_free_fullenv(uvproc->uvopts.env); } diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index b191e8cf67..3e330b88a2 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -968,12 +968,6 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest) mark_adjust_nofold(last_line - num_lines + 1, last_line, -(last_line - dest - extra), 0L, kExtmarkNOOP); - // extmarks are handled separately - extmark_move_region(curbuf, line1-1, 0, start_byte, - line2-line1+1, 0, extent_byte, - dest+line_off, 0, dest_byte+byte_off, - kExtmarkUndo); - changed_lines(last_line - num_lines + 1, 0, last_line + 1, -extra, false); // send update regarding the new lines that were added @@ -995,6 +989,11 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest) smsg(_("%" PRId64 " lines moved"), (int64_t)num_lines); } + extmark_move_region(curbuf, line1-1, 0, start_byte, + line2-line1+1, 0, extent_byte, + dest+line_off, 0, dest_byte+byte_off, + kExtmarkUndo); + /* * Leave the cursor on the last of the moved lines. */ diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index 2965ea7496..d99383303b 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -928,6 +928,12 @@ module.cmds = { func='ex_edit', }, { + command='eval', + flags=bit.bor(EXTRA, NOTRLCOM, SBOXOK, CMDWIN), + addr_type='ADDR_NONE', + func='ex_eval', + }, + { command='ex', flags=bit.bor(BANG, FILE1, CMDARG, ARGOPT, TRLBAR), addr_type='ADDR_NONE', diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index e394edb032..317ca465e1 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -876,7 +876,7 @@ debuggy_find( debug_newval = typval_tostring(bp->dbg_val); line = true; } else { - if (typval_compare(tv, bp->dbg_val, TYPE_EQUAL, true, false) == OK + if (typval_compare(tv, bp->dbg_val, EXPR_IS, false) == OK && tv->vval.v_number == false) { line = true; debug_oldval = typval_tostring(bp->dbg_val); @@ -2719,16 +2719,13 @@ static char_u *get_str_line(int c, void *cookie, int indent, bool do_concat) while (!(p->buf[i] == '\n' || p->buf[i] == '\0')) { i++; } - char buf[2046]; - char *dst; - dst = xstpncpy(buf, (char *)p->buf + p->offset, i - p->offset); - if ((uint32_t)(dst - buf) != i - p->offset) { - smsg(_(":source error parsing command %s"), p->buf); - return NULL; - } - buf[i - p->offset] = '\0'; + size_t line_length = i - p->offset; + garray_T ga; + ga_init(&ga, (int)sizeof(char_u), (int)line_length); + ga_concat_len(&ga, (char *)p->buf + p->offset, line_length); + ga_append(&ga, '\0'); p->offset = i + 1; - return (char_u *)xstrdup(buf); + return ga.ga_data; } static int source_using_linegetter(void *cookie, diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index d1eddfc74f..ae5c334592 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -1857,6 +1857,7 @@ static char_u * do_one_cmd(char_u **cmdlinep, case CMD_echoerr: case CMD_echomsg: case CMD_echon: + case CMD_eval: case CMD_execute: case CMD_filter: case CMD_help: diff --git a/src/nvim/ex_eval.c b/src/nvim/ex_eval.c index 0917c6dd02..5ca88002f1 100644 --- a/src/nvim/ex_eval.c +++ b/src/nvim/ex_eval.c @@ -788,6 +788,15 @@ void report_discard_pending(int pending, void *value) } } +// ":eval". +void ex_eval(exarg_T *eap) +{ + typval_T tv; + + if (eval0(eap->arg, &tv, &eap->nextcmd, !eap->skip) == OK) { + tv_clear(&tv); + } +} /* * ":if". diff --git a/src/nvim/ex_session.c b/src/nvim/ex_session.c index 09453e100d..b11ec4ad05 100644 --- a/src/nvim/ex_session.c +++ b/src/nvim/ex_session.c @@ -690,18 +690,16 @@ static int makeopens(FILE *fd, char_u *dirnow) return FAIL; } - // - // Save current window layout. - // - PUTLINE_FAIL("set splitbelow splitright"); - if (ses_win_rec(fd, tab_topframe) == FAIL) { - return FAIL; - } - if (!p_sb && put_line(fd, "set nosplitbelow") == FAIL) { - return FAIL; - } - if (!p_spr && put_line(fd, "set nosplitright") == FAIL) { - return FAIL; + if (tab_topframe->fr_layout != FR_LEAF) { + // Save current window layout. + PUTLINE_FAIL("let s:save_splitbelow = &splitbelow"); + PUTLINE_FAIL("let s:save_splitright = &splitright"); + PUTLINE_FAIL("set splitbelow splitright"); + if (ses_win_rec(fd, tab_topframe) == FAIL) { + return FAIL; + } + PUTLINE_FAIL("let &splitbelow = s:save_splitbelow"); + PUTLINE_FAIL("let &splitright = s:save_splitright"); } // @@ -720,22 +718,26 @@ static int makeopens(FILE *fd, char_u *dirnow) } } - // Go to the first window. - PUTLINE_FAIL("wincmd t"); - - // If more than one window, see if sizes can be restored. - // First set 'winheight' and 'winwidth' to 1 to avoid the windows being - // resized when moving between windows. - // Do this before restoring the view, so that the topline and the - // cursor can be set. This is done again below. - // winminheight and winminwidth need to be set to avoid an error if the - // user has set winheight or winwidth. - if (fprintf(fd, - "set winminheight=0\n" - "set winheight=1\n" - "set winminwidth=0\n" - "set winwidth=1\n") < 0) { - return FAIL; + if (tab_firstwin->w_next != NULL) { + // Go to the first window. + PUTLINE_FAIL("wincmd t"); + + // If more than one window, see if sizes can be restored. + // First set 'winheight' and 'winwidth' to 1 to avoid the windows + // being resized when moving between windows. + // Do this before restoring the view, so that the topline and the + // cursor can be set. This is done again below. + // winminheight and winminwidth need to be set to avoid an error if + // the user has set winheight or winwidth. + PUTLINE_FAIL("let s:save_winminheight = &winminheight"); + PUTLINE_FAIL("let s:save_winminwidth = &winminwidth"); + if (fprintf(fd, + "set winminheight=0\n" + "set winheight=1\n" + "set winminwidth=0\n" + "set winwidth=1\n") < 0) { + return FAIL; + } } if (nr > 1 && ses_winsizes(fd, restore_size, tab_firstwin) == FAIL) { return FAIL; @@ -817,18 +819,20 @@ static int makeopens(FILE *fd, char_u *dirnow) return FAIL; } - // Re-apply options. + // Re-apply 'winheight', 'winwidth' and 'shortmess'. if (fprintf(fd, "set winheight=%" PRId64 " winwidth=%" PRId64 - " winminheight=%" PRId64 " winminwidth=%" PRId64 " shortmess=%s\n", (int64_t)p_wh, (int64_t)p_wiw, - (int64_t)p_wmh, - (int64_t)p_wmw, p_shm) < 0) { return FAIL; } + if (tab_firstwin->w_next != NULL) { + // Restore 'winminheight' and 'winminwidth'. + PUTLINE_FAIL("let &winminheight = s:save_winminheight"); + PUTLINE_FAIL("let &winminwidth = s:save_winminwidth"); + } // // Lastly, execute the x.vim file if it exists. diff --git a/src/nvim/extmark.c b/src/nvim/extmark.c index cacbeddb32..2906a2196b 100644 --- a/src/nvim/extmark.c +++ b/src/nvim/extmark.c @@ -702,6 +702,7 @@ void extmark_move_region( int new_row, colnr_T new_col, bcount_t new_byte, ExtmarkOp undo) { + curbuf->deleted_bytes2 = 0; // TODO(bfredl): this is not synced to the buffer state inside the callback. // But unless we make the undo implementation smarter, this is not ensured // anyway. diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 65bd809436..792ef81665 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -4947,11 +4947,11 @@ int buf_check_timestamp(buf_T *buf) (void)msg_end(); if (emsg_silent == 0) { ui_flush(); - /* give the user some time to think about it */ - os_delay(1000L, true); + // give the user some time to think about it + os_delay(1004L, true); - /* don't redraw and erase the message */ - redraw_cmdline = FALSE; + // don't redraw and erase the message + redraw_cmdline = false; } } already_warned = TRUE; diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 9b8e9ff8cc..f99a2dd0fe 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -17,6 +17,7 @@ #include "nvim/api/vim.h" #include "nvim/msgpack_rpc/channel.h" #include "nvim/vim.h" +#include "nvim/extmark.h" #include "nvim/ex_getln.h" #include "nvim/ex_cmds2.h" #include "nvim/map.h" @@ -1243,13 +1244,16 @@ void ex_luado(exarg_T *const eap) break; } lua_pushvalue(lstate, -1); - lua_pushstring(lstate, (const char *)ml_get_buf(curbuf, l, false)); + const char *old_line = (const char *)ml_get_buf(curbuf, l, false); + lua_pushstring(lstate, old_line); lua_pushnumber(lstate, (lua_Number)l); if (lua_pcall(lstate, 2, 1, 0)) { nlua_error(lstate, _("E5111: Error calling lua: %.*s")); break; } if (lua_isstring(lstate, -1)) { + size_t old_line_len = STRLEN(old_line); + size_t new_line_len; const char *const new_line = lua_tolstring(lstate, -1, &new_line_len); char *const new_line_transformed = xmemdupz(new_line, new_line_len); @@ -1259,7 +1263,7 @@ void ex_luado(exarg_T *const eap) } } ml_replace(l, (char_u *)new_line_transformed, false); - changed_bytes(l, 0); + inserted_bytes(l, 0, (int)old_line_len, (int)new_line_len); } lua_pop(lstate, 1); } diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index eb54ff28ee..3994c5bc5b 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -309,7 +309,9 @@ setmetatable(vim, { }) -- An easier alias for commands. -vim.cmd = vim.api.nvim_command +vim.cmd = function(command) + return vim.api.nvim_exec(command, false) +end -- These are the vim.env/v/g/o/bo/wo variable magic accessors. do @@ -398,7 +400,10 @@ do wfw = true; winbl = true; winblend = true; winfixheight = true; winfixwidth = true; winhighlight = true; winhl = true; wrap = true; } + + --@private local function new_buf_opt_accessor(bufnr) + --@private local function get(k) if window_options[k] then return a.nvim_err_writeln(k.." is a window option, not a buffer option") @@ -408,23 +413,34 @@ do end return a.nvim_buf_get_option(bufnr or 0, k) end + + --@private local function set(k, v) if window_options[k] then return a.nvim_err_writeln(k.." is a window option, not a buffer option") end return a.nvim_buf_set_option(bufnr or 0, k, v) end + return make_meta_accessor(get, set) end vim.bo = new_buf_opt_accessor(nil) + + --@private local function new_win_opt_accessor(winnr) + + --@private local function get(k) if winnr == nil and type(k) == "number" then return new_win_opt_accessor(k) end return a.nvim_win_get_option(winnr or 0, k) end - local function set(k, v) return a.nvim_win_set_option(winnr or 0, k, v) end + + --@private + local function set(k, v) + return a.nvim_win_set_option(winnr or 0, k, v) + end return make_meta_accessor(get, set) end vim.wo = new_win_opt_accessor(nil) diff --git a/src/nvim/main.c b/src/nvim/main.c index 7064f2a068..56cd97f133 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -375,7 +375,7 @@ int main(int argc, char **argv) // Does ":filetype plugin indent on". filetype_maybe_enable(); // Sources syntax/syntax.vim, which calls `:filetype on`. - syn_maybe_on(); + syn_maybe_enable(); } // Read all the plugin files. diff --git a/src/nvim/message.c b/src/nvim/message.c index 7c98d3c6b5..1783f62247 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -2265,12 +2265,14 @@ void msg_scroll_up(bool may_throttle) /// per screen update. /// /// NB: The bookkeeping is quite messy, and rests on a bunch of poorly -/// documented assumtions. For instance that the message area always grows while -/// being throttled, messages are only being output on the last line etc. +/// documented assumptions. For instance that the message area always grows +/// while being throttled, messages are only being output on the last line +/// etc. /// -/// Probably message scrollback storage should reimplented as a file_buffer, and -/// message scrolling in TUI be reimplemented as a modal floating window. Then -/// we get throttling "for free" using standard redraw_later code paths. +/// Probably message scrollback storage should be reimplemented as a +/// file_buffer, and message scrolling in TUI be reimplemented as a modal +/// floating window. Then we get throttling "for free" using standard +/// redraw_later code paths. void msg_scroll_flush(void) { if (msg_grid.throttled) { diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index a0b439ac45..a2d8859c68 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -219,7 +219,7 @@ static void receive_msgpack(Stream *stream, RBuffer *rbuf, size_t c, char buf[256]; snprintf(buf, sizeof(buf), "ch %" PRIu64 " was closed by the client", channel->id); - call_set_error(channel, buf, WARN_LOG_LEVEL); + call_set_error(channel, buf, INFO_LOG_LEVEL); goto end; } diff --git a/src/nvim/normal.c b/src/nvim/normal.c index f016ef6813..c948881eca 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -630,9 +630,9 @@ static void normal_redraw_mode_message(NormalState *s) ui_cursor_shape(); // show different cursor shape ui_flush(); if (msg_scroll || emsg_on_display) { - os_delay(1000L, true); // wait at least one second + os_delay(1003L, true); // wait at least one second } - os_delay(3000L, false); // wait up to three seconds + os_delay(3003L, false); // wait up to three seconds State = save_State; msg_scroll = false; diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 2cd71f2360..190ca2e93b 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -1676,12 +1676,18 @@ int op_delete(oparg_T *oap) curbuf_splice_pending++; pos_T startpos = curwin->w_cursor; // start position for delete + bcount_t deleted_bytes = (bcount_t)STRLEN( + ml_get(startpos.lnum)) + 1 - startpos.col; truncate_line(true); // delete from cursor to end of line curpos = curwin->w_cursor; // remember curwin->w_cursor curwin->w_cursor.lnum++; + + for (linenr_T i = 1; i <= oap->line_count - 2; i++) { + deleted_bytes += (bcount_t)STRLEN( + ml_get(startpos.lnum + i)) + 1; + } del_lines(oap->line_count - 2, false); - bcount_t deleted_bytes = (bcount_t)curbuf->deleted_bytes2 - startpos.col; // delete from start of line until op_end n = (oap->end.col + 1 - !oap->inclusive); diff --git a/src/nvim/option.c b/src/nvim/option.c index 914b92618c..666c526a18 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -7583,9 +7583,19 @@ int csh_like_shell(void) /// buffer signs and on user configuration. int win_signcol_count(win_T *wp) { + return win_signcol_configured(wp, NULL); +} + +/// Return the number of requested sign columns, based on user / configuration. +int win_signcol_configured(win_T *wp, int *is_fixed) +{ int minimum = 0, maximum = 1, needed_signcols; const char *scl = (const char *)wp->w_p_scl; + if (is_fixed) { + *is_fixed = 1; + } + // Note: It checks "no" or "number" in 'signcolumn' option if (*scl == 'n' && (*(scl + 1) == 'o' || (*(scl + 1) == 'u' @@ -7603,7 +7613,11 @@ int win_signcol_count(win_T *wp) return 1; } - // auto or auto:<NUM> + if (is_fixed) { + // auto or auto:<NUM> + *is_fixed = 0; + } + if (!strncmp(scl, "auto:", 5)) { // Variable depending on a configuration maximum = scl[5] - '0'; diff --git a/src/nvim/os/pty_process_unix.c b/src/nvim/os/pty_process_unix.c index d794969ab5..36d6dbe2db 100644 --- a/src/nvim/os/pty_process_unix.c +++ b/src/nvim/os/pty_process_unix.c @@ -175,7 +175,7 @@ static void init_child(PtyProcess *ptyproc) Process *proc = (Process *)ptyproc; if (proc->cwd && os_chdir(proc->cwd) != 0) { - ELOG("chdir failed: %s", strerror(errno)); + ELOG("chdir(%s) failed: %s", proc->cwd, strerror(errno)); return; } @@ -184,7 +184,7 @@ static void init_child(PtyProcess *ptyproc) assert(proc->env); environ = tv_dict_to_env(proc->env); execvp(prog, proc->argv); - ELOG("execvp failed: %s: %s", strerror(errno), prog); + ELOG("execvp(%s) failed: %s", prog, strerror(errno)); _exit(122); // 122 is EXEC_FAILED in the Vim source. } diff --git a/src/nvim/os/pty_process_win.c b/src/nvim/os/pty_process_win.c index 94444e4d23..2bf73d08e6 100644 --- a/src/nvim/os/pty_process_win.c +++ b/src/nvim/os/pty_process_win.c @@ -203,11 +203,13 @@ int pty_process_spawn(PtyProcess *ptyproc) cleanup: if (status) { // In the case of an error of MultiByteToWideChar or CreateProcessW. - ELOG("pty_process_spawn: %s: error code: %d", emsg, status); + ELOG("pty_process_spawn(%s): %s: error code: %d", + proc->argv[0], emsg, status); status = os_translate_sys_error(status); } else if (err != NULL) { status = (int)winpty_error_code(err); - ELOG("pty_process_spawn: %s: error code: %d", emsg, status); + ELOG("pty_process_spawn(%s): %s: error code: %d", + proc->argv[0], emsg, status); status = translate_winpty_error(status); } winpty_error_free(err); diff --git a/src/nvim/os/time.c b/src/nvim/os/time.c index e7e0dc4013..9ea74716aa 100644 --- a/src/nvim/os/time.c +++ b/src/nvim/os/time.c @@ -62,6 +62,7 @@ uint64_t os_now(void) /// @param ignoreinput If true, only SIGINT (CTRL-C) can interrupt. void os_delay(uint64_t ms, bool ignoreinput) { + DLOG("%" PRIu64 " ms", ms); if (ignoreinput) { if (ms > INT_MAX) { ms = INT_MAX; diff --git a/src/nvim/regexp.c b/src/nvim/regexp.c index d7693c7a6f..184f5da97d 100644 --- a/src/nvim/regexp.c +++ b/src/nvim/regexp.c @@ -6665,6 +6665,10 @@ static int vim_regsub_both(char_u *source, typval_T *expr, char_u *dest, int len = 0; /* init for GCC */ static char_u *eval_result = NULL; + // We need to keep track of how many backslashes we escape, so that the byte + // counts for `extmark_splice` are correct. + int num_escaped = 0; + // Be paranoid... if ((source == NULL && expr == NULL) || dest == NULL) { EMSG(_(e_null)); @@ -6840,6 +6844,7 @@ static int vim_regsub_both(char_u *source, typval_T *expr, char_u *dest, // later. Used to insert a literal CR. default: if (backslash) { + num_escaped += 1; if (copy) { *dst = '\\'; } @@ -6979,7 +6984,7 @@ static int vim_regsub_both(char_u *source, typval_T *expr, char_u *dest, *dst = NUL; exit: - return (int)((dst - dest) + 1); + return (int)((dst - dest) + 1 - num_escaped); } diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 9fb2eb2772..5151d82c1b 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -2101,6 +2101,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool search_attr_from_match = false; // if search_attr is from :match bool has_decor = false; // this buffer has decoration bool do_virttext = false; // draw virtual text for this line + int win_col_offset; // offsett for window columns char_u buf_fold[FOLD_TEXT_LEN + 1]; // Hold value returned by get_foldtext @@ -2790,6 +2791,10 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, } } + if (draw_state == WL_NR && n_extra == 0) { + win_col_offset = off; + } + if (wp->w_briopt_sbr && draw_state == WL_BRI - 1 && n_extra == 0 && *p_sbr != NUL) { // draw indent after showbreak value @@ -2904,7 +2909,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, && vcol >= (long)wp->w_virtcol) || (number_only && draw_state > WL_NR)) && filler_todo <= 0) { - draw_virt_text(buf, &col, grid->Columns); + draw_virt_text(buf, win_col_offset, &col, grid->Columns); grid_put_linebuf(grid, row, 0, col, -grid->Columns, wp->w_p_rl, wp, wp->w_hl_attr_normal, false); // Pretend we have finished updating the window. Except when @@ -3945,13 +3950,15 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, draw_color_col = advance_color_col(VCOL_HLC, &color_cols); VirtText virt_text = KV_INITIAL_VALUE; + bool has_aligned = false; if (err_text) { int hl_err = syn_check_group((char_u *)S_LEN("ErrorMsg")); kv_push(virt_text, ((VirtTextChunk){ .text = err_text, .hl_id = hl_err })); do_virttext = true; } else if (has_decor) { - virt_text = decor_redraw_virt_text(wp->w_buffer, &decor_state); + virt_text = decor_redraw_eol(wp->w_buffer, &decor_state, &line_attr, + &has_aligned); if (kv_size(virt_text)) { do_virttext = true; } @@ -3963,7 +3970,8 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, grid->Columns * (row - startrow + 1) + v && lnum != wp->w_cursor.lnum) || draw_color_col || line_attr_lowprio || line_attr - || diff_hlf != (hlf_T)0 || do_virttext)) { + || diff_hlf != (hlf_T)0 || do_virttext + || has_aligned)) { int rightmost_vcol = 0; int i; @@ -4001,7 +4009,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, } int base_attr = hl_combine_attr(line_attr_lowprio, diff_attr); - if (base_attr || line_attr) { + if (base_attr || line_attr || has_aligned) { rightmost_vcol = INT_MAX; } @@ -4079,7 +4087,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, } } - draw_virt_text(buf, &col, grid->Columns); + draw_virt_text(buf, win_col_offset, &col, grid->Columns); grid_put_linebuf(grid, row, 0, col, grid->Columns, wp->w_p_rl, wp, wp->w_hl_attr_normal, false); row++; @@ -4300,7 +4308,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, && !wp->w_p_rl; // Not right-to-left. int draw_col = col - boguscols; - draw_virt_text(buf, &draw_col, grid->Columns); + draw_virt_text(buf, win_col_offset, &draw_col, grid->Columns); grid_put_linebuf(grid, row, 0, draw_col, grid->Columns, wp->w_p_rl, wp, wp->w_hl_attr_normal, wrap); if (wrap) { @@ -4377,51 +4385,62 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, return row; } -void draw_virt_text(buf_T *buf, int *end_col, int max_col) +void draw_virt_text(buf_T *buf, int col_off, int *end_col, int max_col) { DecorState *state = &decor_state; + int right_pos = max_col; for (size_t i = 0; i < kv_size(state->active); i++) { - HlRange *item = &kv_A(state->active, i); - if (item->start_row == state->row && kv_size(item->virt_text) - && item->virt_text_pos == kVTOverlay - && item->virt_col >= 0) { - VirtText vt = item->virt_text; - LineState s = LINE_STATE(""); - int virt_attr = 0; - int col = item->virt_col; - size_t virt_pos = 0; - item->virt_col = -2; // deactivate + DecorRange *item = &kv_A(state->active, i); + if (item->start_row == state->row && kv_size(item->decor.virt_text)) { + if (item->win_col == -1) { + if (item->decor.virt_text_pos == kVTRightAlign) { + right_pos -= item->decor.col; + item->win_col = right_pos; + } else if (item->decor.virt_text_pos == kVTWinCol) { + item->win_col = MAX(item->decor.col+col_off, 0); + } + } + if (item->win_col < 0) { + continue; + } + VirtText vt = item->decor.virt_text; + HlMode hl_mode = item->decor.hl_mode; + LineState s = LINE_STATE(""); + int virt_attr = 0; + int col = item->win_col; + size_t virt_pos = 0; + item->win_col = -2; // deactivate - while (col < max_col) { - if (!*s.p) { - if (virt_pos == kv_size(vt)) { - break; - } - s.p = kv_A(vt, virt_pos).text; - int hl_id = kv_A(vt, virt_pos).hl_id; - virt_attr = hl_id > 0 ? syn_id2attr(hl_id) : 0; - virt_pos++; - continue; - } - int attr; - bool through = false; - if (item->hl_mode == kHlModeCombine) { - attr = hl_combine_attr(linebuf_attr[col], virt_attr); - } else if (item->hl_mode == kHlModeBlend) { - through = (*s.p == ' '); - attr = hl_blend_attrs(linebuf_attr[col], virt_attr, &through); - } else { - attr = virt_attr; + while (col < max_col) { + if (!*s.p) { + if (virt_pos == kv_size(vt)) { + break; } - schar_T dummy[2]; - int cells = line_putchar(&s, through ? dummy : &linebuf_char[col], - max_col-col, false); + s.p = kv_A(vt, virt_pos).text; + int hl_id = kv_A(vt, virt_pos).hl_id; + virt_attr = hl_id > 0 ? syn_id2attr(hl_id) : 0; + virt_pos++; + continue; + } + int attr; + bool through = false; + if (hl_mode == kHlModeCombine) { + attr = hl_combine_attr(linebuf_attr[col], virt_attr); + } else if (hl_mode == kHlModeBlend) { + through = (*s.p == ' '); + attr = hl_blend_attrs(linebuf_attr[col], virt_attr, &through); + } else { + attr = virt_attr; + } + schar_T dummy[2]; + int cells = line_putchar(&s, through ? dummy : &linebuf_char[col], + max_col-col, false); + linebuf_attr[col++] = attr; + if (cells > 1) { linebuf_attr[col++] = attr; - if (cells > 1) { - linebuf_attr[col++] = attr; - } } - *end_col = MAX(*end_col, col); + } + *end_col = MAX(*end_col, col); } } } @@ -6226,7 +6245,7 @@ void check_for_delay(int check_msg_scroll) && !did_wait_return && emsg_silent == 0) { ui_flush(); - os_delay(1000L, true); + os_delay(1006L, true); emsg_on_display = false; if (check_msg_scroll) { msg_scroll = false; diff --git a/src/nvim/search.c b/src/nvim/search.c index c4479a077e..abe05bbd12 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -2373,10 +2373,11 @@ showmatch( * brief pause, unless 'm' is present in 'cpo' and a character is * available. */ - if (vim_strchr(p_cpo, CPO_SHOWMATCH) != NULL) - os_delay(p_mat * 100L, true); - else if (!char_avail()) - os_delay(p_mat * 100L, false); + if (vim_strchr(p_cpo, CPO_SHOWMATCH) != NULL) { + os_delay(p_mat * 100L + 8, true); + } else if (!char_avail()) { + os_delay(p_mat * 100L + 9, false); + } curwin->w_cursor = save_cursor; // restore cursor position *so = save_so; *siso = save_siso; diff --git a/src/nvim/sign.c b/src/nvim/sign.c index c7dc1a5b22..97e64c6c4c 100644 --- a/src/nvim/sign.c +++ b/src/nvim/sign.c @@ -18,6 +18,7 @@ #include "nvim/move.h" #include "nvim/screen.h" #include "nvim/syntax.h" +#include "nvim/option.h" /// Struct to hold the sign properties. typedef struct sign sign_T; @@ -726,16 +727,30 @@ void sign_mark_adjust( long amount_after ) { - sign_entry_T *sign; // a sign in a b_signlist - linenr_T new_lnum; // new line number to assign to sign + sign_entry_T *sign; // a sign in a b_signlist + sign_entry_T *next; // the next sign in a b_signlist + sign_entry_T *last = NULL; // pointer to pointer to current sign + sign_entry_T **lastp = NULL; // pointer to pointer to current sign + linenr_T new_lnum; // new line number to assign to sign + int is_fixed = 0; + int signcol = win_signcol_configured(curwin, &is_fixed); curbuf->b_signcols_max = -1; + lastp = &curbuf->b_signlist; - FOR_ALL_SIGNS_IN_BUF(curbuf, sign) { + for (sign = curbuf->b_signlist; sign != NULL; sign = next) { + next = sign->se_next; new_lnum = sign->se_lnum; if (sign->se_lnum >= line1 && sign->se_lnum <= line2) { if (amount != MAXLNUM) { new_lnum += amount; + } else if (!is_fixed || signcol >= 2) { + *lastp = next; + if (next) { + next->se_prev = last; + } + xfree(sign); + continue; } } else if (sign->se_lnum > line2) { new_lnum += amount_after; @@ -746,6 +761,9 @@ void sign_mark_adjust( if (sign->se_lnum >= line1 && new_lnum <= curbuf->b_ml.ml_line_count) { sign->se_lnum = new_lnum; } + + last = sign; + lastp = &sign->se_next; } } diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index 825aef1465..ed886ab7f9 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -3469,13 +3469,13 @@ static void syn_cmd_onoff(exarg_T *eap, char *name) } } -void syn_maybe_on(void) +void syn_maybe_enable(void) { if (!did_syntax_onoff) { exarg_T ea; ea.arg = (char_u *)""; ea.skip = false; - syn_cmd_onoff(&ea, "syntax"); + syn_cmd_enable(&ea, false); } } @@ -5306,13 +5306,17 @@ get_id_list( xfree(name); break; } - if (name[1] == 'A') - id = SYNID_ALLBUT; - else if (name[1] == 'T') - id = SYNID_TOP; - else - id = SYNID_CONTAINED; - id += current_syn_inc_tag; + if (name[1] == 'A') { + id = SYNID_ALLBUT + current_syn_inc_tag; + } else if (name[1] == 'T') { + if (curwin->w_s->b_syn_topgrp >= SYNID_CLUSTER) { + id = curwin->w_s->b_syn_topgrp; + } else { + id = SYNID_TOP + current_syn_inc_tag; + } + } else { + id = SYNID_CONTAINED + current_syn_inc_tag; + } } else if (name[1] == '@') { if (skip) { id = -1; diff --git a/src/nvim/tag.c b/src/nvim/tag.c index 588821f260..a6310344e9 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -625,7 +625,7 @@ do_tag( } if (ic && !msg_scrolled && msg_silent == 0) { ui_flush(); - os_delay(1000L, true); + os_delay(1007L, true); } } @@ -2853,7 +2853,7 @@ static int jumpto_tag( MSG(_("E435: Couldn't find tag, just guessing!")); if (!msg_scrolled && msg_silent == 0) { ui_flush(); - os_delay(1000L, true); + os_delay(1010L, true); } } retval = OK; diff --git a/src/nvim/testdir/test_alot.vim b/src/nvim/testdir/test_alot.vim index 71af3eead7..e50602ccad 100644 --- a/src/nvim/testdir/test_alot.vim +++ b/src/nvim/testdir/test_alot.vim @@ -35,6 +35,7 @@ source test_popup.vim source test_put.vim source test_rename.vim source test_scroll_opt.vim +source test_shift.vim source test_sort.vim source test_sha256.vim source test_suspend.vim diff --git a/src/nvim/testdir/test_alot_utf8.vim b/src/nvim/testdir/test_alot_utf8.vim index be0bd01413..70f14320a6 100644 --- a/src/nvim/testdir/test_alot_utf8.vim +++ b/src/nvim/testdir/test_alot_utf8.vim @@ -6,7 +6,6 @@ source test_charsearch_utf8.vim source test_expr_utf8.vim -source test_listlbr_utf8.vim source test_matchadd_conceal_utf8.vim source test_mksession_utf8.vim source test_regexp_utf8.vim diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim index 5e99edf233..5611560b1b 100644 --- a/src/nvim/testdir/test_autocmd.vim +++ b/src/nvim/testdir/test_autocmd.vim @@ -276,28 +276,28 @@ func Test_augroup_warning() augroup TheWarning au VimEnter * echo 'entering' augroup END - call assert_true(match(execute('au VimEnter'), "TheWarning.*VimEnter") >= 0) + call assert_match("TheWarning.*VimEnter", execute('au VimEnter')) redir => res augroup! TheWarning redir END - call assert_true(match(res, "W19:") >= 0) - call assert_true(match(execute('au VimEnter'), "-Deleted-.*VimEnter") >= 0) + call assert_match("W19:", res) + call assert_match("-Deleted-.*VimEnter", execute('au VimEnter')) " check "Another" does not take the pace of the deleted entry augroup Another augroup END - call assert_true(match(execute('au VimEnter'), "-Deleted-.*VimEnter") >= 0) + call assert_match("-Deleted-.*VimEnter", execute('au VimEnter')) augroup! Another " no warning for postpone aucmd delete augroup StartOK au VimEnter * call RemoveGroup() augroup END - call assert_true(match(execute('au VimEnter'), "StartOK.*VimEnter") >= 0) + call assert_match("StartOK.*VimEnter", execute('au VimEnter')) redir => res doautocmd VimEnter redir END - call assert_true(match(res, "W19:") < 0) + call assert_notmatch("W19:", res) au! VimEnter endfunc @@ -325,7 +325,7 @@ func Test_augroup_deleted() au VimEnter * echo augroup end augroup! x - call assert_true(match(execute('au VimEnter'), "-Deleted-.*VimEnter") >= 0) + call assert_match("-Deleted-.*VimEnter", execute('au VimEnter')) au! VimEnter endfunc diff --git a/src/nvim/testdir/test_backspace_opt.vim b/src/nvim/testdir/test_backspace_opt.vim index d680b442db..11459991ea 100644 --- a/src/nvim/testdir/test_backspace_opt.vim +++ b/src/nvim/testdir/test_backspace_opt.vim @@ -1,15 +1,5 @@ " Tests for 'backspace' settings -func Exec(expr) - let str='' - try - exec a:expr - catch /.*/ - let str=v:exception - endtry - return str -endfunc - func Test_backspace_option() set backspace= call assert_equal('', &backspace) @@ -41,10 +31,10 @@ func Test_backspace_option() set backspace-=eol call assert_equal('', &backspace) " Check the error - call assert_equal(0, match(Exec('set backspace=ABC'), '.*E474')) - call assert_equal(0, match(Exec('set backspace+=def'), '.*E474')) + call assert_fails('set backspace=ABC', 'E474:') + call assert_fails('set backspace+=def', 'E474:') " NOTE: Vim doesn't check following error... - "call assert_equal(0, match(Exec('set backspace-=ghi'), '.*E474')) + "call assert_fails('set backspace-=ghi', 'E474:') " Check backwards compatibility with version 5.4 and earlier set backspace=0 @@ -55,8 +45,8 @@ func Test_backspace_option() call assert_equal('2', &backspace) set backspace=3 call assert_equal('3', &backspace) - call assert_false(match(Exec('set backspace=4'), '.*E474')) - call assert_false(match(Exec('set backspace=10'), '.*E474')) + call assert_fails('set backspace=4', 'E474:') + call assert_fails('set backspace=10', 'E474:') " Cleared when 'compatible' is set " set compatible diff --git a/src/nvim/testdir/test_excmd.vim b/src/nvim/testdir/test_excmd.vim index 98a3e60368..15557056ee 100644 --- a/src/nvim/testdir/test_excmd.vim +++ b/src/nvim/testdir/test_excmd.vim @@ -47,7 +47,7 @@ func Test_buffers_lastused() endfor call assert_equal(['bufb', 'bufa', 'bufc'], names) - call assert_match('[0-2] seconds ago', bufs[1][1]) + call assert_match('[0-2] seconds\= ago', bufs[1][1]) bwipeout bufa bwipeout bufb diff --git a/src/nvim/testdir/test_expr.vim b/src/nvim/testdir/test_expr.vim index 09d79979ce..0b41a1127a 100644 --- a/src/nvim/testdir/test_expr.vim +++ b/src/nvim/testdir/test_expr.vim @@ -501,3 +501,12 @@ func Test_empty_concatenate() call assert_equal('b', 'a'[4:0] . 'b') call assert_equal('b', 'b' . 'a'[4:0]) endfunc + +func Test_eval_after_if() + let s:val = '' + func SetVal(x) + let s:val ..= a:x + endfunc + if 0 | eval SetVal('a') | endif | call SetVal('b') + call assert_equal('b', s:val) +endfunc diff --git a/src/nvim/testdir/test_filetype.vim b/src/nvim/testdir/test_filetype.vim index 1a98dc6451..3cfc964f0a 100644 --- a/src/nvim/testdir/test_filetype.vim +++ b/src/nvim/testdir/test_filetype.vim @@ -275,6 +275,8 @@ let s:filename_checks = { \ 'lss': ['file.lss'], \ 'lua': ['file.lua', 'file.rockspec', 'file.nse'], \ 'lynx': ['lynx.cfg'], + \ 'm3build': ['m3makefile', 'm3overrides'], + \ 'm3quake': ['file.quake', 'cm3.cfg'], \ 'm4': ['file.at'], \ 'mail': ['snd.123', '.letter', '.letter.123', '.followup', '.article', '.article.123', 'pico.123', 'mutt-xx-xxx', 'muttng-xx-xxx', 'ae123.txt', 'file.eml'], \ 'mailaliases': ['/etc/mail/aliases', '/etc/aliases'], @@ -374,6 +376,7 @@ let s:filename_checks = { \ 'ps1': ['file.ps1', 'file.psd1', 'file.psm1', 'file.pssc'], \ 'ps1xml': ['file.ps1xml'], \ 'psf': ['file.psf'], + \ 'psl': ['file.psl'], \ 'puppet': ['file.pp'], \ 'pyrex': ['file.pyx', 'file.pxd'], \ 'python': ['file.py', 'file.pyw', '.pythonstartup', '.pythonrc', 'file.ptl', 'file.pyi', 'SConstruct'], diff --git a/src/nvim/testdir/test_functions.vim b/src/nvim/testdir/test_functions.vim index 555f549743..93f567b3a0 100644 --- a/src/nvim/testdir/test_functions.vim +++ b/src/nvim/testdir/test_functions.vim @@ -1071,10 +1071,10 @@ func Test_inputlist() endfunc func Test_balloon_show() - if has('balloon_eval') - " This won't do anything but must not crash either. - call balloon_show('hi!') - endif + CheckFeature balloon_eval + + " This won't do anything but must not crash either. + call balloon_show('hi!') endfunc func Test_shellescape() @@ -1448,4 +1448,12 @@ func Test_nr2char() call assert_equal("\x80\xfc\b\xfd\x80\xfeX\x80\xfeX\x80\xfeX\x80\xfeX\x80\xfeX", eval('"\<M-' .. nr2char(0x40000000) .. '>"')) endfunc +func HasDefault(msg = 'msg') + return a:msg +endfunc + +func Test_default_arg_value() + call assert_equal('msg', HasDefault()) +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_menu.vim b/src/nvim/testdir/test_menu.vim index 055d944b15..de6d4aa359 100644 --- a/src/nvim/testdir/test_menu.vim +++ b/src/nvim/testdir/test_menu.vim @@ -11,7 +11,13 @@ func Test_load_menu() call assert_report('error while loading menus: ' . v:exception) endtry call assert_match('browse confirm w', execute(':menu File.Save')) + + let v:errmsg = '' + doautocmd LoadBufferMenu VimEnter + call assert_equal('', v:errmsg) + source $VIMRUNTIME/delmenu.vim + call assert_equal('', v:errmsg) endfunc func Test_translate_menu() diff --git a/src/nvim/testdir/test_mksession.vim b/src/nvim/testdir/test_mksession.vim index 8486f3ff68..7bb76ad9eb 100644 --- a/src/nvim/testdir/test_mksession.vim +++ b/src/nvim/testdir/test_mksession.vim @@ -680,6 +680,24 @@ func Test_mksession_winpos() set sessionoptions& endfunc +" Test for mksession without options restores winminheight +func Test_mksession_winminheight() + set sessionoptions-=options + split + mksession! Xtest_mks.out + let found_restore = 0 + let lines = readfile('Xtest_mks.out') + for line in lines + if line =~ '= s:save_winmin\(width\|height\)' + let found_restore += 1 + endif + endfor + call assert_equal(2, found_restore) + call delete('Xtest_mks.out') + close + set sessionoptions& +endfunc + " Test for mksession with 'compatible' option func Test_mksession_compatible() throw 'skipped: Nvim does not support "compatible" option' diff --git a/src/nvim/testdir/test_prompt_buffer.vim b/src/nvim/testdir/test_prompt_buffer.vim new file mode 100644 index 0000000000..6fc5850be3 --- /dev/null +++ b/src/nvim/testdir/test_prompt_buffer.vim @@ -0,0 +1,195 @@ +" Tests for setting 'buftype' to "prompt" + +source check.vim +" Nvim's channel implementation differs from Vim's +" CheckFeature channel + +source shared.vim +source screendump.vim + +func CanTestPromptBuffer() + " We need to use a terminal window to be able to feed keys without leaving + " Insert mode. + " Nvim's terminal implementation differs from Vim's + " CheckFeature terminal + + " TODO: make the tests work on MS-Windows + CheckNotMSWindows +endfunc + +func WriteScript(name) + call writefile([ + \ 'func TextEntered(text)', + \ ' if a:text == "exit"', + \ ' " Reset &modified to allow the buffer to be closed.', + \ ' set nomodified', + \ ' stopinsert', + \ ' close', + \ ' else', + \ ' " Add the output above the current prompt.', + \ ' call append(line("$") - 1, "Command: \"" . a:text . "\"")', + \ ' " Reset &modified to allow the buffer to be closed.', + \ ' set nomodified', + \ ' call timer_start(20, {id -> TimerFunc(a:text)})', + \ ' endif', + \ 'endfunc', + \ '', + \ 'func TimerFunc(text)', + \ ' " Add the output above the current prompt.', + \ ' call append(line("$") - 1, "Result: \"" . a:text . "\"")', + \ ' " Reset &modified to allow the buffer to be closed.', + \ ' set nomodified', + \ 'endfunc', + \ '', + \ 'call setline(1, "other buffer")', + \ 'set nomodified', + \ 'new', + \ 'set buftype=prompt', + \ 'call prompt_setcallback(bufnr(""), function("TextEntered"))', + \ 'eval bufnr("")->prompt_setprompt("cmd: ")', + \ 'startinsert', + \ ], a:name) +endfunc + +func Test_prompt_basic() + throw 'skipped: TODO' + call CanTestPromptBuffer() + let scriptName = 'XpromptscriptBasic' + call WriteScript(scriptName) + + let buf = RunVimInTerminal('-S ' . scriptName, {}) + call WaitForAssert({-> assert_equal('cmd:', term_getline(buf, 1))}) + + call term_sendkeys(buf, "hello\<CR>") + call WaitForAssert({-> assert_equal('cmd: hello', term_getline(buf, 1))}) + call WaitForAssert({-> assert_equal('Command: "hello"', term_getline(buf, 2))}) + call WaitForAssert({-> assert_equal('Result: "hello"', term_getline(buf, 3))}) + + call term_sendkeys(buf, "exit\<CR>") + call WaitForAssert({-> assert_equal('other buffer', term_getline(buf, 1))}) + + call StopVimInTerminal(buf) + call delete(scriptName) +endfunc + +func Test_prompt_editing() + throw 'skipped: TODO' + call CanTestPromptBuffer() + let scriptName = 'XpromptscriptEditing' + call WriteScript(scriptName) + + let buf = RunVimInTerminal('-S ' . scriptName, {}) + call WaitForAssert({-> assert_equal('cmd:', term_getline(buf, 1))}) + + let bs = "\<BS>" + call term_sendkeys(buf, "hello" . bs . bs) + call WaitForAssert({-> assert_equal('cmd: hel', term_getline(buf, 1))}) + + let left = "\<Left>" + call term_sendkeys(buf, left . left . left . bs . '-') + call WaitForAssert({-> assert_equal('cmd: -hel', term_getline(buf, 1))}) + + let end = "\<End>" + call term_sendkeys(buf, end . "x") + call WaitForAssert({-> assert_equal('cmd: -helx', term_getline(buf, 1))}) + + call term_sendkeys(buf, "\<C-U>exit\<CR>") + call WaitForAssert({-> assert_equal('other buffer', term_getline(buf, 1))}) + + call StopVimInTerminal(buf) + call delete(scriptName) +endfunc + +func Test_prompt_garbage_collect() + func MyPromptCallback(x, text) + " NOP + endfunc + func MyPromptInterrupt(x) + " NOP + endfunc + + new + set buftype=prompt + " Nvim doesn't support method call syntax yet. + " eval bufnr('')->prompt_setcallback(function('MyPromptCallback', [{}])) + " eval bufnr('')->prompt_setinterrupt(function('MyPromptInterrupt', [{}])) + eval prompt_setcallback(bufnr(''), function('MyPromptCallback', [{}])) + eval prompt_setinterrupt(bufnr(''), function('MyPromptInterrupt', [{}])) + call test_garbagecollect_now() + " Must not crash + call feedkeys("\<CR>\<C-C>", 'xt') + call assert_true(v:true) + + call assert_fails("call prompt_setcallback(bufnr(), [])", 'E921:') + call assert_equal(0, prompt_setcallback({}, '')) + call assert_fails("call prompt_setinterrupt(bufnr(), [])", 'E921:') + call assert_equal(0, prompt_setinterrupt({}, '')) + + delfunc MyPromptCallback + bwipe! +endfunc + +" Test for editing the prompt buffer +func Test_prompt_buffer_edit() + new + set buftype=prompt + normal! i + call assert_beeps('normal! dd') + call assert_beeps('normal! ~') + call assert_beeps('normal! o') + call assert_beeps('normal! O') + call assert_beeps('normal! p') + call assert_beeps('normal! P') + call assert_beeps('normal! u') + call assert_beeps('normal! ra') + call assert_beeps('normal! s') + call assert_beeps('normal! S') + call assert_beeps("normal! \<C-A>") + call assert_beeps("normal! \<C-X>") + " pressing CTRL-W in the prompt buffer should trigger the window commands + call assert_equal(1, winnr()) + " In Nvim, CTRL-W commands aren't usable from insert mode in a prompt buffer + " exe "normal A\<C-W>\<C-W>" + " call assert_equal(2, winnr()) + " wincmd w + close! + call assert_equal(0, prompt_setprompt([], '')) +endfunc + +func Test_prompt_buffer_getbufinfo() + new + call assert_equal('', prompt_getprompt('%')) + call assert_equal('', prompt_getprompt(bufnr('%'))) + let another_buffer = bufnr('%') + + set buftype=prompt + call assert_equal('% ', prompt_getprompt('%')) + call prompt_setprompt( bufnr( '%' ), 'This is a test: ' ) + call assert_equal('This is a test: ', prompt_getprompt('%')) + + call prompt_setprompt( bufnr( '%' ), '' ) + " Nvim doesn't support method call syntax yet. + " call assert_equal('', '%'->prompt_getprompt()) + call assert_equal('', prompt_getprompt('%')) + + call prompt_setprompt( bufnr( '%' ), 'Another: ' ) + call assert_equal('Another: ', prompt_getprompt('%')) + let another = bufnr('%') + + new + + call assert_equal('', prompt_getprompt('%')) + call assert_equal('Another: ', prompt_getprompt(another)) + + " Doesn't exist + let buffers_before = len( getbufinfo() ) + call assert_equal('', prompt_getprompt( bufnr('$') + 1)) + call assert_equal(buffers_before, len( getbufinfo())) + + " invalid type + call assert_fails('call prompt_getprompt({})', 'E728:') + + %bwipe! +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_search.vim b/src/nvim/testdir/test_search.vim index 7aa01c61ca..75d42b986b 100644 --- a/src/nvim/testdir/test_search.vim +++ b/src/nvim/testdir/test_search.vim @@ -7,9 +7,8 @@ source check.vim " See test/functional/legacy/search_spec.lua func Test_search_cmdline() CheckFunction test_override - if !exists('+incsearch') - return - endif + CheckOption incsearch + " need to disable char_avail, " so that expansion of commandline works call test_override("char_avail", 1) @@ -206,9 +205,8 @@ endfunc " See test/functional/legacy/search_spec.lua func Test_search_cmdline2() CheckFunction test_override - if !exists('+incsearch') - return - endif + CheckOption incsearch + " need to disable char_avail, " so that expansion of commandline works call test_override("char_avail", 1) @@ -369,9 +367,8 @@ func Incsearch_cleanup() endfunc func Test_search_cmdline3() - if !exists('+incsearch') - return - endif + CheckOption incsearch + call Cmdline3_prep() 1 " first match @@ -382,9 +379,8 @@ func Test_search_cmdline3() endfunc func Test_search_cmdline3s() - if !exists('+incsearch') - return - endif + CheckOption incsearch + call Cmdline3_prep() 1 call feedkeys(":%s/the\<c-l>/xxx\<cr>", 'tx') @@ -408,9 +404,8 @@ func Test_search_cmdline3s() endfunc func Test_search_cmdline3g() - if !exists('+incsearch') - return - endif + CheckOption incsearch + call Cmdline3_prep() 1 call feedkeys(":g/the\<c-l>/d\<cr>", 'tx') @@ -431,9 +426,8 @@ func Test_search_cmdline3g() endfunc func Test_search_cmdline3v() - if !exists('+incsearch') - return - endif + CheckOption incsearch + call Cmdline3_prep() 1 call feedkeys(":v/the\<c-l>/d\<cr>", 'tx') @@ -450,9 +444,8 @@ endfunc " See test/functional/legacy/search_spec.lua func Test_search_cmdline4() CheckFunction test_override - if !exists('+incsearch') - return - endif + CheckOption incsearch + " need to disable char_avail, " so that expansion of commandline works call test_override("char_avail", 1) @@ -484,9 +477,8 @@ func Test_search_cmdline4() endfunc func Test_search_cmdline5() - if !exists('+incsearch') - return - endif + CheckOption incsearch + " Do not call test_override("char_avail", 1) so that <C-g> and <C-t> work " regardless char_avail. new @@ -503,6 +495,46 @@ func Test_search_cmdline5() bw! endfunc +func Test_search_cmdline6() + " Test that consecutive matches + " are caught by <c-g>/<c-t> + CheckFunction test_override + CheckOption incsearch + + " need to disable char_avail, + " so that expansion of commandline works + call test_override("char_avail", 1) + new + call setline(1, [' bbvimb', '']) + set incsearch + " first match + norm! gg0 + call feedkeys("/b\<cr>", 'tx') + call assert_equal([0,1,2,0], getpos('.')) + " second match + norm! gg0 + call feedkeys("/b\<c-g>\<cr>", 'tx') + call assert_equal([0,1,3,0], getpos('.')) + " third match + norm! gg0 + call feedkeys("/b\<c-g>\<c-g>\<cr>", 'tx') + call assert_equal([0,1,7,0], getpos('.')) + " first match again + norm! gg0 + call feedkeys("/b\<c-g>\<c-g>\<c-g>\<cr>", 'tx') + call assert_equal([0,1,2,0], getpos('.')) + set nowrapscan + " last match + norm! gg0 + call feedkeys("/b\<c-g>\<c-g>\<c-g>\<cr>", 'tx') + call assert_equal([0,1,7,0], getpos('.')) + " clean up + set wrapscan&vim + set noincsearch + call test_override("char_avail", 0) + bw! +endfunc + func Test_search_cmdline7() CheckFunction test_override " Test that pressing <c-g> in an empty command line @@ -598,26 +630,226 @@ func Test_search_regexp() enew! endfunc -" Test for search('multi-byte char', 'bce') -func Test_search_multibyte() - let save_enc = &encoding - set encoding=utf8 - enew! - call append('$', 'A') - call cursor(2, 1) - call assert_equal(2, search('A', 'bce', line('.'))) - enew! - let &encoding = save_enc +func Test_search_cmdline_incsearch_highlight() + CheckFunction test_override + CheckOption incsearch + + set incsearch hlsearch + " need to disable char_avail, + " so that expansion of commandline works + call test_override("char_avail", 1) + new + call setline(1, ['aaa 1 the first', ' 2 the second', ' 3 the third']) + + 1 + call feedkeys("/second\<cr>", 'tx') + call assert_equal('second', @/) + call assert_equal(' 2 the second', getline('.')) + + " Canceling search won't change @/ + 1 + let @/ = 'last pattern' + call feedkeys("/third\<C-c>", 'tx') + call assert_equal('last pattern', @/) + call feedkeys("/third\<Esc>", 'tx') + call assert_equal('last pattern', @/) + call feedkeys("/3\<bs>\<bs>", 'tx') + call assert_equal('last pattern', @/) + call feedkeys("/third\<c-g>\<c-t>\<Esc>", 'tx') + call assert_equal('last pattern', @/) + + " clean up + set noincsearch nohlsearch + bw! endfunc -" Similar to Test_incsearch_substitute() but with a screendump halfway. -func Test_incsearch_substitute_dump() - if !exists('+incsearch') +func Test_search_cmdline_incsearch_highlight_attr() + CheckOption incsearch + CheckFeature terminal + CheckNotGui + + let h = winheight(0) + if h < 3 return endif + + " Prepare buffer text + let lines = ['abb vim vim vi', 'vimvivim'] + call writefile(lines, 'Xsearch.txt') + let buf = term_start([GetVimProg(), '--clean', '-c', 'set noswapfile', 'Xsearch.txt'], {'term_rows': 3}) + + call WaitForAssert({-> assert_equal(lines, [term_getline(buf, 1), term_getline(buf, 2)])}) + " wait for vim to complete initialization + call term_wait(buf) + + " Get attr of normal(a0), incsearch(a1), hlsearch(a2) highlight + call term_sendkeys(buf, ":set incsearch hlsearch\<cr>") + call term_sendkeys(buf, '/b') + call term_wait(buf, 200) + let screen_line1 = term_scrape(buf, 1) + call assert_true(len(screen_line1) > 2) + " a0: attr_normal + let a0 = screen_line1[0].attr + " a1: attr_incsearch + let a1 = screen_line1[1].attr + " a2: attr_hlsearch + let a2 = screen_line1[2].attr + call assert_notequal(a0, a1) + call assert_notequal(a0, a2) + call assert_notequal(a1, a2) + call term_sendkeys(buf, "\<cr>gg0") + + " Test incremental highlight search + call term_sendkeys(buf, "/vim") + call term_wait(buf, 200) + " Buffer: + " abb vim vim vi + " vimvivim + " Search: /vim + let attr_line1 = [a0,a0,a0,a0,a1,a1,a1,a0,a2,a2,a2,a0,a0,a0] + let attr_line2 = [a2,a2,a2,a0,a0,a2,a2,a2] + call assert_equal(attr_line1, map(term_scrape(buf, 1)[:len(attr_line1)-1], 'v:val.attr')) + call assert_equal(attr_line2, map(term_scrape(buf, 2)[:len(attr_line2)-1], 'v:val.attr')) + + " Test <C-g> + call term_sendkeys(buf, "\<C-g>\<C-g>") + call term_wait(buf, 200) + let attr_line1 = [a0,a0,a0,a0,a2,a2,a2,a0,a2,a2,a2,a0,a0,a0] + let attr_line2 = [a1,a1,a1,a0,a0,a2,a2,a2] + call assert_equal(attr_line1, map(term_scrape(buf, 1)[:len(attr_line1)-1], 'v:val.attr')) + call assert_equal(attr_line2, map(term_scrape(buf, 2)[:len(attr_line2)-1], 'v:val.attr')) + + " Test <C-t> + call term_sendkeys(buf, "\<C-t>") + call term_wait(buf, 200) + let attr_line1 = [a0,a0,a0,a0,a2,a2,a2,a0,a1,a1,a1,a0,a0,a0] + let attr_line2 = [a2,a2,a2,a0,a0,a2,a2,a2] + call assert_equal(attr_line1, map(term_scrape(buf, 1)[:len(attr_line1)-1], 'v:val.attr')) + call assert_equal(attr_line2, map(term_scrape(buf, 2)[:len(attr_line2)-1], 'v:val.attr')) + + " Type Enter and a1(incsearch highlight) should become a2(hlsearch highlight) + call term_sendkeys(buf, "\<cr>") + call term_wait(buf, 200) + let attr_line1 = [a0,a0,a0,a0,a2,a2,a2,a0,a2,a2,a2,a0,a0,a0] + let attr_line2 = [a2,a2,a2,a0,a0,a2,a2,a2] + call assert_equal(attr_line1, map(term_scrape(buf, 1)[:len(attr_line1)-1], 'v:val.attr')) + call assert_equal(attr_line2, map(term_scrape(buf, 2)[:len(attr_line2)-1], 'v:val.attr')) + + " Test nohlsearch. a2(hlsearch highlight) should become a0(normal highlight) + call term_sendkeys(buf, ":1\<cr>") + call term_sendkeys(buf, ":set nohlsearch\<cr>") + call term_sendkeys(buf, "/vim") + call term_wait(buf, 200) + let attr_line1 = [a0,a0,a0,a0,a1,a1,a1,a0,a0,a0,a0,a0,a0,a0] + let attr_line2 = [a0,a0,a0,a0,a0,a0,a0,a0] + call assert_equal(attr_line1, map(term_scrape(buf, 1)[:len(attr_line1)-1], 'v:val.attr')) + call assert_equal(attr_line2, map(term_scrape(buf, 2)[:len(attr_line2)-1], 'v:val.attr')) + call delete('Xsearch.txt') + + call delete('Xsearch.txt') + bwipe! +endfunc + +func Test_incsearch_cmdline_modifier() + CheckFunction test_override + CheckOption incsearch + + call test_override("char_avail", 1) + new + call setline(1, ['foo']) + set incsearch + " Test that error E14 does not occur in parsing command modifier. + call feedkeys("V:tab", 'tx') + + call Incsearch_cleanup() +endfunc + +func Test_incsearch_scrolling() if !CanRunVimInTerminal() throw 'Skipped: cannot make screendumps' endif + call assert_equal(0, &scrolloff) + call writefile([ + \ 'let dots = repeat(".", 120)', + \ 'set incsearch cmdheight=2 scrolloff=0', + \ 'call setline(1, [dots, dots, dots, "", "target", dots, dots])', + \ 'normal gg', + \ 'redraw', + \ ], 'Xscript') + let buf = RunVimInTerminal('-S Xscript', {'rows': 9, 'cols': 70}) + " Need to send one key at a time to force a redraw + call term_sendkeys(buf, '/') + sleep 100m + call term_sendkeys(buf, 't') + sleep 100m + call term_sendkeys(buf, 'a') + sleep 100m + call term_sendkeys(buf, 'r') + sleep 100m + call term_sendkeys(buf, 'g') + call VerifyScreenDump(buf, 'Test_incsearch_scrolling_01', {}) + + call term_sendkeys(buf, "\<Esc>") + call StopVimInTerminal(buf) + call delete('Xscript') +endfunc + +func Test_incsearch_search_dump() + CheckOption incsearch + CheckScreendump + + call writefile([ + \ 'set incsearch hlsearch scrolloff=0', + \ 'for n in range(1, 8)', + \ ' call setline(n, "foo " . n)', + \ 'endfor', + \ '3', + \ ], 'Xis_search_script') + let buf = RunVimInTerminal('-S Xis_search_script', {'rows': 9, 'cols': 70}) + " Give Vim a chance to redraw to get rid of the spaces in line 2 caused by + " the 'ambiwidth' check. + sleep 100m + + " Need to send one key at a time to force a redraw. + call term_sendkeys(buf, '/fo') + call VerifyScreenDump(buf, 'Test_incsearch_search_01', {}) + call term_sendkeys(buf, "\<Esc>") + sleep 100m + + call term_sendkeys(buf, '/\v') + call VerifyScreenDump(buf, 'Test_incsearch_search_02', {}) + call term_sendkeys(buf, "\<Esc>") + + call StopVimInTerminal(buf) + call delete('Xis_search_script') +endfunc + +func Test_incsearch_substitute() + CheckFunction test_override + CheckOption incsearch + + call test_override("char_avail", 1) + new + set incsearch + for n in range(1, 10) + call setline(n, 'foo ' . n) + endfor + 4 + call feedkeys(":.,.+2s/foo\<BS>o\<BS>o/xxx\<cr>", 'tx') + call assert_equal('foo 3', getline(3)) + call assert_equal('xxx 4', getline(4)) + call assert_equal('xxx 5', getline(5)) + call assert_equal('xxx 6', getline(6)) + call assert_equal('foo 7', getline(7)) + + call Incsearch_cleanup() +endfunc + +" Similar to Test_incsearch_substitute() but with a screendump halfway. +func Test_incsearch_substitute_dump() + CheckOption incsearch + CheckScreendump + call writefile([ \ 'set incsearch hlsearch scrolloff=0', \ 'for n in range(1, 10)', @@ -724,12 +956,8 @@ func Test_incsearch_substitute_dump() endfunc func Test_incsearch_highlighting() - if !exists('+incsearch') - return - endif - if !CanRunVimInTerminal() - throw 'Skipped: cannot make screendumps' - endif + CheckOption incsearch + CheckScreendump call writefile([ \ 'set incsearch hlsearch', @@ -745,16 +973,40 @@ func Test_incsearch_highlighting() call term_sendkeys(buf, ":%s;ello/the") call VerifyScreenDump(buf, 'Test_incsearch_substitute_15', {}) call term_sendkeys(buf, "<Esc>") + + call StopVimInTerminal(buf) + call delete('Xis_subst_hl_script') +endfunc + +func Test_incsearch_with_change() + CheckFeature timers + CheckOption incsearch + CheckScreendump + + call writefile([ + \ 'set incsearch hlsearch scrolloff=0', + \ 'call setline(1, ["one", "two ------ X", "three"])', + \ 'call timer_start(200, { _ -> setline(2, "x")})', + \ ], 'Xis_change_script') + let buf = RunVimInTerminal('-S Xis_change_script', {'rows': 9, 'cols': 70}) + " Give Vim a chance to redraw to get rid of the spaces in line 2 caused by + " the 'ambiwidth' check. + sleep 300m + + " Highlight X, it will be deleted by the timer callback. + call term_sendkeys(buf, ':%s/X') + call VerifyScreenDump(buf, 'Test_incsearch_change_01', {}) + call term_sendkeys(buf, "\<Esc>") + + call StopVimInTerminal(buf) + call delete('Xis_change_script') endfunc " Similar to Test_incsearch_substitute_dump() for :sort func Test_incsearch_sort_dump() - if !exists('+incsearch') - return - endif - if !CanRunVimInTerminal() - throw 'Skipped: cannot make screendumps' - endif + CheckOption incsearch + CheckScreendump + call writefile([ \ 'set incsearch hlsearch scrolloff=0', \ 'call setline(1, ["another one 2", "that one 3", "the one 1"])', @@ -778,12 +1030,9 @@ endfunc " Similar to Test_incsearch_substitute_dump() for :vimgrep famiry func Test_incsearch_vimgrep_dump() - if !exists('+incsearch') - return - endif - if !CanRunVimInTerminal() - throw 'Skipped: cannot make screendumps' - endif + CheckOption incsearch + CheckScreendump + call writefile([ \ 'set incsearch hlsearch scrolloff=0', \ 'call setline(1, ["another one 2", "that one 3", "the one 1"])', @@ -820,9 +1069,8 @@ endfunc func Test_keep_last_search_pattern() CheckFunction test_override - if !exists('+incsearch') - return - endif + CheckOption incsearch + new call setline(1, ['foo', 'foo', 'foo']) set incsearch @@ -842,9 +1090,8 @@ endfunc func Test_word_under_cursor_after_match() CheckFunction test_override - if !exists('+incsearch') - return - endif + CheckOption incsearch + new call setline(1, 'foo bar') set incsearch @@ -862,9 +1109,8 @@ endfunc func Test_subst_word_under_cursor() CheckFunction test_override - if !exists('+incsearch') - return - endif + CheckOption incsearch + new call setline(1, ['int SomeLongName;', 'for (xxx = 1; xxx < len; ++xxx)']) set incsearch @@ -878,130 +1124,6 @@ func Test_subst_word_under_cursor() set noincsearch endfunc -func Test_incsearch_with_change() - if !has('timers') || !exists('+incsearch') || !CanRunVimInTerminal() - throw 'Skipped: cannot make screendumps and/or timers feature and/or incsearch option missing' - endif - - call writefile([ - \ 'set incsearch hlsearch scrolloff=0', - \ 'call setline(1, ["one", "two ------ X", "three"])', - \ 'call timer_start(200, { _ -> setline(2, "x")})', - \ ], 'Xis_change_script') - let buf = RunVimInTerminal('-S Xis_change_script', {'rows': 9, 'cols': 70}) - " Give Vim a chance to redraw to get rid of the spaces in line 2 caused by - " the 'ambiwidth' check. - sleep 300m - - " Highlight X, it will be deleted by the timer callback. - call term_sendkeys(buf, ':%s/X') - call VerifyScreenDump(buf, 'Test_incsearch_change_01', {}) - call term_sendkeys(buf, "\<Esc>") - - call StopVimInTerminal(buf) - call delete('Xis_change_script') -endfunc - -func Test_incsearch_cmdline_modifier() - CheckFunction test_override - if !exists('+incsearch') - return - endif - call test_override("char_avail", 1) - new - call setline(1, ['foo']) - set incsearch - " Test that error E14 does not occur in parsing command modifier. - call feedkeys("V:tab", 'tx') - - call Incsearch_cleanup() -endfunc - -func Test_incsearch_scrolling() - if !CanRunVimInTerminal() - return - endif - call assert_equal(0, &scrolloff) - call writefile([ - \ 'let dots = repeat(".", 120)', - \ 'set incsearch cmdheight=2 scrolloff=0', - \ 'call setline(1, [dots, dots, dots, "", "target", dots, dots])', - \ 'normal gg', - \ 'redraw', - \ ], 'Xscript') - let buf = RunVimInTerminal('-S Xscript', {'rows': 9, 'cols': 70}) - " Need to send one key at a time to force a redraw - call term_sendkeys(buf, '/') - sleep 100m - call term_sendkeys(buf, 't') - sleep 100m - call term_sendkeys(buf, 'a') - sleep 100m - call term_sendkeys(buf, 'r') - sleep 100m - call term_sendkeys(buf, 'g') - call VerifyScreenDump(buf, 'Test_incsearch_scrolling_01', {}) - - call term_sendkeys(buf, "\<Esc>") - call StopVimInTerminal(buf) - call delete('Xscript') -endfunc - -func Test_incsearch_search_dump() - if !exists('+incsearch') - return - endif - if !CanRunVimInTerminal() - return - endif - call writefile([ - \ 'set incsearch hlsearch scrolloff=0', - \ 'for n in range(1, 8)', - \ ' call setline(n, "foo " . n)', - \ 'endfor', - \ '3', - \ ], 'Xis_search_script') - let buf = RunVimInTerminal('-S Xis_search_script', {'rows': 9, 'cols': 70}) - " Give Vim a chance to redraw to get rid of the spaces in line 2 caused by - " the 'ambiwidth' check. - sleep 100m - - " Need to send one key at a time to force a redraw. - call term_sendkeys(buf, '/fo') - call VerifyScreenDump(buf, 'Test_incsearch_search_01', {}) - call term_sendkeys(buf, "\<Esc>") - sleep 100m - - call term_sendkeys(buf, '/\v') - call VerifyScreenDump(buf, 'Test_incsearch_search_02', {}) - call term_sendkeys(buf, "\<Esc>") - - call StopVimInTerminal(buf) - call delete('Xis_search_script') -endfunc - -func Test_incsearch_substitute() - CheckFunction test_override - if !exists('+incsearch') - return - endif - call test_override("char_avail", 1) - new - set incsearch - for n in range(1, 10) - call setline(n, 'foo ' . n) - endfor - 4 - call feedkeys(":.,.+2s/foo\<BS>o\<BS>o/xxx\<cr>", 'tx') - call assert_equal('foo 3', getline(3)) - call assert_equal('xxx 4', getline(4)) - call assert_equal('xxx 5', getline(5)) - call assert_equal('xxx 6', getline(6)) - call assert_equal('foo 7', getline(7)) - - call Incsearch_cleanup() -endfunc - func Test_incsearch_substitute_long_line() CheckFunction test_override new @@ -1018,9 +1140,8 @@ func Test_incsearch_substitute_long_line() endfunc func Test_search_undefined_behaviour() - if !has("terminal") - return - endif + CheckFeature terminal + let h = winheight(0) if h < 3 return @@ -1036,6 +1157,18 @@ func Test_search_undefined_behaviour2() call search("\%UC0000000") endfunc +" Test for search('multi-byte char', 'bce') +func Test_search_multibyte() + let save_enc = &encoding + set encoding=utf8 + enew! + call append('$', 'A') + call cursor(2, 1) + call assert_equal(2, search('A', 'bce', line('.'))) + enew! + let &encoding = save_enc +endfunc + " This was causing E874. Also causes an invalid read? func Test_look_behind() new @@ -1074,9 +1207,8 @@ func Test_search_Ctrl_L_combining() " ' ̇' U+0307 Dec:775 COMBINING DOT ABOVE ̇ /\%u307\Z "\u0307" " ' ̣' U+0323 Dec:803 COMBINING DOT BELOW ̣ /\%u323 "\u0323" " Those should also appear on the commandline - if !exists('+incsearch') - return - endif + CheckOption incsearch + call Cmdline3_prep() 1 let bufcontent = ['', 'Miạ̀́̇m'] @@ -1126,9 +1258,8 @@ endfunc func Test_incsearch_add_char_under_cursor() CheckFunction test_override - if !exists('+incsearch') - return - endif + CheckOption incsearch + set incsearch new call setline(1, ['find match', 'anything']) @@ -1213,7 +1344,7 @@ func Test_search_smartcase_utf8() close! endfunc -func Test_zzzz_incsearch_highlighting_newline() +func Test_incsearch_highlighting_newline() CheckRunVimInTerminal CheckOption incsearch CheckScreendump @@ -1226,20 +1357,16 @@ func Test_zzzz_incsearch_highlighting_newline() [CODE] call writefile(commands, 'Xincsearch_nl') let buf = RunVimInTerminal('-S Xincsearch_nl', {'rows': 5, 'cols': 10}) - " Need to send one key at a time to force a redraw call term_sendkeys(buf, '/test') - sleep 100m call VerifyScreenDump(buf, 'Test_incsearch_newline1', {}) + " Need to send one key at a time to force a redraw call term_sendkeys(buf, '\n') - sleep 100m call VerifyScreenDump(buf, 'Test_incsearch_newline2', {}) call term_sendkeys(buf, 'x') - sleep 100m call VerifyScreenDump(buf, 'Test_incsearch_newline3', {}) call term_sendkeys(buf, 'x') call VerifyScreenDump(buf, 'Test_incsearch_newline4', {}) call term_sendkeys(buf, "\<CR>") - sleep 100m call VerifyScreenDump(buf, 'Test_incsearch_newline5', {}) call StopVimInTerminal(buf) diff --git a/src/nvim/testdir/test_shift.vim b/src/nvim/testdir/test_shift.vim new file mode 100644 index 0000000000..ec357dac88 --- /dev/null +++ b/src/nvim/testdir/test_shift.vim @@ -0,0 +1,117 @@ +" Test shifting lines with :> and :< + +source check.vim + +func Test_ex_shift_right() + set shiftwidth=2 + + " shift right current line. + call setline(1, range(1, 5)) + 2 + > + 3 + >> + call assert_equal(['1', + \ ' 2', + \ ' 3', + \ '4', + \ '5'], getline(1, '$')) + + " shift right with range. + call setline(1, range(1, 4)) + 2,3>> + call assert_equal(['1', + \ ' 2', + \ ' 3', + \ '4', + \ '5'], getline(1, '$')) + + " shift right with range and count. + call setline(1, range(1, 4)) + 2>3 + call assert_equal(['1', + \ ' 2', + \ ' 3', + \ ' 4', + \ '5'], getline(1, '$')) + + bw! + set shiftwidth& +endfunc + +func Test_ex_shift_left() + set shiftwidth=2 + + call setline(1, range(1, 5)) + %>>> + + " left shift current line. + 2< + 3<< + 4<<<<< + call assert_equal([' 1', + \ ' 2', + \ ' 3', + \ '4', + \ ' 5'], getline(1, '$')) + + " shift right with range. + call setline(1, range(1, 5)) + %>>> + 2,3<< + call assert_equal([' 1', + \ ' 2', + \ ' 3', + \ ' 4', + \ ' 5'], getline(1, '$')) + + " shift right with range and count. + call setline(1, range(1, 5)) + %>>> + 2<<3 + call assert_equal([' 1', + \ ' 2', + \ ' 3', + \ ' 4', + \ ' 5'], getline(1, '$')) + + bw! + set shiftwidth& +endfunc + +func Test_ex_shift_rightleft() + CheckFeature rightleft + + set shiftwidth=2 rightleft + + call setline(1, range(1, 4)) + 2,3<< + call assert_equal(['1', + \ ' 2', + \ ' 3', + \ '4'], getline(1, '$')) + + 3,4> + call assert_equal(['1', + \ ' 2', + \ ' 3', + \ '4'], getline(1, '$')) + + bw! + set rightleft& shiftwidth& +endfunc + +func Test_ex_shift_errors() + call assert_fails('><', 'E488:') + call assert_fails('<>', 'E488:') + + call assert_fails('>!', 'E477:') + call assert_fails('<!', 'E477:') + + " call assert_fails('2,1>', 'E493:') + call assert_fails('execute "2,1>"', 'E493:') + " call assert_fails('2,1<', 'E493:') + call assert_fails('execute "2,1<"', 'E493:') +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_signs.vim b/src/nvim/testdir/test_signs.vim index f6b96c1e5d..9753100375 100644 --- a/src/nvim/testdir/test_signs.vim +++ b/src/nvim/testdir/test_signs.vim @@ -1628,26 +1628,7 @@ func Test_sign_lnum_adjust() " Delete the line with the sign call deletebufline('', 4) let l = sign_getplaced(bufnr('')) - call assert_equal(4, l[0].signs[0].lnum) - - " Undo the delete operation - undo - let l = sign_getplaced(bufnr('')) - call assert_equal(5, l[0].signs[0].lnum) - - " Break the undo - let &undolevels=&undolevels - - " Delete few lines at the end of the buffer including the line with the sign - " Sign line number should not change (as it is placed outside of the buffer) - call deletebufline('', 3, 6) - let l = sign_getplaced(bufnr('')) - call assert_equal(5, l[0].signs[0].lnum) - - " Undo the delete operation. Sign should be restored to the previous line - undo - let l = sign_getplaced(bufnr('')) - call assert_equal(5, l[0].signs[0].lnum) + call assert_equal(0, len(l[0].signs)) sign unplace * group=* sign undefine sign1 diff --git a/src/nvim/testdir/test_syntax.vim b/src/nvim/testdir/test_syntax.vim index 66cb0bbe22..875e23894f 100644 --- a/src/nvim/testdir/test_syntax.vim +++ b/src/nvim/testdir/test_syntax.vim @@ -24,6 +24,32 @@ func GetSyntaxItem(pat) return c endfunc +func AssertHighlightGroups(lnum, startcol, expected, trans = 1, msg = "") + " Assert that the characters starting at a given (line, col) + " sequentially match the expected highlight groups. + " If groups are provided as a string, each character is assumed to be a + " group and spaces represent no group, useful for visually describing tests. + let l:expectedGroups = type(a:expected) == v:t_string + "\ ? a:expected->split('\zs')->map({_, v -> trim(v)}) + \ ? map(split(a:expected, '\zs'), {_, v -> trim(v)}) + \ : a:expected + let l:errors = 0 + " let l:msg = (a:msg->empty() ? "" : a:msg .. ": ") + let l:msg = (empty(a:msg) ? "" : a:msg .. ": ") + \ .. "Wrong highlight group at " .. a:lnum .. "," + + " for l:i in range(a:startcol, a:startcol + l:expectedGroups->len() - 1) + " let l:errors += synID(a:lnum, l:i, a:trans) + " \ ->synIDattr("name") + " \ ->assert_equal(l:expectedGroups[l:i - 1], + for l:i in range(a:startcol, a:startcol + len(l:expectedGroups) - 1) + let l:errors += + \ assert_equal(synIDattr(synID(a:lnum, l:i, a:trans), "name"), + \ l:expectedGroups[l:i - 1], + \ l:msg .. l:i) + endfor +endfunc + func Test_syn_iskeyword() new call setline(1, [ @@ -707,3 +733,22 @@ func Test_syntax_foldlevel() quit! endfunc + +func Test_syn_include_contains_TOP() + let l:case = "TOP in included syntax means its group list name" + new + syntax include @INCLUDED syntax/c.vim + syntax region FencedCodeBlockC start=/```c/ end=/```/ contains=@INCLUDED + + call setline(1, ['```c', '#if 0', 'int', '#else', 'int', '#endif', '```' ]) + let l:expected = ["cCppOutIf2"] + eval AssertHighlightGroups(3, 1, l:expected, 1) + " cCppOutElse has contains=TOP + let l:expected = ["cType"] + eval AssertHighlightGroups(5, 1, l:expected, 1, l:case) + syntax clear + bw! +endfunc + + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/ui_compositor.c b/src/nvim/ui_compositor.c index a2e9266fbb..c1e4a40ef2 100644 --- a/src/nvim/ui_compositor.c +++ b/src/nvim/ui_compositor.c @@ -180,7 +180,8 @@ bool ui_comp_put_grid(ScreenGrid *grid, int row, int col, int height, int width, if (kv_A(layers, insert_at-1) == &pum_grid && (grid != &msg_grid)) { insert_at--; } - if (insert_at > 1 && !on_top) { + if (curwin && kv_A(layers, insert_at-1) == &curwin->w_grid_alloc + && !on_top) { insert_at--; } // not found: new grid diff --git a/src/nvim/window.c b/src/nvim/window.c index c482d265ff..d1163399f5 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -772,9 +772,8 @@ void ui_ext_win_position(win_T *wp) wp->w_winrow = comp_row; wp->w_wincol = comp_col; bool valid = (wp->w_redr_type == 0); - bool on_top = (curwin == wp) || !curwin->w_floating; ui_comp_put_grid(&wp->w_grid_alloc, comp_row, comp_col, - wp->w_height_outer, wp->w_width_outer, valid, on_top); + wp->w_height_outer, wp->w_width_outer, valid, false); ui_check_cursor_grid(wp->w_grid_alloc.handle); wp->w_grid_alloc.focusable = wp->w_float_config.focusable; if (!valid) { diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 3db44f3f11..6926022ee3 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -193,6 +193,44 @@ describe('API', function() eq('', nvim('exec', 'echo', true)) eq('foo 42', nvim('exec', 'echo "foo" 42', true)) end) + + it('displays messages when output=false', function() + local screen = Screen.new(40, 8) + screen:attach() + screen:set_default_attr_ids({ + [0] = {bold=true, foreground=Screen.colors.Blue}, + }) + meths.exec("echo 'hello'", false) + screen:expect{grid=[[ + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + hello | + ]]} + end) + + it('does\'t display messages when output=true', function() + local screen = Screen.new(40, 8) + screen:attach() + screen:set_default_attr_ids({ + [0] = {bold=true, foreground=Screen.colors.Blue}, + }) + meths.exec("echo 'hello'", true) + screen:expect{grid=[[ + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]} + end) end) describe('nvim_command', function() diff --git a/test/functional/autoread/focus_spec.lua b/test/functional/autoread/focus_spec.lua index 1d52e9948f..3f9a0ad09b 100644 --- a/test/functional/autoread/focus_spec.lua +++ b/test/functional/autoread/focus_spec.lua @@ -9,6 +9,7 @@ local feed_data = thelpers.feed_data if helpers.pending_win32(pending) then return end describe('autoread TUI FocusGained/FocusLost', function() + local f1 = 'xtest-foo' local screen before_each(function() @@ -17,8 +18,12 @@ describe('autoread TUI FocusGained/FocusLost', function() ..'", "-u", "NONE", "-i", "NONE", "--cmd", "set noswapfile noshowcmd noruler"]') end) + teardown(function() + os.remove(f1) + end) + it('external file change', function() - local path = 'xtest-foo' + local path = f1 local expected_addition = [[ line 1 line 2 diff --git a/test/functional/lua/buffer_updates_spec.lua b/test/functional/lua/buffer_updates_spec.lua index abca018059..5da8452a51 100644 --- a/test/functional/lua/buffer_updates_spec.lua +++ b/test/functional/lua/buffer_updates_spec.lua @@ -461,6 +461,36 @@ describe('lua: nvim_buf_attach on_bytes', function() } end) + it("deleting lines", function() + local check_events = setup_eventcheck(verify, origlines) + + feed("dd") + + check_events { + { "test1", "bytes", 1, 3, 0, 0, 0, 1, 0, 16, 0, 0, 0 }; + } + + feed("d2j") + + check_events { + { "test1", "bytes", 1, 4, 0, 0, 0, 3, 0, 48, 0, 0, 0 }; + } + + feed("ld<c-v>2j") + + check_events { + { "test1", "bytes", 1, 5, 0, 1, 1, 0, 1, 1, 0, 0, 0 }; + { "test1", "bytes", 1, 5, 1, 1, 16, 0, 1, 1, 0, 0, 0 }; + { "test1", "bytes", 1, 5, 2, 1, 31, 0, 1, 1, 0, 0, 0 }; + } + + feed("vjwd") + + check_events { + { "test1", "bytes", 1, 10, 0, 1, 1, 1, 9, 23, 0, 0, 0 }; + } + end) + it("changing lines", function() local check_events = setup_eventcheck(verify, origlines) @@ -537,20 +567,65 @@ describe('lua: nvim_buf_attach on_bytes', function() end) it('inccomand=nosplit and substitute', function() - local check_events = setup_eventcheck(verify, {"abcde"}) + local check_events = setup_eventcheck(verify, + {"abcde", "12345"}) meths.set_option('inccommand', 'nosplit') - feed ':%s/bcd/' + -- linewise substitute + feed(':%s/bcd/') check_events { { "test1", "bytes", 1, 3, 0, 1, 1, 0, 3, 3, 0, 0, 0 }; { "test1", "bytes", 1, 5, 0, 1, 1, 0, 0, 0, 0, 3, 3 }; } - feed 'a' + feed('a') check_events { { "test1", "bytes", 1, 3, 0, 1, 1, 0, 3, 3, 0, 1, 1 }; { "test1", "bytes", 1, 5, 0, 1, 1, 0, 1, 1, 0, 3, 3 }; } + + feed("<esc>") + + -- splitting lines + feed([[:%s/abc/\r]]) + check_events { + { "test1", "bytes", 1, 3, 0, 0, 0, 0, 3, 3, 1, 0, 1 }; + { "test1", "bytes", 1, 6, 0, 0, 0, 1, 0, 1, 0, 3, 3 }; + } + + feed("<esc>") + -- multi-line regex + feed([[:%s/de\n123/a]]) + + check_events { + { "test1", "bytes", 1, 3, 0, 3, 3, 1, 3, 6, 0, 1, 1 }; + { "test1", "bytes", 1, 6, 0, 3, 3, 0, 1, 1, 1, 3, 6 }; + } + + feed("<esc>") + -- replacing with unicode + feed(":%s/b/→") + + check_events { + { "test1", "bytes", 1, 3, 0, 1, 1, 0, 1, 1, 0, 3, 3 }; + { "test1", "bytes", 1, 5, 0, 1, 1, 0, 3, 3, 0, 1, 1 }; + } + + feed("<esc>") + -- replacing with escaped characters + feed([[:%s/b/\\]]) + check_events { + { "test1", "bytes", 1, 3, 0, 1, 1, 0, 1, 1, 0, 1, 1 }; + { "test1", "bytes", 1, 5, 0, 1, 1, 0, 1, 1, 0, 1, 1 }; + } + + feed("<esc>") + -- replacing with expression register + feed([[:%s/b/\=5+5]]) + check_events { + { "test1", "bytes", 1, 3, 0, 1, 1, 0, 1, 1, 0, 2, 2 }; + { "test1", "bytes", 1, 5, 0, 1, 1, 0, 2, 2, 0, 1, 1 }; + } end) it('nvim_buf_set_text insert', function() @@ -826,6 +901,41 @@ describe('lua: nvim_buf_attach on_bytes', function() end) + it(":luado", function() + local check_events = setup_eventcheck(verify, {"abc", "12345"}) + + command(".luado return 'a'") + + check_events { + { "test1", "bytes", 1, 3, 0, 0, 0, 0, 3, 3, 0, 1, 1 }; + } + + command("luado return 10") + + check_events { + { "test1", "bytes", 1, 4, 0, 0, 0, 0, 1, 1, 0, 2, 2 }; + { "test1", "bytes", 1, 5, 1, 0, 3, 0, 5, 5, 0, 2, 2 }; + } + + end) + + it("flushes deleted bytes on move", function() + local check_events = setup_eventcheck(verify, {"AAA", "BBB", "CCC", "DDD"}) + + feed(":.move+1<cr>") + + check_events { + { "test1", "bytes", 1, 5, 0, 0, 0, 1, 0, 4, 0, 0, 0 }; + { "test1", "bytes", 1, 5, 1, 0, 4, 0, 0, 0, 1, 0, 4 }; + } + + feed("jd2j") + + check_events { + { "test1", "bytes", 1, 6, 2, 0, 8, 2, 0, 8, 0, 0, 0 }; + } + end) + teardown(function() os.remove "Xtest-reload" os.remove "Xtest-undofile" diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 66b33cc9e1..6eda515fb6 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -1124,14 +1124,14 @@ describe('LSP', function() '2nd line of 语text'; }, buf_lines(target_bufnr)) end) - it('correctly goes ahead with the edit if the version is vim.NIL', function() - -- we get vim.NIL when we decode json null value. - local json = exec_lua[[ - return vim.fn.json_decode("{ \"a\": 1, \"b\": null }") - ]] - eq(json.b, exec_lua("return vim.NIL")) - - exec_lua('vim.lsp.util.apply_text_document_edit(...)', text_document_edit(exec_lua("return vim.NIL"))) + it('always accepts edit with version = 0', function() + exec_lua([[ + local args = {...} + local bufnr = select(1, ...) + local text_edit = select(2, ...) + vim.lsp.util.buf_versions[bufnr] = 10 + vim.lsp.util.apply_text_document_edit(text_edit) + ]], target_bufnr, text_document_edit(0)) eq({ 'First ↥ 🤦 🦄 line of text'; '2nd line of 语text'; @@ -1820,20 +1820,36 @@ describe('LSP', function() end) describe('lsp.util.jump_to_location', function() - local target_bufnr + local default_target_bufnr + local default_target_uri = 'file://fake/uri' - before_each(function() - target_bufnr = exec_lua [[ - local bufnr = vim.uri_to_bufnr("file://fake/uri") - local lines = {"1st line of text", "å å ɧ 汉语 ↥ 🤦 🦄"} + local create_buf = function(uri, lines) + for i, line in ipairs(lines) do + lines[i] = '"' .. line .. '"' + end + lines = table.concat(lines, ", ") + + -- Let's set "hidden" to true in order to avoid errors when switching + -- between buffers in test. + local code = string.format([[ + vim.api.nvim_set_option('hidden', true) + + local bufnr = vim.uri_to_bufnr("%s") + local lines = {%s} vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) return bufnr - ]] + ]], uri, lines) + + return exec_lua(code) + end + + before_each(function() + default_target_bufnr = create_buf(default_target_uri, {'1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄'}) end) - local location = function(start_line, start_char, end_line, end_char) + local location = function(uri, start_line, start_char, end_line, end_char) return { - uri = "file://fake/uri", + uri = uri, range = { start = { line = start_line, character = start_char }, ["end"] = { line = end_line, character = end_char }, @@ -1841,9 +1857,9 @@ describe('LSP', function() } end - local jump = function(msg) + local jump = function(bufnr, msg) eq(true, exec_lua('return vim.lsp.util.jump_to_location(...)', msg)) - eq(target_bufnr, exec_lua[[return vim.fn.bufnr('%')]]) + eq(bufnr, exec_lua[[return vim.fn.bufnr('%')]]) return { line = exec_lua[[return vim.fn.line('.')]], col = exec_lua[[return vim.fn.col('.')]], @@ -1851,13 +1867,13 @@ describe('LSP', function() end it('jumps to a Location', function() - local pos = jump(location(0, 9, 0, 9)) + local pos = jump(default_target_bufnr, location(default_target_uri, 0, 9, 0, 9)) eq(1, pos.line) eq(10, pos.col) end) it('jumps to a LocationLink', function() - local pos = jump({ + local pos = jump(default_target_bufnr, { targetUri = "file://fake/uri", targetSelectionRange = { start = { line = 0, character = 4 }, @@ -1873,11 +1889,94 @@ describe('LSP', function() end) it('jumps to the correct multibyte column', function() - local pos = jump(location(1, 2, 1, 2)) + local pos = jump(default_target_bufnr, location(default_target_uri, 1, 2, 1, 2)) eq(2, pos.line) eq(4, pos.col) eq('å', exec_lua[[return vim.fn.expand('<cword>')]]) end) + + it('should not push item to tagstack if destination is the same as source', function() + -- Set cursor at the 2nd line, 1st character. This is the source position + -- for the test, and will also be the destination one, making the cursor + -- "motionless", thus not triggering a push to the tagstack. + exec_lua(string.format([[ + vim.api.nvim_win_set_buf(0, %d) + vim.api.nvim_win_set_cursor(0, {2, 0}) + ]], default_target_bufnr)) + + -- Jump to 'f' in 'foobar', at the 2nd line. + jump(default_target_bufnr, location(default_target_uri, 1, 0, 1, 0)) + + local stack = exec_lua[[return vim.fn.gettagstack()]] + eq(0, stack.length) + end) + + it('should not push the same item from same buffer twice to tagstack', function() + -- Set cursor at the 2nd line, 5th character. + exec_lua(string.format([[ + vim.api.nvim_win_set_buf(0, %d) + vim.api.nvim_win_set_cursor(0, {2, 4}) + ]], default_target_bufnr)) + + local stack + + -- Jump to 1st line, 1st column. + jump(default_target_bufnr, location(default_target_uri, 0, 0, 0, 0)) + + stack = exec_lua[[return vim.fn.gettagstack()]] + eq({default_target_bufnr, 2, 5, 0}, stack.items[1].from) + + -- Go back to 5th character at 2nd line, which is currently at the top of + -- the tagstack. + exec_lua(string.format([[ + vim.api.nvim_win_set_cursor(0, {2, 4}) + ]], default_target_bufnr)) + + -- Jump again to 1st line, 1st column. Since we're jumping from the same + -- position we have just jumped from, this jump shouldn't be pushed to + -- the tagstack. + jump(default_target_bufnr, location(default_target_uri, 0, 0, 0, 0)) + + stack = exec_lua[[return vim.fn.gettagstack()]] + eq({default_target_bufnr, 2, 5, 0}, stack.items[1].from) + eq(1, stack.length) + end) + + it('should not push the same item from another buffer twice to tagstack', function() + local target_uri = 'file://foo/bar' + local target_bufnr = create_buf(target_uri, {'this is a line', 'foobar'}) + + -- Set cursor at the 1st line, 3rd character of the default test buffer. + exec_lua(string.format([[ + vim.api.nvim_win_set_buf(0, %d) + vim.api.nvim_win_set_cursor(0, {1, 2}) + ]], default_target_bufnr)) + + local stack + + -- Jump to 1st line, 1st column of a different buffer from the source + -- position. + jump(target_bufnr, location(target_uri, 0, 0, 0, 0)) + + stack = exec_lua[[return vim.fn.gettagstack()]] + eq({default_target_bufnr, 1, 3, 0}, stack.items[1].from) + + -- Go back to 3rd character at 1st line of the default test buffer, which + -- is currently at the top of the tagstack. + exec_lua(string.format([[ + vim.api.nvim_win_set_buf(0, %d) + vim.api.nvim_win_set_cursor(0, {1, 2}) + ]], default_target_bufnr)) + + -- Jump again to 1st line, 1st column of the different buffer. Since + -- we're jumping from the same position we have just jumped from, this + -- jump shouldn't be pushed to the tagstack. + jump(target_bufnr, location(target_uri, 0, 0, 0, 0)) + + stack = exec_lua[[return vim.fn.gettagstack()]] + eq({default_target_bufnr, 1, 3, 0}, stack.items[1].from) + eq(1, stack.length) + end) end) describe('lsp.util._make_floating_popup_size', function() diff --git a/test/functional/treesitter/highlight_spec.lua b/test/functional/treesitter/highlight_spec.lua index d80d0fdbaf..05e0c5fe2c 100644 --- a/test/functional/treesitter/highlight_spec.lua +++ b/test/functional/treesitter/highlight_spec.lua @@ -445,7 +445,7 @@ describe('treesitter highlighting', function() exec_lua [[ local parser = vim.treesitter.get_parser(0, "c", { - queries = {c = "(preproc_def (preproc_arg) @c) (preproc_function_def value: (preproc_arg) @c)"} + injections = {c = "(preproc_def (preproc_arg) @c) (preproc_function_def value: (preproc_arg) @c)"} }) local highlighter = vim.treesitter.highlighter test_hl = highlighter.new(parser, {queries = {c = hl_query}}) diff --git a/test/functional/treesitter/parser_spec.lua b/test/functional/treesitter/parser_spec.lua index f99362fbdf..72ff6f2fb6 100644 --- a/test/functional/treesitter/parser_spec.lua +++ b/test/functional/treesitter/parser_spec.lua @@ -468,7 +468,7 @@ int x = INT_MAX; it("should inject a language", function() exec_lua([[ parser = vim.treesitter.get_parser(0, "c", { - queries = { + injections = { c = "(preproc_def (preproc_arg) @c) (preproc_function_def value: (preproc_arg) @c)"}}) ]]) @@ -489,7 +489,7 @@ int x = INT_MAX; it("should inject a language", function() exec_lua([[ parser = vim.treesitter.get_parser(0, "c", { - queries = { + injections = { c = "(preproc_def (preproc_arg) @c @combined) (preproc_function_def value: (preproc_arg) @c @combined)"}}) ]]) @@ -506,11 +506,39 @@ int x = INT_MAX; end) end) + describe("when providing parsing information through a directive", function() + it("should inject a language", function() + exec_lua([=[ + vim.treesitter.add_directive("inject-clang!", function(match, _, _, pred, metadata) + metadata.language = "c" + metadata.combined = true + metadata.content = pred[2] + end) + + parser = vim.treesitter.get_parser(0, "c", { + injections = { + c = "(preproc_def ((preproc_arg) @_c (#inject-clang! @_c)))" .. + "(preproc_function_def value: ((preproc_arg) @_a (#inject-clang! @_a)))"}}) + ]=]) + + eq("table", exec_lua("return type(parser:children().c)")) + eq(2, exec_lua("return #parser:children().c:trees()")) + eq({ + {0, 0, 7, 0}, -- root tree + {3, 14, 5, 18}, -- VALUE 123 + -- VALUE1 123 + -- VALUE2 123 + {1, 26, 2, 68} -- READ_STRING(x, y) (char_u *)read_string((x), (size_t)(y)) + -- READ_STRING_OK(x, y) (char_u *)read_string((x), (size_t)(y)) + }, get_ranges()) + end) + end) + describe("when using the offset directive", function() it("should shift the range by the directive amount", function() exec_lua([[ parser = vim.treesitter.get_parser(0, "c", { - queries = { + injections = { c = "(preproc_def ((preproc_arg) @c (#offset! @c 0 2 0 -1))) (preproc_function_def value: (preproc_arg) @c)"}}) ]]) @@ -538,7 +566,7 @@ int x = INT_MAX; it("should return the correct language tree", function() local result = exec_lua([[ parser = vim.treesitter.get_parser(0, "c", { - queries = { c = "(preproc_def (preproc_arg) @c)"}}) + injections = { c = "(preproc_def (preproc_arg) @c)"}}) local sub_tree = parser:language_for_range({1, 18, 1, 19}) @@ -572,28 +600,5 @@ int x = INT_MAX; eq(result, "value") end) end) - - describe("when setting for a capture match", function() - it("should set/get the data correctly", function() - insert([[ - int x = 3; - ]]) - - local result = exec_lua([[ - local result - - query = vim.treesitter.parse_query("c", '((number_literal) @number (#set! @number "key" "value"))') - parser = vim.treesitter.get_parser(0, "c") - - for pattern, match, metadata in query:iter_matches(parser:parse()[1]:root(), 0) do - result = metadata[pattern].key - end - - return result - ]]) - - eq(result, "value") - end) - end) end) end) diff --git a/test/functional/ui/cmdline_spec.lua b/test/functional/ui/cmdline_spec.lua index 29a76c7a07..0ea8bab957 100644 --- a/test/functional/ui/cmdline_spec.lua +++ b/test/functional/ui/cmdline_spec.lua @@ -759,6 +759,7 @@ local function test_cmdline(linegrid) end) it("doesn't send invalid events when aborting mapping #10000", function() + command('set notimeout') command('cnoremap ab c') feed(':xa') diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index 295a54aec8..09638df6c5 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -29,6 +29,7 @@ describe('decorations providers', function() [10] = {italic = true, background = Screen.colors.Magenta}; [11] = {foreground = Screen.colors.Red, background = tonumber('0x005028')}; [12] = {foreground = tonumber('0x990000')}; + [13] = {background = Screen.colors.LightBlue}; } end) @@ -331,10 +332,70 @@ describe('decorations providers', function() | ]]} end) + + it('can have virtual text of the style: right_align', function() + insert(mulholland) + setup_provider [[ + local hl = a.nvim_get_hl_id_by_name "ErrorMsg" + local test_ns = a.nvim_create_namespace "mulholland" + function on_do(event, ...) + if event == "line" then + local win, buf, line = ... + a.nvim_buf_set_extmark(buf, test_ns, line, 0, { + virt_text = {{'+'}, {string.rep(' ', line+1), 'ErrorMsg'}}; + virt_text_pos='right_align'; + ephemeral = true; + }) + end + end + ]] + + screen:expect{grid=[[ + // just to see if there was an acciden+{2: }| + // on Mulholland Drive +{2: }| + try_start(); +{2: }| + bufref_T save_buf; +{2: }| + switch_buffer(&save_buf, buf); +{2: }| + posp = getmark(mark, false); +{2: }| + restore_buffer(&save_buf);^ +{2: }| + | + ]]} + end) + + it('can highlight beyond EOL', function() + insert(mulholland) + setup_provider [[ + local test_ns = a.nvim_create_namespace "veberod" + function on_do(event, ...) + if event == "line" then + local win, buf, line = ... + if string.find(a.nvim_buf_get_lines(buf, line, line+1, true)[1], "buf") then + a.nvim_buf_set_extmark(buf, test_ns, line, 0, { + end_line = line+1; + hl_group = 'DiffAdd'; + hl_eol = true; + ephemeral = true; + }) + end + end + end + ]] + + screen:expect{grid=[[ + // just to see if there was an accident | + // on Mulholland Drive | + try_start(); | + {13:bufref_T save_buf; }| + {13:switch_buffer(&save_buf, buf); }| + posp = getmark(mark, false); | + {13:restore_buffer(&save_buf);^ }| + | + ]]} + end) end) describe('extmark decorations', function() - local screen + local screen, ns before_each( function() clear() screen = Screen.new(50, 15) @@ -365,6 +426,8 @@ describe('extmark decorations', function() [23] = {foreground = Screen.colors.Magenta1, background = Screen.colors.LightGrey}; [24] = {bold = true}; } + + ns = meths.create_namespace 'test' end) local example_text = [[ @@ -385,7 +448,6 @@ end]] insert(example_text) feed 'gg' - local ns = meths.create_namespace 'test' for i = 1,9 do meths.buf_set_extmark(0, ns, i, 0, { virt_text={{'|', 'LineNr'}}, virt_text_pos='overlay'}) if i == 3 or (i >= 6 and i <= 9) then @@ -452,7 +514,6 @@ end]] it('can have virtual text of overlay position and styling', function() insert(example_text) feed 'gg' - local ns = meths.create_namespace 'test' command 'set ft=lua' command 'syntax on' @@ -540,4 +601,88 @@ end]] {24:-- VISUAL LINE --} | ]]} end) + + it('can have virtual text of fixed win_col position', function() + insert(example_text) + feed 'gg' + meths.buf_set_extmark(0, ns, 1, 0, { virt_text={{'Very', 'ErrorMsg'}}, virt_text_win_col=31, hl_mode='blend'}) + meths.buf_set_extmark(0, ns, 2, 10, { virt_text={{'Much', 'ErrorMsg'}}, virt_text_win_col=31, hl_mode='blend'}) + meths.buf_set_extmark(0, ns, 3, 15, { virt_text={{'Error', 'ErrorMsg'}}, virt_text_win_col=31, hl_mode='blend'}) + meths.buf_set_extmark(0, ns, 7, 21, { virt_text={{'-', 'NonText'}}, virt_text_win_col=4, hl_mode='blend'}) + + screen:expect{grid=[[ + ^for _,item in ipairs(items) do | + local text, hl_id_cell, cou{4:Very} unpack(item) | + if hl_id_cell ~= nil then {4:Much} | + hl_id = hl_id_cell {4:Error} | + end | + for _ = 1, (count or 1) do | + local cell = line[colpos] | + {1:-} cell.text = text | + cell.hl_id = hl_id | + colpos = colpos+1 | + end | + end | + {1:~ }| + {1:~ }| + | + ]]} + + feed '3G12|i<cr><esc>' + screen:expect{grid=[[ + for _,item in ipairs(items) do | + local text, hl_id_cell, cou{4:Very} unpack(item) | + if hl_i {4:Much} | + ^d_cell ~= nil then | + hl_id = hl_id_cell {4:Error} | + end | + for _ = 1, (count or 1) do | + local cell = line[colpos] | + {1:-} cell.text = text | + cell.hl_id = hl_id | + colpos = colpos+1 | + end | + end | + {1:~ }| + | + ]]} + + feed 'u:<cr>' + screen:expect{grid=[[ + for _,item in ipairs(items) do | + local text, hl_id_cell, cou{4:Very} unpack(item) | + if hl_i^d_cell ~= nil then {4:Much} | + hl_id = hl_id_cell {4:Error} | + end | + for _ = 1, (count or 1) do | + local cell = line[colpos] | + {1:-} cell.text = text | + cell.hl_id = hl_id | + colpos = colpos+1 | + end | + end | + {1:~ }| + {1:~ }| + : | + ]]} + + feed '8|i<cr><esc>' + screen:expect{grid=[[ + for _,item in ipairs(items) do | + local text, hl_id_cell, cou{4:Very} unpack(item) | + if | + ^hl_id_cell ~= nil then {4:Much} | + hl_id = hl_id_cell {4:Error} | + end | + for _ = 1, (count or 1) do | + local cell = line[colpos] | + {1:-} cell.text = text | + cell.hl_id = hl_id | + colpos = colpos+1 | + end | + end | + {1:~ }| + | + ]]} + end) end) diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index 965b9f160c..ccb13a69d2 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -690,6 +690,49 @@ describe('float window', function() ]]} end + meths.win_set_config(win, {border="solid"}) + if multigrid then + screen:expect{grid=[[ + ## grid 1 + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [3:----------------------------------------]| + ## grid 2 + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + ## grid 3 + | + ## grid 5 + {5: }| + {5: }{1: halloj! }{5: }| + {5: }{1: BORDAA }{5: }| + {5: }| + ]], float_pos={ + [5] = { { id = 1002 }, "NW", 1, 2, 5, true } + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [5] = {win = {id = 1002}, topline = 0, botline = 2, curline = 0, curcol = 0}; + }} + else + screen:expect{grid=[[ + ^ | + {0:~ }| + {0:~ }{5: }{0: }| + {0:~ }{5: }{1: halloj! }{5: }{0: }| + {0:~ }{5: }{1: BORDAA }{5: }{0: }| + {0:~ }{5: }{0: }| + | + ]]} + end + -- support: ascii char, UTF-8 char, composed char, highlight per char meths.win_set_config(win, {border={"x", {"å", "ErrorMsg"}, {"\\"}, {"n̈̊", "Search"}}}) if multigrid then @@ -5974,6 +6017,216 @@ describe('float window', function() ]]) end end) + + it("correctly orders multiple opened floats (current last)", function() + local buf = meths.create_buf(false,false) + local win = meths.open_win(buf, false, {relative='editor', width=20, height=2, row=2, col=5}) + meths.win_set_option(win, "winhl", "Normal:ErrorMsg,EndOfBuffer:ErrorMsg") + + if multigrid then + screen:expect{grid=[[ + ## grid 1 + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [3:----------------------------------------]| + ## grid 2 + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + ## grid 3 + | + ## grid 4 + {7: }| + {7:~ }| + ]], float_pos={ + [4] = { { id = 1001 }, "NW", 1, 2, 5, true }; + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [4] = {win = {id = 1001}, topline = 0, botline = 2, curline = 0, curcol = 0}; + }} + else + screen:expect{grid=[[ + ^ | + {0:~ }| + {0:~ }{7: }{0: }| + {0:~ }{7:~ }{0: }| + {0:~ }| + {0:~ }| + | + ]]} + end + + exec_lua [[ + local buf = vim.api.nvim_create_buf(false,false) + local win = vim.api.nvim_open_win(buf, false, {relative='editor', width=16, height=2, row=3, col=8}) + vim.api.nvim_win_set_option(win, "winhl", "EndOfBuffer:Normal") + buf = vim.api.nvim_create_buf(false,false) + win = vim.api.nvim_open_win(buf, true, {relative='editor', width=12, height=2, row=4, col=10}) + vim.api.nvim_win_set_option(win, "winhl", "Normal:Search,EndOfBuffer:Search") + ]] + + if multigrid then + screen:expect{grid=[[ + ## grid 1 + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [3:----------------------------------------]| + ## grid 2 + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + ## grid 3 + | + ## grid 4 + {7: }| + {7:~ }| + ## grid 5 + {1: }| + {1:~ }| + ## grid 6 + {17:^ }| + {17:~ }| + ]], float_pos={ + [4] = { { id = 1001 }, "NW", 1, 2, 5, true }; + [5] = { { id = 1002 }, "NW", 1, 3, 8, true }; + [6] = { { id = 1003 }, "NW", 1, 4, 10, true }; + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [4] = {win = {id = 1001}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [5] = {win = {id = 1002}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [6] = {win = {id = 1003}, topline = 0, botline = 2, curline = 0, curcol = 0}; + }} + else + screen:expect{grid=[[ + | + {0:~ }| + {0:~ }{7: }{0: }| + {0:~ }{7:~ }{1: }{7: }{0: }| + {0:~ }{1:~ }{17:^ }{1: }{0: }| + {0:~ }{17:~ }{0: }| + | + ]]} + end + end) + + it("correctly orders multiple opened floats (non-current last)", function() + local buf = meths.create_buf(false,false) + local win = meths.open_win(buf, false, {relative='editor', width=20, height=2, row=2, col=5}) + meths.win_set_option(win, "winhl", "Normal:ErrorMsg,EndOfBuffer:ErrorMsg") + + if multigrid then + screen:expect{grid=[[ + ## grid 1 + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [3:----------------------------------------]| + ## grid 2 + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + ## grid 3 + | + ## grid 4 + {7: }| + {7:~ }| + ]], float_pos={ + [4] = { { id = 1001 }, "NW", 1, 2, 5, true }; + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [4] = {win = {id = 1001}, topline = 0, botline = 2, curline = 0, curcol = 0}; + }} + else + screen:expect{grid=[[ + ^ | + {0:~ }| + {0:~ }{7: }{0: }| + {0:~ }{7:~ }{0: }| + {0:~ }| + {0:~ }| + | + ]]} + end + + exec_lua [[ + local buf = vim.api.nvim_create_buf(false,false) + local win = vim.api.nvim_open_win(buf, true, {relative='editor', width=12, height=2, row=4, col=10}) + vim.api.nvim_win_set_option(win, "winhl", "Normal:Search,EndOfBuffer:Search") + buf = vim.api.nvim_create_buf(false,false) + win = vim.api.nvim_open_win(buf, false, {relative='editor', width=16, height=2, row=3, col=8}) + vim.api.nvim_win_set_option(win, "winhl", "EndOfBuffer:Normal") + ]] + + if multigrid then + screen:expect{grid=[[ + ## grid 1 + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [3:----------------------------------------]| + ## grid 2 + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + ## grid 3 + | + ## grid 4 + {7: }| + {7:~ }| + ## grid 5 + {17:^ }| + {17:~ }| + ## grid 6 + {1: }| + {1:~ }| + ]], float_pos={ + [4] = { { id = 1001 }, "NW", 1, 2, 5, true }; + [5] = { { id = 1002 }, "NW", 1, 4, 10, true }; + [6] = { { id = 1003 }, "NW", 1, 3, 8, true }; + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [4] = {win = {id = 1001}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [5] = {win = {id = 1002}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [6] = {win = {id = 1003}, topline = 0, botline = 2, curline = 0, curcol = 0}; + }} + else + screen:expect{grid=[[ + | + {0:~ }| + {0:~ }{7: }{0: }| + {0:~ }{7:~ }{1: }{7: }{0: }| + {0:~ }{1:~ }{17:^ }{1: }{0: }| + {0:~ }{17:~ }{0: }| + | + ]]} + end + end) end describe('with ext_multigrid', function() diff --git a/test/functional/ui/sign_spec.lua b/test/functional/ui/sign_spec.lua index 1937102782..06c92a4b10 100644 --- a/test/functional/ui/sign_spec.lua +++ b/test/functional/ui/sign_spec.lua @@ -264,6 +264,24 @@ describe('Signs', function() {0:~ }| | ]]} + -- line deletion deletes signs. + command('2d') + screen:expect([[ + {1:>>}XX{2: }{6: 1 }a | + XX{1:>>}WW{6: 2 }^c | + {2: }{6: 3 } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) end) it('auto-resize sign column with minimum size (#13783)', function() diff --git a/test/helpers.lua b/test/helpers.lua index 8dbd82cb8c..12d9f19187 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -365,7 +365,11 @@ function module.check_cores(app, force) db_cmd = lldb_db_cmd else initial_path = '.' - re = '/core[^/]*$' + if 'freebsd' == module.uname() then + re = '/nvim.core$' + else + re = '/core[^/]*$' + end exc_re = { '^/%.deps$', '^/%'..deps_prefix()..'$', local_tmpdir, '^/%node_modules$' } db_cmd = gdb_db_cmd random_skip = true diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index d0e7cdc9e3..351f517945 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -175,9 +175,9 @@ set(LUA_COMPAT53_SHA256 bec3a23114a3d9b3218038309657f0f506ad10dfbc03bb54e91da7e5 set(GPERF_URL https://github.com/neovim/deps/raw/ff5b4b18a87397a8564016071ae64f64bcd8c635/opt/gperf-3.1.tar.gz) set(GPERF_SHA256 588546b945bba4b70b6a3a616e80b4ab466e3f33024a352fc2198112cdbb3ae2) -# 7za.exe cat.exe curl.exe ca-bundle.crt diff.exe tee.exe tidy.exe xxd.exe -set(WINTOOLS_URL https://github.com/neovim/deps/raw/2f9acbecf06365c10baa3c0087f34a54c9c6f949/opt/win32tools.zip) -set(WINTOOLS_SHA256 8bfce7e3a365721a027ce842f2ec1cf878f1726233c215c05964aac07300798c) +# cat.exe curl.exe ca-bundle.crt diff.exe tee.exe xxd.exe +set(WINTOOLS_URL https://github.com/neovim/deps/raw/da3520b568054ce057e6168243ff50eea223bfa0/opt/win32tools.zip) +set(WINTOOLS_SHA256 190149d369ae1cd266bc39bceb2d1c061833a23640dfabd4089082c1a7824421) set(WINGUI_URL https://github.com/equalsraf/neovim-qt/releases/download/v0.2.16/neovim-qt.zip) set(WINGUI_SHA256 aad95a1f8413a9ebf36fc0298d0dfd7d786abf88cb0f4ae9f7ec895b70c7b312) |