aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml6
-rwxr-xr-x.github/workflows/env.sh4
-rw-r--r--.gitignore1
-rw-r--r--CMakeLists.txt2
-rw-r--r--README.md2
-rw-r--r--runtime/autoload/dist/ft.vim2
-rw-r--r--runtime/doc/api.txt3
-rw-r--r--runtime/doc/autocmd.txt4
-rw-r--r--runtime/doc/builtin.txt106
-rw-r--r--runtime/doc/change.txt4
-rw-r--r--runtime/doc/cmdline.txt2
-rw-r--r--runtime/doc/diff.txt12
-rw-r--r--runtime/doc/editing.txt4
-rw-r--r--runtime/doc/eval.txt56
-rw-r--r--runtime/doc/fold.txt2
-rw-r--r--runtime/doc/ft_raku.txt24
-rw-r--r--runtime/doc/ft_rust.txt4
-rw-r--r--runtime/doc/ft_sql.txt12
-rw-r--r--runtime/doc/if_pyth.txt2
-rw-r--r--runtime/doc/insert.txt6
-rw-r--r--runtime/doc/lua.txt24
-rw-r--r--runtime/doc/map.txt11
-rw-r--r--runtime/doc/motion.txt2
-rw-r--r--runtime/doc/options.txt16
-rw-r--r--runtime/doc/pattern.txt4
-rw-r--r--runtime/doc/pi_netrw.txt6
-rw-r--r--runtime/doc/print.txt6
-rw-r--r--runtime/doc/quickfix.txt22
-rw-r--r--runtime/doc/sign.txt2
-rw-r--r--runtime/doc/spell.txt4
-rw-r--r--runtime/doc/starting.txt2
-rw-r--r--runtime/doc/syntax.txt10
-rw-r--r--runtime/doc/tabpage.txt18
-rw-r--r--runtime/doc/tagsrch.txt2
-rw-r--r--runtime/doc/term.txt2
-rw-r--r--runtime/doc/tips.txt28
-rw-r--r--runtime/doc/undo.txt4
-rw-r--r--runtime/doc/usr_05.txt2
-rw-r--r--runtime/doc/various.txt4
-rw-r--r--runtime/doc/vim_diff.txt3
-rw-r--r--runtime/doc/windows.txt14
-rw-r--r--runtime/indent/vim.vim20
-rw-r--r--runtime/lua/vim/_load_package.lua49
-rw-r--r--runtime/lua/vim/keymap.lua6
-rwxr-xr-xscripts/gen_vimdoc.py1
-rwxr-xr-xscripts/uncrustify.sh11
-rw-r--r--src/cjson/lua_cjson.c20
-rw-r--r--src/mpack/lmpack.c2
-rw-r--r--src/nvim/CMakeLists.txt3
-rw-r--r--src/nvim/api/autocmd.c669
-rw-r--r--src/nvim/api/autocmd.h11
-rw-r--r--src/nvim/api/keysets.lua29
-rw-r--r--src/nvim/api/private/dispatch.c20
-rw-r--r--src/nvim/api/private/helpers.c34
-rw-r--r--src/nvim/api/private/helpers.h17
-rw-r--r--src/nvim/api/vim.c4
-rw-r--r--src/nvim/api/window.c3
-rw-r--r--src/nvim/aucmd.c123
-rw-r--r--src/nvim/aucmd.h11
-rw-r--r--src/nvim/autocmd.c1347
-rw-r--r--src/nvim/autocmd.h44
-rw-r--r--src/nvim/diff.c9
-rw-r--r--src/nvim/eval.c12
-rw-r--r--src/nvim/eval/funcs.c3
-rw-r--r--src/nvim/eval/typval.c19
-rw-r--r--src/nvim/eval/typval.h3
-rw-r--r--src/nvim/ex_cmds.c2
-rw-r--r--src/nvim/ex_cmds_defs.h30
-rw-r--r--src/nvim/ex_docmd.c24
-rw-r--r--src/nvim/ex_getln.c4
-rw-r--r--src/nvim/fileio.c2
-rw-r--r--src/nvim/getchar.c45
-rw-r--r--src/nvim/globals.h20
-rw-r--r--src/nvim/hardcopy.c43
-rw-r--r--src/nvim/keymap.h3
-rw-r--r--src/nvim/lua/converter.c22
-rw-r--r--src/nvim/lua/executor.c505
-rw-r--r--src/nvim/lua/executor.h24
-rw-r--r--src/nvim/lua/stdlib.c86
-rw-r--r--src/nvim/lua/vim.lua47
-rw-r--r--src/nvim/main.c8
-rw-r--r--src/nvim/map.c1
-rw-r--r--src/nvim/map.h1
-rw-r--r--src/nvim/os/fs.c34
-rw-r--r--src/nvim/runtime.c54
-rwxr-xr-xsrc/nvim/testdir/runnvim.sh6
-rw-r--r--src/nvim/testdir/test_mapping.vim78
-rw-r--r--src/nvim/tui/input.c4
-rw-r--r--src/nvim/ui.c2
-rw-r--r--src/nvim/viml/parser/expressions.c4
-rw-r--r--src/nvim/window.c8
-rw-r--r--test/functional/api/autocmd_spec.lua798
-rw-r--r--test/functional/api/buffer_updates_spec.lua1
-rw-r--r--test/functional/api/command_spec.lua66
-rw-r--r--test/functional/api/window_spec.lua44
-rw-r--r--test/functional/autocmd/autocmd_oldtest_spec.lua86
-rw-r--r--test/functional/autocmd/autocmd_spec.lua167
-rw-r--r--test/functional/autocmd/cursormoved_spec.lua1
-rw-r--r--test/functional/autocmd/show_spec.lua35
-rw-r--r--test/functional/autocmd/termxx_spec.lua4
-rw-r--r--test/functional/ex_cmds/map_spec.lua199
-rw-r--r--test/functional/ex_cmds/mksession_spec.lua4
-rw-r--r--test/functional/lua/thread_spec.lua386
-rw-r--r--test/functional/plugin/shada_spec.lua8
-rw-r--r--test/functional/terminal/cursor_spec.lua7
-rw-r--r--test/functional/ui/cmdline_highlight_spec.lua13
-rw-r--r--test/functional/ui/output_spec.lua3
-rw-r--r--test/helpers.lua10
108 files changed, 4723 insertions, 1080 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 94da9ec6f0..3565b20bfc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,6 +21,7 @@ jobs:
lint:
if: (github.event_name == 'pull_request' && github.base_ref == 'master' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/master')
runs-on: ubuntu-20.04
+ timeout-minutes: 10
env:
CC: gcc
steps:
@@ -66,9 +67,13 @@ jobs:
uses: actions/cache@v2
with:
path: |
+ ${{ env.CACHE_NVIM_DEPS_DIR }}
~/.ccache
key: lint-${{ hashFiles('cmake/*', '**/CMakeLists.txt', '!third-party/**CMakeLists.txt') }}-${{ github.base_ref }}
+ - name: Build third-party
+ run: ./ci/before_script.sh
+
- name: Build nvim
run: ./ci/run_tests.sh build
@@ -208,6 +213,7 @@ jobs:
windows:
runs-on: windows-2019
+ timeout-minutes: 45
if: github.event.pull_request.draft == false
env:
DEPS_BUILD_DIR: ${{ format('{0}/nvim-deps', github.workspace) }}
diff --git a/.github/workflows/env.sh b/.github/workflows/env.sh
index d424924c27..bd170f92fb 100755
--- a/.github/workflows/env.sh
+++ b/.github/workflows/env.sh
@@ -46,7 +46,9 @@ CLANG_SANITIZER=TSAN
EOF
;;
lint)
- BUILD_FLAGS="$BUILD_FLAGS -DLIBLUV_LIBRARY:FILEPATH=/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/lua/5.1/luv.so -DLIBLUV_INCLUDE_DIR:PATH=/usr/include/lua5.1"
+# Re-enable once system deps are available
+# BUILD_FLAGS="$BUILD_FLAGS -DLIBLUV_LIBRARY:FILEPATH=/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/lua/5.1/luv.so -DLIBLUV_INCLUDE_DIR:PATH=/usr/include/lua5.1"
+ DEPS_CMAKE_FLAGS="$DEPS_CMAKE_FLAGS -DUSE_BUNDLED_LUV=ON"
cat <<EOF >> "$GITHUB_ENV"
USE_BUNDLED=OFF
CI_TARGET=lint
diff --git a/.gitignore b/.gitignore
index 0988a51cd9..11479346c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ compile_commands.json
/.clangd/
/.cache/clangd/
/.ccls-cache/
+/.clang-tidy
.DS_Store
*.mo
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 08d52eb071..eae2d75e3f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -387,7 +387,7 @@ include_directories(SYSTEM ${LIBUV_INCLUDE_DIRS})
find_package(Msgpack 1.0.0 REQUIRED)
include_directories(SYSTEM ${MSGPACK_INCLUDE_DIRS})
-find_package(LibLUV 1.30.0 REQUIRED)
+find_package(LibLUV 1.43.0 REQUIRED)
include_directories(SYSTEM ${LIBLUV_INCLUDE_DIRS})
find_package(TreeSitter REQUIRED)
diff --git a/README.md b/README.md
index 8342c27fd7..4ec5cf2be3 100644
--- a/README.md
+++ b/README.md
@@ -119,7 +119,7 @@ Apache 2.0 license, except for contributions copied from Vim (identified by the
[Managed packages]: https://github.com/neovim/neovim/wiki/Installing-Neovim#install-from-package
[Debian]: https://packages.debian.org/testing/neovim
[Ubuntu]: http://packages.ubuntu.com/search?keywords=neovim
-[Fedora]: https://apps.fedoraproject.org/packages/neovim
+[Fedora]: https://packages.fedoraproject.org/pkgs/neovim/neovim/
[Arch Linux]: https://www.archlinux.org/packages/?q=neovim
[Void Linux]: https://voidlinux.org/packages/?arch=x86_64&q=neovim
[Gentoo]: https://packages.gentoo.org/packages/app-editors/neovim
diff --git a/runtime/autoload/dist/ft.vim b/runtime/autoload/dist/ft.vim
index bcb1431b5f..54cb6ce70f 100644
--- a/runtime/autoload/dist/ft.vim
+++ b/runtime/autoload/dist/ft.vim
@@ -1,7 +1,7 @@
" Vim functions for file type detection
"
" Maintainer: Bram Moolenaar <Bram@vim.org>
-" Last Change: 2022 Jan 31
+" Last Change: 2022 Feb 22
" These functions are moved here from runtime/filetype.vim to make startup
" faster.
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt
index de7db3b0b7..cff616cf59 100644
--- a/runtime/doc/api.txt
+++ b/runtime/doc/api.txt
@@ -651,6 +651,9 @@ nvim_add_user_command({name}, {command}, {*opts})
that contains the following keys:
• args: (string) The args passed to the
command, if any |<args>|
+ • fargs: (table) The args split by unescaped
+ whitespace (when more than one argument is
+ allowed), if any |<f-args>|
• bang: (boolean) "true" if the command was
executed with a ! modifier |<bang>|
• line1: (number) The starting line of the
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt
index dbe70b84cf..9054bedc8d 100644
--- a/runtime/doc/autocmd.txt
+++ b/runtime/doc/autocmd.txt
@@ -1406,7 +1406,7 @@ Examples for reading and writing compressed files: >
: autocmd BufReadPre,FileReadPre *.gz set bin
: autocmd BufReadPost,FileReadPost *.gz '[,']!gunzip
: autocmd BufReadPost,FileReadPost *.gz set nobin
- : autocmd BufReadPost,FileReadPost *.gz execute ":doautocmd BufReadPost " . expand("%:r")
+ : autocmd BufReadPost,FileReadPost *.gz execute ":doautocmd BufReadPost " .. expand("%:r")
: autocmd BufWritePost,FileWritePost *.gz !mv <afile> <afile>:r
: autocmd BufWritePost,FileWritePost *.gz !gzip <afile>:r
@@ -1505,7 +1505,7 @@ To insert the current date and time in a *.html file when writing it: >
: else
: let l = line("$")
: endif
- : exe "1," . l . "g/Last modified: /s/Last modified: .*/Last modified: " .
+ : exe "1," .. l .. "g/Last modified: /s/Last modified: .*/Last modified: " ..
: \ strftime("%Y %b %d")
:endfun
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index 833da2622c..03a5f98c6d 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -693,7 +693,7 @@ argv([{nr} [, {winid}]])
:let i = 0
:while i < argc()
: let f = escape(fnameescape(argv(i)), '.')
- : exe 'amenu Arg.' . f . ' :e ' . f . '<CR>'
+ : exe 'amenu Arg.' .. f .. ' :e ' .. f .. '<CR>'
: let i = i + 1
:endwhile
< Without the {nr} argument, or when {nr} is -1, a |List| with
@@ -895,7 +895,7 @@ bufwinid({buf}) *bufwinid()*
see |bufname()| above. If buffer {buf} doesn't exist or
there is no such window, -1 is returned. Example: >
- echo "A window containing buffer 1 is " . (bufwinid(1))
+ echo "A window containing buffer 1 is " .. (bufwinid(1))
<
Only deals with the current tab page.
@@ -908,7 +908,7 @@ bufwinnr({buf}) *bufwinnr()*
If buffer {buf} doesn't exist or there is no such window, -1
is returned. Example: >
- echo "A window containing buffer 1 is " . (bufwinnr(1))
+ echo "A window containing buffer 1 is " .. (bufwinnr(1))
< The number can be used with |CTRL-W_w| and ":wincmd w"
|:wincmd|.
@@ -955,7 +955,7 @@ byteidx({expr}, {nr}) *byteidx()*
byteidxcomp({expr}, {nr}) *byteidxcomp()*
Like byteidx(), except that a composing character is counted
as a separate character. Example: >
- let s = 'e' . nr2char(0x301)
+ let s = 'e' .. nr2char(0x301)
echo byteidx(s, 1)
echo byteidxcomp(s, 1)
echo byteidxcomp(s, 2)
@@ -1152,7 +1152,7 @@ col({expr}) The result is a Number, which is the byte index of the column
col(".") column of cursor
col("$") length of cursor line plus one
col("'t") column of mark t
- col("'" . markname) column of mark markname
+ col("'" .. markname) column of mark markname
< The first column is 1. 0 is returned for an error.
For an uppercase mark the column may actually be in another
buffer.
@@ -1161,7 +1161,7 @@ col({expr}) The result is a Number, which is the byte index of the column
line. This can be used to obtain the column in Insert mode: >
:imap <F2> <C-O>:let save_ve = &ve<CR>
\<C-O>:set ve=all<CR>
- \<C-O>:echo col(".") . "\n" <Bar>
+ \<C-O>:echo col(".") .. "\n" <Bar>
\let &ve = save_ve<CR>
< Can also be used as a |method|: >
@@ -1899,12 +1899,12 @@ expand({string} [, {nosuf} [, {list}]]) *expand()*
:e extension only
Example: >
- :let &tags = expand("%:p:h") . "/tags"
+ :let &tags = expand("%:p:h") .. "/tags"
< Note that when expanding a string that starts with '%', '#' or
'<', any following text is ignored. This does NOT work: >
:let doesntwork = expand("%:h.bak")
< Use this: >
- :let doeswork = expand("%:h") . ".bak"
+ :let doeswork = expand("%:h") .. ".bak"
< Also note that expanding "<cfile>" and others only returns the
referenced file name without further expansion. If "<cfile>"
is "~/.cshrc", you need to do another expand() to have the
@@ -2240,7 +2240,7 @@ fnameescape({string}) *fnameescape()*
and |:write|). And a "-" by itself (special after |:cd|).
Example: >
:let fname = '+some str%nge|name'
- :exe "edit " . fnameescape(fname)
+ :exe "edit " .. fnameescape(fname)
< results in executing: >
edit \+some\ str\%nge\|name
<
@@ -2395,7 +2395,7 @@ function({name} [, {arglist}] [, {dict}])
< The Dictionary is only useful when calling a "dict" function.
In that case the {dict} is passed in as "self". Example: >
function Callback() dict
- echo "called for " . self.name
+ echo "called for " .. self.name
endfunction
...
let context = {"name": "example"}
@@ -2578,7 +2578,7 @@ getbufvar({buf}, {varname} [, {def}]) *getbufvar()*
string is returned, there is no error message.
Examples: >
:let bufmodified = getbufvar(1, "&mod")
- :echo "todo myvar = " . getbufvar("todo", "myvar")
+ :echo "todo myvar = " .. getbufvar("todo", "myvar")
< Can also be used as a |method|: >
GetBufnr()->getbufvar(varname)
@@ -2639,9 +2639,9 @@ getchar([expr]) *getchar()*
This example positions the mouse as it would normally happen: >
let c = getchar()
if c == "\<LeftMouse>" && v:mouse_win > 0
- exe v:mouse_win . "wincmd w"
+ exe v:mouse_win .. "wincmd w"
exe v:mouse_lnum
- exe "normal " . v:mouse_col . "|"
+ exe "normal " .. v:mouse_col .. "|"
endif
<
There is no prompt, you will somehow have to make clear to the
@@ -3374,7 +3374,7 @@ gettabwinvar({tabnr}, {winnr}, {varname} [, {def}]) *gettabwinvar()*
empty string is returned, there is no error message.
Examples: >
:let list_is_on = gettabwinvar(1, 2, '&list')
- :echo "myvar = " . gettabwinvar(3, 1, 'myvar')
+ :echo "myvar = " .. gettabwinvar(3, 1, 'myvar')
<
To obtain all window-local variables use: >
gettabwinvar({tabnr}, {winnr}, '&')
@@ -3489,7 +3489,7 @@ getwinvar({winnr}, {varname} [, {def}]) *getwinvar()*
Like |gettabwinvar()| for the current tabpage.
Examples: >
:let list_is_on = getwinvar(2, '&list')
- :echo "myvar = " . getwinvar(1, 'myvar')
+ :echo "myvar = " .. getwinvar(1, 'myvar')
< Can also be used as a |method|: >
GetWinnr()->getwinvar(varname)
@@ -3758,7 +3758,7 @@ histdel({history} [, {item}]) *histdel()*
The following three are equivalent: >
:call histdel("search", histnr("search"))
:call histdel("search", -1)
- :call histdel("search", '^'.histget("search", -1).'$')
+ :call histdel("search", '^' .. histget("search", -1) .. '$')
<
To delete the last search pattern and use the last-but-one for
the "n" command and 'hlsearch': >
@@ -3777,7 +3777,7 @@ histget({history} [, {index}]) *histget()*
Examples:
Redo the second last search from history. >
- :execute '/' . histget("search", -2)
+ :execute '/' .. histget("search", -2)
< Define an Ex command ":H {num}" that supports re-execution of
the {num}th entry from the output of |:history|. >
@@ -3941,11 +3941,11 @@ input({opts})
let lvl = 0
while i < len(a:cmdline)
if a:cmdline[i] is# '('
- call add(ret, [i, i + 1, 'RBP' . ((lvl % g:rainbow_levels) + 1)])
+ call add(ret, [i, i + 1, 'RBP' .. ((lvl % g:rainbow_levels) + 1)])
let lvl += 1
elseif a:cmdline[i] is# ')'
let lvl -= 1
- call add(ret, [i, i + 1, 'RBP' . ((lvl % g:rainbow_levels) + 1)])
+ call add(ret, [i, i + 1, 'RBP' .. ((lvl % g:rainbow_levels) + 1)])
endif
let i += 1
endwhile
@@ -3975,7 +3975,7 @@ input({opts})
|:execute| or |:normal|.
Example with a mapping: >
- :nmap \x :call GetFoo()<CR>:exe "/" . Foo<CR>
+ :nmap \x :call GetFoo()<CR>:exe "/" .. Foo<CR>
:function GetFoo()
: call inputsave()
: let g:Foo = input("enter search pattern: ")
@@ -4134,7 +4134,7 @@ items({dict}) *items()*
order. Also see |keys()| and |values()|.
Example: >
for [key, value] in items(mydict)
- echo key . ': ' . value
+ echo key .. ': ' .. value
endfor
< Can also be used as a |method|: >
@@ -4270,7 +4270,7 @@ join({list} [, {sep}]) *join()*
{sep} is omitted a single space is used.
Note that {sep} is not added at the end. You might want to
add it there too: >
- let lines = join(mylist, "\n") . "\n"
+ let lines = join(mylist, "\n") .. "\n"
< String items are used as-is. |Lists| and |Dictionaries| are
converted into a string like with |string()|.
The opposite function is |split()|.
@@ -4419,7 +4419,7 @@ line({expr} [, {winid}]) *line()*
line(".") line number of the cursor
line(".", winid) idem, in window "winid"
line("'t") line number of mark t
- line("'" . marker) line number of mark marker
+ line("'" .. marker) line number of mark marker
<
To jump to the last known position when opening a file see
|last-position-jump|.
@@ -4520,7 +4520,7 @@ map({expr1}, {expr2}) *map()*
the current item. For a |Blob| |v:key| has the index of the
current byte.
Example: >
- :call map(mylist, '"> " . v:val . " <"')
+ :call map(mylist, '"> " .. v:val .. " <"')
< This puts "> " before and " <" after each item in "mylist".
Note that {expr2} is the result of an expression and is then
@@ -4534,19 +4534,19 @@ map({expr1}, {expr2}) *map()*
The function must return the new value of the item. Example
that changes each value by "key-value": >
func KeyValue(key, val)
- return a:key . '-' . a:val
+ return a:key .. '-' .. a:val
endfunc
call map(myDict, function('KeyValue'))
< It is shorter when using a |lambda|: >
- call map(myDict, {key, val -> key . '-' . val})
+ call map(myDict, {key, val -> key .. '-' .. val})
< If you do not use "val" you can leave it out: >
- call map(myDict, {key -> 'item: ' . key})
+ call map(myDict, {key -> 'item: ' .. key})
< If you do not use "key" you can use a short name: >
- call map(myDict, {_, val -> 'item: ' . val})
+ call map(myDict, {_, val -> 'item: ' .. val})
<
The operation is done in-place. If you want a |List| or
|Dictionary| to remain unmodified make a copy first: >
- :let tlist = map(copy(mylist), ' v:val . "\t"')
+ :let tlist = map(copy(mylist), ' v:val .. "\t"')
< Returns {expr1}, the |List|, |Blob| or |Dictionary| that was
filtered. When an error is encountered while evaluating
@@ -4612,7 +4612,7 @@ maparg({name} [, {mode} [, {abbr} [, {dict}]]]) *maparg()*
then the global mappings.
This function can be used to map a key even when it's already
mapped, and have it do the original mapping too. Sketch: >
- exe 'nnoremap <Tab> ==' . maparg('<Tab>', 'n')
+ exe 'nnoremap <Tab> ==' .. maparg('<Tab>', 'n')
< Can also be used as a |method|: >
GetKey()->maparg('n')
@@ -5085,7 +5085,7 @@ mkdir({name} [, {path} [, {prot}]])
{prot} is applied for all parts of {name}. Thus if you create
/tmp/foo/bar then /tmp/foo will be created with 0o700. Example: >
- :call mkdir($HOME . "/tmp/foo/bar", "p", 0o700)
+ :call mkdir($HOME .. "/tmp/foo/bar", "p", 0o700)
< This function is not available in the |sandbox|.
@@ -5585,7 +5585,7 @@ prompt_setcallback({buf}, {expr}) *prompt_setcallback()*
stopinsert
close
else
- call append(line('$') - 1, 'Entered: "' . a:text . '"')
+ call append(line('$') - 1, 'Entered: "' .. a:text .. '"')
" Reset 'modified' to allow the buffer to be closed.
set nomodified
endif
@@ -5732,7 +5732,7 @@ readdir({directory} [, {expr}])
function! s:tree(dir)
return {a:dir : map(readdir(a:dir),
\ {_, x -> isdirectory(x) ?
- \ {x : s:tree(a:dir . '/' . x)} : x})}
+ \ {x : s:tree(a:dir .. '/' .. x)} : x})}
endfunction
echo s:tree(".")
<
@@ -5920,7 +5920,7 @@ remote_peek({serverid} [, {retvar}]) *remote_peek()*
This function is not available in the |sandbox|.
Examples: >
:let repl = ""
- :echo "PEEK: ".remote_peek(id, "repl").": ".repl
+ :echo "PEEK: " .. remote_peek(id, "repl") .. ": " .. repl
remote_read({serverid}, [{timeout}]) *remote_read()*
Return the oldest available reply from {serverid} and consume
@@ -5950,12 +5950,12 @@ remote_send({server}, {string} [, {idvar}])
Note: Any errors will be reported in the server and may mess
up the display.
Examples: >
- :echo remote_send("gvim", ":DropAndReply ".file, "serverid").
+ :echo remote_send("gvim", ":DropAndReply " .. file, "serverid") ..
\ remote_read(serverid)
:autocmd NONE RemoteReply *
\ echo remote_read(expand("<amatch>"))
- :echo remote_send("gvim", ":sleep 10 | echo ".
+ :echo remote_send("gvim", ":sleep 10 | echo " ..
\ 'server2client(expand("<client>"), "HELLO")<CR>')
<
*remote_startserver()* *E941* *E942*
@@ -5972,7 +5972,7 @@ remove({list}, {idx} [, {end}]) *remove()*
points to an item before {idx} this is an error.
See |list-index| for possible values of {idx} and {end}.
Example: >
- :echo "last item: " . remove(mylist, -1)
+ :echo "last item: " .. remove(mylist, -1)
:call remove(mylist, 0, 9)
<
Use |delete()| to remove a file.
@@ -5988,13 +5988,13 @@ remove({blob}, {idx} [, {end}])
byte as {end} a |Blob| with one byte is returned. When {end}
points to a byte before {idx} this is an error.
Example: >
- :echo "last byte: " . remove(myblob, -1)
+ :echo "last byte: " .. remove(myblob, -1)
:call remove(mylist, 0, 9)
remove({dict}, {key})
Remove the entry from {dict} with key {key} and return it.
Example: >
- :echo "removed " . remove(dict, "one")
+ :echo "removed " .. remove(dict, "one")
< If there is no {key} in {dict} this is an error.
rename({from}, {to}) *rename()*
@@ -6135,7 +6135,7 @@ screencol() *screencol()*
column inside the command line, which is 1 when the command is
executed. To get the cursor position in the file use one of
the following mappings: >
- nnoremap <expr> GG ":echom ".screencol()."\n"
+ nnoremap <expr> GG ":echom " .. screencol() .. "\n"
nnoremap <silent> GG :echom screencol()<CR>
noremap GG <Cmd>echom screencol()<Cr>
<
@@ -6256,7 +6256,7 @@ search({pattern} [, {flags} [, {stopline} [, {timeout} [, {skip}]]]])
Example (goes over all files in the argument list): >
:let n = 1
:while n <= argc() " loop over all files in arglist
- : exe "argument " . n
+ : exe "argument " .. n
: " start at the last char in the file and wrap for the
: " first search to find match at start of file
: normal G$
@@ -6340,11 +6340,11 @@ searchcount([{options}]) *searchcount()*
return printf(' /%s [%d/%d]', @/,
\ result.current, result.total)
endfunction
- let &statusline .= '%{LastSearchCount()}'
+ let &statusline ..= '%{LastSearchCount()}'
" Or if you want to show the count only when
" 'hlsearch' was on
- " let &statusline .=
+ " let &statusline ..=
" \ '%{v:hlsearch ? LastSearchCount() : ""}'
<
You can also update the search count, which can be useful in a
@@ -7146,10 +7146,10 @@ shellescape({string} [, {special}]) *shellescape()*
inside single quotes.
Example of use with a |:!| command: >
- :exe '!dir ' . shellescape(expand('<cfile>'), 1)
+ :exe '!dir ' .. shellescape(expand('<cfile>'), 1)
< This results in a directory listing for the file under the
cursor. Example of use with |system()|: >
- :call system("chmod +w -- " . shellescape(expand("%")))
+ :call system("chmod +w -- " .. shellescape(expand("%")))
< See also |::S|.
Can also be used as a |method|: >
@@ -7841,7 +7841,7 @@ substitute({string}, {pat}, {sub}, {flags}) *substitute()*
When {sub} starts with "\=", the remainder is interpreted as
an expression. See |sub-replace-expression|. Example: >
:echo substitute(s, '%\(\x\x\)',
- \ '\=nr2char("0x" . submatch(1))', 'g')
+ \ '\=nr2char("0x" .. submatch(1))', 'g')
< When {sub} is a Funcref that function is called, with one
optional argument. Example: >
@@ -7849,7 +7849,7 @@ substitute({string}, {pat}, {sub}, {flags}) *substitute()*
< The optional argument is a list which contains the whole
matched string and up to nine submatches, like what
|submatch()| returns. Example: >
- :echo substitute(s, '%\(\x\x\)', {m -> '0x' . m[1]}, 'g')
+ :echo substitute(s, '%\(\x\x\)', {m -> '0x' .. m[1]}, 'g')
< Can also be used as a |method|: >
GetString()->substitute(pat, sub, flags)
@@ -8167,7 +8167,7 @@ tempname() *tempname()* *temp-file-name*
The result is a String, which is the name of a file that
doesn't exist. It can be used for a temporary file. Example: >
:let tmpfile = tempname()
- :exe "redir > " . tmpfile
+ :exe "redir > " .. tmpfile
< For Unix, the file will be in a private directory |tempfile|.
For MS-Windows forward slashes are used when the 'shellslash'
option is set or when 'shellcmdflag' starts with '-'.
@@ -8343,7 +8343,7 @@ trim({text} [, {mask} [, {dir}]]) *trim()*
Examples: >
echo trim(" some text ")
< returns "some text" >
- echo trim(" \r\t\t\r RESERVE \t\n\x0B\xA0") . "_TAIL"
+ echo trim(" \r\t\t\r RESERVE \t\n\x0B\xA0") .. "_TAIL"
< returns "RESERVE_TAIL" >
echo trim("rm<Xrm<>X>rrm", "rm<>")
< returns "Xrm<>X" (characters in the middle are not removed) >
@@ -8521,7 +8521,7 @@ visualmode([{expr}]) *visualmode()*
character-wise, line-wise, or block-wise Visual mode
respectively.
Example: >
- :exe "normal " . visualmode()
+ :exe "normal " .. visualmode()
< This enters the same Visual mode as before. It is also useful
in scripts if you wish to act differently depending on the
Visual mode that was used.
@@ -8708,7 +8708,7 @@ winbufnr({nr}) The result is a Number, which is the number of the buffer
window is returned.
When window {nr} doesn't exist, -1 is returned.
Example: >
- :echo "The file in the current window is " . bufname(winbufnr(0))
+ :echo "The file in the current window is " .. bufname(winbufnr(0))
<
Can also be used as a |method|: >
FindWindow()->winbufnr()->bufname()
@@ -8733,7 +8733,7 @@ winheight({nr}) *winheight()*
An existing window always has a height of zero or more.
This excludes any window toolbar line.
Examples: >
- :echo "The current window has " . winheight(0) . " lines."
+ :echo "The current window has " .. winheight(0) .. " lines."
< Can also be used as a |method|: >
GetWinid()->winheight()
@@ -8870,7 +8870,7 @@ winwidth({nr}) *winwidth()*
returned. When window {nr} doesn't exist, -1 is returned.
An existing window always has a width of zero or more.
Examples: >
- :echo "The current window has " . winwidth(0) . " columns."
+ :echo "The current window has " .. winwidth(0) .. " columns."
:if winwidth(0) <= 50
: 50 wincmd |
:endif
diff --git a/runtime/doc/change.txt b/runtime/doc/change.txt
index b4d3304880..b170f7cf65 100644
--- a/runtime/doc/change.txt
+++ b/runtime/doc/change.txt
@@ -906,7 +906,7 @@ Consider using a character like "@" or ":". There is no problem if the result
of the expression contains the separation character.
Examples: >
- :s@\n@\="\r" . expand("$HOME") . "\r"@
+ :s@\n@\="\r" .. expand("$HOME") .. "\r"@
This replaces an end-of-line with a new line containing the value of $HOME. >
s/E/\="\<Char-0x20ac>"/g
@@ -1065,7 +1065,7 @@ inside of strings can change! Also see 'softtabstop' option. >
the command. You need to escape the '|' and '"'
characters to prevent them from terminating the
command. Example: >
- :put ='path' . \",/test\"
+ :put ='path' .. \",/test\"
< If there is no expression after '=', Vim uses the
previous expression. You can see it with ":dis =".
diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt
index f2f6ebb2c9..c6e4bf003f 100644
--- a/runtime/doc/cmdline.txt
+++ b/runtime/doc/cmdline.txt
@@ -207,7 +207,7 @@ CTRL-\ e {expr} *c_CTRL-\_e*
Example: >
:cmap <F7> <C-\>eAppendSome()<CR>
:func AppendSome()
- :let cmd = getcmdline() . " Some()"
+ :let cmd = getcmdline() .. " Some()"
:" place the cursor on the )
:call setcmdpos(strlen(cmd))
:return cmd
diff --git a/runtime/doc/diff.txt b/runtime/doc/diff.txt
index abe99102ee..9c5792dd43 100644
--- a/runtime/doc/diff.txt
+++ b/runtime/doc/diff.txt
@@ -371,13 +371,13 @@ Example (this does almost the same as 'diffexpr' being empty): >
function MyDiff()
let opt = ""
if &diffopt =~ "icase"
- let opt = opt . "-i "
+ let opt = opt .. "-i "
endif
if &diffopt =~ "iwhite"
- let opt = opt . "-b "
+ let opt = opt .. "-b "
endif
- silent execute "!diff -a --binary " . opt . v:fname_in . " " . v:fname_new .
- \ " > " . v:fname_out
+ silent execute "!diff -a --binary " .. opt .. v:fname_in .. " " .. v:fname_new ..
+ \ " > " .. v:fname_out
redraw!
endfunction
@@ -427,8 +427,8 @@ Example (this does the same as 'patchexpr' being empty): >
set patchexpr=MyPatch()
function MyPatch()
- :call system("patch -o " . v:fname_out . " " . v:fname_in .
- \ " < " . v:fname_diff)
+ :call system("patch -o " .. v:fname_out .. " " .. v:fname_in ..
+ \ " < " .. v:fname_diff)
endfunction
Make sure that using the "patch" program doesn't have unwanted side effects.
diff --git a/runtime/doc/editing.txt b/runtime/doc/editing.txt
index 4ccf3f145c..bfa01f45a7 100644
--- a/runtime/doc/editing.txt
+++ b/runtime/doc/editing.txt
@@ -411,9 +411,9 @@ does apply like to other wildcards.
Environment variables in the expression are expanded when evaluating the
expression, thus this works: >
- :e `=$HOME . '/.vimrc'`
+ :e `=$HOME .. '/.vimrc'`
This does not work, $HOME is inside a string and used literally: >
- :e `='$HOME' . '/.vimrc'`
+ :e `='$HOME' .. '/.vimrc'`
If the expression returns a string then names are to be separated with line
breaks. When the result is a |List| then each item is used as a name. Line
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index fc422f13e5..3015e232a7 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -402,7 +402,7 @@ It is also possible to put remaining items in a List variable: >
:for [i, j; rest] in listlist
: call Doit(i, j)
: if !empty(rest)
- : echo "remainder: " . string(rest)
+ : echo "remainder: " .. string(rest)
: endif
:endfor
@@ -430,11 +430,11 @@ Functions that are useful with a List: >
:let list = split("a b c") " create list from items in a string
:let string = join(list, ', ') " create string from list items
:let s = string(list) " String representation of list
- :call map(list, '">> " . v:val') " prepend ">> " to each item
+ :call map(list, '">> " .. v:val') " prepend ">> " to each item
Don't forget that a combination of features can make things simple. For
example, to add up all the numbers in a list: >
- :exe 'let sum = ' . join(nrlist, '+')
+ :exe 'let sum = ' .. join(nrlist, '+')
1.4 Dictionaries ~
@@ -496,7 +496,7 @@ turn the Dictionary into a List and pass it to |:for|.
Most often you want to loop over the keys, using the |keys()| function: >
:for key in keys(mydict)
- : echo key . ': ' . mydict[key]
+ : echo key .. ': ' .. mydict[key]
:endfor
The List of keys is unsorted. You may want to sort them first: >
@@ -504,13 +504,13 @@ The List of keys is unsorted. You may want to sort them first: >
To loop over the values use the |values()| function: >
:for v in values(mydict)
- : echo "value: " . v
+ : echo "value: " .. v
:endfor
If you want both the key and the value use the |items()| function. It returns
a List in which each item is a List with two items, the key and the value: >
:for [key, value] in items(mydict)
- : echo key . ': ' . value
+ : echo key .. ': ' .. value
:endfor
@@ -605,7 +605,7 @@ Functions that can be used with a Dictionary: >
:let small = min(dict) " minimum value in dict
:let xs = count(dict, 'x') " count nr of times 'x' appears in dict
:let s = string(dict) " String representation of dict
- :call map(dict, '">> " . v:val') " prepend ">> " to each item
+ :call map(dict, '">> " .. v:val') " prepend ">> " to each item
1.5 Blobs ~
@@ -840,7 +840,7 @@ Example: >
All expressions within one level are parsed from left to right.
-expr1 *expr1* *trinary* *E109*
+expr1 *expr1* *ternary* *E109*
-----
expr2 ? expr1 : expr1
@@ -1362,7 +1362,7 @@ option *expr-option* *E112* *E113*
&l:option local option value
Examples: >
- echo "tabstop is " . &tabstop
+ echo "tabstop is " .. &tabstop
if &insertmode
Any option name can be used here. See |options|. When using the local value
@@ -1637,7 +1637,7 @@ maintain a counter: >
echo "script executed for the first time"
else
let s:counter = s:counter + 1
- echo "script executed " . s:counter . " times now"
+ echo "script executed " .. s:counter .. " times now"
endif
Note that this means that filetype plugins don't get a different set of script
@@ -1736,7 +1736,7 @@ v:completed_item
*v:count* *count-variable*
v:count The count given for the last Normal mode command. Can be used
to get the count before a mapping. Read-only. Example: >
- :map _x :<C-U>echo "the count is " . v:count<CR>
+ :map _x :<C-U>echo "the count is " .. v:count<CR>
< Note: The <C-U> is required to remove the line range that you
get when typing ':' after a count.
When there are two counts, as in "3d2w", they are multiplied,
@@ -2531,9 +2531,9 @@ Example: >
: echohl Title
: echo a:title
: echohl None
- : echo a:0 . " items:"
+ : echo a:0 .. " items:"
: for s in a:000
- : echon ' ' . s
+ : echon ' ' .. s
: endfor
:endfunction
@@ -2572,7 +2572,7 @@ This function can then be called with: >
this works:
*function-range-example* >
:function Mynumber(arg)
- : echo line(".") . " " . a:arg
+ : echo line(".") .. " " .. a:arg
:endfunction
:1,5call Mynumber(getline("."))
<
@@ -2583,7 +2583,7 @@ This function can then be called with: >
Example of a function that handles the range itself: >
:function Cont() range
- : execute (a:firstline + 1) . "," . a:lastline . 's/^/\t\\ '
+ : execute (a:firstline + 1) .. "," .. a:lastline .. 's/^/\t\\ '
:endfunction
:4,8call Cont()
<
@@ -2745,7 +2745,7 @@ This does NOT work: >
This cannot be used to add an item to a |List|.
This cannot be used to set a byte in a String. You
can do that like this: >
- :let var = var[0:2] . 'X' . var[4:]
+ :let var = var[0:2] .. 'X' .. var[4:]
< When {var-name} is a |Blob| then {idx} can be the
length of the blob, in which case one byte is
appended.
@@ -2807,7 +2807,7 @@ This does NOT work: >
is just like using the |:set| command: both the local
value and the global value are changed.
Example: >
- :let &path = &path . ',/usr/local/include'
+ :let &path = &path .. ',/usr/local/include'
:let &{option-name} .= {expr1}
For a string option: Append {expr1} to the value.
@@ -3064,6 +3064,8 @@ text...
:if {expr1} *:if* *:end* *:endif* *:en* *E171* *E579* *E580*
:en[dif] Execute the commands until the next matching ":else"
or ":endif" if {expr1} evaluates to non-zero.
+ Although the short forms work, it is recommended to
+ always use `:endif` to avoid confusion.
From Vim version 4.5 until 5.0, every Ex command in
between the ":if" and ":endif" is ignored. These two
@@ -3661,7 +3663,7 @@ exception most recently caught as long it is not finished.
:function! Caught()
: if v:exception != ""
- : echo 'Caught "' . v:exception . '" in ' . v:throwpoint
+ : echo 'Caught "' .. v:exception .. '" in ' .. v:throwpoint
: else
: echo 'Nothing caught'
: endif
@@ -4064,8 +4066,8 @@ a script in order to catch unexpected things.
:catch /^Vim:Interrupt$/
: echo "Script interrupted"
:catch /.*/
- : echo "Internal error (" . v:exception . ")"
- : echo " - occurred at " . v:throwpoint
+ : echo "Internal error (" .. v:exception .. ")"
+ : echo " - occurred at " .. v:throwpoint
:endtry
:" end of script
@@ -4261,7 +4263,7 @@ parentheses can be cut out from |v:exception| with the ":substitute" command.
:function! CheckRange(a, func)
: if a:a < 0
- : throw "EXCEPT:MATHERR:RANGE(" . a:func . ")"
+ : throw "EXCEPT:MATHERR:RANGE(" .. a:func .. ")"
: endif
:endfunction
:
@@ -4288,7 +4290,7 @@ parentheses can be cut out from |v:exception| with the ":substitute" command.
: try
: execute "write" fnameescape(a:file)
: catch /^Vim(write):/
- : throw "EXCEPT:IO(" . getcwd() . ", " . a:file . "):WRITEERR"
+ : throw "EXCEPT:IO(" .. getcwd() .. ", " .. a:file .. "):WRITEERR"
: endtry
:endfunction
:
@@ -4307,9 +4309,9 @@ parentheses can be cut out from |v:exception| with the ":substitute" command.
: let dir = substitute(v:exception, '.*(\(.\+\),\s*.\+).*', '\1', "")
: let file = substitute(v:exception, '.*(.\+,\s*\(.\+\)).*', '\1', "")
: if file !~ '^/'
- : let file = dir . "/" . file
+ : let file = dir .. "/" .. file
: endif
- : echo 'I/O error for "' . file . '"'
+ : echo 'I/O error for "' .. file .. '"'
:
:catch /^EXCEPT/
: echo "Unspecified error"
@@ -4377,7 +4379,7 @@ clauses, however, is executed.
: echo "inner finally"
: endtry
:catch
- : echo 'outer catch-all caught "' . v:exception . '"'
+ : echo 'outer catch-all caught "' .. v:exception .. '"'
: finally
: echo "outer finally"
:endtry
@@ -4439,7 +4441,7 @@ Printing in Binary ~
: let n = a:nr
: let r = ""
: while n
- : let r = '01'[n % 2] . r
+ : let r = '01'[n % 2] .. r
: let n = n / 2
: endwhile
: return r
@@ -4450,7 +4452,7 @@ Printing in Binary ~
:func String2Bin(str)
: let out = ''
: for ix in range(strlen(a:str))
- : let out = out . '-' . Nr2Bin(char2nr(a:str[ix]))
+ : let out = out .. '-' .. Nr2Bin(char2nr(a:str[ix]))
: endfor
: return out[1:]
:endfunc
diff --git a/runtime/doc/fold.txt b/runtime/doc/fold.txt
index 8bc47a3b10..9e3d78faff 100644
--- a/runtime/doc/fold.txt
+++ b/runtime/doc/fold.txt
@@ -497,7 +497,7 @@ Note the use of backslashes to avoid some characters to be interpreted by the
:function MyFoldText()
: let line = getline(v:foldstart)
: let sub = substitute(line, '/\*\|\*/\|{{{\d\=', '', 'g')
- : return v:folddashes . sub
+ : return v:folddashes .. sub
:endfunction
Evaluating 'foldtext' is done in the |sandbox|. The current window is set to
diff --git a/runtime/doc/ft_raku.txt b/runtime/doc/ft_raku.txt
index 00b140ee9c..3d1179ed4e 100644
--- a/runtime/doc/ft_raku.txt
+++ b/runtime/doc/ft_raku.txt
@@ -47,20 +47,20 @@ Numbers, subscripts and superscripts are available with 's' and 'S':
But some don't come defined by default. Those are digraph definitions you can
add in your ~/.vimrc file. >
- exec 'digraph \\ '.char2nr('∖')
- exec 'digraph \< '.char2nr('≼')
- exec 'digraph \> '.char2nr('≽')
- exec 'digraph (L '.char2nr('⊈')
- exec 'digraph )L '.char2nr('⊉')
- exec 'digraph (/ '.char2nr('⊄')
- exec 'digraph )/ '.char2nr('⊅')
- exec 'digraph )/ '.char2nr('⊅')
- exec 'digraph U+ '.char2nr('⊎')
- exec 'digraph 0- '.char2nr('⊖')
+ exec 'digraph \\ ' .. char2nr('∖')
+ exec 'digraph \< ' .. char2nr('≼')
+ exec 'digraph \> ' .. char2nr('≽')
+ exec 'digraph (L ' .. char2nr('⊈')
+ exec 'digraph )L ' .. char2nr('⊉')
+ exec 'digraph (/ ' .. char2nr('⊄')
+ exec 'digraph )/ ' .. char2nr('⊅')
+ exec 'digraph )/ ' .. char2nr('⊅')
+ exec 'digraph U+ ' .. char2nr('⊎')
+ exec 'digraph 0- ' .. char2nr('⊖')
" Euler's constant
- exec 'digraph ne '.char2nr('𝑒')
+ exec 'digraph ne ' .. char2nr('𝑒')
" Raku's atomic operations marker
- exec 'digraph @@ '.char2nr('⚛')
+ exec 'digraph @@ ' .. char2nr('⚛')
Alternatively, you can write Insert mode abbreviations that convert ASCII-
based operators into their single-character Unicode equivalent. >
diff --git a/runtime/doc/ft_rust.txt b/runtime/doc/ft_rust.txt
index ff2e0ca56f..5c8782ec7a 100644
--- a/runtime/doc/ft_rust.txt
+++ b/runtime/doc/ft_rust.txt
@@ -26,7 +26,7 @@ behavior of the plugin.
g:rustc_path~
Set this option to the path to rustc for use in the |:RustRun| and
|:RustExpand| commands. If unset, "rustc" will be located in $PATH: >
- let g:rustc_path = $HOME."/bin/rustc"
+ let g:rustc_path = $HOME .. "/bin/rustc"
<
*g:rustc_makeprg_no_percent*
@@ -87,7 +87,7 @@ g:rust_bang_comment_leader~
g:ftplugin_rust_source_path~
Set this option to a path that should be prepended to 'path' for Rust
source files: >
- let g:ftplugin_rust_source_path = $HOME.'/dev/rust'
+ let g:ftplugin_rust_source_path = $HOME .. '/dev/rust'
<
*g:rustfmt_command*
diff --git a/runtime/doc/ft_sql.txt b/runtime/doc/ft_sql.txt
index 53a99a9e1d..fccbbce17f 100644
--- a/runtime/doc/ft_sql.txt
+++ b/runtime/doc/ft_sql.txt
@@ -109,8 +109,8 @@ must be configurable. The filetype plugin attempts to define many of the
standard objects, plus many additional ones. In order to make this as
flexible as possible, you can override the list of objects from within your
|vimrc| with the following: >
- let g:ftplugin_sql_objects = 'function,procedure,event,table,trigger' .
- \ ',schema,service,publication,database,datatype,domain' .
+ let g:ftplugin_sql_objects = 'function,procedure,event,table,trigger' ..
+ \ ',schema,service,publication,database,datatype,domain' ..
\ ',index,subscription,synchronization,view,variable'
The following |Normal| mode and |Visual| mode maps have been created which use
@@ -131,10 +131,10 @@ Repeatedly pressing ]} will cycle through each of these create statements: >
create index i1 on t1 (c1);
The default setting for g:ftplugin_sql_objects is: >
- let g:ftplugin_sql_objects = 'function,procedure,event,' .
- \ '\\(existing\\\\|global\\s\\+temporary\\s\\+\\)\\\{,1}' .
- \ 'table,trigger' .
- \ ',schema,service,publication,database,datatype,domain' .
+ let g:ftplugin_sql_objects = 'function,procedure,event,' ..
+ \ '\\(existing\\\\|global\\s\\+temporary\\s\\+\\)\\\{,1}' ..
+ \ 'table,trigger' ..
+ \ ',schema,service,publication,database,datatype,domain' ..
\ ',index,subscription,synchronization,view,variable'
The above will also handle these cases: >
diff --git a/runtime/doc/if_pyth.txt b/runtime/doc/if_pyth.txt
index afdf039aa8..9b434e61d7 100644
--- a/runtime/doc/if_pyth.txt
+++ b/runtime/doc/if_pyth.txt
@@ -658,7 +658,7 @@ To see what version of Python is being used: >
*has-pythonx*
To check if `pyx*` functions and commands are available: >
if has('pythonx')
- echo 'pyx* commands are available. (Python ' . &pyx . ')'
+ echo 'pyx* commands are available. (Python ' .. &pyx .. ')'
endif
==============================================================================
diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt
index ae2b9c4418..39682a2ab2 100644
--- a/runtime/doc/insert.txt
+++ b/runtime/doc/insert.txt
@@ -864,7 +864,7 @@ Groß): >
else
let res = []
let h = ''
- for l in split(system('aiksaurus '.shellescape(a:base)), '\n')
+ for l in split(system('aiksaurus ' .. shellescape(a:base)), '\n')
if l[:3] == '=== '
let h = substitute(l[4:], ' =*$', '', '')
elseif l[0] =~ '\a'
@@ -1199,7 +1199,7 @@ An example that completes the names of the months: >
" find months matching with "a:base"
let res = []
for m in split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec")
- if m =~ '^' . a:base
+ if m =~ '^' .. a:base
call add(res, m)
endif
endfor
@@ -1221,7 +1221,7 @@ The same, but now pretending searching for matches is slow: >
else
" find months matching with "a:base"
for m in split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec")
- if m =~ '^' . a:base
+ if m =~ '^' .. a:base
call complete_add(m)
endif
sleep 300m " simulate searching for next match
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index 355c31090e..98af84e1cb 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -568,6 +568,26 @@ Example: TCP echo-server *tcp-server*
end)
print('TCP echo-server listening on port: '..server:getsockname().port)
+
+Multithreading *lua-loop-threading*
+
+Plugins can perform work in separate (os-level) threads using the threading
+APIs in luv, for instance `vim.loop.new_thread`. Note that every thread
+gets its own separate lua interpreter state, with no access to lua globals
+in the main thread. Neither can the state of the editor (buffers, windows,
+etc) be directly accessed from threads.
+
+A subset of the `vim.*` API is available in threads. This includes:
+
+- `vim.loop` with a separate event loop per thread.
+- `vim.mpack` and `vim.json` (useful for serializing messages between threads)
+- `require` in threads can use lua packages from the global |lua-package-path|
+- `print()` and `vim.inspect`
+- `vim.diff`
+- most utility functions in `vim.*` for working with pure lua values
+ like `vim.split`, `vim.tbl_*`, `vim.list_*`, and so on.
+- `vim.is_thread()` returns true from a non-main thread.
+
------------------------------------------------------------------------------
VIM.HIGHLIGHT *lua-highlight*
@@ -1994,9 +2014,7 @@ set({mode}, {lhs}, {rhs}, {opts}) *vim.keymap.set()*
result of Lua expr maps.
• remap: (boolean) Make the mapping recursive.
This is the inverse of the "noremap" option from
- |nvim_set_keymap()|. Default `true` if `lhs` is
- a string starting with `<plug>`
- (case-insensitive), `false` otherwise.
+ |nvim_set_keymap()|. Default `false` .
See also: ~
|nvim_set_keymap()|
diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt
index 358e944261..8d5449da0b 100644
--- a/runtime/doc/map.txt
+++ b/runtime/doc/map.txt
@@ -65,6 +65,9 @@ modes.
where the map command applies. Disallow mapping of
{rhs}, to avoid nested and recursive mappings. Often
used to redefine a command.
+ Note: "nore" is ignored for a mapping whose result
+ starts with <Plug>. <Plug> is always remapped even if
+ "nore" is used.
:unm[ap] {lhs} |mapmode-nvo| *:unm* *:unmap*
@@ -285,7 +288,7 @@ Here is an example that inserts a list number that increases: >
func ListItem()
let g:counter += 1
- return g:counter . '. '
+ return g:counter .. '. '
endfunc
func ListReset()
@@ -1489,12 +1492,12 @@ The valid escape sequences are
Examples: >
command! -nargs=+ -complete=file MyEdit
\ for f in expand(<q-args>, 0, 1) |
- \ exe '<mods> split ' . f |
+ \ exe '<mods> split ' .. f |
\ endfor
function! SpecialEdit(files, mods)
for f in expand(a:files, 0, 1)
- exe a:mods . ' split ' . f
+ exe a:mods .. ' split ' .. f
endfor
endfunction
command! -nargs=+ -complete=file Sedit
@@ -1570,7 +1573,7 @@ This will invoke: >
: let i = 0
: while i < argc()
: if filereadable(argv(i))
- : execute "e " . argv(i)
+ : execute "e " .. argv(i)
: execute a:command
: endif
: let i = i + 1
diff --git a/runtime/doc/motion.txt b/runtime/doc/motion.txt
index c473244827..20033bd76a 100644
--- a/runtime/doc/motion.txt
+++ b/runtime/doc/motion.txt
@@ -993,7 +993,7 @@ These commands are not marks themselves, but jump to a mark:
:let lnum = line(".")
:keepjumps normal gg
:call SetLastChange()
- :keepjumps exe "normal " . lnum . "G"
+ :keepjumps exe "normal " .. lnum .. "G"
<
Note that ":keepjumps" must be used for every command.
When invoking a function the commands in that function
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 2f76cc018c..56c65394f3 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -897,7 +897,7 @@ A jump table for the options with a short description can be found at |Q_op|.
If you like to keep a lot of backups, you could use a BufWritePre
autocommand to change 'backupext' just before writing the file to
include a timestamp. >
- :au BufWritePre * let &bex = '-' . strftime("%Y%b%d%X") . '~'
+ :au BufWritePre * let &bex = '-' .. strftime("%Y%b%d%X") .. '~'
< Use 'backupdir' to put the backup in a different directory.
*'backupskip'* *'bsk'*
@@ -920,7 +920,7 @@ A jump table for the options with a short description can be found at |Q_op|.
Note that environment variables are not expanded. If you want to use
$HOME you must expand it explicitly, e.g.: >
- :let backupskip = escape(expand('$HOME'), '\') . '/tmp/*'
+ :let backupskip = escape(expand('$HOME'), '\') .. '/tmp/*'
< Note that the default also makes sure that "crontab -e" works (when a
backup would be made by renaming the original file crontab won't see
@@ -1185,7 +1185,7 @@ A jump table for the options with a short description can be found at |Q_op|.
If the default value taken from $CDPATH is not what you want, include
a modified version of the following command in your vimrc file to
override it: >
- :let &cdpath = ',' . substitute(substitute($CDPATH, '[, ]', '\\\0', 'g'), ':', ',', 'g')
+ :let &cdpath = ',' .. substitute(substitute($CDPATH, '[, ]', '\\\0', 'g'), ':', ',', 'g')
< This option cannot be set from a |modeline| or in the |sandbox|, for
security reasons.
(parts of 'cdpath' can be passed to the shell to expand file names).
@@ -1232,8 +1232,8 @@ A jump table for the options with a short description can be found at |Q_op|.
set charconvert=CharConvert()
fun CharConvert()
system("recode "
- \ . v:charconvert_from . ".." . v:charconvert_to
- \ . " <" . v:fname_in . " >" v:fname_out)
+ \ .. v:charconvert_from .. ".." .. v:charconvert_to
+ \ .. " <" .. v:fname_in .. " >" .. v:fname_out)
return v:shell_error
endfun
< The related Vim variables are:
@@ -3620,7 +3620,7 @@ A jump table for the options with a short description can be found at |Q_op|.
global
Language to use for menu translation. Tells which file is loaded
from the "lang" directory in 'runtimepath': >
- "lang/menu_" . &langmenu . ".vim"
+ "lang/menu_" .. &langmenu .. ".vim"
< (without the spaces). For example, to always use the Dutch menus, no
matter what $LANG is set to: >
:set langmenu=nl_NL.ISO_8859-1
@@ -4522,7 +4522,7 @@ A jump table for the options with a short description can be found at |Q_op|.
< To use an environment variable, you probably need to replace the
separator. Here is an example to append $INCL, in which directory
names are separated with a semi-colon: >
- :let &path = &path . "," . substitute($INCL, ';', ',', 'g')
+ :let &path = &path .. "," .. substitute($INCL, ';', ',', 'g')
< Replace the ';' with a ':' or whatever separator is used. Note that
this doesn't work when $INCL contains a comma or white space.
@@ -6570,7 +6570,7 @@ A jump table for the options with a short description can be found at |Q_op|.
This option cannot be set in a modeline when 'modelineexpr' is off.
Example: >
- :auto BufEnter * let &titlestring = hostname() . "/" . expand("%:p")
+ :auto BufEnter * let &titlestring = hostname() .. "/" .. expand("%:p")
:set title titlestring=%<%F%=%l/%L-%P titlelen=70
< The value of 'titlelen' is used to align items in the middle or right
of the available space.
diff --git a/runtime/doc/pattern.txt b/runtime/doc/pattern.txt
index 42005b0d78..c3bd5baff2 100644
--- a/runtime/doc/pattern.txt
+++ b/runtime/doc/pattern.txt
@@ -923,7 +923,7 @@ $ At end of pattern or in front of "\|", "\)" or "\n" ('magic' on):
update the matches. This means Syntax highlighting quickly becomes
wrong.
Example, to highlight the line where the cursor currently is: >
- :exe '/\%' . line(".") . 'l.*'
+ :exe '/\%' .. line(".") .. 'l.*'
< When 'hlsearch' is set and you move the cursor around and make changes
this will clearly show when the match is updated or not.
@@ -939,7 +939,7 @@ $ At end of pattern or in front of "\|", "\)" or "\n" ('magic' on):
update the matches. This means Syntax highlighting quickly becomes
wrong.
Example, to highlight the column where the cursor currently is: >
- :exe '/\%' . col(".") . 'c'
+ :exe '/\%' .. col(".") .. 'c'
< When 'hlsearch' is set and you move the cursor around and make changes
this will clearly show when the match is updated or not.
Example for matching a single byte in column 44: >
diff --git a/runtime/doc/pi_netrw.txt b/runtime/doc/pi_netrw.txt
index 3ac61be6f2..8257152b11 100644
--- a/runtime/doc/pi_netrw.txt
+++ b/runtime/doc/pi_netrw.txt
@@ -968,7 +968,7 @@ itself:
fun! NetReadFixup(method, line1, line2)
if method == 3 " ftp (no <.netrc>)
let fourblanklines= line2 - 3
- silent fourblanklines.",".line2."g/^\s*/d"
+ silent fourblanklines .. "," .. line2 .. "g/^\s*/d"
endif
endfunction
endif
@@ -1975,7 +1975,7 @@ To use this function, simply assign its output to |g:netrw_list_hide| option. >
Example: let g:netrw_list_hide= netrw_gitignore#Hide('my_gitignore_file')
Function can take additional files with git-ignore patterns.
- Example: g:netrw_list_hide= netrw_gitignore#Hide() . '.*\.swp$'
+ Example: let g:netrw_list_hide= netrw_gitignore#Hide() .. '.*\.swp$'
Combining 'netrw_gitignore#Hide' with custom patterns.
<
@@ -2824,7 +2824,7 @@ your browsing preferences. (see also: |netrw-settings|)
Examples:
let g:netrw_list_hide= '.*\.swp$'
- let g:netrw_list_hide= netrw_gitignore#Hide().'.*\.swp$'
+ let g:netrw_list_hide= netrw_gitignore#Hide() .. '.*\.swp$'
default: ""
*g:netrw_localcopycmd* ="cp" Linux/Unix/MacOS/Cygwin
diff --git a/runtime/doc/print.txt b/runtime/doc/print.txt
index f54d0429a6..924fab175e 100644
--- a/runtime/doc/print.txt
+++ b/runtime/doc/print.txt
@@ -127,21 +127,21 @@ file: >
system(['lpr']
+ (empty(&printdevice)?[]:['-P', &printdevice])
+ [v:fname_in])
- . delete(v:fname_in)
+ .. delete(v:fname_in)
+ v:shell_error
On MS-Dos and MS-Windows machines the default is to copy the file to the
currently specified printdevice: >
system(['copy', v:fname_in, empty(&printdevice)?'LPT1':&printdevice])
- . delete(v:fname_in)
+ .. delete(v:fname_in)
If you change this option, using a function is an easy way to avoid having to
escape all the spaces. Example: >
:set printexpr=PrintFile(v:fname_in)
:function PrintFile(fname)
- : call system("ghostview " . a:fname)
+ : call system("ghostview " .. a:fname)
: call delete(a:fname)
: return v:shell_error
:endfunc
diff --git a/runtime/doc/quickfix.txt b/runtime/doc/quickfix.txt
index 5b68da8be9..601384a71f 100644
--- a/runtime/doc/quickfix.txt
+++ b/runtime/doc/quickfix.txt
@@ -341,7 +341,7 @@ processing a quickfix or location list command, it will be aborted.
cursor position will not be changed. See |:cexpr| for
more information.
Example: >
- :g/mypattern/caddexpr expand("%") . ":" . line(".") . ":" . getline(".")
+ :g/mypattern/caddexpr expand("%") .. ":" .. line(".") .. ":" .. getline(".")
<
*:lad* *:addd* *:laddexpr*
:lad[dexpr] {expr} Same as ":caddexpr", except the location list for the
@@ -641,6 +641,24 @@ quickfix window. If there already is a window for that file, it is used
instead. If the buffer in the used window has changed, and the error is in
another file, jumping to the error will fail. You will first have to make
sure the window contains a buffer which can be abandoned.
+
+The following steps are used to find a window to open the file selected from
+the quickfix window:
+1. If 'switchbuf' contains "usetab", then find a window in any tabpage
+ (starting with the first tabpage) that has the selected file and jump to
+ it.
+2. Otherwise find a window displaying the selected file in the current tab
+ page (starting with the window before the quickfix window) and use it.
+3. Otherwise find a window displaying a normal buffer ('buftype' is empty)
+ starting with the window before the quickfix window. If a window is found,
+ open the file in that window.
+4. If a usable window is not found and 'switchbuf' contains "uselast", then
+ open the file in the last used window.
+5. Otherwise open the file in the window before the quickfix window. If there
+ is no previous window, then open the file in the next window.
+6. If a usable window is not found in the above steps, then create a new
+ horizontally split window above the quickfix window and open the file.
+
*CTRL-W_<Enter>* *CTRL-W_<CR>*
You can use CTRL-W <Enter> to open a new window and jump to the error there.
@@ -650,7 +668,7 @@ FileType event (also see |qf.vim|). Then the BufReadPost event is triggered,
using "quickfix" for the buffer name. This can be used to perform some action
on the listed errors. Example: >
au BufReadPost quickfix setlocal modifiable
- \ | silent exe 'g/^/s//\=line(".")." "/'
+ \ | silent exe 'g/^/s//\=line(".") .. " "/'
\ | setlocal nomodifiable
This prepends the line number to each line. Note the use of "\=" in the
substitute string of the ":s" command, which is used to evaluate an
diff --git a/runtime/doc/sign.txt b/runtime/doc/sign.txt
index 5cfa06c33c..a2a5645baa 100644
--- a/runtime/doc/sign.txt
+++ b/runtime/doc/sign.txt
@@ -87,7 +87,7 @@ the delete is undone the sign does not move back.
Here is an example that places a sign "piet", displayed with the text ">>", in
line 23 of the current file: >
:sign define piet text=>> texthl=Search
- :exe ":sign place 2 line=23 name=piet file=" . expand("%:p")
+ :exe ":sign place 2 line=23 name=piet file=" .. expand("%:p")
And here is the command to delete it again: >
:sign unplace 2
diff --git a/runtime/doc/spell.txt b/runtime/doc/spell.txt
index 03c00c8495..bc45b0e511 100644
--- a/runtime/doc/spell.txt
+++ b/runtime/doc/spell.txt
@@ -120,8 +120,8 @@ zuG Undo |zW| and |zG|, remove the word from the internal
rare as this is a fairly uncommon command and all
intuitive commands for this are already taken. If you
want you can add mappings with e.g.: >
- nnoremap z? :exe ':spellrare ' . expand('<cWORD>')<CR>
- nnoremap z/ :exe ':spellrare! ' . expand('<cWORD>')<CR>
+ nnoremap z? :exe ':spellrare ' .. expand('<cWORD>')<CR>
+ nnoremap z/ :exe ':spellrare! ' .. expand('<cWORD>')<CR>
< |:spellundo|, |zuw|, or |zuW| can be used to undo this.
:spellr[rare]! {word} Add {word} as a rare word to the internal word
diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt
index 978142a1e0..1d3fa6c2ca 100644
--- a/runtime/doc/starting.txt
+++ b/runtime/doc/starting.txt
@@ -800,7 +800,7 @@ resulting file, when executed with a ":source" command:
After restoring the Session, the full filename of your current Session is
available in the internal variable |v:this_session|.
An example mapping: >
- :nmap <F2> :wa<Bar>exe "mksession! " . v:this_session<CR>:so ~/sessions/
+ :nmap <F2> :wa<Bar>exe "mksession! " .. v:this_session<CR>:so ~/sessions/
This saves the current Session, and starts off the command to load another.
A session includes all tab pages, unless "tabpages" was removed from
diff --git a/runtime/doc/syntax.txt b/runtime/doc/syntax.txt
index 319a715e40..778f829a4e 100644
--- a/runtime/doc/syntax.txt
+++ b/runtime/doc/syntax.txt
@@ -616,7 +616,7 @@ evaluate to get a unique string to append to each ID used in a given document,
so that the full IDs will be unique even when combined with other content in a
larger HTML document. Example, to append _ and the buffer number to each ID: >
- :let g:html_id_expr = '"_".bufnr("%")'
+ :let g:html_id_expr = '"_" .. bufnr("%")'
<
To append a string "_mystring" to the end of each ID: >
@@ -3550,8 +3550,8 @@ Do you want to draw with the mouse? Try the following: >
:function! GetPixel()
: let c = getline(".")[col(".") - 1]
: echo c
- : exe "noremap <LeftMouse> <LeftMouse>r".c
- : exe "noremap <LeftDrag> <LeftMouse>r".c
+ : exe "noremap <LeftMouse> <LeftMouse>r" .. c
+ : exe "noremap <LeftDrag> <LeftMouse>r" .. c
:endfunction
:noremap <RightMouse> <LeftMouse>:call GetPixel()<CR>
:set guicursor=n:hor20 " to see the color beneath the cursor
@@ -5363,9 +5363,9 @@ types.vim: *.[ch]
And put these lines in your vimrc: >
" load the types.vim highlighting file, if it exists
- autocmd BufRead,BufNewFile *.[ch] let fname = expand('<afile>:p:h') . '/types.vim'
+ autocmd BufRead,BufNewFile *.[ch] let fname = expand('<afile>:p:h') .. '/types.vim'
autocmd BufRead,BufNewFile *.[ch] if filereadable(fname)
- autocmd BufRead,BufNewFile *.[ch] exe 'so ' . fname
+ autocmd BufRead,BufNewFile *.[ch] exe 'so ' .. fname
autocmd BufRead,BufNewFile *.[ch] endif
==============================================================================
diff --git a/runtime/doc/tabpage.txt b/runtime/doc/tabpage.txt
index 7f91fda9f4..c5b61e3a35 100644
--- a/runtime/doc/tabpage.txt
+++ b/runtime/doc/tabpage.txt
@@ -366,24 +366,24 @@ pages and define labels for them. Then get the label for each tab page. >
for i in range(tabpagenr('$'))
" select the highlighting
if i + 1 == tabpagenr()
- let s .= '%#TabLineSel#'
+ let s ..= '%#TabLineSel#'
else
- let s .= '%#TabLine#'
+ let s ..= '%#TabLine#'
endif
" set the tab page number (for mouse clicks)
- let s .= '%' . (i + 1) . 'T'
+ let s ..= '%' .. (i + 1) .. 'T'
" the label is made by MyTabLabel()
- let s .= ' %{MyTabLabel(' . (i + 1) . ')} '
+ let s ..= ' %{MyTabLabel(' .. (i + 1) .. ')} '
endfor
" after the last tab fill with TabLineFill and reset tab page nr
- let s .= '%#TabLineFill#%T'
+ let s ..= '%#TabLineFill#%T'
" right-align the label to close the current tab page
if tabpagenr('$') > 1
- let s .= '%=%#TabLine#%999Xclose'
+ let s ..= '%=%#TabLine#%999Xclose'
endif
return s
@@ -446,14 +446,14 @@ windows in the tab page and a '+' if there is a modified buffer: >
" Append the number of windows in the tab page if more than one
let wincount = tabpagewinnr(v:lnum, '$')
if wincount > 1
- let label .= wincount
+ let label ..= wincount
endif
if label != ''
- let label .= ' '
+ let label ..= ' '
endif
" Append the buffer name
- return label . bufname(bufnrlist[tabpagewinnr(v:lnum) - 1])
+ return label .. bufname(bufnrlist[tabpagewinnr(v:lnum) - 1])
endfunction
set guitablabel=%{GuiTabLabel()}
diff --git a/runtime/doc/tagsrch.txt b/runtime/doc/tagsrch.txt
index 4d938c4a23..5a0d16d6b8 100644
--- a/runtime/doc/tagsrch.txt
+++ b/runtime/doc/tagsrch.txt
@@ -705,7 +705,7 @@ matches the pattern "^# *define" it is not considered to be a comment.
If you want to list matches, and then select one to jump to, you could use a
mapping to do that for you. Here is an example: >
- :map <F4> [I:let nr = input("Which one: ")<Bar>exe "normal " . nr ."[\t"<CR>
+ :map <F4> [I:let nr = input("Which one: ")<Bar>exe "normal " .. nr .. "[\t"<CR>
<
*[i*
[i Display the first line that contains the keyword
diff --git a/runtime/doc/term.txt b/runtime/doc/term.txt
index 62e13285f5..f9b2271756 100644
--- a/runtime/doc/term.txt
+++ b/runtime/doc/term.txt
@@ -321,7 +321,7 @@ an #if/#else/#endif block, the selection becomes linewise.
For MS-Windows and xterm the time for double clicking can be set with the
'mousetime' option. For the other systems this time is defined outside of Vim.
An example, for using a double click to jump to the tag under the cursor: >
- :map <2-LeftMouse> :exe "tag ". expand("<cword>")<CR>
+ :map <2-LeftMouse> :exe "tag " .. expand("<cword>")<CR>
Dragging the mouse with a double click (button-down, button-up, button-down
and then drag) will result in whole words to be selected. This continues
diff --git a/runtime/doc/tips.txt b/runtime/doc/tips.txt
index b77c7d9a6d..d913b53c6b 100644
--- a/runtime/doc/tips.txt
+++ b/runtime/doc/tips.txt
@@ -84,14 +84,14 @@ What you need:
create it with the shell command "mkid file1 file2 ..".
Put this in your |init.vim|: >
- map _u :call ID_search()<Bar>execute "/\\<" . g:word . "\\>"<CR>
- map _n :n<Bar>execute "/\\<" . g:word . "\\>"<CR>
+ map _u :call ID_search()<Bar>execute "/\\<" .. g:word .. "\\>"<CR>
+ map _n :n<Bar>execute "/\\<" .. g:word .. "\\>"<CR>
function! ID_search()
let g:word = expand("<cword>")
- let x = system("lid --key=none ". g:word)
+ let x = system("lid --key=none " .. g:word)
let x = substitute(x, "\n", " ", "g")
- execute "next " . x
+ execute "next " .. x
endfun
To use it, place the cursor on a word, type "_u" and vim will load the file
@@ -285,13 +285,13 @@ This mapping will format any bullet list. It requires that there is an empty
line above and below each list entry. The expression commands are used to
be able to give comments to the parts of the mapping. >
- :let m = ":map _f :set ai<CR>" " need 'autoindent' set
- :let m = m . "{O<Esc>" " add empty line above item
- :let m = m . "}{)^W" " move to text after bullet
- :let m = m . "i <CR> <Esc>" " add space for indent
- :let m = m . "gq}" " format text after the bullet
- :let m = m . "{dd" " remove the empty line
- :let m = m . "5lDJ" " put text after bullet
+ :let m = ":map _f :set ai<CR>" " need 'autoindent' set
+ :let m ..= "{O<Esc>" " add empty line above item
+ :let m ..= "}{)^W" " move to text after bullet
+ :let m ..= "i <CR> <Esc>" " add space for indent
+ :let m ..= "gq}" " format text after the bullet
+ :let m ..= "{dd" " remove the empty line
+ :let m ..= "5lDJ" " put text after bullet
:execute m |" define the mapping
(<> notation |<>|. Note that this is all typed literally. ^W is "^" "W", not
@@ -429,15 +429,15 @@ A slightly more advanced version is used in the |matchparen| plugin.
let c = '\['
let c2 = '\]'
endif
- let s_skip ='synIDattr(synID(line("."), col("."), 0), "name") ' .
+ let s_skip ='synIDattr(synID(line("."), col("."), 0), "name") ' ..
\ '=~? "string\\|comment"'
execute 'if' s_skip '| let s_skip = 0 | endif'
let [m_lnum, m_col] = searchpairpos(c, '', c2, s_flags, s_skip)
if m_lnum > 0 && m_lnum >= line('w0') && m_lnum <= line('w$')
- exe 'match Search /\(\%' . c_lnum . 'l\%' . c_col .
- \ 'c\)\|\(\%' . m_lnum . 'l\%' . m_col . 'c\)/'
+ exe 'match Search /\(\%' .. c_lnum .. 'l\%' .. c_col ..
+ \ 'c\)\|\(\%' .. m_lnum .. 'l\%' .. m_col .. 'c\)/'
let s:paren_hl_on = 1
endif
endfunction
diff --git a/runtime/doc/undo.txt b/runtime/doc/undo.txt
index b11d7581ed..a853aea995 100644
--- a/runtime/doc/undo.txt
+++ b/runtime/doc/undo.txt
@@ -272,12 +272,12 @@ history file. E.g.: >
au BufReadPost * call ReadUndo()
au BufWritePost * call WriteUndo()
func ReadUndo()
- if filereadable(expand('%:h'). '/UNDO/' . expand('%:t'))
+ if filereadable(expand('%:h') .. '/UNDO/' .. expand('%:t'))
rundo %:h/UNDO/%:t
endif
endfunc
func WriteUndo()
- let dirname = expand('%:h') . '/UNDO'
+ let dirname = expand('%:h') .. '/UNDO'
if !isdirectory(dirname)
call mkdir(dirname)
endif
diff --git a/runtime/doc/usr_05.txt b/runtime/doc/usr_05.txt
index f93a221e43..b1ef563e43 100644
--- a/runtime/doc/usr_05.txt
+++ b/runtime/doc/usr_05.txt
@@ -131,7 +131,7 @@ it worked before Vim 5.0. Otherwise the "Q" command starts Ex mode, but you
will not need it.
>
- vnoremap _g y:exe "grep /" . escape(@", '\\/') . "/ *.c *.h"<CR>
+ vnoremap _g y:exe "grep /" .. escape(@", '\\/') .. "/ *.c *.h"<CR>
This mapping yanks the visually selected text and searches for it in C files.
This is a complicated mapping. You can see that mappings can be used to do
diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt
index 38869f8e94..e1b87f60ad 100644
--- a/runtime/doc/various.txt
+++ b/runtime/doc/various.txt
@@ -275,7 +275,7 @@ g8 Print the hex values of the bytes used in the
Special characters are not escaped, use quotes or
|shellescape()|: >
:!ls "%"
- :exe "!ls " . shellescape(expand("%"))
+ :exe "!ls " .. shellescape(expand("%"))
<
Newline character ends {cmd} unless a backslash
precedes the newline. What follows is interpreted as
@@ -432,7 +432,7 @@ g8 Print the hex values of the bytes used in the
used. In this example |:silent| is used to avoid the
message about reading the file and |:unsilent| to be
able to list the first line of each file. >
- :silent argdo unsilent echo expand('%') . ": " . getline(1)
+ :silent argdo unsilent echo expand('%') .. ": " .. getline(1)
<
*:verb* *:verbose*
diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt
index 5ea6a9c5dd..d7a65756bf 100644
--- a/runtime/doc/vim_diff.txt
+++ b/runtime/doc/vim_diff.txt
@@ -442,6 +442,9 @@ Working directory (Vim implemented some of these later than Nvim):
- `getcwd(-1)` is equivalent to `getcwd(-1, 0)` instead of returning the global
working directory. Use `getcwd(-1, -1)` to get the global working directory.
+Mappings:
+- |nore| is ignored for rhs <Plug> mappings. <Plug> mappings are always remapped.
+
==============================================================================
5. Missing legacy features *nvim-features-missing*
diff --git a/runtime/doc/windows.txt b/runtime/doc/windows.txt
index 5b91321c40..bd29cd1d7a 100644
--- a/runtime/doc/windows.txt
+++ b/runtime/doc/windows.txt
@@ -447,7 +447,7 @@ These commands can also be executed with ":wincmd":
the |CursorHold| autocommand event). Or when a Normal mode
command is inconvenient.
The count can also be a window number. Example: >
- :exe nr . "wincmd w"
+ :exe nr .. "wincmd w"
< This goes to window "nr".
==============================================================================
@@ -909,12 +909,12 @@ CTRL-W g } *CTRL-W_g}*
cursor. This is less clever than using |:ptag|, but you don't
need a tags file and it will also find matches in system
include files. Example: >
- :au! CursorHold *.[ch] ++nested exe "silent! psearch " . expand("<cword>")
+ :au! CursorHold *.[ch] ++nested exe "silent! psearch " .. expand("<cword>")
< Warning: This can be slow.
Example *CursorHold-example* >
- :au! CursorHold *.[ch] ++nested exe "silent! ptag " . expand("<cword>")
+ :au! CursorHold *.[ch] ++nested exe "silent! ptag " .. expand("<cword>")
This will cause a ":ptag" to be executed for the keyword under the cursor,
when the cursor hasn't moved for the time set with 'updatetime'. "++nested"
@@ -937,14 +937,14 @@ is no word under the cursor, and a few other things: >
:
: " Delete any existing highlight before showing another tag
: silent! wincmd P " jump to preview window
- : if &previewwindow " if we really get there...
+ : if &previewwindow " if we really get there...
: match none " delete existing highlight
: wincmd p " back to old window
: endif
:
: " Try displaying a matching tag for the word under the cursor
: try
- : exe "ptag " . w
+ : exe "ptag " .. w
: catch
: return
: endtry
@@ -956,10 +956,10 @@ is no word under the cursor, and a few other things: >
: endif
: call search("$", "b") " to end of previous line
: let w = substitute(w, '\\', '\\\\', "")
- : call search('\<\V' . w . '\>') " position cursor on match
+ : call search('\<\V' .. w .. '\>') " position cursor on match
: " Add a match highlight to the word at this position
: hi previewWord term=bold ctermbg=green guibg=green
- : exe 'match previewWord "\%' . line(".") . 'l\%' . col(".") . 'c\k*"'
+ : exe 'match previewWord "\%' .. line(".") .. 'l\%' .. col(".") .. 'c\k*"'
: wincmd p " back to old window
: endif
: endif
diff --git a/runtime/indent/vim.vim b/runtime/indent/vim.vim
index 7c03ff2873..f5a94940bd 100644
--- a/runtime/indent/vim.vim
+++ b/runtime/indent/vim.vim
@@ -1,7 +1,7 @@
" Vim indent file
" Language: Vim script
" Maintainer: Bram Moolenaar <Bram@vim.org>
-" Last Change: 2021 Nov 27
+" Last Change: 2022 Feb 23
" Only load this indent file when no other was loaded.
if exists("b:did_indent")
@@ -10,7 +10,7 @@ endif
let b:did_indent = 1
setlocal indentexpr=GetVimIndent()
-setlocal indentkeys+==end,=},=else,=cat,=finall,=END,0\\,0=\"\\\
+setlocal indentkeys+==endif,=enddef,=endfu,=endfor,=endwh,=endtry,=},=else,=cat,=finall,=END,0\\,0=\"\\\
setlocal indentkeys-=0#
setlocal indentkeys-=:
@@ -103,10 +103,11 @@ function GetVimIndentIntern()
" A line starting with :au does not increment/decrement indent.
" A { may start a block or a dict. Assume that when a } follows it's a
" terminated dict.
+ " ":function" starts a block but "function(" doesn't.
if prev_text !~ '^\s*au\%[tocmd]' && prev_text !~ '^\s*{.*}'
- let i = match(prev_text, '\(^\||\)\s*\(export\s\+\)\?\({\|\(if\|wh\%[ile]\|for\|try\|cat\%[ch]\|fina\|finall\%[y]\|fu\%[nction]\|def\|el\%[seif]\)\>\)')
+ let i = match(prev_text, '\(^\||\)\s*\(export\s\+\)\?\({\|\(if\|wh\%[ile]\|for\|try\|cat\%[ch]\|fina\|finall\%[y]\|def\|el\%[seif]\)\>\|fu\%[nction]\s\)')
if i >= 0
- let ind += shiftwidth()
+ let ind += shiftwidth()
if strpart(prev_text, i, 1) == '|' && has('syntax_items')
\ && synIDattr(synID(lnum, i, 1), "name") =~ '\(Comment\|String\|PatSep\)$'
let ind -= shiftwidth()
@@ -170,10 +171,15 @@ function GetVimIndentIntern()
let ind = ind + shiftwidth()
endif
- " Subtract a 'shiftwidth' on a :endif, :endwhile, :catch, :finally, :endtry,
- " :endfun, :else and :augroup END.
- if cur_text =~ '^\s*\(ene\@!\|cat\|finall\|el\|aug\%[roup]\s\+[eE][nN][dD]\)'
+ " Subtract a 'shiftwidth' on a :endif, :endwhile, :endfor, :catch, :finally,
+ " :endtry, :endfun, :enddef, :else and :augroup END.
+ " Although ":en" would be enough only match short command names as in
+ " 'indentkeys'.
+ if cur_text =~ '^\s*\(endif\|endwh\|endfor\|endtry\|endfu\|enddef\|cat\|finall\|else\|aug\%[roup]\s\+[eE][nN][dD]\)'
let ind = ind - shiftwidth()
+ if ind < 0
+ let ind = 0
+ endif
endif
return ind
diff --git a/runtime/lua/vim/_load_package.lua b/runtime/lua/vim/_load_package.lua
new file mode 100644
index 0000000000..525f603438
--- /dev/null
+++ b/runtime/lua/vim/_load_package.lua
@@ -0,0 +1,49 @@
+-- prevents luacheck from making lints for setting things on vim
+local vim = assert(vim)
+
+local pathtrails = {}
+vim._so_trails = {}
+for s in (package.cpath..';'):gmatch('[^;]*;') do
+ s = s:sub(1, -2) -- Strip trailing semicolon
+ -- Find out path patterns. pathtrail should contain something like
+ -- /?.so, \?.dll. This allows not to bother determining what correct
+ -- suffixes are.
+ local pathtrail = s:match('[/\\][^/\\]*%?.*$')
+ if pathtrail and not pathtrails[pathtrail] then
+ pathtrails[pathtrail] = true
+ table.insert(vim._so_trails, pathtrail)
+ end
+end
+
+function vim._load_package(name)
+ local basename = name:gsub('%.', '/')
+ local paths = {"lua/"..basename..".lua", "lua/"..basename.."/init.lua"}
+ local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true})
+ if #found > 0 then
+ local f, err = loadfile(found[1])
+ return f or error(err)
+ end
+
+ local so_paths = {}
+ for _,trail in ipairs(vim._so_trails) do
+ local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash
+ table.insert(so_paths, path)
+ end
+
+ found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true})
+ if #found > 0 then
+ -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is
+ -- a) strip prefix up to and including the first dash, if any
+ -- b) replace all dots by underscores
+ -- c) prepend "luaopen_"
+ -- So "foo-bar.baz" should result in "luaopen_bar_baz"
+ local dash = name:find("-", 1, true)
+ local modname = dash and name:sub(dash + 1) or name
+ local f, err = package.loadlib(found[1], "luaopen_"..modname:gsub("%.", "_"))
+ return f or error(err)
+ end
+ return nil
+end
+
+-- Insert vim._load_package after the preloader at position 2
+table.insert(package.loaders, 2, vim._load_package)
diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua
index d53b790746..df49eff4b6 100644
--- a/runtime/lua/vim/keymap.lua
+++ b/runtime/lua/vim/keymap.lua
@@ -42,7 +42,7 @@ local keymap = {}
--- |nvim_replace_termcodes()| is applied to the result of Lua expr maps.
--- - remap: (boolean) Make the mapping recursive. This is the
--- inverse of the "noremap" option from |nvim_set_keymap()|.
---- Default `true` if `lhs` is a string starting with `<plug>` (case-insensitive), `false` otherwise.
+--- Default `false`.
---@see |nvim_set_keymap()|
function keymap.set(mode, lhs, rhs, opts)
vim.validate {
@@ -66,8 +66,8 @@ function keymap.set(mode, lhs, rhs, opts)
opts.replace_keycodes = nil
if opts.remap == nil then
- -- remap by default on <plug> mappings and don't otherwise.
- opts.noremap = is_rhs_luaref or rhs:lower():match("^<plug>") == nil
+ -- default remap value is false
+ opts.noremap = true
else
-- remaps behavior is opposite of noremap option.
opts.noremap = not opts.remap
diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py
index 36768953f9..7b6d974181 100755
--- a/scripts/gen_vimdoc.py
+++ b/scripts/gen_vimdoc.py
@@ -95,6 +95,7 @@ CONFIG = {
'window.c',
'win_config.c',
'tabpage.c',
+ 'autocmd.c',
'ui.c',
],
# List of files/directories for doxygen to read, separated by blanks
diff --git a/scripts/uncrustify.sh b/scripts/uncrustify.sh
new file mode 100755
index 0000000000..ac5d542c29
--- /dev/null
+++ b/scripts/uncrustify.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -e
+
+# Check that you have uncrustify
+hash uncrustify
+
+COMMITISH="${1:-master}"
+for file in $(git diff --diff-filter=d --name-only $COMMITISH | grep '\.[ch]$'); do
+ uncrustify -c src/uncrustify.cfg -l C --replace --no-backup "$file"
+done
diff --git a/src/cjson/lua_cjson.c b/src/cjson/lua_cjson.c
index cf9e82c38e..b5f97bc485 100644
--- a/src/cjson/lua_cjson.c
+++ b/src/cjson/lua_cjson.c
@@ -776,7 +776,7 @@ static void json_append_data(lua_State *l, json_config_t *cfg,
if (has_metatable) {
- nlua_pushref(l, nlua_empty_dict_ref);
+ nlua_pushref(l, nlua_get_empty_dict_ref(l));
if (lua_rawequal(l, -2, -1)) {
as_empty_dict = true;
} else {
@@ -822,7 +822,7 @@ static void json_append_data(lua_State *l, json_config_t *cfg,
}
break;
case LUA_TUSERDATA:
- nlua_pushref(l, nlua_nil_ref);
+ nlua_pushref(l, nlua_get_nil_ref(l));
bool is_nil = lua_rawequal(l, -2, -1);
lua_pop(l, 1);
if (is_nil) {
@@ -1285,7 +1285,7 @@ static void json_parse_object_context(lua_State *l, json_parse_t *json)
/* Handle empty objects */
if (token.type == T_OBJ_END) {
- nlua_pushref(l, nlua_empty_dict_ref); \
+ nlua_pushref(l, nlua_get_empty_dict_ref(l)); \
lua_setmetatable(l, -2); \
json_decode_ascend(json);
return;
@@ -1392,7 +1392,7 @@ static void json_process_value(lua_State *l, json_parse_t *json,
if (use_luanil) {
lua_pushnil(l);
} else {
- nlua_pushref(l, nlua_nil_ref);
+ nlua_pushref(l, nlua_get_nil_ref(l));
}
break;;
default:
@@ -1549,7 +1549,15 @@ int lua_cjson_new(lua_State *l)
};
/* Initialise number conversions */
- fpconv_init();
+ lua_getfield(l, LUA_REGISTRYINDEX, "nvim.thread");
+ bool is_thread = lua_toboolean(l, -1);
+ lua_pop(l, 1);
+
+ // Since fpconv_init does not need to be called multiple times and is not
+ // thread safe, it should only be called in the main thread.
+ if (!is_thread) {
+ fpconv_init();
+ }
/* Test if array metatables are in registry */
lua_pushlightuserdata(l, json_lightudata_mask(&json_empty_array));
@@ -1582,7 +1590,7 @@ int lua_cjson_new(lua_State *l)
compat_luaL_setfuncs(l, reg, 1);
/* Set cjson.null */
- nlua_pushref(l, nlua_nil_ref);
+ nlua_pushref(l, nlua_get_nil_ref(l));
lua_setfield(l, -2, "null");
/* Set cjson.empty_array_mt */
diff --git a/src/mpack/lmpack.c b/src/mpack/lmpack.c
index 126f2f3824..53d7092a0c 100644
--- a/src/mpack/lmpack.c
+++ b/src/mpack/lmpack.c
@@ -246,7 +246,7 @@ static mpack_uint32_t lmpack_objlen(lua_State *L, int *is_array)
}
end:
- if ((size_t)-1 > (mpack_uint32_t)-1 && len > (mpack_uint32_t)-1)
+ if ((size_t)-1 > (mpack_uint32_t)-1 && len > (mpack_uint32_t)-1) // -V560
/* msgpack spec doesn't allow lengths > 32 bits */
len = (mpack_uint32_t)-1;
assert(top == lua_gettop(L));
diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt
index dcc20194f0..3ba3923a82 100644
--- a/src/nvim/CMakeLists.txt
+++ b/src/nvim/CMakeLists.txt
@@ -63,6 +63,7 @@ set(LUA_INSPECT_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/inspect.lua)
set(LUA_F_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/F.lua)
set(LUA_META_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/_meta.lua)
set(LUA_FILETYPE_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/filetype.lua)
+set(LUA_LOAD_PACKAGE_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/_load_package.lua)
set(CHAR_BLOB_GENERATOR ${GENERATOR_DIR}/gen_char_blob.lua)
set(LINT_SUPPRESS_FILE ${PROJECT_BINARY_DIR}/errors.json)
set(LINT_SUPPRESS_URL_BASE "https://raw.githubusercontent.com/neovim/doc/gh-pages/reports/clint")
@@ -336,6 +337,7 @@ add_custom_command(
${LUA_F_MODULE_SOURCE} lua_F_module
${LUA_META_MODULE_SOURCE} lua_meta_module
${LUA_FILETYPE_MODULE_SOURCE} lua_filetype_module
+ ${LUA_LOAD_PACKAGE_MODULE_SOURCE} lua_load_package_module
DEPENDS
${CHAR_BLOB_GENERATOR}
${LUA_VIM_MODULE_SOURCE}
@@ -344,6 +346,7 @@ add_custom_command(
${LUA_F_MODULE_SOURCE}
${LUA_META_MODULE_SOURCE}
${LUA_FILETYPE_MODULE_SOURCE}
+ ${LUA_LOAD_PACKAGE_MODULE_SOURCE}
VERBATIM
)
diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c
new file mode 100644
index 0000000000..deb8ec8cf3
--- /dev/null
+++ b/src/nvim/api/autocmd.c
@@ -0,0 +1,669 @@
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "lauxlib.h"
+#include "nvim/api/autocmd.h"
+#include "nvim/api/private/defs.h"
+#include "nvim/api/private/helpers.h"
+#include "nvim/ascii.h"
+#include "nvim/buffer.h"
+#include "nvim/eval/typval.h"
+#include "nvim/fileio.h"
+#include "nvim/lua/executor.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "api/autocmd.c.generated.h"
+#endif
+
+#define AUCMD_MAX_PATTERNS 256
+
+// Check whether every item in the array is a kObjectTypeString
+#define CHECK_STRING_ARRAY(__array, k, v, goto_name) \
+ for (size_t j = 0; j < __array.size; j++) { \
+ Object item = __array.items[j]; \
+ if (item.type != kObjectTypeString) { \
+ api_set_error(err, \
+ kErrorTypeValidation, \
+ "All entries in '%s' must be strings", \
+ k); \
+ goto goto_name; \
+ } \
+ }
+
+// Copy string or array of strings into an empty array.
+#define UNPACK_STRING_OR_ARRAY(__array, k, v, goto_name) \
+ if (v->type == kObjectTypeString) { \
+ ADD(__array, copy_object(*v)); \
+ } else if (v->type == kObjectTypeArray) { \
+ CHECK_STRING_ARRAY(__array, k, v, goto_name); \
+ __array = copy_array(v->data.array); \
+ } else { \
+ api_set_error(err, \
+ kErrorTypeValidation, \
+ "'%s' must be an array or a string.", \
+ k); \
+ goto goto_name; \
+ }
+
+// Get the event number, unless it is an error. Then goto `goto_name`.
+#define GET_ONE_EVENT(event_nr, event_str, goto_name) \
+ char_u *__next_ev; \
+ event_T event_nr = \
+ event_name2nr((char_u *)event_str.data.string.data, &__next_ev); \
+ if (event_nr >= NUM_EVENTS) { \
+ api_set_error(err, kErrorTypeValidation, "unexpected event"); \
+ goto goto_name; \
+ }
+
+
+// ID for associating autocmds created via nvim_create_autocmd
+// Used to delete autocmds from nvim_del_autocmd
+static int64_t next_autocmd_id = 1;
+
+/// Get autocmds that match the requirements passed to {opts}.
+/// group
+/// event
+/// pattern
+///
+/// -- @param {string} event - event or events to match against
+/// vim.api.nvim_get_autocmds({ event = "FileType" })
+///
+Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err)
+ FUNC_API_SINCE(9)
+{
+ Array autocmd_list = ARRAY_DICT_INIT;
+ char_u *pattern_filters[AUCMD_MAX_PATTERNS];
+ char_u pattern_buflocal[BUFLOCAL_PAT_LEN];
+
+ bool event_set[NUM_EVENTS] = { false };
+ bool check_event = false;
+
+ int group = 0;
+
+ if (opts->group.type != kObjectTypeNil) {
+ Object v = opts->group;
+ if (v.type != kObjectTypeString) {
+ api_set_error(err, kErrorTypeValidation, "group must be a string.");
+ goto cleanup;
+ }
+
+ group = augroup_find(v.data.string.data);
+
+ if (group < 0) {
+ api_set_error(err, kErrorTypeValidation, "invalid augroup passed.");
+ goto cleanup;
+ }
+ }
+
+ if (opts->event.type != kObjectTypeNil) {
+ check_event = true;
+
+ Object v = opts->event;
+ if (v.type == kObjectTypeString) {
+ GET_ONE_EVENT(event_nr, v, cleanup);
+ event_set[event_nr] = true;
+ } else if (v.type == kObjectTypeArray) {
+ FOREACH_ITEM(v.data.array, event_v, {
+ if (event_v.type != kObjectTypeString) {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "Every event must be a string in 'event'");
+ goto cleanup;
+ }
+
+ GET_ONE_EVENT(event_nr, event_v, cleanup);
+ event_set[event_nr] = true;
+ })
+ } else {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "Not a valid 'event' value. Must be a string or an array");
+ goto cleanup;
+ }
+ }
+
+ int pattern_filter_count = 0;
+ if (opts->pattern.type != kObjectTypeNil) {
+ Object v = opts->pattern;
+ if (v.type == kObjectTypeString) {
+ pattern_filters[pattern_filter_count] = (char_u *)v.data.string.data;
+ pattern_filter_count += 1;
+ } else if (v.type == kObjectTypeArray) {
+ FOREACH_ITEM(v.data.array, item, {
+ pattern_filters[pattern_filter_count] = (char_u *)item.data.string.data;
+ pattern_filter_count += 1;
+ });
+ } else {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "Not a valid 'pattern' value. Must be a string or an array");
+ goto cleanup;
+ }
+
+ if (pattern_filter_count >= AUCMD_MAX_PATTERNS) {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "Too many patterns. Please limit yourself to less");
+ goto cleanup;
+ }
+ }
+
+ FOR_ALL_AUEVENTS(event) {
+ if (check_event && !event_set[event]) {
+ continue;
+ }
+
+ for (AutoPat *ap = au_get_autopat_for_event(event);
+ ap != NULL;
+ ap = ap->next) {
+ if (ap == NULL || ap->cmds == NULL) {
+ continue;
+ }
+
+ // Skip autocmds from invalid groups if passed.
+ if (group != 0 && ap->group != group) {
+ continue;
+ }
+
+ // Skip 'pattern' from invalid patterns if passed.
+ if (pattern_filter_count > 0) {
+ bool passed = false;
+ for (int i = 0; i < pattern_filter_count; i++) {
+ assert(i < AUCMD_MAX_PATTERNS);
+ assert(pattern_filters[i]);
+
+ char_u *pat = pattern_filters[i];
+ int patlen = (int)STRLEN(pat);
+
+ if (aupat_is_buflocal(pat, patlen)) {
+ aupat_normalize_buflocal_pat(pattern_buflocal,
+ pat,
+ patlen,
+ aupat_get_buflocal_nr(pat, patlen));
+
+ pat = pattern_buflocal;
+ }
+
+ if (strequal((char *)ap->pat, (char *)pat)) {
+ passed = true;
+ break;
+ }
+ }
+
+ if (!passed) {
+ continue;
+ }
+ }
+
+ for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) {
+ if (aucmd_exec_is_deleted(ac->exec)) {
+ continue;
+ }
+ Dictionary autocmd_info = ARRAY_DICT_INIT;
+
+ if (ap->group != AUGROUP_DEFAULT) {
+ PUT(autocmd_info, "group", INTEGER_OBJ(ap->group));
+ }
+
+ if (ac->id > 0) {
+ PUT(autocmd_info, "id", INTEGER_OBJ(ac->id));
+ }
+
+ if (ac->desc != NULL) {
+ PUT(autocmd_info, "desc", CSTR_TO_OBJ(ac->desc));
+ }
+
+ PUT(autocmd_info,
+ "command",
+ STRING_OBJ(cstr_to_string(aucmd_exec_to_string(ac, ac->exec))));
+
+ PUT(autocmd_info,
+ "pattern",
+ STRING_OBJ(cstr_to_string((char *)ap->pat)));
+
+ PUT(autocmd_info, "once", BOOLEAN_OBJ(ac->once));
+
+ if (ap->buflocal_nr) {
+ PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(true));
+ PUT(autocmd_info, "buffer", INTEGER_OBJ(ap->buflocal_nr));
+ } else {
+ PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(false));
+ }
+
+ // TODO(sctx): It would be good to unify script_ctx to actually work with lua
+ // right now it's just super weird, and never really gives you the info that
+ // you would expect from this.
+ //
+ // I think we should be able to get the line number, filename, etc. from lua
+ // when we're executing something, and it should be easy to then save that
+ // info here.
+ //
+ // I think it's a big loss not getting line numbers of where options, autocmds,
+ // etc. are set (just getting "Sourced (lua)" or something is not that helpful.
+ //
+ // Once we do that, we can put these into the autocmd_info, but I don't think it's
+ // useful to do that at this time.
+ //
+ // PUT(autocmd_info, "sid", INTEGER_OBJ(ac->script_ctx.sc_sid));
+ // PUT(autocmd_info, "lnum", INTEGER_OBJ(ac->script_ctx.sc_lnum));
+
+ ADD(autocmd_list, DICTIONARY_OBJ(autocmd_info));
+ }
+ }
+ }
+
+cleanup:
+ return autocmd_list;
+}
+
+/// Define an autocmd.
+/// @param opts Dictionary
+/// Required keys:
+/// event: string | ArrayOf(string)
+/// event = "pat1,pat2,pat3",
+/// event = "pat1"
+/// event = {"pat1"}
+/// event = {"pat1", "pat2", "pat3"}
+///
+///
+/// -- @param {string} name - augroup name
+/// -- @param {string | table} event - event or events to match against
+/// -- @param {string | table} pattern - pattern or patterns to match against
+/// -- @param {string | function} callback - function or string to execute on autocmd
+/// -- @param {string} command - optional, vimscript command
+/// Eg. command = "let g:value_set = v:true"
+/// -- @param {boolean} once - optional, defaults to false
+///
+/// -- pattern = comma delimited list of patterns | pattern | { pattern, ... }
+///
+/// pattern = "*.py,*.pyi"
+/// pattern = "*.py"
+/// pattern = {"*.py"}
+/// pattern = { "*.py", "*.pyi" }
+///
+/// -- not supported
+/// pattern = {"*.py,*.pyi"}
+///
+/// -- event = string | string[]
+/// event = "FileType,CursorHold"
+/// event = "BufPreWrite"
+/// event = {"BufPostWrite"}
+/// event = {"CursorHold", "BufPreWrite", "BufPostWrite"}
+Integer nvim_create_autocmd(uint64_t channel_id, Dict(create_autocmd) *opts, Error *err)
+ FUNC_API_SINCE(9)
+{
+ int64_t autocmd_id = -1;
+
+ const char_u pattern_buflocal[BUFLOCAL_PAT_LEN];
+ int au_group = AUGROUP_DEFAULT;
+ char *desc = NULL;
+
+ Array patterns = ARRAY_DICT_INIT;
+ Array event_array = ARRAY_DICT_INIT;
+
+ AucmdExecutable aucmd = AUCMD_EXECUTABLE_INIT;
+ Callback cb = CALLBACK_NONE;
+
+ if (opts->callback.type != kObjectTypeNil && opts->command.type != kObjectTypeNil) {
+ api_set_error(err, kErrorTypeValidation,
+ "cannot pass both: 'callback' and 'command' for the same autocmd");
+ goto cleanup;
+ } else if (opts->callback.type != kObjectTypeNil) {
+ // TODO(tjdevries): It's possible we could accept callable tables,
+ // but we don't do that many other places, so for the moment let's
+ // not do that.
+
+ Object *callback = &opts->callback;
+ if (callback->type == kObjectTypeLuaRef) {
+ if (callback->data.luaref == LUA_NOREF) {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "must pass an actual value");
+ goto cleanup;
+ }
+
+ if (!nlua_ref_is_function(callback->data.luaref)) {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "must pass a function for callback");
+ goto cleanup;
+ }
+
+ cb.type = kCallbackLua;
+ cb.data.luaref = api_new_luaref(callback->data.luaref);
+ } else if (callback->type == kObjectTypeString) {
+ cb.type = kCallbackFuncref;
+ cb.data.funcref = vim_strsave((char_u *)callback->data.string.data);
+ } else {
+ api_set_error(err,
+ kErrorTypeException,
+ "'callback' must be a lua function or name of vim function");
+ goto cleanup;
+ }
+
+ aucmd.type = CALLABLE_CB;
+ aucmd.callable.cb = cb;
+ } else if (opts->command.type != kObjectTypeNil) {
+ Object *command = &opts->command;
+ if (command->type == kObjectTypeString) {
+ aucmd.type = CALLABLE_EX;
+ aucmd.callable.cmd = vim_strsave((char_u *)command->data.string.data);
+ } else {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "'command' must be a string");
+ goto cleanup;
+ }
+ } else {
+ api_set_error(err, kErrorTypeValidation, "must pass one of: 'command', 'callback'");
+ goto cleanup;
+ }
+
+ if (opts->event.type != kObjectTypeNil) {
+ UNPACK_STRING_OR_ARRAY(event_array, "event", (&opts->event), cleanup)
+ }
+
+ bool is_once = api_object_to_bool(opts->once, "once", false, err);
+ bool is_nested = api_object_to_bool(opts->nested, "nested", false, err);
+
+ // TOOD: accept number for namespace instead
+ if (opts->group.type != kObjectTypeNil) {
+ Object *v = &opts->group;
+ if (v->type != kObjectTypeString) {
+ api_set_error(err, kErrorTypeValidation, "'group' must be a string");
+ goto cleanup;
+ }
+
+ au_group = augroup_find(v->data.string.data);
+
+ if (au_group == AUGROUP_ERROR) {
+ api_set_error(err,
+ kErrorTypeException,
+ "invalid augroup: %s", v->data.string.data);
+
+ goto cleanup;
+ }
+ }
+
+ if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) {
+ api_set_error(err, kErrorTypeValidation,
+ "cannot pass both: 'pattern' and 'buffer' for the same autocmd");
+ goto cleanup;
+ } else if (opts->pattern.type != kObjectTypeNil) {
+ Object *v = &opts->pattern;
+
+ if (v->type == kObjectTypeString) {
+ char_u *pat = (char_u *)v->data.string.data;
+ size_t patlen = aucmd_pattern_length(pat);
+ while (patlen) {
+ ADD(patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen)));
+
+ pat = aucmd_next_pattern(pat, patlen);
+ patlen = aucmd_pattern_length(pat);
+ }
+ } else if (v->type == kObjectTypeArray) {
+ CHECK_STRING_ARRAY(patterns, "pattern", v, cleanup);
+
+ Array array = v->data.array;
+ for (size_t i = 0; i < array.size; i++) {
+ char_u *pat = (char_u *)array.items[i].data.string.data;
+ size_t patlen = aucmd_pattern_length(pat);
+ while (patlen) {
+ ADD(patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen)));
+
+ pat = aucmd_next_pattern(pat, patlen);
+ patlen = aucmd_pattern_length(pat);
+ }
+ }
+ } else {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "'pattern' must be a string");
+ goto cleanup;
+ }
+ } else if (opts->buffer.type != kObjectTypeNil) {
+ if (opts->buffer.type != kObjectTypeInteger) {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "'buffer' must be an integer");
+ goto cleanup;
+ }
+
+ buf_T *buf = find_buffer_by_handle((Buffer)opts->buffer.data.integer, err);
+ if (ERROR_SET(err)) {
+ goto cleanup;
+ }
+
+ snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle);
+ ADD(patterns, STRING_OBJ(cstr_to_string((char *)pattern_buflocal)));
+ }
+
+ if (aucmd.type == CALLABLE_NONE) {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "'command' or 'callback' is required");
+ goto cleanup;
+ }
+
+ if (opts->desc.type != kObjectTypeNil) {
+ if (opts->desc.type == kObjectTypeString) {
+ desc = opts->desc.data.string.data;
+ } else {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "'desc' must be a string");
+ goto cleanup;
+ }
+ }
+
+ if (patterns.size == 0) {
+ ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING("*")));
+ }
+
+ if (event_array.size == 0) {
+ api_set_error(err, kErrorTypeValidation, "'event' is a required key");
+ goto cleanup;
+ }
+
+ autocmd_id = next_autocmd_id++;
+ FOREACH_ITEM(event_array, event_str, {
+ GET_ONE_EVENT(event_nr, event_str, cleanup);
+
+ int retval;
+
+ for (size_t i = 0; i < patterns.size; i++) {
+ Object pat = patterns.items[i];
+
+ // See: TODO(sctx)
+ WITH_SCRIPT_CONTEXT(channel_id, {
+ retval = autocmd_register(autocmd_id,
+ event_nr,
+ (char_u *)pat.data.string.data,
+ (int)pat.data.string.size,
+ au_group,
+ is_once,
+ is_nested,
+ desc,
+ aucmd);
+ });
+
+ if (retval == FAIL) {
+ api_set_error(err, kErrorTypeException, "Failed to set autocmd");
+ goto cleanup;
+ }
+ }
+ });
+
+
+cleanup:
+ aucmd_exec_free(&aucmd);
+ api_free_array(event_array);
+ api_free_array(patterns);
+
+ return autocmd_id;
+}
+
+/// Delete an autocmd by ID. Autocmds only return IDs when created
+/// via the API.
+///
+/// @param id Integer The ID returned by nvim_create_autocmd
+void nvim_del_autocmd(Integer id)
+ FUNC_API_SINCE(9)
+{
+ autocmd_delete_id(id);
+}
+
+/// Create or get an augroup.
+///
+/// To get an existing augroup ID, do:
+/// <pre>
+/// local id = vim.api.nvim_create_augroup({ name = name, clear = false });
+/// </pre>
+///
+/// @param opts Parameters
+/// - name (string): The name of the augroup
+/// - clear (bool): Whether to clear existing commands or not.
+// Defaults to true.
+/// See |autocmd-groups|
+Integer nvim_create_augroup(uint64_t channel_id, Dict(create_augroup) *opts, Error *err)
+ FUNC_API_SINCE(9)
+{
+ bool clear_autocmds = api_object_to_bool(opts->clear, "clear", true, err);
+
+ if (opts->name.type != kObjectTypeString) {
+ api_set_error(err, kErrorTypeValidation, "'name' is required and must be a string");
+ return -1;
+ }
+ char *name = opts->name.data.string.data;
+
+ int augroup = -1;
+ WITH_SCRIPT_CONTEXT(channel_id, {
+ augroup = augroup_add(name);
+ if (augroup == AUGROUP_ERROR) {
+ api_set_error(err, kErrorTypeException, "Failed to set augroup");
+ return -1;
+ }
+
+ if (clear_autocmds) {
+ FOR_ALL_AUEVENTS(event) {
+ aupat_del_for_event_and_group(event, augroup);
+ }
+ }
+ });
+
+ return augroup;
+}
+
+/// NOTE: behavior differs from augroup-delete.
+/// When deleting an augroup, autocmds contained by this augroup will also be deleted and cleared.
+/// This augroup will no longer exist
+void nvim_del_augroup_by_id(Integer id)
+ FUNC_API_SINCE(9)
+{
+ char *name = augroup_name((int)id);
+ augroup_del(name, false);
+}
+
+/// NOTE: behavior differs from augroup-delete.
+/// When deleting an augroup, autocmds contained by this augroup will also be deleted and cleared.
+/// This augroup will no longer exist
+void nvim_del_augroup_by_name(String name)
+ FUNC_API_SINCE(9)
+{
+ augroup_del(name.data, false);
+}
+
+/// -- @param {string} group - autocmd group name
+/// -- @param {number} buffer - buffer number
+/// -- @param {string | table} event - event or events to match against
+/// -- @param {string | table} pattern - optional, defaults to "*".
+/// vim.api.nvim_do_autcmd({ group, buffer, pattern, event, modeline })
+void nvim_do_autocmd(Dict(do_autocmd) *opts, Error *err)
+ FUNC_API_SINCE(9)
+{
+ int au_group = AUGROUP_ALL;
+ bool modeline = true;
+
+ buf_T *buf = curbuf;
+ bool set_buf = false;
+
+ char_u *pattern = NULL;
+ bool set_pattern = false;
+
+ Array event_array = ARRAY_DICT_INIT;
+
+ if (opts->group.type != kObjectTypeNil) {
+ if (opts->group.type != kObjectTypeString) {
+ api_set_error(err, kErrorTypeValidation, "'group' must be a string");
+ goto cleanup;
+ }
+
+ au_group = augroup_find(opts->group.data.string.data);
+
+ if (au_group == AUGROUP_ERROR) {
+ api_set_error(err,
+ kErrorTypeException,
+ "invalid augroup: %s", opts->group.data.string.data);
+
+ goto cleanup;
+ }
+ }
+
+ if (opts->buffer.type != kObjectTypeNil) {
+ Object buf_obj = opts->buffer;
+ if (buf_obj.type != kObjectTypeInteger && buf_obj.type != kObjectTypeBuffer) {
+ api_set_error(err, kErrorTypeException, "invalid buffer: %d", buf_obj.type);
+ goto cleanup;
+ }
+
+ buf = find_buffer_by_handle((Buffer)buf_obj.data.integer, err);
+ set_buf = true;
+
+ if (ERROR_SET(err)) {
+ goto cleanup;
+ }
+ }
+
+ if (opts->pattern.type != kObjectTypeNil) {
+ if (opts->pattern.type != kObjectTypeString) {
+ api_set_error(err, kErrorTypeValidation, "'pattern' must be a string");
+ goto cleanup;
+ }
+
+ pattern = vim_strsave((char_u *)opts->pattern.data.string.data);
+ set_pattern = true;
+ }
+
+ if (opts->event.type != kObjectTypeNil) {
+ UNPACK_STRING_OR_ARRAY(event_array, "event", (&opts->event), cleanup)
+ }
+
+ if (opts->modeline.type != kObjectTypeNil) {
+ modeline = api_object_to_bool(opts->modeline, "modeline", true, err);
+ }
+
+ if (set_pattern && set_buf) {
+ api_set_error(err, kErrorTypeValidation, "must not set 'buffer' and 'pattern'");
+ goto cleanup;
+ }
+
+ bool did_aucmd = false;
+ FOREACH_ITEM(event_array, event_str, {
+ GET_ONE_EVENT(event_nr, event_str, cleanup)
+
+ did_aucmd |= apply_autocmds_group(event_nr, pattern, NULL, true, au_group, buf, NULL);
+ })
+
+ if (did_aucmd && modeline) {
+ do_modelines(0);
+ }
+
+cleanup:
+ api_free_array(event_array);
+ XFREE_CLEAR(pattern);
+}
+
+
+#undef UNPACK_STRING_OR_ARRAY
+#undef CHECK_STRING_ARRAY
+#undef GET_ONE_EVENT
diff --git a/src/nvim/api/autocmd.h b/src/nvim/api/autocmd.h
new file mode 100644
index 0000000000..f9432830d9
--- /dev/null
+++ b/src/nvim/api/autocmd.h
@@ -0,0 +1,11 @@
+#ifndef NVIM_API_AUTOCMD_H
+#define NVIM_API_AUTOCMD_H
+
+#include <stdint.h>
+
+#include "nvim/api/private/defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "api/autocmd.h.generated.h"
+#endif
+#endif // NVIM_API_AUTOCMD_H
diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua
index 45a57b9257..be71c446b1 100644
--- a/src/nvim/api/keysets.lua
+++ b/src/nvim/api/keysets.lua
@@ -110,5 +110,34 @@ return {
"reverse";
"nocombine";
};
+ -- Autocmds
+ create_autocmd = {
+ "buffer";
+ "callback";
+ "command";
+ "desc";
+ "event";
+ "group";
+ "once";
+ "nested";
+ "pattern";
+ };
+ do_autocmd = {
+ "buffer";
+ "event";
+ "group";
+ "modeline";
+ "pattern";
+ };
+ get_autocmds = {
+ "event";
+ "group";
+ "id";
+ "pattern";
+ };
+ create_augroup = {
+ "clear";
+ "name";
+ };
}
diff --git a/src/nvim/api/private/dispatch.c b/src/nvim/api/private/dispatch.c
index 8ab7743e01..f670f06357 100644
--- a/src/nvim/api/private/dispatch.c
+++ b/src/nvim/api/private/dispatch.c
@@ -6,22 +6,30 @@
#include <msgpack.h>
#include <stdbool.h>
-#include "nvim/api/buffer.h"
#include "nvim/api/deprecated.h"
-#include "nvim/api/extmark.h"
#include "nvim/api/private/defs.h"
#include "nvim/api/private/dispatch.h"
#include "nvim/api/private/helpers.h"
+#include "nvim/log.h"
+#include "nvim/map.h"
+#include "nvim/msgpack_rpc/helpers.h"
+#include "nvim/vim.h"
+
+// ===========================================================================
+// NEW API FILES MUST GO HERE.
+//
+// When creating a new API file, you must include it here,
+// so that the dispatcher can find the C functions that you are creating!
+// ===========================================================================
+#include "nvim/api/autocmd.h"
+#include "nvim/api/buffer.h"
+#include "nvim/api/extmark.h"
#include "nvim/api/tabpage.h"
#include "nvim/api/ui.h"
#include "nvim/api/vim.h"
#include "nvim/api/vimscript.h"
#include "nvim/api/win_config.h"
#include "nvim/api/window.h"
-#include "nvim/log.h"
-#include "nvim/map.h"
-#include "nvim/msgpack_rpc/helpers.h"
-#include "nvim/vim.h"
static Map(String, MsgpackRpcRequestHandler) methods = MAP_INIT;
diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c
index 971fa1cb0f..3d4a04f096 100644
--- a/src/nvim/api/private/helpers.c
+++ b/src/nvim/api/private/helpers.c
@@ -396,19 +396,14 @@ void set_option_to(uint64_t channel_id, void *to, int type, String name, Object
stringval = value.data.string.data;
}
- const sctx_T save_current_sctx = current_sctx;
- current_sctx.sc_sid =
- channel_id == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT;
- current_sctx.sc_lnum = 0;
- current_channel_id = channel_id;
-
- const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL))
- ? 0 : (type == SREQ_GLOBAL)
- ? OPT_GLOBAL : OPT_LOCAL;
- set_option_value_for(name.data, numval, stringval,
- opt_flags, type, to, err);
-
- current_sctx = save_current_sctx;
+ WITH_SCRIPT_CONTEXT(channel_id, {
+ const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL))
+ ? 0 : (type == SREQ_GLOBAL)
+ ? OPT_GLOBAL : OPT_LOCAL;
+
+ set_option_value_for(name.data, numval, stringval,
+ opt_flags, type, to, err);
+ });
}
buf_T *find_buffer_by_handle(Buffer buffer, Error *err)
@@ -1614,3 +1609,16 @@ err:
NLUA_CLEAR_REF(luaref);
NLUA_CLEAR_REF(compl_luaref);
}
+
+int find_sid(uint64_t channel_id)
+{
+ switch (channel_id) {
+ case VIML_INTERNAL_CALL:
+ // TODO(autocmd): Figure out what this should be
+ // return SID_API_CLIENT;
+ case LUA_INTERNAL_CALL:
+ return SID_LUA;
+ default:
+ return SID_API_CLIENT;
+ }
+}
diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h
index 6d0aec9c90..6969994c3b 100644
--- a/src/nvim/api/private/helpers.h
+++ b/src/nvim/api/private/helpers.h
@@ -138,10 +138,27 @@ typedef struct {
msg_list = saved_msg_list; /* Restore the exception context. */ \
} while (0)
+// Useful macro for executing some `code` for each item in an array.
+#define FOREACH_ITEM(a, __foreach_item, code) \
+ for (size_t __foreach_i = 0; __foreach_i < (a).size; __foreach_i++) { \
+ Object __foreach_item = (a).items[__foreach_i]; \
+ code; \
+ }
+
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "api/private/helpers.h.generated.h"
# include "keysets.h.generated.h"
#endif
+#define WITH_SCRIPT_CONTEXT(channel_id, code) \
+ const sctx_T save_current_sctx = current_sctx; \
+ current_sctx.sc_sid = \
+ (channel_id) == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; \
+ current_sctx.sc_lnum = 0; \
+ current_channel_id = channel_id; \
+ code; \
+ current_sctx = save_current_sctx;
+
#endif // NVIM_API_PRIVATE_HELPERS_H
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c
index 4dc599564f..302dccbde7 100644
--- a/src/nvim/api/vim.c
+++ b/src/nvim/api/vim.c
@@ -1955,7 +1955,7 @@ Dictionary nvim__stats(void)
Dictionary rv = ARRAY_DICT_INIT;
PUT(rv, "fsync", INTEGER_OBJ(g_stats.fsync));
PUT(rv, "redraw", INTEGER_OBJ(g_stats.redraw));
- PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_refcount));
+ PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_get_global_ref_count()));
return rv;
}
@@ -2415,6 +2415,8 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error *
/// from Lua, the command can also be a Lua function. The function is called with a
/// single table argument that contains the following keys:
/// - args: (string) The args passed to the command, if any |<args>|
+/// - fargs: (table) The args split by unescaped whitespace (when more than one
+/// argument is allowed), if any |<f-args>|
/// - bang: (boolean) "true" if the command was executed with a ! modifier |<bang>|
/// - line1: (number) The starting line of the command range |<line1>|
/// - line2: (number) The final line of the command range |<line2>|
diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c
index fc7823a070..9c473ff724 100644
--- a/src/nvim/api/window.c
+++ b/src/nvim/api/window.c
@@ -71,6 +71,7 @@ ArrayOf(Integer, 2) nvim_win_get_cursor(Window window, Error *err)
}
/// Sets the (1,0)-indexed cursor position in the window. |api-indexing|
+/// Unlike |win_execute()| this scrolls the window.
///
/// @param window Window handle, or 0 for current window
/// @param pos (row, col) tuple representing the new position
@@ -118,6 +119,8 @@ void nvim_win_set_cursor(Window window, ArrayOf(Integer, 2) pos, Error *err)
update_topline_win(win);
redraw_later(win, VALID);
+ redraw_for_cursorline(win);
+ win->w_redr_status = true;
}
/// Gets the window height
diff --git a/src/nvim/aucmd.c b/src/nvim/aucmd.c
deleted file mode 100644
index d7f73fa4a1..0000000000
--- a/src/nvim/aucmd.c
+++ /dev/null
@@ -1,123 +0,0 @@
-// This is an open source non-commercial project. Dear PVS-Studio, please check
-// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
-
-#include "nvim/aucmd.h"
-#include "nvim/buffer.h"
-#include "nvim/eval.h"
-#include "nvim/ex_docmd.h"
-#include "nvim/ex_getln.h"
-#include "nvim/fileio.h"
-#include "nvim/main.h"
-#include "nvim/os/os.h"
-#include "nvim/ui.h"
-#include "nvim/vim.h"
-
-#ifdef INCLUDE_GENERATED_DECLARATIONS
-# include "aucmd.c.generated.h"
-#endif
-
-void do_autocmd_uienter(uint64_t chanid, bool attached)
-{
- static bool recursive = false;
-
- if (recursive) {
- return; // disallow recursion
- }
- recursive = true;
-
- save_v_event_T save_v_event;
- dict_T *dict = get_v_event(&save_v_event);
- assert(chanid < VARNUMBER_MAX);
- tv_dict_add_nr(dict, S_LEN("chan"), (varnumber_T)chanid);
- tv_dict_set_keys_readonly(dict);
- apply_autocmds(attached ? EVENT_UIENTER : EVENT_UILEAVE,
- NULL, NULL, false, curbuf);
- restore_v_event(dict, &save_v_event);
-
- recursive = false;
-}
-
-void init_default_autocmds(void)
-{
- // open terminals when opening files that start with term://
-#define PROTO "term://"
- do_cmdline_cmd("augroup nvim_terminal");
- do_cmdline_cmd("autocmd BufReadCmd " PROTO "* ++nested "
- "if !exists('b:term_title')|call termopen("
- // Capture the command string
- "matchstr(expand(\"<amatch>\"), "
- "'\\c\\m" PROTO "\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), "
- // capture the working directory
- "{'cwd': expand(get(matchlist(expand(\"<amatch>\"), "
- "'\\c\\m" PROTO "\\(.\\{-}\\)//'), 1, ''))})"
- "|endif");
- do_cmdline_cmd("augroup END");
-#undef PROTO
-
- // limit syntax synchronization in the command window
- do_cmdline_cmd("augroup nvim_cmdwin");
- do_cmdline_cmd("autocmd! CmdwinEnter [:>] syntax sync minlines=1 maxlines=1");
- do_cmdline_cmd("augroup END");
-}
-
-static void focusgained_event(void **argv)
-{
- bool *gainedp = argv[0];
- do_autocmd_focusgained(*gainedp);
- xfree(gainedp);
-}
-void aucmd_schedule_focusgained(bool gained)
-{
- bool *gainedp = xmalloc(sizeof(*gainedp));
- *gainedp = gained;
- loop_schedule_deferred(&main_loop,
- event_create(focusgained_event, 1, gainedp));
-}
-
-static void do_autocmd_focusgained(bool gained)
-{
- static bool recursive = false;
- static Timestamp last_time = (time_t)0;
- bool need_redraw = false;
-
- if (recursive) {
- return; // disallow recursion
- }
- recursive = true;
- need_redraw |= apply_autocmds((gained ? EVENT_FOCUSGAINED : EVENT_FOCUSLOST),
- NULL, NULL, false, curbuf);
-
- // When activated: Check if any file was modified outside of Vim.
- // Only do this when not done within the last two seconds as:
- // 1. Some filesystems have modification time granularity in seconds. Fat32
- // has a granularity of 2 seconds.
- // 2. We could get multiple notifications in a row.
- if (gained && last_time + (Timestamp)2000 < os_now()) {
- need_redraw = check_timestamps(true);
- last_time = os_now();
- }
-
- if (need_redraw) {
- // Something was executed, make sure the cursor is put back where it
- // belongs.
- need_wait_return = false;
-
- if (State & CMDLINE) {
- redrawcmdline();
- } else if ((State & NORMAL) || (State & INSERT)) {
- if (must_redraw != 0) {
- update_screen(0);
- }
-
- setcursor();
- }
-
- ui_flush();
- }
-
- if (need_maketitle) {
- maketitle();
- }
-
- recursive = false;
-}
diff --git a/src/nvim/aucmd.h b/src/nvim/aucmd.h
deleted file mode 100644
index 9a4dd79a78..0000000000
--- a/src/nvim/aucmd.h
+++ /dev/null
@@ -1,11 +0,0 @@
-#ifndef NVIM_AUCMD_H
-#define NVIM_AUCMD_H
-
-#include <stdint.h>
-
-#ifdef INCLUDE_GENERATED_DECLARATIONS
-# include "aucmd.h.generated.h"
-#endif
-
-#endif // NVIM_AUCMD_H
-
diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c
index 9117dde089..a9e1978ac0 100644
--- a/src/nvim/autocmd.c
+++ b/src/nvim/autocmd.c
@@ -2,10 +2,15 @@
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
// autocmd.c: Autocommand related functions
+#include <signal.h>
+#include "nvim/autocmd.h"
+
+// #include "nvim/api/private/handle.h"
+
+#include "lauxlib.h"
#include "nvim/api/private/helpers.h"
#include "nvim/ascii.h"
-#include "nvim/autocmd.h"
#include "nvim/buffer.h"
#include "nvim/charset.h"
#include "nvim/cursor.h"
@@ -13,8 +18,11 @@
#include "nvim/eval.h"
#include "nvim/eval/userfunc.h"
#include "nvim/ex_docmd.h"
+#include "nvim/ex_getln.h"
#include "nvim/fileio.h"
#include "nvim/getchar.h"
+#include "nvim/lua/executor.h"
+#include "nvim/map.h"
#include "nvim/option.h"
#include "nvim/os/input.h"
#include "nvim/regexp.h"
@@ -28,6 +36,14 @@
# include "autocmd.c.generated.h"
#endif
+// Naming Conventions:
+// - general autocmd behavior start with au_
+// - AutoCmd start with aucmd_
+// - Autocmd.exec stat with aucmd_exec
+// - AutoPat start with aupat_
+// - Groups start with augroup_
+// - Events start with event_
+
//
// The autocommands are stored in a list for each event.
// Autocommands for the same pattern, that are consecutive, are joined
@@ -67,21 +83,20 @@
// Code for automatic commands.
static AutoPatCmd *active_apc_list = NULL; // stack of active autocommands
-/// List of autocmd group names
-static garray_T augroups = { 0, 0, sizeof(char_u *), 10, NULL };
-#define AUGROUP_NAME(i) (((char **)augroups.ga_data)[i])
-#define BUFLOCAL_PAT_LEN 25
+// ID for associating autocmds created via nvim_create_autocmd
+// Used to delete autocmds from nvim_del_autocmd
+static int next_augroup_id = 1;
// use get_deleted_augroup() to get this
static const char *deleted_augroup = NULL;
-// The ID of the current group. Group 0 is the default one.
+// The ID of the current group.
static int current_augroup = AUGROUP_DEFAULT;
-static int au_need_clean = false; // need to delete marked patterns
+// Whether we need to delete marked patterns.
+// While deleting autocmds, they aren't actually remover, just marked.
+static int au_need_clean = false;
-static event_T last_event;
-static int last_group;
static int autocmd_blocked = 0; // block all autocmds
static bool autocmd_nested = false;
@@ -89,6 +104,33 @@ static bool autocmd_include_groups = false;
static char_u *old_termresponse = NULL;
+/// Iterates over all the AutoPats for a particular event
+#define FOR_ALL_AUPATS_IN_EVENT(event, ap) \
+ for (AutoPat *ap = first_autopat[event]; ap != NULL; ap = ap->next) // NOLINT
+
+/// Handles grabbing arguments from `:autocmd` such as ++once and ++nested
+#define ARG_GET_FLAG(errored, cmd, flag, pattern, len) \
+ if (STRNCMP(cmd, pattern, len) == 0 && ascii_iswhite((cmd)[len])) { \
+ if (flag) { \
+ semsg(_(e_duparg2), pattern); \
+ (errored) = true; \
+ } \
+ (flag) = true; \
+ (cmd) = skipwhite((cmd) + (len)); \
+ }
+
+// Map of autocmd group names.
+// name -> ID
+static Map(String, int) augroup_map = MAP_INIT;
+
+static void augroup_map_del(char *name)
+{
+ String key = map_key(String, int)(&augroup_map, cstr_as_string(name));
+ map_del(String, int)(&augroup_map, key);
+ api_free_string(key);
+}
+
+
static inline const char *get_deleted_augroup(void) FUNC_ATTR_ALWAYS_INLINE
{
if (deleted_augroup == NULL) {
@@ -98,7 +140,7 @@ static inline const char *get_deleted_augroup(void) FUNC_ATTR_ALWAYS_INLINE
}
// Show the autocommands for one AutoPat.
-static void show_autocmd(AutoPat *ap, event_T event)
+static void aupat_show(AutoPat *ap)
{
AutoCmd *ac;
@@ -107,39 +149,21 @@ static void show_autocmd(AutoPat *ap, event_T event)
if (got_int) {
return;
}
+
// pattern has been removed
if (ap->pat == NULL) {
return;
}
- msg_putchar('\n');
- if (got_int) {
- return;
- }
- if (event != last_event || ap->group != last_group) {
- if (ap->group != AUGROUP_DEFAULT) {
- if (AUGROUP_NAME(ap->group) == NULL) {
- msg_puts_attr(get_deleted_augroup(), HL_ATTR(HLF_E));
- } else {
- msg_puts_attr(AUGROUP_NAME(ap->group), HL_ATTR(HLF_T));
- }
- msg_puts(" ");
- }
- msg_puts_attr(event_nr2name(event), HL_ATTR(HLF_T));
- last_event = event;
- last_group = ap->group;
- msg_putchar('\n');
- if (got_int) {
- return;
- }
- }
msg_col = 4;
msg_outtrans(ap->pat);
for (ac = ap->cmds; ac != NULL; ac = ac->next) {
- if (ac->cmd == NULL) { // skip removed commands
+ // skip removed commands
+ if (aucmd_exec_is_deleted(ac->exec)) {
continue;
}
+
if (msg_col >= 14) {
msg_putchar('\n');
}
@@ -147,7 +171,7 @@ static void show_autocmd(AutoPat *ap, event_T event)
if (got_int) {
return;
}
- msg_outtrans(ac->cmd);
+ msg_outtrans((char_u *)aucmd_exec_to_string(ac, ac->exec));
if (p_verbose > 0) {
last_set_msg(ac->script_ctx);
}
@@ -163,27 +187,96 @@ static void show_autocmd(AutoPat *ap, event_T event)
}
}
+static void au_show_for_all_events(int group)
+{
+ FOR_ALL_AUEVENTS(event) {
+ au_show_for_event(group, event);
+ }
+}
+
+static void au_show_for_event(int group, event_T event)
+{
+ // Return early if there are no autocmds for this event
+ if (au_event_is_empty(event)) {
+ return;
+ }
+
+ int previous_group = AUGROUP_ERROR;
+ FOR_ALL_AUPATS_IN_EVENT(event, ap) {
+ if (group != AUGROUP_ALL && group != ap->group) {
+ continue;
+ }
+
+ char *name = augroup_name(ap->group);
+
+ msg_putchar('\n');
+ // When switching groups, we need to show the new group information.
+ if (ap->group != previous_group) {
+ // show the group name, if it's not the default group
+ if (ap->group != AUGROUP_DEFAULT) {
+ if (name == NULL) {
+ msg_puts_attr(get_deleted_augroup(), HL_ATTR(HLF_E));
+ } else {
+ msg_puts_attr(name, HL_ATTR(HLF_T));
+ }
+ msg_puts(" ");
+ }
+
+ // show the event name
+ msg_puts_attr(event_nr2name(event), HL_ATTR(HLF_T));
+ msg_putchar('\n');
+ }
+
+ if (got_int) {
+ return;
+ }
+
+ aupat_show(ap);
+
+ previous_group = ap->group;
+ }
+}
+
// Mark an autocommand handler for deletion.
-static void au_remove_pat(AutoPat *ap)
+static void aupat_del(AutoPat *ap)
{
XFREE_CLEAR(ap->pat);
ap->buflocal_nr = -1;
au_need_clean = true;
}
+void aupat_del_for_event_and_group(event_T event, int group)
+{
+ FOR_ALL_AUPATS_IN_EVENT(event, ap) {
+ if (ap->group == group) {
+ aupat_del(ap);
+ }
+ }
+
+ au_need_clean = true;
+ au_cleanup(); // may really delete removed patterns/commands now
+}
+
// Mark all commands for a pattern for deletion.
-static void au_remove_cmds(AutoPat *ap)
+static void aupat_remove_cmds(AutoPat *ap)
{
for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) {
- XFREE_CLEAR(ac->cmd);
+ aucmd_exec_free(&ac->exec);
+
+ if (ac->desc != NULL) {
+ XFREE_CLEAR(ac->desc);
+ }
}
au_need_clean = true;
}
// Delete one command from an autocmd pattern.
-static void au_del_cmd(AutoCmd *ac)
+static void aucmd_del(AutoCmd *ac)
{
- XFREE_CLEAR(ac->cmd);
+ aucmd_exec_free(&ac->exec);
+ if (ac->desc != NULL) {
+ XFREE_CLEAR(ac->desc);
+ }
au_need_clean = true;
}
@@ -191,16 +284,15 @@ static void au_del_cmd(AutoCmd *ac)
/// This is only done when not executing autocommands.
static void au_cleanup(void)
{
- AutoPat *ap, **prev_ap;
- event_T event;
+ AutoPat *ap;
+ AutoPat **prev_ap;
if (autocmd_busy || !au_need_clean) {
return;
}
// Loop over all events.
- for (event = (event_T)0; (int)event < NUM_EVENTS;
- event = (event_T)((int)event + 1)) {
+ FOR_ALL_AUEVENTS(event) {
// Loop over all autocommand patterns.
prev_ap = &(first_autopat[(int)event]);
for (ap = *prev_ap; ap != NULL; ap = *prev_ap) {
@@ -211,9 +303,13 @@ static void au_cleanup(void)
for (AutoCmd *ac = *prev_ac; ac != NULL; ac = *prev_ac) {
// Remove the command if the pattern is to be deleted or when
// the command has been marked for deletion.
- if (ap->pat == NULL || ac->cmd == NULL) {
+ if (ap->pat == NULL || aucmd_exec_is_deleted(ac->exec)) {
*prev_ac = ac->next;
- xfree(ac->cmd);
+ aucmd_exec_free(&ac->exec);
+ if (ac->desc != NULL) {
+ XFREE_CLEAR(ac->desc);
+ }
+
xfree(ac);
} else {
has_cmd = true;
@@ -224,7 +320,7 @@ static void au_cleanup(void)
if (ap->pat != NULL && !has_cmd) {
// Pattern was not marked for deletion, but all of its commands were.
// So mark the pattern for deletion.
- au_remove_pat(ap);
+ aupat_del(ap);
}
// Remove the pattern if it has been marked for deletion.
@@ -250,12 +346,17 @@ static void au_cleanup(void)
au_need_clean = false;
}
+
+// Get the first AutoPat for a particular event.
+AutoPat *au_get_autopat_for_event(event_T event)
+{
+ return first_autopat[(int)event];
+}
+
// Called when buffer is freed, to remove/invalidate related buffer-local
// autocmds.
void aubuflocal_remove(buf_T *buf)
{
- AutoPat *ap;
- event_T event;
AutoPatCmd *apc;
// invalidate currently executing autocommands
@@ -266,12 +367,11 @@ void aubuflocal_remove(buf_T *buf)
}
// invalidate buflocals looping through events
- for (event = (event_T)0; (int)event < NUM_EVENTS;
- event = (event_T)((int)event + 1)) {
- // loop over all autocommand patterns
- for (ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) {
+ FOR_ALL_AUEVENTS(event) {
+ FOR_ALL_AUPATS_IN_EVENT(event, ap) {
if (ap->buflocal_nr == buf->b_fnum) {
- au_remove_pat(ap);
+ aupat_del(ap);
+
if (p_verbose >= 6) {
verbose_enter();
smsg(_("auto-removing autocommand: %s <buffer=%d>"),
@@ -284,60 +384,61 @@ void aubuflocal_remove(buf_T *buf)
au_cleanup();
}
-// Add an autocmd group name.
+// Add an autocmd group name or return existing group matching name.
// Return its ID.
-static int au_new_group(char_u *name)
+int augroup_add(char *name)
{
- int i = au_find_group(name);
- if (i == AUGROUP_ERROR) { // the group doesn't exist yet, add it.
- // First try using a free entry.
- for (i = 0; i < augroups.ga_len; i++) {
- if (AUGROUP_NAME(i) == NULL) {
- break;
- }
- }
- if (i == augroups.ga_len) {
- ga_grow(&augroups, 1);
- }
+ assert(STRICMP(name, "end") != 0);
- AUGROUP_NAME(i) = xstrdup((char *)name);
- if (i == augroups.ga_len) {
- augroups.ga_len++;
- }
+ int existing_id = augroup_find(name);
+ if (existing_id > 0) {
+ assert(existing_id != AUGROUP_DELETED);
+ return existing_id;
}
- return i;
+ if (existing_id == AUGROUP_DELETED) {
+ augroup_map_del(name);
+ }
+
+ int next_id = next_augroup_id++;
+ String name_copy = cstr_to_string(name);
+ map_put(String, int)(&augroup_map, name_copy, next_id);
+
+ return next_id;
}
-static void au_del_group(char_u *name)
+/// Delete the augroup that matches name.
+void augroup_del(char *name, bool stupid_legacy_mode)
{
- int i = au_find_group(name);
+ int i = augroup_find(name);
if (i == AUGROUP_ERROR) { // the group doesn't exist
semsg(_("E367: No such group: \"%s\""), name);
} else if (i == current_augroup) {
emsg(_("E936: Cannot delete the current group"));
} else {
- event_T event;
- AutoPat *ap;
- int in_use = false;
-
- for (event = (event_T)0; (int)event < NUM_EVENTS;
- event = (event_T)((int)event + 1)) {
- for (ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) {
- if (ap->group == i && ap->pat != NULL) {
- give_warning((char_u *)_("W19: Deleting augroup that is still in use"), true);
- in_use = true;
- event = NUM_EVENTS;
- break;
+ if (stupid_legacy_mode) {
+ FOR_ALL_AUEVENTS(event) {
+ FOR_ALL_AUPATS_IN_EVENT(event, ap) {
+ if (ap->group == i && ap->pat != NULL) {
+ give_warning((char_u *)_("W19: Deleting augroup that is still in use"), true);
+ map_put(String, int)(&augroup_map, cstr_as_string(name), AUGROUP_DELETED);
+ return;
+ }
}
}
- }
- xfree(AUGROUP_NAME(i));
- if (in_use) {
- AUGROUP_NAME(i) = (char *)get_deleted_augroup();
} else {
- AUGROUP_NAME(i) = NULL;
+ FOR_ALL_AUEVENTS(event) {
+ FOR_ALL_AUPATS_IN_EVENT(event, ap) {
+ if (ap->group == i) {
+ aupat_del(ap);
+ }
+ }
+ }
}
+
+ // Remove the group because it's not currently in use.
+ augroup_map_del(name);
+ au_cleanup();
}
}
@@ -346,25 +447,70 @@ static void au_del_group(char_u *name)
/// @param name augroup name
///
/// @return the ID or AUGROUP_ERROR (< 0) for error.
-static int au_find_group(const char_u *name)
+int augroup_find(const char *name)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
- for (int i = 0; i < augroups.ga_len; i++) {
- if (AUGROUP_NAME(i) != NULL && AUGROUP_NAME(i) != get_deleted_augroup()
- && STRCMP(AUGROUP_NAME(i), name) == 0) {
- return i;
- }
+ int existing_id = map_get(String, int)(&augroup_map, cstr_as_string((char *)name));
+ if (existing_id == AUGROUP_DELETED) {
+ return existing_id;
+ }
+
+ if (existing_id > 0) {
+ return existing_id;
}
+
return AUGROUP_ERROR;
}
+/// Gets the name for a particular group.
+char *augroup_name(int group)
+{
+ assert(group != 0);
+
+ if (group == AUGROUP_DELETED) {
+ return (char *)get_deleted_augroup();
+ }
+
+ if (group == AUGROUP_ALL) {
+ group = current_augroup;
+ }
+
+ // next_augroup_id is the "source of truth" about what autocmds have existed
+ //
+ // The map_size is not the source of truth because groups can be removed from
+ // the map. When this happens, the map size is reduced. That's why this function
+ // relies on next_augroup_id instead.
+
+ // "END" is always considered the last augroup ID.
+ // Used for expand_get_event_name and expand_get_augroup_name
+ if (group == next_augroup_id) {
+ return "END";
+ }
+
+ // If it's larger than the largest group, then it doesn't have a name
+ if (group > next_augroup_id) {
+ return NULL;
+ }
+
+ String key;
+ int value;
+ map_foreach(&augroup_map, key, value, {
+ if (value == group) {
+ return key.data;
+ }
+ });
+
+ // If it's not in the map anymore, then it must have been deleted.
+ return (char *)get_deleted_augroup();
+}
+
/// Return true if augroup "name" exists.
///
/// @param name augroup name
-bool au_has_group(const char_u *name)
+bool augroup_exists(const char *name)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
- return au_find_group(name) != AUGROUP_ERROR;
+ return augroup_find(name) > 0;
}
/// ":augroup {name}".
@@ -374,20 +520,27 @@ void do_augroup(char_u *arg, int del_group)
if (*arg == NUL) {
emsg(_(e_argreq));
} else {
- au_del_group(arg);
+ augroup_del((char *)arg, true);
}
} else if (STRICMP(arg, "end") == 0) { // ":aug end": back to group 0
current_augroup = AUGROUP_DEFAULT;
} else if (*arg) { // ":aug xxx": switch to group xxx
- current_augroup = au_new_group(arg);
+ current_augroup = augroup_add((char *)arg);
} else { // ":aug": list the group names
msg_start();
- for (int i = 0; i < augroups.ga_len; i++) {
- if (AUGROUP_NAME(i) != NULL) {
- msg_puts(AUGROUP_NAME(i));
- msg_puts(" ");
+
+ String name;
+ int value;
+ map_foreach(&augroup_map, name, value, {
+ if (value > 0) {
+ msg_puts(name.data);
+ } else {
+ msg_puts(augroup_name(value));
}
- }
+
+ msg_puts(" ");
+ });
+
msg_clr_eos();
msg_end();
}
@@ -396,25 +549,30 @@ void do_augroup(char_u *arg, int del_group)
#if defined(EXITFREE)
void free_all_autocmds(void)
{
- for (current_augroup = -1; current_augroup < augroups.ga_len;
- current_augroup++) {
- do_autocmd((char_u *)"", true);
- }
-
- for (int i = 0; i < augroups.ga_len; i++) {
- char *const s = ((char **)(augroups.ga_data))[i];
- if ((const char *)s != get_deleted_augroup()) {
- xfree(s);
+ FOR_ALL_AUEVENTS(event) {
+ FOR_ALL_AUPATS_IN_EVENT(event, ap) {
+ aupat_del(ap);
}
}
- ga_clear(&augroups);
+
+ au_need_clean = true;
+ au_cleanup(); // may really delete removed patterns/commands now
+
+ // Delete the augroup_map, including free the data
+ String name;
+ int id;
+ map_foreach(&augroup_map, name, id, {
+ (void)id;
+ api_free_string(name);
+ })
+ map_destroy(String, int)(&augroup_map);
}
#endif
// Return the event number for event name "start".
// Return NUM_EVENTS if the event name was not found.
// Return a pointer to the next event name in "end".
-static event_T event_name2nr(const char_u *start, char_u **end)
+event_T event_name2nr(const char_u *start, char_u **end)
{
const char_u *p;
int i;
@@ -457,33 +615,6 @@ static const char *event_nr2name(event_T event)
return "Unknown";
}
-/// Scan over the events. "*" stands for all events.
-/// true when group name was found
-static char_u *find_end_event(char_u *arg, int have_group)
-{
- char_u *pat;
- char_u *p;
-
- if (*arg == '*') {
- if (arg[1] && !ascii_iswhite(arg[1])) {
- semsg(_("E215: Illegal character after *: %s"), arg);
- return NULL;
- }
- pat = arg + 1;
- } else {
- for (pat = arg; *pat && *pat != '|' && !ascii_iswhite(*pat); pat = p) {
- if ((int)event_name2nr(pat, &p) >= NUM_EVENTS) {
- if (have_group) {
- semsg(_("E216: No such event: %s"), pat);
- } else {
- semsg(_("E216: No such group or event: %s"), pat);
- }
- return NULL;
- }
- }
- }
- return pat;
-}
/// Return true if "event" is included in 'eventignore'.
///
@@ -601,12 +732,12 @@ void do_autocmd(char_u *arg_in, int forceit)
group = AUGROUP_ALL; // no argument, use all groups
} else {
// Check for a legal group name. If not, use AUGROUP_ALL.
- group = au_get_grouparg(&arg);
+ group = arg_augroup_get(&arg);
}
// Scan over the events.
// If we find an illegal name, return here, don't do anything.
- pat = find_end_event(arg, group != AUGROUP_ALL);
+ pat = arg_event_skip(arg, group != AUGROUP_ALL);
if (pat == NULL) {
return;
}
@@ -643,37 +774,22 @@ void do_autocmd(char_u *arg_in, int forceit)
}
cmd = skipwhite(cmd);
+
+ bool invalid_flags = false;
for (size_t i = 0; i < 2; i++) {
if (*cmd != NUL) {
- // Check for "++once" flag.
- if (STRNCMP(cmd, "++once", 6) == 0 && ascii_iswhite(cmd[6])) {
- if (once) {
- semsg(_(e_duparg2), "++once");
- }
- once = true;
- cmd = skipwhite(cmd + 6);
- }
+ ARG_GET_FLAG(invalid_flags, cmd, once, "++once", 6);
+ ARG_GET_FLAG(invalid_flags, cmd, nested, "++nested", 8);
- // Check for "++nested" flag.
- if ((STRNCMP(cmd, "++nested", 8) == 0 && ascii_iswhite(cmd[8]))) {
- if (nested) {
- semsg(_(e_duparg2), "++nested");
- }
- nested = true;
- cmd = skipwhite(cmd + 8);
- }
-
- // Check for the old (deprecated) "nested" flag.
- if (STRNCMP(cmd, "nested", 6) == 0 && ascii_iswhite(cmd[6])) {
- if (nested) {
- semsg(_(e_duparg2), "nested");
- }
- nested = true;
- cmd = skipwhite(cmd + 6);
- }
+ // Check the deprecated "nested" flag.
+ ARG_GET_FLAG(invalid_flags, cmd, nested, "nested", 6);
}
}
+ if (invalid_flags) {
+ return;
+ }
+
// Find the start of the commands.
// Expand <sfile> in it.
if (*cmd != NUL) {
@@ -685,36 +801,37 @@ void do_autocmd(char_u *arg_in, int forceit)
}
}
+ bool is_showing = !forceit && *cmd == NUL;
+
// Print header when showing autocommands.
- if (!forceit && *cmd == NUL) {
+ if (is_showing) {
// Highlight title
msg_puts_title(_("\n--- Autocommands ---"));
- }
- // Loop over the events.
- last_event = (event_T)-1; // for listing the event name
- last_group = AUGROUP_ERROR; // for listing the group name
- if (*arg == '*' || *arg == NUL || *arg == '|') {
- if (!forceit && *cmd != NUL) {
- emsg(_(e_cannot_define_autocommands_for_all_events));
+ if (*arg == '*' || *arg == '|' || *arg == NUL) {
+ au_show_for_all_events(group);
} else {
- for (event_T event = (event_T)0; event < NUM_EVENTS;
- event = (event_T)(event + 1)) {
+ event_T event = event_name2nr(arg, &arg);
+ assert(event < NUM_EVENTS);
+ au_show_for_event(group, event);
+ }
+ } else {
+ if (*arg == '*' || *arg == NUL || *arg == '|') {
+ if (!forceit && *cmd != NUL) {
+ emsg(_(e_cannot_define_autocommands_for_all_events));
+ } else {
+ do_all_autocmd_events(pat, once, nested, cmd, forceit, group);
+ }
+ } else {
+ while (*arg && *arg != '|' && !ascii_iswhite(*arg)) {
+ event_T event = event_name2nr(arg, &arg);
+ assert(event < NUM_EVENTS);
if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group)
== FAIL) {
break;
}
}
}
- } else {
- while (*arg && *arg != '|' && !ascii_iswhite(*arg)) {
- event_T event = event_name2nr(arg, &arg);
- assert(event < NUM_EVENTS);
- if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group)
- == FAIL) {
- break;
- }
- }
}
if (need_free) {
@@ -723,30 +840,14 @@ void do_autocmd(char_u *arg_in, int forceit)
xfree(envpat);
}
-// Find the group ID in a ":autocmd" or ":doautocmd" argument.
-// The "argp" argument is advanced to the following argument.
-//
-// Returns the group ID or AUGROUP_ALL.
-static int au_get_grouparg(char_u **argp)
+void do_all_autocmd_events(char_u *pat, bool once, int nested, char_u *cmd, bool delete, int group)
{
- char_u *group_name;
- char_u *p;
- char_u *arg = *argp;
- int group = AUGROUP_ALL;
-
- for (p = arg; *p && !ascii_iswhite(*p) && *p != '|'; p++) {
- }
- if (p > arg) {
- group_name = vim_strnsave(arg, (size_t)(p - arg));
- group = au_find_group(group_name);
- if (group == AUGROUP_ERROR) {
- group = AUGROUP_ALL; // no match, use all groups
- } else {
- *argp = skipwhite(p); // match, skip over group name
+ FOR_ALL_AUEVENTS(event) {
+ if (do_autocmd_event(event, pat, once, nested, cmd, delete, group)
+ == FAIL) {
+ return;
}
- xfree(group_name);
}
- return group;
}
// do_autocmd() for one event.
@@ -756,216 +857,293 @@ static int au_get_grouparg(char_u **argp)
// If *cmd == NUL: show entries.
// If forceit == true: delete entries.
// If group is not AUGROUP_ALL: only use this group.
-static int do_autocmd_event(event_T event, char_u *pat, bool once, int nested, char_u *cmd,
- int forceit, int group)
+int do_autocmd_event(event_T event, char_u *pat, bool once, int nested, char_u *cmd, bool delete,
+ int group)
+ FUNC_ATTR_NONNULL_ALL
{
+ // Cannot be used to show all patterns. See au_show_for_event or au_show_for_all_events
+ assert(*pat != NUL || delete);
+
AutoPat *ap;
AutoPat **prev_ap;
- AutoCmd *ac;
- AutoCmd **prev_ac;
- int brace_level;
- char_u *endpat;
int findgroup;
- int allgroups;
int patlen;
int is_buflocal;
int buflocal_nr;
char_u buflocal_pat[BUFLOCAL_PAT_LEN]; // for "<buffer=X>"
+ bool is_adding_cmd = *cmd != NUL;
+
if (group == AUGROUP_ALL) {
findgroup = current_augroup;
} else {
findgroup = group;
}
- allgroups = (group == AUGROUP_ALL && !forceit && *cmd == NUL);
- // Show or delete all patterns for an event.
- if (*pat == NUL) {
- for (ap = first_autopat[event]; ap != NULL; ap = ap->next) {
- if (forceit) { // delete the AutoPat, if it's in the current group
- if (ap->group == findgroup) {
- au_remove_pat(ap);
- }
- } else if (group == AUGROUP_ALL || ap->group == group) {
- show_autocmd(ap, event);
- }
- }
+ // Delete all aupat for an event.
+ if (*pat == NUL && delete) {
+ aupat_del_for_event_and_group(event, findgroup);
+ return OK;
}
// Loop through all the specified patterns.
- for (; *pat; pat = (*endpat == ',' ? endpat + 1 : endpat)) {
- // Find end of the pattern.
- // Watch out for a comma in braces, like "*.\{obj,o\}".
- endpat = pat;
- // ignore single comma
- if (*endpat == ',') {
- continue;
- }
- brace_level = 0;
- for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\');
- endpat++) {
- if (*endpat == '{') {
- brace_level++;
- } else if (*endpat == '}') {
- brace_level--;
- }
- }
- patlen = (int)(endpat - pat);
-
- // detect special <buflocal[=X]> buffer-local patterns
- is_buflocal = false;
+ patlen = (int)aucmd_pattern_length(pat);
+ while (patlen) {
+ // detect special <buffer[=X]> buffer-local patterns
+ is_buflocal = aupat_is_buflocal(pat, patlen);
buflocal_nr = 0;
- if (patlen >= 8 && STRNCMP(pat, "<buffer", 7) == 0
- && pat[patlen - 1] == '>') {
- // "<buffer...>": Error will be printed only for addition.
- // printing and removing will proceed silently.
- is_buflocal = true;
- if (patlen == 8) {
- // "<buffer>"
- buflocal_nr = curbuf->b_fnum;
- } else if (patlen > 9 && pat[7] == '=') {
- if (patlen == 13 && STRNICMP(pat, "<buffer=abuf>", 13) == 0) {
- // "<buffer=abuf>"
- buflocal_nr = autocmd_bufnr;
- } else if (skipdigits(pat + 8) == pat + patlen - 1) {
- // "<buffer=123>"
- buflocal_nr = atoi((char *)pat + 8);
- }
- }
- }
-
if (is_buflocal) {
+ buflocal_nr = aupat_get_buflocal_nr(pat, patlen);
+
// normalize pat into standard "<buffer>#N" form
- snprintf((char *)buflocal_pat,
- BUFLOCAL_PAT_LEN,
- "<buffer=%d>",
- buflocal_nr);
+ aupat_normalize_buflocal_pat(buflocal_pat, pat, patlen, buflocal_nr);
- pat = buflocal_pat; // can modify pat and patlen
- patlen = (int)STRLEN(buflocal_pat); // but not endpat
+ pat = buflocal_pat;
+ patlen = (int)STRLEN(buflocal_pat);
}
- // Find AutoPat entries with this pattern. When adding a command it
- // always goes at or after the last one, so start at the end.
- if (!forceit && *cmd != NUL && last_autopat[(int)event] != NULL) {
- prev_ap = &last_autopat[(int)event];
- } else {
+ if (delete) {
+ assert(*pat != NUL);
+
+ // Find AutoPat entries with this pattern.
+ // always goes at or after the last one, so start at the end.
prev_ap = &first_autopat[(int)event];
- }
- while ((ap = *prev_ap) != NULL) {
- if (ap->pat != NULL) {
- // Accept a pattern when:
- // - a group was specified and it's that group, or a group was
- // not specified and it's the current group, or a group was
- // not specified and we are listing
- // - the length of the pattern matches
- // - the pattern matches.
- // For <buffer[=X]>, this condition works because we normalize
- // all buffer-local patterns.
- if ((allgroups || ap->group == findgroup) && ap->patlen == patlen
- && STRNCMP(pat, ap->pat, patlen) == 0) {
- // Remove existing autocommands.
- // If adding any new autocmd's for this AutoPat, don't
- // delete the pattern from the autopat list, append to
- // this list.
- if (forceit) {
- if (*cmd != NUL && ap->next == NULL) {
- au_remove_cmds(ap);
+ while ((ap = *prev_ap) != NULL) {
+ if (ap->pat != NULL) {
+ // Accept a pattern when:
+ // - a group was specified and it's that group
+ // - the length of the pattern matches
+ // - the pattern matches.
+ // For <buffer[=X]>, this condition works because we normalize
+ // all buffer-local patterns.
+ if (ap->group == findgroup
+ && ap->patlen == patlen
+ && STRNCMP(pat, ap->pat, patlen) == 0) {
+ // Remove existing autocommands.
+ // If adding any new autocmd's for this AutoPat, don't
+ // delete the pattern from the autopat list, append to
+ // this list.
+ if (is_adding_cmd && ap->next == NULL) {
+ aupat_remove_cmds(ap);
break;
}
- au_remove_pat(ap);
- } else if (*cmd == NUL) {
- // Show autocmd's for this autopat, or buflocals <buffer=X>
- show_autocmd(ap, event);
- } else if (ap->next == NULL) {
- // Add autocmd to this autopat, if it's the last one.
- break;
+ aupat_del(ap);
}
}
+ prev_ap = &ap->next;
}
- prev_ap = &ap->next;
}
- // Add a new command.
- if (*cmd != NUL) {
- // If the pattern we want to add a command to does appear at the
- // end of the list (or not is not in the list at all), add the
- // pattern at the end of the list.
- if (ap == NULL) {
- // refuse to add buffer-local ap if buffer number is invalid
- if (is_buflocal
- && (buflocal_nr == 0 || buflist_findnr(buflocal_nr) == NULL)) {
- semsg(_("E680: <buffer=%d>: invalid buffer number "), buflocal_nr);
- return FAIL;
- }
+ if (is_adding_cmd) {
+ AucmdExecutable exec = AUCMD_EXECUTABLE_INIT;
+ exec.type = CALLABLE_EX;
+ exec.callable.cmd = cmd;
+ autocmd_register(0, event, pat, patlen, group, once, nested, NULL, exec);
+ }
- ap = xmalloc(sizeof(AutoPat));
- ap->pat = vim_strnsave(pat, (size_t)patlen);
- ap->patlen = patlen;
+ pat = aucmd_next_pattern(pat, (size_t)patlen);
+ patlen = (int)aucmd_pattern_length(pat);
+ }
- if (is_buflocal) {
- ap->buflocal_nr = buflocal_nr;
- ap->reg_prog = NULL;
- } else {
- char_u *reg_pat;
+ au_cleanup();
+ return OK;
+}
- ap->buflocal_nr = 0;
- reg_pat = file_pat_to_reg_pat(pat, endpat, &ap->allow_dirs, true);
- if (reg_pat != NULL) {
- ap->reg_prog = vim_regcomp(reg_pat, RE_MAGIC);
- }
- xfree(reg_pat);
- if (reg_pat == NULL || ap->reg_prog == NULL) {
- xfree(ap->pat);
- xfree(ap);
- return FAIL;
- }
- }
+int autocmd_register(int64_t id, event_T event, char_u *pat, int patlen, int group, bool once,
+ bool nested, char *desc, AucmdExecutable aucmd)
+{
+ // 0 is not a valid group.
+ assert(group != 0);
- // need to initialize last_mode for the first ModeChanged autocmd
- if (event == EVENT_MODECHANGED && !has_event(EVENT_MODECHANGED)) {
- xfree(last_mode);
- last_mode = get_mode();
- }
+ AutoPat *ap;
+ AutoPat **prev_ap;
+ AutoCmd *ac;
+ AutoCmd **prev_ac;
+ int is_buflocal;
+ int buflocal_nr;
+ int findgroup;
+ char_u buflocal_pat[BUFLOCAL_PAT_LEN]; // for "<buffer=X>"
- // If the event is CursorMoved, update the last cursor position
- // position to avoid immediately triggering the autocommand
- if (event == EVENT_CURSORMOVED && !has_event(EVENT_CURSORMOVED)) {
- curwin->w_last_cursormoved = curwin->w_cursor;
- }
+ if (patlen > (int)STRLEN(pat)) {
+ return FAIL;
+ }
- ap->cmds = NULL;
- *prev_ap = ap;
- last_autopat[(int)event] = ap;
- ap->next = NULL;
- if (group == AUGROUP_ALL) {
- ap->group = current_augroup;
- } else {
- ap->group = group;
+ if (group == AUGROUP_ALL) {
+ findgroup = current_augroup;
+ } else {
+ findgroup = group;
+ }
+
+
+ // detect special <buffer[=X]> buffer-local patterns
+ is_buflocal = aupat_is_buflocal(pat, patlen);
+ buflocal_nr = 0;
+
+ if (is_buflocal) {
+ buflocal_nr = aupat_get_buflocal_nr(pat, patlen);
+
+ // normalize pat into standard "<buffer>#N" form
+ aupat_normalize_buflocal_pat(buflocal_pat, pat, patlen, buflocal_nr);
+
+ pat = buflocal_pat;
+ patlen = (int)STRLEN(buflocal_pat);
+ }
+
+ if (last_autopat[(int)event] != NULL) {
+ prev_ap = &last_autopat[(int)event];
+ } else {
+ prev_ap = &first_autopat[(int)event];
+ }
+
+ while ((ap = *prev_ap) != NULL) {
+ if (ap->pat != NULL) {
+ // Accept a pattern when:
+ // - a group was specified and it's that group
+ // - the length of the pattern matches
+ // - the pattern matches.
+ // For <buffer[=X]>, this condition works because we normalize
+ // all buffer-local patterns.
+ if (ap->group == findgroup
+ && ap->patlen == patlen
+ && STRNCMP(pat, ap->pat, patlen) == 0) {
+ if (ap->next == NULL) {
+ // Add autocmd to this autopat, if it's the last one.
+ break;
}
}
+ }
+ prev_ap = &ap->next;
+ }
+
+ // If the pattern we want to add a command to does appear at the
+ // end of the list (or not is not in the list at all), add the
+ // pattern at the end of the list.
+ if (ap == NULL) {
+ // refuse to add buffer-local ap if buffer number is invalid
+ if (is_buflocal
+ && (buflocal_nr == 0 || buflist_findnr(buflocal_nr) == NULL)) {
+ semsg(_("E680: <buffer=%d>: invalid buffer number "), buflocal_nr);
+ return FAIL;
+ }
+
+ ap = xmalloc(sizeof(AutoPat));
+ ap->pat = vim_strnsave(pat, (size_t)patlen);
+ ap->patlen = patlen;
+
+ if (is_buflocal) {
+ ap->buflocal_nr = buflocal_nr;
+ ap->reg_prog = NULL;
+ } else {
+ char_u *reg_pat;
- // Add the autocmd at the end of the AutoCmd list.
- prev_ac = &(ap->cmds);
- while ((ac = *prev_ac) != NULL) {
- prev_ac = &ac->next;
+ ap->buflocal_nr = 0;
+ reg_pat = file_pat_to_reg_pat(pat, pat + patlen, &ap->allow_dirs, true);
+ if (reg_pat != NULL) {
+ ap->reg_prog = vim_regcomp(reg_pat, RE_MAGIC);
+ }
+ xfree(reg_pat);
+ if (reg_pat == NULL || ap->reg_prog == NULL) {
+ xfree(ap->pat);
+ xfree(ap);
+ return FAIL;
}
- ac = xmalloc(sizeof(AutoCmd));
- ac->cmd = vim_strsave(cmd);
- ac->script_ctx = current_sctx;
- ac->script_ctx.sc_lnum += sourcing_lnum;
- ac->next = NULL;
- *prev_ac = ac;
- ac->once = once;
- ac->nested = nested;
+ }
+
+ // need to initialize last_mode for the first ModeChanged autocmd
+ if (event == EVENT_MODECHANGED && !has_event(EVENT_MODECHANGED)) {
+ xfree(last_mode);
+ last_mode = get_mode();
+ }
+
+ // If the event is CursorMoved, update the last cursor position
+ // position to avoid immediately triggering the autocommand
+ if (event == EVENT_CURSORMOVED && !has_event(EVENT_CURSORMOVED)) {
+ curwin->w_last_cursormoved = curwin->w_cursor;
+ }
+
+ ap->cmds = NULL;
+ *prev_ap = ap;
+ last_autopat[(int)event] = ap;
+ ap->next = NULL;
+ if (group == AUGROUP_ALL) {
+ ap->group = current_augroup;
+ } else {
+ ap->group = group;
}
}
- au_cleanup(); // may really delete removed patterns/commands now
+ // Add the autocmd at the end of the AutoCmd list.
+ prev_ac = &(ap->cmds);
+ while ((ac = *prev_ac) != NULL) {
+ prev_ac = &ac->next;
+ }
+
+ ac = xmalloc(sizeof(AutoCmd));
+ *prev_ac = ac;
+
+ ac->id = id;
+ ac->exec = aucmd_exec_copy(aucmd);
+ ac->script_ctx = current_sctx;
+ ac->script_ctx.sc_lnum += sourcing_lnum;
+ ac->next = NULL;
+ ac->once = once;
+ ac->nested = nested;
+ ac->desc = NULL;
+
+ // TODO(tjdevries): What to do about :autocmd and where/how to show lua stuffs there.
+ // perhaps: <lua>DESCRIPTION or similar
+ if (desc != NULL) {
+ ac->desc = xstrdup(desc);
+ } else {
+ ac->desc = aucmd_exec_default_desc(aucmd);
+ }
+
return OK;
}
+size_t aucmd_pattern_length(char_u *pat)
+{
+ if (*pat == NUL) {
+ return 0;
+ }
+
+ char_u *endpat;
+
+ for (; *pat; pat = (*endpat == ',' ? endpat + 1 : endpat)) {
+ // Find end of the pattern.
+ // Watch out for a comma in braces, like "*.\{obj,o\}".
+ endpat = pat;
+ // ignore single comma
+ if (*endpat == ',') {
+ continue;
+ }
+ int brace_level = 0;
+ for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\');
+ endpat++) {
+ if (*endpat == '{') {
+ brace_level++;
+ } else if (*endpat == '}') {
+ brace_level--;
+ }
+ }
+
+ return (size_t)(endpat - pat);
+ }
+
+ return STRLEN(pat);
+}
+
+char_u *aucmd_next_pattern(char_u *pat, size_t patlen)
+{
+ pat = pat + patlen;
+ if (*pat == ',') {
+ pat = pat + 1;
+ }
+
+ return pat;
+}
+
/// Implementation of ":doautocmd [group] event [fname]".
/// Return OK for success, FAIL for failure;
///
@@ -981,7 +1159,7 @@ int do_doautocmd(char_u *arg, bool do_msg, bool *did_something)
}
// Check for a legal group name. If not, use AUGROUP_ALL.
- group = au_get_grouparg(&arg);
+ group = arg_augroup_get(&arg);
if (*arg == '*') {
emsg(_("E217: Can't execute autocommands for ALL events"));
@@ -990,7 +1168,7 @@ int do_doautocmd(char_u *arg, bool do_msg, bool *did_something)
// Scan over the events.
// If we find an illegal name, return here, don't do anything.
- fname = find_end_event(arg, group != AUGROUP_ALL);
+ fname = arg_event_skip(arg, group != AUGROUP_ALL);
if (fname == NULL) {
return FAIL;
}
@@ -1378,8 +1556,8 @@ bool trigger_cursorhold(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
/// @param eap Ex command arguments
///
/// @return true if some commands were executed.
-static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, bool force,
- int group, buf_T *buf, exarg_T *eap)
+bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, bool force, int group,
+ buf_T *buf, exarg_T *eap)
{
char_u *sfname = NULL; // short file name
char_u *tail;
@@ -1402,7 +1580,7 @@ static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io,
proftime_T wait_time;
bool did_save_redobuff = false;
save_redo_T save_redo;
- const bool save_KeyTyped = KeyTyped;
+ const bool save_KeyTyped = KeyTyped; // NOLINT
// Quickly return if there are no autocommands for this event or
// autocommands are blocked.
@@ -1814,6 +1992,11 @@ void auto_next_pat(AutoPatCmd *apc, int stop_at_last)
/// @return allocated string, or NULL for end of autocommands.
char_u *getnextac(int c, void *cookie, int indent, bool do_concat)
{
+ // These arguments are required for do_cmdline.
+ (void)c;
+ (void)indent;
+ (void)do_concat;
+
AutoPatCmd *acp = (AutoPatCmd *)cookie;
char_u *retval;
AutoCmd *ac;
@@ -1826,7 +2009,8 @@ char_u *getnextac(int c, void *cookie, int indent, bool do_concat)
// repeat until we find an autocommand to execute
for (;;) {
// skip removed commands
- while (acp->nextcmd != NULL && acp->nextcmd->cmd == NULL) {
+ while (acp->nextcmd != NULL
+ && aucmd_exec_is_deleted(acp->nextcmd->exec)) {
if (acp->nextcmd->last) {
acp->nextcmd = NULL;
} else {
@@ -1856,14 +2040,32 @@ char_u *getnextac(int c, void *cookie, int indent, bool do_concat)
if (p_verbose >= 9) {
verbose_enter_scroll();
- smsg(_("autocommand %s"), ac->cmd);
+ smsg(_("autocommand %s"), aucmd_exec_to_string(ac, ac->exec));
msg_puts("\n"); // don't overwrite this either
verbose_leave_scroll();
}
- retval = vim_strsave(ac->cmd);
+
+ if (ac->exec.type == CALLABLE_CB) {
+ typval_T argsin = TV_INITIAL_VALUE;
+ typval_T rettv = TV_INITIAL_VALUE;
+ callback_call(&ac->exec.callable.cb, 0, &argsin, &rettv);
+
+ // Major Hack Alert:
+ // We just return "not-null" and continue going.
+ // This would be a good candidate for a refactor. You would need to refactor:
+ // 1. do_cmdline to accept something besides a string
+ // OR
+ // 2. make where we call do_cmdline for autocmds not have to return anything,
+ // and instead we loop over all the matches and just execute one-by-one.
+ // However, my expectation woudl be that could be expensive.
+ retval = vim_strsave((char_u *)"");
+ } else {
+ retval = vim_strsave(ac->exec.callable.cmd);
+ }
+
// Remove one-shot ("once") autocmd in anticipation of its execution.
if (ac->once) {
- au_del_cmd(ac);
+ aucmd_del(ac);
}
autocmd_nested = ac->nested;
current_sctx = ac->script_ctx;
@@ -1928,19 +2130,12 @@ bool has_autocmd(event_T event, char_u *sfname, buf_T *buf) FUNC_ATTR_WARN_UNUSE
// Function given to ExpandGeneric() to obtain the list of autocommand group
// names.
-char_u *get_augroup_name(expand_T *xp, int idx)
+char_u *expand_get_augroup_name(expand_T *xp, int idx)
{
- if (idx == augroups.ga_len) { // add "END" add the end
- return (char_u *)"END";
- }
- if (idx >= augroups.ga_len) { // end of list
- return NULL;
- }
- if (AUGROUP_NAME(idx) == NULL || AUGROUP_NAME(idx) == get_deleted_augroup()) {
- // skip deleted entries
- return (char_u *)"";
- }
- return (char_u *)AUGROUP_NAME(idx);
+ // Required for ExpandGeneric
+ (void)xp;
+
+ return (char_u *)augroup_name(idx + 1);
}
/// @param doautocmd true for :doauto*, false for :autocmd
@@ -1952,7 +2147,7 @@ char_u *set_context_in_autocmd(expand_T *xp, char_u *arg, int doautocmd)
// check for a group name, skip it if present
autocmd_include_groups = false;
p = arg;
- group = au_get_grouparg(&arg);
+ group = arg_augroup_get(&arg);
// If there only is a group name that's what we expand.
if (*arg == NUL && group != AUGROUP_ALL && !ascii_iswhite(arg[-1])) {
@@ -1993,16 +2188,25 @@ char_u *set_context_in_autocmd(expand_T *xp, char_u *arg, int doautocmd)
}
// Function given to ExpandGeneric() to obtain the list of event names.
-char_u *get_event_name(expand_T *xp, int idx)
+char_u *expand_get_event_name(expand_T *xp, int idx)
{
- if (idx < augroups.ga_len) { // First list group names, if wanted
- if (!autocmd_include_groups || AUGROUP_NAME(idx) == NULL
- || AUGROUP_NAME(idx) == get_deleted_augroup()) {
- return (char_u *)""; // skip deleted entries
+ // xp is a required parameter to be used with ExpandGeneric
+ (void)xp;
+
+
+ // List group names
+ char *name = augroup_name(idx + 1);
+ if (name != NULL) {
+ // skip when not including groups or skip deleted entries
+ if (!autocmd_include_groups || name == get_deleted_augroup()) {
+ return (char_u *)"";
}
- return (char_u *)AUGROUP_NAME(idx);
+
+ return (char_u *)name;
}
- return (char_u *)event_names[idx - augroups.ga_len].name;
+
+ // List event names
+ return (char_u *)event_names[idx - next_augroup_id].name;
}
/// Check whether given autocommand is supported
@@ -2045,7 +2249,7 @@ bool au_exists(const char *const arg) FUNC_ATTR_WARN_UNUSED_RESULT
}
// First, look for an autocmd group name.
- group = au_find_group((char_u *)arg_save);
+ group = augroup_find(arg_save);
char *event_name;
if (group == AUGROUP_ERROR) {
// Didn't match a group name, assume the first argument is an event.
@@ -2109,3 +2313,346 @@ theend:
xfree(arg_save);
return retval;
}
+
+// Checks if a pattern is buflocal
+bool aupat_is_buflocal(char_u *pat, int patlen)
+{
+ return patlen >= 8
+ && STRNCMP(pat, "<buffer", 7) == 0
+ && (pat)[patlen - 1] == '>';
+}
+
+int aupat_get_buflocal_nr(char_u *pat, int patlen)
+{
+ assert(aupat_is_buflocal(pat, patlen));
+
+ // "<buffer>"
+ if (patlen == 8) {
+ return curbuf->b_fnum;
+ }
+
+ if (patlen > 9 && (pat)[7] == '=') {
+ // "<buffer=abuf>"
+ if (patlen == 13 && STRNICMP(pat, "<buffer=abuf>", 13) == 0) {
+ return autocmd_bufnr;
+ }
+
+ // "<buffer=123>"
+ if (skipdigits(pat + 8) == pat + patlen - 1) {
+ return atoi((char *)pat + 8);
+ }
+ }
+
+ return 0;
+}
+
+// normalize buffer pattern
+void aupat_normalize_buflocal_pat(char_u *dest, char_u *pat, int patlen, int buflocal_nr)
+{
+ assert(aupat_is_buflocal(pat, patlen));
+
+ if (buflocal_nr == 0) {
+ buflocal_nr = curbuf->handle;
+ }
+
+ // normalize pat into standard "<buffer>#N" form
+ snprintf((char *)dest,
+ BUFLOCAL_PAT_LEN,
+ "<buffer=%d>",
+ buflocal_nr);
+}
+
+int autocmd_delete_event(int group, event_T event, char_u *pat)
+ FUNC_ATTR_NONNULL_ALL
+{
+ return do_autocmd_event(event, pat, false, false, (char_u *)"", true, group);
+}
+
+/// Deletes an autocmd by ID.
+/// Only autocmds created via the API have IDs associated with them. There
+/// is no way to delete a specific autocmd created via :autocmd
+void autocmd_delete_id(int64_t id)
+{
+ FOR_ALL_AUEVENTS(event) {
+ FOR_ALL_AUPATS_IN_EVENT(event, ap) {
+ for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) {
+ if (ac->id == id) {
+ aucmd_del(ac);
+ }
+ }
+ }
+ }
+}
+
+// ===========================================================================
+// AucmdExecutable Functions
+// ===========================================================================
+char *aucmd_exec_default_desc(AucmdExecutable acc)
+{
+ size_t msglen = 100;
+
+ switch (acc.type) {
+ case CALLABLE_CB:
+ switch (acc.callable.cb.type) {
+ case kCallbackLua: {
+ char *msg = (char *)xmallocz(msglen);
+ snprintf(msg, msglen, "<Lua function %d>", acc.callable.cb.data.luaref);
+ return msg;
+ }
+ case kCallbackFuncref: {
+ // TODO(tjdevries): Is this enough space for this?
+ char *msg = (char *)xmallocz(msglen);
+ snprintf(msg, msglen, "<vim function: %s>", acc.callable.cb.data.funcref);
+ return msg;
+ }
+ case kCallbackPartial: {
+ char *msg = (char *)xmallocz(msglen);
+ snprintf(msg, msglen, "<vim partial: %s>", acc.callable.cb.data.partial->pt_name);
+ return msg;
+ }
+ default:
+ return NULL;
+ }
+ default:
+ return NULL;
+ }
+
+ abort();
+}
+char *aucmd_exec_to_string(AutoCmd *ac, AucmdExecutable acc)
+{
+ switch (acc.type) {
+ case CALLABLE_EX:
+ return (char *)acc.callable.cmd;
+ case CALLABLE_CB:
+ return ac->desc;
+ case CALLABLE_NONE:
+ return "This is not possible";
+ }
+
+ abort();
+}
+
+void aucmd_exec_free(AucmdExecutable *acc)
+{
+ switch (acc->type) {
+ case CALLABLE_EX:
+ XFREE_CLEAR(acc->callable.cmd);
+ break;
+ case CALLABLE_CB:
+ callback_free(&acc->callable.cb);
+ break;
+ case CALLABLE_NONE:
+ return;
+ }
+
+ acc->type = CALLABLE_NONE;
+}
+
+AucmdExecutable aucmd_exec_copy(AucmdExecutable src)
+{
+ AucmdExecutable dest = AUCMD_EXECUTABLE_INIT;
+
+ switch (src.type) {
+ case CALLABLE_EX:
+ dest.type = CALLABLE_EX;
+ dest.callable.cmd = vim_strsave(src.callable.cmd);
+ return dest;
+ case CALLABLE_CB:
+ dest.type = CALLABLE_CB;
+ callback_copy(&dest.callable.cb, &src.callable.cb);
+ return dest;
+ case CALLABLE_NONE:
+ return dest;
+ }
+
+ abort();
+}
+
+bool aucmd_exec_is_deleted(AucmdExecutable acc)
+{
+ switch (acc.type) {
+ case CALLABLE_EX:
+ return acc.callable.cmd == NULL;
+ case CALLABLE_CB:
+ return callback_is_freed(acc.callable.cb);
+ case CALLABLE_NONE:
+ return true;
+ }
+
+ abort();
+}
+
+bool au_event_is_empty(event_T event)
+{
+ return first_autopat[event] == NULL;
+}
+
+// Arg Parsing Functions
+
+/// Scan over the events. "*" stands for all events.
+/// true when group name was found
+static char_u *arg_event_skip(char_u *arg, int have_group)
+{
+ char_u *pat;
+ char_u *p;
+
+ if (*arg == '*') {
+ if (arg[1] && !ascii_iswhite(arg[1])) {
+ semsg(_("E215: Illegal character after *: %s"), arg);
+ return NULL;
+ }
+ pat = arg + 1;
+ } else {
+ for (pat = arg; *pat && *pat != '|' && !ascii_iswhite(*pat); pat = p) {
+ if ((int)event_name2nr(pat, &p) >= NUM_EVENTS) {
+ if (have_group) {
+ semsg(_("E216: No such event: %s"), pat);
+ } else {
+ semsg(_("E216: No such group or event: %s"), pat);
+ }
+ return NULL;
+ }
+ }
+ }
+ return pat;
+}
+
+// Find the group ID in a ":autocmd" or ":doautocmd" argument.
+// The "argp" argument is advanced to the following argument.
+//
+// Returns the group ID or AUGROUP_ALL.
+static int arg_augroup_get(char_u **argp)
+{
+ char_u *group_name;
+ char_u *p;
+ char_u *arg = *argp;
+ int group = AUGROUP_ALL;
+
+ for (p = arg; *p && !ascii_iswhite(*p) && *p != '|'; p++) {
+ }
+ if (p > arg) {
+ group_name = vim_strnsave(arg, (size_t)(p - arg));
+ group = augroup_find((char *)group_name);
+ if (group == AUGROUP_ERROR) {
+ group = AUGROUP_ALL; // no match, use all groups
+ } else {
+ *argp = skipwhite(p); // match, skip over group name
+ }
+ xfree(group_name);
+ }
+ return group;
+}
+
+
+// UI Enter
+void do_autocmd_uienter(uint64_t chanid, bool attached)
+{
+ static bool recursive = false;
+
+ if (recursive) {
+ return; // disallow recursion
+ }
+ recursive = true;
+
+ save_v_event_T save_v_event;
+ dict_T *dict = get_v_event(&save_v_event);
+ assert(chanid < VARNUMBER_MAX);
+ tv_dict_add_nr(dict, S_LEN("chan"), (varnumber_T)chanid);
+ tv_dict_set_keys_readonly(dict);
+ apply_autocmds(attached ? EVENT_UIENTER : EVENT_UILEAVE,
+ NULL, NULL, false, curbuf);
+ restore_v_event(dict, &save_v_event);
+
+ recursive = false;
+}
+
+// FocusGained
+
+static void focusgained_event(void **argv)
+{
+ bool *gainedp = argv[0];
+ do_autocmd_focusgained(*gainedp);
+ xfree(gainedp);
+}
+
+void autocmd_schedule_focusgained(bool gained)
+{
+ bool *gainedp = xmalloc(sizeof(*gainedp));
+ *gainedp = gained;
+ loop_schedule_deferred(&main_loop,
+ event_create(focusgained_event, 1, gainedp));
+}
+
+static void do_autocmd_focusgained(bool gained)
+{
+ static bool recursive = false;
+ static Timestamp last_time = (time_t)0;
+ bool need_redraw = false;
+
+ if (recursive) {
+ return; // disallow recursion
+ }
+ recursive = true;
+ need_redraw |= apply_autocmds((gained ? EVENT_FOCUSGAINED : EVENT_FOCUSLOST),
+ NULL, NULL, false, curbuf);
+
+ // When activated: Check if any file was modified outside of Vim.
+ // Only do this when not done within the last two seconds as:
+ // 1. Some filesystems have modification time granularity in seconds. Fat32
+ // has a granularity of 2 seconds.
+ // 2. We could get multiple notifications in a row.
+ if (gained && last_time + (Timestamp)2000 < os_now()) {
+ need_redraw = check_timestamps(true);
+ last_time = os_now();
+ }
+
+ if (need_redraw) {
+ // Something was executed, make sure the cursor is put back where it
+ // belongs.
+ need_wait_return = false;
+
+ if (State & CMDLINE) {
+ redrawcmdline();
+ } else if ((State & NORMAL) || (State & INSERT)) {
+ if (must_redraw != 0) {
+ update_screen(0);
+ }
+
+ setcursor();
+ }
+
+ ui_flush();
+ }
+
+ if (need_maketitle) {
+ maketitle();
+ }
+
+ recursive = false;
+}
+
+// initialization
+
+void init_default_autocmds(void)
+{
+ // open terminals when opening files that start with term://
+#define PROTO "term://"
+ do_cmdline_cmd("augroup nvim_terminal");
+ do_cmdline_cmd("autocmd BufReadCmd " PROTO "* ++nested "
+ "if !exists('b:term_title')|call termopen("
+ // Capture the command string
+ "matchstr(expand(\"<amatch>\"), "
+ "'\\c\\m" PROTO "\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), "
+ // capture the working directory
+ "{'cwd': expand(get(matchlist(expand(\"<amatch>\"), "
+ "'\\c\\m" PROTO "\\(.\\{-}\\)//'), 1, ''))})"
+ "|endif");
+ do_cmdline_cmd("augroup END");
+#undef PROTO
+
+ // limit syntax synchronization in the command window
+ do_cmdline_cmd("augroup nvim_cmdwin");
+ do_cmdline_cmd("autocmd! CmdwinEnter [:>] syntax sync minlines=1 maxlines=1");
+ do_cmdline_cmd("augroup END");
+}
diff --git a/src/nvim/autocmd.h b/src/nvim/autocmd.h
index 63c5abd4f8..0a1f036183 100644
--- a/src/nvim/autocmd.h
+++ b/src/nvim/autocmd.h
@@ -4,6 +4,11 @@
#include "nvim/buffer_defs.h"
#include "nvim/ex_cmds_defs.h"
+// event_T definition
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "auevents_enum.generated.h"
+#endif
+
// Struct to save values in before executing autocommands for a buffer that is
// not the current buffer.
typedef struct {
@@ -18,22 +23,23 @@ typedef struct {
} aco_save_T;
typedef struct AutoCmd {
- char_u *cmd; // Command to be executed (NULL when
- // command has been removed)
+ AucmdExecutable exec;
bool once; // "One shot": removed after execution
bool nested; // If autocommands nest here
bool last; // last command in list
+ int64_t id; // TODO(tjdevries): Explain
sctx_T script_ctx; // script context where defined
- struct AutoCmd *next; // Next AutoCmd in list
+ char *desc; // Description for the autocmd.
+ struct AutoCmd *next; // Next AutoCmd in list
} AutoCmd;
typedef struct AutoPat {
- struct AutoPat *next; // next AutoPat in AutoPat list; MUST
- // be the first entry
- char_u *pat; // pattern as typed (NULL when pattern
- // has been removed)
- regprog_T *reg_prog; // compiled regprog for pattern
- AutoCmd *cmds; // list of commands to do
+ struct AutoPat *next; // next AutoPat in AutoPat list; MUST
+ // be the first entry
+ char_u *pat; // pattern as typed (NULL when pattern
+ // has been removed)
+ regprog_T *reg_prog; // compiled regprog for pattern
+ AutoCmd *cmds; // list of commands to do
int group; // group ID
int patlen; // strlen() of pat
int buflocal_nr; // !=0 for buffer-local AutoPat
@@ -41,13 +47,7 @@ typedef struct AutoPat {
char last; // last pattern for apply_autocmds()
} AutoPat;
-#ifdef INCLUDE_GENERATED_DECLARATIONS
-# include "auevents_enum.generated.h"
-#endif
-
-///
/// Struct used to keep status while executing autocommands for an event.
-///
typedef struct AutoPatCmd {
AutoPat *curpat; // next AutoPat to examine
AutoCmd *nextcmd; // next AutoCmd to execute
@@ -75,8 +75,16 @@ EXTERN bool au_did_filetype INIT(= false);
# include "autocmd.h.generated.h"
#endif
-#define AUGROUP_DEFAULT -1 // default autocmd group
-#define AUGROUP_ERROR -2 // erroneous autocmd group
-#define AUGROUP_ALL -3 // all autocmd groups
+#define AUGROUP_DEFAULT (-1) // default autocmd group
+#define AUGROUP_ERROR (-2) // erroneous autocmd group
+#define AUGROUP_ALL (-3) // all autocmd groups
+#define AUGROUP_DELETED (-4) // all autocmd groups
+// #define AUGROUP_NS -5 // TODO(tjdevries): Support namespaced based augroups
+
+#define BUFLOCAL_PAT_LEN 25
+
+/// Iterates over all the events for auto commands
+#define FOR_ALL_AUEVENTS(event) \
+ for (event_T event = (event_T)0; (int)event < (int)NUM_EVENTS; event = (event_T)((int)event + 1)) // NOLINT
#endif // NVIM_AUTOCMD_H
diff --git a/src/nvim/diff.c b/src/nvim/diff.c
index 233753839b..80bd3229c6 100644
--- a/src/nvim/diff.c
+++ b/src/nvim/diff.c
@@ -85,9 +85,9 @@ typedef struct {
// used for recording hunks from xdiff
typedef struct {
linenr_T lnum_orig;
- long count_orig;
+ long count_orig;
linenr_T lnum_new;
- long count_new;
+ long count_new;
} diffhunk_T;
// two diff inputs and one result
@@ -1285,7 +1285,7 @@ void ex_diffpatch(exarg_T *eap)
ex_file(eap);
// Do filetype detection with the new name.
- if (au_has_group((char_u *)"filetypedetect")) {
+ if (augroup_exists("filetypedetect")) {
do_cmdline_cmd(":doau filetypedetect BufRead");
}
}
@@ -3159,8 +3159,7 @@ static int parse_diff_unified(char_u *line, diffhunk_T *hunk)
/// Callback function for the xdl_diff() function.
/// Stores the diff output in a grow array.
///
-static int xdiff_out(long start_a, long count_a, long start_b, long count_b,
- void *priv)
+static int xdiff_out(long start_a, long count_a, long start_b, long count_b, void *priv)
{
diffout_T *dout = (diffout_T *)priv;
diffhunk_T *p = xmalloc(sizeof(*p));
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index 07f0799bc4..0322898827 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -6,6 +6,7 @@
*/
#include <math.h>
+#include <stdlib.h>
#include "auto/config.h"
@@ -3258,7 +3259,7 @@ char_u *get_user_var_name(expand_T *xp, int idx)
// b: variables
// In cmdwin, the alternative buffer should be used.
hashtab_T *ht
- = is_in_cmdwin() ? &prevwin->w_buffer->b_vars->dv_hashtab : &curbuf->b_vars->dv_hashtab;
+ = is_in_cmdwin() ? &prevwin->w_buffer->b_vars->dv_hashtab : &curbuf->b_vars->dv_hashtab;
if (bdone < ht->ht_used) {
if (bdone++ == 0) {
hi = ht->ht_array;
@@ -7746,6 +7747,7 @@ bool callback_from_typval(Callback *const callback, typval_T *const arg)
callback->type = kCallbackFuncref;
}
} else if (nlua_is_table_from_lua(arg)) {
+ // TODO(tjdvries): UnifiedCallback
char_u *name = nlua_register_table_as_callable(arg);
if (name != NULL) {
@@ -7775,6 +7777,7 @@ bool callback_call(Callback *const callback, const int argcount_in, typval_T *co
{
partial_T *partial;
char_u *name;
+ Array args = ARRAY_DICT_INIT;
switch (callback->type) {
case kCallbackFuncref:
name = callback->data.funcref;
@@ -7786,6 +7789,13 @@ bool callback_call(Callback *const callback, const int argcount_in, typval_T *co
name = partial_name(partial);
break;
+ case kCallbackLua:
+ ILOG(" We tryin to call dat dang lua ref ");
+ nlua_call_ref(callback->data.luaref, "aucmd", args, false, NULL);
+
+ return false;
+ break;
+
case kCallbackNone:
return false;
break;
diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c
index 3763390c22..49dde537c3 100644
--- a/src/nvim/eval/funcs.c
+++ b/src/nvim/eval/funcs.c
@@ -894,6 +894,7 @@ static void f_call(typval_T *argvars, typval_T *rettv, FunPtr fptr)
partial = argvars[0].vval.v_partial;
func = partial_name(partial);
} else if (nlua_is_table_from_lua(&argvars[0])) {
+ // TODO(tjdevries): UnifiedCallback
func = nlua_register_table_as_callable(&argvars[0]);
owned = true;
} else {
@@ -4242,7 +4243,7 @@ static void win_move_into_split(win_T *wp, win_T *targetwin, int size, int flags
int height = wp->w_height;
win_T *oldwin = curwin;
- if (wp == targetwin) {
+ if (wp == targetwin || wp == aucmd_win) {
return;
}
diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c
index 6f8b032d41..fbda7fbc3c 100644
--- a/src/nvim/eval/typval.c
+++ b/src/nvim/eval/typval.c
@@ -28,11 +28,11 @@
#include "nvim/mbyte.h"
#include "nvim/memory.h"
#include "nvim/message.h"
+#include "nvim/os/fileio.h"
#include "nvim/os/input.h"
#include "nvim/pos.h"
#include "nvim/types.h"
#include "nvim/vim.h"
-#include "nvim/os/fileio.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "eval/typval.c.generated.h"
@@ -1123,6 +1123,8 @@ bool tv_callback_equal(const Callback *cb1, const Callback *cb2)
// FIXME: this is inconsistent with tv_equal but is needed for precision
// maybe change dictwatcheradd to return a watcher id instead?
return cb1->data.partial == cb2->data.partial;
+ case kCallbackLua:
+ return cb1->data.luaref == cb2->data.luaref;
case kCallbackNone:
return true;
}
@@ -1142,6 +1144,9 @@ void callback_free(Callback *callback)
case kCallbackPartial:
partial_unref(callback->data.partial);
break;
+ case kCallbackLua:
+ NLUA_CLEAR_REF(callback->data.luaref);
+ break;
case kCallbackNone:
break;
}
@@ -1149,6 +1154,12 @@ void callback_free(Callback *callback)
callback->data.funcref = NULL;
}
+/// Check if callback is freed
+bool callback_is_freed(Callback callback)
+{
+ return false;
+}
+
/// Copy a callback into a typval_T.
void callback_put(Callback *cb, typval_T *tv)
FUNC_ATTR_NONNULL_ALL
@@ -1164,6 +1175,9 @@ void callback_put(Callback *cb, typval_T *tv)
tv->vval.v_string = vim_strsave(cb->data.funcref);
func_ref(cb->data.funcref);
break;
+ case kCallbackLua:
+ // TODO(tjdevries): I'm not even sure if this is strictly necessary?
+ abort();
default:
tv->v_type = VAR_SPECIAL;
tv->vval.v_special = kSpecialVarNull;
@@ -1185,6 +1199,9 @@ void callback_copy(Callback *dest, Callback *src)
dest->data.funcref = vim_strsave(src->data.funcref);
func_ref(src->data.funcref);
break;
+ case kCallbackLua:
+ dest->data.luaref = api_new_luaref(src->data.luaref);
+ break;
default:
dest->data.funcref = NULL;
break;
diff --git a/src/nvim/eval/typval.h b/src/nvim/eval/typval.h
index ad01c01499..40dc819754 100644
--- a/src/nvim/eval/typval.h
+++ b/src/nvim/eval/typval.h
@@ -72,15 +72,18 @@ typedef enum {
kCallbackNone = 0,
kCallbackFuncref,
kCallbackPartial,
+ kCallbackLua,
} CallbackType;
typedef struct {
union {
char_u *funcref;
partial_T *partial;
+ LuaRef luaref;
} data;
CallbackType type;
} Callback;
+
#define CALLBACK_INIT { .type = kCallbackNone }
#define CALLBACK_NONE ((Callback)CALLBACK_INIT)
diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c
index 49bf9972b1..c7910e148d 100644
--- a/src/nvim/ex_cmds.c
+++ b/src/nvim/ex_cmds.c
@@ -1943,7 +1943,7 @@ int do_write(exarg_T *eap)
// If 'filetype' was empty try detecting it now.
if (*curbuf->b_p_ft == NUL) {
- if (au_has_group((char_u *)"filetypedetect")) {
+ if (augroup_exists("filetypedetect")) {
(void)do_doautocmd((char_u *)"filetypedetect BufRead", true, NULL);
}
do_modelines(0);
diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h
index eaf5f627b6..92675499be 100644
--- a/src/nvim/ex_cmds_defs.h
+++ b/src/nvim/ex_cmds_defs.h
@@ -4,6 +4,7 @@
#include <stdbool.h>
#include <stdint.h>
+#include "nvim/eval/typval.h"
#include "nvim/normal.h"
#include "nvim/pos.h" // for linenr_T
#include "nvim/regexp_defs.h"
@@ -91,6 +92,35 @@ typedef struct exarg exarg_T;
typedef void (*ex_func_T)(exarg_T *eap);
+// NOTE: These possible could be removed and changed so that
+// Callback could take a "command" style string, and simply
+// execute that (instead of it being a function).
+//
+// But it's still a bit weird to do that.
+//
+// Another option would be that we just make a callback reference to
+// "execute($INPUT)" or something like that, so whatever the user
+// sends in via autocmds is just executed via this.
+//
+// However, that would probably have some performance cost (probably
+// very marginal, but still some cost either way).
+typedef enum {
+ CALLABLE_NONE,
+ CALLABLE_EX,
+ CALLABLE_CB,
+} AucmdExecutableType;
+
+typedef struct aucmd_executable_t AucmdExecutable;
+struct aucmd_executable_t {
+ AucmdExecutableType type;
+ union {
+ char_u *cmd;
+ Callback cb;
+ } callable;
+};
+
+#define AUCMD_EXECUTABLE_INIT { .type = CALLABLE_NONE }
+
typedef char_u *(*LineGetter)(int, void *, int, bool);
/// Structure for command definition.
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index 48749afcb3..152a41c407 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -5802,6 +5802,30 @@ static void ex_delcommand(exarg_T *eap)
}
}
+/// Split a string by unescaped whitespace (space & tab), used for f-args on Lua commands callback.
+/// Similar to uc_split_args(), but does not allocate, add quotes, add commas and is an iterator.
+///
+/// @note If no separator is found start = 0 and end = length - 1
+/// @param[in] arg String to split
+/// @param[in] iter Iteration counter
+/// @param[out] start Start of the split
+/// @param[out] end End of the split
+/// @param[in] length Length of the string
+/// @return false if it's the last split (don't call again), true otherwise (call again).
+bool uc_split_args_iter(const char_u *arg, int iter, int *start, int *end, int length)
+{
+ int pos;
+ *start = *end + (iter > 1 ? 2 : 0); // Skip whitespace after the first split
+ for (pos = *start; pos < length - 2; pos++) {
+ if (arg[pos] != '\\' && ascii_iswhite(arg[pos + 1])) {
+ *end = pos;
+ return true;
+ }
+ }
+ *end = length - 1;
+ return false;
+}
+
/*
* split and quote args for <f-args>
*/
diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c
index ed4475eb1a..30287cd6f2 100644
--- a/src/nvim/ex_getln.c
+++ b/src/nvim/ex_getln.c
@@ -5049,8 +5049,8 @@ static int ExpandFromContext(expand_T *xp, char_u *pat, int *num_file, char_u **
{ EXPAND_SYNTAX, get_syntax_name, true, true },
{ EXPAND_SYNTIME, get_syntime_arg, true, true },
{ EXPAND_HIGHLIGHT, (ExpandFunc)get_highlight_name, true, true },
- { EXPAND_EVENTS, get_event_name, true, true },
- { EXPAND_AUGROUP, get_augroup_name, true, true },
+ { EXPAND_EVENTS, expand_get_event_name, true, true },
+ { EXPAND_AUGROUP, expand_get_augroup_name, true, true },
{ EXPAND_CSCOPE, get_cscope_name, true, true },
{ EXPAND_SIGN, get_sign_name, true, true },
{ EXPAND_PROFILE, get_profile_name, true, true },
diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c
index f5824d83be..965aa8749d 100644
--- a/src/nvim/fileio.c
+++ b/src/nvim/fileio.c
@@ -3796,7 +3796,7 @@ static int set_rw_fname(char_u *fname, char_u *sfname)
// Do filetype detection now if 'filetype' is empty.
if (*curbuf->b_p_ft == NUL) {
- if (au_has_group((char_u *)"filetypedetect")) {
+ if (augroup_exists("filetypedetect")) {
(void)do_doautocmd((char_u *)"filetypedetect BufRead", false, NULL);
}
do_modelines(0);
diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c
index 34cde9a7c4..f249cde9a0 100644
--- a/src/nvim/getchar.c
+++ b/src/nvim/getchar.c
@@ -1710,6 +1710,15 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth)
int keylen = *keylenp;
int i;
int local_State = get_real_state();
+ bool is_plug_map = false;
+
+ // Check if typehead starts with a <Plug> mapping.
+ // In that case we will ignore nore flag on it.
+ if (typebuf.tb_buf[typebuf.tb_off] == K_SPECIAL
+ && typebuf.tb_buf[typebuf.tb_off+1] == KS_EXTRA
+ && typebuf.tb_buf[typebuf.tb_off+2] == KE_PLUG) {
+ is_plug_map = true;
+ }
// Check for a mappable key sequence.
// Walk through one maphash[] list until we find an entry that matches.
@@ -1725,7 +1734,7 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth)
tb_c1 = typebuf.tb_buf[typebuf.tb_off];
if (no_mapping == 0 && maphash_valid
&& (no_zero_mapping == 0 || tb_c1 != '0')
- && (typebuf.tb_maplen == 0
+ && (typebuf.tb_maplen == 0 || is_plug_map
|| (p_remap
&& !(typebuf.tb_noremap[typebuf.tb_off] & (RM_NONE|RM_ABBR))))
&& !(p_paste && (State & (INSERT + CMDLINE)))
@@ -1813,7 +1822,7 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth)
break;
}
}
- if (n >= 0) {
+ if (!is_plug_map && n >= 0) {
continue;
}
@@ -1949,8 +1958,11 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth)
// expression. Also save and restore the command line
// for "normal :".
if (mp->m_expr) {
- int save_vgetc_busy = vgetc_busy;
+ const int save_vgetc_busy = vgetc_busy;
const bool save_may_garbage_collect = may_garbage_collect;
+ const int save_cursor_row = ui_current_row();
+ const int save_cursor_col = ui_current_col();
+ const int prev_did_emsg = did_emsg;
vgetc_busy = 0;
may_garbage_collect = false;
@@ -1960,6 +1972,32 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth)
save_m_str = vim_strsave(mp->m_str);
}
map_str = eval_map_expr(mp, NUL);
+
+ // The mapping may do anything, but we expect it to take care of
+ // redrawing. Do put the cursor back where it was.
+ ui_cursor_goto(save_cursor_row, save_cursor_col);
+ ui_flush();
+
+ // If an error was displayed and the expression returns an empty
+ // string, generate a <Nop> to allow for a redraw.
+ if (prev_did_emsg != did_emsg && (map_str == NULL || *map_str == NUL)) {
+ char_u buf[4];
+ xfree(map_str);
+ buf[0] = K_SPECIAL;
+ buf[1] = KS_EXTRA;
+ buf[2] = KE_IGNORE;
+ buf[3] = NUL;
+ map_str = vim_strsave(buf);
+ if (State & CMDLINE) {
+ // redraw the command below the error
+ msg_didout = true;
+ if (msg_row < cmdline_row) {
+ msg_row = cmdline_row;
+ }
+ redrawcmd();
+ }
+ }
+
vgetc_busy = save_vgetc_busy;
may_garbage_collect = save_may_garbage_collect;
} else {
@@ -3490,6 +3528,7 @@ static void showmap(mapblock_T *mp, bool local)
if (p_verbose > 0) {
last_set_msg(mp->m_script_ctx);
}
+ msg_clr_eos();
ui_flush(); // show one line at a time
}
diff --git a/src/nvim/globals.h b/src/nvim/globals.h
index 5aa564623f..35ad57906b 100644
--- a/src/nvim/globals.h
+++ b/src/nvim/globals.h
@@ -326,16 +326,16 @@ EXTERN int want_garbage_collect INIT(= false);
EXTERN int garbage_collect_at_exit INIT(= false);
// Special values for current_SID.
-#define SID_MODELINE -1 // when using a modeline
-#define SID_CMDARG -2 // for "--cmd" argument
-#define SID_CARG -3 // for "-c" argument
-#define SID_ENV -4 // for sourcing environment variable
-#define SID_ERROR -5 // option was reset because of an error
-#define SID_NONE -6 // don't set scriptID
-#define SID_WINLAYOUT -7 // changing window size
-#define SID_LUA -8 // for Lua scripts/chunks
-#define SID_API_CLIENT -9 // for API clients
-#define SID_STR -10 // for sourcing a string with no script item
+#define SID_MODELINE (-1) // when using a modeline
+#define SID_CMDARG (-2) // for "--cmd" argument
+#define SID_CARG (-3) // for "-c" argument
+#define SID_ENV (-4) // for sourcing environment variable
+#define SID_ERROR (-5) // option was reset because of an error
+#define SID_NONE (-6) // don't set scriptID
+#define SID_WINLAYOUT (-7) // changing window size
+#define SID_LUA (-8) // for Lua scripts/chunks
+#define SID_API_CLIENT (-9) // for API clients
+#define SID_STR (-10) // for sourcing a string with no script item
// Script CTX being sourced or was sourced to define the current function.
EXTERN sctx_T current_sctx INIT(= { 0 COMMA 0 COMMA 0 });
diff --git a/src/nvim/hardcopy.c b/src/nvim/hardcopy.c
index 6fc70144ac..eb10c65be9 100644
--- a/src/nvim/hardcopy.c
+++ b/src/nvim/hardcopy.c
@@ -386,30 +386,43 @@ static uint32_t prt_get_term_color(int colorindex)
return cterm_color_8[colorindex % 8];
}
-static void prt_get_attr(int hl_id, prt_text_attr_T *pattr, int modec)
+static uint32_t prt_get_color(int hl_id, int modec)
{
int colorindex;
uint32_t fg_color;
+ const char *color = highlight_color(hl_id, "fg#", 'g');
+ if (color != NULL) {
+ RgbValue rgb = name_to_color(color);
+ if (rgb != -1) {
+ return (uint32_t)rgb;
+ }
+ }
+
+ color = highlight_color(hl_id, "fg", modec);
+ if (color == NULL) {
+ colorindex = 0;
+ } else {
+ colorindex = atoi(color);
+ }
+
+ if (colorindex >= 0 && colorindex < t_colors) {
+ fg_color = prt_get_term_color(colorindex);
+ } else {
+ fg_color = PRCOLOR_BLACK;
+ }
+
+ return fg_color;
+}
+
+static void prt_get_attr(int hl_id, prt_text_attr_T *pattr, int modec)
+{
pattr->bold = (highlight_has_attr(hl_id, HL_BOLD, modec) != NULL);
pattr->italic = (highlight_has_attr(hl_id, HL_ITALIC, modec) != NULL);
pattr->underline = (highlight_has_attr(hl_id, HL_UNDERLINE, modec) != NULL);
pattr->undercurl = (highlight_has_attr(hl_id, HL_UNDERCURL, modec) != NULL);
- {
- const char *color = highlight_color(hl_id, "fg", modec);
- if (color == NULL) {
- colorindex = 0;
- } else {
- colorindex = atoi(color);
- }
-
- if (colorindex >= 0 && colorindex < t_colors) {
- fg_color = prt_get_term_color(colorindex);
- } else {
- fg_color = PRCOLOR_BLACK;
- }
- }
+ uint32_t fg_color = prt_get_color(hl_id, modec);
if (fg_color == PRCOLOR_WHITE) {
fg_color = PRCOLOR_BLACK;
diff --git a/src/nvim/keymap.h b/src/nvim/keymap.h
index ae2ec7835e..4bf0617dfa 100644
--- a/src/nvim/keymap.h
+++ b/src/nvim/keymap.h
@@ -245,6 +245,7 @@ enum key_extra {
KE_EVENT = 102, // event
KE_LUA = 103, // lua special key
KE_COMMAND = 104, // <Cmd> special key
+ KE_AUCMD_SPECIAL = 105,
};
/*
@@ -443,6 +444,8 @@ enum key_extra {
#define K_COMMAND TERMCAP2KEY(KS_EXTRA, KE_COMMAND)
#define K_LUA TERMCAP2KEY(KS_EXTRA, KE_LUA)
+#define K_AUCMD_SPECIAL TERMCAP2KEY(KS_EXTRA, KE_AUCMD_SPECIAL)
+
// Bits for modifier mask
// 0x01 cannot be used, because the modifier must be 0x02 or higher
#define MOD_MASK_SHIFT 0x02
diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c
index 0fbd56ed53..2b54e56df1 100644
--- a/src/nvim/lua/converter.c
+++ b/src/nvim/lua/converter.c
@@ -156,7 +156,7 @@ static LuaTableProps nlua_traverse_table(lua_State *const lstate)
&& ret.string_keys_num == 0)) {
ret.type = kObjectTypeArray;
if (tsize == 0 && lua_getmetatable(lstate, -1)) {
- nlua_pushref(lstate, nlua_empty_dict_ref);
+ nlua_pushref(lstate, nlua_global_refs->empty_dict_ref);
if (lua_rawequal(lstate, -2, -1)) {
ret.type = kObjectTypeDictionary;
}
@@ -316,7 +316,7 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv)
LuaRef table_ref = LUA_NOREF;
if (lua_getmetatable(lstate, -1)) {
lua_pop(lstate, 1);
- table_ref = nlua_ref(lstate, -1);
+ table_ref = nlua_ref_global(lstate, -1);
}
const LuaTableProps table_props = nlua_traverse_table(lstate);
@@ -389,7 +389,7 @@ nlua_pop_typval_table_processing_end:
}
case LUA_TFUNCTION: {
LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState));
- state->lua_callable.func_ref = nlua_ref(lstate, -1);
+ state->lua_callable.func_ref = nlua_ref_global(lstate, -1);
char_u *name = register_cfunc(&nlua_CFunction_func_call,
&nlua_CFunction_func_free,
@@ -401,7 +401,7 @@ nlua_pop_typval_table_processing_end:
}
case LUA_TUSERDATA: {
// TODO(bfredl): check mt.__call and convert to function?
- nlua_pushref(lstate, nlua_nil_ref);
+ nlua_pushref(lstate, nlua_global_refs->nil_ref);
bool is_nil = lua_rawequal(lstate, -2, -1);
lua_pop(lstate, 1);
if (is_nil) {
@@ -445,7 +445,7 @@ static bool typval_conv_special = false;
if (typval_conv_special) { \
lua_pushnil(lstate); \
} else { \
- nlua_pushref(lstate, nlua_nil_ref); \
+ nlua_pushref(lstate, nlua_global_refs->nil_ref); \
} \
} while (0)
@@ -495,7 +495,7 @@ static bool typval_conv_special = false;
nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary); \
} else { \
lua_createtable(lstate, 0, 0); \
- nlua_pushref(lstate, nlua_empty_dict_ref); \
+ nlua_pushref(lstate, nlua_global_refs->empty_dict_ref); \
lua_setmetatable(lstate, -2); \
} \
} while (0)
@@ -734,7 +734,7 @@ void nlua_push_Dictionary(lua_State *lstate, const Dictionary dict, bool special
} else {
lua_createtable(lstate, 0, (int)dict.size);
if (dict.size == 0 && !special) {
- nlua_pushref(lstate, nlua_empty_dict_ref);
+ nlua_pushref(lstate, nlua_global_refs->empty_dict_ref);
lua_setmetatable(lstate, -2);
}
}
@@ -782,7 +782,7 @@ void nlua_push_Object(lua_State *lstate, const Object obj, bool special)
if (special) {
lua_pushnil(lstate);
} else {
- nlua_pushref(lstate, nlua_nil_ref);
+ nlua_pushref(lstate, nlua_global_refs->nil_ref);
}
break;
case kObjectTypeLuaRef: {
@@ -1199,14 +1199,14 @@ Object nlua_pop_Object(lua_State *const lstate, bool ref, Error *const err)
case LUA_TFUNCTION:
if (ref) {
- *cur.obj = LUAREF_OBJ(nlua_ref(lstate, -1));
+ *cur.obj = LUAREF_OBJ(nlua_ref_global(lstate, -1));
} else {
goto type_error;
}
break;
case LUA_TUSERDATA: {
- nlua_pushref(lstate, nlua_nil_ref);
+ nlua_pushref(lstate, nlua_global_refs->nil_ref);
bool is_nil = lua_rawequal(lstate, -2, -1);
lua_pop(lstate, 1);
if (is_nil) {
@@ -1240,7 +1240,7 @@ type_error:
LuaRef nlua_pop_LuaRef(lua_State *const lstate, Error *err)
{
- LuaRef rv = nlua_ref(lstate, -1);
+ LuaRef rv = nlua_ref_global(lstate, -1);
lua_pop(lstate, 1);
return rv;
}
diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c
index 029e7eb660..18abf04ff6 100644
--- a/src/nvim/lua/executor.c
+++ b/src/nvim/lua/executor.c
@@ -45,6 +45,8 @@ static int in_fast_callback = 0;
// Initialized in nlua_init().
static lua_State *global_lstate = NULL;
+static uv_thread_t main_thread;
+
typedef struct {
Error err;
String lua_err_str;
@@ -65,11 +67,16 @@ typedef struct {
}
#if __has_feature(address_sanitizer)
-static PMap(handle_T) nlua_ref_markers = MAP_INIT;
static bool nlua_track_refs = false;
# define NLUA_TRACK_REFS
#endif
+typedef enum luv_err_type {
+ kCallback,
+ kThread,
+ kThreadCallback,
+} luv_err_t;
+
/// Convert lua error into a Vim error message
///
/// @param lstate Lua interpreter state.
@@ -122,8 +129,21 @@ static int nlua_nvim_version(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
static void nlua_luv_error_event(void **argv)
{
char *error = (char *)argv[0];
+ luv_err_t type = (luv_err_t)(intptr_t)argv[1];
msg_ext_set_kind("lua_error");
- semsg_multiline("Error executing luv callback:\n%s", error);
+ switch (type) {
+ case kCallback:
+ semsg_multiline("Error executing luv callback:\n%s", error);
+ break;
+ case kThread:
+ semsg_multiline("Error in luv thread:\n%s", error);
+ break;
+ case kThreadCallback:
+ semsg_multiline("Error in luv callback, thread:\n%s", error);
+ break;
+ default:
+ break;
+ }
xfree(error);
}
@@ -148,7 +168,7 @@ static int nlua_luv_cfpcall(lua_State *lstate, int nargs, int nresult, int flags
const char *error = lua_tostring(lstate, -1);
multiqueue_put(main_loop.events, nlua_luv_error_event,
- 1, xstrdup(error));
+ 2, xstrdup(error), (intptr_t)kCallback);
lua_pop(lstate, 1); // error message
retval = -status;
} else { // LUA_OK
@@ -162,12 +182,112 @@ static int nlua_luv_cfpcall(lua_State *lstate, int nargs, int nresult, int flags
return retval;
}
+static int nlua_luv_thread_cb_cfpcall(lua_State *lstate, int nargs, int nresult,
+ int flags)
+{
+ return nlua_luv_thread_common_cfpcall(lstate, nargs, nresult, flags, true);
+}
+
+static int nlua_luv_thread_cfpcall(lua_State *lstate, int nargs, int nresult,
+ int flags)
+ FUNC_ATTR_NONNULL_ALL
+{
+ return nlua_luv_thread_common_cfpcall(lstate, nargs, nresult, flags, false);
+}
+
+static int nlua_luv_thread_cfcpcall(lua_State *lstate, lua_CFunction func,
+ void *ud, int flags)
+ FUNC_ATTR_NONNULL_ARG(1, 2)
+{
+ lua_pushcfunction(lstate, func);
+ lua_pushlightuserdata(lstate, ud);
+ int retval = nlua_luv_thread_cfpcall(lstate, 1, 0, flags);
+ return retval;
+}
+
+static int nlua_luv_thread_common_cfpcall(lua_State *lstate, int nargs, int nresult,
+ int flags, bool is_callback)
+ FUNC_ATTR_NONNULL_ALL
+{
+ int retval;
+
+ int top = lua_gettop(lstate);
+ int status = lua_pcall(lstate, nargs, nresult, 0);
+ if (status) {
+ if (status == LUA_ERRMEM && !(flags & LUVF_CALLBACK_NOEXIT)) {
+ // Terminate this thread, as the main thread may be able to continue
+ // execution.
+ mch_errmsg(e_outofmem);
+ mch_errmsg("\n");
+ lua_close(lstate);
+#ifdef WIN32
+ ExitThread(0);
+#else
+ pthread_exit(0);
+#endif
+ }
+ const char *error = lua_tostring(lstate, -1);
+
+ loop_schedule_deferred(&main_loop,
+ event_create(nlua_luv_error_event, 2,
+ xstrdup(error),
+ is_callback
+ ? (intptr_t)kThreadCallback
+ : (intptr_t)kThread));
+ lua_pop(lstate, 1); // error message
+ retval = -status;
+ } else { // LUA_OK
+ if (nresult == LUA_MULTRET) {
+ nresult = lua_gettop(lstate) - top + nargs + 1;
+ }
+ retval = nresult;
+ }
+
+ return retval;
+}
+
+static int nlua_thr_api_nvim__get_runtime(lua_State *lstate)
+{
+ if (lua_gettop(lstate) != 3) {
+ return luaL_error(lstate, "Expected 3 arguments");
+ }
+
+ luaL_checktype(lstate, -1, LUA_TTABLE);
+ lua_getfield(lstate, -1, "is_lua");
+ if (!lua_isboolean(lstate, -1)) {
+ return luaL_error(lstate, "is_lua is not a boolean");
+ }
+ bool is_lua = lua_toboolean(lstate, -1);
+ lua_pop(lstate, 2);
+
+ luaL_checktype(lstate, -1, LUA_TBOOLEAN);
+ bool all = lua_toboolean(lstate, -1);
+ lua_pop(lstate, 1);
+
+ Error err = ERROR_INIT;
+ const Array pat = nlua_pop_Array(lstate, &err);
+ if (ERROR_SET(&err)) {
+ luaL_where(lstate, 1);
+ lua_pushstring(lstate, err.msg);
+ api_clear_error(&err);
+ lua_concat(lstate, 2);
+ return lua_error(lstate);
+ }
+
+ ArrayOf(String) ret = runtime_get_named_thread(is_lua, pat, all);
+ nlua_push_Array(lstate, ret, true);
+ api_free_array(ret);
+ api_free_array(pat);
+
+ return 1;
+}
+
static void nlua_schedule_event(void **argv)
{
LuaRef cb = (LuaRef)(ptrdiff_t)argv[0];
lua_State *const lstate = global_lstate;
nlua_pushref(lstate, cb);
- nlua_unref(lstate, cb);
+ nlua_unref_global(lstate, cb);
if (nlua_pcall(lstate, 0, 0)) {
nlua_error(lstate, _("Error executing vim.schedule lua callback: %.*s"));
}
@@ -184,7 +304,7 @@ static int nlua_schedule(lua_State *const lstate)
return lua_error(lstate);
}
- LuaRef cb = nlua_ref(lstate, 1);
+ LuaRef cb = nlua_ref_global(lstate, 1);
multiqueue_put(main_loop.events, nlua_schedule_event,
1, (void *)(ptrdiff_t)cb);
@@ -302,6 +422,152 @@ static int nlua_wait(lua_State *lstate)
return 2;
}
+static nlua_ref_state_t *nlua_new_ref_state(lua_State *lstate, bool is_thread)
+ FUNC_ATTR_NONNULL_ALL
+{
+ nlua_ref_state_t *ref_state = lua_newuserdata(lstate, sizeof(*ref_state));
+ memset(ref_state, 0, sizeof(*ref_state));
+ ref_state->nil_ref = LUA_NOREF;
+ ref_state->empty_dict_ref = LUA_NOREF;
+ if (!is_thread) {
+ nlua_global_refs = ref_state;
+ }
+ return ref_state;
+}
+
+static nlua_ref_state_t *nlua_get_ref_state(lua_State *lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_getfield(lstate, LUA_REGISTRYINDEX, "nlua.ref_state");
+ nlua_ref_state_t *ref_state = lua_touserdata(lstate, -1);
+ lua_pop(lstate, 1);
+
+ return ref_state;
+}
+
+LuaRef nlua_get_nil_ref(lua_State *lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ nlua_ref_state_t *ref_state = nlua_get_ref_state(lstate);
+ return ref_state->nil_ref;
+}
+
+LuaRef nlua_get_empty_dict_ref(lua_State *lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ nlua_ref_state_t *ref_state = nlua_get_ref_state(lstate);
+ return ref_state->empty_dict_ref;
+}
+
+int nlua_get_global_ref_count(void)
+{
+ return nlua_global_refs->ref_count;
+}
+
+static void nlua_common_vim_init(lua_State *lstate, bool is_thread)
+ FUNC_ATTR_NONNULL_ARG(1)
+{
+ nlua_ref_state_t *ref_state = nlua_new_ref_state(lstate, is_thread);
+ lua_setfield(lstate, LUA_REGISTRYINDEX, "nlua.ref_state");
+
+ // vim.is_thread
+ lua_pushboolean(lstate, is_thread);
+ lua_setfield(lstate, LUA_REGISTRYINDEX, "nvim.thread");
+ lua_pushcfunction(lstate, &nlua_is_thread);
+ lua_setfield(lstate, -2, "is_thread");
+
+ // vim.NIL
+ lua_newuserdata(lstate, 0);
+ lua_createtable(lstate, 0, 0);
+ lua_pushcfunction(lstate, &nlua_nil_tostring);
+ lua_setfield(lstate, -2, "__tostring");
+ lua_setmetatable(lstate, -2);
+ ref_state->nil_ref = nlua_ref(lstate, ref_state, -1);
+ lua_pushvalue(lstate, -1);
+ lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.NIL");
+ lua_setfield(lstate, -2, "NIL");
+
+ // vim._empty_dict_mt
+ lua_createtable(lstate, 0, 0);
+ lua_pushcfunction(lstate, &nlua_empty_dict_tostring);
+ lua_setfield(lstate, -2, "__tostring");
+ ref_state->empty_dict_ref = nlua_ref(lstate, ref_state, -1);
+ lua_pushvalue(lstate, -1);
+ lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.empty_dict");
+ lua_setfield(lstate, -2, "_empty_dict_mt");
+
+ // vim.loop
+ if (is_thread) {
+ luv_set_callback(lstate, nlua_luv_thread_cb_cfpcall);
+ luv_set_thread(lstate, nlua_luv_thread_cfpcall);
+ luv_set_cthread(lstate, nlua_luv_thread_cfcpcall);
+ } else {
+ luv_set_loop(lstate, &main_loop.uv);
+ luv_set_callback(lstate, nlua_luv_cfpcall);
+ }
+ luaopen_luv(lstate);
+ lua_pushvalue(lstate, -1);
+ lua_setfield(lstate, -3, "loop");
+
+ // package.loaded.luv = vim.loop
+ // otherwise luv will be reinitialized when require'luv'
+ lua_getglobal(lstate, "package");
+ lua_getfield(lstate, -1, "loaded");
+ lua_pushvalue(lstate, -3);
+ lua_setfield(lstate, -2, "luv");
+ lua_pop(lstate, 3);
+}
+
+static void nlua_common_package_init(lua_State *lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ {
+ const char *code = (char *)&shared_module[0];
+ if (luaL_loadbuffer(lstate, code, sizeof(shared_module) - 1, "@vim/shared.lua")
+ || nlua_pcall(lstate, 0, 0)) {
+ nlua_error(lstate, _("E5106: Error while creating shared module: %.*s\n"));
+ return;
+ }
+ }
+
+ {
+ const char *code = (char *)&lua_load_package_module[0];
+ if (luaL_loadbuffer(lstate, code, sizeof(lua_load_package_module) - 1, "@vim/_load_package.lua")
+ || lua_pcall(lstate, 0, 0, 0)) {
+ nlua_error(lstate, _("E5106: Error while creating _load_package module: %.*s"));
+ return;
+ }
+ }
+
+ {
+ lua_getglobal(lstate, "package"); // [package]
+ lua_getfield(lstate, -1, "loaded"); // [package, loaded]
+
+ const char *code = (char *)&inspect_module[0];
+ if (luaL_loadbuffer(lstate, code, sizeof(inspect_module) - 1, "@vim/inspect.lua")
+ || nlua_pcall(lstate, 0, 1)) {
+ nlua_error(lstate, _("E5106: Error while creating inspect module: %.*s\n"));
+ return;
+ }
+
+ // [package, loaded, inspect]
+ lua_setfield(lstate, -2, "vim.inspect"); // [package, loaded]
+ }
+
+ {
+ const char *code = (char *)&lua_F_module[0];
+ if (luaL_loadbuffer(lstate, code, sizeof(lua_F_module) - 1, "@vim/F.lua")
+ || nlua_pcall(lstate, 0, 1)) {
+ nlua_error(lstate, _("E5106: Error while creating vim.F module: %.*s\n"));
+ return;
+ }
+ // [package, loaded, module]
+ lua_setfield(lstate, -2, "vim.F"); // [package, loaded]
+
+ lua_pop(lstate, 2); // []
+ }
+}
+
/// Initialize lua interpreter state
///
/// Called by lua interpreter itself to initialize state.
@@ -362,80 +628,22 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
lua_pushcfunction(lstate, &nlua_wait);
lua_setfield(lstate, -2, "wait");
- // vim.NIL
- lua_newuserdata(lstate, 0);
- lua_createtable(lstate, 0, 0);
- lua_pushcfunction(lstate, &nlua_nil_tostring);
- lua_setfield(lstate, -2, "__tostring");
- lua_setmetatable(lstate, -2);
- nlua_nil_ref = nlua_ref(lstate, -1);
- lua_pushvalue(lstate, -1);
- lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.NIL");
- lua_setfield(lstate, -2, "NIL");
-
- // vim._empty_dict_mt
- lua_createtable(lstate, 0, 0);
- lua_pushcfunction(lstate, &nlua_empty_dict_tostring);
- lua_setfield(lstate, -2, "__tostring");
- nlua_empty_dict_ref = nlua_ref(lstate, -1);
- lua_pushvalue(lstate, -1);
- lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.empty_dict");
- lua_setfield(lstate, -2, "_empty_dict_mt");
+ nlua_common_vim_init(lstate, false);
// internal vim._treesitter... API
nlua_add_treesitter(lstate);
- // vim.loop
- luv_set_loop(lstate, &main_loop.uv);
- luv_set_callback(lstate, nlua_luv_cfpcall);
- luaopen_luv(lstate);
- lua_pushvalue(lstate, -1);
- lua_setfield(lstate, -3, "loop");
-
- // package.loaded.luv = vim.loop
- // otherwise luv will be reinitialized when require'luv'
- lua_getglobal(lstate, "package");
- lua_getfield(lstate, -1, "loaded");
- lua_pushvalue(lstate, -3);
- lua_setfield(lstate, -2, "luv");
- lua_pop(lstate, 3);
-
- nlua_state_add_stdlib(lstate);
+ nlua_state_add_stdlib(lstate, false);
lua_setglobal(lstate, "vim");
- {
- const char *code = (char *)&shared_module[0];
- if (luaL_loadbuffer(lstate, code, sizeof(shared_module) - 1, "@vim/shared.lua")
- || nlua_pcall(lstate, 0, 0)) {
- nlua_error(lstate, _("E5106: Error while creating shared module: %.*s\n"));
- return 1;
- }
- }
+ nlua_common_package_init(lstate);
{
lua_getglobal(lstate, "package"); // [package]
lua_getfield(lstate, -1, "loaded"); // [package, loaded]
- const char *code = (char *)&inspect_module[0];
- if (luaL_loadbuffer(lstate, code, sizeof(inspect_module) - 1, "@vim/inspect.lua")
- || nlua_pcall(lstate, 0, 1)) {
- nlua_error(lstate, _("E5106: Error while creating inspect module: %.*s\n"));
- return 1;
- }
- // [package, loaded, inspect]
- lua_setfield(lstate, -2, "vim.inspect"); // [package, loaded]
-
- code = (char *)&lua_F_module[0];
- if (luaL_loadbuffer(lstate, code, sizeof(lua_F_module) - 1, "@vim/F.lua")
- || nlua_pcall(lstate, 0, 1)) {
- nlua_error(lstate, _("E5106: Error while creating vim.F module: %.*s\n"));
- return 1;
- }
- // [package, loaded, module]
- lua_setfield(lstate, -2, "vim.F"); // [package, loaded]
-
- code = (char *)&lua_filetype_module[0];
+ char *code = (char *)&lua_filetype_module[0];
if (luaL_loadbuffer(lstate, code, sizeof(lua_filetype_module) - 1, "@vim/filetype.lua")
|| nlua_pcall(lstate, 0, 1)) {
nlua_error(lstate, _("E5106: Error while creating vim.filetype module: %.*s"));
@@ -495,9 +703,60 @@ void nlua_init(void)
luaL_openlibs(lstate);
nlua_state_init(lstate);
+ luv_set_thread_cb(nlua_thread_acquire_vm, nlua_common_free_all_mem);
+
global_lstate = lstate;
+
+ main_thread = uv_thread_self();
}
+static lua_State *nlua_thread_acquire_vm(void)
+{
+ // If it is called from the main thread, it will attempt to rebuild the cache.
+ const uv_thread_t self = uv_thread_self();
+ if (uv_thread_equal(&main_thread, &self)) {
+ runtime_search_path_validate();
+ }
+
+ lua_State *lstate = luaL_newstate();
+
+ // Add in the lua standard libraries
+ luaL_openlibs(lstate);
+
+ // print
+ lua_pushcfunction(lstate, &nlua_print);
+ lua_setglobal(lstate, "print");
+
+ lua_pushinteger(lstate, 0);
+ lua_setfield(lstate, LUA_REGISTRYINDEX, "nlua.refcount");
+
+ // vim
+ lua_newtable(lstate);
+
+ nlua_common_vim_init(lstate, true);
+
+ nlua_state_add_stdlib(lstate, true);
+
+ lua_setglobal(lstate, "vim");
+
+ nlua_common_package_init(lstate);
+
+ lua_getglobal(lstate, "vim");
+ lua_getglobal(lstate, "package");
+ lua_getfield(lstate, -1, "loaded");
+ lua_getfield(lstate, -1, "vim.inspect");
+ lua_setfield(lstate, -4, "inspect");
+ lua_pop(lstate, 3);
+
+ lua_getglobal(lstate, "vim");
+ lua_createtable(lstate, 0, 0);
+ lua_pushcfunction(lstate, nlua_thr_api_nvim__get_runtime);
+ lua_setfield(lstate, -2, "nvim__get_runtime");
+ lua_setfield(lstate, -2, "api");
+ lua_pop(lstate, 1);
+
+ return lstate;
+}
void nlua_free_all_mem(void)
{
@@ -505,26 +764,30 @@ void nlua_free_all_mem(void)
return;
}
lua_State *lstate = global_lstate;
+ nlua_common_free_all_mem(lstate);
+}
- nlua_unref(lstate, nlua_nil_ref);
- nlua_unref(lstate, nlua_empty_dict_ref);
+static void nlua_common_free_all_mem(lua_State *lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ nlua_ref_state_t *ref_state = nlua_get_ref_state(lstate);
+ nlua_unref(lstate, ref_state, ref_state->nil_ref);
+ nlua_unref(lstate, ref_state, ref_state->empty_dict_ref);
#ifdef NLUA_TRACK_REFS
- if (nlua_refcount) {
- fprintf(stderr, "%d lua references were leaked!", nlua_refcount);
+ if (ref_state->ref_count) {
+ fprintf(stderr, "%d lua references were leaked!", ref_state->ref_count);
}
if (nlua_track_refs) {
// in case there are leaked luarefs, leak the associated memory
// to get LeakSanitizer stacktraces on exit
- pmap_destroy(handle_T)(&nlua_ref_markers);
+ pmap_destroy(handle_T)(&ref_state->ref_markers);
}
#endif
- nlua_refcount = 0;
lua_close(lstate);
}
-
static void nlua_print_event(void **argv)
{
char *str = argv[0];
@@ -602,9 +865,18 @@ static int nlua_print(lua_State *const lstate)
#undef PRINT_ERROR
ga_append(&msg_ga, NUL);
- if (in_fast_callback) {
+ lua_getfield(lstate, LUA_REGISTRYINDEX, "nvim.thread");
+ bool is_thread = lua_toboolean(lstate, -1);
+ lua_pop(lstate, 1);
+
+ if (is_thread) {
+ loop_schedule_deferred(&main_loop,
+ event_create(nlua_print_event, 2,
+ msg_ga.ga_data,
+ (intptr_t)msg_ga.ga_len));
+ } else if (in_fast_callback) {
multiqueue_put(main_loop.events, nlua_print_event,
- 2, msg_ga.ga_data, msg_ga.ga_len);
+ 2, msg_ga.ga_data, (intptr_t)msg_ga.ga_len);
} else {
nlua_print_event((void *[]){ msg_ga.ga_data,
(void *)(intptr_t)msg_ga.ga_len });
@@ -613,10 +885,12 @@ static int nlua_print(lua_State *const lstate)
nlua_print_error:
ga_clear(&msg_ga);
+ char *buff = xmalloc(IOSIZE);
const char *fmt = _("E5114: Error while converting print argument #%i: %.*s");
- size_t len = (size_t)vim_snprintf((char *)IObuff, IOSIZE, fmt, curargidx,
+ size_t len = (size_t)vim_snprintf(buff, IOSIZE, fmt, curargidx,
(int)errmsg_len, errmsg);
- lua_pushlstring(lstate, (char *)IObuff, len);
+ lua_pushlstring(lstate, buff, len);
+ xfree(buff);
return lua_error(lstate);
}
@@ -806,40 +1080,53 @@ static int nlua_getenv(lua_State *lstate)
/// add the value to the registry
-LuaRef nlua_ref(lua_State *lstate, int index)
+/// The current implementation does not support calls from threads.
+LuaRef nlua_ref(lua_State *lstate, nlua_ref_state_t *ref_state, int index)
{
lua_pushvalue(lstate, index);
LuaRef ref = luaL_ref(lstate, LUA_REGISTRYINDEX);
if (ref > 0) {
- nlua_refcount++;
+ ref_state->ref_count++;
#ifdef NLUA_TRACK_REFS
if (nlua_track_refs) {
// dummy allocation to make LeakSanitizer track our luarefs
- pmap_put(handle_T)(&nlua_ref_markers, ref, xmalloc(3));
+ pmap_put(handle_T)(&ref_state->ref_markers, ref, xmalloc(3));
}
#endif
}
return ref;
}
+
+LuaRef nlua_ref_global(lua_State *lstate, int index)
+{
+ return nlua_ref(lstate, nlua_global_refs, index);
+}
+
/// remove the value from the registry
-void nlua_unref(lua_State *lstate, LuaRef ref)
+void nlua_unref(lua_State *lstate, nlua_ref_state_t *ref_state, LuaRef ref)
{
if (ref > 0) {
- nlua_refcount--;
+ ref_state->ref_count--;
#ifdef NLUA_TRACK_REFS
// NB: don't remove entry from map to track double-unref
if (nlua_track_refs) {
- xfree(pmap_get(handle_T)(&nlua_ref_markers, ref));
+ xfree(pmap_get(handle_T)(&ref_state->ref_markers, ref));
}
#endif
luaL_unref(lstate, LUA_REGISTRYINDEX, ref);
}
}
+void nlua_unref_global(lua_State *lstate, LuaRef ref)
+{
+ nlua_unref(lstate, nlua_global_refs, ref);
+}
+
+
void api_free_luaref(LuaRef ref)
{
- nlua_unref(global_lstate, ref);
+ nlua_unref_global(global_lstate, ref);
}
/// push a value referenced in the registry
@@ -861,7 +1148,7 @@ LuaRef api_new_luaref(LuaRef original_ref)
lua_State *const lstate = global_lstate;
nlua_pushref(lstate, original_ref);
- LuaRef new_ref = nlua_ref(lstate, -1);
+ LuaRef new_ref = nlua_ref_global(lstate, -1);
lua_pop(lstate, 1);
return new_ref;
}
@@ -1063,6 +1350,16 @@ Object nlua_exec(const String str, const Array args, Error *err)
return nlua_pop_Object(lstate, false, err);
}
+bool nlua_ref_is_function(LuaRef ref)
+{
+ lua_State *const lstate = global_lstate;
+ nlua_pushref(lstate, ref);
+ bool is_function = (lua_type(lstate, -1) == LUA_TFUNCTION);
+ lua_pop(lstate, 1);
+
+ return is_function;
+}
+
/// call a LuaRef as a function (or table with __call metamethod)
///
/// @param ref the reference to call (not consumed)
@@ -1391,6 +1688,13 @@ cleanup:
return ret;
}
+static int nlua_is_thread(lua_State *lstate)
+{
+ lua_getfield(lstate, LUA_REGISTRYINDEX, "nvim.thread");
+
+ return 1;
+}
+
// Required functions for lua c functions as VimL callbacks
int nlua_CFunction_func_call(int argcount, typval_T *argvars, typval_T *rettv, void *state)
@@ -1407,7 +1711,7 @@ void nlua_CFunction_func_free(void *state)
lua_State *const lstate = global_lstate;
LuaCFunctionState *funcstate = (LuaCFunctionState *)state;
- nlua_unref(lstate, funcstate->lua_callable.func_ref);
+ nlua_unref_global(lstate, funcstate->lua_callable.func_ref);
xfree(funcstate);
}
@@ -1457,7 +1761,7 @@ char_u *nlua_register_table_as_callable(typval_T *const arg)
lua_pop(lstate, 2); // [table]
LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState));
- state->lua_callable.func_ref = nlua_ref(lstate, -1);
+ state->lua_callable.func_ref = nlua_ref_global(lstate, -1);
char_u *name = register_cfunc(&nlua_CFunction_func_call,
&nlua_CFunction_func_free, state);
@@ -1520,8 +1824,31 @@ void nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap)
lua_pushinteger(lstate, eap->line2);
lua_setfield(lstate, -2, "line2");
+ lua_newtable(lstate); // f-args table
lua_pushstring(lstate, (const char *)eap->arg);
- lua_setfield(lstate, -2, "args");
+ lua_pushvalue(lstate, -1); // Reference for potential use on f-args
+ lua_setfield(lstate, -4, "args");
+
+ // Split args by unescaped whitespace |<f-args>| (nargs dependent)
+ if (cmd->uc_argt & EX_NOSPC) {
+ // Commands where nargs = 1 or "?" fargs is the same as args
+ lua_rawseti(lstate, -2, 1);
+ } else {
+ // Commands with more than one possible argument we split
+ lua_pop(lstate, 1); // Pop the reference of opts.args
+ int length = (int)STRLEN(eap->arg);
+ int start = 0;
+ int end = 0;
+ int i = 1;
+ bool res = true;
+ while (res) {
+ res = uc_split_args_iter(eap->arg, i, &start, &end, length);
+ lua_pushlstring(lstate, (const char *)eap->arg + start, (size_t)(end - start + 1));
+ lua_rawseti(lstate, -2, i);
+ i++;
+ }
+ }
+ lua_setfield(lstate, -2, "fargs");
lua_pushstring(lstate, (const char *)&eap->regname);
lua_setfield(lstate, -2, "reg");
diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h
index bf78f7ec5e..47ac51dadb 100644
--- a/src/nvim/lua/executor.h
+++ b/src/nvim/lua/executor.h
@@ -4,6 +4,7 @@
#include <lauxlib.h>
#include <lua.h>
+#include "nvim/assert.h"
#include "nvim/api/private/defs.h"
#include "nvim/eval/typval.h"
#include "nvim/ex_cmds_defs.h"
@@ -14,18 +15,14 @@
// Generated by msgpack-gen.lua
void nlua_add_api_functions(lua_State *lstate) REAL_FATTR_NONNULL_ALL;
-EXTERN LuaRef nlua_nil_ref INIT(= LUA_NOREF);
-EXTERN LuaRef nlua_empty_dict_ref INIT(= LUA_NOREF);
-
-EXTERN int nlua_refcount INIT(= 0);
-
-#define set_api_error(s, err) \
- do { \
- Error *err_ = (err); \
- err_->type = kErrorTypeException; \
- err_->set = true; \
- memcpy(&err_->msg[0], s, sizeof(s)); \
- } while (0)
+typedef struct {
+ LuaRef nil_ref;
+ LuaRef empty_dict_ref;
+ int ref_count;
+#if __has_feature(address_sanitizer)
+ PMap(handle_T) ref_markers;
+#endif
+} nlua_ref_state_t;
#define NLUA_CLEAR_REF(x) \
do { \
@@ -39,4 +36,7 @@ EXTERN int nlua_refcount INIT(= 0);
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "lua/executor.h.generated.h"
#endif
+
+EXTERN nlua_ref_state_t *nlua_global_refs INIT(= NULL);
+
#endif // NVIM_LUA_EXECUTOR_H
diff --git a/src/nvim/lua/stdlib.c b/src/nvim/lua/stdlib.c
index 55b23cf0c8..c2ce899a74 100644
--- a/src/nvim/lua/stdlib.c
+++ b/src/nvim/lua/stdlib.c
@@ -471,43 +471,52 @@ static int nlua_stricmp(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
}
-void nlua_state_add_stdlib(lua_State *const lstate)
+void nlua_state_add_stdlib(lua_State *const lstate, bool is_thread)
{
- // stricmp
- lua_pushcfunction(lstate, &nlua_stricmp);
- lua_setfield(lstate, -2, "stricmp");
- // str_utfindex
- lua_pushcfunction(lstate, &nlua_str_utfindex);
- lua_setfield(lstate, -2, "str_utfindex");
- // str_byteindex
- lua_pushcfunction(lstate, &nlua_str_byteindex);
- lua_setfield(lstate, -2, "str_byteindex");
- // str_utf_pos
- lua_pushcfunction(lstate, &nlua_str_utf_pos);
- lua_setfield(lstate, -2, "str_utf_pos");
- // str_utf_start
- lua_pushcfunction(lstate, &nlua_str_utf_start);
- lua_setfield(lstate, -2, "str_utf_start");
- // str_utf_end
- lua_pushcfunction(lstate, &nlua_str_utf_end);
- lua_setfield(lstate, -2, "str_utf_end");
- // regex
- lua_pushcfunction(lstate, &nlua_regex);
- lua_setfield(lstate, -2, "regex");
- luaL_newmetatable(lstate, "nvim_regex");
- luaL_register(lstate, NULL, regex_meta);
-
- lua_pushvalue(lstate, -1); // [meta, meta]
- lua_setfield(lstate, -2, "__index"); // [meta]
- lua_pop(lstate, 1); // don't use metatable now
-
- // _getvar
- lua_pushcfunction(lstate, &nlua_getvar);
- lua_setfield(lstate, -2, "_getvar");
-
- // _setvar
- lua_pushcfunction(lstate, &nlua_setvar);
- lua_setfield(lstate, -2, "_setvar");
+ if (!is_thread) {
+ // TODO(bfredl): some of basic string functions should already be
+ // (or be easy to make) threadsafe
+
+ // stricmp
+ lua_pushcfunction(lstate, &nlua_stricmp);
+ lua_setfield(lstate, -2, "stricmp");
+ // str_utfindex
+ lua_pushcfunction(lstate, &nlua_str_utfindex);
+ lua_setfield(lstate, -2, "str_utfindex");
+ // str_byteindex
+ lua_pushcfunction(lstate, &nlua_str_byteindex);
+ lua_setfield(lstate, -2, "str_byteindex");
+ // str_utf_pos
+ lua_pushcfunction(lstate, &nlua_str_utf_pos);
+ lua_setfield(lstate, -2, "str_utf_pos");
+ // str_utf_start
+ lua_pushcfunction(lstate, &nlua_str_utf_start);
+ lua_setfield(lstate, -2, "str_utf_start");
+ // str_utf_end
+ lua_pushcfunction(lstate, &nlua_str_utf_end);
+ lua_setfield(lstate, -2, "str_utf_end");
+ // regex
+ lua_pushcfunction(lstate, &nlua_regex);
+ lua_setfield(lstate, -2, "regex");
+ luaL_newmetatable(lstate, "nvim_regex");
+ luaL_register(lstate, NULL, regex_meta);
+
+ lua_pushvalue(lstate, -1); // [meta, meta]
+ lua_setfield(lstate, -2, "__index"); // [meta]
+ lua_pop(lstate, 1); // don't use metatable now
+
+ // _getvar
+ lua_pushcfunction(lstate, &nlua_getvar);
+ lua_setfield(lstate, -2, "_getvar");
+
+ // _setvar
+ lua_pushcfunction(lstate, &nlua_setvar);
+ lua_setfield(lstate, -2, "_setvar");
+
+ // vim.spell
+ luaopen_spell(lstate);
+ lua_setfield(lstate, -2, "spell");
+ }
// vim.mpack
luaopen_mpack(lstate);
@@ -526,10 +535,7 @@ void nlua_state_add_stdlib(lua_State *const lstate)
lua_pushcfunction(lstate, &nlua_xdl_diff);
lua_setfield(lstate, -2, "diff");
- // vim.spell
- luaopen_spell(lstate);
- lua_setfield(lstate, -2, "spell");
-
+ // vim.json
lua_cjson_new(lstate);
lua_setfield(lstate, -2, "json");
}
diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua
index f5993c3f55..68de4f960c 100644
--- a/src/nvim/lua/vim.lua
+++ b/src/nvim/lua/vim.lua
@@ -43,53 +43,6 @@ assert(vim.inspect)
vim.filetype = package.loaded['vim.filetype']
assert(vim.filetype)
-local pathtrails = {}
-vim._so_trails = {}
-for s in (package.cpath..';'):gmatch('[^;]*;') do
- s = s:sub(1, -2) -- Strip trailing semicolon
- -- Find out path patterns. pathtrail should contain something like
- -- /?.so, \?.dll. This allows not to bother determining what correct
- -- suffixes are.
- local pathtrail = s:match('[/\\][^/\\]*%?.*$')
- if pathtrail and not pathtrails[pathtrail] then
- pathtrails[pathtrail] = true
- table.insert(vim._so_trails, pathtrail)
- end
-end
-
-function vim._load_package(name)
- local basename = name:gsub('%.', '/')
- local paths = {"lua/"..basename..".lua", "lua/"..basename.."/init.lua"}
- local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true})
- if #found > 0 then
- local f, err = loadfile(found[1])
- return f or error(err)
- end
-
- local so_paths = {}
- for _,trail in ipairs(vim._so_trails) do
- local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash
- table.insert(so_paths, path)
- end
-
- found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true})
- if #found > 0 then
- -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is
- -- a) strip prefix up to and including the first dash, if any
- -- b) replace all dots by underscores
- -- c) prepend "luaopen_"
- -- So "foo-bar.baz" should result in "luaopen_bar_baz"
- local dash = name:find("-", 1, true)
- local modname = dash and name:sub(dash + 1) or name
- local f, err = package.loadlib(found[1], "luaopen_"..modname:gsub("%.", "_"))
- return f or error(err)
- end
- return nil
-end
-
--- Insert vim._load_package after the preloader at position 2
-table.insert(package.loaders, 2, vim._load_package)
-
-- These are for loading runtime modules lazily since they aren't available in
-- the nvim binary as specified in executor.c
setmetatable(vim, {
diff --git a/src/nvim/main.c b/src/nvim/main.c
index 8991ae7f00..b02ebbe030 100644
--- a/src/nvim/main.c
+++ b/src/nvim/main.c
@@ -9,7 +9,7 @@
#include <string.h>
#include "nvim/ascii.h"
-#include "nvim/aucmd.h"
+#include "nvim/autocmd.h"
#include "nvim/buffer.h"
#include "nvim/channel.h"
#include "nvim/charset.h"
@@ -154,10 +154,10 @@ bool event_teardown(void)
void early_init(mparm_T *paramp)
{
env_init();
- fs_init();
eval_init(); // init global variables
init_path(argv0 ? argv0 : "nvim");
init_normal_cmds(); // Init the table of Normal mode commands.
+ runtime_init();
highlight_init();
#ifdef WIN32
@@ -230,6 +230,10 @@ int main(int argc, char **argv)
// `argc` and `argv` are also copied, so that they can be changed.
init_params(&params, argc, argv);
+ // Since os_open is called during the init_startuptime, we need to call
+ // fs_init before it.
+ fs_init();
+
init_startuptime(&params);
// Need to find "--clean" before actually parsing arguments.
diff --git a/src/nvim/map.c b/src/nvim/map.c
index 77ebc2a387..091d653046 100644
--- a/src/nvim/map.c
+++ b/src/nvim/map.c
@@ -177,6 +177,7 @@ MAP_IMPL(handle_T, ptr_t, DEFAULT_INITIALIZER)
MAP_IMPL(String, MsgpackRpcRequestHandler, MSGPACK_HANDLER_INITIALIZER)
MAP_IMPL(HlEntry, int, DEFAULT_INITIALIZER)
MAP_IMPL(String, handle_T, 0)
+MAP_IMPL(String, int, DEFAULT_INITIALIZER)
MAP_IMPL(ColorKey, ColorItem, COLOR_ITEM_INITIALIZER)
diff --git a/src/nvim/map.h b/src/nvim/map.h
index 5e56f4dd65..c9c89bf2fd 100644
--- a/src/nvim/map.h
+++ b/src/nvim/map.h
@@ -46,6 +46,7 @@ MAP_DECLS(handle_T, ptr_t)
MAP_DECLS(String, MsgpackRpcRequestHandler)
MAP_DECLS(HlEntry, int)
MAP_DECLS(String, handle_T)
+MAP_DECLS(String, int)
MAP_DECLS(ColorKey, ColorItem)
diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c
index 24c7678633..daf974ee74 100644
--- a/src/nvim/os/fs.c
+++ b/src/nvim/os/fs.c
@@ -40,8 +40,10 @@
bool did_try_to_free = false; \
uv_call_start: {} \
uv_fs_t req; \
+ fs_loop_lock(); \
ret = func(&fs_loop, &req, __VA_ARGS__); \
uv_fs_req_cleanup(&req); \
+ fs_loop_unlock(); \
if (ret == UV_ENOMEM && !did_try_to_free) { \
try_to_free_memory(); \
did_try_to_free = true; \
@@ -52,12 +54,27 @@ uv_call_start: {} \
// Many fs functions from libuv return that value on success.
static const int kLibuvSuccess = 0;
static uv_loop_t fs_loop;
+static uv_mutex_t fs_loop_mutex;
// Initialize the fs module
void fs_init(void)
{
uv_loop_init(&fs_loop);
+ uv_mutex_init_recursive(&fs_loop_mutex);
+}
+
+/// TODO(bfredl): some of these operations should
+/// be possible to do the private libuv loop of the
+/// thread, instead of contending the global fs loop
+void fs_loop_lock(void)
+{
+ uv_mutex_lock(&fs_loop_mutex);
+}
+
+void fs_loop_unlock(void)
+{
+ uv_mutex_unlock(&fs_loop_mutex);
}
@@ -98,9 +115,12 @@ bool os_isrealdir(const char *name)
FUNC_ATTR_NONNULL_ALL
{
uv_fs_t request;
+ fs_loop_lock();
if (uv_fs_lstat(&fs_loop, &request, name, NULL) != kLibuvSuccess) {
+ fs_loop_unlock();
return false;
}
+ fs_loop_unlock();
if (S_ISLNK(request.statbuf.st_mode)) {
return false;
} else {
@@ -738,7 +758,9 @@ static int os_stat(const char *name, uv_stat_t *statbuf)
return UV_EINVAL;
}
uv_fs_t request;
+ fs_loop_lock();
int result = uv_fs_stat(&fs_loop, &request, name, NULL);
+ fs_loop_unlock();
if (result == kLibuvSuccess) {
*statbuf = request.statbuf;
}
@@ -935,9 +957,11 @@ int os_mkdtemp(const char *template, char *path)
FUNC_ATTR_NONNULL_ALL
{
uv_fs_t request;
+ fs_loop_lock();
int result = uv_fs_mkdtemp(&fs_loop, &request, template, NULL);
+ fs_loop_unlock();
if (result == kLibuvSuccess) {
- STRNCPY(path, request.path, TEMP_FILE_PATH_MAXLEN);
+ xstrlcpy(path, request.path, TEMP_FILE_PATH_MAXLEN);
}
uv_fs_req_cleanup(&request);
return result;
@@ -962,7 +986,9 @@ int os_rmdir(const char *path)
bool os_scandir(Directory *dir, const char *path)
FUNC_ATTR_NONNULL_ALL
{
+ fs_loop_lock();
int r = uv_fs_scandir(&fs_loop, &dir->request, path, 0, NULL);
+ fs_loop_unlock();
if (r < 0) {
os_closedir(dir);
}
@@ -1023,7 +1049,9 @@ bool os_fileinfo_link(const char *path, FileInfo *file_info)
return false;
}
uv_fs_t request;
+ fs_loop_lock();
bool ok = uv_fs_lstat(&fs_loop, &request, path, NULL) == kLibuvSuccess;
+ fs_loop_unlock();
if (ok) {
file_info->stat = request.statbuf;
}
@@ -1041,6 +1069,7 @@ bool os_fileinfo_fd(int file_descriptor, FileInfo *file_info)
{
uv_fs_t request;
memset(file_info, 0, sizeof(*file_info));
+ fs_loop_lock();
bool ok = uv_fs_fstat(&fs_loop,
&request,
file_descriptor,
@@ -1049,6 +1078,7 @@ bool os_fileinfo_fd(int file_descriptor, FileInfo *file_info)
file_info->stat = request.statbuf;
}
uv_fs_req_cleanup(&request);
+ fs_loop_unlock();
return ok;
}
@@ -1165,6 +1195,7 @@ char *os_realpath(const char *name, char *buf)
FUNC_ATTR_NONNULL_ARG(1)
{
uv_fs_t request;
+ fs_loop_lock();
int result = uv_fs_realpath(&fs_loop, &request, name, NULL);
if (result == kLibuvSuccess) {
if (buf == NULL) {
@@ -1173,6 +1204,7 @@ char *os_realpath(const char *name, char *buf)
xstrlcpy(buf, request.ptr, MAXPATHL + 1);
}
uv_fs_req_cleanup(&request);
+ fs_loop_unlock();
return result == kLibuvSuccess ? buf : NULL;
}
diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c
index 1c04cb16b3..1ec3e40abe 100644
--- a/src/nvim/runtime.c
+++ b/src/nvim/runtime.c
@@ -24,6 +24,13 @@
static bool runtime_search_path_valid = false;
static int *runtime_search_path_ref = NULL;
static RuntimeSearchPath runtime_search_path;
+static RuntimeSearchPath runtime_search_path_thread;
+static uv_mutex_t runtime_search_path_mutex;
+
+void runtime_init(void)
+{
+ uv_mutex_init(&runtime_search_path_mutex);
+}
/// ":runtime [what] {name}"
void ex_runtime(exarg_T *eap)
@@ -172,6 +179,17 @@ RuntimeSearchPath runtime_search_path_get_cached(int *ref)
return runtime_search_path;
}
+RuntimeSearchPath copy_runtime_search_path(const RuntimeSearchPath src)
+{
+ RuntimeSearchPath dst = KV_INITIAL_VALUE;
+ for (size_t j = 0; j < kv_size(src); j++) {
+ SearchPathItem src_item = kv_A(src, j);
+ kv_push(dst, ((SearchPathItem){ xstrdup(src_item.path), src_item.after, src_item.has_lua }));
+ }
+
+ return dst;
+}
+
void runtime_search_path_unref(RuntimeSearchPath path, int *ref)
FUNC_ATTR_NONNULL_ALL
{
@@ -302,15 +320,35 @@ ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all)
{
int ref;
RuntimeSearchPath path = runtime_search_path_get_cached(&ref);
- ArrayOf(String) rv = ARRAY_DICT_INIT;
static char buf[MAXPATHL];
+ ArrayOf(String) rv = runtime_get_named_common(lua, pat, all, path, buf, sizeof buf);
+
+ runtime_search_path_unref(path, &ref);
+ return rv;
+}
+
+ArrayOf(String) runtime_get_named_thread(bool lua, Array pat, bool all)
+{
+ // TODO(bfredl): avoid contention between multiple worker threads?
+ uv_mutex_lock(&runtime_search_path_mutex);
+ static char buf[MAXPATHL];
+ ArrayOf(String) rv = runtime_get_named_common(lua, pat, all, runtime_search_path_thread,
+ buf, sizeof buf);
+ uv_mutex_unlock(&runtime_search_path_mutex);
+ return rv;
+}
+
+ArrayOf(String) runtime_get_named_common(bool lua, Array pat, bool all,
+ RuntimeSearchPath path, char *buf, size_t buf_len)
+{
+ ArrayOf(String) rv = ARRAY_DICT_INIT;
for (size_t i = 0; i < kv_size(path); i++) {
SearchPathItem *item = &kv_A(path, i);
if (lua) {
if (item->has_lua == kNone) {
- size_t size = (size_t)snprintf(buf, sizeof buf, "%s/lua/", item->path);
- item->has_lua = (size < sizeof buf && os_isdir((char_u *)buf)) ? kTrue : kFalse;
+ size_t size = (size_t)snprintf(buf, buf_len, "%s/lua/", item->path);
+ item->has_lua = (size < buf_len && os_isdir((char_u *)buf));
}
if (item->has_lua == kFalse) {
continue;
@@ -320,9 +358,9 @@ ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all)
for (size_t j = 0; j < pat.size; j++) {
Object pat_item = pat.items[j];
if (pat_item.type == kObjectTypeString) {
- size_t size = (size_t)snprintf(buf, sizeof buf, "%s/%s",
+ size_t size = (size_t)snprintf(buf, buf_len, "%s/%s",
item->path, pat_item.data.string.data);
- if (size < sizeof buf) {
+ if (size < buf_len) {
if (os_file_is_readable(buf)) {
ADD(rv, STRING_OBJ(cstr_to_string(buf)));
if (!all) {
@@ -333,9 +371,7 @@ ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all)
}
}
}
-
done:
- runtime_search_path_unref(path, &ref);
return rv;
}
@@ -569,6 +605,10 @@ void runtime_search_path_validate(void)
runtime_search_path = runtime_search_path_build();
runtime_search_path_valid = true;
runtime_search_path_ref = NULL; // initially unowned
+ uv_mutex_lock(&runtime_search_path_mutex);
+ runtime_search_path_free(runtime_search_path_thread);
+ runtime_search_path_thread = copy_runtime_search_path(runtime_search_path);
+ uv_mutex_unlock(&runtime_search_path_mutex);
}
}
diff --git a/src/nvim/testdir/runnvim.sh b/src/nvim/testdir/runnvim.sh
index fdd3f3144b..72d88f9f93 100755
--- a/src/nvim/testdir/runnvim.sh
+++ b/src/nvim/testdir/runnvim.sh
@@ -65,9 +65,6 @@ main() {(
fi
fi
fi
- if test "$FAILED" = 1 ; then
- ci_fold start "$test_name"
- fi
valgrind_check .
if test -n "$LOG_DIR" ; then
check_sanitizer "$LOG_DIR"
@@ -78,9 +75,6 @@ main() {(
fi
rm -f "$tlog"
if test "$FAILED" = 1 ; then
- ci_fold end ""
- fi
- if test "$FAILED" = 1 ; then
echo "Test $test_name failed, see output above and summary for more details" >> test.log
# When Neovim crashed/aborted it might not have created messages.
# test.log itself is used as an indicator to exit non-zero in the Makefile.
diff --git a/src/nvim/testdir/test_mapping.vim b/src/nvim/testdir/test_mapping.vim
index f88e8cf843..1080a3c85b 100644
--- a/src/nvim/testdir/test_mapping.vim
+++ b/src/nvim/testdir/test_mapping.vim
@@ -1,6 +1,8 @@
" Tests for mappings and abbreviations
source shared.vim
+source check.vim
+source screendump.vim
func Test_abbreviation()
" abbreviation with 0x80 should work
@@ -451,6 +453,82 @@ func Test_expr_map_gets_cursor()
nunmap !
endfunc
+func Test_expr_map_restore_cursor()
+ CheckScreendump
+
+ let lines =<< trim END
+ call setline(1, ['one', 'two', 'three'])
+ 2
+ set ls=2
+ hi! link StatusLine ErrorMsg
+ noremap <expr> <C-B> Func()
+ func Func()
+ let g:on = !get(g:, 'on', 0)
+ redraws
+ return ''
+ endfunc
+ func Status()
+ return get(g:, 'on', 0) ? '[on]' : ''
+ endfunc
+ set stl=%{Status()}
+ END
+ call writefile(lines, 'XtestExprMap')
+ let buf = RunVimInTerminal('-S XtestExprMap', #{rows: 10})
+ call term_sendkeys(buf, "\<C-B>")
+ call VerifyScreenDump(buf, 'Test_map_expr_1', {})
+
+ " clean up
+ call StopVimInTerminal(buf)
+ call delete('XtestExprMap')
+endfunc
+
+func Test_map_listing()
+ CheckScreendump
+
+ let lines =<< trim END
+ nmap a b
+ END
+ call writefile(lines, 'XtestMapList')
+ let buf = RunVimInTerminal('-S XtestMapList', #{rows: 6})
+ call term_sendkeys(buf, ": nmap a\<CR>")
+ call VerifyScreenDump(buf, 'Test_map_list_1', {})
+
+ " clean up
+ call StopVimInTerminal(buf)
+ call delete('XtestMapList')
+endfunc
+
+func Test_expr_map_error()
+ CheckScreendump
+
+ let lines =<< trim END
+ func Func()
+ throw 'test'
+ return ''
+ endfunc
+
+ nnoremap <expr> <F2> Func()
+ cnoremap <expr> <F2> Func()
+
+ call test_override('ui_delay', 10)
+ END
+ call writefile(lines, 'XtestExprMap')
+ let buf = RunVimInTerminal('-S XtestExprMap', #{rows: 10})
+ call term_sendkeys(buf, "\<F2>")
+ call TermWait(buf)
+ call term_sendkeys(buf, "\<CR>")
+ call VerifyScreenDump(buf, 'Test_map_expr_2', {})
+
+ call term_sendkeys(buf, ":abc\<F2>")
+ call VerifyScreenDump(buf, 'Test_map_expr_3', {})
+ call term_sendkeys(buf, "\<Esc>0")
+ call VerifyScreenDump(buf, 'Test_map_expr_4', {})
+
+ " clean up
+ call StopVimInTerminal(buf)
+ call delete('XtestExprMap')
+endfunc
+
" Test for mapping errors
func Test_map_error()
call assert_fails('unmap', 'E474:')
diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c
index 6b889cf97c..b262fc6c54 100644
--- a/src/nvim/tui/input.c
+++ b/src/nvim/tui/input.c
@@ -5,7 +5,7 @@
#include "nvim/api/private/helpers.h"
#include "nvim/api/vim.h"
#include "nvim/ascii.h"
-#include "nvim/aucmd.h"
+#include "nvim/autocmd.h"
#include "nvim/charset.h"
#include "nvim/ex_docmd.h"
#include "nvim/macros.h"
@@ -378,7 +378,7 @@ static bool handle_focus_event(TermInput *input)
bool focus_gained = *rbuffer_get(input->read_stream.buffer, 2) == 'I';
// Advance past the sequence
rbuffer_consumed(input->read_stream.buffer, 3);
- aucmd_schedule_focusgained(focus_gained);
+ autocmd_schedule_focusgained(focus_gained);
return true;
}
return false;
diff --git a/src/nvim/ui.c b/src/nvim/ui.c
index 1aadaf5c9d..31b9614c34 100644
--- a/src/nvim/ui.c
+++ b/src/nvim/ui.c
@@ -8,7 +8,7 @@
#include <string.h>
#include "nvim/ascii.h"
-#include "nvim/aucmd.h"
+#include "nvim/autocmd.h"
#include "nvim/charset.h"
#include "nvim/cursor.h"
#include "nvim/cursor_shape.h"
diff --git a/src/nvim/viml/parser/expressions.c b/src/nvim/viml/parser/expressions.c
index 8a14710351..9d1318724e 100644
--- a/src/nvim/viml/parser/expressions.c
+++ b/src/nvim/viml/parser/expressions.c
@@ -1592,7 +1592,7 @@ typedef struct {
/// string is a regex.
/// @param[in] is_invalid Whether currently processed token is not valid.
static void parse_quoted_string(ParserState *const pstate, ExprASTNode *const node,
- const LexExprToken token, const ExprASTStack ast_stack,
+ const LexExprToken token, const ExprASTStack *ast_stack,
const bool is_invalid)
FUNC_ATTR_NONNULL_ALL
{
@@ -2907,7 +2907,7 @@ viml_pexpr_parse_no_paren_closing_error: {}
? kExprNodeDoubleQuotedString
: kExprNodeSingleQuotedString));
*top_node_p = cur_node;
- parse_quoted_string(pstate, cur_node, cur_token, ast_stack, is_invalid);
+ parse_quoted_string(pstate, cur_node, cur_token, &ast_stack, is_invalid);
want_node = kENodeOperator;
break;
}
diff --git a/src/nvim/window.c b/src/nvim/window.c
index 43667377c5..83048d911f 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -958,6 +958,11 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
int wmh1;
bool did_set_fraction = false;
+ // aucmd_win should always remain floating
+ if (new_wp != NULL && new_wp == aucmd_win) {
+ return FAIL;
+ }
+
if (flags & WSP_TOP) {
oldwin = firstwin;
} else if (flags & WSP_BOT || curwin->w_floating) {
@@ -1833,6 +1838,9 @@ static void win_totop(int size, int flags)
beep_flush();
return;
}
+ if (curwin == aucmd_win) {
+ return;
+ }
if (curwin->w_floating) {
ui_comp_remove_grid(&curwin->w_grid_alloc);
diff --git a/test/functional/api/autocmd_spec.lua b/test/functional/api/autocmd_spec.lua
new file mode 100644
index 0000000000..2b48ffa6f0
--- /dev/null
+++ b/test/functional/api/autocmd_spec.lua
@@ -0,0 +1,798 @@
+local helpers = require('test.functional.helpers')(after_each)
+
+local clear = helpers.clear
+local command = helpers.command
+local eq = helpers.eq
+local neq = helpers.neq
+local exec_lua = helpers.exec_lua
+local matches = helpers.matches
+local meths = helpers.meths
+local source = helpers.source
+
+before_each(clear)
+
+describe('autocmd api', function()
+ describe('nvim_create_autocmd', function()
+ it('does not allow "command" and "callback" in the same autocmd', function()
+ local ok, _ = pcall(meths.create_autocmd, {
+ event = "BufReadPost",
+ pattern = "*.py,*.pyi",
+ command = "echo 'Should Have Errored",
+ callback = "not allowed",
+ })
+
+ eq(false, ok)
+ end)
+
+ it('doesnt leak when you use ++once', function()
+ eq(1, exec_lua([[
+ local count = 0
+
+ vim.api.nvim_create_autocmd {
+ event = "FileType",
+ pattern = "*",
+ callback = function() count = count + 1 end,
+ once = true
+ }
+
+ vim.cmd "set filetype=txt"
+ vim.cmd "set filetype=python"
+
+ return count
+ ]], {}))
+ end)
+
+ it('allows passing buffer by key', function()
+ meths.set_var('called', 0)
+
+ meths.create_autocmd {
+ event = "Filetype",
+ command = "let g:called = g:called + 1",
+ buffer = 0,
+ }
+
+ meths.command "set filetype=txt"
+ eq(1, meths.get_var('called'))
+
+ -- switch to a new buffer
+ meths.command "new"
+ meths.command "set filetype=python"
+
+ eq(1, meths.get_var('called'))
+ end)
+
+ it('does not allow passing buffer and patterns', function()
+ local ok = pcall(meths.create_autocmd, {
+ event = "Filetype",
+ command = "let g:called = g:called + 1",
+ buffer = 0,
+ pattern = "*.py",
+ })
+
+ eq(false, ok)
+ end)
+
+ it('does not allow passing invalid buffers', function()
+ local ok, msg = pcall(meths.create_autocmd, {
+ event = "Filetype",
+ command = "let g:called = g:called + 1",
+ buffer = -1,
+ })
+
+ eq(false, ok)
+ matches('Invalid buffer id', msg)
+ end)
+
+ it('errors on non-functions for cb', function()
+ eq(false, pcall(exec_lua, [[
+ vim.api.nvim_create_autocmd {
+ event = "BufReadPost",
+ pattern = "*.py,*.pyi",
+ callback = 5,
+ }
+ ]]))
+ end)
+
+ it('allow passing pattern and <buffer> in same pattern', function()
+ local ok = pcall(meths.create_autocmd, {
+ event = "BufReadPost",
+ pattern = "*.py,<buffer>",
+ command = "echo 'Should Not Error'"
+ })
+
+ eq(true, ok)
+ end)
+
+ it('should handle multiple values as comma separated list', function()
+ meths.create_autocmd {
+ event = "BufReadPost",
+ pattern = "*.py,*.pyi",
+ command = "echo 'Should Not Have Errored'"
+ }
+
+ -- We should have one autocmd for *.py and one for *.pyi
+ eq(2, #meths.get_autocmds { event = "BufReadPost" })
+ end)
+
+ it('should handle multiple values as array', function()
+ meths.create_autocmd {
+ event = "BufReadPost",
+ pattern = { "*.py", "*.pyi", },
+ command = "echo 'Should Not Have Errored'"
+ }
+
+ -- We should have one autocmd for *.py and one for *.pyi
+ eq(2, #meths.get_autocmds { event = "BufReadPost" })
+ end)
+
+ describe('desc', function()
+ it('can add description to one autocmd', function()
+ meths.create_autocmd {
+ event = "BufReadPost",
+ pattern = "*.py",
+ command = "echo 'Should Not Have Errored'",
+ desc = "Can show description",
+ }
+
+ eq("Can show description", meths.get_autocmds { event = "BufReadPost" }[1].desc)
+ end)
+
+ it('can add description to multiple autocmd', function()
+ meths.create_autocmd {
+ event = "BufReadPost",
+ pattern = {"*.py", "*.pyi"},
+ command = "echo 'Should Not Have Errored'",
+ desc = "Can show description",
+ }
+
+ local aus = meths.get_autocmds { event = "BufReadPost" }
+ eq(2, #aus)
+ eq("Can show description", aus[1].desc)
+ eq("Can show description", aus[2].desc)
+ end)
+ end)
+
+ pending('script and verbose settings', function()
+ it('marks API client', function()
+ meths.create_autocmd {
+ event = "BufReadPost",
+ pattern = "*.py",
+ command = "echo 'Should Not Have Errored'",
+ desc = "Can show description",
+ }
+
+ local aus = meths.get_autocmds { event = "BufReadPost" }
+ eq(1, #aus, aus)
+ end)
+ end)
+ end)
+
+ describe('nvim_get_autocmds', function()
+ describe('events', function()
+ it('should return one autocmd when there is only one for an event', function()
+ command [[au! InsertEnter]]
+ command [[au InsertEnter * :echo "1"]]
+
+ local aus = meths.get_autocmds { event = "InsertEnter" }
+ eq(1, #aus)
+ end)
+
+ it('should return two autocmds when there are two for an event', function()
+ command [[au! InsertEnter]]
+ command [[au InsertEnter * :echo "1"]]
+ command [[au InsertEnter * :echo "2"]]
+
+ local aus = meths.get_autocmds { event = "InsertEnter" }
+ eq(2, #aus)
+ end)
+
+ it('should return the same thing if you use string or list', function()
+ command [[au! InsertEnter]]
+ command [[au InsertEnter * :echo "1"]]
+ command [[au InsertEnter * :echo "2"]]
+
+ local string_aus = meths.get_autocmds { event = "InsertEnter" }
+ local array_aus = meths.get_autocmds { event = { "InsertEnter" } }
+ eq(string_aus, array_aus)
+ end)
+
+ it('should return two autocmds when there are two for an event', function()
+ command [[au! InsertEnter]]
+ command [[au! InsertLeave]]
+ command [[au InsertEnter * :echo "1"]]
+ command [[au InsertEnter * :echo "2"]]
+
+ local aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } }
+ eq(2, #aus)
+ end)
+
+ it('should return different IDs for different autocmds', function()
+ command [[au! InsertEnter]]
+ command [[au! InsertLeave]]
+ command [[au InsertEnter * :echo "1"]]
+ source [[
+ call nvim_create_autocmd(#{
+ \ event: "InsertLeave",
+ \ command: ":echo 2",
+ \ })
+ ]]
+
+ local aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } }
+ local first = aus[1]
+ eq(first.id, nil)
+
+ -- TODO: Maybe don't have this number, just assert it's not nil
+ local second = aus[2]
+ neq(second.id, nil)
+
+ meths.del_autocmd(second.id)
+ local new_aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } }
+ eq(1, #new_aus)
+ eq(first, new_aus[1])
+ end)
+ end)
+
+ describe('groups', function()
+ before_each(function()
+ command [[au! InsertEnter]]
+
+ command [[au InsertEnter * :echo "No Group"]]
+
+ command [[augroup GroupOne]]
+ command [[ au InsertEnter * :echo "GroupOne:1"]]
+ command [[augroup END]]
+
+ command [[augroup GroupTwo]]
+ command [[ au InsertEnter * :echo "GroupTwo:2"]]
+ command [[ au InsertEnter * :echo "GroupTwo:3"]]
+ command [[augroup END]]
+ end)
+
+ it('should return all groups if no group is specified', function()
+ local aus = meths.get_autocmds { event = "InsertEnter" }
+ if #aus ~= 4 then
+ eq({}, aus)
+ end
+
+ eq(4, #aus)
+ end)
+
+ it('should return only the group specified', function()
+ local aus = meths.get_autocmds {
+ event = "InsertEnter",
+ group = "GroupOne",
+ }
+
+ eq(1, #aus)
+ eq([[:echo "GroupOne:1"]], aus[1].command)
+ end)
+
+ it('should return only the group specified, multiple values', function()
+ local aus = meths.get_autocmds {
+ event = "InsertEnter",
+ group = "GroupTwo",
+ }
+
+ eq(2, #aus)
+ eq([[:echo "GroupTwo:2"]], aus[1].command)
+ eq([[:echo "GroupTwo:3"]], aus[2].command)
+ end)
+ end)
+
+ describe('groups: 2', function()
+ it('raises error for undefined augroup', function()
+ local success, code = unpack(meths.exec_lua([[
+ return {pcall(function()
+ vim.api.nvim_create_autocmd {
+ event = "FileType",
+ pattern = "*",
+ group = "NotDefined",
+ command = "echo 'hello'",
+ }
+ end)}
+ ]], {}))
+
+ eq(false, success)
+ matches('invalid augroup: NotDefined', code)
+ end)
+ end)
+
+ describe('patterns', function()
+ before_each(function()
+ command [[au! InsertEnter]]
+
+ command [[au InsertEnter * :echo "No Group"]]
+ command [[au InsertEnter *.one :echo "GroupOne:1"]]
+ command [[au InsertEnter *.two :echo "GroupTwo:2"]]
+ command [[au InsertEnter *.two :echo "GroupTwo:3"]]
+ command [[au InsertEnter <buffer> :echo "Buffer"]]
+ end)
+
+ it('should should return for literal match', function()
+ local aus = meths.get_autocmds {
+ event = "InsertEnter",
+ pattern = "*"
+ }
+
+ eq(1, #aus)
+ eq([[:echo "No Group"]], aus[1].command)
+ end)
+
+ it('should return for multiple matches', function()
+ -- vim.api.nvim_get_autocmds
+ local aus = meths.get_autocmds {
+ event = "InsertEnter",
+ pattern = { "*.one", "*.two" },
+ }
+
+ eq(3, #aus)
+ eq([[:echo "GroupOne:1"]], aus[1].command)
+ eq([[:echo "GroupTwo:2"]], aus[2].command)
+ eq([[:echo "GroupTwo:3"]], aus[3].command)
+ end)
+
+ it('should work for buffer autocmds', function()
+ local normalized_aus = meths.get_autocmds {
+ event = "InsertEnter",
+ pattern = "<buffer=1>",
+ }
+
+ local raw_aus = meths.get_autocmds {
+ event = "InsertEnter",
+ pattern = "<buffer>",
+ }
+
+ local zero_aus = meths.get_autocmds {
+ event = "InsertEnter",
+ pattern = "<buffer=0>",
+ }
+
+ eq(normalized_aus, raw_aus)
+ eq(normalized_aus, zero_aus)
+ eq([[:echo "Buffer"]], normalized_aus[1].command)
+ end)
+ end)
+ end)
+
+ describe('nvim_do_autocmd', function()
+ it("can trigger builtin autocmds", function()
+ meths.set_var("autocmd_executed", false)
+
+ meths.create_autocmd {
+ event = "BufReadPost",
+ pattern = "*",
+ command = "let g:autocmd_executed = v:true",
+ }
+
+ eq(false, meths.get_var("autocmd_executed"))
+ meths.do_autocmd { event = "BufReadPost" }
+ eq(true, meths.get_var("autocmd_executed"))
+ end)
+
+ it("can pass the buffer", function()
+ meths.set_var("buffer_executed", -1)
+ eq(-1, meths.get_var("buffer_executed"))
+
+ meths.create_autocmd {
+ event = "BufLeave",
+ pattern = "*",
+ command = 'let g:buffer_executed = +expand("<abuf>")',
+ }
+
+ -- Doesn't execute for other non-matching events
+ meths.do_autocmd { event = "CursorHold", buffer = 1 }
+ eq(-1, meths.get_var("buffer_executed"))
+
+ meths.do_autocmd { event = "BufLeave", buffer = 1 }
+ eq(1, meths.get_var("buffer_executed"))
+ end)
+
+ it("can pass the filename, pattern match", function()
+ meths.set_var("filename_executed", 'none')
+ eq('none', meths.get_var("filename_executed"))
+
+ meths.create_autocmd {
+ event = "BufEnter",
+ pattern = "*.py",
+ command = 'let g:filename_executed = expand("<afile>")',
+ }
+
+ -- Doesn't execute for other non-matching events
+ meths.do_autocmd { event = "CursorHold", buffer = 1 }
+ eq('none', meths.get_var("filename_executed"))
+
+ meths.command('edit __init__.py')
+ eq('__init__.py', meths.get_var("filename_executed"))
+ end)
+
+ it('cannot pass buf and fname', function()
+ local ok = pcall(meths.do_autocmd, { pattern = "literally_cannot_error.rs", buffer = 1 })
+ eq(false, ok)
+ end)
+
+ it("can pass the filename, exact match", function()
+ meths.set_var("filename_executed", 'none')
+ eq('none', meths.get_var("filename_executed"))
+
+ meths.command('edit other_file.txt')
+ meths.command('edit __init__.py')
+ eq('none', meths.get_var("filename_executed"))
+
+ meths.create_autocmd {
+ event = "CursorHoldI",
+ pattern = "__init__.py",
+ command = 'let g:filename_executed = expand("<afile>")',
+ }
+
+ -- Doesn't execute for other non-matching events
+ meths.do_autocmd { event = "CursorHoldI", buffer = 1 }
+ eq('none', meths.get_var("filename_executed"))
+
+ meths.do_autocmd { event = "CursorHoldI", buffer = tonumber(meths.get_current_buf()) }
+ eq('__init__.py', meths.get_var("filename_executed"))
+
+ -- Reset filename
+ meths.set_var("filename_executed", 'none')
+
+ meths.do_autocmd { event = "CursorHoldI", pattern = '__init__.py' }
+ eq('__init__.py', meths.get_var("filename_executed"))
+ end)
+
+ it("works with user autocmds", function()
+ meths.set_var("matched", 'none')
+
+ meths.create_autocmd {
+ event = "User",
+ pattern = "TestCommand",
+ command = 'let g:matched = "matched"'
+ }
+
+ meths.do_autocmd { event = "User", pattern = "OtherCommand" }
+ eq('none', meths.get_var('matched'))
+ meths.do_autocmd { event = "User", pattern = "TestCommand" }
+ eq('matched', meths.get_var('matched'))
+ end)
+ end)
+
+ describe('nvim_create_augroup', function()
+ before_each(function()
+ clear()
+
+ meths.set_var('executed', 0)
+ end)
+
+ local make_counting_autocmd = function(opts)
+ opts = opts or {}
+
+ local resulting = {
+ event = "FileType",
+ pattern = "*",
+ command = "let g:executed = g:executed + 1",
+ }
+
+ resulting.group = opts.group
+ resulting.once = opts.once
+
+ meths.create_autocmd(resulting)
+ end
+
+ local set_ft = function(ft)
+ ft = ft or "txt"
+ source(string.format("set filetype=%s", ft))
+ end
+
+ local get_executed_count = function()
+ return meths.get_var('executed')
+ end
+
+ it('can be added in a group', function()
+ local augroup = "TestGroup"
+ meths.create_augroup({ name = augroup, clear = true })
+ make_counting_autocmd { group = augroup }
+
+ set_ft("txt")
+ set_ft("python")
+
+ eq(get_executed_count(), 2)
+ end)
+
+ it('works getting called multiple times', function()
+ make_counting_autocmd()
+ set_ft()
+ set_ft()
+ set_ft()
+
+ eq(get_executed_count(), 3)
+ end)
+
+ it('handles ++once', function()
+ make_counting_autocmd {once = true}
+ set_ft('txt')
+ set_ft('help')
+ set_ft('txt')
+ set_ft('help')
+
+ eq(get_executed_count(), 1)
+ end)
+
+ it('errors on unexpected keys', function()
+ local success, code = pcall(meths.create_autocmd, {
+ event = "FileType",
+ pattern = "*",
+ not_a_valid_key = "NotDefined",
+ })
+
+ eq(false, success)
+ matches('not_a_valid_key', code)
+ end)
+
+ it('can execute simple callback', function()
+ exec_lua([[
+ vim.g.executed = false
+
+ vim.api.nvim_create_autocmd {
+ event = "FileType",
+ pattern = "*",
+ callback = function() vim.g.executed = true end,
+ }
+ ]], {})
+
+
+ eq(true, exec_lua([[
+ vim.cmd "set filetype=txt"
+ return vim.g.executed
+ ]], {}))
+ end)
+
+ it('calls multiple lua callbacks for the same autocmd execution', function()
+ eq(4, exec_lua([[
+ local count = 0
+ local counter = function()
+ count = count + 1
+ end
+
+ vim.api.nvim_create_autocmd {
+ event = "FileType",
+ pattern = "*",
+ callback = counter,
+ }
+
+ vim.api.nvim_create_autocmd {
+ event = "FileType",
+ pattern = "*",
+ callback = counter,
+ }
+
+ vim.cmd "set filetype=txt"
+ vim.cmd "set filetype=txt"
+
+ return count
+ ]], {}))
+ end)
+
+ it('properly releases functions with ++once', function()
+ exec_lua([[
+ WeakTable = setmetatable({}, { __mode = "k" })
+
+ OnceCount = 0
+
+ MyVal = {}
+ WeakTable[MyVal] = true
+
+ vim.api.nvim_create_autocmd {
+ event = "FileType",
+ pattern = "*",
+ callback = function()
+ OnceCount = OnceCount + 1
+ MyVal = {}
+ end,
+ once = true
+ }
+ ]])
+
+ command [[set filetype=txt]]
+ eq(1, exec_lua([[return OnceCount]], {}))
+
+ exec_lua([[collectgarbage()]], {})
+
+ command [[set filetype=txt]]
+ eq(1, exec_lua([[return OnceCount]], {}))
+
+ eq(0, exec_lua([[
+ local count = 0
+ for _ in pairs(WeakTable) do
+ count = count + 1
+ end
+
+ return count
+ ]]), "Should have no keys remaining")
+ end)
+
+ it('groups can be cleared', function()
+ local augroup = "TestGroup"
+ meths.create_augroup({ name = augroup, clear = true })
+ meths.create_autocmd({
+ group = augroup,
+ event = "FileType",
+ command = "let g:executed = g:executed + 1"
+ })
+
+ set_ft("txt")
+ set_ft("txt")
+ eq(2, get_executed_count(), "should only count twice")
+
+ meths.create_augroup({ name = augroup, clear = true })
+ eq({}, meths.get_autocmds { group = augroup })
+
+ set_ft("txt")
+ set_ft("txt")
+ eq(2, get_executed_count(), "No additional counts")
+ end)
+
+ it('groups work with once', function()
+ local augroup = "TestGroup"
+
+ meths.create_augroup({ name = augroup, clear = true })
+ make_counting_autocmd { group = augroup, once = true }
+
+ set_ft("txt")
+ set_ft("python")
+
+ eq(get_executed_count(), 1)
+ end)
+
+ it('autocmds can be registered multiple times.', function()
+ local augroup = "TestGroup"
+
+ meths.create_augroup({ name = augroup, clear = true })
+ make_counting_autocmd { group = augroup, once = false }
+ make_counting_autocmd { group = augroup, once = false }
+ make_counting_autocmd { group = augroup, once = false }
+
+ set_ft("txt")
+ set_ft("python")
+
+ eq(get_executed_count(), 3 * 2)
+ end)
+
+ it('can be deleted', function()
+ local augroup = "WillBeDeleted"
+
+ meths.create_augroup({ name = augroup, clear = true })
+ meths.create_autocmd {
+ event = {"Filetype"},
+ pattern = "*",
+ command = "echo 'does not matter'",
+ }
+
+ -- Clears the augroup from before, which erases the autocmd
+ meths.create_augroup({ name = augroup, clear = true })
+
+ local result = #meths.get_autocmds { group = augroup }
+
+ eq(0, result)
+ end)
+
+ it('can be used for buffer local autocmds', function()
+ local augroup = "WillBeDeleted"
+
+ meths.set_var("value_set", false)
+
+ meths.create_augroup({ name = augroup, clear = true })
+ meths.create_autocmd {
+ event = "Filetype",
+ pattern = "<buffer>",
+ command = "let g:value_set = v:true",
+ }
+
+ command "new"
+ command "set filetype=python"
+
+ eq(false, meths.get_var("value_set"))
+ end)
+
+ it('can accept vimscript functions', function()
+ source [[
+ let g:vimscript_executed = 0
+
+ function! MyVimscriptFunction() abort
+ let g:vimscript_executed = g:vimscript_executed + 1
+ endfunction
+
+ call nvim_create_autocmd(#{
+ \ event: "Filetype",
+ \ pattern: ["python", "javascript"],
+ \ callback: "MyVimscriptFunction",
+ \ })
+
+ set filetype=txt
+ set filetype=python
+ set filetype=txt
+ set filetype=javascript
+ set filetype=txt
+ ]]
+
+ eq(2, meths.get_var("vimscript_executed"))
+ end)
+ end)
+
+ describe('augroup!', function()
+ it('legacy: should clear and not return any autocmds for delete groups', function()
+ command('augroup TEMP_A')
+ command(' autocmd! BufReadPost *.py :echo "Hello"')
+ command('augroup END')
+
+ command('augroup! TEMP_A')
+
+ eq(false, pcall(meths.get_autocmds, { group = 'TEMP_A' }))
+
+ -- For some reason, augroup! doesn't clear the autocmds themselves, which is just wild
+ -- but we managed to keep this behavior.
+ eq(1, #meths.get_autocmds { event = 'BufReadPost' })
+ end)
+
+ it('legacy: remove augroups that have no autocmds', function()
+ command('augroup TEMP_AB')
+ command('augroup END')
+
+ command('augroup! TEMP_AB')
+
+ eq(false, pcall(meths.get_autocmds, { group = 'TEMP_AB' }))
+ eq(0, #meths.get_autocmds { event = 'BufReadPost' })
+ end)
+
+ it('legacy: multiple remove and add augroup', function()
+ command('augroup TEMP_ABC')
+ command(' au!')
+ command(' autocmd BufReadPost *.py echo "Hello"')
+ command('augroup END')
+
+ command('augroup! TEMP_ABC')
+
+ -- Should still have one autocmd :'(
+ local aus = meths.get_autocmds { event = 'BufReadPost' }
+ eq(1, #aus, aus)
+
+ command('augroup TEMP_ABC')
+ command(' au!')
+ command(' autocmd BufReadPost *.py echo "Hello"')
+ command('augroup END')
+
+ -- Should now have two autocmds :'(
+ aus = meths.get_autocmds { event = 'BufReadPost' }
+ eq(2, #aus, aus)
+
+ command('augroup! TEMP_ABC')
+
+ eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABC' }))
+ eq(2, #meths.get_autocmds { event = 'BufReadPost' })
+ end)
+
+ it('api: should clear and not return any autocmds for delete groups by id', function()
+ command('augroup TEMP_ABCD')
+ command('autocmd! BufReadPost *.py :echo "Hello"')
+ command('augroup END')
+
+ local augroup_id = meths.create_augroup { name = "TEMP_ABCD", clear = false }
+ meths.del_augroup_by_id(augroup_id)
+
+ -- For good reason, we kill all the autocmds from del_augroup,
+ -- so now this works as expected
+ eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABCD' }))
+ eq(0, #meths.get_autocmds { event = 'BufReadPost' })
+ end)
+
+ it('api: should clear and not return any autocmds for delete groups by name', function()
+ command('augroup TEMP_ABCDE')
+ command('autocmd! BufReadPost *.py :echo "Hello"')
+ command('augroup END')
+
+ meths.del_augroup_by_name("TEMP_ABCDE")
+
+ -- For good reason, we kill all the autocmds from del_augroup,
+ -- so now this works as expected
+ eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABCDE' }))
+ eq(0, #meths.get_autocmds { event = 'BufReadPost' })
+ end)
+ end)
+end)
diff --git a/test/functional/api/buffer_updates_spec.lua b/test/functional/api/buffer_updates_spec.lua
index 25ca8cfe4a..e9ad756947 100644
--- a/test/functional/api/buffer_updates_spec.lua
+++ b/test/functional/api/buffer_updates_spec.lua
@@ -824,6 +824,7 @@ describe('API: buffer events:', function()
end
msg = next_msg()
end
+ -- FIXME: Windows
assert(iswin(), 'did not match/receive expected nvim_buf_lines_event lines')
end
diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua
index de22c9078c..b80004f67c 100644
--- a/test/functional/api/command_spec.lua
+++ b/test/functional/api/command_spec.lua
@@ -107,7 +107,8 @@ describe('nvim_add_user_command', function()
]]
eq({
- args = "hello",
+ args = [[hello my\ friend how\ are\ you?]],
+ fargs = {[[hello]], [[my\ friend]], [[how\ are\ you?]]},
bang = false,
line1 = 1,
line2 = 1,
@@ -115,13 +116,14 @@ describe('nvim_add_user_command', function()
range = 0,
count = 2,
reg = "",
- }, exec_lua [[
- vim.api.nvim_command('CommandWithLuaCallback hello')
+ }, exec_lua [=[
+ vim.api.nvim_command([[CommandWithLuaCallback hello my\ friend how\ are\ you?]])
return result
- ]])
+ ]=])
eq({
- args = "",
+ args = 'h\tey',
+ fargs = {[[h]], [[ey]]},
bang = true,
line1 = 10,
line2 = 10,
@@ -129,13 +131,14 @@ describe('nvim_add_user_command', function()
range = 1,
count = 10,
reg = "",
- }, exec_lua [[
- vim.api.nvim_command('botright 10CommandWithLuaCallback!')
+ }, exec_lua [=[
+ vim.api.nvim_command('botright 10CommandWithLuaCallback! h\tey')
return result
- ]])
+ ]=])
eq({
- args = "",
+ args = "h",
+ fargs = {"h"},
bang = false,
line1 = 1,
line2 = 42,
@@ -144,9 +147,52 @@ describe('nvim_add_user_command', function()
count = 42,
reg = "",
}, exec_lua [[
- vim.api.nvim_command('CommandWithLuaCallback 42')
+ vim.api.nvim_command('CommandWithLuaCallback 42 h')
return result
]])
+
+ eq({
+ args = "",
+ fargs = {""}, -- fargs works without args
+ bang = false,
+ line1 = 1,
+ line2 = 1,
+ mods = "",
+ range = 0,
+ count = 2,
+ reg = "",
+ }, exec_lua [[
+ vim.api.nvim_command('CommandWithLuaCallback')
+ return result
+ ]])
+
+ -- f-args doesn't split when command nargs is 1 or "?"
+ exec_lua [[
+ result = {}
+ vim.api.nvim_add_user_command('CommandWithOneArg', function(opts)
+ result = opts
+ end, {
+ nargs = "?",
+ bang = true,
+ count = 2,
+ })
+ ]]
+
+ eq({
+ args = "hello I'm one argmuent",
+ fargs = {"hello I'm one argmuent"}, -- Doesn't split args
+ bang = false,
+ line1 = 1,
+ line2 = 1,
+ mods = "",
+ range = 0,
+ count = 2,
+ reg = "",
+ }, exec_lua [[
+ vim.api.nvim_command('CommandWithOneArg hello I\'m one argmuent')
+ return result
+ ]])
+
end)
it('can define buffer-local commands', function()
diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua
index 4d2ffa316a..c31ab2060a 100644
--- a/test/functional/api/window_spec.lua
+++ b/test/functional/api/window_spec.lua
@@ -1,4 +1,5 @@
local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
local clear, nvim, curbuf, curbuf_contents, window, curwin, eq, neq,
ok, feed, insert, eval, tabpage = helpers.clear, helpers.nvim, helpers.curbuf,
helpers.curbuf_contents, helpers.window, helpers.curwin, helpers.eq,
@@ -73,8 +74,7 @@ describe('API/win', function()
eq('typing\n some dumb text', curbuf_contents())
end)
- it('does not leak memory when using invalid window ID with invalid pos',
- function()
+ it('does not leak memory when using invalid window ID with invalid pos', function()
eq('Invalid window id: 1', pcall_err(meths.win_set_cursor, 1, {"b\na"}))
end)
@@ -147,6 +147,46 @@ describe('API/win', function()
eq({2, 5}, window('get_cursor', win))
end)
+ it('updates cursorline and statusline ruler in non-current window', function()
+ local screen = Screen.new(60, 8)
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue}, -- NonText
+ [2] = {background = Screen.colors.Grey90}, -- CursorLine
+ [3] = {bold = true, reverse = true}, -- StatusLine
+ [4] = {reverse = true}, -- VertSplit, StatusLineNC
+ })
+ screen:attach()
+ command('set ruler')
+ command('set cursorline')
+ insert([[
+ aaa
+ bbb
+ ccc
+ ddd]])
+ local oldwin = curwin()
+ command('vsplit')
+ screen:expect([[
+ aaa {4:│}aaa |
+ bbb {4:│}bbb |
+ ccc {4:│}ccc |
+ {2:dd^d }{4:│}{2:ddd }|
+ {1:~ }{4:│}{1:~ }|
+ {1:~ }{4:│}{1:~ }|
+ {3:[No Name] [+] 4,3 All }{4:[No Name] [+] 4,3 All}|
+ |
+ ]])
+ window('set_cursor', oldwin, {1, 0})
+ screen:expect([[
+ aaa {4:│}{2:aaa }|
+ bbb {4:│}bbb |
+ ccc {4:│}ccc |
+ {2:dd^d }{4:│}ddd |
+ {1:~ }{4:│}{1:~ }|
+ {1:~ }{4:│}{1:~ }|
+ {3:[No Name] [+] 4,3 All }{4:[No Name] [+] 1,1 All}|
+ |
+ ]])
+ end)
end)
describe('{get,set}_height', function()
diff --git a/test/functional/autocmd/autocmd_oldtest_spec.lua b/test/functional/autocmd/autocmd_oldtest_spec.lua
new file mode 100644
index 0000000000..ad3687d7b0
--- /dev/null
+++ b/test/functional/autocmd/autocmd_oldtest_spec.lua
@@ -0,0 +1,86 @@
+local helpers = require('test.functional.helpers')(after_each)
+
+local clear = helpers.clear
+local eq = helpers.eq
+local meths = helpers.meths
+local funcs = helpers.funcs
+
+local exec = function(str)
+ meths.exec(str, false)
+end
+
+describe('oldtests', function()
+ before_each(clear)
+
+ local exec_lines = function(str)
+ return funcs.split(funcs.execute(str), "\n")
+ end
+
+ local add_an_autocmd = function()
+ exec [[
+ augroup vimBarTest
+ au BufReadCmd * echo 'hello'
+ augroup END
+ ]]
+
+ eq(3, #exec_lines('au vimBarTest'))
+ eq(1, #meths.get_autocmds({ group = 'vimBarTest' }))
+ end
+
+ it('should recognize a bar before the {event}', function()
+ -- Good spacing
+ add_an_autocmd()
+ exec [[ augroup vimBarTest | au! | augroup END ]]
+ eq(1, #exec_lines('au vimBarTest'))
+ eq({}, meths.get_autocmds({ group = 'vimBarTest' }))
+
+ -- Sad spacing
+ add_an_autocmd()
+ exec [[ augroup vimBarTest| au!| augroup END ]]
+ eq(1, #exec_lines('au vimBarTest'))
+
+
+ -- test that a bar is recognized after the {event}
+ add_an_autocmd()
+ exec [[ augroup vimBarTest| au!BufReadCmd| augroup END ]]
+ eq(1, #exec_lines('au vimBarTest'))
+
+ add_an_autocmd()
+ exec [[ au! vimBarTest|echo 'hello' ]]
+ eq(1, #exec_lines('au vimBarTest'))
+ end)
+
+ it('should fire on unload buf', function()
+ funcs.writefile({'Test file Xxx1'}, 'Xxx1')
+ funcs.writefile({'Test file Xxx2'}, 'Xxx2')
+
+ local content = [[
+ func UnloadAllBufs()
+ let i = 1
+ while i <= bufnr('$')
+ if i != bufnr('%') && bufloaded(i)
+ exe i . 'bunload'
+ endif
+ let i += 1
+ endwhile
+ endfunc
+ au BufUnload * call UnloadAllBufs()
+ au VimLeave * call writefile(['Test Finished'], 'Xout')
+ set nohidden
+ edit Xxx1
+ split Xxx2
+ q
+ ]]
+
+ funcs.writefile(funcs.split(content, "\n"), 'Xtest')
+
+ funcs.delete('Xout')
+ funcs.system(meths.get_vvar('progpath') .. ' -u NORC -i NONE -N -S Xtest')
+ eq(1, funcs.filereadable('Xout'))
+
+ funcs.delete('Xxx1')
+ funcs.delete('Xxx2')
+ funcs.delete('Xtest')
+ funcs.delete('Xout')
+ end)
+end)
diff --git a/test/functional/autocmd/autocmd_spec.lua b/test/functional/autocmd/autocmd_spec.lua
index 93d71a9e45..f37f04b32f 100644
--- a/test/functional/autocmd/autocmd_spec.lua
+++ b/test/functional/autocmd/autocmd_spec.lua
@@ -2,8 +2,10 @@ local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local assert_visible = helpers.assert_visible
+local assert_alive = helpers.assert_alive
local dedent = helpers.dedent
local eq = helpers.eq
+local neq = helpers.neq
local eval = helpers.eval
local feed = helpers.feed
local clear = helpers.clear
@@ -13,6 +15,7 @@ local funcs = helpers.funcs
local expect = helpers.expect
local command = helpers.command
local exc_exec = helpers.exc_exec
+local exec_lua = helpers.exec_lua
local curbufmeths = helpers.curbufmeths
local source = helpers.source
@@ -333,6 +336,68 @@ describe('autocmd', function()
pcall_err(command, "call nvim_set_current_win(g:winid)"))
end)
+ it("`aucmd_win` cannot be changed into a normal window #13699", function()
+ local screen = Screen.new(50, 10)
+ screen:attach()
+ screen:set_default_attr_ids {
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {reverse = true},
+ [3] = {bold = true, reverse = true},
+ }
+
+ -- Create specific layout and ensure it's left unchanged.
+ -- Use nvim_buf_call on a hidden buffer so aucmd_win is used.
+ exec_lua [[
+ vim.cmd "wincmd s | wincmd _"
+ _G.buf = vim.api.nvim_create_buf(true, true)
+ vim.api.nvim_buf_call(_G.buf, function() vim.cmd "wincmd J" end)
+ ]]
+ screen:expect [[
+ ^ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {3:[No Name] }|
+ |
+ {2:[No Name] }|
+ |
+ ]]
+ -- This used to crash after making aucmd_win a normal window via the above.
+ exec_lua [[
+ vim.cmd "tabnew | tabclose # | wincmd s | wincmd _"
+ vim.api.nvim_buf_call(_G.buf, function() vim.cmd "wincmd K" end)
+ ]]
+ assert_alive()
+ screen:expect_unchanged()
+
+ -- Ensure splitting still works from inside the aucmd_win.
+ exec_lua [[vim.api.nvim_buf_call(_G.buf, function() vim.cmd "split" end)]]
+ screen:expect [[
+ ^ |
+ {1:~ }|
+ {3:[No Name] }|
+ |
+ {1:~ }|
+ {2:[Scratch] }|
+ |
+ {1:~ }|
+ {2:[No Name] }|
+ |
+ ]]
+
+ -- After all of our messing around, aucmd_win should still be floating.
+ -- Use :only to ensure _G.buf is hidden again (so the aucmd_win is used).
+ eq("editor", exec_lua [[
+ vim.cmd "only"
+ vim.api.nvim_buf_call(_G.buf, function()
+ _G.config = vim.api.nvim_win_get_config(0)
+ end)
+ return _G.config.relative
+ ]])
+ end)
+
it(':doautocmd does not warn "No matching autocommands" #10689', function()
local screen = Screen.new(32, 3)
screen:attach()
@@ -354,4 +419,106 @@ describe('autocmd', function()
:doautocmd SessionLoadPost |
]]}
end)
+
+ describe('old_tests', function()
+ it('vimscript: WinNew ++once', function()
+ source [[
+ " Without ++once WinNew triggers twice
+ let g:did_split = 0
+ augroup Testing
+ au!
+ au WinNew * let g:did_split += 1
+ augroup END
+ split
+ split
+ call assert_equal(2, g:did_split)
+ call assert_true(exists('#WinNew'))
+ close
+ close
+
+ " With ++once WinNew triggers once
+ let g:did_split = 0
+ augroup Testing
+ au!
+ au WinNew * ++once let g:did_split += 1
+ augroup END
+ split
+ split
+ call assert_equal(1, g:did_split)
+ call assert_false(exists('#WinNew'))
+ close
+ close
+
+ call assert_fails('au WinNew * ++once ++once echo bad', 'E983:')
+ ]]
+
+ meths.set_var('did_split', 0)
+
+ source [[
+ augroup Testing
+ au!
+ au WinNew * let g:did_split += 1
+ augroup END
+
+ split
+ split
+ ]]
+
+ eq(2, meths.get_var('did_split'))
+ eq(1, funcs.exists('#WinNew'))
+
+ -- Now with once
+ meths.set_var('did_split', 0)
+
+ source [[
+ augroup Testing
+ au!
+ au WinNew * ++once let g:did_split += 1
+ augroup END
+
+ split
+ split
+ ]]
+
+ eq(1, meths.get_var('did_split'))
+ eq(0, funcs.exists('#WinNew'))
+
+ -- call assert_fails('au WinNew * ++once ++once echo bad', 'E983:')
+ local ok, msg = pcall(source, [[
+ au WinNew * ++once ++once echo bad
+ ]])
+
+ eq(false, ok)
+ eq(true, not not string.find(msg, 'E983:'))
+ end)
+
+ it('should have autocmds in filetypedetect group', function()
+ source [[filetype on]]
+ neq({}, meths.get_autocmds { group = "filetypedetect" })
+ end)
+
+ it('should not access freed mem', function()
+ source [[
+ au BufEnter,BufLeave,WinEnter,WinLeave 0 vs xxx
+ arg 0
+ argadd
+ all
+ all
+ au!
+ bwipe xxx
+ ]]
+ end)
+
+ it('should allow comma-separated patterns', function()
+ source [[
+ augroup TestingPatterns
+ au!
+ autocmd BufReadCmd *.shada,*.shada.tmp.[a-z] echo 'hello'
+ autocmd BufReadCmd *.shada,*.shada.tmp.[a-z] echo 'hello'
+ augroup END
+ ]]
+
+ eq(4, #meths.get_autocmds { event = "BufReadCmd", group = "TestingPatterns" })
+ end)
+ end)
end)
diff --git a/test/functional/autocmd/cursormoved_spec.lua b/test/functional/autocmd/cursormoved_spec.lua
index 9641d4b096..85d8628d7e 100644
--- a/test/functional/autocmd/cursormoved_spec.lua
+++ b/test/functional/autocmd/cursormoved_spec.lua
@@ -35,6 +35,7 @@ describe('CursorMoved', function()
it("is not triggered by cursor movement prior to first CursorMoved instantiation", function()
source([[
let g:cursormoved = 0
+ autocmd! CursorMoved
autocmd CursorMoved * let g:cursormoved += 1
]])
eq(0, eval('g:cursormoved'))
diff --git a/test/functional/autocmd/show_spec.lua b/test/functional/autocmd/show_spec.lua
new file mode 100644
index 0000000000..59757a7d61
--- /dev/null
+++ b/test/functional/autocmd/show_spec.lua
@@ -0,0 +1,35 @@
+local helpers = require('test.functional.helpers')(after_each)
+
+local clear = helpers.clear
+local command = helpers.command
+local dedent = helpers.dedent
+local eq = helpers.eq
+local funcs = helpers.funcs
+
+describe(":autocmd", function()
+ before_each(clear)
+
+ it("should not segfault when you just do autocmd", function()
+ command ":autocmd"
+ end)
+
+ it("should filter based on ++once", function()
+ command "autocmd! BufEnter"
+ command "autocmd BufEnter * :echo 'Hello'"
+ command [[augroup TestingOne]]
+ command [[ autocmd BufEnter * :echo "Line 1"]]
+ command [[ autocmd BufEnter * :echo "Line 2"]]
+ command [[augroup END]]
+
+ eq(dedent([[
+
+ --- Autocommands ---
+ BufEnter
+ * :echo 'Hello'
+ TestingOne BufEnter
+ * :echo "Line 1"
+ :echo "Line 2"]]),
+ funcs.execute('autocmd BufEnter'))
+
+ end)
+end)
diff --git a/test/functional/autocmd/termxx_spec.lua b/test/functional/autocmd/termxx_spec.lua
index 1e8f981437..bc7a6d6c36 100644
--- a/test/functional/autocmd/termxx_spec.lua
+++ b/test/functional/autocmd/termxx_spec.lua
@@ -32,7 +32,7 @@ describe('autocmd TermClose', function()
retry(nil, nil, function() eq(23, eval('g:test_termclose')) end)
end)
- it('kills job trapping SIGTERM', function()
+ pending('kills job trapping SIGTERM', function()
if iswin() then return end
nvim('set_option', 'shell', 'sh')
nvim('set_option', 'shellcmdflag', '-c')
@@ -52,7 +52,7 @@ describe('autocmd TermClose', function()
ok(duration <= 4000) -- Epsilon for slow CI
end)
- it('kills PTY job trapping SIGHUP and SIGTERM', function()
+ pending('kills PTY job trapping SIGHUP and SIGTERM', function()
if iswin() then return end
nvim('set_option', 'shell', 'sh')
nvim('set_option', 'shellcmdflag', '-c')
diff --git a/test/functional/ex_cmds/map_spec.lua b/test/functional/ex_cmds/map_spec.lua
index 84d5bc2335..007d68d61a 100644
--- a/test/functional/ex_cmds/map_spec.lua
+++ b/test/functional/ex_cmds/map_spec.lua
@@ -1,11 +1,15 @@
local helpers = require("test.functional.helpers")(after_each)
+local Screen = require('test.functional.ui.screen')
local eq = helpers.eq
+local exec = helpers.exec
local feed = helpers.feed
local meths = helpers.meths
local clear = helpers.clear
local command = helpers.command
local expect = helpers.expect
+local insert = helpers.insert
+local eval = helpers.eval
describe(':*map', function()
before_each(clear)
@@ -25,4 +29,199 @@ describe(':*map', function()
feed('i-<M-">-')
expect('-foo-')
end)
+
+ it('<Plug> keymaps ignore nore', function()
+ command('let x = 0')
+ eq(0, meths.eval('x'))
+ command [[
+ nnoremap <Plug>(Increase_x) <cmd>let x+=1<cr>
+ nmap increase_x_remap <Plug>(Increase_x)
+ nnoremap increase_x_noremap <Plug>(Increase_x)
+ ]]
+ feed('increase_x_remap')
+ eq(1, meths.eval('x'))
+ feed('increase_x_noremap')
+ eq(2, meths.eval('x'))
+ end)
+ it("Doesn't auto ignore nore for keys before or after <Plug> keymap", function()
+ command('let x = 0')
+ eq(0, meths.eval('x'))
+ command [[
+ nnoremap x <nop>
+ nnoremap <Plug>(Increase_x) <cmd>let x+=1<cr>
+ nmap increase_x_remap x<Plug>(Increase_x)x
+ nnoremap increase_x_noremap x<Plug>(Increase_x)x
+ ]]
+ insert("Some text")
+ eq('Some text', eval("getline('.')"))
+
+ feed('increase_x_remap')
+ eq(1, meths.eval('x'))
+ eq('Some text', eval("getline('.')"))
+ feed('increase_x_noremap')
+ eq(2, meths.eval('x'))
+ eq('Some te', eval("getline('.')"))
+ end)
+end)
+
+describe(':*map cursor and redrawing', function()
+ local screen
+ before_each(function()
+ clear()
+ screen = Screen.new(20, 5)
+ screen:attach()
+ end)
+
+ it('cursor is restored after :map <expr> which calls input()', function()
+ command('map <expr> x input("> ")')
+ screen:expect([[
+ ^ |
+ ~ |
+ ~ |
+ ~ |
+ |
+ ]])
+ feed('x')
+ screen:expect([[
+ |
+ ~ |
+ ~ |
+ ~ |
+ > ^ |
+ ]])
+ feed('\n')
+ screen:expect([[
+ ^ |
+ ~ |
+ ~ |
+ ~ |
+ > |
+ ]])
+ end)
+
+ it('cursor is restored after :imap <expr> which calls input()', function()
+ command('imap <expr> x input("> ")')
+ feed('i')
+ screen:expect([[
+ ^ |
+ ~ |
+ ~ |
+ ~ |
+ -- INSERT -- |
+ ]])
+ feed('x')
+ screen:expect([[
+ |
+ ~ |
+ ~ |
+ ~ |
+ > ^ |
+ ]])
+ feed('\n')
+ screen:expect([[
+ ^ |
+ ~ |
+ ~ |
+ ~ |
+ > |
+ ]])
+ end)
+
+ it('cursor is restored after :map <expr> which redraws statusline vim-patch:8.1.2336', function()
+ exec([[
+ call setline(1, ['one', 'two', 'three'])
+ 2
+ set ls=2
+ hi! link StatusLine ErrorMsg
+ noremap <expr> <C-B> Func()
+ func Func()
+ let g:on = !get(g:, 'on', 0)
+ redraws
+ return ''
+ endfunc
+ func Status()
+ return get(g:, 'on', 0) ? '[on]' : ''
+ endfunc
+ set stl=%{Status()}
+ ]])
+ feed('<C-B>')
+ screen:expect([[
+ one |
+ ^two |
+ three |
+ [on] |
+ |
+ ]])
+ end)
+
+ it('error in :nmap <expr> does not mess up display vim-patch:4.2.4338', function()
+ screen:try_resize(40, 5)
+ command('nmap <expr> <F2> execute("throw 42")')
+ feed('<F2>')
+ screen:expect([[
+ |
+ |
+ Error detected while processing : |
+ E605: Exception not caught: 42 |
+ Press ENTER or type command to continue^ |
+ ]])
+ feed('<CR>')
+ screen:expect([[
+ ^ |
+ ~ |
+ ~ |
+ ~ |
+ |
+ ]])
+ end)
+
+ it('error in :cmap <expr> handled correctly vim-patch:4.2.4338', function()
+ screen:try_resize(40, 5)
+ command('cmap <expr> <F2> execute("throw 42")')
+ feed(':echo "foo')
+ screen:expect([[
+ |
+ ~ |
+ ~ |
+ ~ |
+ :echo "foo^ |
+ ]])
+ feed('<F2>')
+ screen:expect([[
+ |
+ :echo "foo |
+ Error detected while processing : |
+ E605: Exception not caught: 42 |
+ :echo "foo^ |
+ ]])
+ feed('"')
+ screen:expect([[
+ |
+ :echo "foo |
+ Error detected while processing : |
+ E605: Exception not caught: 42 |
+ :echo "foo"^ |
+ ]])
+ feed('\n')
+ screen:expect([[
+ :echo "foo |
+ Error detected while processing : |
+ E605: Exception not caught: 42 |
+ foo |
+ Press ENTER or type command to continue^ |
+ ]])
+ end)
+
+ it('listing mappings clears command line vim-patch:8.2.4401', function()
+ screen:try_resize(40, 5)
+ command('nmap a b')
+ feed(': nmap a<CR>')
+ screen:expect([[
+ ^ |
+ ~ |
+ ~ |
+ ~ |
+ n a b |
+ ]])
+ end)
end)
diff --git a/test/functional/ex_cmds/mksession_spec.lua b/test/functional/ex_cmds/mksession_spec.lua
index d11a2cef99..2702fb196f 100644
--- a/test/functional/ex_cmds/mksession_spec.lua
+++ b/test/functional/ex_cmds/mksession_spec.lua
@@ -10,12 +10,11 @@ local funcs = helpers.funcs
local matches = helpers.matches
local pesc = helpers.pesc
local rmdir = helpers.rmdir
-local iswin = helpers.iswin
local file_prefix = 'Xtest-functional-ex_cmds-mksession_spec'
describe(':mksession', function()
- if not iswin() then
+ if helpers.pending_win32(pending) then return end
local session_file = file_prefix .. '.vim'
local tab_dir = file_prefix .. '.d'
@@ -117,5 +116,4 @@ describe(':mksession', function()
matches('^term://'..pesc(expected_cwd)..'//%d+:', funcs.expand('%'))
command('qall!')
end)
- end
end)
diff --git a/test/functional/lua/thread_spec.lua b/test/functional/lua/thread_spec.lua
new file mode 100644
index 0000000000..2e0ab7bdff
--- /dev/null
+++ b/test/functional/lua/thread_spec.lua
@@ -0,0 +1,386 @@
+local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
+local assert_alive = helpers.assert_alive
+local clear = helpers.clear
+local feed = helpers.feed
+local eq = helpers.eq
+local exec_lua = helpers.exec_lua
+local next_msg = helpers.next_msg
+local NIL = helpers.NIL
+local pcall_err = helpers.pcall_err
+
+describe('thread', function()
+ local screen
+
+ before_each(function()
+ clear()
+ screen = Screen.new(50, 10)
+ screen:attach()
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {bold = true, reverse = true},
+ [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ [4] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ [5] = {bold = true},
+ })
+ end)
+
+ it('entry func is executed in protected mode', function()
+ exec_lua [[
+ local thread = vim.loop.new_thread(function()
+ error('Error in thread entry func')
+ end)
+ vim.loop.thread_join(thread)
+ ]]
+
+ screen:expect([[
+ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {2: }|
+ {3:Error in luv thread:} |
+ {3:[string "<nvim>"]:2: Error in thread entry func} |
+ {4:Press ENTER or type command to continue}^ |
+ ]])
+ feed('<cr>')
+ assert_alive()
+ end)
+
+ it('callback is executed in protected mode', function()
+ exec_lua [[
+ local thread = vim.loop.new_thread(function()
+ local timer = vim.loop.new_timer()
+ local function ontimeout()
+ timer:stop()
+ timer:close()
+ error('Error in thread callback')
+ end
+ timer:start(10, 0, ontimeout)
+ vim.loop.run()
+ end)
+ vim.loop.thread_join(thread)
+ ]]
+
+ screen:expect([[
+ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {2: }|
+ {3:Error in luv callback, thread:} |
+ {3:[string "<nvim>"]:6: Error in thread callback} |
+ {4:Press ENTER or type command to continue}^ |
+ ]])
+ feed('<cr>')
+ assert_alive()
+ end)
+
+ describe('print', function()
+ it('works', function()
+ exec_lua [[
+ local thread = vim.loop.new_thread(function()
+ print('print in thread')
+ end)
+ vim.loop.thread_join(thread)
+ ]]
+
+ screen:expect([[
+ ^ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ print in thread |
+ ]])
+ end)
+ end)
+
+ describe('vim.*', function()
+ before_each(function()
+ clear()
+ exec_lua [[
+ Thread_Test = {}
+
+ Thread_Test.entry_func = function(async, entry_str, args)
+ local decoded_args = vim.mpack.decode(args)
+ assert(loadstring(entry_str))(async, decoded_args)
+ end
+
+ function Thread_Test:do_test()
+ local async
+ local on_async = self.on_async
+ async = vim.loop.new_async(function(ret)
+ on_async(ret)
+ async:close()
+ end)
+ local thread =
+ vim.loop.new_thread(self.entry_func, async, self.entry_str, self.args)
+ vim.loop.thread_join(thread)
+ end
+
+ Thread_Test.new = function(entry, on_async, ...)
+ self = {}
+ setmetatable(self, {__index = Thread_Test})
+ self.args = vim.mpack.encode({...})
+ self.entry_str = string.dump(entry)
+ self.on_async = on_async
+ return self
+ end
+ ]]
+ end)
+
+ it('is_thread', function()
+ exec_lua [[
+ local entry = function(async)
+ async:send(vim.is_thread())
+ end
+ local on_async = function(ret)
+ vim.rpcnotify(1, 'result', ret)
+ end
+ local thread_test = Thread_Test.new(entry, on_async)
+ thread_test:do_test()
+ ]]
+
+ eq({'notification', 'result', {true}}, next_msg())
+ end)
+
+ it('loop', function()
+ exec_lua [[
+ local entry = function(async)
+ async:send(vim.loop.version())
+ end
+ local on_async = function(ret)
+ vim.rpcnotify(1, ret)
+ end
+ local thread_test = Thread_Test.new(entry, on_async)
+ thread_test:do_test()
+ ]]
+
+ local msg = next_msg()
+ eq(msg[1], 'notification')
+ assert(tonumber(msg[2]) >= 72961)
+ end)
+
+ it('mpack', function()
+ exec_lua [[
+ local entry = function(async)
+ async:send(vim.mpack.encode({33, vim.NIL, 'text'}))
+ end
+ local on_async = function(ret)
+ vim.rpcnotify(1, 'result', vim.mpack.decode(ret))
+ end
+ local thread_test = Thread_Test.new(entry, on_async)
+ thread_test:do_test()
+ ]]
+
+ eq({'notification', 'result', {{33, NIL, 'text'}}}, next_msg())
+ end)
+
+ it('json', function()
+ exec_lua [[
+ local entry = function(async)
+ async:send(vim.json.encode({33, vim.NIL, 'text'}))
+ end
+ local on_async = function(ret)
+ vim.rpcnotify(1, 'result', vim.json.decode(ret))
+ end
+ local thread_test = Thread_Test.new(entry, on_async)
+ thread_test:do_test()
+ ]]
+
+ eq({'notification', 'result', {{33, NIL, 'text'}}}, next_msg())
+ end)
+
+ it('diff', function()
+ exec_lua [[
+ local entry = function(async)
+ async:send(vim.diff('Hello\n', 'Helli\n'))
+ end
+ local on_async = function(ret)
+ vim.rpcnotify(1, 'result', ret)
+ end
+ local thread_test = Thread_Test.new(entry, on_async)
+ thread_test:do_test()
+ ]]
+
+ eq({'notification', 'result',
+ {table.concat({
+ '@@ -1 +1 @@',
+ '-Hello',
+ '+Helli',
+ ''
+ }, '\n')}},
+ next_msg())
+ end)
+ end)
+end)
+
+describe('threadpool', function()
+ before_each(clear)
+
+ it('is_thread', function()
+ eq(false, exec_lua [[return vim.is_thread()]])
+
+ exec_lua [[
+ local work_fn = function()
+ return vim.is_thread()
+ end
+ local after_work_fn = function(ret)
+ vim.rpcnotify(1, 'result', ret)
+ end
+ local work = vim.loop.new_work(work_fn, after_work_fn)
+ work:queue()
+ ]]
+
+ eq({'notification', 'result', {true}}, next_msg())
+ end)
+
+ it('with invalid argument', function()
+ local status = pcall_err(exec_lua, [[
+ local work = vim.loop.new_thread(function() end, function() end)
+ work:queue({})
+ ]])
+
+ eq([[Error executing lua: [string "<nvim>"]:0: Error: thread arg not support type 'function' at 1]],
+ status)
+ end)
+
+ it('with invalid return value', function()
+ local screen = Screen.new(50, 10)
+ screen:attach()
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {bold = true, reverse = true},
+ [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ [4] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ [5] = {bold = true},
+ })
+
+ exec_lua [[
+ local work = vim.loop.new_work(function() return {} end, function() end)
+ work:queue()
+ ]]
+
+ screen:expect([[
+ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {2: }|
+ {3:Error in luv thread:} |
+ {3:Error: thread arg not support type 'table' at 1} |
+ {4:Press ENTER or type command to continue}^ |
+ ]])
+ end)
+
+ describe('vim.*', function()
+ before_each(function()
+ clear()
+ exec_lua [[
+ Threadpool_Test = {}
+
+ Threadpool_Test.work_fn = function(work_fn_str, args)
+ local decoded_args = vim.mpack.decode(args)
+ return assert(loadstring(work_fn_str))(decoded_args)
+ end
+
+ function Threadpool_Test:do_test()
+ local work =
+ vim.loop.new_work(self.work_fn, self.after_work)
+ work:queue(self.work_fn_str, self.args)
+ end
+
+ Threadpool_Test.new = function(work_fn, after_work, ...)
+ self = {}
+ setmetatable(self, {__index = Threadpool_Test})
+ self.args = vim.mpack.encode({...})
+ self.work_fn_str = string.dump(work_fn)
+ self.after_work = after_work
+ return self
+ end
+ ]]
+ end)
+
+ it('loop', function()
+ exec_lua [[
+ local work_fn = function()
+ return vim.loop.version()
+ end
+ local after_work_fn = function(ret)
+ vim.rpcnotify(1, ret)
+ end
+ local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn)
+ threadpool_test:do_test()
+ ]]
+
+ local msg = next_msg()
+ eq(msg[1], 'notification')
+ assert(tonumber(msg[2]) >= 72961)
+ end)
+
+ it('mpack', function()
+ exec_lua [[
+ local work_fn = function()
+ local var = vim.mpack.encode({33, vim.NIL, 'text'})
+ return var
+ end
+ local after_work_fn = function(ret)
+ vim.rpcnotify(1, 'result', vim.mpack.decode(ret))
+ end
+ local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn)
+ threadpool_test:do_test()
+ ]]
+
+ eq({'notification', 'result', {{33, NIL, 'text'}}}, next_msg())
+ end)
+
+ it('json', function()
+ exec_lua [[
+ local work_fn = function()
+ local var = vim.json.encode({33, vim.NIL, 'text'})
+ return var
+ end
+ local after_work_fn = function(ret)
+ vim.rpcnotify(1, 'result', vim.json.decode(ret))
+ end
+ local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn)
+ threadpool_test:do_test()
+ ]]
+
+ eq({'notification', 'result', {{33, NIL, 'text'}}}, next_msg())
+ end)
+
+ it('work', function()
+ exec_lua [[
+ local work_fn = function()
+ return vim.diff('Hello\n', 'Helli\n')
+ end
+ local after_work_fn = function(ret)
+ vim.rpcnotify(1, 'result', ret)
+ end
+ local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn)
+ threadpool_test:do_test()
+ ]]
+
+ eq({'notification', 'result',
+ {table.concat({
+ '@@ -1 +1 @@',
+ '-Hello',
+ '+Helli',
+ ''
+ }, '\n')}},
+ next_msg())
+ end)
+ end)
+end)
diff --git a/test/functional/plugin/shada_spec.lua b/test/functional/plugin/shada_spec.lua
index a4d78682ad..6f22f865e6 100644
--- a/test/functional/plugin/shada_spec.lua
+++ b/test/functional/plugin/shada_spec.lua
@@ -1,7 +1,7 @@
local helpers = require('test.functional.helpers')(after_each)
local clear = helpers.clear
-local eq, nvim_eval, nvim_command, nvim, exc_exec, funcs, nvim_feed, curbuf =
- helpers.eq, helpers.eval, helpers.command, helpers.nvim, helpers.exc_exec,
+local eq, meths, nvim_eval, nvim_command, nvim, exc_exec, funcs, nvim_feed, curbuf =
+ helpers.eq, helpers.meths, helpers.eval, helpers.command, helpers.nvim, helpers.exc_exec,
helpers.funcs, helpers.feed, helpers.curbuf
local neq = helpers.neq
local read_file = helpers.read_file
@@ -2162,6 +2162,10 @@ describe('plugin/shada.vim', function()
wshada('\004\000\009\147\000\196\002ab\196\001a')
wshada_tmp('\004\000\009\147\000\196\002ab\196\001b')
+
+ local bufread_commands = meths.get_autocmds({ group = "ShaDaCommands", event = "BufReadCmd" })
+ eq(2, #bufread_commands--[[, vim.inspect(bufread_commands) ]])
+
-- Need to set nohidden so that the buffer containing 'fname' is not unloaded
-- after loading 'fname_tmp', otherwise the '++opt not supported' test below
-- won't work since the BufReadCmd autocmd won't be triggered.
diff --git a/test/functional/terminal/cursor_spec.lua b/test/functional/terminal/cursor_spec.lua
index d120995c82..3b905f1f56 100644
--- a/test/functional/terminal/cursor_spec.lua
+++ b/test/functional/terminal/cursor_spec.lua
@@ -8,7 +8,6 @@ local eq, eval = helpers.eq, helpers.eval
local feed_command = helpers.feed_command
local hide_cursor = thelpers.hide_cursor
local show_cursor = thelpers.show_cursor
-local iswin = helpers.iswin
describe(':terminal cursor', function()
local screen
@@ -178,7 +177,7 @@ describe('cursor with customized highlighting', function()
end)
describe('buffer cursor position is correct in terminal without number column', function()
- if not iswin() then
+ if helpers.pending_win32(pending) then return end
local screen
local function setup_ex_register(str)
@@ -526,11 +525,10 @@ describe('buffer cursor position is correct in terminal without number column',
eq({6, 1}, eval('nvim_win_get_cursor(0)'))
end)
end)
- end
end)
describe('buffer cursor position is correct in terminal with number column', function()
- if not iswin() then
+ if helpers.pending_win32(pending) then return end
local screen
local function setup_ex_register(str)
@@ -881,5 +879,4 @@ describe('buffer cursor position is correct in terminal with number column', fun
eq({6, 1}, eval('nvim_win_get_cursor(0)'))
end)
end)
- end
end)
diff --git a/test/functional/ui/cmdline_highlight_spec.lua b/test/functional/ui/cmdline_highlight_spec.lua
index 9c746b99bd..384761ab17 100644
--- a/test/functional/ui/cmdline_highlight_spec.lua
+++ b/test/functional/ui/cmdline_highlight_spec.lua
@@ -33,7 +33,7 @@ before_each(function()
let g:NUM_LVLS = 4
function Redraw()
mode
- return ''
+ return "\<Ignore>"
endfunction
let g:id = ''
cnoremap <expr> {REDRAW} Redraw()
@@ -42,7 +42,7 @@ before_each(function()
let Cb = g:Nvim_color_input{g:id}
let out = input({'prompt': ':', 'highlight': Cb})
let g:out{id} = out
- return (a:do_return ? out : '')
+ return (a:do_return ? out : "\<Ignore>")
endfunction
nnoremap <expr> {PROMPT} DoPrompt(0)
cnoremap <expr> {PROMPT} DoPrompt(1)
@@ -410,7 +410,7 @@ describe('Command-line coloring', function()
end)
it('stops executing callback after a number of errors', function()
set_color_cb('SplittedMultibyteStart')
- start_prompt('let x = "«»«»«»«»«»"\n')
+ start_prompt('let x = "«»«»«»«»«»"')
screen:expect([[
{EOB:~ }|
{EOB:~ }|
@@ -419,7 +419,7 @@ describe('Command-line coloring', function()
:let x = " |
{ERR:E5405: Chunk 0 start 10 splits multibyte}|
{ERR: character} |
- ^:let x = "«»«»«»«»«»" |
+ :let x = "«»«»«»«»«»"^ |
]])
feed('\n')
screen:expect([[
@@ -432,6 +432,7 @@ describe('Command-line coloring', function()
{EOB:~ }|
|
]])
+ feed('\n')
eq('let x = "«»«»«»«»«»"', meths.get_var('out'))
local msg = '\nE5405: Chunk 0 start 10 splits multibyte character'
eq(msg:rep(1), funcs.execute('messages'))
@@ -474,14 +475,14 @@ describe('Command-line coloring', function()
]])
feed('\n')
screen:expect([[
- |
+ ^ |
{EOB:~ }|
{EOB:~ }|
{EOB:~ }|
{EOB:~ }|
{EOB:~ }|
{EOB:~ }|
- ^:echo 42 |
+ :echo 42 |
]])
feed('\n')
eq('echo 42', meths.get_var('out'))
diff --git a/test/functional/ui/output_spec.lua b/test/functional/ui/output_spec.lua
index 7df63aca50..7305baa761 100644
--- a/test/functional/ui/output_spec.lua
+++ b/test/functional/ui/output_spec.lua
@@ -14,7 +14,7 @@ local has_powershell = helpers.has_powershell
local set_shell_powershell = helpers.set_shell_powershell
describe("shell command :!", function()
- if not iswin() then
+ if helpers.pending_win32(pending) then return end
local screen
before_each(function()
clear()
@@ -78,7 +78,6 @@ describe("shell command :!", function()
[10] = {foreground = 2},
})
end)
- end
end)
describe("shell command :!", function()
diff --git a/test/helpers.lua b/test/helpers.lua
index 333e98d495..7d2f8f760a 100644
--- a/test/helpers.lua
+++ b/test/helpers.lua
@@ -58,9 +58,9 @@ local check_logs_useless_lines = {
--- Invokes `fn` and includes the tail of `logfile` in the error message if it
--- fails.
---
----@param logfile Log file, defaults to $NVIM_LOG_FILE or '.nvimlog'
----@param fn Function to invoke
----@param ... Function arguments
+---@param logfile string Log file, defaults to $NVIM_LOG_FILE or '.nvimlog'
+---@param fn string Function to invoke
+---@param ... string Function arguments
local function dumplog(logfile, fn, ...)
-- module.validate({
-- logfile={logfile,'s',true},
@@ -102,8 +102,8 @@ end
--- Asserts that `pat` matches one or more lines in the tail of $NVIM_LOG_FILE.
---
----@param pat (string) Lua pattern to search for in the log file.
----@param logfile (string, default=$NVIM_LOG_FILE) full path to log file.
+---@param pat string Lua pattern to search for in the log file
+---@param logfile string Full path to log file (default=$NVIM_LOG_FILE)
function module.assert_log(pat, logfile)
logfile = logfile or os.getenv('NVIM_LOG_FILE') or '.nvimlog'
local nrlines = 10