aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames McCoy <jamessan@jamessan.com>2019-12-15 21:17:16 -0500
committerJames McCoy <jamessan@jamessan.com>2019-12-15 21:17:16 -0500
commit6566251d144d2c9c9e08e05c8c3a3fe9915a19b8 (patch)
tree980fd0c7287e295c8ac72858b0794f72224415e0
parent9c4223215f71e1212462ada4e698be1b31437dd9 (diff)
parent9f3d483c79f03c48239fdc82cc02e8685a03d22a (diff)
downloadrneovim-6566251d144d2c9c9e08e05c8c3a3fe9915a19b8.tar.gz
rneovim-6566251d144d2c9c9e08e05c8c3a3fe9915a19b8.tar.bz2
rneovim-6566251d144d2c9c9e08e05c8c3a3fe9915a19b8.zip
Merge remote-tracking branch 'upstream/master' into libcall
-rw-r--r--.builds/freebsd.yml44
-rw-r--r--.builds/openbsd.yml30
-rwxr-xr-x.gitattributes1
-rw-r--r--.gitignore3
-rw-r--r--.luacheckrc2
-rw-r--r--.luacov22
-rw-r--r--.travis.yml57
-rw-r--r--CMakeLists.txt79
-rw-r--r--CONTRIBUTING.md25
-rw-r--r--Makefile24
-rw-r--r--README.md2
-rw-r--r--appveyor.yml5
-rwxr-xr-xci/before_install.sh10
-rwxr-xr-xci/before_script.sh5
-rw-r--r--ci/build.ps153
-rw-r--r--ci/common/build.sh6
-rwxr-xr-xci/common/submit_coverage.sh11
-rwxr-xr-xci/install.sh26
-rwxr-xr-xci/run_lint.sh4
-rwxr-xr-xci/run_tests.sh2
-rwxr-xr-xci/snap/after_success.sh14
-rwxr-xr-xci/snap/deploy.sh21
-rwxr-xr-xci/snap/install.sh10
-rwxr-xr-xci/snap/script.sh8
-rw-r--r--ci/snap/travis_snapcraft.cfgbin0 -> 2448 bytes
-rw-r--r--cmake/FindUTF8PROC.cmake16
-rw-r--r--cmake/GetCompileFlags.cmake7
-rw-r--r--cmake/GetGitRevisionDescription.cmake180
-rw-r--r--cmake/GetGitRevisionDescription.cmake.in38
-rw-r--r--codecov.yml3
-rw-r--r--config/versiondef.h.in4
-rw-r--r--contrib/local.mk.example7
-rw-r--r--runtime/autoload/health/provider.vim52
-rw-r--r--runtime/autoload/man.vim105
-rw-r--r--runtime/autoload/netrw.vim36
-rw-r--r--runtime/autoload/provider/pythonx.vim11
-rw-r--r--runtime/autoload/spellfile.vim7
-rw-r--r--runtime/doc/api.txt291
-rw-r--r--runtime/doc/autocmd.txt6
-rw-r--r--runtime/doc/change.txt11
-rw-r--r--runtime/doc/cmdline.txt8
-rw-r--r--runtime/doc/deprecated.txt2
-rw-r--r--runtime/doc/develop.txt90
-rw-r--r--runtime/doc/digraph.txt4
-rw-r--r--runtime/doc/eval.txt140
-rw-r--r--runtime/doc/filetype.txt11
-rw-r--r--runtime/doc/help.txt2
-rw-r--r--runtime/doc/if_lua.txt666
-rw-r--r--runtime/doc/index.txt11
-rw-r--r--runtime/doc/intro.txt12
-rw-r--r--runtime/doc/lsp.txt575
-rw-r--r--runtime/doc/lua.txt1001
-rw-r--r--runtime/doc/map.txt2
-rw-r--r--runtime/doc/message.txt7
-rw-r--r--runtime/doc/motion.txt128
-rw-r--r--runtime/doc/msgpack_rpc.txt7
-rw-r--r--runtime/doc/options.txt121
-rw-r--r--runtime/doc/quickfix.txt30
-rw-r--r--runtime/doc/quickref.txt2
-rw-r--r--runtime/doc/starting.txt2
-rw-r--r--runtime/doc/syntax.txt5
-rw-r--r--runtime/doc/tagsrch.txt66
-rw-r--r--runtime/doc/ui.txt14
-rw-r--r--runtime/doc/usr_25.txt9
-rw-r--r--runtime/doc/vim_diff.txt30
-rw-r--r--runtime/doc/visual.txt8
-rw-r--r--runtime/doc/windows.txt8
-rw-r--r--runtime/filetype.vim12
-rw-r--r--runtime/ftplugin/man.vim7
-rw-r--r--runtime/indent/Makefile14
-rw-r--r--runtime/indent/README.txt2
-rw-r--r--runtime/indent/testdir/README.txt97
-rw-r--r--runtime/indent/testdir/runtest.vim132
-rw-r--r--runtime/indent/testdir/vim.in46
-rw-r--r--runtime/indent/testdir/vim.ok46
-rw-r--r--runtime/keymap/russian-jcukenwintype.vim112
-rw-r--r--runtime/lua/vim/inspect.lua2
-rw-r--r--runtime/lua/vim/lsp.lua958
-rw-r--r--runtime/lua/vim/lsp/buf.lua136
-rw-r--r--runtime/lua/vim/lsp/callbacks.lua223
-rw-r--r--runtime/lua/vim/lsp/log.lua95
-rw-r--r--runtime/lua/vim/lsp/protocol.lua922
-rw-r--r--runtime/lua/vim/lsp/rpc.lua452
-rw-r--r--runtime/lua/vim/lsp/util.lua727
-rw-r--r--runtime/lua/vim/shared.lua333
-rw-r--r--runtime/lua/vim/treesitter.lua73
-rw-r--r--runtime/lua/vim/uri.lua94
-rw-r--r--runtime/makemenu.vim15
-rw-r--r--runtime/nvim.appdata.xml4
-rw-r--r--runtime/optwin.vim5
-rw-r--r--runtime/pack/dist/opt/termdebug/plugin/termdebug.vim12
-rw-r--r--runtime/plugin/man.vim2
-rw-r--r--runtime/synmenu.vim421
-rw-r--r--runtime/syntax/man.vim3
-rw-r--r--runtime/syntax/vim.vim4
-rw-r--r--runtime/tutor/en/vim-01-beginner.tutor40
-rw-r--r--runtime/tutor/en/vim-01-beginner.tutor.json84
-rw-r--r--runtime/tutor/tutor.tutor2
-rwxr-xr-xscripts/gen_vimdoc.py45
-rw-r--r--scripts/lua2dox.lua36
-rwxr-xr-xscripts/release.sh6
-rwxr-xr-xscripts/shadacat.py2
-rwxr-xr-xscripts/update_version_stamp.lua55
-rwxr-xr-xscripts/vim-patch.sh76
-rw-r--r--snap/snapcraft.yaml45
-rw-r--r--src/nvim/CMakeLists.txt64
-rw-r--r--src/nvim/api/buffer.c368
-rw-r--r--src/nvim/api/private/helpers.c126
-rw-r--r--src/nvim/api/private/helpers.h14
-rw-r--r--src/nvim/api/vim.c167
-rw-r--r--src/nvim/auevents.lua6
-rw-r--r--src/nvim/buffer.c116
-rw-r--r--src/nvim/buffer_defs.h39
-rw-r--r--src/nvim/change.c42
-rw-r--r--src/nvim/channel.c30
-rw-r--r--src/nvim/context.c2
-rw-r--r--src/nvim/cursor.c11
-rw-r--r--src/nvim/diff.c36
-rw-r--r--src/nvim/diff.h7
-rw-r--r--src/nvim/digraph.c237
-rw-r--r--src/nvim/edit.c84
-rw-r--r--src/nvim/eval.c656
-rw-r--r--src/nvim/eval.h1
-rw-r--r--src/nvim/eval.lua3
-rw-r--r--src/nvim/eval/typval.c2
-rw-r--r--src/nvim/event/libuv_process.c2
-rw-r--r--src/nvim/event/loop.c4
-rw-r--r--src/nvim/event/process.h1
-rw-r--r--src/nvim/ex_cmds.c332
-rw-r--r--src/nvim/ex_cmds.lua28
-rw-r--r--src/nvim/ex_cmds2.c102
-rw-r--r--src/nvim/ex_cmds_defs.h2
-rw-r--r--src/nvim/ex_docmd.c943
-rw-r--r--src/nvim/ex_getln.c96
-rw-r--r--src/nvim/ex_getln.h2
-rw-r--r--src/nvim/file_search.c4
-rw-r--r--src/nvim/fileio.c66
-rw-r--r--src/nvim/fold.c5
-rw-r--r--src/nvim/generators/gen_char_blob.lua70
-rwxr-xr-xsrc/nvim/generators/gen_declarations.lua4
-rw-r--r--src/nvim/getchar.c19
-rw-r--r--src/nvim/globals.h13
-rw-r--r--src/nvim/hashtab.h2
-rw-r--r--src/nvim/highlight.c31
-rw-r--r--src/nvim/highlight_defs.h3
-rw-r--r--src/nvim/lib/kbtree.h6
-rw-r--r--src/nvim/log.c1
-rw-r--r--src/nvim/lua/converter.c56
-rw-r--r--src/nvim/lua/executor.c312
-rw-r--r--src/nvim/lua/executor.h2
-rw-r--r--src/nvim/lua/treesitter.c652
-rw-r--r--src/nvim/lua/treesitter.h14
-rw-r--r--src/nvim/lua/vim.lua234
-rw-r--r--src/nvim/macros.h2
-rw-r--r--src/nvim/main.c22
-rw-r--r--src/nvim/mark.c60
-rw-r--r--src/nvim/mark_extended.c1139
-rw-r--r--src/nvim/mark_extended.h282
-rw-r--r--src/nvim/mark_extended_defs.h54
-rw-r--r--src/nvim/memline.c28
-rw-r--r--src/nvim/memory.c2
-rw-r--r--src/nvim/message.c19
-rw-r--r--src/nvim/misc1.c43
-rw-r--r--src/nvim/mouse.c29
-rw-r--r--src/nvim/move.c53
-rw-r--r--src/nvim/normal.c120
-rw-r--r--src/nvim/ops.c181
-rw-r--r--src/nvim/option.c89
-rw-r--r--src/nvim/option_defs.h22
-rw-r--r--src/nvim/options.lua28
-rw-r--r--src/nvim/os/env.c188
-rw-r--r--src/nvim/os/shell.c3
-rw-r--r--src/nvim/os/tty.c1
-rw-r--r--src/nvim/os/users.c83
-rw-r--r--src/nvim/os_unix.c2
-rw-r--r--src/nvim/path.c35
-rw-r--r--src/nvim/path.h11
-rw-r--r--src/nvim/po/af.po2
-rw-r--r--src/nvim/po/check.vim11
-rw-r--r--src/nvim/po/fi.po2
-rw-r--r--src/nvim/po/uk.po4
-rw-r--r--src/nvim/pos.h4
-rw-r--r--src/nvim/quickfix.c4634
-rw-r--r--src/nvim/regexp_nfa.c2
-rw-r--r--src/nvim/screen.c58
-rw-r--r--src/nvim/search.c164
-rw-r--r--src/nvim/search.h9
-rw-r--r--src/nvim/spell.c97
-rw-r--r--src/nvim/spellfile.c22
-rw-r--r--src/nvim/state.c2
-rw-r--r--src/nvim/strings.c14
-rw-r--r--src/nvim/syntax.c20
-rw-r--r--src/nvim/tag.c1268
-rw-r--r--src/nvim/tag.h27
-rw-r--r--src/nvim/terminal.c21
-rw-r--r--src/nvim/testdir/Makefile25
-rwxr-xr-xsrc/nvim/testdir/runnvim.sh5
-rw-r--r--src/nvim/testdir/runtest.vim5
-rw-r--r--src/nvim/testdir/setup.vim1
-rw-r--r--src/nvim/testdir/shared.vim101
-rw-r--r--src/nvim/testdir/test42.inbin2438 -> 2373 bytes
-rw-r--r--src/nvim/testdir/test48.in82
-rw-r--r--src/nvim/testdir/test48.ok23
-rw-r--r--src/nvim/testdir/test49.vim2
-rw-r--r--src/nvim/testdir/test_alot.vim3
-rw-r--r--src/nvim/testdir/test_assert.vim35
-rw-r--r--src/nvim/testdir/test_autocmd.vim106
-rw-r--r--src/nvim/testdir/test_backup.vim58
-rw-r--r--src/nvim/testdir/test_cindent.vim60
-rw-r--r--src/nvim/testdir/test_clientserver.vim13
-rw-r--r--src/nvim/testdir/test_cmdline.vim132
-rw-r--r--src/nvim/testdir/test_compiler.vim10
-rw-r--r--src/nvim/testdir/test_const.vim20
-rw-r--r--src/nvim/testdir/test_debugger.vim56
-rw-r--r--src/nvim/testdir/test_diffmode.vim25
-rw-r--r--src/nvim/testdir/test_digraph.vim13
-rw-r--r--src/nvim/testdir/test_display.vim56
-rw-r--r--src/nvim/testdir/test_exit.vim88
-rw-r--r--src/nvim/testdir/test_expr.vim3
-rw-r--r--src/nvim/testdir/test_filetype.vim5
-rw-r--r--src/nvim/testdir/test_fnamemodify.vim75
-rw-r--r--src/nvim/testdir/test_fold.vim35
-rw-r--r--src/nvim/testdir/test_functions.vim7
-rw-r--r--src/nvim/testdir/test_gf.vim25
-rw-r--r--src/nvim/testdir/test_gn.vim27
-rw-r--r--src/nvim/testdir/test_goto.vim406
-rw-r--r--src/nvim/testdir/test_help.vim6
-rw-r--r--src/nvim/testdir/test_ins_complete.vim18
-rw-r--r--src/nvim/testdir/test_join.vim21
-rw-r--r--src/nvim/testdir/test_let.vim162
-rw-r--r--src/nvim/testdir/test_mksession_utf8.vim57
-rw-r--r--src/nvim/testdir/test_normal.vim491
-rw-r--r--src/nvim/testdir/test_options.vim6
-rw-r--r--src/nvim/testdir/test_popup.vim53
-rw-r--r--src/nvim/testdir/test_profile.vim268
-rw-r--r--src/nvim/testdir/test_quickfix.vim1261
-rw-r--r--src/nvim/testdir/test_quotestar.vim22
-rw-r--r--src/nvim/testdir/test_recover.vim6
-rw-r--r--src/nvim/testdir/test_registers.vim86
-rw-r--r--src/nvim/testdir/test_search.vim4
-rw-r--r--src/nvim/testdir/test_spell.vim37
-rw-r--r--src/nvim/testdir/test_startup.vim150
-rw-r--r--src/nvim/testdir/test_statusline.vim22
-rw-r--r--src/nvim/testdir/test_substitute.vim12
-rw-r--r--src/nvim/testdir/test_swap.vim84
-rw-r--r--src/nvim/testdir/test_syntax.vim9
-rw-r--r--src/nvim/testdir/test_tagfunc.vim84
-rw-r--r--src/nvim/testdir/test_tagjump.vim47
-rw-r--r--src/nvim/testdir/test_textobjects.vim10
-rw-r--r--src/nvim/testdir/test_timers.vim17
-rw-r--r--src/nvim/testdir/test_vimscript.vim72
-rw-r--r--src/nvim/testdir/test_virtualedit.vim144
-rw-r--r--src/nvim/testdir/test_window_cmd.vim135
-rw-r--r--src/nvim/testdir/view_util.vim1
-rw-r--r--src/nvim/tui/input.c23
-rw-r--r--src/nvim/tui/input.h5
-rw-r--r--src/nvim/tui/terminfo.c3
-rw-r--r--src/nvim/tui/tui.c57
-rw-r--r--src/nvim/ui_compositor.c16
-rw-r--r--src/nvim/undo.c77
-rw-r--r--src/nvim/undo_defs.h16
-rw-r--r--src/nvim/version.c80
-rw-r--r--src/nvim/window.c81
-rw-r--r--src/tree_sitter/LICENSE21
-rw-r--r--src/tree_sitter/alloc.h94
-rw-r--r--src/tree_sitter/api.h660
-rw-r--r--src/tree_sitter/array.h142
-rw-r--r--src/tree_sitter/atomic.h42
-rw-r--r--src/tree_sitter/clock.h141
-rw-r--r--src/tree_sitter/error_costs.h11
-rw-r--r--src/tree_sitter/get_changed_ranges.c482
-rw-r--r--src/tree_sitter/get_changed_ranges.h36
-rw-r--r--src/tree_sitter/language.c107
-rw-r--r--src/tree_sitter/language.h138
-rw-r--r--src/tree_sitter/length.h44
-rw-r--r--src/tree_sitter/lexer.c322
-rw-r--r--src/tree_sitter/lexer.h48
-rw-r--r--src/tree_sitter/lib.c20
-rw-r--r--src/tree_sitter/node.c673
-rw-r--r--src/tree_sitter/parser.c1887
-rw-r--r--src/tree_sitter/parser.h220
-rw-r--r--src/tree_sitter/point.h53
-rw-r--r--src/tree_sitter/reduce_action.h34
-rw-r--r--src/tree_sitter/reusable_node.h88
-rw-r--r--src/tree_sitter/stack.c846
-rw-r--r--src/tree_sitter/stack.h135
-rw-r--r--src/tree_sitter/subtree.c996
-rw-r--r--src/tree_sitter/subtree.h281
-rw-r--r--src/tree_sitter/tree.c149
-rw-r--r--src/tree_sitter/tree.h34
-rw-r--r--src/tree_sitter/tree_cursor.c302
-rw-r--r--src/tree_sitter/tree_cursor.h20
-rw-r--r--src/tree_sitter/utf16.c33
-rw-r--r--src/tree_sitter/utf16.h21
-rw-r--r--test/README.md22
-rw-r--r--test/busted/outputHandlers/nvim.lua44
-rw-r--r--test/functional/api/keymap_spec.lua4
-rw-r--r--test/functional/api/mark_extended_spec.lua1389
-rw-r--r--test/functional/api/menu_spec.lua4
-rw-r--r--test/functional/api/proc_spec.lua2
-rw-r--r--test/functional/api/rpc_fixture.lua14
-rw-r--r--test/functional/api/server_requests_spec.lua27
-rw-r--r--test/functional/api/vim_spec.lua157
-rw-r--r--test/functional/autocmd/cmdline_spec.lua2
-rw-r--r--test/functional/autocmd/tabclose_spec.lua18
-rw-r--r--test/functional/autocmd/tabnewentered_spec.lua8
-rw-r--r--test/functional/cmdline/ctrl_r_spec.lua2
-rw-r--r--test/functional/cmdline/history_spec.lua4
-rw-r--r--test/functional/core/fileio_spec.lua25
-rw-r--r--test/functional/core/job_spec.lua58
-rw-r--r--test/functional/core/main_spec.lua2
-rw-r--r--test/functional/eval/api_functions_spec.lua1
-rw-r--r--test/functional/eval/environ_spec.lua1
-rw-r--r--test/functional/eval/fnamemodify_spec.lua119
-rw-r--r--test/functional/eval/let_spec.lua4
-rw-r--r--test/functional/eval/system_spec.lua12
-rw-r--r--test/functional/eval/timer_spec.lua21
-rw-r--r--test/functional/eval/wait_spec.lua7
-rw-r--r--test/functional/ex_cmds/dict_notifications_spec.lua14
-rw-r--r--test/functional/ex_cmds/drop_spec.lua4
-rw-r--r--test/functional/ex_cmds/highlight_spec.lua4
-rw-r--r--test/functional/ex_cmds/script_spec.lua4
-rw-r--r--test/functional/ex_cmds/sign_spec.lua4
-rw-r--r--test/functional/ex_cmds/write_spec.lua7
-rw-r--r--test/functional/example_spec.lua38
-rw-r--r--test/functional/fixtures/CMakeLists.txt10
-rw-r--r--test/functional/fixtures/api_level_6.mpackbin25194 -> 25287 bytes
-rw-r--r--test/functional/fixtures/lsp-test-rpc-server.lua459
-rw-r--r--test/functional/fixtures/printenv-test.c6
-rw-r--r--test/functional/helpers.lua32
-rw-r--r--test/functional/legacy/022_line_ending_spec.lua25
-rw-r--r--test/functional/legacy/041_writing_and_reading_hundred_kbyte_spec.lua43
-rw-r--r--test/functional/legacy/045_folding_spec.lua3
-rw-r--r--test/functional/legacy/063_match_and_matchadd_spec.lua20
-rw-r--r--test/functional/legacy/077_mf_hash_grow_spec.lua52
-rw-r--r--test/functional/legacy/084_curswant_spec.lua49
-rw-r--r--test/functional/legacy/098_scrollbind_spec.lua48
-rw-r--r--test/functional/legacy/104_let_assignment_spec.lua54
-rw-r--r--test/functional/legacy/assert_spec.lua54
-rw-r--r--test/functional/legacy/breakindent_spec.lua214
-rw-r--r--test/functional/legacy/delete_spec.lua2
-rw-r--r--test/functional/legacy/search_spec.lua34
-rw-r--r--test/functional/lua/api_spec.lua30
-rw-r--r--test/functional/lua/commands_spec.lua99
-rw-r--r--test/functional/lua/luaeval_spec.lua96
-rw-r--r--test/functional/lua/overrides_spec.lua34
-rw-r--r--test/functional/lua/treesitter_spec.lua147
-rw-r--r--test/functional/lua/uri_spec.lua107
-rw-r--r--test/functional/lua/utility_functions_spec.lua291
-rw-r--r--test/functional/lua/vim_spec.lua663
-rw-r--r--test/functional/normal/jump_spec.lua91
-rw-r--r--test/functional/normal/put_spec.lua6
-rw-r--r--test/functional/options/chars_spec.lua48
-rw-r--r--test/functional/options/defaults_spec.lua9
-rw-r--r--test/functional/options/keymap_spec.lua2
-rw-r--r--test/functional/plugin/health_spec.lua20
-rw-r--r--test/functional/plugin/lsp/lsp_spec.lua682
-rw-r--r--test/functional/plugin/lsp/util_spec.lua76
-rw-r--r--test/functional/plugin/man_spec.lua54
-rw-r--r--test/functional/preload.lua10
-rw-r--r--test/functional/provider/clipboard_spec.lua48
-rw-r--r--test/functional/terminal/buffer_spec.lua25
-rw-r--r--test/functional/terminal/helpers.lua2
-rw-r--r--test/functional/terminal/highlight_spec.lua57
-rw-r--r--test/functional/terminal/mouse_spec.lua4
-rw-r--r--test/functional/terminal/scrollback_spec.lua32
-rw-r--r--test/functional/terminal/tui_spec.lua281
-rw-r--r--test/functional/ui/bufhl_spec.lua204
-rw-r--r--test/functional/ui/cmdline_highlight_spec.lua8
-rw-r--r--test/functional/ui/cmdline_spec.lua13
-rw-r--r--test/functional/ui/cursor_spec.lua4
-rw-r--r--test/functional/ui/diff_spec.lua74
-rw-r--r--test/functional/ui/embed_spec.lua3
-rw-r--r--test/functional/ui/float_spec.lua67
-rw-r--r--test/functional/ui/fold_spec.lua4
-rw-r--r--test/functional/ui/highlight_spec.lua99
-rw-r--r--test/functional/ui/hlstate_spec.lua12
-rw-r--r--test/functional/ui/inccommand_spec.lua151
-rw-r--r--test/functional/ui/messages_spec.lua193
-rw-r--r--test/functional/ui/mode_spec.lua53
-rw-r--r--test/functional/ui/mouse_spec.lua30
-rw-r--r--test/functional/ui/multibyte_spec.lua8
-rw-r--r--test/functional/ui/multigrid_spec.lua4
-rw-r--r--test/functional/ui/options_spec.lua33
-rw-r--r--test/functional/ui/output_spec.lua27
-rw-r--r--test/functional/ui/popupmenu_spec.lua9
-rw-r--r--test/functional/ui/screen.lua176
-rw-r--r--test/functional/ui/screen_basic_spec.lua1
-rw-r--r--test/functional/ui/searchhl_spec.lua2
-rw-r--r--test/functional/ui/sign_spec.lua4
-rw-r--r--test/functional/ui/spell_spec.lua4
-rw-r--r--test/functional/ui/syntax_conceal_spec.lua97
-rw-r--r--test/functional/ui/tabline_spec.lua12
-rw-r--r--test/helpers.lua23
-rw-r--r--test/unit/helpers.lua4
-rw-r--r--test/unit/mbyte_spec.lua5
-rw-r--r--test/unit/os/env_spec.lua2
-rw-r--r--test/unit/os/fs_spec.lua2
-rw-r--r--test/unit/path_spec.lua6
-rw-r--r--test/unit/preprocess.lua4
-rw-r--r--test/unit/tui_spec.lua136
-rw-r--r--third-party/CMakeLists.txt43
-rw-r--r--third-party/cmake/BuildLibtermkey.cmake1
-rw-r--r--third-party/cmake/BuildLibvterm.cmake1
-rw-r--r--third-party/cmake/BuildLuajit.cmake22
-rw-r--r--third-party/cmake/BuildLuarocks.cmake30
-rw-r--r--third-party/cmake/BuildLuv.cmake2
-rw-r--r--third-party/cmake/BuildUnibilium.cmake1
-rw-r--r--third-party/cmake/BuildUtf8proc.cmake68
409 files changed, 35579 insertions, 8742 deletions
diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml
new file mode 100644
index 0000000000..36f44f236b
--- /dev/null
+++ b/.builds/freebsd.yml
@@ -0,0 +1,44 @@
+image: freebsd/12.x
+
+packages:
+- cmake
+- gmake
+- ninja
+- libtool
+- sha
+- automake
+- pkgconf
+- unzip
+- wget
+- gettext
+- python
+- libffi
+
+sources:
+- https://github.com/neovim/neovim
+
+environment:
+ SOURCEHUT: 1
+ LANG: en_US.UTF-8
+ CMAKE_EXTRA_FLAGS: -DTRAVIS_CI_BUILD=ON -DMIN_LOG_LEVEL=3
+
+tasks:
+- build-deps: |
+ cd neovim
+ gmake deps
+- build: |
+ cd neovim
+ gmake CMAKE_BUILD_TYPE=Release CMAKE_EXTRA_FLAGS="${CMAKE_EXTRA_FLAGS}" nvim
+- functionaltest: |
+ cd neovim
+ gmake functionaltest
+- unittest: |
+ cd neovim
+ gmake unittest
+
+# Unfortunately, oldtest is tanking hard on sourcehut's FreeBSD instance
+# and not producing any logs as a result. So don't do this task for now.
+# Ref: https://github.com/neovim/neovim/pull/11477#discussion_r352095005.
+# - test-oldtest: |
+# cd neovim
+# gmake oldtest
diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml
index a8f6992e3f..ed2962998c 100644
--- a/.builds/openbsd.yml
+++ b/.builds/openbsd.yml
@@ -16,26 +16,28 @@ packages:
sources:
- https://github.com/neovim/neovim
+environment:
+ SOURCEHUT: 1
+ LC_CTYPE: en_US.UTF-8
+ CMAKE_EXTRA_FLAGS: -DTRAVIS_CI_BUILD=ON -DMIN_LOG_LEVEL=3
+
tasks:
-- build: |
+- build-deps: |
export AUTOCONF_VERSION=2.69
export AUTOMAKE_VERSION=1.15
- cd neovim
- mkdir .deps
- cd .deps
+ mkdir neovim/.deps
+ cd neovim/.deps
cmake -G Ninja ../third-party/
cmake --build . --config Debug
- cd ..
- mkdir build
- cd build
- cmake -G Ninja -DMIN_LOG_LEVEL=3 ..
+- build: |
+ mkdir neovim/build
+ cd neovim/build
+ cmake -G Ninja $CMAKE_EXTRA_FLAGS ..
cmake --build . --config Debug
./bin/nvim --version
-- test: |
- export LC_CTYPE=en_US.UTF-8
- # functional tests
+- functionaltest: |
cd neovim/build
- # cmake --build . --config Debug --target functionaltest
- # oldtests
- cd ..
+ cmake --build . --config Debug --target functionaltest
+- oldtest: |
+ cd neovim
gmake oldtest
diff --git a/.gitattributes b/.gitattributes
index 15a5c58091..cb5934a2a1 100755
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
*.h linguist-language=C
+src/nvim/testdir/test42.in diff
diff --git a/.gitignore b/.gitignore
index 7db3d96e2b..f4037c718a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,9 @@ compile_commands.json
/dist/
/.deps/
/tmp/
+/.clangd/
+.DS_Store
*.mo
.*.sw?
*~
@@ -42,6 +44,7 @@ tags
/src/nvim/testdir/valgrind.*
/src/nvim/testdir/.gdbinit
/runtime/indent/testdir/*.out
++runtime/indent/testdir/*.fail
# Generated by src/nvim/testdir/runnvim.sh.
/src/nvim/testdir/*.tlog
diff --git a/.luacheckrc b/.luacheckrc
index b945835bba..9c8bddb88e 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -2,6 +2,8 @@
-- Ignore W211 (unused variable) with preload files.
files["**/preload.lua"] = {ignore = { "211" }}
+-- Allow vim module to modify itself, but only here.
+files["src/nvim/lua/vim.lua"] = {ignore = { "122/vim" }}
-- Don't report unused self arguments of methods.
self = false
diff --git a/.luacov b/.luacov
new file mode 100644
index 0000000000..f8eb28e3f7
--- /dev/null
+++ b/.luacov
@@ -0,0 +1,22 @@
+-- Configuration file for LuaCov
+
+local source = require("lfs").currentdir()
+
+local function pesc(s)
+ assert(type(s) == 'string', s)
+ return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1')
+end
+
+return {
+ include = {
+ -- Absolute paths (starting with source dir, not hidden (i.e. .deps)).
+ pesc(source) .. "[/\\][^.].+",
+ -- Relative (non-hidden) paths.
+ '^[^/\\.]',
+ },
+ modules = {
+ ['vim'] = 'runtime/lua/vim/shared.lua'
+ },
+}
+
+-- vim: ft=lua tw=80 sw=2 et
diff --git a/.travis.yml b/.travis.yml
index 325b5e7b56..0417b67a5f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,19 @@ language: c
env:
global:
+ # Encrypted environment variables, see
+ # http://docs.travis-ci.com/user/encryption-keys/
+ #
+ # SNAP_SECRET_KEY: generated by:
+ # travis encrypt SNAP_SECRET_KEY=xx --add
+ # https://github.com/neovim/neovim/pull/11428
+ # snapcraft key expires after 1 year. Steps to refresh it:
+ # 1. snapcraft enable-ci travis --refresh
+ # 2. mv .snapcraft/travis_snapcraft.cfg ci/snap/travis_snapcraft.cfg
+ # 3. Copy after_success command to ci/snap/deploy.sh from .travis.yml
+ # 4. Undo changes to .travis.yml
+ - secure: hd0qn2u8ABbJg5Bx4pBRcUQbKYFmcSHoecyHIPTCnGJT+NI41Bvm/IkN/N5DhBF+LbD3Q2nmR/dzI5H/dqS7RxMFvEx1DuFLendFHHX3MYf0AuKpXYY3gwgMTmqx8p/v6srlU7RBGWNGzHCWqksAem+EIWCe3I7WvfdKo1/DV/Y=
+
# Set "false" to force rebuild of third-party dependencies.
- CACHE_ENABLE=true
# Build directory for Neovim.
@@ -24,10 +37,6 @@ env:
-DDEPS_PREFIX=$DEPS_BUILD_DIR/usr
-DMIN_LOG_LEVEL=3"
- DEPS_CMAKE_FLAGS="-DUSE_BUNDLED_GPERF=OFF"
- # Additional CMake flags for 32-bit builds.
- - CMAKE_FLAGS_32BIT="-DCMAKE_SYSTEM_LIBRARY_PATH=/lib32:/usr/lib32:/usr/local/lib32
- -DCMAKE_IGNORE_PATH=/lib:/usr/lib:/usr/local/lib
- -DCMAKE_TOOLCHAIN_FILE=$TRAVIS_BUILD_DIR/cmake/i386-linux-gnu.toolchain.cmake"
# Environment variables for Clang sanitizers.
- ASAN_OPTIONS="detect_leaks=1:check_initialization_order=1:log_path=$LOG_DIR/asan"
- TSAN_OPTIONS="log_path=$LOG_DIR/tsan"
@@ -99,6 +108,8 @@ jobs:
- GCOV=gcov-9
- CMAKE_FLAGS="$CMAKE_FLAGS -DUSE_GCOV=ON"
- GCOV_ERROR_FILE="/tmp/libgcov-errors.log"
+ - USE_LUACOV=1
+ - BUSTED_ARGS="--coverage"
- *common-job-env
addons:
apt:
@@ -136,6 +147,8 @@ jobs:
compiler: gcc
env:
- BUILD_32BIT=ON
+ - CMAKE_FLAGS="$CMAKE_FLAGS -m32 -DCMAKE_TOOLCHAIN_FILE=$TRAVIS_BUILD_DIR/cmake/i386-linux-gnu.toolchain.cmake"
+ - DEPS_CMAKE_FLAGS="$DEPS_CMAKE_FLAGS -m32 -DCMAKE_TOOLCHAIN_FILE=$TRAVIS_BUILD_DIR/cmake/i386-linux-gnu.toolchain.cmake"
# Minimum required CMake.
- CMAKE_URL=https://cmake.org/files/v2.8/cmake-2.8.12-Linux-i386.sh
- *common-job-env
@@ -145,6 +158,42 @@ jobs:
env:
- CLANG_SANITIZER=TSAN
- *common-job-env
+ - if: type != pull_request
+ name: snap
+ os: linux
+ env:
+ - LC_ALL: C.UTF-8
+ - LANG: C.UTF-8
+ - SNAPCRAFT_ENABLE_SILENT_REPORT: y
+ - SNAPCRAFT_ENABLE_DEVELOPER_DEBUG: y
+ addons:
+ snaps:
+ - name: snapcraft
+ channel: stable
+ classic: true
+ - name: http
+ - name: transfer
+ - name: lxd
+ channel: stable
+ # Override default before_install, before_cache.
+ before_install: /bin/true
+ before_cache: /bin/true
+ install: ci/snap/install.sh
+ before_script: echo "Building snap..."
+ script: ci/snap/script.sh
+ after_success: ci/snap/after_success.sh
+ deploy:
+ skip_cleanup: true
+ provider: script
+ script: ci/snap/deploy.sh
+ on:
+ branch: master
+ allow_failures:
+ - env:
+ - LC_ALL: C.UTF-8
+ - LANG: C.UTF-8
+ - SNAPCRAFT_ENABLE_SILENT_REPORT: y
+ - SNAPCRAFT_ENABLE_DEVELOPER_DEBUG: y
fast_finish: true
before_install: ci/before_install.sh
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9c3c20d23f..dbe2bf4d10 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -6,6 +6,13 @@
cmake_minimum_required(VERSION 2.8.12)
project(nvim C)
+if(POLICY CMP0065)
+ cmake_policy(SET CMP0065 NEW)
+endif()
+if(POLICY CMP0060)
+ cmake_policy(SET CMP0060 NEW)
+endif()
+
# Point CMake at any custom modules we may ship
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
@@ -113,47 +120,32 @@ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY
# If not in a git repo (e.g., a tarball) these tokens define the complete
# version string, else they are combined with the result of `git describe`.
set(NVIM_VERSION_MAJOR 0)
-set(NVIM_VERSION_MINOR 4)
+set(NVIM_VERSION_MINOR 5)
set(NVIM_VERSION_PATCH 0)
-set(NVIM_VERSION_PRERELEASE "") # for package maintainers
+set(NVIM_VERSION_PRERELEASE "-dev") # for package maintainers
# API level
-set(NVIM_API_LEVEL 6) # Bump this after any API change.
+set(NVIM_API_LEVEL 7) # Bump this after any API change.
set(NVIM_API_LEVEL_COMPAT 0) # Adjust this after a _breaking_ API change.
-set(NVIM_API_PRERELEASE false)
-
-file(TO_CMAKE_PATH ${CMAKE_CURRENT_LIST_DIR}/.git FORCED_GIT_DIR)
-include(GetGitRevisionDescription)
-get_git_head_revision(GIT_REFSPEC NVIM_VERSION_COMMIT)
-if(NVIM_VERSION_COMMIT) # is a git repo
- git_describe(NVIM_VERSION_MEDIUM)
- # `git describe` annotates the most recent tagged release; for pre-release
- # builds we must replace that with the unreleased version.
- string(REGEX REPLACE "^v[0-9]+\\.[0-9]+\\.[0-9]+"
- "v${NVIM_VERSION_MAJOR}.${NVIM_VERSION_MINOR}.${NVIM_VERSION_PATCH}"
- NVIM_VERSION_MEDIUM
- ${NVIM_VERSION_MEDIUM})
-endif()
+set(NVIM_API_PRERELEASE true)
set(NVIM_VERSION_BUILD_TYPE "${CMAKE_BUILD_TYPE}")
# NVIM_VERSION_CFLAGS set further below.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
-# Minimize logging for release-type builds.
-if(NOT CMAKE_C_FLAGS_RELEASE MATCHES DMIN_LOG_LEVEL)
- set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -DMIN_LOG_LEVEL=3")
-endif()
-if(NOT CMAKE_C_FLAGS_MINSIZEREL MATCHES DMIN_LOG_LEVEL)
- set(CMAKE_C_FLAGS_MINSIZEREL "${CMAKE_C_FLAGS_MINSIZEREL} -DMIN_LOG_LEVEL=3")
-endif()
-if(NOT CMAKE_C_FLAGS_RELWITHDEBINFO MATCHES DMIN_LOG_LEVEL)
- set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} -DMIN_LOG_LEVEL=3")
-endif()
-
# Log level (MIN_LOG_LEVEL in log.h)
if("${MIN_LOG_LEVEL}" MATCHES "^$")
- message(STATUS "MIN_LOG_LEVEL not specified, default is 1 (INFO)")
+ # Minimize logging for release-type builds.
+ if(CMAKE_BUILD_TYPE STREQUAL "Release"
+ OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo"
+ OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
+ message(STATUS "MIN_LOG_LEVEL not specified, default is 3 (ERROR) for release builds")
+ set(MIN_LOG_LEVEL 3)
+ else()
+ message(STATUS "MIN_LOG_LEVEL not specified, default is 1 (INFO)")
+ set(MIN_LOG_LEVEL 1)
+ endif()
else()
if(NOT MIN_LOG_LEVEL MATCHES "^[0-3]$")
message(FATAL_ERROR "invalid MIN_LOG_LEVEL: " ${MIN_LOG_LEVEL})
@@ -309,8 +301,10 @@ if(UNIX)
if(HAS_FSTACK_PROTECTOR_STRONG_FLAG)
add_compile_options(-fstack-protector-strong)
+ link_libraries(-fstack-protector-strong)
elseif(HAS_FSTACK_PROTECTOR_FLAG)
add_compile_options(-fstack-protector --param ssp-buffer-size=4)
+ link_libraries(-fstack-protector --param ssp-buffer-size=4)
endif()
endif()
@@ -323,10 +317,10 @@ if(HAS_DIAG_COLOR_FLAG)
endif()
endif()
-option(TRAVIS_CI_BUILD "Travis/QuickBuild CI, extra flags will be set" OFF)
+option(TRAVIS_CI_BUILD "Travis/sourcehut CI, extra flags will be set" OFF)
if(TRAVIS_CI_BUILD)
- message(STATUS "Travis/QuickBuild CI build enabled")
+ message(STATUS "Travis/sourcehut CI build enabled")
add_compile_options(-Werror)
if(DEFINED ENV{BUILD_32BIT})
# Get some test coverage for unsigned char
@@ -375,6 +369,13 @@ include_directories(SYSTEM ${MSGPACK_INCLUDE_DIRS})
find_package(LibLUV 1.30.0 REQUIRED)
include_directories(SYSTEM ${LIBLUV_INCLUDE_DIRS})
+find_package(UTF8PROC REQUIRED)
+include_directories(SYSTEM ${UTF8PROC_INCLUDE_DIRS})
+if(WIN32)
+ add_definitions(-DUTF8PROC_STATIC)
+endif()
+
+
# Note: The test lib requires LuaJIT; it will be skipped if LuaJIT is missing.
option(PREFER_LUA "Prefer Lua over LuaJIT in the nvim executable." OFF)
@@ -486,18 +487,19 @@ include(LuaHelpers)
set(LUA_DEPENDENCIES lpeg mpack bit)
if(NOT LUA_PRG)
foreach(CURRENT_LUA_PRG luajit lua5.1 lua5.2 lua)
- # If LUA_PRG is set find_program() will not search
- unset(LUA_PRG CACHE)
+ unset(_CHECK_LUA_PRG CACHE)
unset(LUA_PRG_WORKS)
- find_program(LUA_PRG ${CURRENT_LUA_PRG})
+ find_program(_CHECK_LUA_PRG ${CURRENT_LUA_PRG})
- if(LUA_PRG)
- check_lua_deps(${LUA_PRG} "${LUA_DEPENDENCIES}" LUA_PRG_WORKS)
+ if(_CHECK_LUA_PRG)
+ check_lua_deps(${_CHECK_LUA_PRG} "${LUA_DEPENDENCIES}" LUA_PRG_WORKS)
if(LUA_PRG_WORKS)
+ set(LUA_PRG "${_CHECK_LUA_PRG}" CACHE FILEPATH "Path to a program.")
break()
endif()
endif()
endforeach()
+ unset(_CHECK_LUA_PRG CACHE)
else()
check_lua_deps(${LUA_PRG} "${LUA_DEPENDENCIES}" LUA_PRG_WORKS)
endif()
@@ -560,10 +562,7 @@ if(BUSTED_PRG)
endif()
set(UNITTEST_PREREQS nvim-test unittest-headers)
- set(FUNCTIONALTEST_PREREQS nvim printargs-test shell-test streams-test ${GENERATED_HELP_TAGS})
- if(NOT WIN32)
- list(APPEND FUNCTIONALTEST_PREREQS tty-test)
- endif()
+ set(FUNCTIONALTEST_PREREQS nvim printenv-test printargs-test shell-test streams-test tty-test ${GENERATED_HELP_TAGS})
set(BENCHMARK_PREREQS nvim tty-test)
# Useful for automated build systems, if they want to manually run the tests.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 607f1aae83..04b4d23a2e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -85,7 +85,7 @@ the VCS/git logs more valuable.
### Automated builds (CI)
-Each pull request must pass the automated builds on [Travis CI], [QuickBuild]
+Each pull request must pass the automated builds on [Travis CI], [sourcehut]
and [AppVeyor].
- CI builds are compiled with [`-Werror`][gcc-warnings], so compiler warnings
@@ -100,14 +100,19 @@ and [AppVeyor].
- The [lint](#lint) build checks modified lines _and their immediate
neighbors_, to encourage incrementally updating the legacy style to meet our
[style](#style). (See [#3174][3174] for background.)
-- [How to investigate QuickBuild failures](https://github.com/neovim/neovim/pull/4718#issuecomment-217631350)
- - QuickBuild uses this invocation:
- ```
- mkdir -p build/${params.get("buildType")} \
- && cd build/${params.get("buildType")} \
- && cmake -G "Unix Makefiles" -DBUSTED_OUTPUT_TYPE=TAP -DCMAKE_BUILD_TYPE=${params.get("buildType")}
- -DTRAVIS_CI_BUILD=ON ../.. && ${node.getAttribute("make", "make")}
- VERBOSE=1 nvim unittest-prereqs functionaltest-prereqs
+- CI for freebsd and openbsd runs on [sourcehut].
+ - To get a backtrace on freebsd (after connecting via ssh):
+ ```sh
+ sudo pkg install tmux # If you want tmux.
+ lldb build/bin/nvim -c nvim.core
+
+ # To get a full backtrace:
+ # 1. Rebuild with debug info.
+ rm -rf nvim.core build
+ gmake CMAKE_BUILD_TYPE=RelWithDebInfo CMAKE_EXTRA_FLAGS="-DTRAVIS_CI_BUILD=ON -DMIN_LOG_LEVEL=3" nvim
+ # 2. Run the failing test to generate a new core file.
+ TEST_FILE=test/functional/foo.lua gmake functionaltest
+ lldb build/bin/nvim -c nvim.core
```
### Clang scan-build
@@ -223,7 +228,7 @@ as context, use the `-W` argument as well.
[review-checklist]: https://github.com/neovim/neovim/wiki/Code-review-checklist
[3174]: https://github.com/neovim/neovim/issues/3174
[Travis CI]: https://travis-ci.org/neovim/neovim
-[QuickBuild]: http://neovim-qb.szakmeister.net/dashboard
+[sourcehut]: https://builds.sr.ht/~jmk
[AppVeyor]: https://ci.appveyor.com/project/neovim/neovim
[Merge a Vim patch]: https://github.com/neovim/neovim/wiki/Merging-patches-from-upstream-Vim
[Clang report]: https://neovim.io/doc/reports/clang/
diff --git a/Makefile b/Makefile
index 264ae8a470..07dad26d7b 100644
--- a/Makefile
+++ b/Makefile
@@ -119,8 +119,13 @@ oldtest: | nvim build/runtime/doc/tags
ifeq ($(strip $(TEST_FILE)),)
+$(SINGLE_MAKE) -C src/nvim/testdir NVIM_PRG="$(realpath build/bin/nvim)" $(MAKEOVERRIDES)
else
- +$(SINGLE_MAKE) -C src/nvim/testdir NVIM_PRG="$(realpath build/bin/nvim)" NEW_TESTS=$(TEST_FILE) SCRIPTS= $(MAKEOVERRIDES)
+ @# Handle TEST_FILE=test_foo{,.res,.vim}.
+ +$(SINGLE_MAKE) -C src/nvim/testdir NVIM_PRG="$(realpath build/bin/nvim)" SCRIPTS= $(MAKEOVERRIDES) $(patsubst %.vim,%,$(patsubst %.res,%,$(TEST_FILE)))
endif
+# Build oldtest by specifying the relative .vim filename.
+.PHONY: phony_force
+src/nvim/testdir/%.vim: phony_force
+ +$(SINGLE_MAKE) -C src/nvim/testdir NVIM_PRG="$(realpath build/bin/nvim)" SCRIPTS= $(MAKEOVERRIDES) $(patsubst src/nvim/testdir/%.vim,%,$@)
build/runtime/doc/tags helptags: | nvim
+$(BUILD_CMD) -C build runtime/doc/tags
@@ -138,6 +143,14 @@ functionaltest-lua: | nvim
lualint: | build/.ran-cmake deps
$(BUILD_CMD) -C build lualint
+shlint:
+ @shellcheck --version | head -n 2
+ shellcheck scripts/vim-patch.sh
+
+_opt_shlint:
+ @command -v shellcheck && { $(MAKE) shlint; exit $$?; } \
+ || echo "SKIP: shlint (shellcheck not found)"
+
pylint:
flake8 contrib/ scripts/ src/ test/
@@ -158,6 +171,7 @@ clean:
+test -d build && $(BUILD_CMD) -C build clean || true
$(MAKE) -C src/nvim/testdir clean
$(MAKE) -C runtime/doc clean
+ $(MAKE) -C runtime/indent clean
distclean:
rm -rf $(DEPS_BUILD_DIR) build
@@ -187,16 +201,16 @@ appimage:
appimage-%:
bash scripts/genappimage.sh $*
-lint: check-single-includes clint lualint _opt_pylint
+lint: check-single-includes clint lualint _opt_pylint _opt_shlint
# Generic pattern rules, allowing for `make build/bin/nvim` etc.
# Does not work with "Unix Makefiles".
ifeq ($(BUILD_TYPE),Ninja)
-build/%:
+build/%: phony_force
$(BUILD_CMD) -C build $(patsubst build/%,%,$@)
-$(DEPS_BUILD_DIR)/%:
+$(DEPS_BUILD_DIR)/%: phony_force
$(BUILD_CMD) -C $(DEPS_BUILD_DIR) $(patsubst $(DEPS_BUILD_DIR)/%,%,$@)
endif
-.PHONY: test lualint pylint functionaltest unittest lint clint clean distclean nvim libnvim cmake deps install appimage checkprefix
+.PHONY: test lualint pylint shlint functionaltest unittest lint clint clean distclean nvim libnvim cmake deps install appimage checkprefix
diff --git a/README.md b/README.md
index 1bdf33d6bb..338c8df70e 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,7 @@ To skip bundled (`third-party/*`) dependencies:
1. Install the dependencies using a package manager.
```
- sudo apt install gperf luajit luarocks libuv1-dev libluajit-5.1-dev libunibilium-dev libmsgpack-dev libtermkey-dev libvterm-dev
+ sudo apt install gperf luajit luarocks libuv1-dev libluajit-5.1-dev libunibilium-dev libmsgpack-dev libtermkey-dev libvterm-dev libutf8proc-dev
sudo luarocks build mpack
sudo luarocks build lpeg
sudo luarocks build inspect
diff --git a/appveyor.yml b/appveyor.yml
index 756c7fea2e..7e2aef345b 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -24,7 +24,7 @@ matrix:
fast_finish: true
install: []
before_build:
-- ps: Install-Product node 8
+- ps: Install-Product node 10
build_script:
- powershell ci\build.ps1
after_build:
@@ -40,6 +40,3 @@ cache:
artifacts:
- path: build/Neovim.zip
- path: build/bin/nvim.exe
-branches:
- only:
- - master
diff --git a/ci/before_install.sh b/ci/before_install.sh
index 5cb6894b8c..5810bec71a 100755
--- a/ci/before_install.sh
+++ b/ci/before_install.sh
@@ -3,10 +3,6 @@
set -e
set -o pipefail
-if [[ "${CI_TARGET}" == lint ]]; then
- exit
-fi
-
echo 'Python info:'
(
set -x
@@ -47,12 +43,12 @@ if [[ "${TRAVIS_OS_NAME}" == osx ]] || [ ! -f ~/.nvm/nvm.sh ]; then
fi
source ~/.nvm/nvm.sh
-nvm install --lts
-nvm use --lts
+nvm install 10
+nvm use 10
if [[ -n "$CMAKE_URL" ]]; then
echo "Installing custom CMake: $CMAKE_URL"
- curl --retry 5 --silent --fail -o /tmp/cmake-installer.sh "$CMAKE_URL"
+ curl --retry 5 --silent --show-error --fail -o /tmp/cmake-installer.sh "$CMAKE_URL"
mkdir -p "$HOME/.local/bin" /opt/cmake-custom
bash /tmp/cmake-installer.sh --prefix=/opt/cmake-custom --skip-license
ln -sfn /opt/cmake-custom/bin/cmake "$HOME/.local/bin/cmake"
diff --git a/ci/before_script.sh b/ci/before_script.sh
index 605ecdbf66..a0e87adb9e 100755
--- a/ci/before_script.sh
+++ b/ci/before_script.sh
@@ -35,5 +35,10 @@ fi
# Compile dependencies.
build_deps
+# Install cluacov for Lua coverage.
+if [[ "$USE_LUACOV" == 1 ]]; then
+ "${DEPS_BUILD_DIR}/usr/bin/luarocks" install cluacov
+fi
+
rm -rf "${LOG_DIR}"
mkdir -p "${LOG_DIR}"
diff --git a/ci/build.ps1 b/ci/build.ps1
index d533d7b4e0..244b4766b2 100644
--- a/ci/build.ps1
+++ b/ci/build.ps1
@@ -1,10 +1,11 @@
-$ErrorActionPreference = 'stop'
-Set-PSDebug -Strict -Trace 1
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$ProgressPreference = 'SilentlyContinue'
$isPullRequest = ($env:APPVEYOR_PULL_REQUEST_HEAD_COMMIT -ne $null)
$env:CONFIGURATION -match '^(?<compiler>\w+)_(?<bits>32|64)(?:-(?<option>\w+))?$'
$compiler = $Matches.compiler
-$compileOption = $Matches.option
+$compileOption = if ($Matches -contains 'option') {$Matches.option} else {''}
$bits = $Matches.bits
$cmakeBuildType = $(if ($env:CMAKE_BUILD_TYPE -ne $null) {$env:CMAKE_BUILD_TYPE} else {'RelWithDebInfo'});
$buildDir = [System.IO.Path]::GetFullPath("$(pwd)")
@@ -23,7 +24,6 @@ $uploadToCodeCov = $false
function exitIfFailed() {
if ($LastExitCode -ne 0) {
- Set-PSDebug -Off
exit $LastExitCode
}
}
@@ -46,6 +46,10 @@ if ($compiler -eq 'MINGW') {
$nvimCmakeVars['USE_GCOV'] = 'ON'
$uploadToCodecov = $true
$env:GCOV = "C:\msys64\mingw$bits\bin\gcov"
+
+ # Setup/build Lua coverage.
+ $env:USE_LUACOV = 1
+ $env:BUSTED_ARGS = "--coverage"
}
# These are native MinGW builds, but they use the toolchain inside
# MSYS2, this allows using all the dependencies and tools available
@@ -94,6 +98,27 @@ npm.cmd install -g neovim
Get-Command -CommandType Application neovim-node-host.cmd
npm.cmd link neovim
+
+$env:TREE_SITTER_DIR = $env:USERPROFILE + "\tree-sitter-build"
+mkdir "$env:TREE_SITTER_DIR\bin"
+
+$xbits = if ($bits -eq '32') {'x86'} else {'x64'}
+Invoke-WebRequest -UseBasicParsing -Uri "https://github.com/tree-sitter/tree-sitter/releases/download/0.15.9/tree-sitter-windows-$xbits.gz" -OutFile tree-sitter.exe.gz
+C:\msys64\usr\bin\gzip -d tree-sitter.exe.gz
+
+Invoke-WebRequest -UseBasicParsing -Uri "https://codeload.github.com/tree-sitter/tree-sitter-c/zip/v0.15.2" -OutFile tree_sitter_c.zip
+Expand-Archive .\tree_sitter_c.zip -DestinationPath .
+cd tree-sitter-c-0.15.2
+..\tree-sitter.exe test
+if (-Not (Test-Path -PathType Leaf "$env:TREE_SITTER_DIR\bin\c.dll")) {
+ exit 1
+}
+
+if ($compiler -eq 'MSVC') {
+ # Required for LuaRocks (https://github.com/luarocks/luarocks/issues/1039#issuecomment-507296940).
+ $env:VCINSTALLDIR = "C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Tools/MSVC/14.16.27023/"
+}
+
function convertToCmakeArgs($vars) {
return $vars.GetEnumerator() | foreach { "-D$($_.Key)=$($_.Value)" }
}
@@ -113,26 +138,26 @@ cmake --build . --config $cmakeBuildType -- $cmakeGeneratorArgs ; exitIfFailed
# Ensure that the "win32" feature is set.
.\bin\nvim -u NONE --headless -c 'exe !has(\"win32\").\"cq\"' ; exitIfFailed
+if ($env:USE_LUACOV -eq 1) {
+ & $env:DEPS_PREFIX\luarocks\luarocks.bat install cluacov
+}
+
# Functional tests
# The $LastExitCode from MSBuild can't be trusted
$failed = $false
-# Temporarily turn off tracing to reduce log file output
-Set-PSDebug -Off
cmake --build . --config $cmakeBuildType --target functionaltest -- $cmakeGeneratorArgs 2>&1 |
foreach { $failed = $failed -or
$_ -match 'functional tests failed with error'; $_ }
-if ($failed) {
- if ($uploadToCodecov) {
- bash -l /c/projects/neovim/ci/common/submit_coverage.sh functionaltest
- }
- exit $LastExitCode
-}
-Set-PSDebug -Strict -Trace 1
-
if ($uploadToCodecov) {
+ if ($env:USE_LUACOV -eq 1) {
+ & $env:DEPS_PREFIX\bin\luacov.bat
+ }
bash -l /c/projects/neovim/ci/common/submit_coverage.sh functionaltest
}
+if ($failed) {
+ exit $LastExitCode
+}
# Old tests
# Add MSYS to path, required for e.g. `find` used in test scripts.
diff --git a/ci/common/build.sh b/ci/common/build.sh
index 8e9b2f8ebb..02e1110a15 100644
--- a/ci/common/build.sh
+++ b/ci/common/build.sh
@@ -18,9 +18,6 @@ build_make() {
}
build_deps() {
- if test "${BUILD_32BIT}" = ON ; then
- DEPS_CMAKE_FLAGS="${DEPS_CMAKE_FLAGS} ${CMAKE_FLAGS_32BIT}"
- fi
if test "${FUNCTIONALTEST}" = "functionaltest-lua" \
|| test "${CLANG_SANITIZER}" = "ASAN_UBSAN" ; then
DEPS_CMAKE_FLAGS="${DEPS_CMAKE_FLAGS} -DUSE_BUNDLED_LUA=ON"
@@ -53,9 +50,6 @@ prepare_build() {
if test -n "${CLANG_SANITIZER}" ; then
CMAKE_FLAGS="${CMAKE_FLAGS} -DCLANG_${CLANG_SANITIZER}=ON"
fi
- if test "${BUILD_32BIT}" = ON ; then
- CMAKE_FLAGS="${CMAKE_FLAGS} ${CMAKE_FLAGS_32BIT}"
- fi
mkdir -p "${BUILD_DIR}"
cd "${BUILD_DIR}"
diff --git a/ci/common/submit_coverage.sh b/ci/common/submit_coverage.sh
index 4e92975d22..9c7887de0b 100755
--- a/ci/common/submit_coverage.sh
+++ b/ci/common/submit_coverage.sh
@@ -43,3 +43,14 @@ fi
# Cleanup always, especially collected data.
find . \( -name '*.gcov' -o -name '*.gcda' \) -ls -delete | wc -l
rm -f coverage.xml
+
+# Upload Lua coverage (generated manually on AppVeyor/Windows).
+if [ "$USE_LUACOV" = 1 ] && [ "$1" != "oldtest" ]; then
+ if [ -x "${DEPS_BUILD_DIR}/usr/bin/luacov" ]; then
+ "${DEPS_BUILD_DIR}/usr/bin/luacov"
+ fi
+ if ! "$codecov_sh" -f luacov.report.out -X gcov -X fix -Z -F "lua,${codecov_flags}"; then
+ echo "codecov upload failed."
+ fi
+ rm luacov.stats.out
+fi
diff --git a/ci/install.sh b/ci/install.sh
index cda9a11f08..a6cd955da5 100755
--- a/ci/install.sh
+++ b/ci/install.sh
@@ -4,7 +4,7 @@ set -e
set -o pipefail
if [[ "${CI_TARGET}" == lint ]]; then
- python -m pip -q install --user --upgrade flake8
+ python3 -m pip -q install --user --upgrade flake8
exit
fi
@@ -24,3 +24,27 @@ gem install --no-document --version ">= 0.8.0" neovim
echo "Install neovim npm package"
npm install -g neovim
npm link neovim
+
+echo "Install tree-sitter npm package"
+
+# FIXME
+# https://github.com/tree-sitter/tree-sitter/commit/e14e285a1087264a8c74a7c62fcaecc49db9d904
+# If queries added to tree-sitter-c, we can use latest tree-sitter-cli
+npm install -g tree-sitter-cli@v0.15.9
+
+echo "Install tree-sitter c parser"
+curl "https://codeload.github.com/tree-sitter/tree-sitter-c/tar.gz/v0.15.2" -o tree_sitter_c.tar.gz
+tar xf tree_sitter_c.tar.gz
+cd tree-sitter-c-0.15.2
+export TREE_SITTER_DIR=$HOME/tree-sitter-build/
+mkdir -p "$TREE_SITTER_DIR/bin"
+
+if [[ "$BUILD_32BIT" != "ON" ]]; then
+ # builds c parser in $HOME/tree-sitter-build/bin/c.(so|dylib)
+ tree-sitter test
+else
+ # no tree-sitter binary for 32bit linux, so fake it (no tree-sitter unit tests)
+ cd src/
+ gcc -m32 -o "$TREE_SITTER_DIR/bin/c.so" -shared parser.c -I.
+fi
+test -f "$TREE_SITTER_DIR/bin/c.so"
diff --git a/ci/run_lint.sh b/ci/run_lint.sh
index 88af163e80..8373a3cb36 100755
--- a/ci/run_lint.sh
+++ b/ci/run_lint.sh
@@ -20,6 +20,10 @@ enter_suite 'pylint'
run_test 'make pylint' pylint
exit_suite --continue
+enter_suite 'shlint'
+run_test 'make shlint' shlint
+exit_suite --continue
+
enter_suite single-includes
CLICOLOR_FORCE=1 run_test_wd \
--allow-hang \
diff --git a/ci/run_tests.sh b/ci/run_tests.sh
index c175910da5..6b2f69293c 100755
--- a/ci/run_tests.sh
+++ b/ci/run_tests.sh
@@ -19,6 +19,8 @@ exit_suite --continue
enter_suite tests
+export TREE_SITTER_DIR=$HOME/tree-sitter-build/
+
if test "$CLANG_SANITIZER" != "TSAN" ; then
# Additional threads are only created when the builtin UI starts, which
# doesn't happen in the unit/functional tests
diff --git a/ci/snap/after_success.sh b/ci/snap/after_success.sh
new file mode 100755
index 0000000000..e66721a5e2
--- /dev/null
+++ b/ci/snap/after_success.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+set -e
+set -o pipefail
+
+
+RESULT_SNAP=$(find ./ -name "*.snap")
+
+sudo snap install "$RESULT_SNAP" --dangerous --classic
+
+/snap/bin/nvim --version
+
+SHA256=$(sha256sum "$RESULT_SNAP")
+echo "SHA256: ${SHA256} ."
diff --git a/ci/snap/deploy.sh b/ci/snap/deploy.sh
new file mode 100755
index 0000000000..5fbd52d775
--- /dev/null
+++ b/ci/snap/deploy.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+set -e
+set -o pipefail
+
+# not a tagged release, abort
+# [[ "$TRAVIS_TAG" != "$TRAVIS_BRANCH" ]] && exit 0
+
+mkdir -p .snapcraft
+# shellcheck disable=SC2154
+openssl aes-256-cbc -K "$encrypted_ece1c4844832_key" -iv "$encrypted_ece1c4844832_iv" \
+ -in ci/snap/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d
+
+SNAP=$(find ./ -name "*.snap")
+
+# TODO(justinmk): This always does `edge` until we enable tagged builds.
+if [[ "$SNAP" =~ "dirty" || "$SNAP" =~ "nightly" ]]; then
+ snapcraft push "$SNAP" --release edge
+else
+ snapcraft push "$SNAP" --release candidate
+fi
diff --git a/ci/snap/install.sh b/ci/snap/install.sh
new file mode 100755
index 0000000000..23e0bc5eb8
--- /dev/null
+++ b/ci/snap/install.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -e
+set -o pipefail
+
+sudo apt update
+sudo /snap/bin/lxd.migrate -yes
+sudo /snap/bin/lxd waitready
+sudo /snap/bin/lxd init --auto
+
diff --git a/ci/snap/script.sh b/ci/snap/script.sh
new file mode 100755
index 0000000000..647cda4874
--- /dev/null
+++ b/ci/snap/script.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+set -o pipefail
+
+mkdir -p "$TRAVIS_BUILD_DIR/snaps-cache"
+sudo snapcraft --use-lxd
+
diff --git a/ci/snap/travis_snapcraft.cfg b/ci/snap/travis_snapcraft.cfg
new file mode 100644
index 0000000000..3e6a60c30d
--- /dev/null
+++ b/ci/snap/travis_snapcraft.cfg
Binary files differ
diff --git a/cmake/FindUTF8PROC.cmake b/cmake/FindUTF8PROC.cmake
new file mode 100644
index 0000000000..fdb462b779
--- /dev/null
+++ b/cmake/FindUTF8PROC.cmake
@@ -0,0 +1,16 @@
+# - Try to find utf8proc
+# Once done this will define
+# UTF8PROC_FOUND - System has utf8proc
+# UTF8PROC_INCLUDE_DIRS - The utf8proc include directories
+# UTF8PROC_LIBRARIES - The libraries needed to use utf8proc
+
+include(LibFindMacros)
+
+set(UTF8PROC_NAMES utf8proc)
+if(MSVC)
+ # "utf8proc_static" is used for MSVC (when built statically from third-party).
+ # https://github.com/JuliaStrings/utf8proc/commit/0975bf9b6.
+ list(APPEND UTF8PROC_NAMES utf8proc_static)
+endif()
+libfind_pkg_detect(UTF8PROC utf8proc FIND_PATH utf8proc.h FIND_LIBRARY ${UTF8PROC_NAMES})
+libfind_process(UTF8PROC REQUIRED)
diff --git a/cmake/GetCompileFlags.cmake b/cmake/GetCompileFlags.cmake
index 2238744a66..49b57f6f75 100644
--- a/cmake/GetCompileFlags.cmake
+++ b/cmake/GetCompileFlags.cmake
@@ -3,6 +3,13 @@ function(get_compile_flags _compile_flags)
set(compile_flags "<CMAKE_C_COMPILER> <CFLAGS> <BUILD_TYPE_CFLAGS> <COMPILE_OPTIONS><COMPILE_DEFINITIONS> <INCLUDES>")
# Get C compiler.
+ if(CMAKE_C_COMPILER_ARG1)
+ string(REPLACE
+ "<CMAKE_C_COMPILER>"
+ "<CMAKE_C_COMPILER> ${CMAKE_C_COMPILER_ARG1}"
+ compile_flags
+ "${compile_flags}")
+ endif()
string(REPLACE
"<CMAKE_C_COMPILER>"
"${CMAKE_C_COMPILER}"
diff --git a/cmake/GetGitRevisionDescription.cmake b/cmake/GetGitRevisionDescription.cmake
deleted file mode 100644
index 5044c682e4..0000000000
--- a/cmake/GetGitRevisionDescription.cmake
+++ /dev/null
@@ -1,180 +0,0 @@
-# https://github.com/rpavlik/cmake-modules
-#
-# - Returns a version string from Git
-#
-# These functions force a re-configure on each git commit so that you can
-# trust the values of the variables in your build system.
-#
-# get_git_head_revision(<refspecvar> <hashvar> [<additional arguments to git describe> ...])
-#
-# Returns the refspec and sha hash of the current head revision
-#
-# git_describe(<var> [<additional arguments to git describe> ...])
-#
-# Returns the results of git describe on the source tree, and adjusting
-# the output so that it tests false if an error occurs.
-#
-# git_get_exact_tag(<var> [<additional arguments to git describe> ...])
-#
-# Returns the results of git describe --exact-match on the source tree,
-# and adjusting the output so that it tests false if there was no exact
-# matching tag.
-#
-# Requires CMake 2.6 or newer (uses the 'function' command)
-#
-# Original Author:
-# 2009-2010 Ryan Pavlik <rpavlik@iastate.edu> <abiryan@ryand.net>
-# http://academic.cleardefinition.com
-# Iowa State University HCI Graduate Program/VRAC
-#
-# Copyright Iowa State University 2009-2010.
-# Distributed under the Boost Software License, Version 1.0.
-# (See accompanying file LICENSE_1_0.txt or copy at
-# http://www.boost.org/LICENSE_1_0.txt)
-
-if(__get_git_revision_description)
- return()
-endif()
-set(__get_git_revision_description YES)
-
-# We must run the following at "include" time, not at function call time,
-# to find the path to this module rather than the path to a calling list file
-get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH)
-
-function(get_git_dir _gitdir)
- # check FORCED_GIT_DIR first
- if(FORCED_GIT_DIR)
- set(${_gitdir} ${FORCED_GIT_DIR} PARENT_SCOPE)
- return()
- endif()
-
- # check GIT_DIR in environment
- set(GIT_DIR $ENV{GIT_DIR})
- if(NOT GIT_DIR)
- set(GIT_PARENT_DIR ${CMAKE_CURRENT_SOURCE_DIR})
- set(GIT_DIR ${GIT_PARENT_DIR}/.git)
- endif()
- # .git dir not found, search parent directories
- while(NOT EXISTS ${GIT_DIR})
- set(GIT_PREVIOUS_PARENT ${GIT_PARENT_DIR})
- get_filename_component(GIT_PARENT_DIR ${GIT_PARENT_DIR} PATH)
- if(GIT_PARENT_DIR STREQUAL GIT_PREVIOUS_PARENT)
- return()
- endif()
- set(GIT_DIR ${GIT_PARENT_DIR}/.git)
- endwhile()
- # check if this is a submodule
- if(NOT IS_DIRECTORY ${GIT_DIR})
- file(READ ${GIT_DIR} submodule)
- string(REGEX REPLACE "gitdir: (.*)\n$" "\\1" GIT_DIR_RELATIVE ${submodule})
- get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH)
- get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE)
- endif()
- set(${_gitdir} ${GIT_DIR} PARENT_SCOPE)
-endfunction()
-
-function(get_git_head_revision _refspecvar _hashvar)
- get_git_dir(GIT_DIR)
- if(NOT GIT_DIR)
- return()
- endif()
-
- set(GIT_DATA ${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data)
- if(NOT EXISTS ${GIT_DATA})
- file(MAKE_DIRECTORY ${GIT_DATA})
- endif()
-
- if(NOT EXISTS ${GIT_DIR}/HEAD)
- return()
- endif()
- set(HEAD_FILE ${GIT_DATA}/HEAD)
- configure_file(${GIT_DIR}/HEAD ${HEAD_FILE} COPYONLY)
-
- configure_file(${_gitdescmoddir}/GetGitRevisionDescription.cmake.in
- ${GIT_DATA}/grabRef.cmake
- @ONLY)
- include(${GIT_DATA}/grabRef.cmake)
-
- set(${_refspecvar} ${HEAD_REF} PARENT_SCOPE)
- set(${_hashvar} ${HEAD_HASH} PARENT_SCOPE)
-endfunction()
-
-function(git_describe _var)
- get_git_dir(GIT_DIR)
- if(NOT GIT_DIR)
- return()
- endif()
-
- if(NOT GIT_FOUND)
- find_package(Git QUIET)
- endif()
- if(NOT GIT_FOUND)
- set(${_var} "GIT-NOTFOUND" PARENT_SCOPE)
- return()
- endif()
-
- get_git_head_revision(refspec hash)
- if(NOT hash)
- set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE)
- return()
- endif()
-
- execute_process(COMMAND
- ${GIT_EXECUTABLE}
- describe
- ${hash}
- ${ARGN}
- WORKING_DIRECTORY
- ${GIT_DIR}
- RESULT_VARIABLE
- res
- OUTPUT_VARIABLE
- out
- ERROR_QUIET
- OUTPUT_STRIP_TRAILING_WHITESPACE)
- if(NOT res EQUAL 0)
- set(out "${out}-${res}-NOTFOUND")
- endif()
-
- set(${_var} ${out} PARENT_SCOPE)
-endfunction()
-
-function(git_timestamp _var)
- get_git_dir(GIT_DIR)
- if(NOT GIT_DIR)
- return()
- endif()
-
- if(NOT GIT_FOUND)
- find_package(Git QUIET)
- endif()
- if(NOT GIT_FOUND)
- return()
- endif()
-
- get_git_head_revision(refspec hash)
- if(NOT hash)
- set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE)
- return()
- endif()
-
- execute_process(COMMAND ${GIT_EXECUTABLE} log -1 --format="%ci" ${hash} ${ARGN}
- WORKING_DIRECTORY ${GIT_DIR}
- RESULT_VARIABLE res
- OUTPUT_VARIABLE out
- ERROR_QUIET
- OUTPUT_STRIP_TRAILING_WHITESPACE)
- if(res EQUAL 0)
- string(REGEX REPLACE "[-\" :]" "" out ${out})
- string(SUBSTRING ${out} 0 12 out)
- else()
- set(out "${out}-${res}-NOTFOUND")
- endif()
-
- set(${_var} ${out} PARENT_SCOPE)
-endfunction()
-
-function(git_get_exact_tag _var)
- git_describe(out --exact-match ${ARGN})
- set(${_var} ${out} PARENT_SCOPE)
-endfunction()
diff --git a/cmake/GetGitRevisionDescription.cmake.in b/cmake/GetGitRevisionDescription.cmake.in
deleted file mode 100644
index 8a085b2671..0000000000
--- a/cmake/GetGitRevisionDescription.cmake.in
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# Internal file for GetGitRevisionDescription.cmake
-#
-# Requires CMake 2.6 or newer (uses the 'function' command)
-#
-# Original Author:
-# 2009-2010 Ryan Pavlik <rpavlik@iastate.edu> <abiryan@ryand.net>
-# http://academic.cleardefinition.com
-# Iowa State University HCI Graduate Program/VRAC
-#
-# Copyright Iowa State University 2009-2010.
-# Distributed under the Boost Software License, Version 1.0.
-# (See accompanying file LICENSE_1_0.txt or copy at
-# http://www.boost.org/LICENSE_1_0.txt)
-
-set(HEAD_HASH)
-
-file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024)
-
-string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS)
-if(HEAD_CONTENTS MATCHES "ref")
- # named branch
- string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}")
- if(EXISTS "@GIT_DIR@/${HEAD_REF}")
- configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY)
- elseif(EXISTS "@GIT_DIR@/logs/${HEAD_REF}")
- configure_file("@GIT_DIR@/logs/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY)
- set(HEAD_HASH "${HEAD_REF}")
- endif()
-else()
- # detached HEAD
- configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY)
-endif()
-
-if(NOT HEAD_HASH)
- file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024)
- string(STRIP "${HEAD_HASH}" HEAD_HASH)
-endif()
diff --git a/codecov.yml b/codecov.yml
index a83fd916ee..0f867db668 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -25,3 +25,6 @@ coverage:
changes: no
comment: off
+
+ignore:
+ - "src/tree_sitter"
diff --git a/config/versiondef.h.in b/config/versiondef.h.in
index b9565735b3..22cad87249 100644
--- a/config/versiondef.h.in
+++ b/config/versiondef.h.in
@@ -5,7 +5,11 @@
#define NVIM_VERSION_MINOR @NVIM_VERSION_MINOR@
#define NVIM_VERSION_PATCH @NVIM_VERSION_PATCH@
#define NVIM_VERSION_PRERELEASE "@NVIM_VERSION_PRERELEASE@"
+
#cmakedefine NVIM_VERSION_MEDIUM "@NVIM_VERSION_MEDIUM@"
+#ifndef NVIM_VERSION_MEDIUM
+# include "auto/versiondef_git.h"
+#endif
#define NVIM_API_LEVEL @NVIM_API_LEVEL@
#define NVIM_API_LEVEL_COMPAT @NVIM_API_LEVEL_COMPAT@
diff --git a/contrib/local.mk.example b/contrib/local.mk.example
index 5a31ded59b..778e848d60 100644
--- a/contrib/local.mk.example
+++ b/contrib/local.mk.example
@@ -25,6 +25,12 @@
#
# CMAKE_BUILD_TYPE := Debug
+# With non-Debug builds interprocedural optimization (IPO) (which includes
+# link-time optimization (LTO)) is enabled by default, which causes the link
+# step to take a significant amout of time, which is relevant when building
+# often. You can disable it explicitly:
+# CMAKE_EXTRA_FLAGS += -DENABLE_LTO=OFF
+
# Log levels: 0 (DEBUG), 1 (INFO), 2 (WARNING), 3 (ERROR)
# Default is 1 (INFO) unless CMAKE_BUILD_TYPE is Release or RelWithDebInfo.
# CMAKE_EXTRA_FLAGS += -DMIN_LOG_LEVEL=1
@@ -42,6 +48,7 @@
# DEPS_CMAKE_FLAGS += -DUSE_BUNDLED_LUAROCKS=OFF
# DEPS_CMAKE_FLAGS += -DUSE_BUNDLED_MSGPACK=OFF
# DEPS_CMAKE_FLAGS += -DUSE_BUNDLED_UNIBILIUM=OFF
+# DEPS_CMAKE_FLAGS += -DUSE_BUNDLED_UTF8PROC=OFF
#
# Or disable all bundled dependencies at once.
#
diff --git a/runtime/autoload/health/provider.vim b/runtime/autoload/health/provider.vim
index 87d82150b6..ad7a614ff5 100644
--- a/runtime/autoload/health/provider.vim
+++ b/runtime/autoload/health/provider.vim
@@ -38,9 +38,10 @@ endfunction
" Handler for s:system() function.
function! s:system_handler(jobid, data, event) dict abort
if a:event ==# 'stderr'
- let self.stderr .= join(a:data, '')
- if !self.ignore_stderr
+ if self.add_stderr_to_output
let self.output .= join(a:data, '')
+ else
+ let self.stderr .= join(a:data, '')
endif
elseif a:event ==# 'stdout'
let self.output .= join(a:data, '')
@@ -64,7 +65,7 @@ function! s:system(cmd, ...) abort
let stdin = a:0 ? a:1 : ''
let ignore_error = a:0 > 2 ? a:3 : 0
let opts = {
- \ 'ignore_stderr': a:0 > 1 ? a:2 : 0,
+ \ 'add_stderr_to_output': a:0 > 1 ? a:2 : 0,
\ 'output': '',
\ 'stderr': '',
\ 'on_stdout': function('s:system_handler'),
@@ -89,8 +90,15 @@ function! s:system(cmd, ...) abort
call health#report_error(printf('Command timed out: %s', s:shellify(a:cmd)))
call jobstop(jobid)
elseif s:shell_error != 0 && !ignore_error
- call health#report_error(printf("Command error (job=%d, exit code %d): `%s` (in %s)\nOutput: %s\nStderr: %s",
- \ jobid, s:shell_error, s:shellify(a:cmd), string(getcwd()), opts.output, opts.stderr))
+ let emsg = printf("Command error (job=%d, exit code %d): `%s` (in %s)",
+ \ jobid, s:shell_error, s:shellify(a:cmd), string(getcwd()))
+ if !empty(opts.output)
+ let emsg .= "\noutput: " . opts.output
+ end
+ if !empty(opts.stderr)
+ let emsg .= "\nstderr: " . opts.stderr
+ end
+ call health#report_error(emsg)
endif
return opts.output
@@ -194,7 +202,8 @@ function! s:version_info(python) abort
let nvim_path = s:trim(s:system([
\ a:python, '-c',
- \ 'import sys; sys.path.remove(""); ' .
+ \ 'import sys; ' .
+ \ 'sys.path = list(filter(lambda x: x != "", sys.path)); ' .
\ 'import neovim; print(neovim.__file__)']))
if s:shell_error || empty(nvim_path)
return [python_version, 'unable to load neovim Python module', pypi_version,
@@ -257,6 +266,22 @@ function! s:check_bin(bin) abort
return 1
endfunction
+" Check "loaded" var for given a:provider.
+" Returns 1 if the caller should return (skip checks).
+function! s:disabled_via_loaded_var(provider) abort
+ let loaded_var = 'g:loaded_'.a:provider.'_provider'
+ if exists(loaded_var) && !exists('*provider#'.a:provider.'#Call')
+ let v = eval(loaded_var)
+ if 0 is v
+ call health#report_info('Disabled ('.loaded_var.'='.v.').')
+ return 1
+ else
+ call health#report_info('Disabled ('.loaded_var.'='.v.'). This might be due to some previous error.')
+ endif
+ endif
+ return 0
+endfunction
+
function! s:check_python(version) abort
call health#report_start('Python ' . a:version . ' provider (optional)')
@@ -264,11 +289,10 @@ function! s:check_python(version) abort
let python_exe = ''
let venv = exists('$VIRTUAL_ENV') ? resolve($VIRTUAL_ENV) : ''
let host_prog_var = pyname.'_host_prog'
- let loaded_var = 'g:loaded_'.pyname.'_provider'
let python_multiple = []
- if exists(loaded_var) && !exists('*provider#'.pyname.'#Call')
- call health#report_info('Disabled ('.loaded_var.'='.eval(loaded_var).'). This might be due to some previous error.')
+ if s:disabled_via_loaded_var(pyname)
+ return
endif
let [pyenv, pyenv_root] = s:check_for_pyenv()
@@ -286,7 +310,7 @@ function! s:check_python(version) abort
let python_exe = pyname
endif
- " No Python executable could `import neovim`.
+ " No Python executable could `import neovim`, or host_prog_var was used.
if !empty(pythonx_errors)
call health#report_error('Python provider error:', pythonx_errors)
@@ -476,9 +500,7 @@ endfunction
function! s:check_ruby() abort
call health#report_start('Ruby provider (optional)')
- let loaded_var = 'g:loaded_ruby_provider'
- if exists(loaded_var) && !exists('*provider#ruby#Call')
- call health#report_info('Disabled. '.loaded_var.'='.eval(loaded_var))
+ if s:disabled_via_loaded_var('ruby')
return
endif
@@ -532,9 +554,7 @@ endfunction
function! s:check_node() abort
call health#report_start('Node.js provider (optional)')
- let loaded_var = 'g:loaded_node_provider'
- if exists(loaded_var) && !exists('*provider#node#Call')
- call health#report_info('Disabled. '.loaded_var.'='.eval(loaded_var))
+ if s:disabled_via_loaded_var('node')
return
endif
diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim
index 153f1afed8..e4c0080ae9 100644
--- a/runtime/autoload/man.vim
+++ b/runtime/autoload/man.vim
@@ -1,4 +1,4 @@
-" Maintainer: Anmol Sethi <anmol@aubble.com>
+" Maintainer: Anmol Sethi <hi@nhooyr.io>
if exists('s:loaded_man')
finish
@@ -64,33 +64,20 @@ function! man#open_page(count, count1, mods, ...) abort
return
endtry
- call s:push_tag()
- let bufname = 'man://'.name.(empty(sect)?'':'('.sect.')')
-
+ let [l:buf, l:save_tfu] = [bufnr(), &tagfunc]
try
- set eventignore+=BufReadCmd
+ set tagfunc=man#goto_tag
+ let l:target = l:name . '(' . l:sect . ')'
if a:mods !~# 'tab' && s:find_man()
- execute 'silent keepalt edit' fnameescape(bufname)
+ execute 'silent keepalt tag' l:target
else
- execute 'silent keepalt' a:mods 'split' fnameescape(bufname)
+ execute 'silent keepalt' a:mods 'stag' l:target
endif
finally
- set eventignore-=BufReadCmd
- endtry
-
- try
- let page = s:get_page(path)
- catch
- if a:mods =~# 'tab' || !s:find_man()
- " a new window was opened
- close
- endif
- call s:error(v:exception)
- return
+ call setbufvar(l:buf, '&tagfunc', l:save_tfu)
endtry
let b:man_sect = sect
- call s:put_page(page)
endfunction
function! man#read_page(ref) abort
@@ -152,7 +139,7 @@ function! s:get_page(path) abort
" Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065).
" Soft-wrap: ftplugin/man.vim sets wrap/breakindent/….
" Hard-wrap: driven by `man`.
- let manwidth = !get(g:,'man_hardwrap') ? 999 : (empty($MANWIDTH) ? winwidth(0) : $MANWIDTH)
+ let manwidth = !get(g:,'man_hardwrap', 1) ? 999 : (empty($MANWIDTH) ? winwidth(0) : $MANWIDTH)
" Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db).
" http://comments.gmane.org/gmane.editors.vim.devel/29085
" Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces.
@@ -163,6 +150,9 @@ endfunction
function! s:put_page(page) abort
setlocal modifiable
setlocal noreadonly
+ setlocal noswapfile
+ " git-ls-files(1) is all one keyword/tag-target
+ setlocal iskeyword+=(,)
silent keepjumps %delete _
silent put =a:page
while getline(1) =~# '^\s*$'
@@ -254,24 +244,6 @@ function! s:verify_exists(sect, name) abort
return s:extract_sect_and_name_path(path) + [path]
endfunction
-let s:tag_stack = []
-
-function! s:push_tag() abort
- let s:tag_stack += [{
- \ 'buf': bufnr('%'),
- \ 'lnum': line('.'),
- \ 'col': col('.'),
- \ }]
-endfunction
-
-function! man#pop_tag() abort
- if !empty(s:tag_stack)
- let tag = remove(s:tag_stack, -1)
- execute 'silent' tag['buf'].'buffer'
- call cursor(tag['lnum'], tag['col'])
- endif
-endfunction
-
" extracts the name and sect out of 'path/name.sect'
function! s:extract_sect_and_name_path(path) abort
let tail = fnamemodify(a:path, ':t')
@@ -284,20 +256,16 @@ function! s:extract_sect_and_name_path(path) abort
endfunction
function! s:find_man() abort
- if &filetype ==# 'man'
- return 1
- elseif winnr('$') ==# 1
- return 0
- endif
- let thiswin = winnr()
- while 1
- wincmd w
- if &filetype ==# 'man'
+ let l:win = 1
+ while l:win <= winnr('$')
+ let l:buf = winbufnr(l:win)
+ if getbufvar(l:buf, '&filetype', '') ==# 'man'
+ execute l:win.'wincmd w'
return 1
- elseif thiswin ==# winnr()
- return 0
endif
+ let l:win += 1
endwhile
+ return 0
endfunction
function! s:error(msg) abort
@@ -360,14 +328,18 @@ function! man#complete(arg_lead, cmd_line, cursor_pos) abort
return s:complete(sect, sect, name)
endfunction
-function! s:complete(sect, psect, name) abort
+function! s:get_paths(sect, name) abort
try
let mandirs = join(split(s:system(['man', s:find_arg]), ':\|\n'), ',')
catch
call s:error(v:exception)
return
endtry
- let pages = globpath(mandirs,'man?/'.a:name.'*.'.a:sect.'*', 0, 1)
+ return globpath(mandirs,'man?/'.a:name.'*.'.a:sect.'*', 0, 1)
+endfunction
+
+function! s:complete(sect, psect, name) abort
+ let pages = s:get_paths(a:sect, a:name)
" We remove duplicates in case the same manpage in different languages was found.
return uniq(sort(map(pages, 's:format_candidate(v:val, a:psect)'), 'i'))
endfunction
@@ -387,6 +359,10 @@ function! s:format_candidate(path, psect) abort
endfunction
function! man#init_pager() abort
+ " https://github.com/neovim/neovim/issues/6828
+ let og_modifiable = &modifiable
+ setlocal modifiable
+
if getline(1) =~# '^\s*$'
silent keepjumps 1delete _
else
@@ -404,6 +380,31 @@ function! man#init_pager() abort
if -1 == match(bufname('%'), 'man:\/\/') " Avoid duplicate buffers, E95.
execute 'silent file man://'.tolower(fnameescape(ref))
endif
+
+ let &l:modifiable = og_modifiable
+endfunction
+
+function! man#goto_tag(pattern, flags, info) abort
+ let [l:sect, l:name] = man#extract_sect_and_name_ref(a:pattern)
+
+ let l:paths = s:get_paths(l:sect, l:name)
+ let l:structured = []
+
+ for l:path in l:paths
+ let l:n = s:extract_sect_and_name_path(l:path)[1]
+ let l:structured += [{ 'name': l:n, 'path': l:path }]
+ endfor
+
+ " sort by relevance - exact matches first, then the previous order
+ call sort(l:structured, { a, b -> a.name ==? l:name ? -1 : b.name ==? l:name ? 1 : 0 })
+
+ return map(l:structured, {
+ \ _, entry -> {
+ \ 'name': entry.name,
+ \ 'filename': 'man://' . entry.path,
+ \ 'cmd': '1'
+ \ }
+ \ })
endfunction
call s:init()
diff --git a/runtime/autoload/netrw.vim b/runtime/autoload/netrw.vim
index a5b47e06d5..9b1266c4ca 100644
--- a/runtime/autoload/netrw.vim
+++ b/runtime/autoload/netrw.vim
@@ -688,10 +688,6 @@ fun! netrw#Explore(indx,dosplit,style,...)
endif
" save registers
- if has("clipboard")
- sil! let keepregstar = @*
- sil! let keepregplus = @+
- endif
sil! let keepregslash= @/
" if dosplit
@@ -915,10 +911,6 @@ fun! netrw#Explore(indx,dosplit,style,...)
" call Decho("..case Nexplore with starpat=".starpat.": (indx=".indx.")",'~'.expand("<slnum>"))
if !exists("w:netrw_explore_list") " sanity check
NetrwKeepj call netrw#ErrorMsg(s:WARNING,"using Nexplore or <s-down> improperly; see help for netrw-starstar",40)
- if has("clipboard")
- sil! let @* = keepregstar
- sil! let @+ = keepregplus
- endif
sil! let @/ = keepregslash
" call Dret("netrw#Explore")
return
@@ -940,10 +932,6 @@ fun! netrw#Explore(indx,dosplit,style,...)
" call Decho("case Pexplore with starpat=".starpat.": (indx=".indx.")",'~'.expand("<slnum>"))
if !exists("w:netrw_explore_list") " sanity check
NetrwKeepj call netrw#ErrorMsg(s:WARNING,"using Pexplore or <s-up> improperly; see help for netrw-starstar",41)
- if has("clipboard")
- sil! let @* = keepregstar
- sil! let @+ = keepregplus
- endif
sil! let @/ = keepregslash
" call Dret("netrw#Explore")
return
@@ -995,10 +983,6 @@ fun! netrw#Explore(indx,dosplit,style,...)
catch /^Vim\%((\a\+)\)\=:E480/
keepalt call netrw#ErrorMsg(s:WARNING,'no files matched pattern<'.pattern.'>',45)
if &hls | let keepregslash= s:ExplorePatHls(pattern) | endif
- if has("clipboard")
- sil! let @* = keepregstar
- sil! let @+ = keepregplus
- endif
sil! let @/ = keepregslash
" call Dret("netrw#Explore : no files matched pattern")
return
@@ -1031,10 +1015,6 @@ fun! netrw#Explore(indx,dosplit,style,...)
if w:netrw_explore_listlen == 0 || (w:netrw_explore_listlen == 1 && w:netrw_explore_list[0] =~ '\*\*\/')
keepalt NetrwKeepj call netrw#ErrorMsg(s:WARNING,"no files matched",42)
- if has("clipboard")
- sil! let @* = keepregstar
- sil! let @+ = keepregplus
- endif
sil! let @/ = keepregslash
" call Dret("netrw#Explore : no files matched")
return
@@ -1079,10 +1059,6 @@ fun! netrw#Explore(indx,dosplit,style,...)
if !exists("g:netrw_quiet")
keepalt NetrwKeepj call netrw#ErrorMsg(s:WARNING,"your vim needs the +path_extra feature for Exploring with **!",44)
endif
- if has("clipboard")
- sil! let @* = keepregstar
- sil! let @+ = keepregplus
- endif
sil! let @/ = keepregslash
" call Dret("netrw#Explore : missing +path_extra")
return
@@ -1152,10 +1128,6 @@ fun! netrw#Explore(indx,dosplit,style,...)
" there's no danger of a late FocusGained event on initialization.
" Consequently, set s:netrw_events to 2.
let s:netrw_events= 2
- if has("clipboard")
- sil! let @* = keepregstar
- sil! let @+ = keepregplus
- endif
sil! let @/ = keepregslash
" call Dret("netrw#Explore : @/<".@/.">")
endfun
@@ -9559,10 +9531,6 @@ fun! s:NetrwWideListing()
let newcolstart = w:netrw_bannercnt + fpc
let newcolend = newcolstart + fpc - 1
" call Decho("bannercnt=".w:netrw_bannercnt." fpl=".w:netrw_fpl." fpc=".fpc." newcol[".newcolstart.",".newcolend."]",'~'.expand("<slnum>"))
- if has("clipboard")
- sil! let keepregstar = @*
- sil! let keepregplus = @+
- endif
while line("$") >= newcolstart
if newcolend > line("$") | let newcolend= line("$") | endif
let newcolqty= newcolend - newcolstart
@@ -9575,10 +9543,6 @@ fun! s:NetrwWideListing()
exe "sil! NetrwKeepj ".newcolstart.','.newcolend.'d _'
exe 'sil! NetrwKeepj '.w:netrw_bannercnt
endwhile
- if has("clipboard")
- sil! let @*= keepregstar
- sil! let @+= keepregplus
- endif
exe "sil! NetrwKeepj ".w:netrw_bannercnt.',$s/\s\+$//e'
NetrwKeepj call histdel("/",-1)
exe 'nno <buffer> <silent> w :call search(''^.\\|\s\s\zs\S'',''W'')'."\<cr>"
diff --git a/runtime/autoload/provider/pythonx.vim b/runtime/autoload/provider/pythonx.vim
index 59b1c27b72..23e7ff8f64 100644
--- a/runtime/autoload/provider/pythonx.vim
+++ b/runtime/autoload/provider/pythonx.vim
@@ -10,7 +10,8 @@ function! provider#pythonx#Require(host) abort
" Python host arguments
let prog = (ver == '2' ? provider#python#Prog() : provider#python3#Prog())
- let args = [prog, '-c', 'import sys; sys.path.remove(""); import neovim; neovim.start_host()']
+ let args = [prog, '-c', 'import sys; sys.path = list(filter(lambda x: x != "", sys.path)); import neovim; neovim.start_host()']
+
" Collect registered Python plugins into args
let python_plugins = remote#host#PluginsForHost(a:host.name)
@@ -28,8 +29,8 @@ endfunction
function! s:get_python_candidates(major_version) abort
return {
\ 2: ['python2', 'python2.7', 'python2.6', 'python'],
- \ 3: ['python3', 'python3.7', 'python3.6', 'python3.5', 'python3.4', 'python3.3',
- \ 'python']
+ \ 3: ['python3', 'python3.8', 'python3.7', 'python3.6', 'python3.5',
+ \ 'python3.4', 'python3.3', 'python']
\ }[a:major_version]
endfunction
@@ -43,7 +44,7 @@ function! provider#pythonx#DetectByModule(module, major_version) abort
let python_exe = s:get_python_executable_from_host_var(a:major_version)
if !empty(python_exe)
- return [python_exe, '']
+ return [exepath(expand(python_exe)), '']
endif
let candidates = s:get_python_candidates(a:major_version)
@@ -66,7 +67,7 @@ endfunction
function! s:import_module(prog, module) abort
let prog_version = system([a:prog, '-c' , printf(
\ 'import sys; ' .
- \ 'sys.path.remove(""); ' .
+ \ 'sys.path = list(filter(lambda x: x != "", sys.path)); ' .
\ 'sys.stdout.write(str(sys.version_info[0]) + "." + str(sys.version_info[1])); ' .
\ 'import pkgutil; ' .
\ 'exit(2*int(pkgutil.get_loader("%s") is None))',
diff --git a/runtime/autoload/spellfile.vim b/runtime/autoload/spellfile.vim
index c0ef51cdfe..d098902305 100644
--- a/runtime/autoload/spellfile.vim
+++ b/runtime/autoload/spellfile.vim
@@ -13,6 +13,13 @@ let s:spellfile_URL = '' " Start with nothing so that s:donedict is reset.
" This function is used for the spellfile plugin.
function! spellfile#LoadFile(lang)
+ " Check for sandbox/modeline. #11359
+ try
+ :!
+ catch /\<E12\>/
+ throw 'Cannot download spellfile in sandbox/modeline. Try ":set spell" from the cmdline.'
+ endtry
+
" If the netrw plugin isn't loaded we silently skip everything.
if !exists(":Nread")
if &verbose
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt
index 98dd330b48..d52a9a8409 100644
--- a/runtime/doc/api.txt
+++ b/runtime/doc/api.txt
@@ -19,6 +19,7 @@ API Usage *api-rpc* *RPC* *rpc*
*msgpack-rpc*
RPC is the typical way to control Nvim programmatically. Nvim implements the
MessagePack-RPC protocol:
+ https://github.com/msgpack-rpc/msgpack-rpc/blob/master/spec.md
https://github.com/msgpack/msgpack/blob/0b8f5ac/spec.md
Many clients use the API: user interfaces (GUIs), remote plugins, scripts like
@@ -439,8 +440,68 @@ Example: create a float with scratch buffer: >
>
==============================================================================
+Extended marks *api-extended-marks*
+
+Extended marks (extmarks) represent buffer annotations that track text changes
+in the buffer. They could be used to represent cursors, folds, misspelled
+words, and anything else that needs to track a logical location in the buffer
+over time.
+
+Example:
+
+We will set an extmark at the first row and third column. |api-indexing| is
+zero-indexed, so we use row=0 and column=2. Passing id=0 creates a new mark
+and returns the id: >
+
+ let g:mark_ns = nvim_create_namespace('myplugin')
+ let g:mark_id = nvim_buf_set_extmark(0, g:mark_ns, 0, 0, 2, {})
+
+We can get a mark by its id: >
+
+ echo nvim_buf_get_extmark_by_id(0, g:mark_ns, g:mark_id)
+ => [0, 2]
+
+We can get all marks in a buffer for our namespace (or by a range): >
+
+ echo nvim_buf_get_extmarks(0, g:mark_ns, 0, -1, {})
+ => [[1, 0, 2]]
+
+Deleting all text surrounding an extmark does not remove the extmark. To
+remove an extmark use |nvim_buf_del_extmark()|.
+
+Namespaces allow your plugin to manage only its own extmarks, ignoring those
+created by another plugin.
+
+Extmark positions changed by an edit will be restored on undo/redo. Creating
+and deleting extmarks is not a buffer change, thus new undo states are not
+created for extmark changes.
+
+==============================================================================
Global Functions *api-global*
+nvim_exec({src}, {output}) *nvim_exec()*
+ Executes Vimscript (multiline block of Ex-commands), like
+ anonymous |:source|.
+
+ Unlike |nvim_command()| this function supports heredocs,
+ script-scope (s:), etc.
+
+ On execution error: fails with VimL error, does not update
+ v:errmsg.
+
+ Parameters: ~
+ {src} Vimscript code
+ {output} Capture and return all (non-error, non-shell
+ |:!|) output
+
+ Return: ~
+ Output (non-error, non-shell |:!|) if `output` is true,
+ else empty string.
+
+ See also: ~
+ |execute()|
+ |nvim_command()|
+
nvim_command({command}) *nvim_command()*
Executes an ex-command.
@@ -450,6 +511,9 @@ nvim_command({command}) *nvim_command()*
Parameters: ~
{command} Ex-command string
+ See also: ~
+ |nvim_exec()|
+
nvim_get_hl_by_name({name}, {rgb}) *nvim_get_hl_by_name()*
Gets a highlight definition by name.
@@ -571,19 +635,9 @@ nvim_replace_termcodes({str}, {from_part}, {do_lt}, {special})
replace_termcodes
cpoptions
-nvim_command_output({command}) *nvim_command_output()*
- Executes an ex-command and returns its (non-error) output.
- Shell |:!| output is not captured.
-
- On execution error: fails with VimL error, does not update
- v:errmsg.
-
- Parameters: ~
- {command} Ex-command string
-
nvim_eval({expr}) *nvim_eval()*
- Evaluates a VimL expression (:help expression). Dictionaries
- and Lists are recursively expanded.
+ Evaluates a VimL |expression|. Dictionaries and Lists are
+ recursively expanded.
On execution error: fails with VimL error, does not update
v:errmsg.
@@ -594,7 +648,7 @@ nvim_eval({expr}) *nvim_eval()*
Return: ~
Evaluation result or expanded object
-nvim_execute_lua({code}, {args}) *nvim_execute_lua()*
+nvim_exec_lua({code}, {args}) *nvim_exec_lua()*
Execute Lua code. Parameters (if any) are available as `...`
inside the chunk. The chunk can return a value.
@@ -850,10 +904,10 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()*
{enter} Enter the window (make it the current window)
{config} Map defining the window configuration. Keys:
• `relative` : Sets the window layout to "floating", placed
- at (row,col) coordinates relative to one of:
+ at (row,col) coordinates relative to:
• "editor" The global editor grid
• "win" Window given by the `win` field, or
- current window by default.
+ current window.
• "cursor" Cursor position in current window.
• `win` : |window-ID| for relative="win".
@@ -896,10 +950,11 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()*
'number', 'relativenumber', 'cursorline',
'cursorcolumn', 'foldcolumn', 'spell' and
'list' options. 'signcolumn' is changed to
- `auto` . The end-of-buffer region is hidden
- by setting `eob` flag of 'fillchars' to a
- space char, and clearing the |EndOfBuffer|
- region in 'winhighlight'.
+ `auto` and 'colorcolumn' is cleared. The
+ end-of-buffer region is hidden by setting
+ `eob` flag of 'fillchars' to a space char,
+ and clearing the |EndOfBuffer| region in
+ 'winhighlight'.
Return: ~
Window handle, or 0 on error
@@ -984,7 +1039,7 @@ nvim_put({lines}, {type}, {after}, {follow}) *nvim_put()*
{type} Edit behavior: any |getregtype()| result, or:
• "b" |blockwise-visual| mode (may include
width, e.g. "b3")
- • "c" |characterwise| mode
+ • "c" |charwise| mode
• "l" |linewise| mode
• "" guess by contents, see |setreg()|
{after} Insert after cursor (like |p|), or before (like
@@ -1476,45 +1531,73 @@ nvim_buf_line_count({buffer}) *nvim_buf_line_count()*
Line count, or 0 for unloaded buffer. |api-buffer|
nvim_buf_attach({buffer}, {send_buffer}, {opts}) *nvim_buf_attach()*
- Activates buffer-update events on a channel, or as lua
+ Activates buffer-update events on a channel, or as Lua
callbacks.
+ Example (Lua): capture buffer updates in a global `events` variable (use "print(vim.inspect(events))" to see its
+ contents): >
+ events = {}
+ vim.api.nvim_buf_attach(0, false, {
+ on_lines=function(...) table.insert(events, {...}) end})
+<
+
Parameters: ~
{buffer} Buffer handle, or 0 for current buffer
- {send_buffer} Set to true if the initial notification
- should contain the whole buffer. If so, the
- first notification will be a
- `nvim_buf_lines_event` . Otherwise, the
- first notification will be a
- `nvim_buf_changedtick_event` . Not used for
- lua callbacks.
+ {send_buffer} True if the initial notification should
+ contain the whole buffer: first
+ notification will be `nvim_buf_lines_event`
+ . Else the first notification will be
+ `nvim_buf_changedtick_event` . Not for Lua
+ callbacks.
{opts} Optional parameters.
- • `on_lines` : lua callback received on
- change.
- • `on_changedtick` : lua callback received
- on changedtick increment without text
- change.
- • `utf_sizes` : include UTF-32 and UTF-16
- size of the replaced region. See
- |api-buffer-updates-lua| for more
- information
+ • on_lines: Lua callback invoked on change.
+ Return `true` to detach. Args:
+ • buffer handle
+ • b:changedtick
+ • first line that changed (zero-indexed)
+ • last line that was changed
+ • last line in the updated range
+ • byte count of previous contents
+ • deleted_codepoints (if `utf_sizes` is
+ true)
+ • deleted_codeunits (if `utf_sizes` is
+ true)
+
+ • on_changedtick: Lua callback invoked on
+ changedtick increment without text
+ change. Args:
+ • buffer handle
+ • b:changedtick
+
+ • on_detach: Lua callback invoked on
+ detach. Args:
+ • buffer handle
+
+ • utf_sizes: include UTF-32 and UTF-16 size
+ of the replaced region, as args to
+ `on_lines` .
+
+ Return: ~
+ False if attach failed (invalid parameter, or buffer isn't
+ loaded); otherwise True. TODO: LUA_API_NO_EVAL
- Return: ~
- False when updates couldn't be enabled because the buffer
- isn't loaded or `opts` contained an invalid key; otherwise
- True. TODO: LUA_API_NO_EVAL
+ See also: ~
+ |nvim_buf_detach()|
+ |api-buffer-updates-lua|
nvim_buf_detach({buffer}) *nvim_buf_detach()*
Deactivates buffer-update events on the channel.
- For Lua callbacks see |api-lua-detach|.
-
Parameters: ~
{buffer} Buffer handle, or 0 for current buffer
Return: ~
- False when updates couldn't be disabled because the buffer
- isn't loaded; otherwise True.
+ False if detach failed (because the buffer isn't loaded);
+ otherwise True.
+
+ See also: ~
+ |nvim_buf_attach()|
+ |api-lua-detach| for detaching Lua callbacks
*nvim_buf_get_lines()*
nvim_buf_get_lines({buffer}, {start}, {end}, {strict_indexing})
@@ -1726,6 +1809,99 @@ nvim_buf_get_mark({buffer}, {name}) *nvim_buf_get_mark()*
Return: ~
(row, col) tuple
+ *nvim_buf_get_extmark_by_id()*
+nvim_buf_get_extmark_by_id({buffer}, {ns_id}, {id})
+ Returns position for a given extmark id
+
+ Parameters: ~
+ {buffer} Buffer handle, or 0 for current buffer
+ {ns_id} Namespace id from |nvim_create_namespace()|
+ {id} Extmark id
+
+ Return: ~
+ (row, col) tuple or empty list () if extmark id was absent
+
+ *nvim_buf_get_extmarks()*
+nvim_buf_get_extmarks({buffer}, {ns_id}, {start}, {end}, {opts})
+ Gets extmarks in "traversal order" from a |charwise| region
+ defined by buffer positions (inclusive, 0-indexed
+ |api-indexing|).
+
+ Region can be given as (row,col) tuples, or valid extmark ids
+ (whose positions define the bounds). 0 and -1 are understood
+ as (0,0) and (-1,-1) respectively, thus the following are
+ equivalent:
+>
+ nvim_buf_get_extmarks(0, my_ns, 0, -1, {})
+ nvim_buf_get_extmarks(0, my_ns, [0,0], [-1,-1], {})
+<
+
+ If `end` is less than `start` , traversal works backwards.
+ (Useful with `limit` , to get the first marks prior to a given
+ position.)
+
+ Example:
+>
+ local a = vim.api
+ local pos = a.nvim_win_get_cursor(0)
+ local ns = a.nvim_create_namespace('my-plugin')
+ -- Create new extmark at line 1, column 1.
+ local m1 = a.nvim_buf_set_extmark(0, ns, 0, 0, 0, {})
+ -- Create new extmark at line 3, column 1.
+ local m2 = a.nvim_buf_set_extmark(0, ns, 0, 2, 0, {})
+ -- Get extmarks only from line 3.
+ local ms = a.nvim_buf_get_extmarks(0, ns, {2,0}, {2,0}, {})
+ -- Get all marks in this buffer + namespace.
+ local all = a.nvim_buf_get_extmarks(0, ns, 0, -1, {})
+ print(vim.inspect(ms))
+<
+
+ Parameters: ~
+ {buffer} Buffer handle, or 0 for current buffer
+ {ns_id} Namespace id from |nvim_create_namespace()|
+ {start} Start of range, given as (row, col) or valid
+ extmark id (whose position defines the bound)
+ {end} End of range, given as (row, col) or valid
+ extmark id (whose position defines the bound)
+ {opts} Optional parameters. Keys:
+ • limit: Maximum number of marks to return
+
+ Return: ~
+ List of [extmark_id, row, col] tuples in "traversal
+ order".
+
+ *nvim_buf_set_extmark()*
+nvim_buf_set_extmark({buffer}, {ns_id}, {id}, {line}, {col}, {opts})
+ Creates or updates an extmark.
+
+ To create a new extmark, pass id=0. The extmark id will be
+ returned. It is also allowed to create a new mark by passing
+ in a previously unused id, but the caller must then keep track
+ of existing and unused ids itself. (Useful over RPC, to avoid
+ waiting for the return value.)
+
+ Parameters: ~
+ {buffer} Buffer handle, or 0 for current buffer
+ {ns_id} Namespace id from |nvim_create_namespace()|
+ {id} Extmark id, or 0 to create new
+ {line} Line number where to place the mark
+ {col} Column where to place the mark
+ {opts} Optional parameters. Currently not used.
+
+ Return: ~
+ Id of the created/updated extmark
+
+nvim_buf_del_extmark({buffer}, {ns_id}, {id}) *nvim_buf_del_extmark()*
+ Removes an extmark.
+
+ Parameters: ~
+ {buffer} Buffer handle, or 0 for current buffer
+ {ns_id} Namespace id from |nvim_create_namespace()|
+ {id} Extmark id
+
+ Return: ~
+ true if the extmark was found, else false
+
*nvim_buf_add_highlight()*
nvim_buf_add_highlight({buffer}, {ns_id}, {hl_group}, {line},
{col_start}, {col_end})
@@ -1769,8 +1945,8 @@ nvim_buf_add_highlight({buffer}, {ns_id}, {hl_group}, {line},
*nvim_buf_clear_namespace()*
nvim_buf_clear_namespace({buffer}, {ns_id}, {line_start}, {line_end})
- Clears namespaced objects, highlights and virtual text, from a
- line range
+ Clears namespaced objects (highlights, extmarks, virtual text)
+ from a region.
Lines are 0-indexed. |api-indexing| To clear the namespace in
the entire buffer, specify line_start=0 and line_end=-1.
@@ -1821,6 +1997,27 @@ nvim_buf_set_virtual_text({buffer}, {ns_id}, {line}, {chunks}, {opts})
Return: ~
The ns_id that was used
+nvim_buf_get_virtual_text({buffer}, {lnum}) *nvim_buf_get_virtual_text()*
+ Get the virtual text (annotation) for a buffer line.
+
+ The virtual text is returned as list of lists, whereas the
+ inner lists have either one or two elements. The first element
+ is the actual text, the optional second element is the
+ highlight group.
+
+ The format is exactly the same as given to
+ nvim_buf_set_virtual_text().
+
+ If there is no virtual text associated with the given line, an
+ empty list is returned.
+
+ Parameters: ~
+ {buffer} Buffer handle, or 0 for current buffer
+ {line} Line to get the virtual text from (zero-indexed)
+
+ Return: ~
+ List of virtual text chunks
+
nvim__buf_stats({buffer}) *nvim__buf_stats()*
TODO: Documentation
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt
index 34ea083f96..18dfa587e8 100644
--- a/runtime/doc/autocmd.txt
+++ b/runtime/doc/autocmd.txt
@@ -559,16 +559,14 @@ CmdlineLeave Before leaving the command-line (including
*CmdwinEnter*
CmdwinEnter After entering the command-line window.
Useful for setting options specifically for
- this special type of window. This is
- triggered _instead_ of BufEnter and WinEnter.
+ this special type of window.
<afile> is set to a single character,
indicating the type of command-line.
|cmdwin-char|
*CmdwinLeave*
CmdwinLeave Before leaving the command-line window.
Useful to clean up any global setting done
- with CmdwinEnter. This is triggered _instead_
- of BufLeave and WinLeave.
+ with CmdwinEnter.
<afile> is set to a single character,
indicating the type of command-line.
|cmdwin-char|
diff --git a/runtime/doc/change.txt b/runtime/doc/change.txt
index bd3f22a371..dcebbc524c 100644
--- a/runtime/doc/change.txt
+++ b/runtime/doc/change.txt
@@ -90,7 +90,7 @@ start and end of the motion are not in the same line, and there are only
blanks before the start and there are no non-blanks after the end of the
motion, the delete becomes linewise. This means that the delete also removes
the line of blanks that you might expect to remain. Use the |o_v| operator to
-force the motion to be characterwise.
+force the motion to be charwise.
Trying to delete an empty region of text (e.g., "d0" in the first column)
is an error when 'cpoptions' includes the 'E' flag.
@@ -1074,7 +1074,7 @@ also use these commands to move text from one file to another, because Vim
preserves all registers when changing buffers (the CTRL-^ command is a quick
way to toggle between two files).
- *linewise-register* *characterwise-register*
+ *linewise-register* *charwise-register*
You can repeat the put commands with "." (except for :put) and undo them. If
the command that was used to get the text into the register was |linewise|,
Vim inserts the text below ("p") or above ("P") the line where the cursor is.
@@ -1116,10 +1116,9 @@ this happen. However, if the width of the block is not a multiple of a <Tab>
width and the text after the inserted block contains <Tab>s, that text may be
misaligned.
-Note that after a characterwise yank command, Vim leaves the cursor on the
-first yanked character that is closest to the start of the buffer. This means
-that "yl" doesn't move the cursor, but "yh" moves the cursor one character
-left.
+Note that after a charwise yank command, Vim leaves the cursor on the first
+yanked character that is closest to the start of the buffer. This means that
+"yl" doesn't move the cursor, but "yh" moves the cursor one character left.
Rationale: In Vi the "y" command followed by a backwards motion would
sometimes not move the cursor to the first yanked character,
because redisplaying was skipped. In Vim it always moves to
diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt
index ee1f76e4e4..b31177ce0e 100644
--- a/runtime/doc/cmdline.txt
+++ b/runtime/doc/cmdline.txt
@@ -1122,11 +1122,9 @@ edited as described in |cmdwin-char|.
AUTOCOMMANDS
-Two autocommand events are used: |CmdwinEnter| and |CmdwinLeave|. Since this
-window is of a special type, the WinEnter, WinLeave, BufEnter and BufLeave
-events are not triggered. You can use the Cmdwin events to do settings
-specifically for the command-line window. Be careful not to cause side
-effects!
+Two autocommand events are used: |CmdwinEnter| and |CmdwinLeave|. You can use
+the Cmdwin events to do settings specifically for the command-line window.
+Be careful not to cause side effects!
Example: >
:au CmdwinEnter : let b:cpt_save = &cpt | set cpt=.
:au CmdwinLeave : let &cpt = b:cpt_save
diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt
index b76a37810c..7c6b9ad1d3 100644
--- a/runtime/doc/deprecated.txt
+++ b/runtime/doc/deprecated.txt
@@ -14,6 +14,8 @@ updated.
API ~
*nvim_buf_clear_highlight()* Use |nvim_buf_clear_namespace()| instead.
+*nvim_command_output()* Use |nvim_exec()| instead.
+*nvim_execute_lua()* Use |nvim_exec_lua()| instead.
Commands ~
*:rv*
diff --git a/runtime/doc/develop.txt b/runtime/doc/develop.txt
index 90c2e30771..09c5b7c4ad 100644
--- a/runtime/doc/develop.txt
+++ b/runtime/doc/develop.txt
@@ -143,6 +143,87 @@ DOCUMENTATION *dev-doc*
/// @param dirname The path fragment before `pend`
<
+C docstrings ~
+
+Nvim API documentation lives in the source code, as docstrings (Doxygen
+comments) on the function definitions. The |api| :help is generated
+from the docstrings defined in src/nvim/api/*.c.
+
+Docstring format:
+- Lines start with `///`
+- Special tokens start with `@` followed by the token name:
+ `@note`, `@param`, `@returns`
+- Limited markdown is supported.
+ - List-items start with `-` (useful to nest or "indent")
+- Use `<pre>` for code samples.
+
+Example: the help for |nvim_open_win()| is generated from a docstring defined
+in src/nvim/api/vim.c like this: >
+
+ /// Opens a new window.
+ /// ...
+ ///
+ /// Example (Lua): window-relative float
+ /// <pre>
+ /// vim.api.nvim_open_win(0, false,
+ /// {relative='win', row=3, col=3, width=12, height=3})
+ /// </pre>
+ ///
+ /// @param buffer Buffer to display
+ /// @param enter Enter the window
+ /// @param config Map defining the window configuration. Keys:
+ /// - relative: Sets the window layout, relative to:
+ /// - "editor" The global editor grid.
+ /// - "win" Window given by the `win` field.
+ /// - "cursor" Cursor position in current window.
+ /// ...
+ /// @param[out] err Error details, if any
+ ///
+ /// @return Window handle, or 0 on error
+
+
+Lua docstrings ~
+ *dev-lua-doc*
+Lua documentation lives in the source code, as docstrings on the function
+definitions. The |lua-vim| :help is generated from the docstrings.
+
+Docstring format:
+- Lines in the main description start with `---`
+- Special tokens start with `--@` followed by the token name:
+ `--@see`, `--@param`, `--@returns`
+- Limited markdown is supported.
+ - List-items start with `-` (useful to nest or "indent")
+- Use `<pre>` for code samples.
+
+Example: the help for |vim.paste()| is generated from a docstring decorating
+vim.paste in src/nvim/lua/vim.lua like this: >
+
+ --- Paste handler, invoked by |nvim_paste()| when a conforming UI
+ --- (such as the |TUI|) pastes text into the editor.
+ ---
+ --- Example: To remove ANSI color codes when pasting:
+ --- <pre>
+ --- vim.paste = (function()
+ --- local overridden = vim.paste
+ --- ...
+ --- end)()
+ --- </pre>
+ ---
+ --@see |paste|
+ ---
+ --@param lines ...
+ --@param phase ...
+ --@returns false if client should cancel the paste.
+
+
+LUA *dev-lua*
+
+- Keep the core Lua modules |lua-stdlib| simple. Avoid elaborate OOP or
+ pseudo-OOP designs. Plugin authors just want functions to call, they don't
+ want to learn a big, fancy inheritance hierarchy. So we should avoid complex
+ objects: tables are usually better.
+
+
API *dev-api*
Use this template to name new API functions:
@@ -155,10 +236,11 @@ with a {thing} that groups functions under a common concept).
Use existing common {action} names if possible:
add Append to, or insert into, a collection
- get Get a thing (or group of things by query)
- set Set a thing (or group of things)
del Delete a thing (or group of things)
+ exec Execute code
+ get Get a thing (or group of things by query)
list Get all things
+ set Set a thing (or group of things)
Use consistent names for {thing} in all API functions. E.g. a buffer is called
"buf" everywhere, not "buffer" in some places and "buf" in others.
@@ -268,8 +350,8 @@ External UIs are expected to implement these common features:
chords (<C-,> <C-Enter> <C-S-x> <D-x>) and patterns ("shift shift") that do
not potentially conflict with Nvim defaults, plugins, etc.
- Consider the "option_set" |ui-global| event as a hint for other GUI
- behaviors. UI-related options ('guifont', 'ambiwidth', …) are published in
- this event.
+ behaviors. Various UI-related options ('guifont', 'ambiwidth', …) are
+ published in this event. See also "mouse_on", "mouse_off".
vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/digraph.txt b/runtime/doc/digraph.txt
index b106e625f2..7f807b5eee 100644
--- a/runtime/doc/digraph.txt
+++ b/runtime/doc/digraph.txt
@@ -20,7 +20,9 @@ An alternative is using the 'keymap' option.
1. Defining digraphs *digraphs-define*
*:dig* *:digraphs*
-:dig[raphs] show currently defined digraphs.
+:dig[raphs][!] Show currently defined digraphs.
+ With [!] headers are used to make it a bit easier to
+ find a specific character.
*E104* *E39*
:dig[raphs] {char1}{char2} {number} ...
Add digraph {char1}{char2} to the list. {number} is
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index 512cfc4e58..597175b5e5 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -1217,7 +1217,7 @@ lambda expression *expr-lambda* *lambda*
{args -> expr1} lambda expression
A lambda expression creates a new unnamed function which returns the result of
-evaluating |expr1|. Lambda expressions differ from |user-functions| in
+evaluating |expr1|. Lambda expressions differ from |user-function|s in
the following ways:
1. The body of the lambda expression is an |expr1| and not a sequence of |Ex|
@@ -1547,10 +1547,12 @@ v:errmsg Last given error message.
:if v:errmsg != ""
: ... handle error
<
- *v:errors* *errors-variable*
+ *v:errors* *errors-variable* *assert-return*
v:errors Errors found by assert functions, such as |assert_true()|.
This is a list of strings.
The assert functions append an item when an assert fails.
+ The return value indicates this: a one is returned if an item
+ was added to v:errors, otherwise zero is returned.
To remove old results make it empty: >
:let v:errors = []
< If v:errors is set to anything but a list it is made an empty
@@ -1735,6 +1737,10 @@ v:lnum Line number for the 'foldexpr' |fold-expr|, 'formatexpr' and
expressions is being evaluated. Read-only when in the
|sandbox|.
+ *v:lua* *lua-variable*
+v:lua Prefix for calling Lua functions from expressions.
+ See |v:lua-call| for more information.
+
*v:mouse_win* *mouse_win-variable*
v:mouse_win Window number for a mouse click obtained with |getchar()|.
First window has number 1, like with |winnr()|. The value is
@@ -1984,9 +1990,12 @@ v:windowid Application-specific window "handle" which may be set by any
|window-ID|.
==============================================================================
-4. Builtin Functions *functions*
+4. Builtin Functions *vim-function* *functions*
+
+The Vimscript subsystem (referred to as "eval" internally) provides the
+following builtin functions. Scripts can also define |user-function|s.
-See |function-list| for a list grouped by what the function is used for.
+See |function-list| to browse functions by topic.
(Use CTRL-] on the function name to jump to the full explanation.)
@@ -2004,24 +2013,26 @@ argidx() Number current index in the argument list
arglistid([{winnr} [, {tabnr}]]) Number argument list id
argv({nr} [, {winid}]) String {nr} entry of the argument list
argv([-1, {winid}]) List the argument list
-assert_beeps({cmd}) none assert {cmd} causes a beep
+assert_beeps({cmd}) Number assert {cmd} causes a beep
assert_equal({exp}, {act} [, {msg}])
- none assert {exp} is equal to {act}
+ Number assert {exp} is equal to {act}
+assert_equalfile({fname-one}, {fname-two})
+ Number assert file contents is equal
assert_exception({error} [, {msg}])
- none assert {error} is in v:exception
-assert_fails({cmd} [, {error}]) none assert {cmd} fails
+ Number assert {error} is in v:exception
+assert_fails({cmd} [, {error}]) Number assert {cmd} fails
assert_false({actual} [, {msg}])
- none assert {actual} is false
+ Number assert {actual} is false
assert_inrange({lower}, {upper}, {actual} [, {msg}])
- none assert {actual} is inside the range
+ Number assert {actual} is inside the range
assert_match({pat}, {text} [, {msg}])
- none assert {pat} matches {text}
+ Number assert {pat} matches {text}
assert_notequal({exp}, {act} [, {msg}])
- none assert {exp} is not equal {act}
+ Number assert {exp} is not equal {act}
assert_notmatch({pat}, {text} [, {msg}])
- none assert {pat} not matches {text}
-assert_report({msg}) none report a test failure
-assert_true({actual} [, {msg}]) none assert {actual} is true
+ Number assert {pat} not matches {text}
+assert_report({msg}) Number report a test failure
+assert_true({actual} [, {msg}]) Number assert {actual} is true
asin({expr}) Float arc sine of {expr}
atan({expr}) Float arc tangent of {expr}
atan2({expr}, {expr}) Float arc tangent of {expr1} / {expr2}
@@ -2580,12 +2591,13 @@ argv([{nr} [, {winid}])
assert_beeps({cmd}) *assert_beeps()*
Run {cmd} and add an error message to |v:errors| if it does
NOT produce a beep or visual bell.
- Also see |assert_fails()|.
+ Also see |assert_fails()| and |assert-return|.
*assert_equal()*
assert_equal({expected}, {actual}, [, {msg}])
When {expected} and {actual} are not equal an error message is
- added to |v:errors|.
+ added to |v:errors| and 1 is returned. Otherwise zero is
+ returned |assert-return|.
There is no automatic conversion, the String "4" is different
from the Number 4. And the number 4 is different from the
Float 4.0. The value of 'ignorecase' is not used here, case
@@ -2597,9 +2609,17 @@ assert_equal({expected}, {actual}, [, {msg}])
< Will result in a string to be added to |v:errors|:
test.vim line 12: Expected 'foo' but got 'bar' ~
+ *assert_equalfile()*
+assert_equalfile({fname-one}, {fname-two})
+ When the files {fname-one} and {fname-two} do not contain
+ exactly the same text an error message is added to |v:errors|.
+ Also see |assert-return|.
+ When {fname-one} or {fname-two} does not exist the error will
+ mention that.
+
assert_exception({error} [, {msg}]) *assert_exception()*
When v:exception does not contain the string {error} an error
- message is added to |v:errors|.
+ message is added to |v:errors|. Also see |assert-return|.
This can be used to assert that a command throws an exception.
Using the error number, followed by a colon, avoids problems
with translations: >
@@ -2612,7 +2632,7 @@ assert_exception({error} [, {msg}]) *assert_exception()*
assert_fails({cmd} [, {error} [, {msg}]]) *assert_fails()*
Run {cmd} and add an error message to |v:errors| if it does
- NOT produce an error.
+ NOT produce an error. Also see |assert-return|.
When {error} is given it must match in |v:errmsg|.
Note that beeping is not considered an error, and some failing
commands only beep. Use |assert_beeps()| for those.
@@ -2620,6 +2640,7 @@ assert_fails({cmd} [, {error} [, {msg}]]) *assert_fails()*
assert_false({actual} [, {msg}]) *assert_false()*
When {actual} is not false an error message is added to
|v:errors|, like with |assert_equal()|.
+ Also see |assert-return|.
A value is false when it is zero or |v:false|. When "{actual}"
is not a number or |v:false| the assert fails.
When {msg} is omitted an error in the form
@@ -2636,7 +2657,7 @@ assert_inrange({lower}, {upper}, {actual} [, {msg}]) *assert_inrange()*
*assert_match()*
assert_match({pattern}, {actual} [, {msg}])
When {pattern} does not match {actual} an error message is
- added to |v:errors|.
+ added to |v:errors|. Also see |assert-return|.
{pattern} is used as with |=~|: The matching is always done
like 'magic' was set and 'cpoptions' is empty, no matter what
@@ -2657,18 +2678,22 @@ assert_match({pattern}, {actual} [, {msg}])
assert_notequal({expected}, {actual} [, {msg}])
The opposite of `assert_equal()`: add an error message to
|v:errors| when {expected} and {actual} are equal.
+ Also see |assert-return|.
*assert_notmatch()*
assert_notmatch({pattern}, {actual} [, {msg}])
The opposite of `assert_match()`: add an error message to
|v:errors| when {pattern} matches {actual}.
+ Also see |assert-return|.
assert_report({msg}) *assert_report()*
Report a test failure directly, using {msg}.
+ Always returns one.
assert_true({actual} [, {msg}]) *assert_true()*
When {actual} is not true an error message is added to
|v:errors|, like with |assert_equal()|.
+ Also see |assert-return|.
A value is |TRUE| when it is a non-zero number or |v:true|.
When {actual} is not a number or |v:true| the assert fails.
When {msg} is omitted an error in the form "Expected True but
@@ -3525,7 +3550,7 @@ exists({expr}) The result is a Number, which is |TRUE| if {expr} is
string)
*funcname built-in function (see |functions|)
or user defined function (see
- |user-functions|). Also works for a
+ |user-function|). Also works for a
variable that is a Funcref.
varname internal variable (see
|internal-variables|). Also works
@@ -4479,8 +4504,7 @@ getftype({fname}) *getftype()*
systems that support it. On some systems only "dir" and
"file" are returned.
- *getjumplist()*
-getjumplist([{winnr} [, {tabnr}]])
+getjumplist([{winnr} [, {tabnr}]]) *getjumplist()*
Returns the |jumplist| for the specified window.
Without arguments use the current window.
@@ -4536,6 +4560,10 @@ getloclist({nr},[, {what}]) *getloclist()*
If the optional {what} dictionary argument is supplied, then
returns the items listed in {what} as a dictionary. Refer to
|getqflist()| for the supported items in {what}.
+ If {what} contains 'filewinid', then returns the id of the
+ window used to display files from the location list. This
+ field is applicable only when called from a location list
+ window.
getmatches() *getmatches()*
Returns a |List| with all matches previously defined for the
@@ -4699,7 +4727,7 @@ getreg([{regname} [, 1 [, {list}]]]) *getreg()*
getregtype([{regname}]) *getregtype()*
The result is a String, which is type of register {regname}.
The value will be one of:
- "v" for |characterwise| text
+ "v" for |charwise| text
"V" for |linewise| text
"<CTRL-V>{width}" for |blockwise-visual| text
"" for an empty or unknown register
@@ -4941,9 +4969,11 @@ has({feature}) Returns 1 if {feature} is supported, 0 otherwise. The
< *feature-list*
List of supported pseudo-feature names:
acl |ACL| support
+ bsd BSD system (not macOS, use "mac" for that).
iconv Can use |iconv()| for conversion.
+shellslash Can use backslashes in filenames (Windows)
clipboard |clipboard| provider is available.
+ mac MacOS system.
nvim This is Nvim.
python2 Legacy Vim |python2| interface. |has-python|
python3 Legacy Vim |python3| interface. |has-python|
@@ -4953,6 +4983,7 @@ has({feature}) Returns 1 if {feature} is supported, 0 otherwise. The
unix Unix system.
*vim_starting* True during |startup|.
win32 Windows system (32 or 64 bit).
+ win64 Windows system (64 bit).
wsl WSL (Windows Subsystem for Linux) system
*has-patch*
@@ -5420,6 +5451,9 @@ jobstart({cmd}[, {opts}]) *jobstart()*
|on_exit| : exit event handler (function name or |Funcref|)
cwd : Working directory of the job; defaults to
|current-directory|.
+ env : A dict of strings to append (or replace see
+ |clear_env|) to the current environment.
+ clear_env: If set, use the exact values passed in |env|
rpc : If set, |msgpack-rpc| will be used to communicate
with the job over stdin and stdout. "on_stdout" is
then ignored, but "on_stderr" can still be used.
@@ -6103,7 +6137,7 @@ mode([expr]) Return a string that indicates the current mode.
n Normal
no Operator-pending
- nov Operator-pending (forced characterwise |o_v|)
+ nov Operator-pending (forced charwise |o_v|)
noV Operator-pending (forced linewise |o_V|)
noCTRL-V Operator-pending (forced blockwise |o_CTRL-V|)
niI Normal using |i_CTRL-O| in |Insert-mode|
@@ -6640,7 +6674,7 @@ remote_expr({server}, {string} [, {idvar} [, {timeout}]])
between (not at the end), like with join(expr, "\n").
If {idvar} is present and not empty, it is taken as the name
of a variable and a {serverid} for later use with
- remote_read() is stored there.
+ |remote_read()| is stored there.
If {timeout} is given the read times out after this many
seconds. Otherwise a timeout of 600 seconds is used.
See also |clientserver| |RemoteReply|.
@@ -7413,7 +7447,7 @@ setreg({regname}, {value} [, {options}])
If {options} contains "a" or {regname} is upper case,
then the value is appended.
{options} can also contain a register type specification:
- "c" or "v" |characterwise| mode
+ "c" or "v" |charwise| mode
"l" or "V" |linewise| mode
"b" or "<CTRL-V>" |blockwise-visual| mode
If a number immediately follows "b" or "<CTRL-V>" then this is
@@ -9222,7 +9256,7 @@ Don't forget that "^" will only match at the first character of the String and
"\n".
==============================================================================
-5. Defining functions *user-functions*
+5. Defining functions *user-function*
New functions can be defined. These can be called just like builtin
functions. The function executes a sequence of Ex commands. Normal mode
@@ -9680,7 +9714,7 @@ This does NOT work: >
register, "@/" for the search pattern.
If the result of {expr1} ends in a <CR> or <NL>, the
register will be linewise, otherwise it will be set to
- characterwise.
+ charwise.
This can be used to clear the last search pattern: >
:let @/ = ""
< This is different from searching for an empty string,
@@ -9762,6 +9796,54 @@ This does NOT work: >
Like above, but append/add/subtract the value for each
|List| item.
+ *:let=<<* *:let-heredoc*
+ *E990* *E991* *E172* *E221*
+:let {var-name} =<< [trim] {marker}
+text...
+text...
+{marker}
+ Set internal variable {var-name} to a List containing
+ the lines of text bounded by the string {marker}.
+ {marker} cannot start with a lower case character.
+ The last line should end only with the {marker} string
+ without any other character. Watch out for white
+ space after {marker}!
+
+ Without "trim" any white space characters in the lines
+ of text are preserved. If "trim" is specified before
+ {marker}, then indentation is stripped so you can do: >
+ let text =<< trim END
+ if ok
+ echo 'done'
+ endif
+ END
+< Results in: ["if ok", " echo 'done'", "endif"]
+ The marker must line up with "let" and the indentation
+ of the first line is removed from all the text lines.
+ Specifically: all the leading indentation exactly
+ matching the leading indentation of the first
+ non-empty text line is stripped from the input lines.
+ All leading indentation exactly matching the leading
+ indentation before `let` is stripped from the line
+ containing {marker}. Note that the difference between
+ space and tab matters here.
+
+ If {var-name} didn't exist yet, it is created.
+ Cannot be followed by another command, but can be
+ followed by a comment.
+
+ Examples: >
+ let var1 =<< END
+ Sample text 1
+ Sample text 2
+ Sample text 3
+ END
+
+ let data =<< trim DATA
+ 1 2 3 4
+ 5 6 7 8
+ DATA
+<
*E121*
:let {var-name} .. List the value of variable {var-name}. Multiple
variable names may be given. Special names recognized
diff --git a/runtime/doc/filetype.txt b/runtime/doc/filetype.txt
index c579c390c6..c649688d99 100644
--- a/runtime/doc/filetype.txt
+++ b/runtime/doc/filetype.txt
@@ -549,7 +549,9 @@ Variables:
*b:man_default_sects* Comma-separated, ordered list of preferred sections.
For example in C one usually wants section 3 or 2: >
:let b:man_default_sections = '3,2'
-*g:man_hardwrap* Hard-wrap to $MANWIDTH. May improve layout.
+*g:man_hardwrap* Hard-wrap to $MANWIDTH or window width if $MANWIDTH is
+ empty. Enabled by default. Set |FALSE| to enable soft
+ wrapping.
To use Nvim as a manpager: >
export MANPAGER='nvim +Man!'
@@ -558,10 +560,13 @@ Note that when running `man` from the shell and with that `MANPAGER` in your
environment, `man` will pre-format the manpage using `groff`. Thus, Neovim
will inevitably display the manual page as it was passed to it from stdin. One
of the caveats of this is that the width will _always_ be hard-wrapped and not
-soft wrapped as with `:Man`. You can set in your environment: >
+soft wrapped as with `g:man_hardwrap=0`. You can set in your environment: >
export MANWIDTH=999
-So `groff`'s pre-formatting output will be the same as with `:Man` i.e soft-wrapped.
+So `groff`'s pre-formatting output will be the same as with `g:man_hardwrap=0` i.e soft-wrapped.
+
+To disable bold highlighting: >
+ :highlight link manBold Normal
PDF *ft-pdf-plugin*
diff --git a/runtime/doc/help.txt b/runtime/doc/help.txt
index 284cd26583..6090fa96bb 100644
--- a/runtime/doc/help.txt
+++ b/runtime/doc/help.txt
@@ -129,6 +129,7 @@ Advanced editing ~
|autocmd.txt| automatically executing commands on an event
|eval.txt| expression evaluation, conditional commands
|fold.txt| hide (fold) ranges of lines
+|lua.txt| Lua API
Special issues ~
|print.txt| printing
@@ -157,7 +158,6 @@ GUI ~
Interfaces ~
|if_cscop.txt| using Cscope with Vim
-|if_lua.txt| Lua interface
|if_pyth.txt| Python interface
|if_ruby.txt| Ruby interface
|sign.txt| debugging signs
diff --git a/runtime/doc/if_lua.txt b/runtime/doc/if_lua.txt
index aa2d0a03c6..34bcf0f039 100644
--- a/runtime/doc/if_lua.txt
+++ b/runtime/doc/if_lua.txt
@@ -1,668 +1,8 @@
-*if_lua.txt* Nvim
- NVIM REFERENCE MANUAL
+ NVIM REFERENCE MANUAL
-
-Lua engine *lua* *Lua*
-
- Type |gO| to see the table of contents.
-
-==============================================================================
-Introduction *lua-intro*
-
-The Lua 5.1 language is builtin and always available. Try this command to get
-an idea of what lurks beneath: >
-
- :lua print(vim.inspect(package.loaded))
-
-Nvim includes a "standard library" |lua-stdlib| for Lua. It complements the
-"editor stdlib" (|functions| and Ex commands) and the |API|, all of which can
-be used from Lua code.
-
-Module conflicts are resolved by "last wins". For example if both of these
-are on 'runtimepath':
- runtime/lua/foo.lua
- ~/.config/nvim/lua/foo.lua
-then `require('foo')` loads "~/.config/nvim/lua/foo.lua", and
-"runtime/lua/foo.lua" is not used. See |lua-require| to understand how Nvim
-finds and loads Lua modules. The conventions are similar to VimL plugins,
-with some extra features. See |lua-require-example| for a walkthrough.
-
-==============================================================================
-Importing Lua modules *lua-require*
-
-Nvim automatically adjusts `package.path` and `package.cpath` according to
-effective 'runtimepath' value. Adjustment happens whenever 'runtimepath' is
-changed. `package.path` is adjusted by simply appending `/lua/?.lua` and
-`/lua/?/init.lua` to each directory from 'runtimepath' (`/` is actually the
-first character of `package.config`).
-
-Similarly to `package.path`, modified directories from 'runtimepath' are also
-added to `package.cpath`. In this case, instead of appending `/lua/?.lua` and
-`/lua/?/init.lua` to each runtimepath, all unique `?`-containing suffixes of
-the existing `package.cpath` are used. Example:
-
-1. Given that
- - 'runtimepath' contains `/foo/bar,/xxx;yyy/baz,/abc`;
- - initial (defined at compile-time or derived from
- `$LUA_CPATH`/`$LUA_INIT`) `package.cpath` contains
- `./?.so;/def/ghi/a?d/j/g.elf;/def/?.so`.
-2. It finds `?`-containing suffixes `/?.so`, `/a?d/j/g.elf` and `/?.so`, in
- order: parts of the path starting from the first path component containing
- question mark and preceding path separator.
-3. The suffix of `/def/?.so`, namely `/?.so` is not unique, as it’s the same
- as the suffix of the first path from `package.path` (i.e. `./?.so`). Which
- leaves `/?.so` and `/a?d/j/g.elf`, in this order.
-4. 'runtimepath' has three paths: `/foo/bar`, `/xxx;yyy/baz` and `/abc`. The
- second one contains semicolon which is a paths separator so it is out,
- leaving only `/foo/bar` and `/abc`, in order.
-5. The cartesian product of paths from 4. and suffixes from 3. is taken,
- giving four variants. In each variant `/lua` path segment is inserted
- between path and suffix, leaving
-
- - `/foo/bar/lua/?.so`
- - `/foo/bar/lua/a?d/j/g.elf`
- - `/abc/lua/?.so`
- - `/abc/lua/a?d/j/g.elf`
-
-6. New paths are prepended to the original `package.cpath`.
-
-The result will look like this:
-
- `/foo/bar,/xxx;yyy/baz,/abc` ('runtimepath')
- × `./?.so;/def/ghi/a?d/j/g.elf;/def/?.so` (`package.cpath`)
-
- = `/foo/bar/lua/?.so;/foo/bar/lua/a?d/j/g.elf;/abc/lua/?.so;/abc/lua/a?d/j/g.elf;./?.so;/def/ghi/a?d/j/g.elf;/def/?.so`
-
-Note:
-
-- To track 'runtimepath' updates, paths added at previous update are
- remembered and removed at the next update, while all paths derived from the
- new 'runtimepath' are prepended as described above. This allows removing
- paths when path is removed from 'runtimepath', adding paths when they are
- added and reordering `package.path`/`package.cpath` content if 'runtimepath'
- was reordered.
-
-- Although adjustments happen automatically, Nvim does not track current
- values of `package.path` or `package.cpath`. If you happen to delete some
- paths from there you can set 'runtimepath' to trigger an update: >
- let &runtimepath = &runtimepath
-
-- Skipping paths from 'runtimepath' which contain semicolons applies both to
- `package.path` and `package.cpath`. Given that there are some badly written
- plugins using shell which will not work with paths containing semicolons it
- is better to not have them in 'runtimepath' at all.
-
-------------------------------------------------------------------------------
-LUA PLUGIN EXAMPLE *lua-require-example*
-
-The following example plugin adds a command `:MakeCharBlob` which transforms
-current buffer into a long `unsigned char` array. Lua contains transformation
-function in a module `lua/charblob.lua` which is imported in
-`autoload/charblob.vim` (`require("charblob")`). Example plugin is supposed
-to be put into any directory from 'runtimepath', e.g. `~/.config/nvim` (in
-this case `lua/charblob.lua` means `~/.config/nvim/lua/charblob.lua`).
-
-autoload/charblob.vim: >
-
- function charblob#encode_buffer()
- call setline(1, luaeval(
- \ 'require("charblob").encode(unpack(_A))',
- \ [getline(1, '$'), &textwidth, ' ']))
- endfunction
-
-plugin/charblob.vim: >
-
- if exists('g:charblob_loaded')
- finish
- endif
- let g:charblob_loaded = 1
-
- command MakeCharBlob :call charblob#encode_buffer()
-
-lua/charblob.lua: >
-
- local function charblob_bytes_iter(lines)
- local init_s = {
- next_line_idx = 1,
- next_byte_idx = 1,
- lines = lines,
- }
- local function next(s, _)
- if lines[s.next_line_idx] == nil then
- return nil
- end
- if s.next_byte_idx > #(lines[s.next_line_idx]) then
- s.next_line_idx = s.next_line_idx + 1
- s.next_byte_idx = 1
- return ('\n'):byte()
- end
- local ret = lines[s.next_line_idx]:byte(s.next_byte_idx)
- if ret == ('\n'):byte() then
- ret = 0 -- See :h NL-used-for-NUL.
- end
- s.next_byte_idx = s.next_byte_idx + 1
- return ret
- end
- return next, init_s, nil
- end
-
- local function charblob_encode(lines, textwidth, indent)
- local ret = {
- 'const unsigned char blob[] = {',
- indent,
- }
- for byte in charblob_bytes_iter(lines) do
- -- .- space + number (width 3) + comma
- if #(ret[#ret]) + 5 > textwidth then
- ret[#ret + 1] = indent
- else
- ret[#ret] = ret[#ret] .. ' '
- end
- ret[#ret] = ret[#ret] .. (('%3u,'):format(byte))
- end
- ret[#ret + 1] = '};'
- return ret
- end
-
- return {
- bytes_iter = charblob_bytes_iter,
- encode = charblob_encode,
- }
-
-==============================================================================
-Commands *lua-commands*
-
- *:lua*
-:[range]lua {chunk}
- Execute Lua chunk {chunk}.
-
-Examples:
->
- :lua vim.api.nvim_command('echo "Hello, Nvim!"')
-<
-To see the Lua version: >
- :lua print(_VERSION)
-
-To see the LuaJIT version: >
- :lua print(jit.version)
-<
-
-:[range]lua << [endmarker]
-{script}
-{endmarker}
- Execute Lua script {script}. Useful for including Lua
- code in Vim scripts.
-
-The {endmarker} must NOT be preceded by any white space.
-
-If [endmarker] is omitted from after the "<<", a dot '.' must be used after
-{script}, like for the |:append| and |:insert| commands.
-
-Example:
->
- function! CurrentLineInfo()
- lua << EOF
- local linenr = vim.api.nvim_win_get_cursor(0)[1]
- local curline = vim.api.nvim_buf_get_lines(
- 0, linenr, linenr + 1, false)[1]
- print(string.format("Current line [%d] has %d bytes",
- linenr, #curline))
- EOF
- endfunction
-
-Note that the `local` variables will disappear when block finishes. This is
-not the case for globals.
-
- *:luado*
-:[range]luado {body} Execute Lua function "function (line, linenr) {body}
- end" for each line in the [range], with the function
- argument being set to the text of each line in turn,
- without a trailing <EOL>, and the current line number.
- If the value returned by the function is a string it
- becomes the text of the line in the current turn. The
- default for [range] is the whole file: "1,$".
-
-Examples:
->
- :luado return string.format("%s\t%d", line:reverse(), #line)
-
- :lua require"lpeg"
- :lua -- balanced parenthesis grammar:
- :lua bp = lpeg.P{ "(" * ((1 - lpeg.S"()") + lpeg.V(1))^0 * ")" }
- :luado if bp:match(line) then return "-->\t" .. line end
-<
-
- *:luafile*
-:[range]luafile {file}
- Execute Lua script in {file}.
- The whole argument is used as a single file name.
-
-Examples:
->
- :luafile script.lua
- :luafile %
-<
-
-All these commands execute a Lua chunk from either the command line (:lua and
-:luado) or a file (:luafile) with the given line [range]. Similarly to the Lua
-interpreter, each chunk has its own scope and so only global variables are
-shared between command calls. All Lua default libraries are available. In
-addition, Lua "print" function has its output redirected to the Nvim message
-area, with arguments separated by a white space instead of a tab.
-
-Lua uses the "vim" module (see |lua-vim|) to issue commands to Nvim. However,
-procedures that alter buffer content, open new buffers, and change cursor
-position are restricted when the command is executed in the |sandbox|.
-
-
-==============================================================================
-luaeval() *lua-eval* *luaeval()*
-
-The (dual) equivalent of "vim.eval" for passing Lua values to Nvim is
-"luaeval". "luaeval" takes an expression string and an optional argument used
-for _A inside expression and returns the result of the expression. It is
-semantically equivalent in Lua to:
->
- local chunkheader = "local _A = select(1, ...) return "
- function luaeval (expstr, arg)
- local chunk = assert(loadstring(chunkheader .. expstr, "luaeval"))
- return chunk(arg) -- return typval
- end
-
-Lua nils, numbers, strings, tables and booleans are converted to their
-respective VimL types. An error is thrown if conversion of any other Lua types
-is attempted.
-
-The magic global "_A" contains the second argument to luaeval().
-
-Example: >
- :echo luaeval('_A[1] + _A[2]', [40, 2])
- 42
- :echo luaeval('string.match(_A, "[a-z]+")', 'XYXfoo123')
- foo
-
-Lua tables are used as both dictionaries and lists, so it is impossible to
-determine whether empty table is meant to be empty list or empty dictionary.
-Additionally lua does not have integer numbers. To distinguish between these
-cases there is the following agreement:
-
-0. Empty table is empty list.
-1. Table with N incrementally growing integral numbers, starting from 1 and
- ending with N is considered to be a list.
-2. Table with string keys, none of which contains NUL byte, is considered to
- be a dictionary.
-3. Table with string keys, at least one of which contains NUL byte, is also
- considered to be a dictionary, but this time it is converted to
- a |msgpack-special-map|.
- *lua-special-tbl*
-4. Table with `vim.type_idx` key may be a dictionary, a list or floating-point
- value:
- - `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to
- a floating-point 1.0. Note that by default integral lua numbers are
- converted to |Number|s, non-integral are converted to |Float|s. This
- variant allows integral |Float|s.
- - `{[vim.type_idx]=vim.types.dictionary}` is converted to an empty
- dictionary, `{[vim.type_idx]=vim.types.dictionary, [42]=1, a=2}` is
- converted to a dictionary `{'a': 42}`: non-string keys are ignored.
- Without `vim.type_idx` key tables with keys not fitting in 1., 2. or 3.
- are errors.
- - `{[vim.type_idx]=vim.types.list}` is converted to an empty list. As well
- as `{[vim.type_idx]=vim.types.list, [42]=1}`: integral keys that do not
- form a 1-step sequence from 1 to N are ignored, as well as all
- non-integral keys.
-
-Examples: >
-
- :echo luaeval('math.pi')
- :function Rand(x,y) " random uniform between x and y
- : return luaeval('(_A.y-_A.x)*math.random()+_A.x', {'x':a:x,'y':a:y})
- : endfunction
- :echo Rand(1,10)
-
-Note that currently second argument to `luaeval` undergoes VimL to lua
-conversion, so changing containers in lua do not affect values in VimL. Return
-value is also always converted. When converting, |msgpack-special-dict|s are
-treated specially.
-
-==============================================================================
-Lua standard modules *lua-stdlib*
-
-The Nvim Lua "standard library" (stdlib) is the `vim` module, which exposes
-various functions and sub-modules. It is always loaded, thus require("vim")
-is unnecessary.
-
-You can peek at the module properties: >
-
- :lua print(vim.inspect(vim))
-
-Result is something like this: >
-
- {
- _os_proc_children = <function 1>,
- _os_proc_info = <function 2>,
- ...
- api = {
- nvim__id = <function 5>,
- nvim__id_array = <function 6>,
- ...
- },
- deepcopy = <function 106>,
- gsplit = <function 107>,
- ...
- }
-
-To find documentation on e.g. the "deepcopy" function: >
-
- :help vim.deepcopy
-
-Note that underscore-prefixed functions (e.g. "_os_proc_children") are
-internal/private and must not be used by plugins.
-
-------------------------------------------------------------------------------
-VIM.API *lua-api*
-
-`vim.api` exposes the full Nvim |API| as a table of Lua functions.
-
-Example: to use the "nvim_get_current_line()" API function, call
-"vim.api.nvim_get_current_line()": >
-
- print(tostring(vim.api.nvim_get_current_line()))
-
-------------------------------------------------------------------------------
-VIM.LOOP *lua-loop*
-
-`vim.loop` exposes all features of the Nvim event-loop. This is a low-level
-API that provides functionality for networking, filesystem, and process
-management. Try this command to see available functions: >
-
- :lua print(vim.inspect(vim.loop))
-
-Reference: http://docs.libuv.org
-Examples: https://github.com/luvit/luv/tree/master/examples
-
- *E5560* *lua-loop-callbacks*
-It is an error to directly invoke `vim.api` functions (except |api-fast|) in
-`vim.loop` callbacks. For example, this is an error: >
-
- local timer = vim.loop.new_timer()
- timer:start(1000, 0, function()
- vim.api.nvim_command('echomsg "test"')
- end)
-
-To avoid the error use |vim.schedule_wrap()| to defer the callback: >
-
- local timer = vim.loop.new_timer()
- timer:start(1000, 0, vim.schedule_wrap(function()
- vim.api.nvim_command('echomsg "test"')
- end))
-
-Example: repeating timer
- 1. Save this code to a file.
- 2. Execute it with ":luafile %". >
-
- -- Create a timer handle (implementation detail: uv_timer_t).
- local timer = vim.loop.new_timer()
- local i = 0
- -- Waits 1000ms, then repeats every 750ms until timer:close().
- timer:start(1000, 750, function()
- print('timer invoked! i='..tostring(i))
- if i > 4 then
- timer:close() -- Always close handles to avoid leaks.
- end
- i = i + 1
- end)
- print('sleeping');
-
-
-Example: TCP echo-server *tcp-server*
- 1. Save this code to a file.
- 2. Execute it with ":luafile %".
- 3. Note the port number.
- 4. Connect from any TCP client (e.g. "nc 0.0.0.0 36795"): >
-
- local function create_server(host, port, on_connection)
- local server = vim.loop.new_tcp()
- server:bind(host, port)
- server:listen(128, function(err)
- assert(not err, err) -- Check for errors.
- local sock = vim.loop.new_tcp()
- server:accept(sock) -- Accept client connection.
- on_connection(sock) -- Start reading messages.
- end)
- return server
- end
- local server = create_server('0.0.0.0', 0, function(sock)
- sock:read_start(function(err, chunk)
- assert(not err, err) -- Check for errors.
- if chunk then
- sock:write(chunk) -- Echo received messages to the channel.
- else -- EOF (stream closed).
- sock:close() -- Always close handles to avoid leaks.
- end
- end)
- end)
- print('TCP echo-server listening on port: '..server:getsockname().port)
-
-------------------------------------------------------------------------------
-VIM *lua-util*
-
-vim.in_fast_event() *vim.in_fast_event()*
- Returns true if the code is executing as part of a "fast" event
- handler, where most of the API is disabled. These are low-level events
- (e.g. |lua-loop-callbacks|) which can be invoked whenever Nvim polls
- for input. When this is `false` most API functions are callable (but
- may be subject to other restrictions such as |textlock|).
-
-vim.stricmp({a}, {b}) *vim.stricmp()*
- Compares strings case-insensitively. Returns 0, 1 or -1 if strings
- are equal, {a} is greater than {b} or {a} is lesser than {b},
- respectively.
-
-vim.str_utfindex({str}[, {index}]) *vim.str_utfindex()*
- Convert byte index to UTF-32 and UTF-16 indicies. If {index} is not
- supplied, the length of the string is used. All indicies are zero-based.
- Returns two values: the UTF-32 and UTF-16 indicies respectively.
-
- Embedded NUL bytes are treated as terminating the string. Invalid
- UTF-8 bytes, and embedded surrogates are counted as one code
- point each. An {index} in the middle of a UTF-8 sequence is rounded
- upwards to the end of that sequence.
-
-vim.str_byteindex({str}, {index}[, {use_utf16}]) *vim.str_byteindex()*
- Convert UTF-32 or UTF-16 {index} to byte index. If {use_utf16} is not
- supplied, it defaults to false (use UTF-32). Returns the byte index.
-
- Invalid UTF-8 and NUL is treated like by |vim.str_byteindex()|. An {index}
- in the middle of a UTF-16 sequence is rounded upwards to the end of that
- sequence.
-
-vim.schedule({callback}) *vim.schedule()*
- Schedules {callback} to be invoked soon by the main event-loop. Useful
- to avoid |textlock| or other temporary restrictions.
-
-vim.type_idx *vim.type_idx*
- Type index for use in |lua-special-tbl|. Specifying one of the
- values from |vim.types| allows typing the empty table (it is
- unclear whether empty lua table represents empty list or empty array)
- and forcing integral numbers to be |Float|. See |lua-special-tbl| for
- more details.
-
-vim.val_idx *vim.val_idx*
- Value index for tables representing |Float|s. A table representing
- floating-point value 1.0 looks like this: >
- {
- [vim.type_idx] = vim.types.float,
- [vim.val_idx] = 1.0,
- }
-< See also |vim.type_idx| and |lua-special-tbl|.
-
-vim.types *vim.types*
- Table with possible values for |vim.type_idx|. Contains two sets
- of key-value pairs: first maps possible values for |vim.type_idx|
- to human-readable strings, second maps human-readable type names to
- values for |vim.type_idx|. Currently contains pairs for `float`,
- `array` and `dictionary` types.
-
- Note: one must expect that values corresponding to `vim.types.float`,
- `vim.types.array` and `vim.types.dictionary` fall under only two
- following assumptions:
- 1. Value may serve both as a key and as a value in a table. Given the
- properties of lua tables this basically means “value is not `nil`â€.
- 2. For each value in `vim.types` table `vim.types[vim.types[value]]`
- is the same as `value`.
- No other restrictions are put on types, and it is not guaranteed that
- values corresponding to `vim.types.float`, `vim.types.array` and
- `vim.types.dictionary` will not change or that `vim.types` table will
- only contain values for these three types.
+Moved to |lua.txt|
==============================================================================
-Lua module: vim *lua-vim*
-
-inspect({object}, {options}) *vim.inspect()*
- Return a human-readable representation of the given object.
-
- See also: ~
- https://github.com/kikito/inspect.lua
- https://github.com/mpeterv/vinspect
-
-paste({lines}, {phase}) *vim.paste()*
- Paste handler, invoked by |nvim_paste()| when a conforming UI
- (such as the |TUI|) pastes text into the editor.
-
- Parameters: ~
- {lines} |readfile()|-style list of lines to paste.
- |channel-lines|
- {phase} -1: "non-streaming" paste: the call contains all
- lines. If paste is "streamed", `phase` indicates the stream state:
- • 1: starts the paste (exactly once)
- • 2: continues the paste (zero or more times)
- • 3: ends the paste (exactly once)
-
- Return: ~
- false if client should cancel the paste.
-
- See also: ~
- |paste|
-
-schedule_wrap({cb}) *vim.schedule_wrap()*
- Defers callback `cb` until the Nvim API is safe to call.
-
- See also: ~
- |lua-loop-callbacks|
- |vim.schedule()|
- |vim.in_fast_event()|
-
-
-
-
-deepcopy({orig}) *vim.deepcopy()*
- Returns a deep copy of the given object. Non-table objects are
- copied as in a typical Lua assignment, whereas table objects
- are copied recursively.
-
- Parameters: ~
- {orig} Table to copy
-
- Return: ~
- New table of copied keys and (nested) values.
-
-gsplit({s}, {sep}, {plain}) *vim.gsplit()*
- Splits a string at each instance of a separator.
-
- Parameters: ~
- {s} String to split
- {sep} Separator string or pattern
- {plain} If `true` use `sep` literally (passed to
- String.find)
-
- Return: ~
- Iterator over the split components
-
- See also: ~
- |vim.split()|
- https://www.lua.org/pil/20.2.html
- http://lua-users.org/wiki/StringLibraryTutorial
-
-split({s}, {sep}, {plain}) *vim.split()*
- Splits a string at each instance of a separator.
-
- Examples: >
- split(":aa::b:", ":") --> {'','aa','','bb',''}
- split("axaby", "ab?") --> {'','x','y'}
- split(x*yz*o, "*", true) --> {'x','yz','o'}
-<
-
- Parameters: ~
- {s} String to split
- {sep} Separator string or pattern
- {plain} If `true` use `sep` literally (passed to
- String.find)
-
- Return: ~
- List-like table of the split components.
-
- See also: ~
- |vim.gsplit()|
-
-tbl_contains({t}, {value}) *vim.tbl_contains()*
- Checks if a list-like (vector) table contains `value` .
-
- Parameters: ~
- {t} Table to check
- {value} Value to compare
-
- Return: ~
- true if `t` contains `value`
-
-tbl_extend({behavior}, {...}) *vim.tbl_extend()*
- Merges two or more map-like tables.
-
- Parameters: ~
- {behavior} Decides what to do if a key is found in more
- than one map:
- • "error": raise an error
- • "keep": use value from the leftmost map
- • "force": use value from the rightmost map
- {...} Two or more map-like tables.
-
- See also: ~
- |extend()|
-
-tbl_flatten({t}) *vim.tbl_flatten()*
- Creates a copy of a list-like table such that any nested
- tables are "unrolled" and appended to the result.
-
- Parameters: ~
- {t} List-like table
-
- Return: ~
- Flattened copy of the given list-like table.
-
-trim({s}) *vim.trim()*
- Trim whitespace (Lua pattern "%s") from both sides of a
- string.
-
- Parameters: ~
- {s} String to trim
-
- Return: ~
- String with whitespace removed from its beginning and end
-
- See also: ~
- https://www.lua.org/pil/20.2.html
-
-pesc({s}) *vim.pesc()*
- Escapes magic chars in a Lua pattern string.
-
- Parameters: ~
- {s} String to escape
-
- Return: ~
- %-escaped pattern string
-
- See also: ~
- https://github.com/rxi/lume
-
- vim:tw=78:ts=8:ft=help:norl:
+ vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt
index be9e25113a..ed9853a8da 100644
--- a/runtime/doc/index.txt
+++ b/runtime/doc/index.txt
@@ -404,7 +404,7 @@ tag char note action in Normal mode ~
|t| t{char} 1 cursor till before Nth occurrence of {char}
to the right
|u| u 2 undo changes
-|v| v start characterwise Visual mode
+|v| v start charwise Visual mode
|w| w 1 cursor N words forward
|x| ["x]x 2 delete N characters under and after the
cursor [into register x]
@@ -767,6 +767,7 @@ tag char note action in Normal mode ~
|gn| gn 1,2 find the next match with the last used
search pattern and Visually select it
|gm| gm 1 go to character at middle of the screenline
+|gM| gM 1 go to character at middle of the text line
|go| go 1 cursor to byte N in the buffer
|gp| ["x]gp 2 put the text [from register x] after the
cursor N times, leave the cursor after it
@@ -865,7 +866,7 @@ These can be used after an operator, but before a {motion} has been entered.
tag char action in Operator-pending mode ~
-----------------------------------------------------------------------
-|o_v| v force operator to work characterwise
+|o_v| v force operator to work charwise
|o_V| V force operator to work linewise
|o_CTRL-V| CTRL-V force operator to work blockwise
@@ -977,7 +978,7 @@ tag command note action in Visual mode ~
|v_r| r 2 replace highlighted area with a character
|v_s| s 2 delete highlighted area and start insert
|v_u| u 2 make highlighted area lowercase
-|v_v| v make Visual mode characterwise or stop
+|v_v| v make Visual mode charwise or stop
Visual mode
|v_x| x 2 delete the highlighted area
|v_y| y yank the highlighted area
@@ -1163,11 +1164,13 @@ tag command action ~
|:cNfile| :cNf[ile] go to last error in previous file
|:cabbrev| :ca[bbrev] like ":abbreviate" but for Command-line mode
|:cabclear| :cabc[lear] clear all abbreviations for Command-line mode
+|:cabove| :cabo[ve] go to error above current line
|:caddbuffer| :cad[dbuffer] add errors from buffer
|:caddexpr| :cadde[xpr] add errors from expr
|:caddfile| :caddf[ile] add error message to current quickfix list
|:call| :cal[l] call a function
|:catch| :cat[ch] part of a :try command
+|:cbelow| :cbe[low] go to error below current line
|:cbottom| :cbo[ttom] scroll to the bottom of the quickfix window
|:cbuffer| :cb[uffer] parse error messages and jump to first error
|:cc| :cc go to specific error
@@ -1324,12 +1327,14 @@ tag command action ~
|:lNext| :lN[ext] go to previous entry in location list
|:lNfile| :lNf[ile] go to last entry in previous file
|:list| :l[ist] print lines
+|:labove| :lab[ove] go to location above current line
|:laddexpr| :lad[dexpr] add locations from expr
|:laddbuffer| :laddb[uffer] add locations from buffer
|:laddfile| :laddf[ile] add locations to current location list
|:last| :la[st] go to the last file in the argument list
|:language| :lan[guage] set the language (locale)
|:later| :lat[er] go to newer change, redo
+|:lbelow| :lbe[low] go to location below current line
|:lbottom| :lbo[ttom] scroll to the bottom of the location window
|:lbuffer| :lb[uffer] parse locations and jump to first location
|:lcd| :lc[d] change directory locally
diff --git a/runtime/doc/intro.txt b/runtime/doc/intro.txt
index 887ef764bd..c59ed43a47 100644
--- a/runtime/doc/intro.txt
+++ b/runtime/doc/intro.txt
@@ -271,7 +271,7 @@ and <> are part of what you type, the context should make this clear.
operator is pending.
- Ex commands can be used to move the cursor. This can be
used to call a function that does some complicated motion.
- The motion is always characterwise exclusive, no matter
+ The motion is always charwise exclusive, no matter
what ":" command is used. This means it's impossible to
include the last character of a line without the line break
(unless 'virtualedit' is set).
@@ -378,11 +378,11 @@ notation meaning equivalent decimal value(s) ~
<kEqual> keypad = *keypad-equal*
<kEnter> keypad Enter *keypad-enter*
<k0> - <k9> keypad 0 to 9 *keypad-0* *keypad-9*
-<S-...> shift-key *shift* *<S-*
-<C-...> control-key *control* *ctrl* *<C-*
-<M-...> alt-key or meta-key *META* *ALT* *<M-*
-<A-...> same as <M-...> *<A-*
-<D-...> command-key or "super" key *<D-*
+<S-…> shift-key *shift* *<S-*
+<C-…> control-key *control* *ctrl* *<C-*
+<M-…> alt-key or meta-key *META* *ALT* *<M-*
+<A-…> same as <M-…> *<A-*
+<D-…> command-key or "super" key *<D-*
-----------------------------------------------------------------------
Note: The shifted cursor keys, the help key, and the undo key are only
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
new file mode 100644
index 0000000000..d6d16b8481
--- /dev/null
+++ b/runtime/doc/lsp.txt
@@ -0,0 +1,575 @@
+*lsp.txt* Nvim LSP API
+
+ NVIM REFERENCE MANUAL
+
+
+Nvim Language Server Protocol (LSP) API *lsp*
+
+Nvim is a client to the Language Server Protocol:
+
+ https://microsoft.github.io/language-server-protocol/
+
+ Type |gO| to see the table of contents.
+
+================================================================================
+LANGUAGE SERVER PROTOCOL (LSP) CLIENT *lsp-intro*
+
+The `vim.lsp` Lua module provides a flexible API for consuming LSP servers.
+
+To use LSP in practice, a language server must be installed.
+ https://microsoft.github.io/language-server-protocol/implementors/servers/
+
+After installing a language server to your machine, you must tell Nvim how to
+start and interact with that language server.
+- Easy way: use the configs provided here by the nvim-lsp plugin.
+ https://github.com/neovim/nvim-lsp
+- Low-level way: use |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()|
+ directly. Useful if you want to build advanced LSP plugins based on the
+ Nvim LSP module. |lsp-advanced-js-example|
+
+ *lsp-config*
+Nvim LSP client will automatically provide inline diagnostics when available.
+|lsp-callbacks| But you probably want to use other features too, such as
+go-to-definition, "hover", etc. Example config: >
+
+ nnoremap <silent> gd <cmd>lua vim.lsp.buf.declaration()<CR>
+ nnoremap <silent> <c-]> <cmd>lua vim.lsp.buf.definition()<CR>
+ nnoremap <silent> K <cmd>lua vim.lsp.buf.hover()<CR>
+ nnoremap <silent> gD <cmd>lua vim.lsp.buf.implementation()<CR>
+ nnoremap <silent> <c-k> <cmd>lua vim.lsp.buf.signature_help()<CR>
+ nnoremap <silent> 1gD <cmd>lua vim.lsp.buf.type_definition()<CR>
+
+<
+ *vim.lsp.omnifunc()*
+Nvim provides the vim.lsp.omnifunc 'omnifunc' handler which allows
+|i_CTRL-X_CTRL-O| to consume LSP completion features. Example config (note the
+use of |v:lua| to call Lua from Vimscript): >
+
+ " Use LSP omni-completion in Python files.
+ autocmd Filetype python setlocal omnifunc=v:lua.vim.lsp.omnifunc
+
+
+FAQ ~
+
+> How to force-reload LSP?
+
+Stop all clients, then reload the buffer. >
+
+ :lua vim.lsp.stop_all_clients()
+ :edit
+
+> Why isn't completion working?
+
+In the buffer where you want to use LSP, check that 'omnifunc' is set to
+"v:lua.vim.lsp.omnifunc": >
+
+ :verbose set omnifunc?
+
+Some other plugin may be overriding the option. To avoid that, you could set
+the option in an |after-directory| ftplugin, e.g. "after/ftplugin/python.vim".
+
+================================================================================
+ *lsp-core-api*
+These are the core api functions for working with clients. You will mainly be
+using |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()| for operations
+and |vim.lsp.get_client_by_id()| to retrieve a client by its id after it has
+initialized (or {config.on_init}. see below)
+
+ *vim.lsp.start_client()*
+
+vim.lsp.start_client({config})
+
+ The main function used for starting clients.
+ Start a client and initialize it.
+
+ Its arguments are passed via a configuration object {config}.
+
+ Mandatory parameters:~
+
+ `root_dir`
+ {string} specifying the directory where the LSP server will base
+ as its rootUri on initialization.
+
+ `cmd`
+ {string} or {list} which is the base command to execute for the LSP. A
+ string will be run using |'shell'| and a list will be interpreted as a
+ bare command with arguments passed. This is the same as |jobstart()|.
+
+ Optional parameters:~
+
+ `cmd_cwd`
+ {string} specifying the directory to launch the `cmd` process. This is not
+ related to `root_dir`.
+ By default, |getcwd()| is used.
+
+ `cmd_env`
+ {table} specifying the environment flags to pass to the LSP on spawn.
+ This can be specified using keys like a map or as a list with `k=v` pairs
+ or both. Non-string values are coerced to a string.
+ For example:
+ `{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`
+
+ `capabilities`
+ A {table} which will be used instead of
+ `vim.lsp.protocol.make_client_capabilities()` which contains Nvim's
+ default capabilities and passed to the language server on initialization.
+ You'll probably want to use make_client_capabilities() and modify the
+ result.
+ NOTE:
+ To send an empty dictionary, you should use
+ `{[vim.type_idx]=vim.types.dictionary}` Otherwise, it will be encoded as
+ an array.
+
+ `callbacks`
+ A {table} of whose keys are language server method names and the values
+ are `function(err, method, params, client_id)` See |lsp-callbacks| for
+ more. This will be combined with |lsp-default-callbacks| to resolve
+ the callbacks for a client as a fallback.
+
+ `init_options`
+ A {table} of values to pass in the initialization request as
+ `initializationOptions`. See the `initialize` in the LSP spec.
+
+ `name`
+ A {string} used in log messages. Defaults to {client_id}
+
+ `offset_encoding`
+ One of "utf-8", "utf-16", or "utf-32" which is the encoding that the LSP
+ server expects.
+ The default encoding for Language Server Protocol is UTF-16, but there are
+ language servers which may use other encodings.
+ By default, it is "utf-16" as specified in the LSP specification. The
+ client does not verify this is correct.
+
+ `on_error(code, ...)`
+ A function for handling errors thrown by client operation. {code} is a
+ number describing the error. Other arguments may be passed depending on
+ the error kind. See |vim.lsp.client_errors| for possible errors.
+ `vim.lsp.client_errors[code]` can be used to retrieve a human
+ understandable string.
+
+ `before_init(initialize_params, config)`
+ A function which is called *before* the request `initialize` is completed.
+ `initialize_params` contains the parameters we are sending to the server
+ and `config` is the config that was passed to `start_client()` for
+ convenience. You can use this to modify parameters before they are sent.
+
+ `on_init(client, initialize_result)`
+ A function which is called after the request `initialize` is completed.
+ `initialize_result` contains `capabilities` and anything else the server
+ may send. For example, `clangd` sends `initialize_result.offsetEncoding`
+ if `capabilities.offsetEncoding` was sent to it. You can *only* modify the
+ `client.offset_encoding` here before any notifications are sent.
+
+ `on_attach(client, bufnr)`
+ A function which is called after the client is attached to a buffer.
+
+ `on_exit(code, signal, client_id)`
+ A function which is called after the client has exited. code is the exit
+ code of the process, and signal is a number describing the signal used to
+ terminate (if any).
+
+ `trace`
+ "off" | "messages" | "verbose" | nil passed directly to the language
+ server in the initialize request.
+ Invalid/empty values will default to "off"
+
+ Returns:~
+ {client_id}
+ You can use |vim.lsp.get_client_by_id()| to get the actual client object.
+ See |lsp-client| for what the client structure will be.
+
+ NOTE: The client is only available *after* it has been initialized, which
+ may happen after a small delay (or never if there is an error). For this
+ reason, you may want to use `on_init` to do any actions once the client has
+ been initialized.
+
+ *lsp-client*
+
+The client object has some methods and members related to using the client.
+
+ Methods:~
+
+ `request(method, params, [callback])`
+ Send a request to the server. If callback is not specified, it will use
+ {client.callbacks} to try to find a callback. If one is not found there,
+ then an error will occur.
+ This is a thin wrapper around {client.rpc.request} with some additional
+ checking.
+ Returns a boolean to indicate if the notification was successful. If it
+ is false, then it will always be false (the client has shutdown).
+ If it was successful, then it will return the request id as the second
+ result. You can use this with `notify("$/cancel", { id = request_id })`
+ to cancel the request. This helper is made automatically with
+ |vim.lsp.buf_request()|
+ Returns: status, [client_id]
+
+ `notify(method, params)`
+ This is just {client.rpc.notify}()
+ Returns a boolean to indicate if the notification was successful. If it
+ is false, then it will always be false (the client has shutdown).
+ Returns: status
+
+ `cancel_request(id)`
+ This is just {client.rpc.notify}("$/cancelRequest", { id = id })
+ Returns the same as `notify()`.
+
+ `stop([force])`
+ Stop a client, optionally with force.
+ By default, it will just ask the server to shutdown without force.
+ If you request to stop a client which has previously been requested to
+ shutdown, it will automatically escalate and force shutdown.
+
+ `is_stopped()`
+ Returns true if the client is fully stopped.
+
+ Members: ~
+ `id` (number)
+ The id allocated to the client.
+
+ `name` (string)
+ If a name is specified on creation, that will be used. Otherwise it is
+ just the client id. This is used for logs and messages.
+
+ `offset_encoding` (string)
+ The encoding used for communicating with the server. You can modify this
+ in the `on_init` method before text is sent to the server.
+
+ `callbacks` (table)
+ The callbacks used by the client as described in |lsp-callbacks|.
+
+ `config` (table)
+ A copy of the table that was passed by the user to
+ |vim.lsp.start_client()|.
+
+ `server_capabilities` (table)
+ The response from the server sent on `initialize` describing the
+ server's capabilities.
+
+ `resolved_capabilities` (table)
+ A normalized table of capabilities that we have detected based on the
+ initialize response from the server in `server_capabilities`.
+
+
+ *vim.lsp.buf_attach_client()*
+vim.lsp.buf_attach_client({bufnr}, {client_id})
+
+ Implements the `textDocument/did*` notifications required to track a buffer
+ for any language server.
+
+ Without calling this, the server won't be notified of changes to a buffer.
+
+ *vim.lsp.get_client_by_id()*
+vim.lsp.get_client_by_id({client_id})
+
+ Look up an active client by its id, returns nil if it is not yet initialized
+ or is not a valid id. Returns |lsp-client|
+
+ *vim.lsp.stop_client()*
+vim.lsp.stop_client({client_id}, [{force}])
+
+ Stop a client, optionally with force.
+ By default, it will just ask the server to shutdown without force.
+ If you request to stop a client which has previously been requested to
+ shutdown, it will automatically escalate and force shutdown.
+
+ You can also use `client.stop()` if you have access to the client.
+
+ *vim.lsp.stop_all_clients()*
+vim.lsp.stop_all_clients([{force}])
+
+ |vim.lsp.stop_client()|, but for all active clients.
+
+ *vim.lsp.get_active_clients()*
+vim.lsp.get_active_clients()
+
+ Return a list of all of the active clients. See |lsp-client| for a
+ description of what a client looks like.
+
+ *vim.lsp.rpc_response_error()*
+vim.lsp.rpc_response_error({code}, [{message}], [{data}])
+
+ Helper function to create an RPC response object/table. This is an alias for
+ |vim.lsp.rpc.rpc_response_error|. Code must be an RPC error code as
+ described in `vim.lsp.protocol.ErrorCodes`.
+
+ You can describe an optional {message} string or arbitrary {data} to send to
+ the server.
+
+================================================================================
+LSP CALLBACKS *lsp-callbacks*
+
+DEFAULT CALLBACKS ~
+ *vim.lsp.callbacks*
+The `vim.lsp.callbacks` table defines default callbacks used when
+creating a new client. Keys are LSP method names: >
+
+ :lua print(vim.inspect(vim.tbl_keys(vim.lsp.callbacks)))
+
+These LSP requests/notifications are defined by default:
+
+ textDocument/publishDiagnostics
+ window/logMessage
+ window/showMessage
+
+You can check these via `vim.tbl_keys(vim.lsp.callbacks)`.
+
+These will be used preferrentially in `vim.lsp.buf` methods when handling
+requests. They will also be used when responding to server requests and
+notifications.
+
+Use cases:
+- Users can modify this to customize to their preferences.
+- UI plugins can modify this by assigning to
+ `vim.lsp.callbacks[method]` so as to provide more specialized
+ handling, allowing you to leverage the UI capabilities available. UIs should
+ try to be conscientious of any existing changes the user may have set
+ already by checking for existing values.
+
+Any callbacks passed directly to `request` methods on a server client will
+have the highest precedence, followed by the `callbacks`.
+
+You can override the default handlers,
+- globally: by modifying the `vim.lsp.callbacks` table
+- per-client: by passing the {callbacks} table parameter to
+ |vim.lsp.start_client|
+
+Each handler has this signature: >
+
+ function(err, method, params, client_id)
+
+Callbacks are functions which are called in a variety of situations by the
+client. Their signature is `function(err, method, params, client_id)` They can
+be set by the {callbacks} parameter for |vim.lsp.start_client| or via the
+|vim.lsp.callbacks|.
+
+Handlers are called for:
+- Notifications from the server (`err` is always `nil`).
+- Requests initiated by the server (`err` is always `nil`).
+ The handler can respond by returning two values: `result, err`
+ where `err` must be shaped like an RPC error:
+ `{ code, message, data? }`
+ You can use |vim.lsp.rpc_response_error()| to create this object.
+- Handling requests initiated by the client if the request doesn't explicitly
+ specify a callback (such as in |vim.lsp.buf_request|).
+
+================================================================================
+VIM.LSP.PROTOCOL *vim.lsp.protocol*
+
+The `vim.lsp.protocol` module provides constants defined in the LSP
+specification, and helper functions for creating protocol-related objects.
+
+ https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md
+
+Useful examples are `vim.lsp.protocol.ErrorCodes`. These objects allow reverse
+lookup by either the number or string name.
+
+ e.g. vim.lsp.protocol.TextDocumentSyncKind.Full == 1
+ vim.lsp.protocol.TextDocumentSyncKind[1] == "Full"
+
+ Utility functions used internally are:
+ `vim.lsp.protocol.make_client_capabilities()`
+ Make a ClientCapabilities object. These are the builtin
+ capabilities.
+ `vim.lsp.protocol.resolve_capabilities(server_capabilites)`
+ Creates a normalized object describing capabilities from the server
+ capabilities.
+
+================================================================================
+ *vim.lsp.util*
+
+TODO: Describe the utils here for handling/applying things from LSP.
+
+================================================================================
+ *lsp-buf-methods*
+There are methods which operate on the buffer level for all of the active
+clients attached to the buffer.
+
+ *vim.lsp.buf_request()*
+vim.lsp.buf_request({bufnr}, {method}, {params}, [{callback}])
+ Send a async request for all the clients active and attached to the buffer.
+
+ Parameters: ~
+ {bufnr}: The buffer handle or 0 for the current buffer.
+
+ {method}: The LSP method name.
+
+ {params}: The parameters to send.
+
+ {callback}: An optional `function(err, method, params, client_id)` which
+ will be called for this request. If you do not specify it, then it will
+ use the client's callback in {client.callbacks}. See |lsp-callbacks| for
+ more information.
+
+ Returns:~
+
+ A table from client id to the request id for all of the successful
+ requests.
+
+ The second result is a function which can be used to cancel all the
+ requests. You can do this individually with `client.cancel_request()`
+
+ *vim.lsp.buf_request_sync()*
+vim.lsp.buf_request_sync({bufnr}, {method}, {params}, [{timeout_ms}])
+ Calls |vim.lsp.buf_request()|, but it will wait for the result and block Vim
+ in the process.
+ The parameters are the same as |vim.lsp.buf_request()|, but the return
+ result is different.
+ It will wait maximum of {timeout_ms} which defaults to 100ms.
+
+ Returns:~
+
+ If the timeout is exceeded or a cancel is sent or an error, it will cancel
+ the request and return `nil, err` where `err` is a string that describes
+ the reason why it failed.
+
+ If it is successful, it will return a table from client id to result id.
+
+ *vim.lsp.buf_notify()*
+vim.lsp.buf_notify({bufnr}, {method}, {params})
+ Send a notification to all servers on the buffer.
+
+ Parameters: ~
+ {bufnr}: The buffer handle or 0 for the current buffer.
+
+ {method}: The LSP method name.
+
+ {params}: The parameters to send.
+
+================================================================================
+ *lsp-logging*
+
+ *vim.lsp.set_log_level()*
+vim.lsp.set_log_level({level})
+ You can set the log level for language server client logging.
+ Possible values: "trace", "debug", "info", "warn", "error"
+
+ Default: "warn"
+
+ Example: `lua vim.lsp.set_log_level("debug")`
+
+ *vim.lsp.get_log_path()*
+vim.lsp.get_log_path()
+ Returns the path that LSP logs are written.
+
+ *vim.lsp.log_levels*
+vim.lsp.log_levels
+ Log level dictionary with reverse lookup as well.
+
+ Can be used to lookup the number from the name or vice-versa.
+ Levels: "trace" (0), "debug" (1), "info" (2), "warn" (3), "error" (4)
+
+================================================================================
+LSP EXAMPLE *lsp-advanced-js-example*
+
+For more advanced configurations where just filtering by filetype isn't
+sufficient, you can use the `vim.lsp.start_client()` and
+`vim.lsp.buf_attach_client()` commands to easily customize the configuration
+however you please. For example, if you want to do your own filtering, or
+start a new LSP client based on the root directory for if you plan to work
+with multiple projects in a single session. Below is a fully working Lua
+example which can do exactly that.
+
+The example will:
+1. Check for each new buffer whether or not we want to start an LSP client.
+2. Try to find a root directory by ascending from the buffer's path.
+3. Create a new LSP for that root directory if one doesn't exist.
+4. Attach the buffer to the client for that root directory.
+
+>
+ -- Some path manipulation utilities
+ local function is_dir(filename)
+ local stat = vim.loop.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+ end
+
+ local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/"
+ -- Asumes filepath is a file.
+ local function dirname(filepath)
+ local is_changed = false
+ local result = filepath:gsub(path_sep.."([^"..path_sep.."]+)$", function()
+ is_changed = true
+ return ""
+ end)
+ return result, is_changed
+ end
+
+ local function path_join(...)
+ return table.concat(vim.tbl_flatten {...}, path_sep)
+ end
+
+ -- Ascend the buffer's path until we find the rootdir.
+ -- is_root_path is a function which returns bool
+ local function buffer_find_root_dir(bufnr, is_root_path)
+ local bufname = vim.api.nvim_buf_get_name(bufnr)
+ if vim.fn.filereadable(bufname) == 0 then
+ return nil
+ end
+ local dir = bufname
+ -- Just in case our algo is buggy, don't infinite loop.
+ for _ = 1, 100 do
+ local did_change
+ dir, did_change = dirname(dir)
+ if is_root_path(dir, bufname) then
+ return dir, bufname
+ end
+ -- If we can't ascend further, then stop looking.
+ if not did_change then
+ return nil
+ end
+ end
+ end
+
+ -- A table to store our root_dir to client_id lookup. We want one LSP per
+ -- root directory, and this is how we assert that.
+ local javascript_lsps = {}
+ -- Which filetypes we want to consider.
+ local javascript_filetypes = {
+ ["javascript.jsx"] = true;
+ ["javascript"] = true;
+ ["typescript"] = true;
+ ["typescript.jsx"] = true;
+ }
+
+ -- Create a template configuration for a server to start, minus the root_dir
+ -- which we will specify later.
+ local javascript_lsp_config = {
+ name = "javascript";
+ cmd = { path_join(os.getenv("JAVASCRIPT_LANGUAGE_SERVER_DIRECTORY"), "lib", "language-server-stdio.js") };
+ }
+
+ -- This needs to be global so that we can call it from the autocmd.
+ function check_start_javascript_lsp()
+ local bufnr = vim.api.nvim_get_current_buf()
+ -- Filter which files we are considering.
+ if not javascript_filetypes[vim.api.nvim_buf_get_option(bufnr, 'filetype')] then
+ return
+ end
+ -- Try to find our root directory. We will define this as a directory which contains
+ -- node_modules. Another choice would be to check for `package.json`, or for `.git`.
+ local root_dir = buffer_find_root_dir(bufnr, function(dir)
+ return is_dir(path_join(dir, 'node_modules'))
+ -- return vim.fn.filereadable(path_join(dir, 'package.json')) == 1
+ -- return is_dir(path_join(dir, '.git'))
+ end)
+ -- We couldn't find a root directory, so ignore this file.
+ if not root_dir then return end
+
+ -- Check if we have a client alredy or start and store it.
+ local client_id = javascript_lsps[root_dir]
+ if not client_id then
+ local new_config = vim.tbl_extend("error", javascript_lsp_config, {
+ root_dir = root_dir;
+ })
+ client_id = vim.lsp.start_client(new_config)
+ javascript_lsps[root_dir] = client_id
+ end
+ -- Finally, attach to the buffer to track changes. This will do nothing if we
+ -- are already attached.
+ vim.lsp.buf_attach_client(bufnr, client_id)
+ end
+
+ vim.api.nvim_command [[autocmd BufReadPost * lua check_start_javascript_lsp()]]
+<
+
+vim:tw=78:ts=8:ft=help:norl:
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
new file mode 100644
index 0000000000..c0da06ffe3
--- /dev/null
+++ b/runtime/doc/lua.txt
@@ -0,0 +1,1001 @@
+*lua.txt* Nvim
+
+
+ NVIM REFERENCE MANUAL
+
+
+Lua engine *lua* *Lua*
+
+ Type |gO| to see the table of contents.
+
+==============================================================================
+Introduction *lua-intro*
+
+The Lua 5.1 language is builtin and always available. Try this command to get
+an idea of what lurks beneath: >
+
+ :lua print(vim.inspect(package.loaded))
+
+Nvim includes a "standard library" |lua-stdlib| for Lua. It complements the
+"editor stdlib" (|functions| and Ex commands) and the |API|, all of which can
+be used from Lua code.
+
+Module conflicts are resolved by "last wins". For example if both of these
+are on 'runtimepath':
+ runtime/lua/foo.lua
+ ~/.config/nvim/lua/foo.lua
+then `require('foo')` loads "~/.config/nvim/lua/foo.lua", and
+"runtime/lua/foo.lua" is not used. See |lua-require| to understand how Nvim
+finds and loads Lua modules. The conventions are similar to VimL plugins,
+with some extra features. See |lua-require-example| for a walkthrough.
+
+==============================================================================
+Importing Lua modules *lua-require*
+
+ *lua-package-path*
+Nvim automatically adjusts `package.path` and `package.cpath` according to
+effective 'runtimepath' value. Adjustment happens whenever 'runtimepath' is
+changed. `package.path` is adjusted by simply appending `/lua/?.lua` and
+`/lua/?/init.lua` to each directory from 'runtimepath' (`/` is actually the
+first character of `package.config`).
+
+Similarly to `package.path`, modified directories from 'runtimepath' are also
+added to `package.cpath`. In this case, instead of appending `/lua/?.lua` and
+`/lua/?/init.lua` to each runtimepath, all unique `?`-containing suffixes of
+the existing `package.cpath` are used. Example:
+
+1. Given that
+ - 'runtimepath' contains `/foo/bar,/xxx;yyy/baz,/abc`;
+ - initial (defined at compile-time or derived from
+ `$LUA_CPATH`/`$LUA_INIT`) `package.cpath` contains
+ `./?.so;/def/ghi/a?d/j/g.elf;/def/?.so`.
+2. It finds `?`-containing suffixes `/?.so`, `/a?d/j/g.elf` and `/?.so`, in
+ order: parts of the path starting from the first path component containing
+ question mark and preceding path separator.
+3. The suffix of `/def/?.so`, namely `/?.so` is not unique, as it’s the same
+ as the suffix of the first path from `package.path` (i.e. `./?.so`). Which
+ leaves `/?.so` and `/a?d/j/g.elf`, in this order.
+4. 'runtimepath' has three paths: `/foo/bar`, `/xxx;yyy/baz` and `/abc`. The
+ second one contains semicolon which is a paths separator so it is out,
+ leaving only `/foo/bar` and `/abc`, in order.
+5. The cartesian product of paths from 4. and suffixes from 3. is taken,
+ giving four variants. In each variant `/lua` path segment is inserted
+ between path and suffix, leaving
+
+ - `/foo/bar/lua/?.so`
+ - `/foo/bar/lua/a?d/j/g.elf`
+ - `/abc/lua/?.so`
+ - `/abc/lua/a?d/j/g.elf`
+
+6. New paths are prepended to the original `package.cpath`.
+
+The result will look like this:
+
+ `/foo/bar,/xxx;yyy/baz,/abc` ('runtimepath')
+ × `./?.so;/def/ghi/a?d/j/g.elf;/def/?.so` (`package.cpath`)
+
+ = `/foo/bar/lua/?.so;/foo/bar/lua/a?d/j/g.elf;/abc/lua/?.so;/abc/lua/a?d/j/g.elf;./?.so;/def/ghi/a?d/j/g.elf;/def/?.so`
+
+Note:
+
+- To track 'runtimepath' updates, paths added at previous update are
+ remembered and removed at the next update, while all paths derived from the
+ new 'runtimepath' are prepended as described above. This allows removing
+ paths when path is removed from 'runtimepath', adding paths when they are
+ added and reordering `package.path`/`package.cpath` content if 'runtimepath'
+ was reordered.
+
+- Although adjustments happen automatically, Nvim does not track current
+ values of `package.path` or `package.cpath`. If you happen to delete some
+ paths from there you can set 'runtimepath' to trigger an update: >
+ let &runtimepath = &runtimepath
+
+- Skipping paths from 'runtimepath' which contain semicolons applies both to
+ `package.path` and `package.cpath`. Given that there are some badly written
+ plugins using shell which will not work with paths containing semicolons it
+ is better to not have them in 'runtimepath' at all.
+
+------------------------------------------------------------------------------
+LUA PLUGIN EXAMPLE *lua-require-example*
+
+The following example plugin adds a command `:MakeCharBlob` which transforms
+current buffer into a long `unsigned char` array. Lua contains transformation
+function in a module `lua/charblob.lua` which is imported in
+`autoload/charblob.vim` (`require("charblob")`). Example plugin is supposed
+to be put into any directory from 'runtimepath', e.g. `~/.config/nvim` (in
+this case `lua/charblob.lua` means `~/.config/nvim/lua/charblob.lua`).
+
+autoload/charblob.vim: >
+
+ function charblob#encode_buffer()
+ call setline(1, luaeval(
+ \ 'require("charblob").encode(unpack(_A))',
+ \ [getline(1, '$'), &textwidth, ' ']))
+ endfunction
+
+plugin/charblob.vim: >
+
+ if exists('g:charblob_loaded')
+ finish
+ endif
+ let g:charblob_loaded = 1
+
+ command MakeCharBlob :call charblob#encode_buffer()
+
+lua/charblob.lua: >
+
+ local function charblob_bytes_iter(lines)
+ local init_s = {
+ next_line_idx = 1,
+ next_byte_idx = 1,
+ lines = lines,
+ }
+ local function next(s, _)
+ if lines[s.next_line_idx] == nil then
+ return nil
+ end
+ if s.next_byte_idx > #(lines[s.next_line_idx]) then
+ s.next_line_idx = s.next_line_idx + 1
+ s.next_byte_idx = 1
+ return ('\n'):byte()
+ end
+ local ret = lines[s.next_line_idx]:byte(s.next_byte_idx)
+ if ret == ('\n'):byte() then
+ ret = 0 -- See :h NL-used-for-NUL.
+ end
+ s.next_byte_idx = s.next_byte_idx + 1
+ return ret
+ end
+ return next, init_s, nil
+ end
+
+ local function charblob_encode(lines, textwidth, indent)
+ local ret = {
+ 'const unsigned char blob[] = {',
+ indent,
+ }
+ for byte in charblob_bytes_iter(lines) do
+ -- .- space + number (width 3) + comma
+ if #(ret[#ret]) + 5 > textwidth then
+ ret[#ret + 1] = indent
+ else
+ ret[#ret] = ret[#ret] .. ' '
+ end
+ ret[#ret] = ret[#ret] .. (('%3u,'):format(byte))
+ end
+ ret[#ret + 1] = '};'
+ return ret
+ end
+
+ return {
+ bytes_iter = charblob_bytes_iter,
+ encode = charblob_encode,
+ }
+
+==============================================================================
+Commands *lua-commands*
+
+These commands execute a Lua chunk from either the command line (:lua, :luado)
+or a file (:luafile) on the given line [range]. As always in Lua, each chunk
+has its own scope (closure), so only global variables are shared between
+command calls. The |lua-stdlib| modules, user modules, and anything else on
+|lua-package-path| are available.
+
+The Lua print() function redirects its output to the Nvim message area, with
+arguments separated by " " (space) instead of "\t" (tab).
+
+ *:lua*
+:[range]lua {chunk}
+ Executes Lua chunk {chunk}.
+
+ Examples: >
+ :lua vim.api.nvim_command('echo "Hello, Nvim!"')
+< To see the Lua version: >
+ :lua print(_VERSION)
+< To see the LuaJIT version: >
+ :lua print(jit.version)
+<
+ *:lua-heredoc*
+:[range]lua << [endmarker]
+{script}
+{endmarker}
+ Executes Lua script {script} from within Vimscript.
+ {endmarker} must NOT be preceded by whitespace. You
+ can omit [endmarker] after the "<<" and use a dot "."
+ after {script} (similar to |:append|, |:insert|).
+
+ Example:
+ >
+ function! CurrentLineInfo()
+ lua << EOF
+ local linenr = vim.api.nvim_win_get_cursor(0)[1]
+ local curline = vim.api.nvim_buf_get_lines(
+ 0, linenr, linenr + 1, false)[1]
+ print(string.format("Current line [%d] has %d bytes",
+ linenr, #curline))
+ EOF
+ endfunction
+
+< Note that the `local` variables will disappear when
+ the block finishes. But not globals.
+
+ *:luado*
+:[range]luado {body} Executes Lua chunk "function(line, linenr) {body} end"
+ for each buffer line in [range], where `line` is the
+ current line text (without <EOL>), and `linenr` is the
+ current line number. If the function returns a string
+ that becomes the text of the corresponding buffer
+ line. Default [range] is the whole file: "1,$".
+
+ Examples:
+ >
+ :luado return string.format("%s\t%d", line:reverse(), #line)
+
+ :lua require"lpeg"
+ :lua -- balanced parenthesis grammar:
+ :lua bp = lpeg.P{ "(" * ((1 - lpeg.S"()") + lpeg.V(1))^0 * ")" }
+ :luado if bp:match(line) then return "-->\t" .. line end
+<
+
+ *:luafile*
+:[range]luafile {file}
+ Execute Lua script in {file}.
+ The whole argument is used as a single file name.
+
+ Examples:
+ >
+ :luafile script.lua
+ :luafile %
+<
+
+==============================================================================
+luaeval() *lua-eval* *luaeval()*
+
+The (dual) equivalent of "vim.eval" for passing Lua values to Nvim is
+"luaeval". "luaeval" takes an expression string and an optional argument used
+for _A inside expression and returns the result of the expression. It is
+semantically equivalent in Lua to:
+>
+ local chunkheader = "local _A = select(1, ...) return "
+ function luaeval (expstr, arg)
+ local chunk = assert(loadstring(chunkheader .. expstr, "luaeval"))
+ return chunk(arg) -- return typval
+ end
+
+Lua nils, numbers, strings, tables and booleans are converted to their
+respective VimL types. An error is thrown if conversion of any other Lua types
+is attempted.
+
+The magic global "_A" contains the second argument to luaeval().
+
+Example: >
+ :echo luaeval('_A[1] + _A[2]', [40, 2])
+ 42
+ :echo luaeval('string.match(_A, "[a-z]+")', 'XYXfoo123')
+ foo
+
+Lua tables are used as both dictionaries and lists, so it is impossible to
+determine whether empty table is meant to be empty list or empty dictionary.
+Additionally Lua does not have integer numbers. To distinguish between these
+cases there is the following agreement:
+
+0. Empty table is empty list.
+1. Table with N incrementally growing integral numbers, starting from 1 and
+ ending with N is considered to be a list.
+2. Table with string keys, none of which contains NUL byte, is considered to
+ be a dictionary.
+3. Table with string keys, at least one of which contains NUL byte, is also
+ considered to be a dictionary, but this time it is converted to
+ a |msgpack-special-map|.
+ *lua-special-tbl*
+4. Table with `vim.type_idx` key may be a dictionary, a list or floating-point
+ value:
+ - `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to
+ a floating-point 1.0. Note that by default integral Lua numbers are
+ converted to |Number|s, non-integral are converted to |Float|s. This
+ variant allows integral |Float|s.
+ - `{[vim.type_idx]=vim.types.dictionary}` is converted to an empty
+ dictionary, `{[vim.type_idx]=vim.types.dictionary, [42]=1, a=2}` is
+ converted to a dictionary `{'a': 42}`: non-string keys are ignored.
+ Without `vim.type_idx` key tables with keys not fitting in 1., 2. or 3.
+ are errors.
+ - `{[vim.type_idx]=vim.types.list}` is converted to an empty list. As well
+ as `{[vim.type_idx]=vim.types.list, [42]=1}`: integral keys that do not
+ form a 1-step sequence from 1 to N are ignored, as well as all
+ non-integral keys.
+
+Examples: >
+
+ :echo luaeval('math.pi')
+ :function Rand(x,y) " random uniform between x and y
+ : return luaeval('(_A.y-_A.x)*math.random()+_A.x', {'x':a:x,'y':a:y})
+ : endfunction
+ :echo Rand(1,10)
+
+Note: second argument to `luaeval` undergoes VimL to Lua conversion
+("marshalled"), so changes to Lua containers do not affect values in VimL.
+Return value is also always converted. When converting,
+|msgpack-special-dict|s are treated specially.
+
+==============================================================================
+Vimscript v:lua interface *v:lua-call*
+
+From Vimscript the special `v:lua` prefix can be used to call Lua functions
+which are global or accessible from global tables. The expression >
+ v:lua.func(arg1, arg2)
+is equivalent to the Lua chunk >
+ return func(...)
+where the args are converted to Lua values. The expression >
+ v:lua.somemod.func(args)
+is equivalent to the Lua chunk >
+ return somemod.func(...)
+
+You can use `v:lua` in "func" options like 'tagfunc', 'omnifunc', etc.
+For example consider the following Lua omnifunc handler: >
+
+ function mymod.omnifunc(findstart, base)
+ if findstart == 1 then
+ return 0
+ else
+ return {'stuff', 'steam', 'strange things'}
+ end
+ end
+ vim.api.nvim_buf_set_option(0, 'omnifunc', 'v:lua.mymod.omnifunc')
+
+Note: the module ("mymod" in the above example) must be a Lua global.
+
+Note: `v:lua` without a call is not allowed in a Vimscript expression:
+|Funcref|s cannot represent Lua functions. The following are errors: >
+
+ let g:Myvar = v:lua.myfunc " Error
+ call SomeFunc(v:lua.mycallback) " Error
+ let g:foo = v:lua " Error
+ let g:foo = v:['lua'] " Error
+
+
+==============================================================================
+Lua standard modules *lua-stdlib*
+
+The Nvim Lua "standard library" (stdlib) is the `vim` module, which exposes
+various functions and sub-modules. It is always loaded, thus require("vim")
+is unnecessary.
+
+You can peek at the module properties: >
+
+ :lua print(vim.inspect(vim))
+
+Result is something like this: >
+
+ {
+ _os_proc_children = <function 1>,
+ _os_proc_info = <function 2>,
+ ...
+ api = {
+ nvim__id = <function 5>,
+ nvim__id_array = <function 6>,
+ ...
+ },
+ deepcopy = <function 106>,
+ gsplit = <function 107>,
+ ...
+ }
+
+To find documentation on e.g. the "deepcopy" function: >
+
+ :help vim.deepcopy()
+
+Note that underscore-prefixed functions (e.g. "_os_proc_children") are
+internal/private and must not be used by plugins.
+
+------------------------------------------------------------------------------
+VIM.LOOP *lua-loop* *vim.loop*
+
+`vim.loop` exposes all features of the Nvim event-loop. This is a low-level
+API that provides functionality for networking, filesystem, and process
+management. Try this command to see available functions: >
+
+ :lua print(vim.inspect(vim.loop))
+
+Reference: http://docs.libuv.org
+Examples: https://github.com/luvit/luv/tree/master/examples
+
+ *E5560* *lua-loop-callbacks*
+It is an error to directly invoke `vim.api` functions (except |api-fast|) in
+`vim.loop` callbacks. For example, this is an error: >
+
+ local timer = vim.loop.new_timer()
+ timer:start(1000, 0, function()
+ vim.api.nvim_command('echomsg "test"')
+ end)
+
+To avoid the error use |vim.schedule_wrap()| to defer the callback: >
+
+ local timer = vim.loop.new_timer()
+ timer:start(1000, 0, vim.schedule_wrap(function()
+ vim.api.nvim_command('echomsg "test"')
+ end))
+
+Example: repeating timer
+ 1. Save this code to a file.
+ 2. Execute it with ":luafile %". >
+
+ -- Create a timer handle (implementation detail: uv_timer_t).
+ local timer = vim.loop.new_timer()
+ local i = 0
+ -- Waits 1000ms, then repeats every 750ms until timer:close().
+ timer:start(1000, 750, function()
+ print('timer invoked! i='..tostring(i))
+ if i > 4 then
+ timer:close() -- Always close handles to avoid leaks.
+ end
+ i = i + 1
+ end)
+ print('sleeping');
+
+
+Example: File-change detection *watch-file*
+ 1. Save this code to a file.
+ 2. Execute it with ":luafile %".
+ 3. Use ":Watch %" to watch any file.
+ 4. Try editing the file from another text editor.
+ 5. Observe that the file reloads in Nvim (because on_change() calls
+ |:checktime|). >
+
+ local w = vim.loop.new_fs_event()
+ local function on_change(err, fname, status)
+ -- Do work...
+ vim.api.nvim_command('checktime')
+ -- Debounce: stop/start.
+ w:stop()
+ watch_file(fname)
+ end
+ function watch_file(fname)
+ local fullpath = vim.api.nvim_call_function(
+ 'fnamemodify', {fname, ':p'})
+ w:start(fullpath, {}, vim.schedule_wrap(function(...)
+ on_change(...) end))
+ end
+ vim.api.nvim_command(
+ "command! -nargs=1 Watch call luaeval('watch_file(_A)', expand('<args>'))")
+
+
+Example: TCP echo-server *tcp-server*
+ 1. Save this code to a file.
+ 2. Execute it with ":luafile %".
+ 3. Note the port number.
+ 4. Connect from any TCP client (e.g. "nc 0.0.0.0 36795"): >
+
+ local function create_server(host, port, on_connect)
+ local server = vim.loop.new_tcp()
+ server:bind(host, port)
+ server:listen(128, function(err)
+ assert(not err, err) -- Check for errors.
+ local sock = vim.loop.new_tcp()
+ server:accept(sock) -- Accept client connection.
+ on_connect(sock) -- Start reading messages.
+ end)
+ return server
+ end
+ local server = create_server('0.0.0.0', 0, function(sock)
+ sock:read_start(function(err, chunk)
+ assert(not err, err) -- Check for errors.
+ if chunk then
+ sock:write(chunk) -- Echo received messages to the channel.
+ else -- EOF (stream closed).
+ sock:close() -- Always close handles to avoid leaks.
+ end
+ end)
+ end)
+ print('TCP echo-server listening on port: '..server:getsockname().port)
+
+------------------------------------------------------------------------------
+VIM.TREESITTER *lua-treesitter*
+
+Nvim integrates the tree-sitter library for incremental parsing of buffers.
+
+Currently Nvim does not provide the tree-sitter parsers, instead these must
+be built separately, for instance using the tree-sitter utility.
+The parser is loaded into nvim using >
+
+ vim.treesitter.add_language("/path/to/c_parser.so", "c")
+
+<Create a parser for a buffer and a given language (if another plugin uses the
+same buffer/language combination, it will be safely reused). Use >
+
+ parser = vim.treesitter.get_parser(bufnr, lang)
+
+<`bufnr=0` can be used for current buffer. `lang` will default to 'filetype' (this
+doesn't work yet for some filetypes like "cpp") Currently, the parser will be
+retained for the lifetime of a buffer but this is subject to change. A plugin
+should keep a reference to the parser object as long as it wants incremental
+updates.
+
+Whenever you need to access the current syntax tree, parse the buffer: >
+
+ tstree = parser:parse()
+
+<This will return an immutable tree that represents the current state of the
+buffer. When the plugin wants to access the state after a (possible) edit
+it should call `parse()` again. If the buffer wasn't edited, the same tree will
+be returned again without extra work. If the buffer was parsed before,
+incremental parsing will be done of the changed parts.
+
+NB: to use the parser directly inside a |nvim_buf_attach| Lua callback, you must
+call `get_parser()` before you register your callback. But preferably parsing
+shouldn't be done directly in the change callback anyway as they will be very
+frequent. Rather a plugin that does any kind of analysis on a tree should use
+a timer to throttle too frequent updates.
+
+Tree methods *lua-treesitter-tree*
+
+tstree:root() *tstree:root()*
+ Return the root node of this tree.
+
+
+Node methods *lua-treesitter-node*
+
+tsnode:parent() *tsnode:parent()*
+ Get the node's immediate parent.
+
+tsnode:child_count() *tsnode:child_count()*
+ Get the node's number of children.
+
+tsnode:child(N) *tsnode:child()*
+ Get the node's child at the given index, where zero represents the
+ first child.
+
+tsnode:named_child_count() *tsnode:named_child_count()*
+ Get the node's number of named children.
+
+tsnode:named_child(N) *tsnode:named_child()*
+ Get the node's named child at the given index, where zero represents
+ the first named child.
+
+tsnode:start() *tsnode:start()*
+ Get the node's start position. Return three values: the row, column
+ and total byte count (all zero-based).
+
+tsnode:end_() *tsnode:end_()*
+ Get the node's end position. Return three values: the row, column
+ and total byte count (all zero-based).
+
+tsnode:range() *tsnode:range()*
+ Get the range of the node. Return four values: the row, column
+ of the start position, then the row, column of the end position.
+
+tsnode:type() *tsnode:type()*
+ Get the node's type as a string.
+
+tsnode:symbol() *tsnode:symbol()*
+ Get the node's type as a numerical id.
+
+tsnode:named() *tsnode:named()*
+ Check if the node is named. Named nodes correspond to named rules in
+ the grammar, whereas anonymous nodes correspond to string literals
+ in the grammar.
+
+tsnode:missing() *tsnode:missing()*
+ Check if the node is missing. Missing nodes are inserted by the
+ parser in order to recover from certain kinds of syntax errors.
+
+tsnode:has_error() *tsnode:has_error()*
+ Check if the node is a syntax error or contains any syntax errors.
+
+tsnode:sexpr() *tsnode:sexpr()*
+ Get an S-expression representing the node as a string.
+
+tsnode:descendant_for_range(start_row, start_col, end_row, end_col)
+ *tsnode:descendant_for_range()*
+ Get the smallest node within this node that spans the given range of
+ (row, column) positions
+
+tsnode:named_descendant_for_range(start_row, start_col, end_row, end_col)
+ *tsnode:named_descendant_for_range()*
+ Get the smallest named node within this node that spans the given
+ range of (row, column) positions
+
+------------------------------------------------------------------------------
+VIM *lua-builtin*
+
+vim.api.{func}({...}) *vim.api*
+ Invokes Nvim |API| function {func} with arguments {...}.
+ Example: call the "nvim_get_current_line()" API function: >
+ print(tostring(vim.api.nvim_get_current_line()))
+
+vim.call({func}, {...}) *vim.call()*
+ Invokes |vim-function| or |user-function| {func} with arguments {...}.
+ See also |vim.fn|. Equivalent to: >
+ vim.fn[func]({...})
+
+vim.in_fast_event() *vim.in_fast_event()*
+ Returns true if the code is executing as part of a "fast" event
+ handler, where most of the API is disabled. These are low-level events
+ (e.g. |lua-loop-callbacks|) which can be invoked whenever Nvim polls
+ for input. When this is `false` most API functions are callable (but
+ may be subject to other restrictions such as |textlock|).
+
+vim.NIL *vim.NIL*
+ Special value used to represent NIL in msgpack-rpc and |v:null| in
+ vimL interaction, and similar cases. Lua `nil` cannot be used as
+ part of a lua table representing a Dictionary or Array, as it
+ is equivalent to a missing value: `{"foo", nil}` is the same as
+ `{"foo"}`
+
+vim.rpcnotify({channel}, {method}[, {args}...]) *vim.rpcnotify()*
+ Sends {event} to {channel} via |RPC| and returns immediately.
+ If {channel} is 0, the event is broadcast to all channels.
+
+ This function also works in a fast callback |lua-loop-callbacks|.
+
+vim.rpcrequest({channel}, {method}[, {args}...]) *vim.rpcrequest()*
+ Sends a request to {channel} to invoke {method} via
+ |RPC| and blocks until a response is received.
+
+ Note: NIL values as part of the return value is represented as
+ |vim.NIL| special value
+
+vim.stricmp({a}, {b}) *vim.stricmp()*
+ Compares strings case-insensitively. Returns 0, 1 or -1 if strings
+ are equal, {a} is greater than {b} or {a} is lesser than {b},
+ respectively.
+
+vim.str_utfindex({str}[, {index}]) *vim.str_utfindex()*
+ Convert byte index to UTF-32 and UTF-16 indicies. If {index} is not
+ supplied, the length of the string is used. All indicies are zero-based.
+ Returns two values: the UTF-32 and UTF-16 indicies respectively.
+
+ Embedded NUL bytes are treated as terminating the string. Invalid
+ UTF-8 bytes, and embedded surrogates are counted as one code
+ point each. An {index} in the middle of a UTF-8 sequence is rounded
+ upwards to the end of that sequence.
+
+vim.str_byteindex({str}, {index}[, {use_utf16}]) *vim.str_byteindex()*
+ Convert UTF-32 or UTF-16 {index} to byte index. If {use_utf16} is not
+ supplied, it defaults to false (use UTF-32). Returns the byte index.
+
+ Invalid UTF-8 and NUL is treated like by |vim.str_byteindex()|. An {index}
+ in the middle of a UTF-16 sequence is rounded upwards to the end of that
+ sequence.
+
+vim.schedule({callback}) *vim.schedule()*
+ Schedules {callback} to be invoked soon by the main event-loop. Useful
+ to avoid |textlock| or other temporary restrictions.
+
+vim.fn.{func}({...}) *vim.fn*
+ Invokes |vim-function| or |user-function| {func} with arguments {...}.
+ To call autoload functions, use the syntax: >
+ vim.fn['some#function']({...})
+<
+ Unlike vim.api.|nvim_call_function| this converts directly between Vim
+ objects and Lua objects. If the Vim function returns a float, it will
+ be represented directly as a Lua number. Empty lists and dictionaries
+ both are represented by an empty table.
+
+ Note: |v:null| values as part of the return value is represented as
+ |vim.NIL| special value
+
+ Note: vim.fn keys are generated lazily, thus `pairs(vim.fn)` only
+ enumerates functions that were called at least once.
+
+vim.type_idx *vim.type_idx*
+ Type index for use in |lua-special-tbl|. Specifying one of the
+ values from |vim.types| allows typing the empty table (it is
+ unclear whether empty Lua table represents empty list or empty array)
+ and forcing integral numbers to be |Float|. See |lua-special-tbl| for
+ more details.
+
+vim.val_idx *vim.val_idx*
+ Value index for tables representing |Float|s. A table representing
+ floating-point value 1.0 looks like this: >
+ {
+ [vim.type_idx] = vim.types.float,
+ [vim.val_idx] = 1.0,
+ }
+< See also |vim.type_idx| and |lua-special-tbl|.
+
+vim.types *vim.types*
+ Table with possible values for |vim.type_idx|. Contains two sets
+ of key-value pairs: first maps possible values for |vim.type_idx|
+ to human-readable strings, second maps human-readable type names to
+ values for |vim.type_idx|. Currently contains pairs for `float`,
+ `array` and `dictionary` types.
+
+ Note: one must expect that values corresponding to `vim.types.float`,
+ `vim.types.array` and `vim.types.dictionary` fall under only two
+ following assumptions:
+ 1. Value may serve both as a key and as a value in a table. Given the
+ properties of Lua tables this basically means “value is not `nil`â€.
+ 2. For each value in `vim.types` table `vim.types[vim.types[value]]`
+ is the same as `value`.
+ No other restrictions are put on types, and it is not guaranteed that
+ values corresponding to `vim.types.float`, `vim.types.array` and
+ `vim.types.dictionary` will not change or that `vim.types` table will
+ only contain values for these three types.
+
+==============================================================================
+Lua module: vim *lua-vim*
+
+inspect({object}, {options}) *vim.inspect()*
+ Return a human-readable representation of the given object.
+
+ See also: ~
+ https://github.com/kikito/inspect.lua
+ https://github.com/mpeterv/vinspect
+
+paste({lines}, {phase}) *vim.paste()*
+ Paste handler, invoked by |nvim_paste()| when a conforming UI
+ (such as the |TUI|) pastes text into the editor.
+
+ Example: To remove ANSI color codes when pasting: >
+
+ vim.paste = (function(overridden)
+ return function(lines, phase)
+ for i,line in ipairs(lines) do
+ -- Scrub ANSI color codes from paste input.
+ lines[i] = line:gsub('\27%[[0-9;mK]+', '')
+ end
+ overridden(lines, phase)
+ end
+ end)(vim.paste)
+<
+
+ Parameters: ~
+ {lines} |readfile()|-style list of lines to paste.
+ |channel-lines|
+ {phase} -1: "non-streaming" paste: the call contains all
+ lines. If paste is "streamed", `phase` indicates the stream state:
+ • 1: starts the paste (exactly once)
+ • 2: continues the paste (zero or more times)
+ • 3: ends the paste (exactly once)
+
+ Return: ~
+ false if client should cancel the paste.
+
+ See also: ~
+ |paste|
+
+schedule_wrap({cb}) *vim.schedule_wrap()*
+ Defers callback `cb` until the Nvim API is safe to call.
+
+ See also: ~
+ |lua-loop-callbacks|
+ |vim.schedule()|
+ |vim.in_fast_event()|
+
+
+
+
+deepcopy({orig}) *vim.deepcopy()*
+ Returns a deep copy of the given object. Non-table objects are
+ copied as in a typical Lua assignment, whereas table objects
+ are copied recursively.
+
+ Parameters: ~
+ {orig} Table to copy
+
+ Return: ~
+ New table of copied keys and (nested) values.
+
+gsplit({s}, {sep}, {plain}) *vim.gsplit()*
+ Splits a string at each instance of a separator.
+
+ Parameters: ~
+ {s} String to split
+ {sep} Separator string or pattern
+ {plain} If `true` use `sep` literally (passed to
+ String.find)
+
+ Return: ~
+ Iterator over the split components
+
+ See also: ~
+ |vim.split()|
+ https://www.lua.org/pil/20.2.html
+ http://lua-users.org/wiki/StringLibraryTutorial
+
+split({s}, {sep}, {plain}) *vim.split()*
+ Splits a string at each instance of a separator.
+
+ Examples: >
+ split(":aa::b:", ":") --> {'','aa','','bb',''}
+ split("axaby", "ab?") --> {'','x','y'}
+ split(x*yz*o, "*", true) --> {'x','yz','o'}
+<
+
+ Parameters: ~
+ {s} String to split
+ {sep} Separator string or pattern
+ {plain} If `true` use `sep` literally (passed to
+ String.find)
+
+ Return: ~
+ List-like table of the split components.
+
+ See also: ~
+ |vim.gsplit()|
+
+tbl_keys({t}) *vim.tbl_keys()*
+ Return a list of all keys used in a table. However, the order
+ of the return table of keys is not guaranteed.
+
+ Parameters: ~
+ {t} Table
+
+ Return: ~
+ list of keys
+
+ See also: ~
+ Fromhttps://github.com/premake/premake-core/blob/master/src/base/table.lua
+
+tbl_values({t}) *vim.tbl_values()*
+ Return a list of all values used in a table. However, the
+ order of the return table of values is not guaranteed.
+
+ Parameters: ~
+ {t} Table
+
+ Return: ~
+ list of values
+
+tbl_contains({t}, {value}) *vim.tbl_contains()*
+ Checks if a list-like (vector) table contains `value` .
+
+ Parameters: ~
+ {t} Table to check
+ {value} Value to compare
+
+ Return: ~
+ true if `t` contains `value`
+
+tbl_isempty({t}) *vim.tbl_isempty()*
+ See also: ~
+ Fromhttps://github.com/premake/premake-core/blob/master/src/base/table.lua@paramt Table to check
+
+tbl_extend({behavior}, {...}) *vim.tbl_extend()*
+ Merges two or more map-like tables.
+
+ Parameters: ~
+ {behavior} Decides what to do if a key is found in more
+ than one map:
+ • "error": raise an error
+ • "keep": use value from the leftmost map
+ • "force": use value from the rightmost map
+ {...} Two or more map-like tables.
+
+ See also: ~
+ |extend()|
+
+deep_equal({a}, {b}) *vim.deep_equal()*
+ TODO: Documentation
+
+tbl_add_reverse_lookup({o}) *vim.tbl_add_reverse_lookup()*
+ Add the reverse lookup values to an existing table. For
+ example: `tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A =
+ 1 }`
+
+ Parameters: ~
+ {o} table The table to add the reverse to.
+
+list_extend({dst}, {src}, {start}, {finish}) *vim.list_extend()*
+ Extends a list-like table with the values of another list-like
+ table.
+
+ NOTE: This mutates dst!
+
+ Parameters: ~
+ {dst} list which will be modified and appended to.
+ {src} list from which values will be inserted.
+ {start} Start index on src. defaults to 1
+ {finish} Final index on src. defaults to #src
+
+ Return: ~
+ dst
+
+ See also: ~
+ |vim.tbl_extend()|
+
+tbl_flatten({t}) *vim.tbl_flatten()*
+ Creates a copy of a list-like table such that any nested
+ tables are "unrolled" and appended to the result.
+
+ Parameters: ~
+ {t} List-like table
+
+ Return: ~
+ Flattened copy of the given list-like table.
+
+ See also: ~
+ Fromhttps://github.com/premake/premake-core/blob/master/src/base/table.lua
+
+tbl_islist({t}) *vim.tbl_islist()*
+ Table
+
+ Return: ~
+ true: A non-empty array, false: A non-empty table, nil: An
+ empty table
+
+trim({s}) *vim.trim()*
+ Trim whitespace (Lua pattern "%s") from both sides of a
+ string.
+
+ Parameters: ~
+ {s} String to trim
+
+ Return: ~
+ String with whitespace removed from its beginning and end
+
+ See also: ~
+ https://www.lua.org/pil/20.2.html
+
+pesc({s}) *vim.pesc()*
+ Escapes magic chars in a Lua pattern string.
+
+ Parameters: ~
+ {s} String to escape
+
+ Return: ~
+ %-escaped pattern string
+
+ See also: ~
+ https://github.com/rxi/lume
+
+validate({opt}) *vim.validate()*
+ Validates a parameter specification (types and values).
+
+ Usage example: >
+
+ function user.new(name, age, hobbies)
+ vim.validate{
+ name={name, 'string'},
+ age={age, 'number'},
+ hobbies={hobbies, 'table'},
+ }
+ ...
+ end
+<
+
+ Examples with explicit argument values (can be run directly): >
+
+ vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
+ => NOP (success)
+<
+>
+ vim.validate{arg1={1, 'table'}}
+ => error('arg1: expected table, got number')
+<
+>
+ vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
+ => error('arg1: expected even number, got 3')
+<
+
+ Parameters: ~
+ {opt} Map of parameter names to validations. Each key is
+ a parameter name; each value is a tuple in one of
+ these forms:
+ 1. (arg_value, type_name, optional)
+ • arg_value: argument value
+ • type_name: string type name, one of: ("table",
+ "t", "string", "s", "number", "n", "boolean",
+ "b", "function", "f", "nil", "thread",
+ "userdata")
+ • optional: (optional) boolean, if true, `nil`
+ is valid
+
+ 2. (arg_value, fn, msg)
+ • arg_value: argument value
+ • fn: any function accepting one argument,
+ returns true if and only if the argument is
+ valid
+ • msg: (optional) error string if validation
+ fails
+
+is_callable({f}) *vim.is_callable()*
+ Returns true if object `f` can be called as a function.
+
+ Parameters: ~
+ {f} Any object
+
+ Return: ~
+ true if `f` is callable, else false
+
+ vim:tw=78:ts=8:ft=help:norl:
diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt
index abe86749c4..58c0d832e6 100644
--- a/runtime/doc/map.txt
+++ b/runtime/doc/map.txt
@@ -786,7 +786,7 @@ g@{motion} Call the function set by the 'operatorfunc' option.
character of the text.
The function is called with one String argument:
"line" {motion} was |linewise|
- "char" {motion} was |characterwise|
+ "char" {motion} was |charwise|
"block" {motion} was |blockwise-visual|
Although "block" would rarely appear, since it can
only result from Visual mode where "g@" is not useful.
diff --git a/runtime/doc/message.txt b/runtime/doc/message.txt
index e8c76adad4..bcfd985e71 100644
--- a/runtime/doc/message.txt
+++ b/runtime/doc/message.txt
@@ -556,7 +556,8 @@ allowed for the command that was used.
Vim was not able to create a swap file. You can still edit the file, but if
Vim unexpectedly exits the changes will be lost. And Vim may consume a lot of
memory when editing a big file. You may want to change the 'directory' option
-to avoid this error. See |swap-file|.
+to avoid this error. This error is not given when 'directory' is empty. See
+|swap-file|.
*E140* >
Use ! to write partial buffer
@@ -678,8 +679,8 @@ no argument has been specified.
Invalid argument: {arg}
Duplicate argument: {arg}
-An Ex command or function has been executed, but an invalid argument has been
-specified.
+Ex command or function has been executed, but an invalid argument was
+specified. Or a non-executable command was given to |system()|.
*E488* >
Trailing characters
diff --git a/runtime/doc/motion.txt b/runtime/doc/motion.txt
index 97c7d1cc43..3947e583b7 100644
--- a/runtime/doc/motion.txt
+++ b/runtime/doc/motion.txt
@@ -60,11 +60,11 @@ After applying the operator the cursor is mostly left at the start of the text
that was operated upon. For example, "yfe" doesn't move the cursor, but "yFe"
moves the cursor leftwards to the "e" where the yank started.
- *linewise* *characterwise*
+ *linewise* *charwise* *characterwise*
The operator either affects whole lines, or the characters between the start
and end position. Generally, motions that move between lines affect lines
(are linewise), and motions that move within a line affect characters (are
-characterwise). However, there are some exceptions.
+charwise). However, there are some exceptions.
*exclusive* *inclusive*
Character motion is either inclusive or exclusive. When inclusive, the
@@ -106,10 +106,10 @@ This cannot be repeated: >
d:if 1<CR>
call search("f")<CR>
endif<CR>
-Note that when using ":" any motion becomes characterwise exclusive.
+Note that when using ":" any motion becomes charwise exclusive.
*forced-motion*
-FORCING A MOTION TO BE LINEWISE, CHARACTERWISE OR BLOCKWISE
+FORCING A MOTION TO BE LINEWISE, CHARWISE OR BLOCKWISE
When a motion is not of the type you would like to use, you can force another
type by using "v", "V" or CTRL-V just after the operator.
@@ -121,22 +121,22 @@ deletes from the cursor position until the character below the cursor >
d<C-V>j
deletes the character under the cursor and the character below the cursor. >
-Be careful with forcing a linewise movement to be used characterwise or
-blockwise, the column may not always be defined.
+Be careful with forcing a linewise movement to be used charwise or blockwise,
+the column may not always be defined.
*o_v*
v When used after an operator, before the motion command: Force
- the operator to work characterwise, also when the motion is
+ the operator to work charwise, also when the motion is
linewise. If the motion was linewise, it will become
|exclusive|.
- If the motion already was characterwise, toggle
+ If the motion already was charwise, toggle
inclusive/exclusive. This can be used to make an exclusive
motion inclusive and an inclusive motion exclusive.
*o_V*
V When used after an operator, before the motion command: Force
the operator to work linewise, also when the motion is
- characterwise.
+ charwise.
*o_CTRL-V*
CTRL-V When used after an operator, before the motion command: Force
@@ -219,6 +219,12 @@ g^ When lines wrap ('wrap' on): To the first non-blank
gm Like "g0", but half a screenwidth to the right (or as
much as possible).
+ *gM*
+gM Like "g0", but to halfway the text of the line.
+ With a count: to this percentage of text in the line.
+ Thus "10gM" is near the start of the text and "90gM"
+ is near the end of the text.
+
*g$* *g<End>*
g$ or g<End> When lines wrap ('wrap' on): To the last character of
the screen line and [count - 1] screen lines downward
@@ -412,35 +418,35 @@ between Vi and Vim.
5. Text object motions *object-motions*
*(*
-( [count] sentences backward. |exclusive| motion.
+( [count] |sentence|s backward. |exclusive| motion.
*)*
-) [count] sentences forward. |exclusive| motion.
+) [count] |sentence|s forward. |exclusive| motion.
*{*
-{ [count] paragraphs backward. |exclusive| motion.
+{ [count] |paragraph|s backward. |exclusive| motion.
*}*
-} [count] paragraphs forward. |exclusive| motion.
+} [count] |paragraph|s forward. |exclusive| motion.
*]]*
-]] [count] sections forward or to the next '{' in the
+]] [count] |section|s forward or to the next '{' in the
first column. When used after an operator, then also
stops below a '}' in the first column. |exclusive|
Note that |exclusive-linewise| often applies.
*][*
-][ [count] sections forward or to the next '}' in the
+][ [count] |section|s forward or to the next '}' in the
first column. |exclusive|
Note that |exclusive-linewise| often applies.
*[[*
-[[ [count] sections backward or to the previous '{' in
+[[ [count] |section|s backward or to the previous '{' in
the first column. |exclusive|
Note that |exclusive-linewise| often applies.
*[]*
-[] [count] sections backward or to the previous '}' in
+[] [count] |section|s backward or to the previous '}' in
the first column. |exclusive|
Note that |exclusive-linewise| often applies.
@@ -502,36 +508,36 @@ aw "a word", select [count] words (see |word|).
Leading or trailing white space is included, but not
counted.
When used in Visual linewise mode "aw" switches to
- Visual characterwise mode.
+ Visual charwise mode.
*v_iw* *iw*
iw "inner word", select [count] words (see |word|).
White space between words is counted too.
When used in Visual linewise mode "iw" switches to
- Visual characterwise mode.
+ Visual charwise mode.
*v_aW* *aW*
aW "a WORD", select [count] WORDs (see |WORD|).
Leading or trailing white space is included, but not
counted.
When used in Visual linewise mode "aW" switches to
- Visual characterwise mode.
+ Visual charwise mode.
*v_iW* *iW*
iW "inner WORD", select [count] WORDs (see |WORD|).
White space between words is counted too.
When used in Visual linewise mode "iW" switches to
- Visual characterwise mode.
+ Visual charwise mode.
*v_as* *as*
as "a sentence", select [count] sentences (see
|sentence|).
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
*v_is* *is*
is "inner sentence", select [count] sentences (see
|sentence|).
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
*v_ap* *ap*
ap "a paragraph", select [count] paragraphs (see
@@ -552,14 +558,14 @@ a[ "a [] block", select [count] '[' ']' blocks. This
goes backwards to the [count] unclosed '[', and finds
the matching ']'. The enclosed text is selected,
including the '[' and ']'.
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
i] *v_i]* *v_i[* *i]* *i[*
i[ "inner [] block", select [count] '[' ']' blocks. This
goes backwards to the [count] unclosed '[', and finds
the matching ']'. The enclosed text is selected,
excluding the '[' and ']'.
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
a) *v_a)* *a)* *a(*
a( *vab* *v_ab* *v_a(* *ab*
@@ -567,54 +573,54 @@ ab "a block", select [count] blocks, from "[count] [(" to
the matching ')', including the '(' and ')' (see
|[(|). Does not include white space outside of the
parenthesis.
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
i) *v_i)* *i)* *i(*
i( *vib* *v_ib* *v_i(* *ib*
ib "inner block", select [count] blocks, from "[count] [("
to the matching ')', excluding the '(' and ')' (see
|[(|).
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
a> *v_a>* *v_a<* *a>* *a<*
a< "a <> block", select [count] <> blocks, from the
[count]'th unmatched '<' backwards to the matching
'>', including the '<' and '>'.
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
i> *v_i>* *v_i<* *i>* *i<*
i< "inner <> block", select [count] <> blocks, from
the [count]'th unmatched '<' backwards to the matching
'>', excluding the '<' and '>'.
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
*v_at* *at*
at "a tag block", select [count] tag blocks, from the
[count]'th unmatched "<aaa>" backwards to the matching
"</aaa>", including the "<aaa>" and "</aaa>".
See |tag-blocks| about the details.
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
*v_it* *it*
it "inner tag block", select [count] tag blocks, from the
[count]'th unmatched "<aaa>" backwards to the matching
"</aaa>", excluding the "<aaa>" and "</aaa>".
See |tag-blocks| about the details.
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
a} *v_a}* *a}* *a{*
a{ *v_aB* *v_a{* *aB*
aB "a Block", select [count] Blocks, from "[count] [{" to
the matching '}', including the '{' and '}' (see
|[{|).
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
i} *v_i}* *i}* *i{*
i{ *v_iB* *v_i{* *iB*
iB "inner Block", select [count] Blocks, from "[count] [{"
to the matching '}', excluding the '{' and '}' (see
|[{|).
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
a" *v_aquote* *aquote*
a' *v_a'* *a'*
@@ -628,7 +634,7 @@ a` *v_a`* *a`*
start of the line.
Any trailing white space is included, unless there is
none, then leading white space is included.
- When used in Visual mode it is made characterwise.
+ When used in Visual mode it is made charwise.
Repeating this object in Visual mode another string is
included. A count is currently not used.
@@ -1077,6 +1083,60 @@ When you split a window, the jumplist will be copied to the new window.
If you have included the ' item in the 'shada' option the jumplist will be
stored in the ShaDa file and restored when starting Vim.
+ *jumplist-stack*
+When jumpoptions includes "stack", the jumplist behaves like the history in a
+web browser and like the tag stack. When jumping to a new location from the
+middle of the jumplist, the locations after the current position will be
+discarded.
+
+This behavior corresponds to the following situation in a web browser.
+Navigate to first.com, second.com, third.com, fourth.com and then fifth.com.
+Then navigate backwards twice so that third.com is displayed. At that point,
+the history is:
+- first.com
+- second.com
+- third.com <--
+- fourth.com
+- fifth.com
+
+Finally, navigate to a different webpage, new.com. The history is
+- first.com
+- second.com
+- third.com
+- new.com <--
+
+When the jumpoptions includes "stack", this is the behavior of neovim as well.
+That is, given a jumplist like the following in which CTRL-O has been used to
+move back three times to location X
+
+ jump line col file/text
+ 2 1260 8 src/nvim/mark.c <-- location X-2
+ 1 685 0 src/nvim/option_defs.h <-- location X-1
+> 0 462 36 src/nvim/option_defs.h <-- location X
+ 1 479 39 src/nvim/option_defs.h
+ 2 213 2 src/nvim/mark.c
+ 3 181 0 src/nvim/mark.c
+
+jumping to location Y results in the locations after the current locations being
+removed:
+
+ jump line col file/text
+ 3 1260 8 src/nvim/mark.c
+ 2 685 0 src/nvim/option_defs.h
+ 1 462 36 src/nvim/option_defs.h <-- location X
+>
+
+Then, when yet another location Z is jumped to, the new location Y appears
+directly after location X in the jumplist and location X remains in the same
+position relative to the locations (X-1, X-2, etc., ...) that had been before it
+prior to the original jump from X to Y:
+
+ jump line col file/text
+ 4 1260 8 src/nvim/mark.c <-- location X-2
+ 3 685 0 src/nvim/option_defs.h <-- location X-1
+ 2 462 36 src/nvim/option_defs.h <-- location X
+ 1 100 0 src/nvim/option_defs.h <-- location Y
+>
CHANGE LIST JUMPS *changelist* *change-list-jumps* *E664*
diff --git a/runtime/doc/msgpack_rpc.txt b/runtime/doc/msgpack_rpc.txt
index f5d42dfeb2..5368cf0f4f 100644
--- a/runtime/doc/msgpack_rpc.txt
+++ b/runtime/doc/msgpack_rpc.txt
@@ -1,7 +1,8 @@
- NVIM REFERENCE MANUAL by Thiago de Arruda
-
-
+ NVIM REFERENCE MANUAL
This document was merged into |api.txt| and |develop.txt|.
+
+==============================================================================
+ vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 188f7fc2e2..4b8740c5d2 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -843,6 +843,14 @@ A jump table for the options with a short description can be found at |Q_op|.
name, precede it with a backslash.
- To include a comma in a directory name precede it with a backslash.
- A directory name may end in an '/'.
+ - For Unix and Win32, if a directory ends in two path separators "//",
+ the swap file name will be built from the complete path to the file
+ with all path separators changed to percent '%' signs. This will
+ ensure file name uniqueness in the backup directory.
+ On Win32, it is also possible to end with "\\". However, When a
+ separating comma is following, you must use "//", since "\\" will
+ include the comma in the file name. Therefore it is recommended to
+ use '//', instead of '\\'.
- Environment variables are expanded |:set_env|.
- Careful with '\' characters, type one before a space, type two to
get one in the option (see |option-backslash|), for example: >
@@ -1875,7 +1883,7 @@ A jump table for the options with a short description can be found at |Q_op|.
security reasons.
*'dip'* *'diffopt'*
-'diffopt' 'dip' string (default "internal,filler")
+'diffopt' 'dip' string (default "internal,filler,closeoff")
global
Option settings for diff mode. It can consist of the following items.
All are optional. Items must be separated by a comma.
@@ -1932,6 +1940,12 @@ A jump table for the options with a short description can be found at |Q_op|.
vertical Start diff mode with vertical splits (unless
explicitly specified otherwise).
+ closeoff When a window is closed where 'diff' is set
+ and there is only one window remaining in the
+ same tab page with 'diff' set, execute
+ `:diffoff` in that window. This undoes a
+ `:diffsplit` command.
+
hiddenoff Do not use diff mode for a buffer when it
becomes hidden.
@@ -1978,7 +1992,7 @@ A jump table for the options with a short description can be found at |Q_op|.
possible. If it is not possible in any directory, but last
directory listed in the option does not exist, it is created.
- Empty means that no swap file will be used (recovery is
- impossible!).
+ impossible!) and no |E303| error will be given.
- A directory "." means to put the swap file in the same directory as
the edited file. On Unix, a dot is prepended to the file name, so
it doesn't show in a directory listing. On MS-Windows the "hidden"
@@ -1986,12 +2000,14 @@ A jump table for the options with a short description can be found at |Q_op|.
- A directory starting with "./" (or ".\" for Windows) means to
put the swap file relative to where the edited file is. The leading
"." is replaced with the path name of the edited file.
- - For Unix and Win32, if a directory ends in two path separators "//"
- or "\\", the swap file name will be built from the complete path to
- the file with all path separators substituted to percent '%' signs.
- This will ensure file name uniqueness in the preserve directory.
- On Win32, when a separating comma is following, you must use "//",
- since "\\" will include the comma in the file name.
+ - For Unix and Win32, if a directory ends in two path separators "//",
+ the swap file name will be built from the complete path to the file
+ with all path separators substituted to percent '%' signs. This will
+ ensure file name uniqueness in the preserve directory.
+ On Win32, it is also possible to end with "\\". However, When a
+ separating comma is following, you must use "//", since "\\" will
+ include the comma in the file name. Therefore it is recommended to
+ use '//', instead of '\\'.
- Spaces after the comma are ignored, other spaces are considered part
of the directory name. To have a space at the start of a directory
name, precede it with a backslash.
@@ -2242,8 +2258,7 @@ A jump table for the options with a short description can be found at |Q_op|.
*'fileformat'* *'ff'*
'fileformat' 'ff' string (Windows default: "dos",
- Unix default: "unix",
- Macintosh default: "mac")
+ Unix default: "unix")
local to buffer
This gives the <EOL> of the current buffer, which is used for
reading/writing the buffer from/to a file:
@@ -2265,7 +2280,6 @@ A jump table for the options with a short description can be found at |Q_op|.
'fileformats' 'ffs' string (default:
Vim+Vi Win32: "dos,unix",
Vim Unix: "unix,dos",
- Vim Mac: "mac,unix,dos",
Vi others: "")
global
This gives the end-of-line (<EOL>) formats that will be tried when
@@ -2348,7 +2362,7 @@ A jump table for the options with a short description can be found at |Q_op|.
*'fillchars'* *'fcs'*
'fillchars' 'fcs' string (default "")
- local to window
+ global or local to window |global-local|
Characters to fill the statuslines and vertical separators.
It is a comma separated list of items:
@@ -3443,6 +3457,17 @@ A jump table for the options with a short description can be found at |Q_op|.
Unprintable and zero-width Unicode characters are displayed as <xxxx>.
There is no option to specify these characters.
+ *'jumpoptions'* *'jop'*
+'jumpoptions' 'jop' string (default "")
+ global
+ List of words that change the behavior of the |jumplist|.
+ stack Make the jumplist behave like the tagstack or like a
+ web browser. Relative location of entries in the
+ jumplist is preserved at the cost of discarding
+ subsequent entries when navigating backwards in the
+ jumplist and then jumping to a location.
+ |jumplist-stack|
+
*'joinspaces'* *'js'* *'nojoinspaces'* *'nojs'*
'joinspaces' 'js' boolean (default on)
global
@@ -3657,7 +3682,7 @@ A jump table for the options with a short description can be found at |Q_op|.
*'listchars'* *'lcs'*
'listchars' 'lcs' string (default: "tab:> ,trail:-,nbsp:+"
Vi default: "eol:$")
- local to window
+ global or local to window |global-local|
Strings to use in 'list' mode and for the |:list| command. It is a
comma separated list of string settings.
@@ -3698,9 +3723,9 @@ A jump table for the options with a short description can be found at |Q_op|.
off and the line continues beyond the right of the
screen.
*lcs-precedes*
- precedes:c Character to show in the first column, when 'wrap'
- is off and there is text preceding the character
- visible in the first column.
+ precedes:c Character to show in the first visible column of the
+ physical line, when there is text preceding the
+ character visible in the first column.
*lcs-conceal*
conceal:c Character to show in place of concealed text, when
'conceallevel' is set to 1. A space when omitted.
@@ -4583,6 +4608,16 @@ A jump table for the options with a short description can be found at |Q_op|.
RedrawDebugRecompose guibg=Red redraw generated by the
compositor itself, due to a
grid being moved or deleted.
+ nothrottle Turn off throttling of the message grid. This is an
+ optimization that joins many small scrolls to one
+ larger scroll when drawing the message area (with
+ 'display' msgsep flag active).
+ invalid Enable stricter checking (abort) of inconsistencies
+ of the internal screen state. This is mostly
+ useful when running nvim inside a debugger (and
+ the test suite).
+ nodelta Send all internally redrawn cells to the UI, even if
+ they are unchanged from the already displayed state.
*'redrawtime'* *'rdt'*
'redrawtime' 'rdt' number (default 2000)
@@ -5159,7 +5194,7 @@ A jump table for the options with a short description can be found at |Q_op|.
unescaping, so to keep yourself sane use |:let-&| like shown above.
*shell-powershell*
To use powershell (on Windows): >
- set shell=powershell shellquote=( shellpipe=\| shellxquote=
+ set shell=powershell shellquote= shellpipe=\| shellxquote=
set shellcmdflag=-NoLogo\ -NoProfile\ -ExecutionPolicy\ RemoteSigned\ -Command
set shellredir=\|\ Out-File\ -Encoding\ UTF8
@@ -5722,7 +5757,7 @@ A jump table for the options with a short description can be found at |Q_op|.
current one. |:vsplit|
*'startofline'* *'sol'* *'nostartofline'* *'nosol'*
-'startofline' 'sol' boolean (default on)
+'startofline' 'sol' boolean (default off)
global
When "on" the commands listed below move the cursor to the first
non-blank of the line. When off the cursor is kept in the same column
@@ -5990,6 +6025,8 @@ A jump table for the options with a short description can be found at |Q_op|.
vsplit Just like "split" but split vertically.
newtab Like "split", but open a new tab page. Overrules
"split" when both are present.
+ uselast If included, jump to the previously used window when
+ jumping to errors with |quickfix| commands.
*'synmaxcol'* *'smc'*
'synmaxcol' 'smc' number (default 3000)
@@ -6149,6 +6186,14 @@ A jump table for the options with a short description can be found at |Q_op|.
match Match case
smart Ignore case unless an upper case letter is used
+ *'tagfunc'* *'tfu'*
+'tagfunc' 'tfu' string (default: empty)
+ local to buffer
+ This option specifies a function to be used to perform tag searches.
+ The function gets the tag pattern and should return a List of matching
+ tags. See |tag-function| for an explanation of how to write the
+ function and an example.
+
*'taglength'* *'tl'*
'taglength' 'tl' number (default 0)
global
@@ -6620,22 +6665,18 @@ A jump table for the options with a short description can be found at |Q_op|.
*'wildmenu'* *'wmnu'* *'nowildmenu'* *'nowmnu'*
'wildmenu' 'wmnu' boolean (default on)
global
- When 'wildmenu' is on, command-line completion operates in an enhanced
- mode. On pressing 'wildchar' (usually <Tab>) to invoke completion,
- the possible matches are shown just above the command line, with the
- first match highlighted (overwriting the status line, if there is
- one). Keys that show the previous/next match, such as <Tab> or
- CTRL-P/CTRL-N, cause the highlight to move to the appropriate match.
- When 'wildmode' is used, "wildmenu" mode is used where "full" is
- specified. "longest" and "list" do not start "wildmenu" mode.
- You can check the current mode with |wildmenumode()|.
- If there are more matches than can fit in the line, a ">" is shown on
- the right and/or a "<" is shown on the left. The status line scrolls
- as needed.
- The "wildmenu" mode is abandoned when a key is hit that is not used
- for selecting a completion.
- While the "wildmenu" is active the following keys have special
- meanings:
+ Enables "enhanced mode" of command-line completion. When user hits
+ <Tab> (or 'wildchar') to invoke completion, the possible matches are
+ shown in a menu just above the command-line (see 'wildoptions'), with
+ the first match highlighted (overwriting the statusline). Keys that
+ show the previous/next match (<Tab>/CTRL-P/CTRL-N) highlight the
+ match.
+ 'wildmode' must specify "full": "longest" and "list" do not start
+ 'wildmenu' mode. You can check the current mode with |wildmenumode()|.
+ The menu is canceled when a key is hit that is not used for selecting
+ a completion.
+
+ While the menu is active these keys have special meanings:
<Left> <Right> - select previous/next match (like CTRL-P/CTRL-N)
<Down> - in filename/menu name completion: move into a
@@ -6645,15 +6686,12 @@ A jump table for the options with a short description can be found at |Q_op|.
<Up> - in filename/menu name completion: move up into
parent directory or parent menu.
- This makes the menus accessible from the console |console-menus|.
-
- If you prefer the <Left> and <Right> keys to move the cursor instead
- of selecting a different match, use this: >
+ If you want <Left> and <Right> to move the cursor instead of selecting
+ a different match, use this: >
:cnoremap <Left> <Space><BS><Left>
:cnoremap <Right> <Space><BS><Right>
<
- The "WildMenu" highlighting is used for displaying the current match
- |hl-WildMenu|.
+ |hl-WildMenu| highlights the current match.
*'wildmode'* *'wim'*
'wildmode' 'wim' string (default: "full")
@@ -6908,7 +6946,6 @@ A jump table for the options with a short description can be found at |Q_op|.
global
The number of milliseconds to wait for each character sent to the
screen. When positive, characters are sent to the UI one by one.
- When negative, all redrawn characters cause a delay, even if the
- character already was displayed by the UI. For debugging purposes.
+ See 'redrawdebug' for more options. For debugging purposes.
vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/quickfix.txt b/runtime/doc/quickfix.txt
index 3ae6d9461f..61e090cc78 100644
--- a/runtime/doc/quickfix.txt
+++ b/runtime/doc/quickfix.txt
@@ -109,6 +109,36 @@ processing a quickfix or location list command, it will be aborted.
list for the current window is used instead of the
quickfix list.
+ *:cabo* *:cabove*
+:[count]cabo[ve] Go to the [count] error above the current line in the
+ current buffer. If [count] is omitted, then 1 is
+ used. If there are no errors, then an error message
+ is displayed. Assumes that the entries in a quickfix
+ list are sorted by their buffer number and line
+ number. If there are multiple errors on the same line,
+ then only the first entry is used. If [count] exceeds
+ the number of entries above the current line, then the
+ first error in the file is selected.
+
+ *:lab* *:labove*
+:[count]lab[ove] Same as ":cabove", except the location list for the
+ current window is used instead of the quickfix list.
+
+ *:cbe* *:cbelow*
+:[count]cbe[low] Go to the [count] error below the current line in the
+ current buffer. If [count] is omitted, then 1 is
+ used. If there are no errors, then an error message
+ is displayed. Assumes that the entries in a quickfix
+ list are sorted by their buffer number and line
+ number. If there are multiple errors on the same
+ line, then only the first entry is used. If [count]
+ exceeds the number of entries below the current line,
+ then the last error in the file is selected.
+
+ *:lbe* *:lbelow*
+:[count]lbe[low] Same as ":cbelow", except the location list for the
+ current window is used instead of the quickfix list.
+
*:cnf* *:cnfile*
:[count]cnf[ile][!] Display the first error in the [count] next file in
the list that includes a file name. If there are no
diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt
index 87cb9b54f5..224f14a18b 100644
--- a/runtime/doc/quickref.txt
+++ b/runtime/doc/quickref.txt
@@ -47,6 +47,7 @@ N is used to indicate an optional count that can be given before the command.
|g$| N g$ to last character in screen line (differs from "$"
when lines wrap)
|gm| gm to middle of the screen line
+|gM| gM to middle of the line
|bar| N | to column N (default: 1)
|f| N f{char} to the Nth occurrence of {char} to the right
|F| N F{char} to the Nth occurrence of {char} to the left
@@ -742,6 +743,7 @@ Short explanation of each option: *option-list*
'iskeyword' 'isk' characters included in keywords
'isprint' 'isp' printable characters
'joinspaces' 'js' two spaces after a period with a join command
+'jumpoptions' 'jop' specifies how jumping is done
'keymap' 'kmp' name of a keyboard mapping
'keymodel' 'km' enable starting/stopping selection with keys
'keywordprg' 'kp' program to use for the "K" command
diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt
index 7dbbb2d424..e3f0d593a7 100644
--- a/runtime/doc/starting.txt
+++ b/runtime/doc/starting.txt
@@ -1270,7 +1270,7 @@ exactly four MessagePack objects:
Key Type Def Description ~
rt UInteger 0 Register type:
No Description ~
- 0 |characterwise-register|
+ 0 |charwise-register|
1 |linewise-register|
2 |blockwise-register|
rw UInteger 0 Register width. Only valid
diff --git a/runtime/doc/syntax.txt b/runtime/doc/syntax.txt
index 0a4257e2b2..5424ad00ec 100644
--- a/runtime/doc/syntax.txt
+++ b/runtime/doc/syntax.txt
@@ -4720,18 +4720,19 @@ the same syntax file on all UIs.
*bold* *underline* *undercurl*
*inverse* *italic* *standout*
- *strikethrough*
+ *nocombine* *strikethrough*
cterm={attr-list} *attr-list* *highlight-cterm* *E418*
attr-list is a comma separated list (without spaces) of the
following items (in any order):
bold
underline
undercurl curly underline
+ strikethrough
reverse
inverse same as reverse
italic
standout
- strikethrough
+ nocombine override attributes instead of combining them
NONE no attributes used (used to reset it)
Note that "bold" can be used here and by using a bold font. They
diff --git a/runtime/doc/tagsrch.txt b/runtime/doc/tagsrch.txt
index bb3134feb6..b011db3dd3 100644
--- a/runtime/doc/tagsrch.txt
+++ b/runtime/doc/tagsrch.txt
@@ -838,4 +838,70 @@ Common arguments for the commands above:
< For a ":djump", ":dsplit", ":dlist" and ":dsearch" command the pattern
is used as a literal string, not as a search pattern.
+==============================================================================
+7. Using 'tagfunc' *tag-function*
+
+It is possible to provide Vim with a function which will generate a list of
+tags used for commands like |:tag|, |:tselect| and Normal mode tag commands
+like |CTRL-]|.
+
+The function used for generating the taglist is specified by setting the
+'tagfunc' option. The function will be called with three arguments:
+ a:pattern The tag identifier used during the tag search.
+ a:flags List of flags to control the function behavior.
+ a:info Dict containing the following entries:
+ buf_ffname Full filename which can be used for priority.
+ user_data Custom data String, if stored in the tag
+ stack previously by tagfunc.
+
+Currently two flags may be passed to the tag function:
+ 'c' The function was invoked by a normal command being processed
+ (mnemonic: the tag function may use the context around the
+ cursor to perform a better job of generating the tag list.)
+ 'i' In Insert mode, the user was completing a tag (with
+ |i_CTRL-X_CTRL-]|).
+
+Note that when 'tagfunc' is set, the priority of the tags described in
+|tag-priority| does not apply. Instead, the priority is exactly as the
+ordering of the elements in the list returned by the function.
+ *E987*
+The function should return a List of Dict entries. Each Dict must at least
+include the following entries and each value must be a string:
+ name Name of the tag.
+ filename Name of the file where the tag is defined. It is
+ either relative to the current directory or a full path.
+ cmd Ex command used to locate the tag in the file. This
+ can be either an Ex search pattern or a line number.
+Note that the format is similar to that of |taglist()|, which makes it possible
+to use its output to generate the result.
+The following fields are optional:
+ kind Type of the tag.
+ user_data String of custom data stored in the tag stack which
+ can be used to disambiguate tags between operations.
+
+If the function returns |v:null| instead of a List, a standard tag lookup will
+be performed instead.
+
+It is not allowed to change the tagstack from inside 'tagfunc'. *E986*
+
+The following is a hypothetical example of a function used for 'tagfunc'. It
+uses the output of |taglist()| to generate the result: a list of tags in the
+inverse order of file names.
+>
+ function! TagFunc(pattern, flags, info)
+ function! CompareFilenames(item1, item2)
+ let f1 = a:item1['filename']
+ let f2 = a:item2['filename']
+ return f1 >=# f2 ?
+ \ -1 : f1 <=# f2 ? 1 : 0
+ endfunction
+
+ let result = taglist(a:pattern)
+ call sort(result, "CompareFilenames")
+
+ return result
+ endfunc
+ set tagfunc=TagFunc
+<
+
vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/ui.txt b/runtime/doc/ui.txt
index a2f19593ae..d5f4a59ab3 100644
--- a/runtime/doc/ui.txt
+++ b/runtime/doc/ui.txt
@@ -201,8 +201,8 @@ the editor.
sent from Nvim, like for |ui-cmdline|.
["mode_change", mode, mode_idx]
- The mode changed. The first parameter `mode` is a string representing
- the current mode. `mode_idx` is an index into the array received in
+ Editor mode changed. The `mode` parameter is a string representing
+ the current mode. `mode_idx` is an index into the array emitted in
the `mode_info_set` event. UIs should change the cursor style
according to the properties specified in the corresponding item. The
set of modes reported will change in new versions of Nvim, for
@@ -211,11 +211,11 @@ the editor.
["mouse_on"]
["mouse_off"]
- Tells the client whether mouse support, as determined by |'mouse'|
- option, is considered to be active in the current mode. This is mostly
- useful for a terminal frontend, or other situations where Nvim mouse
- would conflict with other usages of the mouse. It is safe for a client
- to ignore this and always send mouse events.
+ |'mouse'| was enabled/disabled in the current editor mode. Useful for
+ a terminal UI, or other situations where Nvim mouse would conflict
+ with other usages of the mouse. UIs may ignore this and always send
+ mouse input, because 'mouse' decides the behavior of |nvim_input()|
+ implicitly.
["busy_start"]
["busy_stop"]
diff --git a/runtime/doc/usr_25.txt b/runtime/doc/usr_25.txt
index 3a58af6412..2efb67e55f 100644
--- a/runtime/doc/usr_25.txt
+++ b/runtime/doc/usr_25.txt
@@ -346,12 +346,13 @@ scroll:
g0 to first visible character in this line
g^ to first non-blank visible character in this line
- gm to middle of this line
+ gm to middle of screen line
+ gM to middle of the text in this line
g$ to last visible character in this line
- |<-- window -->|
- some long text, part of which is visible ~
- g0 g^ gm g$
+ |<-- window -->|
+ some long text, part of which is visible in one line ~
+ g0 g^ gm gM g$
BREAKING AT WORDS *edit-no-break*
diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt
index 45a94bb961..64b5830575 100644
--- a/runtime/doc/vim_diff.txt
+++ b/runtime/doc/vim_diff.txt
@@ -55,6 +55,7 @@ the differences.
- 'showcmd' is enabled
- 'sidescroll' defaults to 1
- 'smarttab' is enabled
+- 'startofline' is disabled
- 'tabpagemax' defaults to 50
- 'tags' defaults to "./tags;,tags"
- 'ttimeoutlen' defaults to 50
@@ -168,6 +169,7 @@ Functions:
|system()|, |systemlist()| can run {cmd} directly (without 'shell')
Highlight groups:
+ |highlight-blend| controls blend level for a highlight group
|expr-highlight| highlight groups (prefixed with "Nvim")
|hl-NormalFloat| highlights floating window
|hl-NormalNC| highlights non-current windows
@@ -206,6 +208,7 @@ Options:
'statusline' supports unlimited alignment sections
'tabline' %@Func@foo%X can call any function on mouse-click
'wildoptions' `pum` flag to use popupmenu for wildmode completion
+ 'winblend' pseudo-transparency in floating windows |api-floatwin|
'winhighlight' window-local highlights
Signs:
@@ -296,7 +299,7 @@ coerced to strings. See |id()| for more details, currently it uses
|c_CTRL-R| pasting a non-special register into |cmdline| omits the last <CR>.
-Lua interface (|if_lua.txt|):
+Lua interface (|lua.txt|):
- `:lua print("a\0b")` will print `a^@b`, like with `:echomsg "a\nb"` . In Vim
that prints `a` and `b` on separate lines, exactly like
@@ -307,15 +310,15 @@ Lua interface (|if_lua.txt|):
- Lua package.path and package.cpath are automatically updated according to
'runtimepath': |lua-require|.
-|input()| and |inputdialog()| support for each other’s features (return on
-cancel and completion respectively) via dictionary argument (replaces all
-other arguments if used).
-
-|input()| and |inputdialog()| support user-defined cmdline highlighting.
-
Commands:
|:doautocmd| does not warn about "No matching autocommands".
+Functions:
+ |input()| and |inputdialog()| support for each other’s features (return on
+ cancel and completion respectively) via dictionary argument (replaces all
+ other arguments if used).
+ |input()| and |inputdialog()| support user-defined cmdline highlighting.
+
Highlight groups:
|hl-ColorColumn|, |hl-CursorColumn| are lower priority than most other
groups
@@ -333,6 +336,11 @@ Macro/|recording| behavior
Motion:
The |jumplist| avoids useless/phantom jumps.
+ When the new option |jumpoptions| includes 'stack', the jumplist behaves
+ like the tagstack or history in a web browser--jumping from the middle of
+ the jumplist discards the locations after the jumped-from position
+ (|jumplist-stack|).
+
Normal commands:
|Q| is the same as |gQ|
@@ -399,10 +407,10 @@ VimL (Vim script) compatibility:
Some legacy Vim features are not implemented:
-- |if_py|: *python-bindeval* *python-Function* are not supported
-- |if_lua|: the `vim` object is missing some legacy methods
-- *if_perl*
+- |if_lua|: Nvim Lua API is not compatible with Vim's "if_lua"
- *if_mzscheme*
+- *if_perl*
+- |if_py|: *python-bindeval* *python-Function* are not supported
- *if_tcl*
==============================================================================
@@ -524,4 +532,4 @@ TUI:
always uses 7-bit control sequences.
==============================================================================
- vim:tw=78:ts=8:sw=2:noet:ft=help:norl:
+ vim:tw=78:ts=8:sw=2:et:ft=help:norl:
diff --git a/runtime/doc/visual.txt b/runtime/doc/visual.txt
index ccbbc092ec..0052382044 100644
--- a/runtime/doc/visual.txt
+++ b/runtime/doc/visual.txt
@@ -48,7 +48,7 @@ position.
==============================================================================
2. Starting and stopping Visual mode *visual-start*
- *v* *characterwise-visual*
+ *v* *charwise-visual*
[count]v Start Visual mode per character.
With [count] select the same number of characters or
lines as used for the last Visual operation, but at
@@ -74,7 +74,7 @@ position.
If you use <Esc>, click the left mouse button or use any command that
does a jump to another buffer while in Visual mode, the highlighting stops
-and no text is affected. Also when you hit "v" in characterwise Visual mode,
+and no text is affected. Also when you hit "v" in charwise Visual mode,
"CTRL-V" in blockwise Visual mode or "V" in linewise Visual mode. If you hit
CTRL-Z the highlighting stops and the editor is suspended or a new shell is
started |CTRL-Z|.
@@ -477,7 +477,7 @@ Commands in Select mode:
Otherwise, typed characters are handled as in Visual mode.
When using an operator in Select mode, and the selection is linewise, the
-selected lines are operated upon, but like in characterwise selection. For
+selected lines are operated upon, but like in charwise selection. For
example, when a whole line is deleted, it can later be pasted in the middle of
a line.
@@ -510,7 +510,7 @@ gV Avoid the automatic reselection of the Visual area
selection.
*gh*
-gh Start Select mode, characterwise. This is like "v",
+gh Start Select mode, charwise. This is like "v",
but starts Select mode instead of Visual mode.
Mnemonic: "get highlighted".
diff --git a/runtime/doc/windows.txt b/runtime/doc/windows.txt
index 76bb096ee3..977e0daef7 100644
--- a/runtime/doc/windows.txt
+++ b/runtime/doc/windows.txt
@@ -201,9 +201,11 @@ CTRL-W CTRL_N *CTRL-W_CTRL-N*
|:find|. Doesn't split if {file} is not found.
CTRL-W CTRL-^ *CTRL-W_CTRL-^* *CTRL-W_^*
-CTRL-W ^ Does ":split #", split window in two and edit alternate file.
- When a count is given, it becomes ":split #N", split window
- and edit buffer N.
+CTRL-W ^ Split the current window in two and edit the alternate file.
+ When a count N is given, split the current window and edit
+ buffer N. Similar to ":sp #" and ":sp #N", but it allows the
+ other buffer to be unnamed. This command matches the behavior
+ of |CTRL-^|, except that it splits a window first.
CTRL-W ge *CTRL-W_ge*
Detach the current window as an external window.
diff --git a/runtime/filetype.vim b/runtime/filetype.vim
index 6abf9da55f..e27d234a96 100644
--- a/runtime/filetype.vim
+++ b/runtime/filetype.vim
@@ -1,7 +1,7 @@
" Vim support file to detect file types
"
" Maintainer: Bram Moolenaar <Bram@vim.org>
-" Last Change: 2019 Aug 26
+" Last Change: 2019 Nov 26
" Listen very carefully, I will say this only once
if exists("did_load_filetypes")
@@ -421,6 +421,9 @@ au BufNewFile,BufRead *.csp,*.fdr setf csp
au BufNewFile,BufRead *.pld setf cupl
au BufNewFile,BufRead *.si setf cuplsim
+" Dart
+au BufRead,BufNewfile *.dart,*.drt setf dart
+
" Debian Control
au BufNewFile,BufRead */debian/control setf debcontrol
au BufNewFile,BufRead control
@@ -793,8 +796,8 @@ au BufNewFile,BufRead *.java,*.jav setf java
" JavaCC
au BufNewFile,BufRead *.jj,*.jjt setf javacc
-" JavaScript, ECMAScript
-au BufNewFile,BufRead *.js,*.javascript,*.es,*.mjs setf javascript
+" JavaScript, ECMAScript, ES module script, CommonJS script
+au BufNewFile,BufRead *.js,*.javascript,*.es,*.mjs,*.cjs setf javascript
" JavaScript with React
au BufNewFile,BufRead *.jsx setf javascriptreact
@@ -975,6 +978,9 @@ au BufNewFile,BufRead hg-editor-*.txt setf hgcommit
" Mercurial config (looks like generic config file)
au BufNewFile,BufRead *.hgrc,*hgrc setf cfg
+" Meson Build system config
+au BufNewFile,BufRead meson.build,meson_options.txt setf meson
+
" Messages (logs mostly)
au BufNewFile,BufRead */log/{auth,cron,daemon,debug,kern,lpr,mail,messages,news/news,syslog,user}{,.log,.err,.info,.warn,.crit,.notice}{,.[0-9]*,-[0-9]*} setf messages
diff --git a/runtime/ftplugin/man.vim b/runtime/ftplugin/man.vim
index 6c7f095f62..081181cfe9 100644
--- a/runtime/ftplugin/man.vim
+++ b/runtime/ftplugin/man.vim
@@ -1,4 +1,4 @@
-" Maintainer: Anmol Sethi <anmol@aubble.com>
+" Maintainer: Anmol Sethi <hi@nhooyr.io>
" Previous Maintainer: SungHyun Nam <goweol@gmail.com>
if exists('b:did_ftplugin') || &filetype !=# 'man'
@@ -20,13 +20,12 @@ setlocal wrap breakindent linebreak
setlocal nonumber norelativenumber
setlocal foldcolumn=0 colorcolumn=0 nolist nofoldenable
+setlocal tagfunc=man#goto_tag
+
if !exists('g:no_plugin_maps') && !exists('g:no_man_maps')
nnoremap <silent> <buffer> j gj
nnoremap <silent> <buffer> k gk
nnoremap <silent> <buffer> gO :call man#show_toc()<CR>
- nnoremap <silent> <buffer> <C-]> :Man<CR>
- nnoremap <silent> <buffer> K :Man<CR>
- nnoremap <silent> <buffer> <C-T> :call man#pop_tag()<CR>
if 1 == bufnr('%') || s:pager
nnoremap <silent> <buffer> <nowait> q :lclose<CR>:q<CR>
else
diff --git a/runtime/indent/Makefile b/runtime/indent/Makefile
new file mode 100644
index 0000000000..d192605527
--- /dev/null
+++ b/runtime/indent/Makefile
@@ -0,0 +1,14 @@
+# Portable Makefile for running indent tests.
+
+VIM = vim
+VIMRUNTIME = ..
+
+# Run the tests that didn't run yet or failed previously.
+# If a test succeeds a testdir/*.out file will be written.
+# If a test fails a testdir/*.fail file will be written.
+test:
+ VIMRUNTIME=$(VIMRUNTIME) $(VIM) --clean --not-a-term -u testdir/runtest.vim
+
+
+clean:
+ rm -f testdir/*.fail testdir/*.out
diff --git a/runtime/indent/README.txt b/runtime/indent/README.txt
index a424b4f8c0..8b114365c6 100644
--- a/runtime/indent/README.txt
+++ b/runtime/indent/README.txt
@@ -43,3 +43,5 @@ running. Add a test if the function exists and use ":finish", like this:
The user may have several options set unlike you, try to write the file such
that it works with any option settings. Also be aware of certain features not
being compiled in.
+
+To test the indent file, see testdir/README.txt.
diff --git a/runtime/indent/testdir/README.txt b/runtime/indent/testdir/README.txt
new file mode 100644
index 0000000000..65975605c2
--- /dev/null
+++ b/runtime/indent/testdir/README.txt
@@ -0,0 +1,97 @@
+TESTING INDENT SCRIPTS
+
+We'll use FILETYPE for the filetype name here.
+
+
+FORMAT OF THE FILETYPE.IN FILE
+
+First of all, create a FILETYPE.in file. It should contain:
+
+- A modeline setting the 'filetype' and any other option values.
+ This must work like a comment for FILETYPE. E.g. for vim:
+ " vim: set ft=vim sw=4 :
+
+- At least one block of lines to indent, prefixed with START_INDENT and
+ followed by END_INDENT. These lines must also look like a comment for your
+ FILETYPE. You would normally leave out all indent, so that the effect of
+ the indent command results in adding indent. Example:
+
+ " START_INDENT
+ func Some()
+ let x = 1
+ endfunc
+ " END_INDENT
+
+ If you just want to test normal indenting with default options, you can make
+ this a large number of lines. Just add all kinds of language constructs,
+ nested statements, etc. with valid syntax.
+
+- Optionally, add lines with INDENT_EXE after START_INDENT, followed by a Vim
+ command. This will be executed before indenting the lines. Example:
+
+ " START_INDENT
+ " INDENT_EXE let g:vim_indent_cont = 6
+ let cmd =
+ \ 'some '
+ \ 'string'
+ " END_INDENT
+
+ Note that the command is not undone, you may need to reverse the effect for
+ the next block of lines.
+
+- Alternatively to indenting all the lines between START_INDENT and
+ END_INDENT, use an INDENT_AT line, which specifies a pattern to find the
+ line to indent. Example:
+
+ " START_INDENT
+ " INDENT_AT this-line
+ func Some()
+ let f = x " this-line
+ endfunc
+ " END_INDENT
+
+ Alternatively you can use INDENT_NEXT to indent the line below the matching
+ pattern. Keep in mind that quite often it will indent relative to the
+ matching line:
+
+ " START_INDENT
+ " INDENT_NEXT next-line
+ func Some()
+ " next-line
+ let f = x
+ endfunc
+ " END_INDENT
+
+ Or use INDENT_PREV to indent the line above the matching pattern:
+
+ " START_INDENT
+ " INDENT_PREV prev-line
+ func Some()
+ let f = x
+ " prev-line
+ endfunc
+ " END_INDENT
+
+It's best to keep the whole file valid for FILETYPE, so that syntax
+highlighting works normally, and any indenting that depends on the syntax
+highlighting also works.
+
+
+RUNNING THE TEST
+
+Before running the test, create a FILETYPE.ok file. You can leave it empty at
+first.
+
+Now run "make test" from the parent directory. After Vim has done the
+indenting you will see a FILETYPE.fail file. This contains the actual result
+of indenting, and it's different from the FILETYPE.ok file.
+
+Check the contents of the FILETYPE.fail file. If it is perfectly OK, then
+rename it to overwrite the FILETYPE.ok file. If you now run "make test" again,
+the test will pass and create a FILETYPE.out file, which is identical to the
+FILETYPE.ok file. The FILETYPE.fail file will be deleted.
+
+If you try to run "make test" again you will notice that nothing happens,
+because the FILETYPE.out file already exists. Delete it, or do "make clean",
+so that the text runs again. If you edit the FILETYPE.in file, so that it's
+newer than the FILETYPE.out file, the test will also run.
diff --git a/runtime/indent/testdir/runtest.vim b/runtime/indent/testdir/runtest.vim
new file mode 100644
index 0000000000..9502c42f3e
--- /dev/null
+++ b/runtime/indent/testdir/runtest.vim
@@ -0,0 +1,132 @@
+" Runs all the indent tests for which there is no .out file.
+"
+" Current directory must be runtime/indent.
+
+" Only do this with the +eval feature
+if 1
+
+set nocp
+filetype indent on
+syn on
+set nowrapscan
+set report=9999
+
+au! SwapExists * call HandleSwapExists()
+func HandleSwapExists()
+ " Ignore finding a swap file for the test input and output, the user might be
+ " editing them and that's OK.
+ if expand('<afile>') =~ '.*\.\(in\|out\|fail\|ok\)'
+ let v:swapchoice = 'e'
+ endif
+endfunc
+
+let failed_count = 0
+for fname in glob('testdir/*.in', 1, 1)
+ let root = substitute(fname, '\.in', '', '')
+
+ " Execute the test if the .out file does not exist of when the .in file is
+ " newer.
+ let in_time = getftime(fname)
+ let out_time = getftime(root . '.out')
+ if out_time < 0 || in_time > out_time
+ call delete(root . '.fail')
+ call delete(root . '.out')
+
+ set sw& ts& filetype=
+ exe 'split ' . fname
+
+ let did_some = 0
+ let failed = 0
+ let end = 1
+ while 1
+ " Indent all the lines between "START_INDENT" and "END_INDENT"
+ exe end
+ let start = search('\<START_INDENT\>')
+ let end = search('\<END_INDENT\>')
+ if start <= 0 || end <= 0 || end <= start
+ if did_some == 0
+ call append(0, 'ERROR: START_INDENT and/or END_INDENT not found')
+ let failed = 1
+ endif
+ break
+ else
+ let did_some = 1
+
+ " Execute all commands marked with INDENT_EXE and find any pattern.
+ let lnum = start
+ let pattern = ''
+ let at = ''
+ while 1
+ exe lnum + 1
+ let lnum_exe = search('\<INDENT_EXE\>')
+ exe lnum + 1
+ let indent_at = search('\<INDENT_\(AT\|NEXT\|PREV\)\>')
+ if lnum_exe > 0 && lnum_exe < end && (indent_at <= 0 || lnum_exe < indent_at)
+ exe substitute(getline(lnum_exe), '.*INDENT_EXE', '', '')
+ let lnum = lnum_exe
+ let start = lnum
+ elseif indent_at > 0 && indent_at < end
+ if pattern != ''
+ call append(indent_at, 'ERROR: duplicate pattern')
+ let failed = 1
+ break
+ endif
+ let text = getline(indent_at)
+ let pattern = substitute(text, '.*INDENT_\S*\s*', '', '')
+ let at = substitute(text, '.*INDENT_\(\S*\).*', '\1', '')
+ let lnum = indent_at
+ let start = lnum
+ else
+ break
+ endif
+ endwhile
+
+ exe start + 1
+ if pattern == ''
+ exe 'normal =' . (end - 1) . 'G'
+ else
+ let lnum = search(pattern)
+ if lnum <= 0
+ call append(indent_at, 'ERROR: pattern not found: ' . pattern)
+ let failed = 1
+ break
+ endif
+ if at == 'AT'
+ exe lnum
+ elseif at == 'NEXT'
+ exe lnum + 1
+ else
+ exe lnum - 1
+ endif
+ normal ==
+ endif
+ endif
+ endwhile
+
+ if !failed
+ " Check the resulting text equals the .ok file.
+ if getline(1, '$') != readfile(root . '.ok')
+ let failed = 1
+ endif
+ endif
+
+ if failed
+ let failed_count += 1
+ exe 'write ' . root . '.fail'
+ echoerr 'Test ' . fname . ' FAILED!'
+ else
+ exe 'write ' . root . '.out'
+ endif
+
+ quit! " close the indented file
+ endif
+endfor
+
+" Matching "if 1" at the start.
+endif
+
+if failed_count > 0
+ " have make report an error
+ cquit
+endif
+qall!
diff --git a/runtime/indent/testdir/vim.in b/runtime/indent/testdir/vim.in
new file mode 100644
index 0000000000..ca105f6eda
--- /dev/null
+++ b/runtime/indent/testdir/vim.in
@@ -0,0 +1,46 @@
+" vim: set ft=vim sw=4 :
+
+" START_INDENT
+
+func Some()
+let x = 1
+endfunc
+
+let cmd =
+\ 'some '
+\ 'string'
+
+" END_INDENT
+
+" START_INDENT
+" INDENT_EXE let g:vim_indent_cont = 6
+
+let cmd =
+\ 'some '
+\ 'string'
+
+" END_INDENT
+
+" START_INDENT
+" INDENT_EXE unlet g:vim_indent_cont
+" INDENT_AT this-line
+func Some()
+let f = x " this-line
+endfunc
+" END_INDENT
+
+" START_INDENT
+" INDENT_NEXT next-line
+func Some()
+ " next-line
+let f = x
+endfunc
+" END_INDENT
+
+" START_INDENT
+" INDENT_PREV prev-line
+func Some()
+let f = x
+" prev-line
+endfunc
+" END_INDENT
diff --git a/runtime/indent/testdir/vim.ok b/runtime/indent/testdir/vim.ok
new file mode 100644
index 0000000000..542861ec9c
--- /dev/null
+++ b/runtime/indent/testdir/vim.ok
@@ -0,0 +1,46 @@
+" vim: set ft=vim sw=4 :
+
+" START_INDENT
+
+func Some()
+ let x = 1
+endfunc
+
+let cmd =
+ \ 'some '
+ \ 'string'
+
+" END_INDENT
+
+" START_INDENT
+" INDENT_EXE let g:vim_indent_cont = 6
+
+let cmd =
+ \ 'some '
+ \ 'string'
+
+" END_INDENT
+
+" START_INDENT
+" INDENT_EXE unlet g:vim_indent_cont
+" INDENT_AT this-line
+func Some()
+ let f = x " this-line
+endfunc
+" END_INDENT
+
+" START_INDENT
+" INDENT_NEXT next-line
+func Some()
+ " next-line
+ let f = x
+endfunc
+" END_INDENT
+
+" START_INDENT
+" INDENT_PREV prev-line
+func Some()
+ let f = x
+" prev-line
+endfunc
+" END_INDENT
diff --git a/runtime/keymap/russian-jcukenwintype.vim b/runtime/keymap/russian-jcukenwintype.vim
new file mode 100644
index 0000000000..25d6286e24
--- /dev/null
+++ b/runtime/keymap/russian-jcukenwintype.vim
@@ -0,0 +1,112 @@
+" Vim Keymap file for russian characters, layout 'jcuken', MS Windows
+" Typewriter variant (slightly incompatible with XFree86 'ru' keymap -
+" makes use of NUMERO SIGN)
+" Useful mainly with utf-8 but may work with other encodings
+
+" Derived from russian-jcuken.vim by Artem Chuprina <ran@ran.pp.ru>
+" Typewriter variant of standart russian layout
+" Maintainer: Denis Proskurin <danwerspb@gmail.com>
+" Last Changed: 2015 May 15
+
+" All characters are given literally, conversion to another encoding (e.g.,
+" UTF-8) should work.
+
+scriptencoding utf-8
+
+let b:keymap_name = "ru"
+
+loadkeymap
+F Ð CYRILLIC CAPITAL LETTER A
+< Б CYRILLIC CAPITAL LETTER BE
+D Ð’ CYRILLIC CAPITAL LETTER VE
+U Г CYRILLIC CAPITAL LETTER GHE
+L Д CYRILLIC CAPITAL LETTER DE
+T Е CYRILLIC CAPITAL LETTER IE
+? Ð CYRILLIC CAPITAL LETTER IO
+: Ж CYRILLIC CAPITAL LETTER ZHE
+P З CYRILLIC CAPITAL LETTER ZE
+B И CYRILLIC CAPITAL LETTER I
+Q Й CYRILLIC CAPITAL LETTER SHORT I
+R К CYRILLIC CAPITAL LETTER KA
+K Л CYRILLIC CAPITAL LETTER EL
+V М CYRILLIC CAPITAL LETTER EM
+Y Ð CYRILLIC CAPITAL LETTER EN
+J О CYRILLIC CAPITAL LETTER O
+G П CYRILLIC CAPITAL LETTER PE
+H Р CYRILLIC CAPITAL LETTER ER
+C С CYRILLIC CAPITAL LETTER ES
+N Т CYRILLIC CAPITAL LETTER TE
+E У CYRILLIC CAPITAL LETTER U
+A Ф CYRILLIC CAPITAL LETTER EF
+{ Х CYRILLIC CAPITAL LETTER HA
+W Ц CYRILLIC CAPITAL LETTER TSE
+X Ч CYRILLIC CAPITAL LETTER CHE
+I Ш CYRILLIC CAPITAL LETTER SHA
+O Щ CYRILLIC CAPITAL LETTER SHCHA
+} Ъ CYRILLIC CAPITAL LETTER HARD SIGN
+S Ы CYRILLIC CAPITAL LETTER YERU
+M Ь CYRILLIC CAPITAL LETTER SOFT SIGN
+\" Э CYRILLIC CAPITAL LETTER E
+> Ю CYRILLIC CAPITAL LETTER YU
+Z Я CYRILLIC CAPITAL LETTER YA
+f а CYRILLIC SMALL LETTER A
+, б CYRILLIC SMALL LETTER BE
+d в CYRILLIC SMALL LETTER VE
+u г CYRILLIC SMALL LETTER GHE
+l д CYRILLIC SMALL LETTER DE
+t е CYRILLIC SMALL LETTER IE
+/ Ñ‘ CYRILLIC SMALL LETTER IO
+; ж CYRILLIC SMALL LETTER ZHE
+p з CYRILLIC SMALL LETTER ZE
+b и CYRILLIC SMALL LETTER I
+q й CYRILLIC SMALL LETTER SHORT I
+r к CYRILLIC SMALL LETTER KA
+k л CYRILLIC SMALL LETTER EL
+v м CYRILLIC SMALL LETTER EM
+y н CYRILLIC SMALL LETTER EN
+j о CYRILLIC SMALL LETTER O
+g п CYRILLIC SMALL LETTER PE
+h р CYRILLIC SMALL LETTER ER
+c Ñ CYRILLIC SMALL LETTER ES
+n Ñ‚ CYRILLIC SMALL LETTER TE
+e у CYRILLIC SMALL LETTER U
+a Ñ„ CYRILLIC SMALL LETTER EF
+[ Ñ… CYRILLIC SMALL LETTER HA
+w ц CYRILLIC SMALL LETTER TSE
+x ч CYRILLIC SMALL LETTER CHE
+i ш CYRILLIC SMALL LETTER SHA
+o щ CYRILLIC SMALL LETTER SHCHA
+] ÑŠ CYRILLIC SMALL LETTER HARD SIGN
+s Ñ‹ CYRILLIC SMALL LETTER YERU
+m ь CYRILLIC SMALL LETTER SOFT SIGN
+' Ñ CYRILLIC SMALL LETTER E
+. ÑŽ CYRILLIC SMALL LETTER YU
+z Ñ CYRILLIC SMALL LETTER YA
+` |
+1 â„–
+2 -
+3 /
+4 "
+5 :
+6 ,
+7 .
+8 _
+9 ?
+0 %
+- !
+= ;
+~ +
+! 1
+@ 2
+# 3
+$ 4
+% 5
+^ 6
+& 7
+* 8
+( 9
+) 0
+_ =
++ \\
+\\ )
+\| (
diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua
index 7cb40ca64d..0f3b908dc1 100644
--- a/runtime/lua/vim/inspect.lua
+++ b/runtime/lua/vim/inspect.lua
@@ -289,7 +289,7 @@ function Inspector:putValue(v)
if tv == 'string' then
self:puts(smartQuote(escape(v)))
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
- tv == 'cdata' or tv == 'ctype' then
+ tv == 'cdata' or tv == 'ctype' or (vim and v == vim.NIL) then
self:puts(tostring(v))
elseif tv == 'table' then
self:putTable(v)
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
new file mode 100644
index 0000000000..82f4fda66f
--- /dev/null
+++ b/runtime/lua/vim/lsp.lua
@@ -0,0 +1,958 @@
+local default_callbacks = require 'vim.lsp.callbacks'
+local log = require 'vim.lsp.log'
+local lsp_rpc = require 'vim.lsp.rpc'
+local protocol = require 'vim.lsp.protocol'
+local util = require 'vim.lsp.util'
+
+local vim = vim
+local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option
+ = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option
+local uv = vim.loop
+local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend
+local validate = vim.validate
+
+local lsp = {
+ protocol = protocol;
+ callbacks = default_callbacks;
+ buf = require'vim.lsp.buf';
+ util = util;
+ -- Allow raw RPC access.
+ rpc = lsp_rpc;
+ -- Export these directly from rpc.
+ rpc_response_error = lsp_rpc.rpc_response_error;
+ -- You probably won't need this directly, since __tostring is set for errors
+ -- by the RPC.
+ -- format_rpc_error = lsp_rpc.format_rpc_error;
+}
+
+-- TODO improve handling of scratch buffers with LSP attached.
+
+local function err_message(...)
+ nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
+ nvim_command("redraw")
+end
+
+local function resolve_bufnr(bufnr)
+ validate { bufnr = { bufnr, 'n', true } }
+ if bufnr == nil or bufnr == 0 then
+ return vim.api.nvim_get_current_buf()
+ end
+ return bufnr
+end
+
+local function is_dir(filename)
+ validate{filename={filename,'s'}}
+ local stat = uv.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+end
+
+-- TODO Use vim.wait when that is available, but provide an alternative for now.
+local wait = vim.wait or function(timeout_ms, condition, interval)
+ validate {
+ timeout_ms = { timeout_ms, 'n' };
+ condition = { condition, 'f' };
+ interval = { interval, 'n', true };
+ }
+ assert(timeout_ms > 0, "timeout_ms must be > 0")
+ local _ = log.debug() and log.debug("wait.fallback", timeout_ms)
+ interval = interval or 200
+ local interval_cmd = "sleep "..interval.."m"
+ local timeout = timeout_ms + uv.now()
+ -- TODO is there a better way to sync this?
+ while true do
+ uv.update_time()
+ if condition() then
+ return 0
+ end
+ if uv.now() >= timeout then
+ return -1
+ end
+ nvim_command(interval_cmd)
+ -- vim.loop.sleep(10)
+ end
+end
+local wait_result_reason = { [-1] = "timeout"; [-2] = "interrupted"; [-3] = "error" }
+
+local valid_encodings = {
+ ["utf-8"] = 'utf-8'; ["utf-16"] = 'utf-16'; ["utf-32"] = 'utf-32';
+ ["utf8"] = 'utf-8'; ["utf16"] = 'utf-16'; ["utf32"] = 'utf-32';
+ UTF8 = 'utf-8'; UTF16 = 'utf-16'; UTF32 = 'utf-32';
+}
+
+local client_index = 0
+local function next_client_id()
+ client_index = client_index + 1
+ return client_index
+end
+-- Tracks all clients created via lsp.start_client
+local active_clients = {}
+local all_buffer_active_clients = {}
+local uninitialized_clients = {}
+
+local function for_each_buffer_client(bufnr, callback)
+ validate {
+ callback = { callback, 'f' };
+ }
+ bufnr = resolve_bufnr(bufnr)
+ local client_ids = all_buffer_active_clients[bufnr]
+ if not client_ids or tbl_isempty(client_ids) then
+ return
+ end
+ for client_id in pairs(client_ids) do
+ local client = active_clients[client_id]
+ if client then
+ callback(client, client_id)
+ end
+ end
+end
+
+-- Error codes to be used with `on_error` from |vim.lsp.start_client|.
+-- Can be used to look up the string from a the number or the number
+-- from the string.
+lsp.client_errors = tbl_extend("error", lsp_rpc.client_errors, vim.tbl_add_reverse_lookup {
+ ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1;
+})
+
+local function validate_encoding(encoding)
+ validate {
+ encoding = { encoding, 's' };
+ }
+ return valid_encodings[encoding:lower()]
+ or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding))
+end
+
+local function validate_command(input)
+ local cmd, cmd_args
+ if type(input) == 'string' then
+ -- Use a shell to execute the command if it is a string.
+ cmd = vim.api.nvim_get_option('shell')
+ cmd_args = {vim.api.nvim_get_option('shellcmdflag'), input}
+ elseif vim.tbl_islist(input) then
+ cmd = input[1]
+ cmd_args = {}
+ -- Don't mutate our input.
+ for i, v in ipairs(input) do
+ assert(type(v) == 'string', "input arguments must be strings")
+ if i > 1 then
+ table.insert(cmd_args, v)
+ end
+ end
+ else
+ error("cmd type must be string or list.")
+ end
+ return cmd, cmd_args
+end
+
+local function optional_validator(fn)
+ return function(v)
+ return v == nil or fn(v)
+ end
+end
+
+local function validate_client_config(config)
+ validate {
+ config = { config, 't' };
+ }
+ validate {
+ root_dir = { config.root_dir, is_dir, "directory" };
+ callbacks = { config.callbacks, "t", true };
+ capabilities = { config.capabilities, "t", true };
+ cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" };
+ cmd_env = { config.cmd_env, "f", true };
+ name = { config.name, 's', true };
+ on_error = { config.on_error, "f", true };
+ on_exit = { config.on_exit, "f", true };
+ on_init = { config.on_init, "f", true };
+ before_init = { config.before_init, "f", true };
+ offset_encoding = { config.offset_encoding, "s", true };
+ }
+ local cmd, cmd_args = validate_command(config.cmd)
+ local offset_encoding = valid_encodings.UTF16
+ if config.offset_encoding then
+ offset_encoding = validate_encoding(config.offset_encoding)
+ end
+ return {
+ cmd = cmd; cmd_args = cmd_args;
+ offset_encoding = offset_encoding;
+ }
+end
+
+local function buf_get_full_text(bufnr)
+ local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), '\n')
+ if nvim_buf_get_option(bufnr, 'eol') then
+ text = text .. '\n'
+ end
+ return text
+end
+
+local function text_document_did_open_handler(bufnr, client)
+ if not client.resolved_capabilities.text_document_open_close then
+ return
+ end
+ if not vim.api.nvim_buf_is_loaded(bufnr) then
+ return
+ end
+ local params = {
+ textDocument = {
+ version = 0;
+ uri = vim.uri_from_bufnr(bufnr);
+ -- TODO make sure our filetypes are compatible with languageId names.
+ languageId = nvim_buf_get_option(bufnr, 'filetype');
+ text = buf_get_full_text(bufnr);
+ }
+ }
+ client.notify('textDocument/didOpen', params)
+end
+
+
+--- Start a client and initialize it.
+-- Its arguments are passed via a configuration object.
+--
+-- Mandatory parameters:
+--
+-- root_dir: {string} specifying the directory where the LSP server will base
+-- as its rootUri on initialization.
+--
+-- cmd: {string} or {list} which is the base command to execute for the LSP. A
+-- string will be run using |'shell'| and a list will be interpreted as a bare
+-- command with arguments passed. This is the same as |jobstart()|.
+--
+-- Optional parameters:
+
+-- cmd_cwd: {string} specifying the directory to launch the `cmd` process. This
+-- is not related to `root_dir`. By default, |getcwd()| is used.
+--
+-- cmd_env: {table} specifying the environment flags to pass to the LSP on
+-- spawn. This can be specified using keys like a map or as a list with `k=v`
+-- pairs or both. Non-string values are coerced to a string.
+-- For example: `{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`.
+--
+-- capabilities: A {table} which will be used instead of
+-- `vim.lsp.protocol.make_client_capabilities()` which contains neovim's
+-- default capabilities and passed to the language server on initialization.
+-- You'll probably want to use make_client_capabilities() and modify the
+-- result.
+-- NOTE:
+-- To send an empty dictionary, you should use
+-- `{[vim.type_idx]=vim.types.dictionary}` Otherwise, it will be encoded as
+-- an array.
+--
+-- callbacks: A {table} of whose keys are language server method names and the
+-- values are `function(err, method, params, client_id)`.
+-- This will be called for:
+-- - notifications from the server, where `err` will always be `nil`
+-- - requests initiated by the server. For these, you can respond by returning
+-- two values: `result, err`. The err must be in the format of an RPC error,
+-- which is `{ code, message, data? }`. You can use |vim.lsp.rpc_response_error()|
+-- to help with this.
+-- - as a callback for requests initiated by the client if the request doesn't
+-- explicitly specify a callback.
+--
+-- init_options: A {table} of values to pass in the initialization request
+-- as `initializationOptions`. See the `initialize` in the LSP spec.
+--
+-- name: A {string} used in log messages. Defaults to {client_id}
+--
+-- offset_encoding: One of 'utf-8', 'utf-16', or 'utf-32' which is the
+-- encoding that the LSP server expects. By default, it is 'utf-16' as
+-- specified in the LSP specification. The client does not verify this
+-- is correct.
+--
+-- on_error(code, ...): A function for handling errors thrown by client
+-- operation. {code} is a number describing the error. Other arguments may be
+-- passed depending on the error kind. @see |vim.lsp.client_errors| for
+-- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a
+-- human understandable string.
+--
+-- before_init(initialize_params, config): A function which is called *before*
+-- the request `initialize` is completed. `initialize_params` contains
+-- the parameters we are sending to the server and `config` is the config that
+-- was passed to `start_client()` for convenience. You can use this to modify
+-- parameters before they are sent.
+--
+-- on_init(client, initialize_result): A function which is called after the
+-- request `initialize` is completed. `initialize_result` contains
+-- `capabilities` and anything else the server may send. For example, `clangd`
+-- sends `result.offsetEncoding` if `capabilities.offsetEncoding` was sent to
+-- it.
+--
+-- on_exit(code, signal, client_id): A function which is called after the
+-- client has exited. code is the exit code of the process, and signal is a
+-- number describing the signal used to terminate (if any).
+--
+-- on_attach(client, bufnr): A function which is called after the client is
+-- attached to a buffer.
+--
+-- trace: 'off' | 'messages' | 'verbose' | nil passed directly to the language
+-- server in the initialize request. Invalid/empty values will default to 'off'
+--
+-- @returns client_id You can use |vim.lsp.get_client_by_id()| to get the
+-- actual client.
+--
+-- NOTE: The client is only available *after* it has been initialized, which
+-- may happen after a small delay (or never if there is an error).
+-- For this reason, you may want to use `on_init` to do any actions once the
+-- client has been initialized.
+function lsp.start_client(config)
+ local cleaned_config = validate_client_config(config)
+ local cmd, cmd_args, offset_encoding = cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding
+
+ local client_id = next_client_id()
+
+ local callbacks = config.callbacks or {}
+ local name = config.name or tostring(client_id)
+ local log_prefix = string.format("LSP[%s]", name)
+
+ local handlers = {}
+
+ local function resolve_callback(method)
+ return callbacks[method] or default_callbacks[method]
+ end
+
+ function handlers.notification(method, params)
+ local _ = log.debug() and log.debug('notification', method, params)
+ local callback = resolve_callback(method)
+ if callback then
+ -- Method name is provided here for convenience.
+ callback(nil, method, params, client_id)
+ end
+ end
+
+ function handlers.server_request(method, params)
+ local _ = log.debug() and log.debug('server_request', method, params)
+ local callback = resolve_callback(method)
+ if callback then
+ local _ = log.debug() and log.debug("server_request: found callback for", method)
+ return callback(nil, method, params, client_id)
+ end
+ local _ = log.debug() and log.debug("server_request: no callback found for", method)
+ return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
+ end
+
+ function handlers.on_error(code, err)
+ local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err })
+ err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err))
+ if config.on_error then
+ local status, usererr = pcall(config.on_error, code, err)
+ if not status then
+ local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr })
+ err_message(log_prefix, ' user on_error failed: ', tostring(usererr))
+ end
+ end
+ end
+
+ function handlers.on_exit(code, signal)
+ active_clients[client_id] = nil
+ uninitialized_clients[client_id] = nil
+ local active_buffers = {}
+ for bufnr, client_ids in pairs(all_buffer_active_clients) do
+ if client_ids[client_id] then
+ table.insert(active_buffers, bufnr)
+ end
+ client_ids[client_id] = nil
+ end
+ -- Buffer level cleanup
+ vim.schedule(function()
+ for _, bufnr in ipairs(active_buffers) do
+ util.buf_clear_diagnostics(bufnr)
+ end
+ end)
+ if config.on_exit then
+ pcall(config.on_exit, code, signal, client_id)
+ end
+ end
+
+ -- Start the RPC client.
+ local rpc = lsp_rpc.start(cmd, cmd_args, handlers, {
+ cwd = config.cmd_cwd;
+ env = config.cmd_env;
+ })
+
+ local client = {
+ id = client_id;
+ name = name;
+ rpc = rpc;
+ offset_encoding = offset_encoding;
+ callbacks = callbacks;
+ config = config;
+ }
+
+ -- Store the uninitialized_clients for cleanup in case we exit before
+ -- initialize finishes.
+ uninitialized_clients[client_id] = client;
+
+ local function initialize()
+ local valid_traces = {
+ off = 'off'; messages = 'messages'; verbose = 'verbose';
+ }
+ local initialize_params = {
+ -- The process Id of the parent process that started the server. Is null if
+ -- the process has not been started by another process. If the parent
+ -- process is not alive then the server should exit (see exit notification)
+ -- its process.
+ processId = uv.getpid();
+ -- The rootPath of the workspace. Is null if no folder is open.
+ --
+ -- @deprecated in favour of rootUri.
+ rootPath = nil;
+ -- The rootUri of the workspace. Is null if no folder is open. If both
+ -- `rootPath` and `rootUri` are set `rootUri` wins.
+ rootUri = vim.uri_from_fname(config.root_dir);
+ -- User provided initialization options.
+ initializationOptions = config.init_options;
+ -- The capabilities provided by the client (editor or tool)
+ capabilities = config.capabilities or protocol.make_client_capabilities();
+ -- The initial trace setting. If omitted trace is disabled ('off').
+ -- trace = 'off' | 'messages' | 'verbose';
+ trace = valid_traces[config.trace] or 'off';
+ -- The workspace folders configured in the client when the server starts.
+ -- This property is only available if the client supports workspace folders.
+ -- It can be `null` if the client supports workspace folders but none are
+ -- configured.
+ --
+ -- Since 3.6.0
+ -- workspaceFolders?: WorkspaceFolder[] | null;
+ -- export interface WorkspaceFolder {
+ -- -- The associated URI for this workspace folder.
+ -- uri
+ -- -- The name of the workspace folder. Used to refer to this
+ -- -- workspace folder in the user interface.
+ -- name
+ -- }
+ workspaceFolders = nil;
+ }
+ if config.before_init then
+ -- TODO(ashkan) handle errors here.
+ pcall(config.before_init, initialize_params, config)
+ end
+ local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params)
+ rpc.request('initialize', initialize_params, function(init_err, result)
+ assert(not init_err, tostring(init_err))
+ assert(result, "server sent empty result")
+ rpc.notify('initialized', {[vim.type_idx]=vim.types.dictionary})
+ client.initialized = true
+ uninitialized_clients[client_id] = nil
+ client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities")
+ -- These are the cleaned up capabilities we use for dynamically deciding
+ -- when to send certain events to clients.
+ client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities)
+ if config.on_init then
+ local status, err = pcall(config.on_init, client, result)
+ if not status then
+ pcall(handlers.on_error, lsp.client_errors.ON_INIT_CALLBACK_ERROR, err)
+ end
+ end
+ local _ = log.debug() and log.debug(log_prefix, "server_capabilities", client.server_capabilities)
+ local _ = log.info() and log.info(log_prefix, "initialized", { resolved_capabilities = client.resolved_capabilities })
+
+ -- Only assign after initialized.
+ active_clients[client_id] = client
+ -- If we had been registered before we start, then send didOpen This can
+ -- happen if we attach to buffers before initialize finishes or if
+ -- someone restarts a client.
+ for bufnr, client_ids in pairs(all_buffer_active_clients) do
+ if client_ids[client_id] then
+ client._on_attach(bufnr)
+ end
+ end
+ end)
+ end
+
+ local function unsupported_method(method)
+ local msg = "server doesn't support "..method
+ local _ = log.warn() and log.warn(msg)
+ err_message(msg)
+ return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg)
+ end
+
+ --- Checks capabilities before rpc.request-ing.
+ function client.request(method, params, callback)
+ if not callback then
+ callback = resolve_callback(method)
+ or error("not found: request callback for client "..client.name)
+ end
+ local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback)
+ -- TODO keep these checks or just let it go anyway?
+ if (not client.resolved_capabilities.hover and method == 'textDocument/hover')
+ or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp')
+ or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition')
+ or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation')
+ then
+ callback(unsupported_method(method), method, nil, client_id)
+ return
+ end
+ return rpc.request(method, params, function(err, result)
+ callback(err, method, result, client_id)
+ end)
+ end
+
+ function client.notify(...)
+ return rpc.notify(...)
+ end
+
+ function client.cancel_request(id)
+ validate{id = {id, 'n'}}
+ return rpc.notify("$/cancelRequest", { id = id })
+ end
+
+ -- Track this so that we can escalate automatically if we've alredy tried a
+ -- graceful shutdown
+ local tried_graceful_shutdown = false
+ function client.stop(force)
+ local handle = rpc.handle
+ if handle:is_closing() then
+ return
+ end
+ if force or (not client.initialized) or tried_graceful_shutdown then
+ handle:kill(15)
+ return
+ end
+ tried_graceful_shutdown = true
+ -- Sending a signal after a process has exited is acceptable.
+ rpc.request('shutdown', nil, function(err, _)
+ if err == nil then
+ rpc.notify('exit')
+ else
+ -- If there was an error in the shutdown request, then term to be safe.
+ handle:kill(15)
+ end
+ end)
+ end
+
+ function client.is_stopped()
+ return rpc.handle:is_closing()
+ end
+
+ function client._on_attach(bufnr)
+ text_document_did_open_handler(bufnr, client)
+ if config.on_attach then
+ -- TODO(ashkan) handle errors.
+ pcall(config.on_attach, client, bufnr)
+ end
+ end
+
+ initialize()
+
+ return client_id
+end
+
+local function once(fn)
+ local value
+ return function(...)
+ if not value then value = fn(...) end
+ return value
+ end
+end
+
+local text_document_did_change_handler
+do
+ local encoding_index = { ["utf-8"] = 1; ["utf-16"] = 2; ["utf-32"] = 3; }
+ text_document_did_change_handler = function(_, bufnr, changedtick,
+ firstline, lastline, new_lastline, old_byte_size, old_utf32_size,
+ old_utf16_size)
+ local _ = log.debug() and log.debug("on_lines", bufnr, changedtick, firstline,
+ lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size, nvim_buf_get_lines(bufnr, firstline, new_lastline, true))
+ if old_byte_size == 0 then
+ return
+ end
+ -- Don't do anything if there are no clients attached.
+ if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then
+ return
+ end
+ -- Lazy initialize these because clients may not even need them.
+ local incremental_changes = once(function(client)
+ local size_index = encoding_index[client.offset_encoding]
+ local length = select(size_index, old_byte_size, old_utf16_size, old_utf32_size)
+ local lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
+ -- This is necessary because we are specifying the full line including the
+ -- newline in range. Therefore, we must replace the newline as well.
+ if #lines > 0 then
+ table.insert(lines, '')
+ end
+ return {
+ range = {
+ start = { line = firstline, character = 0 };
+ ["end"] = { line = lastline, character = 0 };
+ };
+ rangeLength = length;
+ text = table.concat(lines, '\n');
+ };
+ end)
+ local full_changes = once(function()
+ return {
+ text = buf_get_full_text(bufnr);
+ };
+ end)
+ local uri = vim.uri_from_bufnr(bufnr)
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ local text_document_did_change = client.resolved_capabilities.text_document_did_change
+ local changes
+ if text_document_did_change == protocol.TextDocumentSyncKind.None then
+ return
+ --[=[ TODO(ashkan) there seem to be problems with the byte_sizes sent by
+ -- neovim right now so only send the full content for now. In general, we
+ -- can assume that servers *will* support both versions anyway, as there
+ -- is no way to specify the sync capability by the client.
+ -- See https://github.com/palantir/python-language-server/commit/cfd6675bc10d5e8dbc50fc50f90e4a37b7178821#diff-f68667852a14e9f761f6ebf07ba02fc8 for an example of pyls handling both.
+ --]=]
+ elseif true or text_document_did_change == protocol.TextDocumentSyncKind.Full then
+ changes = full_changes(client)
+ elseif text_document_did_change == protocol.TextDocumentSyncKind.Incremental then
+ changes = incremental_changes(client)
+ end
+ client.notify("textDocument/didChange", {
+ textDocument = {
+ uri = uri;
+ version = changedtick;
+ };
+ contentChanges = { changes; }
+ })
+ end)
+ end
+end
+
+-- Buffer lifecycle handler for textDocument/didSave
+function lsp._text_document_did_save_handler(bufnr)
+ bufnr = resolve_bufnr(bufnr)
+ local uri = vim.uri_from_bufnr(bufnr)
+ local text = once(function()
+ return table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n')
+ end)
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ if client.resolved_capabilities.text_document_save then
+ local included_text
+ if client.resolved_capabilities.text_document_save_include_text then
+ included_text = text()
+ end
+ client.notify('textDocument/didSave', {
+ textDocument = {
+ uri = uri;
+ text = included_text;
+ }
+ })
+ end
+ end)
+end
+
+-- Implements the textDocument/did* notifications required to track a buffer
+-- for any language server.
+-- @param bufnr [number] buffer handle or 0 for current
+-- @param client_id [number] the client id
+function lsp.buf_attach_client(bufnr, client_id)
+ validate {
+ bufnr = {bufnr, 'n', true};
+ client_id = {client_id, 'n'};
+ }
+ bufnr = resolve_bufnr(bufnr)
+ local buffer_client_ids = all_buffer_active_clients[bufnr]
+ -- This is our first time attaching to this buffer.
+ if not buffer_client_ids then
+ buffer_client_ids = {}
+ all_buffer_active_clients[bufnr] = buffer_client_ids
+
+ local uri = vim.uri_from_bufnr(bufnr)
+ nvim_command(string.format("autocmd BufWritePost <buffer=%d> lua vim.lsp._text_document_did_save_handler(0)", bufnr))
+ -- First time, so attach and set up stuff.
+ vim.api.nvim_buf_attach(bufnr, false, {
+ on_lines = text_document_did_change_handler;
+ on_detach = function()
+ local params = { textDocument = { uri = uri; } }
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ if client.resolved_capabilities.text_document_open_close then
+ client.notify('textDocument/didClose', params)
+ end
+ end)
+ all_buffer_active_clients[bufnr] = nil
+ end;
+ -- TODO if we know all of the potential clients ahead of time, then we
+ -- could conditionally set this.
+ -- utf_sizes = size_index > 1;
+ utf_sizes = true;
+ })
+ end
+ if buffer_client_ids[client_id] then return end
+ -- This is our first time attaching this client to this buffer.
+ buffer_client_ids[client_id] = true
+
+ local client = active_clients[client_id]
+ -- Send didOpen for the client if it is initialized. If it isn't initialized
+ -- then it will send didOpen on initialize.
+ if client then
+ client._on_attach(bufnr)
+ end
+ return true
+end
+
+-- Check if a buffer is attached for a particular client.
+-- @param bufnr [number] buffer handle or 0 for current
+-- @param client_id [number] the client id
+function lsp.buf_is_attached(bufnr, client_id)
+ return (all_buffer_active_clients[bufnr] or {})[client_id] == true
+end
+
+-- Look up an active client by its id, returns nil if it is not yet initialized
+-- or is not a valid id.
+-- @param client_id number the client id.
+function lsp.get_client_by_id(client_id)
+ return active_clients[client_id]
+end
+
+-- Stop a client by its id, optionally with force.
+-- You can also use the `stop()` function on a client if you already have
+-- access to it.
+-- By default, it will just ask the server to shutdown without force.
+-- If you request to stop a client which has previously been requested to shutdown,
+-- it will automatically force shutdown.
+-- @param client_id number the client id.
+-- @param force boolean (optional) whether to use force or request shutdown
+function lsp.stop_client(client_id, force)
+ local client
+ client = active_clients[client_id]
+ if client then
+ client.stop(force)
+ return
+ end
+ client = uninitialized_clients[client_id]
+ if client then
+ client.stop(true)
+ end
+end
+
+-- Returns a list of all the active clients.
+function lsp.get_active_clients()
+ return vim.tbl_values(active_clients)
+end
+
+-- Stop all the clients, optionally with force.
+-- You can also use the `stop()` function on a client if you already have
+-- access to it.
+-- By default, it will just ask the server to shutdown without force.
+-- If you request to stop a client which has previously been requested to shutdown,
+-- it will automatically force shutdown.
+-- @param force boolean (optional) whether to use force or request shutdown
+function lsp.stop_all_clients(force)
+ for _, client in pairs(uninitialized_clients) do
+ client.stop(true)
+ end
+ for _, client in pairs(active_clients) do
+ client.stop(force)
+ end
+end
+
+function lsp._vim_exit_handler()
+ log.info("exit_handler", active_clients)
+ for _, client in pairs(uninitialized_clients) do
+ client.stop(true)
+ end
+ -- TODO handle v:dying differently?
+ if tbl_isempty(active_clients) then
+ return
+ end
+ for _, client in pairs(active_clients) do
+ client.stop()
+ end
+ local wait_result = wait(500, function() return tbl_isempty(active_clients) end, 50)
+ if wait_result ~= 0 then
+ for _, client in pairs(active_clients) do
+ client.stop(true)
+ end
+ end
+end
+
+nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()")
+
+---
+--- Buffer level client functions.
+---
+
+--- Send a request to a server and return the response
+-- @param bufnr [number] Buffer handle or 0 for current.
+-- @param method [string] Request method name
+-- @param params [table|nil] Parameters to send to the server
+-- @param callback [function|nil] Request callback (or uses the client's callbacks)
+--
+-- @returns: client_request_ids, cancel_all_requests
+function lsp.buf_request(bufnr, method, params, callback)
+ validate {
+ bufnr = { bufnr, 'n', true };
+ method = { method, 's' };
+ callback = { callback, 'f', true };
+ }
+ local client_request_ids = {}
+ for_each_buffer_client(bufnr, function(client, client_id)
+ local request_success, request_id = client.request(method, params, callback)
+
+ -- This could only fail if the client shut down in the time since we looked
+ -- it up and we did the request, which should be rare.
+ if request_success then
+ client_request_ids[client_id] = request_id
+ end
+ end)
+
+ local function cancel_all_requests()
+ for client_id, request_id in pairs(client_request_ids) do
+ local client = active_clients[client_id]
+ client.cancel_request(request_id)
+ end
+ end
+
+ return client_request_ids, cancel_all_requests
+end
+
+--- Send a request to a server and wait for the response.
+-- @param bufnr [number] Buffer handle or 0 for current.
+-- @param method [string] Request method name
+-- @param params [string] Parameters to send to the server
+-- @param timeout_ms [number|100] Maximum ms to wait for a result
+--
+-- @returns: The table of {[client_id] = request_result}
+function lsp.buf_request_sync(bufnr, method, params, timeout_ms)
+ local request_results = {}
+ local result_count = 0
+ local function callback(err, _method, result, client_id)
+ request_results[client_id] = { error = err, result = result }
+ result_count = result_count + 1
+ end
+ local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, callback)
+ local expected_result_count = 0
+ for _ in pairs(client_request_ids) do
+ expected_result_count = expected_result_count + 1
+ end
+ local wait_result = wait(timeout_ms or 100, function()
+ return result_count >= expected_result_count
+ end, 10)
+ if wait_result ~= 0 then
+ cancel()
+ return nil, wait_result_reason[wait_result]
+ end
+ return request_results
+end
+
+--- Send a notification to a server
+-- @param bufnr [number] (optional): The number of the buffer
+-- @param method [string]: Name of the request method
+-- @param params [string]: Arguments to send to the server
+--
+-- @returns nil
+function lsp.buf_notify(bufnr, method, params)
+ validate {
+ bufnr = { bufnr, 'n', true };
+ method = { method, 's' };
+ }
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ client.rpc.notify(method, params)
+ end)
+end
+
+--- Function which can be called to generate omnifunc compatible completion.
+function lsp.omnifunc(findstart, base)
+ local _ = log.debug() and log.debug("omnifunc.findstart", { findstart = findstart, base = base })
+
+ local bufnr = resolve_bufnr()
+ local has_buffer_clients = not tbl_isempty(all_buffer_active_clients[bufnr] or {})
+ if not has_buffer_clients then
+ if findstart == 1 then
+ return -1
+ else
+ return {}
+ end
+ end
+
+ if findstart == 1 then
+ return vim.fn.col('.')
+ else
+ local pos = vim.api.nvim_win_get_cursor(0)
+ local line = assert(nvim_buf_get_lines(bufnr, pos[1]-1, pos[1], false)[1])
+ local _ = log.trace() and log.trace("omnifunc.line", pos, line)
+ local line_to_cursor = line:sub(1, pos[2]+1)
+ local _ = log.trace() and log.trace("omnifunc.line_to_cursor", line_to_cursor)
+ local params = {
+ textDocument = {
+ uri = vim.uri_from_bufnr(bufnr);
+ };
+ position = {
+ -- 0-indexed for both line and character
+ line = pos[1] - 1,
+ character = vim.str_utfindex(line, pos[2]),
+ };
+ -- The completion context. This is only available if the client specifies
+ -- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true`
+ -- context = nil or {
+ -- triggerKind = protocol.CompletionTriggerKind.Invoked;
+ -- triggerCharacter = nil or "";
+ -- };
+ }
+ -- TODO handle timeout error differently? Like via an error?
+ local client_responses = lsp.buf_request_sync(bufnr, 'textDocument/completion', params) or {}
+ local matches = {}
+ for _, response in pairs(client_responses) do
+ -- TODO how to handle errors?
+ if not response.error then
+ local data = response.result
+ local completion_items = util.text_document_completion_list_to_complete_items(data or {}, line_to_cursor)
+ local _ = log.trace() and log.trace("omnifunc.completion_items", completion_items)
+ vim.list_extend(matches, completion_items)
+ end
+ end
+ return matches
+ end
+end
+
+function lsp.client_is_stopped(client_id)
+ return active_clients[client_id] == nil
+end
+
+---
+--- Miscellaneous utilities.
+---
+
+-- Retrieve a map from client_id to client of all active buffer clients.
+-- @param bufnr [number] (optional): buffer handle or 0 for current
+function lsp.buf_get_clients(bufnr)
+ bufnr = resolve_bufnr(bufnr)
+ local result = {}
+ for_each_buffer_client(bufnr, function(client, client_id)
+ result[client_id] = client
+ end)
+ return result
+end
+
+-- Print some debug information about the current buffer clients.
+-- The output of this function should not be relied upon and may change.
+function lsp.buf_print_debug_info(bufnr)
+ print(vim.inspect(lsp.buf_get_clients(bufnr)))
+end
+
+-- Print some debug information about all LSP related things.
+-- The output of this function should not be relied upon and may change.
+function lsp.print_debug_info()
+ print(vim.inspect({ clients = active_clients }))
+end
+
+-- Log level dictionary with reverse lookup as well.
+--
+-- Can be used to lookup the number from the name or the
+-- name from the number.
+-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
+-- Level numbers begin with 'trace' at 0
+lsp.log_levels = log.levels
+
+-- Set the log level for lsp logging.
+-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
+-- Level numbers begin with 'trace' at 0
+-- @param level [number|string] the case insensitive level name or number @see |vim.lsp.log_levels|
+function lsp.set_log_level(level)
+ if type(level) == 'string' or type(level) == 'number' then
+ log.set_level(level)
+ else
+ error(string.format("Invalid log level: %q", level))
+ end
+end
+
+-- Return the path of the logfile used by the LSP client.
+function lsp.get_log_path()
+ return log.get_filename()
+end
+
+return lsp
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
new file mode 100644
index 0000000000..a6a05fb095
--- /dev/null
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -0,0 +1,136 @@
+local vim = vim
+local validate = vim.validate
+local api = vim.api
+local vfn = vim.fn
+local util = require 'vim.lsp.util'
+local list_extend = vim.list_extend
+
+local M = {}
+
+local function ok_or_nil(status, ...)
+ if not status then return end
+ return ...
+end
+local function npcall(fn, ...)
+ return ok_or_nil(pcall(fn, ...))
+end
+
+local function request(method, params, callback)
+ validate {
+ method = {method, 's'};
+ callback = {callback, 'f', true};
+ }
+ return vim.lsp.buf_request(0, method, params, callback)
+end
+
+function M.hover()
+ local params = util.make_position_params()
+ request('textDocument/hover', params)
+end
+
+function M.peek_definition()
+ local params = util.make_position_params()
+ request('textDocument/peekDefinition', params)
+end
+
+
+function M.declaration()
+ local params = util.make_position_params()
+ request('textDocument/declaration', params)
+end
+
+function M.definition()
+ local params = util.make_position_params()
+ request('textDocument/definition', params)
+end
+
+function M.type_definition()
+ local params = util.make_position_params()
+ request('textDocument/typeDefinition', params)
+end
+
+function M.implementation()
+ local params = util.make_position_params()
+ request('textDocument/implementation', params)
+end
+
+function M.signature_help()
+ local params = util.make_position_params()
+ request('textDocument/signatureHelp', params)
+end
+
+-- TODO(ashkan) ?
+function M.completion(context)
+ local params = util.make_position_params()
+ params.context = context
+ return request('textDocument/completion', params)
+end
+
+function M.formatting(options)
+ validate { options = {options, 't', true} }
+ options = vim.tbl_extend('keep', options or {}, {
+ tabSize = vim.bo.tabstop;
+ insertSpaces = vim.bo.expandtab;
+ })
+ local params = {
+ textDocument = { uri = vim.uri_from_bufnr(0) };
+ options = options;
+ }
+ return request('textDocument/formatting', params)
+end
+
+function M.range_formatting(options, start_pos, end_pos)
+ validate {
+ options = {options, 't', true};
+ start_pos = {start_pos, 't', true};
+ end_pos = {end_pos, 't', true};
+ }
+ options = vim.tbl_extend('keep', options or {}, {
+ tabSize = vim.bo.tabstop;
+ insertSpaces = vim.bo.expandtab;
+ })
+ local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<'))
+ local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>'))
+ -- convert to 0-index
+ A[1] = A[1] - 1
+ B[1] = B[1] - 1
+ -- account for encoding.
+ if A[2] > 0 then
+ A = {A[1], util.character_offset(0, A[1], A[2])}
+ end
+ if B[2] > 0 then
+ B = {B[1], util.character_offset(0, B[1], B[2])}
+ end
+ local params = {
+ textDocument = { uri = vim.uri_from_bufnr(0) };
+ range = {
+ start = { line = A[1]; character = A[2]; };
+ ["end"] = { line = B[1]; character = B[2]; };
+ };
+ options = options;
+ }
+ return request('textDocument/rangeFormatting', params)
+end
+
+function M.rename(new_name)
+ -- TODO(ashkan) use prepareRename
+ -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position.
+ local params = util.make_position_params()
+ new_name = new_name or npcall(vfn.input, "New Name: ")
+ if not (new_name and #new_name > 0) then return end
+ params.newName = new_name
+ request('textDocument/rename', params)
+end
+
+function M.references(context)
+ validate { context = { context, 't', true } }
+ local params = util.make_position_params()
+ params.context = context or {
+ includeDeclaration = true;
+ }
+ params[vim.type_idx] = vim.types.dictionary
+ request('textDocument/references', params)
+end
+
+return M
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua
new file mode 100644
index 0000000000..4fc3f74519
--- /dev/null
+++ b/runtime/lua/vim/lsp/callbacks.lua
@@ -0,0 +1,223 @@
+local log = require 'vim.lsp.log'
+local protocol = require 'vim.lsp.protocol'
+local util = require 'vim.lsp.util'
+local vim = vim
+local api = vim.api
+
+local M = {}
+
+local function err_message(...)
+ api.nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
+ api.nvim_command("redraw")
+end
+
+M['workspace/applyEdit'] = function(_, _, workspace_edit)
+ if not workspace_edit then return end
+ -- TODO(ashkan) Do something more with label?
+ if workspace_edit.label then
+ print("Workspace edit", workspace_edit.label)
+ end
+ util.apply_workspace_edit(workspace_edit.edit)
+end
+
+M['textDocument/publishDiagnostics'] = function(_, _, result)
+ if not result then return end
+ local uri = result.uri
+ local bufnr = vim.uri_to_bufnr(uri)
+ if not bufnr then
+ err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri)
+ return
+ end
+ util.buf_clear_diagnostics(bufnr)
+ util.buf_diagnostics_save_positions(bufnr, result.diagnostics)
+ util.buf_diagnostics_underline(bufnr, result.diagnostics)
+ util.buf_diagnostics_virtual_text(bufnr, result.diagnostics)
+ -- util.set_loclist(result.diagnostics)
+end
+
+M['textDocument/references'] = function(_, _, result)
+ if not result then return end
+ util.set_qflist(result)
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+end
+
+M['textDocument/rename'] = function(_, _, result)
+ if not result then return end
+ util.apply_workspace_edit(result)
+end
+
+M['textDocument/rangeFormatting'] = function(_, _, result)
+ if not result then return end
+ util.apply_text_edits(result)
+end
+
+M['textDocument/formatting'] = function(_, _, result)
+ if not result then return end
+ util.apply_text_edits(result)
+end
+
+M['textDocument/completion'] = function(_, _, result)
+ if vim.tbl_isempty(result or {}) then return end
+ local row, col = unpack(api.nvim_win_get_cursor(0))
+ local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1])
+ local line_to_cursor = line:sub(col+1)
+
+ local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor)
+ vim.fn.complete(col, matches)
+end
+
+M['textDocument/hover'] = function(_, method, result)
+ util.focusable_preview(method, function()
+ if not (result and result.contents) then
+ return { 'No information available' }
+ end
+ local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
+ markdown_lines = util.trim_empty_lines(markdown_lines)
+ if vim.tbl_isempty(markdown_lines) then
+ return { 'No information available' }
+ end
+ return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines)
+ end)
+end
+
+local function location_callback(_, method, result)
+ if result == nil or vim.tbl_isempty(result) then
+ local _ = log.info() and log.info(method, 'No location found')
+ return nil
+ end
+ util.jump_to_location(result[1])
+ if #result > 1 then
+ util.set_qflist(result)
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+ end
+end
+
+M['textDocument/declaration'] = location_callback
+M['textDocument/definition'] = location_callback
+M['textDocument/typeDefinition'] = location_callback
+M['textDocument/implementation'] = location_callback
+
+--- Convert SignatureHelp response to preview contents.
+-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp
+local function signature_help_to_preview_contents(input)
+ if not input.signatures then
+ return
+ end
+ --The active signature. If omitted or the value lies outside the range of
+ --`signatures` the value defaults to zero or is ignored if `signatures.length
+ --=== 0`. Whenever possible implementors should make an active decision about
+ --the active signature and shouldn't rely on a default value.
+ local contents = {}
+ local active_signature = input.activeSignature or 0
+ -- If the activeSignature is not inside the valid range, then clip it.
+ if active_signature >= #input.signatures then
+ active_signature = 0
+ end
+ local signature = input.signatures[active_signature + 1]
+ if not signature then
+ return
+ end
+ vim.list_extend(contents, vim.split(signature.label, '\n', true))
+ if signature.documentation then
+ util.convert_input_to_markdown_lines(signature.documentation, contents)
+ end
+ if input.parameters then
+ local active_parameter = input.activeParameter or 0
+ -- If the activeParameter is not inside the valid range, then clip it.
+ if active_parameter >= #input.parameters then
+ active_parameter = 0
+ end
+ local parameter = signature.parameters and signature.parameters[active_parameter]
+ if parameter then
+ --[=[
+ --Represents a parameter of a callable-signature. A parameter can
+ --have a label and a doc-comment.
+ interface ParameterInformation {
+ --The label of this parameter information.
+ --
+ --Either a string or an inclusive start and exclusive end offsets within its containing
+ --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
+ --string representation as `Position` and `Range` does.
+ --
+ --*Note*: a label of type string should be a substring of its containing signature label.
+ --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
+ label: string | [number, number];
+ --The human-readable doc-comment of this parameter. Will be shown
+ --in the UI but can be omitted.
+ documentation?: string | MarkupContent;
+ }
+ --]=]
+ -- TODO highlight parameter
+ if parameter.documentation then
+ util.convert_input_to_markdown_lines(parameter.documentation, contents)
+ end
+ end
+ end
+ return contents
+end
+
+M['textDocument/signatureHelp'] = function(_, method, result)
+ util.focusable_preview(method, function()
+ if not (result and result.signatures and result.signatures[1]) then
+ return { 'No signature available' }
+ end
+ -- TODO show popup when signatures is empty?
+ local lines = signature_help_to_preview_contents(result)
+ lines = util.trim_empty_lines(lines)
+ if vim.tbl_isempty(lines) then
+ return { 'No signature available' }
+ end
+ return lines, util.try_trim_markdown_code_blocks(lines)
+ end)
+end
+
+M['textDocument/peekDefinition'] = function(_, _, result, _)
+ if not (result and result[1]) then return end
+ local loc = result[1]
+ local bufnr = vim.uri_to_bufnr(loc.uri) or error("not found: "..tostring(loc.uri))
+ local start = loc.range.start
+ local finish = loc.range["end"]
+ util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 })
+ local headbuf = util.open_floating_preview({"Peek:"}, nil, {
+ offset_y = -(finish.line - start.line);
+ width = finish.character - start.character + 2;
+ })
+ -- TODO(ashkan) change highlight group?
+ api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1)
+end
+
+local function log_message(_, _, result, client_id)
+ local message_type = result.type
+ local message = result.message
+ local client = vim.lsp.get_client_by_id(client_id)
+ local client_name = client and client.name or string.format("id=%d", client_id)
+ if not client then
+ err_message("LSP[", client_name, "] client has shut down after sending the message")
+ end
+ if message_type == protocol.MessageType.Error then
+ err_message("LSP[", client_name, "] ", message)
+ else
+ local message_type_name = protocol.MessageType[message_type]
+ api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message))
+ end
+ return result
+end
+
+M['window/showMessage'] = log_message
+M['window/logMessage'] = log_message
+
+-- Add boilerplate error validation and logging for all of these.
+for k, fn in pairs(M) do
+ M[k] = function(err, method, params, client_id)
+ local _ = log.debug() and log.debug('default_callback', method, { params = params, client_id = client_id, err = err })
+ if err then
+ error(tostring(err))
+ end
+ return fn(err, method, params, client_id)
+ end
+end
+
+return M
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
new file mode 100644
index 0000000000..974eaae38c
--- /dev/null
+++ b/runtime/lua/vim/lsp/log.lua
@@ -0,0 +1,95 @@
+-- Logger for language client plugin.
+
+local log = {}
+
+-- Log level dictionary with reverse lookup as well.
+--
+-- Can be used to lookup the number from the name or the name from the number.
+-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
+-- Level numbers begin with 'trace' at 0
+log.levels = {
+ TRACE = 0;
+ DEBUG = 1;
+ INFO = 2;
+ WARN = 3;
+ ERROR = 4;
+ -- FATAL = 4;
+}
+
+-- Default log level is warn.
+local current_log_level = log.levels.WARN
+local log_date_format = "%FT%H:%M:%SZ%z"
+
+do
+ local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/"
+ local function path_join(...)
+ return table.concat(vim.tbl_flatten{...}, path_sep)
+ end
+ local logfilename = path_join(vim.fn.stdpath('data'), 'vim-lsp.log')
+
+ --- Return the log filename.
+ function log.get_filename()
+ return logfilename
+ end
+
+ vim.fn.mkdir(vim.fn.stdpath('data'), "p")
+ local logfile = assert(io.open(logfilename, "a+"))
+ for level, levelnr in pairs(log.levels) do
+ -- Also export the log level on the root object.
+ log[level] = levelnr
+ -- Set the lowercase name as the main use function.
+ -- If called without arguments, it will check whether the log level is
+ -- greater than or equal to this one. When called with arguments, it will
+ -- log at that level (if applicable, it is checked either way).
+ --
+ -- Recommended usage:
+ -- ```
+ -- local _ = log.warn() and log.warn("123")
+ -- ```
+ --
+ -- This way you can avoid string allocations if the log level isn't high enough.
+ log[level:lower()] = function(...)
+ local argc = select("#", ...)
+ if levelnr < current_log_level then return false end
+ if argc == 0 then return true end
+ local info = debug.getinfo(2, "Sl")
+ local fileinfo = string.format("%s:%s", info.short_src, info.currentline)
+ local parts = { table.concat({"[", level, "]", os.date(log_date_format), "]", fileinfo, "]"}, " ") }
+ for i = 1, argc do
+ local arg = select(i, ...)
+ if arg == nil then
+ table.insert(parts, "nil")
+ else
+ table.insert(parts, vim.inspect(arg, {newline=''}))
+ end
+ end
+ logfile:write(table.concat(parts, '\t'), "\n")
+ logfile:flush()
+ end
+ end
+ -- Add some space to make it easier to distinguish different neovim runs.
+ logfile:write("\n")
+end
+
+-- This is put here on purpose after the loop above so that it doesn't
+-- interfere with iterating the levels
+vim.tbl_add_reverse_lookup(log.levels)
+
+function log.set_level(level)
+ if type(level) == 'string' then
+ current_log_level = assert(log.levels[level:upper()], string.format("Invalid log level: %q", level))
+ else
+ assert(type(level) == 'number', "level must be a number or string")
+ assert(log.levels[level], string.format("Invalid log level: %d", level))
+ current_log_level = level
+ end
+end
+
+-- Return whether the level is sufficient for logging.
+-- @param level number log level
+function log.should_log(level)
+ return level >= current_log_level
+end
+
+return log
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
new file mode 100644
index 0000000000..ead90cc75a
--- /dev/null
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -0,0 +1,922 @@
+-- Protocol for the Microsoft Language Server Protocol (mslsp)
+
+local protocol = {}
+
+local function ifnil(a, b)
+ if a == nil then return b end
+ return a
+end
+
+
+--[=[
+-- Useful for interfacing with:
+-- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md
+function transform_schema_comments()
+ nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]]
+ nvim.command [[silent! '<,'>s/^\(\s*\) \* \=\(.*\)/\1--\2/]]
+end
+function transform_schema_to_table()
+ transform_schema_comments()
+ nvim.command [[silent! '<,'>s/: \S\+//]]
+ nvim.command [[silent! '<,'>s/export const //]]
+ nvim.command [[silent! '<,'>s/export namespace \(\S*\)\s*{/protocol.\1 = {/]]
+ nvim.command [[silent! '<,'>s/namespace \(\S*\)\s*{/protocol.\1 = {/]]
+end
+--]=]
+
+local constants = {
+ DiagnosticSeverity = {
+ -- Reports an error.
+ Error = 1;
+ -- Reports a warning.
+ Warning = 2;
+ -- Reports an information.
+ Information = 3;
+ -- Reports a hint.
+ Hint = 4;
+ };
+
+ MessageType = {
+ -- An error message.
+ Error = 1;
+ -- A warning message.
+ Warning = 2;
+ -- An information message.
+ Info = 3;
+ -- A log message.
+ Log = 4;
+ };
+
+ -- The file event type.
+ FileChangeType = {
+ -- The file got created.
+ Created = 1;
+ -- The file got changed.
+ Changed = 2;
+ -- The file got deleted.
+ Deleted = 3;
+ };
+
+ -- The kind of a completion entry.
+ CompletionItemKind = {
+ Text = 1;
+ Method = 2;
+ Function = 3;
+ Constructor = 4;
+ Field = 5;
+ Variable = 6;
+ Class = 7;
+ Interface = 8;
+ Module = 9;
+ Property = 10;
+ Unit = 11;
+ Value = 12;
+ Enum = 13;
+ Keyword = 14;
+ Snippet = 15;
+ Color = 16;
+ File = 17;
+ Reference = 18;
+ Folder = 19;
+ EnumMember = 20;
+ Constant = 21;
+ Struct = 22;
+ Event = 23;
+ Operator = 24;
+ TypeParameter = 25;
+ };
+
+ -- How a completion was triggered
+ CompletionTriggerKind = {
+ -- Completion was triggered by typing an identifier (24x7 code
+ -- complete), manual invocation (e.g Ctrl+Space) or via API.
+ Invoked = 1;
+ -- Completion was triggered by a trigger character specified by
+ -- the `triggerCharacters` properties of the `CompletionRegistrationOptions`.
+ TriggerCharacter = 2;
+ -- Completion was re-triggered as the current completion list is incomplete.
+ TriggerForIncompleteCompletions = 3;
+ };
+
+ -- A document highlight kind.
+ DocumentHighlightKind = {
+ -- A textual occurrence.
+ Text = 1;
+ -- Read-access of a symbol, like reading a variable.
+ Read = 2;
+ -- Write-access of a symbol, like writing to a variable.
+ Write = 3;
+ };
+
+ -- A symbol kind.
+ SymbolKind = {
+ File = 1;
+ Module = 2;
+ Namespace = 3;
+ Package = 4;
+ Class = 5;
+ Method = 6;
+ Property = 7;
+ Field = 8;
+ Constructor = 9;
+ Enum = 10;
+ Interface = 11;
+ Function = 12;
+ Variable = 13;
+ Constant = 14;
+ String = 15;
+ Number = 16;
+ Boolean = 17;
+ Array = 18;
+ Object = 19;
+ Key = 20;
+ Null = 21;
+ EnumMember = 22;
+ Struct = 23;
+ Event = 24;
+ Operator = 25;
+ TypeParameter = 26;
+ };
+
+ -- Represents reasons why a text document is saved.
+ TextDocumentSaveReason = {
+ -- Manually triggered, e.g. by the user pressing save, by starting debugging,
+ -- or by an API call.
+ Manual = 1;
+ -- Automatic after a delay.
+ AfterDelay = 2;
+ -- When the editor lost focus.
+ FocusOut = 3;
+ };
+
+ ErrorCodes = {
+ -- Defined by JSON RPC
+ ParseError = -32700;
+ InvalidRequest = -32600;
+ MethodNotFound = -32601;
+ InvalidParams = -32602;
+ InternalError = -32603;
+ serverErrorStart = -32099;
+ serverErrorEnd = -32000;
+ ServerNotInitialized = -32002;
+ UnknownErrorCode = -32001;
+ -- Defined by the protocol.
+ RequestCancelled = -32800;
+ ContentModified = -32801;
+ };
+
+ -- Describes the content type that a client supports in various
+ -- result literals like `Hover`, `ParameterInfo` or `CompletionItem`.
+ --
+ -- Please note that `MarkupKinds` must not start with a `$`. This kinds
+ -- are reserved for internal usage.
+ MarkupKind = {
+ -- Plain text is supported as a content format
+ PlainText = 'plaintext';
+ -- Markdown is supported as a content format
+ Markdown = 'markdown';
+ };
+
+ ResourceOperationKind = {
+ -- Supports creating new files and folders.
+ Create = 'create';
+ -- Supports renaming existing files and folders.
+ Rename = 'rename';
+ -- Supports deleting existing files and folders.
+ Delete = 'delete';
+ };
+
+ FailureHandlingKind = {
+ -- Applying the workspace change is simply aborted if one of the changes provided
+ -- fails. All operations executed before the failing operation stay executed.
+ Abort = 'abort';
+ -- All operations are executed transactionally. That means they either all
+ -- succeed or no changes at all are applied to the workspace.
+ Transactional = 'transactional';
+ -- If the workspace edit contains only textual file changes they are executed transactionally.
+ -- If resource changes (create, rename or delete file) are part of the change the failure
+ -- handling strategy is abort.
+ TextOnlyTransactional = 'textOnlyTransactional';
+ -- The client tries to undo the operations already executed. But there is no
+ -- guarantee that this succeeds.
+ Undo = 'undo';
+ };
+
+ -- Known error codes for an `InitializeError`;
+ InitializeError = {
+ -- If the protocol version provided by the client can't be handled by the server.
+ -- @deprecated This initialize error got replaced by client capabilities. There is
+ -- no version handshake in version 3.0x
+ unknownProtocolVersion = 1;
+ };
+
+ -- Defines how the host (editor) should sync document changes to the language server.
+ TextDocumentSyncKind = {
+ -- Documents should not be synced at all.
+ None = 0;
+ -- Documents are synced by always sending the full content
+ -- of the document.
+ Full = 1;
+ -- Documents are synced by sending the full content on open.
+ -- After that only incremental updates to the document are
+ -- send.
+ Incremental = 2;
+ };
+
+ WatchKind = {
+ -- Interested in create events.
+ Create = 1;
+ -- Interested in change events
+ Change = 2;
+ -- Interested in delete events
+ Delete = 4;
+ };
+
+ -- Defines whether the insert text in a completion item should be interpreted as
+ -- plain text or a snippet.
+ InsertTextFormat = {
+ -- The primary text to be inserted is treated as a plain string.
+ PlainText = 1;
+ -- The primary text to be inserted is treated as a snippet.
+ --
+ -- A snippet can define tab stops and placeholders with `$1`, `$2`
+ -- and `${3:foo};`. `$0` defines the final tab stop, it defaults to
+ -- the end of the snippet. Placeholders with equal identifiers are linked,
+ -- that is typing in one will update others too.
+ Snippet = 2;
+ };
+
+ -- A set of predefined code action kinds
+ CodeActionKind = {
+ -- Empty kind.
+ Empty = '';
+ -- Base kind for quickfix actions
+ QuickFix = 'quickfix';
+ -- Base kind for refactoring actions
+ Refactor = 'refactor';
+ -- Base kind for refactoring extraction actions
+ --
+ -- Example extract actions:
+ --
+ -- - Extract method
+ -- - Extract function
+ -- - Extract variable
+ -- - Extract interface from class
+ -- - ...
+ RefactorExtract = 'refactor.extract';
+ -- Base kind for refactoring inline actions
+ --
+ -- Example inline actions:
+ --
+ -- - Inline function
+ -- - Inline variable
+ -- - Inline constant
+ -- - ...
+ RefactorInline = 'refactor.inline';
+ -- Base kind for refactoring rewrite actions
+ --
+ -- Example rewrite actions:
+ --
+ -- - Convert JavaScript function to class
+ -- - Add or remove parameter
+ -- - Encapsulate field
+ -- - Make method static
+ -- - Move method to base class
+ -- - ...
+ RefactorRewrite = 'refactor.rewrite';
+ -- Base kind for source actions
+ --
+ -- Source code actions apply to the entire file.
+ Source = 'source';
+ -- Base kind for an organize imports source action
+ SourceOrganizeImports = 'source.organizeImports';
+ };
+}
+
+for k, v in pairs(constants) do
+ vim.tbl_add_reverse_lookup(v)
+ protocol[k] = v
+end
+
+--[=[
+--Text document specific client capabilities.
+export interface TextDocumentClientCapabilities {
+ synchronization?: {
+ --Whether text document synchronization supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports sending will save notifications.
+ willSave?: boolean;
+ --The client supports sending a will save request and
+ --waits for a response providing text edits which will
+ --be applied to the document before it is saved.
+ willSaveWaitUntil?: boolean;
+ --The client supports did save notifications.
+ didSave?: boolean;
+ }
+ --Capabilities specific to the `textDocument/completion`
+ completion?: {
+ --Whether completion supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports the following `CompletionItem` specific
+ --capabilities.
+ completionItem?: {
+ --The client supports snippets as insert text.
+ --
+ --A snippet can define tab stops and placeholders with `$1`, `$2`
+ --and `${3:foo}`. `$0` defines the final tab stop, it defaults to
+ --the end of the snippet. Placeholders with equal identifiers are linked,
+ --that is typing in one will update others too.
+ snippetSupport?: boolean;
+ --The client supports commit characters on a completion item.
+ commitCharactersSupport?: boolean
+ --The client supports the following content formats for the documentation
+ --property. The order describes the preferred format of the client.
+ documentationFormat?: MarkupKind[];
+ --The client supports the deprecated property on a completion item.
+ deprecatedSupport?: boolean;
+ --The client supports the preselect property on a completion item.
+ preselectSupport?: boolean;
+ }
+ completionItemKind?: {
+ --The completion item kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ --
+ --If this property is not present the client only supports
+ --the completion items kinds from `Text` to `Reference` as defined in
+ --the initial version of the protocol.
+ valueSet?: CompletionItemKind[];
+ },
+ --The client supports to send additional context information for a
+ --`textDocument/completion` request.
+ contextSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/hover`
+ hover?: {
+ --Whether hover supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports the follow content formats for the content
+ --property. The order describes the preferred format of the client.
+ contentFormat?: MarkupKind[];
+ };
+ --Capabilities specific to the `textDocument/signatureHelp`
+ signatureHelp?: {
+ --Whether signature help supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports the following `SignatureInformation`
+ --specific properties.
+ signatureInformation?: {
+ --The client supports the follow content formats for the documentation
+ --property. The order describes the preferred format of the client.
+ documentationFormat?: MarkupKind[];
+ --Client capabilities specific to parameter information.
+ parameterInformation?: {
+ --The client supports processing label offsets instead of a
+ --simple label string.
+ --
+ --Since 3.14.0
+ labelOffsetSupport?: boolean;
+ }
+ };
+ };
+ --Capabilities specific to the `textDocument/references`
+ references?: {
+ --Whether references supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentHighlight`
+ documentHighlight?: {
+ --Whether document highlight supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentSymbol`
+ documentSymbol?: {
+ --Whether document symbol supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --Specific capabilities for the `SymbolKind`.
+ symbolKind?: {
+ --The symbol kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ --
+ --If this property is not present the client only supports
+ --the symbol kinds from `File` to `Array` as defined in
+ --the initial version of the protocol.
+ valueSet?: SymbolKind[];
+ }
+ --The client supports hierarchical document symbols.
+ hierarchicalDocumentSymbolSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/formatting`
+ formatting?: {
+ --Whether formatting supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/rangeFormatting`
+ rangeFormatting?: {
+ --Whether range formatting supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/onTypeFormatting`
+ onTypeFormatting?: {
+ --Whether on type formatting supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/declaration`
+ declaration?: {
+ --Whether declaration supports dynamic registration. If this is set to `true`
+ --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of declaration links.
+ --
+ --Since 3.14.0
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/definition`.
+ --
+ --Since 3.14.0
+ definition?: {
+ --Whether definition supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of definition links.
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/typeDefinition`
+ --
+ --Since 3.6.0
+ typeDefinition?: {
+ --Whether typeDefinition supports dynamic registration. If this is set to `true`
+ --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of definition links.
+ --
+ --Since 3.14.0
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/implementation`.
+ --
+ --Since 3.6.0
+ implementation?: {
+ --Whether implementation supports dynamic registration. If this is set to `true`
+ --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of definition links.
+ --
+ --Since 3.14.0
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/codeAction`
+ codeAction?: {
+ --Whether code action supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client support code action literals as a valid
+ --response of the `textDocument/codeAction` request.
+ --
+ --Since 3.8.0
+ codeActionLiteralSupport?: {
+ --The code action kind is support with the following value
+ --set.
+ codeActionKind: {
+ --The code action kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ valueSet: CodeActionKind[];
+ };
+ };
+ };
+ --Capabilities specific to the `textDocument/codeLens`
+ codeLens?: {
+ --Whether code lens supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentLink`
+ documentLink?: {
+ --Whether document link supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentColor` and the
+ --`textDocument/colorPresentation` request.
+ --
+ --Since 3.6.0
+ colorProvider?: {
+ --Whether colorProvider supports dynamic registration. If this is set to `true`
+ --the client supports the new `(ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ }
+ --Capabilities specific to the `textDocument/rename`
+ rename?: {
+ --Whether rename supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports testing for validity of rename operations
+ --before execution.
+ prepareSupport?: boolean;
+ };
+ --Capabilities specific to `textDocument/publishDiagnostics`.
+ publishDiagnostics?: {
+ --Whether the clients accepts diagnostics with related information.
+ relatedInformation?: boolean;
+ };
+ --Capabilities specific to `textDocument/foldingRange` requests.
+ --
+ --Since 3.10.0
+ foldingRange?: {
+ --Whether implementation supports dynamic registration for folding range providers. If this is set to `true`
+ --the client supports the new `(FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The maximum number of folding ranges that the client prefers to receive per document. The value serves as a
+ --hint, servers are free to follow the limit.
+ rangeLimit?: number;
+ --If set, the client signals that it only supports folding complete lines. If set, client will
+ --ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange.
+ lineFoldingOnly?: boolean;
+ };
+}
+--]=]
+
+--[=[
+--Workspace specific client capabilities.
+export interface WorkspaceClientCapabilities {
+ --The client supports applying batch edits to the workspace by supporting
+ --the request 'workspace/applyEdit'
+ applyEdit?: boolean;
+ --Capabilities specific to `WorkspaceEdit`s
+ workspaceEdit?: {
+ --The client supports versioned document changes in `WorkspaceEdit`s
+ documentChanges?: boolean;
+ --The resource operations the client supports. Clients should at least
+ --support 'create', 'rename' and 'delete' files and folders.
+ resourceOperations?: ResourceOperationKind[];
+ --The failure handling strategy of a client if applying the workspace edit
+ --fails.
+ failureHandling?: FailureHandlingKind;
+ };
+ --Capabilities specific to the `workspace/didChangeConfiguration` notification.
+ didChangeConfiguration?: {
+ --Did change configuration notification supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `workspace/didChangeWatchedFiles` notification.
+ didChangeWatchedFiles?: {
+ --Did change watched files notification supports dynamic registration. Please note
+ --that the current protocol doesn't support static configuration for file changes
+ --from the server side.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `workspace/symbol` request.
+ symbol?: {
+ --Symbol request supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --Specific capabilities for the `SymbolKind` in the `workspace/symbol` request.
+ symbolKind?: {
+ --The symbol kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ --
+ --If this property is not present the client only supports
+ --the symbol kinds from `File` to `Array` as defined in
+ --the initial version of the protocol.
+ valueSet?: SymbolKind[];
+ }
+ };
+ --Capabilities specific to the `workspace/executeCommand` request.
+ executeCommand?: {
+ --Execute command supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --The client has support for workspace folders.
+ --
+ --Since 3.6.0
+ workspaceFolders?: boolean;
+ --The client supports `workspace/configuration` requests.
+ --
+ --Since 3.6.0
+ configuration?: boolean;
+}
+--]=]
+
+function protocol.make_client_capabilities()
+ return {
+ textDocument = {
+ synchronization = {
+ dynamicRegistration = false;
+
+ -- TODO(ashkan) Send textDocument/willSave before saving (BufWritePre)
+ willSave = false;
+
+ -- TODO(ashkan) Implement textDocument/willSaveWaitUntil
+ willSaveWaitUntil = false;
+
+ -- Send textDocument/didSave after saving (BufWritePost)
+ didSave = true;
+ };
+ completion = {
+ dynamicRegistration = false;
+ completionItem = {
+
+ -- TODO(tjdevries): Is it possible to implement this in plain lua?
+ snippetSupport = false;
+ commitCharactersSupport = false;
+ preselectSupport = false;
+ deprecatedSupport = false;
+ documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ };
+ completionItemKind = {
+ valueSet = (function()
+ local res = {}
+ for k in pairs(protocol.CompletionItemKind) do
+ if type(k) == 'number' then table.insert(res, k) end
+ end
+ return res
+ end)();
+ };
+
+ -- TODO(tjdevries): Implement this
+ contextSupport = false;
+ };
+ hover = {
+ dynamicRegistration = false;
+ contentFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ };
+ signatureHelp = {
+ dynamicRegistration = false;
+ signatureInformation = {
+ documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ -- parameterInformation = {
+ -- labelOffsetSupport = false;
+ -- };
+ };
+ };
+ references = {
+ dynamicRegistration = false;
+ };
+ documentHighlight = {
+ dynamicRegistration = false
+ };
+ -- documentSymbol = {
+ -- dynamicRegistration = false;
+ -- symbolKind = {
+ -- valueSet = (function()
+ -- local res = {}
+ -- for k in pairs(protocol.SymbolKind) do
+ -- if type(k) == 'string' then table.insert(res, k) end
+ -- end
+ -- return res
+ -- end)();
+ -- };
+ -- hierarchicalDocumentSymbolSupport = false;
+ -- };
+ };
+ workspace = nil;
+ experimental = nil;
+ }
+end
+
+--[=[
+export interface DocumentFilter {
+ --A language id, like `typescript`.
+ language?: string;
+ --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
+ scheme?: string;
+ --A glob pattern, like `*.{ts,js}`.
+ --
+ --Glob patterns can have the following syntax:
+ --- `*` to match one or more characters in a path segment
+ --- `?` to match on one character in a path segment
+ --- `**` to match any number of path segments, including none
+ --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
+ --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+ --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+ pattern?: string;
+}
+--]=]
+
+--[[
+--Static registration options to be returned in the initialize request.
+interface StaticRegistrationOptions {
+ --The id used to register the request. The id can be used to deregister
+ --the request again. See also Registration#id.
+ id?: string;
+}
+
+export interface DocumentFilter {
+ --A language id, like `typescript`.
+ language?: string;
+ --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
+ scheme?: string;
+ --A glob pattern, like `*.{ts,js}`.
+ --
+ --Glob patterns can have the following syntax:
+ --- `*` to match one or more characters in a path segment
+ --- `?` to match on one character in a path segment
+ --- `**` to match any number of path segments, including none
+ --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
+ --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+ --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+ pattern?: string;
+}
+export type DocumentSelector = DocumentFilter[];
+export interface TextDocumentRegistrationOptions {
+ --A document selector to identify the scope of the registration. If set to null
+ --the document selector provided on the client side will be used.
+ documentSelector: DocumentSelector | null;
+}
+
+--Code Action options.
+export interface CodeActionOptions {
+ --CodeActionKinds that this server may return.
+ --
+ --The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server
+ --may list out every specific kind they provide.
+ codeActionKinds?: CodeActionKind[];
+}
+
+interface ServerCapabilities {
+ --Defines how text documents are synced. Is either a detailed structure defining each notification or
+ --for backwards compatibility the TextDocumentSyncKind number. If omitted it defaults to `TextDocumentSyncKind.None`.
+ textDocumentSync?: TextDocumentSyncOptions | number;
+ --The server provides hover support.
+ hoverProvider?: boolean;
+ --The server provides completion support.
+ completionProvider?: CompletionOptions;
+ --The server provides signature help support.
+ signatureHelpProvider?: SignatureHelpOptions;
+ --The server provides goto definition support.
+ definitionProvider?: boolean;
+ --The server provides Goto Type Definition support.
+ --
+ --Since 3.6.0
+ typeDefinitionProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides Goto Implementation support.
+ --
+ --Since 3.6.0
+ implementationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides find references support.
+ referencesProvider?: boolean;
+ --The server provides document highlight support.
+ documentHighlightProvider?: boolean;
+ --The server provides document symbol support.
+ documentSymbolProvider?: boolean;
+ --The server provides workspace symbol support.
+ workspaceSymbolProvider?: boolean;
+ --The server provides code actions. The `CodeActionOptions` return type is only
+ --valid if the client signals code action literal support via the property
+ --`textDocument.codeAction.codeActionLiteralSupport`.
+ codeActionProvider?: boolean | CodeActionOptions;
+ --The server provides code lens.
+ codeLensProvider?: CodeLensOptions;
+ --The server provides document formatting.
+ documentFormattingProvider?: boolean;
+ --The server provides document range formatting.
+ documentRangeFormattingProvider?: boolean;
+ --The server provides document formatting on typing.
+ documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions;
+ --The server provides rename support. RenameOptions may only be
+ --specified if the client states that it supports
+ --`prepareSupport` in its initial `initialize` request.
+ renameProvider?: boolean | RenameOptions;
+ --The server provides document link support.
+ documentLinkProvider?: DocumentLinkOptions;
+ --The server provides color provider support.
+ --
+ --Since 3.6.0
+ colorProvider?: boolean | ColorProviderOptions | (ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides folding provider support.
+ --
+ --Since 3.10.0
+ foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides go to declaration support.
+ --
+ --Since 3.14.0
+ declarationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides execute command support.
+ executeCommandProvider?: ExecuteCommandOptions;
+ --Workspace specific server capabilities
+ workspace?: {
+ --The server supports workspace folder.
+ --
+ --Since 3.6.0
+ workspaceFolders?: {
+ * The server has support for workspace folders
+ supported?: boolean;
+ * Whether the server wants to receive workspace folder
+ * change notifications.
+ *
+ * If a strings is provided the string is treated as a ID
+ * under which the notification is registered on the client
+ * side. The ID can be used to unregister for these events
+ * using the `client/unregisterCapability` request.
+ changeNotifications?: string | boolean;
+ }
+ }
+ --Experimental server capabilities.
+ experimental?: any;
+}
+--]]
+function protocol.resolve_capabilities(server_capabilities)
+ local general_properties = {}
+ local text_document_sync_properties
+ do
+ local TextDocumentSyncKind = protocol.TextDocumentSyncKind
+ local textDocumentSync = server_capabilities.textDocumentSync
+ if textDocumentSync == nil then
+ -- Defaults if omitted.
+ text_document_sync_properties = {
+ text_document_open_close = false;
+ text_document_did_change = TextDocumentSyncKind.None;
+-- text_document_did_change = false;
+ text_document_will_save = false;
+ text_document_will_save_wait_until = false;
+ text_document_save = false;
+ text_document_save_include_text = false;
+ }
+ elseif type(textDocumentSync) == 'number' then
+ -- Backwards compatibility
+ if not TextDocumentSyncKind[textDocumentSync] then
+ return nil, "Invalid server TextDocumentSyncKind for textDocumentSync"
+ end
+ text_document_sync_properties = {
+ text_document_open_close = true;
+ text_document_did_change = textDocumentSync;
+ text_document_will_save = false;
+ text_document_will_save_wait_until = false;
+ text_document_save = false;
+ text_document_save_include_text = false;
+ }
+ elseif type(textDocumentSync) == 'table' then
+ text_document_sync_properties = {
+ text_document_open_close = ifnil(textDocumentSync.openClose, false);
+ text_document_did_change = ifnil(textDocumentSync.change, TextDocumentSyncKind.None);
+ text_document_will_save = ifnil(textDocumentSync.willSave, false);
+ text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false);
+ text_document_save = ifnil(textDocumentSync.save, false);
+ text_document_save_include_text = ifnil(textDocumentSync.save and textDocumentSync.save.includeText, false);
+ }
+ else
+ return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync))
+ end
+ end
+ general_properties.hover = server_capabilities.hoverProvider or false
+ general_properties.goto_definition = server_capabilities.definitionProvider or false
+ general_properties.find_references = server_capabilities.referencesProvider or false
+ general_properties.document_highlight = server_capabilities.documentHighlightProvider or false
+ general_properties.document_symbol = server_capabilities.documentSymbolProvider or false
+ general_properties.workspace_symbol = server_capabilities.workspaceSymbolProvider or false
+ general_properties.document_formatting = server_capabilities.documentFormattingProvider or false
+ general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false
+
+ if server_capabilities.codeActionProvider == nil then
+ general_properties.code_action = false
+ elseif type(server_capabilities.codeActionProvider) == 'boolean' then
+ general_properties.code_action = server_capabilities.codeActionProvider
+ elseif type(server_capabilities.codeActionProvider) == 'table' then
+ -- TODO(ashkan) support CodeActionKind
+ general_properties.code_action = false
+ else
+ error("The server sent invalid codeActionProvider")
+ end
+
+ if server_capabilities.implementationProvider == nil then
+ general_properties.implementation = false
+ elseif type(server_capabilities.implementationProvider) == 'boolean' then
+ general_properties.implementation = server_capabilities.implementationProvider
+ elseif type(server_capabilities.implementationProvider) == 'table' then
+ -- TODO(ashkan) support more detailed implementation options.
+ general_properties.implementation = false
+ else
+ error("The server sent invalid implementationProvider")
+ end
+
+ local signature_help_properties
+ if server_capabilities.signatureHelpProvider == nil then
+ signature_help_properties = {
+ signature_help = false;
+ signature_help_trigger_characters = {};
+ }
+ elseif type(server_capabilities.signatureHelpProvider) == 'table' then
+ signature_help_properties = {
+ signature_help = true;
+ -- The characters that trigger signature help automatically.
+ signature_help_trigger_characters = server_capabilities.signatureHelpProvider.triggerCharacters or {};
+ }
+ else
+ error("The server sent invalid signatureHelpProvider")
+ end
+
+ return vim.tbl_extend("error"
+ , text_document_sync_properties
+ , signature_help_properties
+ , general_properties
+ )
+end
+
+return protocol
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
new file mode 100644
index 0000000000..a558f66a42
--- /dev/null
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -0,0 +1,452 @@
+local uv = vim.loop
+local log = require('vim.lsp.log')
+local protocol = require('vim.lsp.protocol')
+local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap
+
+-- TODO replace with a better implementation.
+local function json_encode(data)
+ local status, result = pcall(vim.fn.json_encode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+local function json_decode(data)
+ local status, result = pcall(vim.fn.json_decode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+
+local function is_dir(filename)
+ local stat = vim.loop.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+end
+
+local NIL = vim.NIL
+local function convert_NIL(v)
+ if v == NIL then return nil end
+ return v
+end
+
+-- If a dictionary is passed in, turn it into a list of string of "k=v"
+-- Accepts a table which can be composed of k=v strings or map-like
+-- specification, such as:
+--
+-- ```
+-- {
+-- "PRODUCTION=false";
+-- "PATH=/usr/bin/";
+-- PORT = 123;
+-- HOST = "0.0.0.0";
+-- }
+-- ```
+--
+-- Non-string values will be cast with `tostring`
+local function force_env_list(final_env)
+ if final_env then
+ local env = final_env
+ final_env = {}
+ for k,v in pairs(env) do
+ -- If it's passed in as a dict, then convert to list of "k=v"
+ if type(k) == "string" then
+ table.insert(final_env, k..'='..tostring(v))
+ elseif type(v) == 'string' then
+ table.insert(final_env, v)
+ else
+ -- TODO is this right or should I exception here?
+ -- Try to coerce other values to string.
+ table.insert(final_env, tostring(v))
+ end
+ end
+ return final_env
+ end
+end
+
+local function format_message_with_content_length(encoded_message)
+ return table.concat {
+ 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n';
+ encoded_message;
+ }
+end
+
+--- Parse an LSP Message's header
+-- @param header: The header to parse.
+local function parse_headers(header)
+ if type(header) ~= 'string' then
+ return nil
+ end
+ local headers = {}
+ for line in vim.gsplit(header, '\r\n', true) do
+ if line == '' then
+ break
+ end
+ local key, value = line:match("^%s*(%S+)%s*:%s*(.+)%s*$")
+ if key then
+ key = key:lower():gsub('%-', '_')
+ headers[key] = value
+ else
+ local _ = log.error() and log.error("invalid header line %q", line)
+ error(string.format("invalid header line %q", line))
+ end
+ end
+ headers.content_length = tonumber(headers.content_length)
+ or error(string.format("Content-Length not found in headers. %q", header))
+ return headers
+end
+
+-- This is the start of any possible header patterns. The gsub converts it to a
+-- case insensitive pattern.
+local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end)
+
+local function request_parser_loop()
+ local buffer = ''
+ while true do
+ -- A message can only be complete if it has a double CRLF and also the full
+ -- payload, so first let's check for the CRLFs
+ local start, finish = buffer:find('\r\n\r\n', 1, true)
+ -- Start parsing the headers
+ if start then
+ -- This is a workaround for servers sending initial garbage before
+ -- sending headers, such as if a bash script sends stdout. It assumes
+ -- that we know all of the headers ahead of time. At this moment, the
+ -- only valid headers start with "Content-*", so that's the thing we will
+ -- be searching for.
+ -- TODO(ashkan) I'd like to remove this, but it seems permanent :(
+ local buffer_start = buffer:find(header_start_pattern)
+ local headers = parse_headers(buffer:sub(buffer_start, start-1))
+ buffer = buffer:sub(finish+1)
+ local content_length = headers.content_length
+ -- Keep waiting for data until we have enough.
+ while #buffer < content_length do
+ buffer = buffer..(coroutine.yield()
+ or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
+ end
+ local body = buffer:sub(1, content_length)
+ buffer = buffer:sub(content_length + 1)
+ -- Yield our data.
+ buffer = buffer..(coroutine.yield(headers, body)
+ or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
+ else
+ -- Get more data since we don't have enough.
+ buffer = buffer..(coroutine.yield()
+ or error("Expected more data for the header. The server may have died.")) -- TODO hmm.
+ end
+ end
+end
+
+local client_errors = vim.tbl_add_reverse_lookup {
+ INVALID_SERVER_MESSAGE = 1;
+ INVALID_SERVER_JSON = 2;
+ NO_RESULT_CALLBACK_FOUND = 3;
+ READ_ERROR = 4;
+ NOTIFICATION_HANDLER_ERROR = 5;
+ SERVER_REQUEST_HANDLER_ERROR = 6;
+ SERVER_RESULT_CALLBACK_ERROR = 7;
+}
+
+local function format_rpc_error(err)
+ validate {
+ err = { err, 't' };
+ }
+ local code_name = assert(protocol.ErrorCodes[err.code], "err.code is invalid")
+ local message_parts = {"RPC", code_name}
+ if err.message then
+ table.insert(message_parts, "message = ")
+ table.insert(message_parts, string.format("%q", err.message))
+ end
+ if err.data then
+ table.insert(message_parts, "data = ")
+ table.insert(message_parts, vim.inspect(err.data))
+ end
+ return table.concat(message_parts, ' ')
+end
+
+local function rpc_response_error(code, message, data)
+ -- TODO should this error or just pick a sane error (like InternalError)?
+ local code_name = assert(protocol.ErrorCodes[code], 'Invalid rpc error code')
+ return setmetatable({
+ code = code;
+ message = message or code_name;
+ data = data;
+ }, {
+ __tostring = format_rpc_error;
+ })
+end
+
+local default_handlers = {}
+function default_handlers.notification(method, params)
+ local _ = log.debug() and log.debug('notification', method, params)
+end
+function default_handlers.server_request(method, params)
+ local _ = log.debug() and log.debug('server_request', method, params)
+ return nil, rpc_response_error(protocol.ErrorCodes.MethodNotFound)
+end
+function default_handlers.on_exit(code, signal)
+ local _ = log.info() and log.info("client exit", { code = code, signal = signal })
+end
+function default_handlers.on_error(code, err)
+ local _ = log.error() and log.error('client_error:', client_errors[code], err)
+end
+
+--- Create and start an RPC client.
+-- @param cmd [
+local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_params)
+ local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params})
+ validate {
+ cmd = { cmd, 's' };
+ cmd_args = { cmd_args, 't' };
+ handlers = { handlers, 't', true };
+ }
+
+ if not (vim.fn.executable(cmd) == 1) then
+ error(string.format("The given command %q is not executable.", cmd))
+ end
+ if handlers then
+ local user_handlers = handlers
+ handlers = {}
+ for handle_name, default_handler in pairs(default_handlers) do
+ local user_handler = user_handlers[handle_name]
+ if user_handler then
+ if type(user_handler) ~= 'function' then
+ error(string.format("handler.%s must be a function", handle_name))
+ end
+ -- server_request is wrapped elsewhere.
+ if not (handle_name == 'server_request'
+ or handle_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
+ then
+ user_handler = schedule_wrap(user_handler)
+ end
+ handlers[handle_name] = user_handler
+ else
+ handlers[handle_name] = default_handler
+ end
+ end
+ else
+ handlers = default_handlers
+ end
+
+ local stdin = uv.new_pipe(false)
+ local stdout = uv.new_pipe(false)
+ local stderr = uv.new_pipe(false)
+
+ local message_index = 0
+ local message_callbacks = {}
+
+ local handle, pid
+ do
+ local function onexit(code, signal)
+ stdin:close()
+ stdout:close()
+ stderr:close()
+ handle:close()
+ -- Make sure that message_callbacks can be gc'd.
+ message_callbacks = nil
+ handlers.on_exit(code, signal)
+ end
+ local spawn_params = {
+ args = cmd_args;
+ stdio = {stdin, stdout, stderr};
+ }
+ if extra_spawn_params then
+ spawn_params.cwd = extra_spawn_params.cwd
+ if spawn_params.cwd then
+ assert(is_dir(spawn_params.cwd), "cwd must be a directory")
+ end
+ spawn_params.env = force_env_list(extra_spawn_params.env)
+ end
+ handle, pid = uv.spawn(cmd, spawn_params, onexit)
+ end
+
+ local function encode_and_send(payload)
+ local _ = log.debug() and log.debug("rpc.send.payload", payload)
+ if handle:is_closing() then return false end
+ -- TODO(ashkan) remove this once we have a Lua json_encode
+ schedule(function()
+ local encoded = assert(json_encode(payload))
+ stdin:write(format_message_with_content_length(encoded))
+ end)
+ return true
+ end
+
+ local function send_notification(method, params)
+ local _ = log.debug() and log.debug("rpc.notify", method, params)
+ return encode_and_send {
+ jsonrpc = "2.0";
+ method = method;
+ params = params;
+ }
+ end
+
+ local function send_response(request_id, err, result)
+ return encode_and_send {
+ id = request_id;
+ jsonrpc = "2.0";
+ error = err;
+ result = result;
+ }
+ end
+
+ local function send_request(method, params, callback)
+ validate {
+ callback = { callback, 'f' };
+ }
+ message_index = message_index + 1
+ local message_id = message_index
+ local result = encode_and_send {
+ id = message_id;
+ jsonrpc = "2.0";
+ method = method;
+ params = params;
+ }
+ if result then
+ message_callbacks[message_id] = schedule_wrap(callback)
+ return result, message_id
+ else
+ return false
+ end
+ end
+
+ stderr:read_start(function(_err, chunk)
+ if chunk then
+ local _ = log.error() and log.error("rpc", cmd, "stderr", chunk)
+ end
+ end)
+
+ local function on_error(errkind, ...)
+ assert(client_errors[errkind])
+ -- TODO what to do if this fails?
+ pcall(handlers.on_error, errkind, ...)
+ end
+ local function pcall_handler(errkind, status, head, ...)
+ if not status then
+ on_error(errkind, head, ...)
+ return status, head
+ end
+ return status, head, ...
+ end
+ local function try_call(errkind, fn, ...)
+ return pcall_handler(errkind, pcall(fn, ...))
+ end
+
+ -- TODO periodically check message_callbacks for old requests past a certain
+ -- time and log them. This would require storing the timestamp. I could call
+ -- them with an error then, perhaps.
+
+ local function handle_body(body)
+ local decoded, err = json_decode(body)
+ if not decoded then
+ on_error(client_errors.INVALID_SERVER_JSON, err)
+ return
+ end
+ local _ = log.debug() and log.debug("decoded", decoded)
+
+ if type(decoded.method) == 'string' and decoded.id then
+ -- Server Request
+ decoded.params = convert_NIL(decoded.params)
+ -- Schedule here so that the users functions don't trigger an error and
+ -- we can still use the result.
+ schedule(function()
+ local status, result
+ status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR,
+ handlers.server_request, decoded.method, decoded.params)
+ local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err })
+ if status then
+ if not (result or err) then
+ -- TODO this can be a problem if `null` is sent for result. needs vim.NIL
+ error(string.format("method %q: either a result or an error must be sent to the server in response", decoded.method))
+ end
+ if err then
+ assert(type(err) == 'table', "err must be a table. Use rpc_response_error to help format errors.")
+ local code_name = assert(protocol.ErrorCodes[err.code], "Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.")
+ err.message = err.message or code_name
+ end
+ else
+ -- On an exception, result will contain the error message.
+ err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
+ result = nil
+ end
+ send_response(decoded.id, err, result)
+ end)
+ -- This works because we are expecting vim.NIL here
+ elseif decoded.id and (decoded.result or decoded.error) then
+ -- Server Result
+ decoded.error = convert_NIL(decoded.error)
+ decoded.result = convert_NIL(decoded.result)
+
+ -- We sent a number, so we expect a number.
+ local result_id = tonumber(decoded.id)
+ local callback = message_callbacks[result_id]
+ if callback then
+ message_callbacks[result_id] = nil
+ validate {
+ callback = { callback, 'f' };
+ }
+ if decoded.error then
+ decoded.error = setmetatable(decoded.error, {
+ __tostring = format_rpc_error;
+ })
+ end
+ try_call(client_errors.SERVER_RESULT_CALLBACK_ERROR,
+ callback, decoded.error, decoded.result)
+ else
+ on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
+ local _ = log.error() and log.error("No callback found for server response id "..result_id)
+ end
+ elseif type(decoded.method) == 'string' then
+ -- Notification
+ decoded.params = convert_NIL(decoded.params)
+ try_call(client_errors.NOTIFICATION_HANDLER_ERROR,
+ handlers.notification, decoded.method, decoded.params)
+ else
+ -- Invalid server message
+ on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
+ end
+ end
+ -- TODO(ashkan) remove this once we have a Lua json_decode
+ handle_body = schedule_wrap(handle_body)
+
+ local request_parser = coroutine.wrap(request_parser_loop)
+ request_parser()
+ stdout:read_start(function(err, chunk)
+ if err then
+ -- TODO better handling. Can these be intermittent errors?
+ on_error(client_errors.READ_ERROR, err)
+ return
+ end
+ -- This should signal that we are done reading from the client.
+ if not chunk then return end
+ -- Flush anything in the parser by looping until we don't get a result
+ -- anymore.
+ while true do
+ local headers, body = request_parser(chunk)
+ -- If we successfully parsed, then handle the response.
+ if headers then
+ handle_body(body)
+ -- Set chunk to empty so that we can call request_parser to get
+ -- anything existing in the parser to flush.
+ chunk = ''
+ else
+ break
+ end
+ end
+ end)
+
+ return {
+ pid = pid;
+ handle = handle;
+ request = send_request;
+ notify = send_notification;
+ }
+end
+
+return {
+ start = create_and_start_client;
+ rpc_response_error = rpc_response_error;
+ format_rpc_error = format_rpc_error;
+ client_errors = client_errors;
+}
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
new file mode 100644
index 0000000000..3798b30a46
--- /dev/null
+++ b/runtime/lua/vim/lsp/util.lua
@@ -0,0 +1,727 @@
+local protocol = require 'vim.lsp.protocol'
+local vim = vim
+local validate = vim.validate
+local api = vim.api
+local list_extend = vim.list_extend
+
+local M = {}
+
+local split = vim.split
+local function split_lines(value)
+ return split(value, '\n', true)
+end
+
+local function ok_or_nil(status, ...)
+ if not status then return end
+ return ...
+end
+local function npcall(fn, ...)
+ return ok_or_nil(pcall(fn, ...))
+end
+
+--- Find the longest shared prefix between prefix and word.
+-- e.g. remove_prefix("123tes", "testing") == "ting"
+local function remove_prefix(prefix, word)
+ local max_prefix_length = math.min(#prefix, #word)
+ local prefix_length = 0
+ for i = 1, max_prefix_length do
+ local current_line_suffix = prefix:sub(-i)
+ local word_prefix = word:sub(1, i)
+ if current_line_suffix == word_prefix then
+ prefix_length = i
+ end
+ end
+ return word:sub(prefix_length + 1)
+end
+
+-- TODO(ashkan) @performance this could do less copying.
+function M.set_lines(lines, A, B, new_lines)
+ -- 0-indexing to 1-indexing
+ local i_0 = A[1] + 1
+ local i_n = B[1] + 1
+ if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then
+ error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
+ end
+ local prefix = ""
+ local suffix = lines[i_n]:sub(B[2]+1)
+ if A[2] > 0 then
+ prefix = lines[i_0]:sub(1, A[2])
+ end
+ local n = i_n - i_0 + 1
+ if n ~= #new_lines then
+ for _ = 1, n - #new_lines do table.remove(lines, i_0) end
+ for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end
+ end
+ for i = 1, #new_lines do
+ lines[i - 1 + i_0] = new_lines[i]
+ end
+ if #suffix > 0 then
+ local i = i_0 + #new_lines - 1
+ lines[i] = lines[i]..suffix
+ end
+ if #prefix > 0 then
+ lines[i_0] = prefix..lines[i_0]
+ end
+ return lines
+end
+
+local function sort_by_key(fn)
+ return function(a,b)
+ local ka, kb = fn(a), fn(b)
+ assert(#ka == #kb)
+ for i = 1, #ka do
+ if ka[i] ~= kb[i] then
+ return ka[i] < kb[i]
+ end
+ end
+ -- every value must have been equal here, which means it's not less than.
+ return false
+ end
+end
+local edit_sort_key = sort_by_key(function(e)
+ return {e.A[1], e.A[2], e.i}
+end)
+
+function M.apply_text_edits(text_edits, bufnr)
+ if not next(text_edits) then return end
+ local start_line, finish_line = math.huge, -1
+ local cleaned = {}
+ for i, e in ipairs(text_edits) do
+ start_line = math.min(e.range.start.line, start_line)
+ finish_line = math.max(e.range["end"].line, finish_line)
+ -- TODO(ashkan) sanity check ranges for overlap.
+ table.insert(cleaned, {
+ i = i;
+ A = {e.range.start.line; e.range.start.character};
+ B = {e.range["end"].line; e.range["end"].character};
+ lines = vim.split(e.newText, '\n', true);
+ })
+ end
+
+ -- Reverse sort the orders so we can apply them without interfering with
+ -- eachother. Also add i as a sort key to mimic a stable sort.
+ table.sort(cleaned, edit_sort_key)
+ local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false)
+ local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol')
+ local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) == finish_line + 1
+ if set_eol and #lines[#lines] ~= 0 then
+ table.insert(lines, '')
+ end
+
+ for i = #cleaned, 1, -1 do
+ local e = cleaned[i]
+ local A = {e.A[1] - start_line, e.A[2]}
+ local B = {e.B[1] - start_line, e.B[2]}
+ lines = M.set_lines(lines, A, B, e.lines)
+ end
+ if set_eol and #lines[#lines] == 0 then
+ table.remove(lines)
+ end
+ api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines)
+end
+
+-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
+-- local valid_unix_path_characters = "[^/]"
+-- https://github.com/davidm/lua-glob-pattern
+-- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
+-- function M.glob_to_regex(glob)
+-- end
+
+-- textDocument/completion response returns one of CompletionItem[], CompletionList or null.
+-- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
+function M.extract_completion_items(result)
+ if type(result) == 'table' and result.items then
+ return result.items
+ elseif result ~= nil then
+ return result
+ else
+ return {}
+ end
+end
+
+--- Apply the TextDocumentEdit response.
+-- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification
+function M.apply_text_document_edit(text_document_edit)
+ local text_document = text_document_edit.textDocument
+ local bufnr = vim.uri_to_bufnr(text_document.uri)
+ -- TODO(ashkan) check this is correct.
+ if api.nvim_buf_get_changedtick(bufnr) > text_document.version then
+ print("Buffer ", text_document.uri, " newer than edits.")
+ return
+ end
+ M.apply_text_edits(text_document_edit.edits, bufnr)
+end
+
+function M.get_current_line_to_cursor()
+ local pos = api.nvim_win_get_cursor(0)
+ local line = assert(api.nvim_buf_get_lines(0, pos[1]-1, pos[1], false)[1])
+ return line:sub(pos[2]+1)
+end
+
+--- Getting vim complete-items with incomplete flag.
+-- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion)
+-- @return { matches = complete-items table, incomplete = boolean }
+function M.text_document_completion_list_to_complete_items(result, line_prefix)
+ local items = M.extract_completion_items(result)
+ if vim.tbl_isempty(items) then
+ return {}
+ end
+ -- Only initialize if we have some items.
+ if not line_prefix then
+ line_prefix = M.get_current_line_to_cursor()
+ end
+
+ local matches = {}
+
+ for _, completion_item in ipairs(items) do
+ local info = ' '
+ local documentation = completion_item.documentation
+ if documentation then
+ if type(documentation) == 'string' and documentation ~= '' then
+ info = documentation
+ elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
+ info = documentation.value
+ -- else
+ -- TODO(ashkan) Validation handling here?
+ end
+ end
+
+ local word = completion_item.insertText or completion_item.label
+
+ -- Ref: `:h complete-items`
+ table.insert(matches, {
+ word = remove_prefix(line_prefix, word),
+ abbr = completion_item.label,
+ kind = protocol.CompletionItemKind[completion_item.kind] or '',
+ menu = completion_item.detail or '',
+ info = info,
+ icase = 1,
+ dup = 0,
+ empty = 1,
+ })
+ end
+
+ return matches
+end
+
+-- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification
+function M.apply_workspace_edit(workspace_edit)
+ if workspace_edit.documentChanges then
+ for _, change in ipairs(workspace_edit.documentChanges) do
+ if change.kind then
+ -- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile
+ error(string.format("Unsupported change: %q", vim.inspect(change)))
+ else
+ M.apply_text_document_edit(change)
+ end
+ end
+ return
+ end
+
+ local all_changes = workspace_edit.changes
+ if not (all_changes and not vim.tbl_isempty(all_changes)) then
+ return
+ end
+
+ for uri, changes in pairs(all_changes) do
+ local bufnr = vim.uri_to_bufnr(uri)
+ M.apply_text_edits(changes, bufnr)
+ end
+end
+
+--- Convert any of MarkedString | MarkedString[] | MarkupContent into markdown text lines
+-- see https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_hover
+-- Useful for textDocument/hover, textDocument/signatureHelp, and potentially others.
+function M.convert_input_to_markdown_lines(input, contents)
+ contents = contents or {}
+ -- MarkedString variation 1
+ if type(input) == 'string' then
+ list_extend(contents, split_lines(input))
+ else
+ assert(type(input) == 'table', "Expected a table for Hover.contents")
+ -- MarkupContent
+ if input.kind then
+ -- The kind can be either plaintext or markdown. However, either way we
+ -- will just be rendering markdown, so we handle them both the same way.
+ -- TODO these can have escaped/sanitized html codes in markdown. We
+ -- should make sure we handle this correctly.
+
+ -- Some servers send input.value as empty, so let's ignore this :(
+ -- assert(type(input.value) == 'string')
+ list_extend(contents, split_lines(input.value or ''))
+ -- MarkupString variation 2
+ elseif input.language then
+ -- Some servers send input.value as empty, so let's ignore this :(
+ -- assert(type(input.value) == 'string')
+ table.insert(contents, "```"..input.language)
+ list_extend(contents, split_lines(input.value or ''))
+ table.insert(contents, "```")
+ -- By deduction, this must be MarkedString[]
+ else
+ -- Use our existing logic to handle MarkedString
+ for _, marked_string in ipairs(input) do
+ M.convert_input_to_markdown_lines(marked_string, contents)
+ end
+ end
+ end
+ if contents[1] == '' or contents[1] == nil then
+ return {}
+ end
+ return contents
+end
+
+function M.make_floating_popup_options(width, height, opts)
+ validate {
+ opts = { opts, 't', true };
+ }
+ opts = opts or {}
+ validate {
+ ["opts.offset_x"] = { opts.offset_x, 'n', true };
+ ["opts.offset_y"] = { opts.offset_y, 'n', true };
+ }
+
+ local anchor = ''
+ local row, col
+
+ if vim.fn.winline() <= height then
+ anchor = anchor..'N'
+ row = 1
+ else
+ anchor = anchor..'S'
+ row = 0
+ end
+
+ if vim.fn.wincol() + width <= api.nvim_get_option('columns') then
+ anchor = anchor..'W'
+ col = 0
+ else
+ anchor = anchor..'E'
+ col = 1
+ end
+
+ return {
+ anchor = anchor,
+ col = col + (opts.offset_x or 0),
+ height = height,
+ relative = 'cursor',
+ row = row + (opts.offset_y or 0),
+ style = 'minimal',
+ width = width,
+ }
+end
+
+function M.jump_to_location(location)
+ if location.uri == nil then return end
+ local bufnr = vim.uri_to_bufnr(location.uri)
+ -- Save position in jumplist
+ vim.cmd "normal! m'"
+ -- TODO(ashkan) use tagfunc here to update tagstack.
+ api.nvim_set_current_buf(bufnr)
+ local row = location.range.start.line
+ local col = location.range.start.character
+ local line = api.nvim_buf_get_lines(0, row, row+1, true)[1]
+ col = vim.str_byteindex(line, col)
+ api.nvim_win_set_cursor(0, {row + 1, col})
+ return true
+end
+
+local function find_window_by_var(name, value)
+ for _, win in ipairs(api.nvim_list_wins()) do
+ if npcall(api.nvim_win_get_var, win, name) == value then
+ return win
+ end
+ end
+end
+
+-- Check if a window with `unique_name` tagged is associated with the current
+-- buffer. If not, make a new preview.
+--
+-- fn()'s return values will be passed directly to open_floating_preview in the
+-- case that a new floating window should be created.
+function M.focusable_preview(unique_name, fn)
+ if npcall(api.nvim_win_get_var, 0, unique_name) then
+ return api.nvim_command("wincmd p")
+ end
+ local bufnr = api.nvim_get_current_buf()
+ do
+ local win = find_window_by_var(unique_name, bufnr)
+ if win then
+ api.nvim_set_current_win(win)
+ api.nvim_command("stopinsert")
+ return
+ end
+ end
+ local pbufnr, pwinnr = M.open_floating_preview(fn())
+ api.nvim_win_set_var(pwinnr, unique_name, bufnr)
+ return pbufnr, pwinnr
+end
+
+function M.open_floating_preview(contents, filetype, opts)
+ validate {
+ contents = { contents, 't' };
+ filetype = { filetype, 's', true };
+ opts = { opts, 't', true };
+ }
+ opts = opts or {}
+
+ -- Trim empty lines from the end.
+ contents = M.trim_empty_lines(contents)
+
+ local width = opts.width
+ local height = opts.height or #contents
+ if not width then
+ width = 0
+ for i, line in ipairs(contents) do
+ -- Clean up the input and add left pad.
+ line = " "..line:gsub("\r", "")
+ -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
+ local line_width = vim.fn.strdisplaywidth(line)
+ width = math.max(line_width, width)
+ contents[i] = line
+ end
+ -- Add right padding of 1 each.
+ width = width + 1
+ end
+
+ local floating_bufnr = api.nvim_create_buf(false, true)
+ if filetype then
+ api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype)
+ end
+ local float_option = M.make_floating_popup_options(width, height, opts)
+ local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
+ if filetype == 'markdown' then
+ api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
+ end
+ api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
+ api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
+ -- TODO make InsertCharPre disappearing optional?
+ api.nvim_command("autocmd CursorMoved,BufHidden,InsertCharPre <buffer> ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
+ return floating_bufnr, floating_winnr
+end
+
+local function validate_lsp_position(pos)
+ validate { pos = {pos, 't'} }
+ validate {
+ line = {pos.line, 'n'};
+ character = {pos.character, 'n'};
+ }
+ return true
+end
+
+function M.open_floating_peek_preview(bufnr, start, finish, opts)
+ validate {
+ bufnr = {bufnr, 'n'};
+ start = {start, validate_lsp_position, 'valid start Position'};
+ finish = {finish, validate_lsp_position, 'valid finish Position'};
+ opts = { opts, 't', true };
+ }
+ local width = math.max(finish.character - start.character + 1, 1)
+ local height = math.max(finish.line - start.line + 1, 1)
+ local floating_winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
+ api.nvim_win_set_cursor(floating_winnr, {start.line+1, start.character})
+ api.nvim_command("autocmd CursorMoved * ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
+ return floating_winnr
+end
+
+
+local function highlight_range(bufnr, ns, hiname, start, finish)
+ if start[1] == finish[1] then
+ -- TODO care about encoding here since this is in byte index?
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], finish[2])
+ else
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], -1)
+ for line = start[1] + 1, finish[1] - 1 do
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, line, 0, -1)
+ end
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, finish[1], 0, finish[2])
+ end
+end
+
+do
+ local all_buffer_diagnostics = {}
+
+ local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics")
+
+ local underline_highlight_name = "LspDiagnosticsUnderline"
+ api.nvim_command(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name))
+
+ local severity_highlights = {}
+
+ local default_severity_highlight = {
+ [protocol.DiagnosticSeverity.Error] = { guifg = "Red" };
+ [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" };
+ [protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" };
+ [protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
+ }
+
+ -- Initialize default severity highlights
+ for severity, hi_info in pairs(default_severity_highlight) do
+ local severity_name = protocol.DiagnosticSeverity[severity]
+ local highlight_name = "LspDiagnostics"..severity_name
+ -- Try to fill in the foreground color with a sane default.
+ local cmd_parts = {"highlight", "default", highlight_name}
+ for k, v in pairs(hi_info) do
+ table.insert(cmd_parts, k.."="..v)
+ end
+ api.nvim_command(table.concat(cmd_parts, ' '))
+ severity_highlights[severity] = highlight_name
+ end
+
+ function M.buf_clear_diagnostics(bufnr)
+ validate { bufnr = {bufnr, 'n', true} }
+ bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
+ api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1)
+ end
+
+ function M.get_severity_highlight_name(severity)
+ return severity_highlights[severity]
+ end
+
+ function M.show_line_diagnostics()
+ local bufnr = api.nvim_get_current_buf()
+ local line = api.nvim_win_get_cursor(0)[1] - 1
+ -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {})
+ -- if #marks == 0 then
+ -- return
+ -- end
+ -- local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+ local lines = {"Diagnostics:"}
+ local highlights = {{0, "Bold"}}
+
+ local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+ if not buffer_diagnostics then return end
+ local line_diagnostics = buffer_diagnostics[line]
+ if not line_diagnostics then return end
+
+ for i, diagnostic in ipairs(line_diagnostics) do
+ -- for i, mark in ipairs(marks) do
+ -- local mark_id = mark[1]
+ -- local diagnostic = buffer_diagnostics[mark_id]
+
+ -- TODO(ashkan) make format configurable?
+ local prefix = string.format("%d. ", i)
+ local hiname = severity_highlights[diagnostic.severity]
+ local message_lines = split_lines(diagnostic.message)
+ table.insert(lines, prefix..message_lines[1])
+ table.insert(highlights, {#prefix + 1, hiname})
+ for j = 2, #message_lines do
+ table.insert(lines, message_lines[j])
+ table.insert(highlights, {0, hiname})
+ end
+ end
+ local popup_bufnr, winnr = M.open_floating_preview(lines, 'plaintext')
+ for i, hi in ipairs(highlights) do
+ local prefixlen, hiname = unpack(hi)
+ -- Start highlight after the prefix
+ api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1)
+ end
+ return popup_bufnr, winnr
+ end
+
+ function M.buf_diagnostics_save_positions(bufnr, diagnostics)
+ validate {
+ bufnr = {bufnr, 'n', true};
+ diagnostics = {diagnostics, 't', true};
+ }
+ if not diagnostics then return end
+ bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
+
+ if not all_buffer_diagnostics[bufnr] then
+ -- Clean up our data when the buffer unloads.
+ api.nvim_buf_attach(bufnr, false, {
+ on_detach = function(b)
+ all_buffer_diagnostics[b] = nil
+ end
+ })
+ end
+ all_buffer_diagnostics[bufnr] = {}
+ local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+
+ for _, diagnostic in ipairs(diagnostics) do
+ local start = diagnostic.range.start
+ -- local mark_id = api.nvim_buf_set_extmark(bufnr, diagnostic_ns, 0, start.line, 0, {})
+ -- buffer_diagnostics[mark_id] = diagnostic
+ local line_diagnostics = buffer_diagnostics[start.line]
+ if not line_diagnostics then
+ line_diagnostics = {}
+ buffer_diagnostics[start.line] = line_diagnostics
+ end
+ table.insert(line_diagnostics, diagnostic)
+ end
+ end
+
+
+ function M.buf_diagnostics_underline(bufnr, diagnostics)
+ for _, diagnostic in ipairs(diagnostics) do
+ local start = diagnostic.range.start
+ local finish = diagnostic.range["end"]
+
+ -- TODO care about encoding here since this is in byte index?
+ highlight_range(bufnr, diagnostic_ns, underline_highlight_name,
+ {start.line, start.character},
+ {finish.line, finish.character}
+ )
+ end
+ end
+
+ function M.buf_diagnostics_virtual_text(bufnr, diagnostics)
+ local buffer_line_diagnostics = all_buffer_diagnostics[bufnr]
+ if not buffer_line_diagnostics then
+ M.buf_diagnostics_save_positions(bufnr, diagnostics)
+ end
+ buffer_line_diagnostics = all_buffer_diagnostics[bufnr]
+ if not buffer_line_diagnostics then
+ return
+ end
+ for line, line_diags in pairs(buffer_line_diagnostics) do
+ local virt_texts = {}
+ for i = 1, #line_diags - 1 do
+ table.insert(virt_texts, {"â– ", severity_highlights[line_diags[i].severity]})
+ end
+ local last = line_diags[#line_diags]
+ -- TODO(ashkan) use first line instead of subbing 2 spaces?
+ table.insert(virt_texts, {"â–  "..last.message:gsub("\r", ""):gsub("\n", " "), severity_highlights[last.severity]})
+ api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {})
+ end
+ end
+end
+
+local position_sort = sort_by_key(function(v)
+ return {v.line, v.character}
+end)
+
+-- Returns the items with the byte position calculated correctly and in sorted
+-- order.
+function M.locations_to_items(locations)
+ local items = {}
+ local grouped = setmetatable({}, {
+ __index = function(t, k)
+ local v = {}
+ rawset(t, k, v)
+ return v
+ end;
+ })
+ for _, d in ipairs(locations) do
+ local start = d.range.start
+ local fname = assert(vim.uri_to_fname(d.uri))
+ table.insert(grouped[fname], start)
+ end
+ local keys = vim.tbl_keys(grouped)
+ table.sort(keys)
+ -- TODO(ashkan) I wish we could do this lazily.
+ for _, fname in ipairs(keys) do
+ local rows = grouped[fname]
+ table.sort(rows, position_sort)
+ local i = 0
+ for line in io.lines(fname) do
+ for _, pos in ipairs(rows) do
+ local row = pos.line
+ if i == row then
+ local col
+ if pos.character > #line then
+ col = #line
+ else
+ col = vim.str_byteindex(line, pos.character)
+ end
+ table.insert(items, {
+ filename = fname,
+ lnum = row + 1,
+ col = col + 1;
+ text = line;
+ })
+ end
+ end
+ i = i + 1
+ end
+ end
+ return items
+end
+
+-- locations is Location[]
+-- Only sets for the current window.
+function M.set_loclist(locations)
+ vim.fn.setloclist(0, {}, ' ', {
+ title = 'Language Server';
+ items = M.locations_to_items(locations);
+ })
+end
+
+-- locations is Location[]
+function M.set_qflist(locations)
+ vim.fn.setqflist({}, ' ', {
+ title = 'Language Server';
+ items = M.locations_to_items(locations);
+ })
+end
+
+-- Remove empty lines from the beginning and end.
+function M.trim_empty_lines(lines)
+ local start = 1
+ for i = 1, #lines do
+ if #lines[i] > 0 then
+ start = i
+ break
+ end
+ end
+ local finish = 1
+ for i = #lines, 1, -1 do
+ if #lines[i] > 0 then
+ finish = i
+ break
+ end
+ end
+ return vim.list_extend({}, lines, start, finish)
+end
+
+-- Accepts markdown lines and tries to reduce it to a filetype if it is
+-- just a single code block.
+-- Note: This modifies the input.
+--
+-- Returns: filetype or 'markdown' if it was unchanged.
+function M.try_trim_markdown_code_blocks(lines)
+ local language_id = lines[1]:match("^```(.*)")
+ if language_id then
+ local has_inner_code_fence = false
+ for i = 2, (#lines - 1) do
+ local line = lines[i]
+ if line:sub(1,3) == '```' then
+ has_inner_code_fence = true
+ break
+ end
+ end
+ -- No inner code fences + starting with code fence = hooray.
+ if not has_inner_code_fence then
+ table.remove(lines, 1)
+ table.remove(lines)
+ return language_id
+ end
+ end
+ return 'markdown'
+end
+
+local str_utfindex = vim.str_utfindex
+function M.make_position_params()
+ local row, col = unpack(api.nvim_win_get_cursor(0))
+ row = row - 1
+ local line = api.nvim_buf_get_lines(0, row, row+1, true)[1]
+ col = str_utfindex(line, col)
+ return {
+ textDocument = { uri = vim.uri_from_bufnr(0) };
+ position = { line = row; character = col; }
+ }
+end
+
+-- @param buf buffer handle or 0 for current.
+-- @param row 0-indexed line
+-- @param col 0-indexed byte offset in line
+function M.character_offset(buf, row, col)
+ local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
+ -- If the col is past the EOL, use the line length.
+ if col > #line then
+ return str_utfindex(line)
+ end
+ return str_utfindex(line, col)
+end
+
+return M
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index cd6f8a04d8..b5b04d7757 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -4,34 +4,37 @@
-- test-suite. If, in the future, Nvim itself is used to run the test-suite
-- instead of "vanilla Lua", these functions could move to src/nvim/lua/vim.lua
+local vim = vim or {}
--- Returns a deep copy of the given object. Non-table objects are copied as
--- in a typical Lua assignment, whereas table objects are copied recursively.
---
--@param orig Table to copy
--@returns New table of copied keys and (nested) values.
-local function deepcopy(orig)
- error(orig)
-end
-local function _id(v)
- return v
-end
-local deepcopy_funcs = {
- table = function(orig)
- local copy = {}
- for k, v in pairs(orig) do
- copy[deepcopy(k)] = deepcopy(v)
- end
- return copy
- end,
- number = _id,
- string = _id,
- ['nil'] = _id,
- boolean = _id,
-}
-deepcopy = function(orig)
- return deepcopy_funcs[type(orig)](orig)
-end
+function vim.deepcopy(orig) end -- luacheck: no unused
+vim.deepcopy = (function()
+ local function _id(v)
+ return v
+ end
+
+ local deepcopy_funcs = {
+ table = function(orig)
+ local copy = {}
+ for k, v in pairs(orig) do
+ copy[vim.deepcopy(k)] = vim.deepcopy(v)
+ end
+ return copy
+ end,
+ number = _id,
+ string = _id,
+ ['nil'] = _id,
+ boolean = _id,
+ }
+
+ return function(orig)
+ return deepcopy_funcs[type(orig)](orig)
+ end
+end)()
--- Splits a string at each instance of a separator.
---
@@ -43,10 +46,8 @@ end
--@param sep Separator string or pattern
--@param plain If `true` use `sep` literally (passed to String.find)
--@returns Iterator over the split components
-local function gsplit(s, sep, plain)
- assert(type(s) == "string")
- assert(type(sep) == "string")
- assert(type(plain) == "boolean" or type(plain) == "nil")
+function vim.gsplit(s, sep, plain)
+ vim.validate{s={s,'s'},sep={sep,'s'},plain={plain,'b',true}}
local start = 1
local done = false
@@ -92,20 +93,51 @@ end
--@param sep Separator string or pattern
--@param plain If `true` use `sep` literally (passed to String.find)
--@returns List-like table of the split components.
-local function split(s,sep,plain)
- local t={} for c in gsplit(s, sep, plain) do table.insert(t,c) end
+function vim.split(s,sep,plain)
+ local t={} for c in vim.gsplit(s, sep, plain) do table.insert(t,c) end
return t
end
+--- Return a list of all keys used in a table.
+--- However, the order of the return table of keys is not guaranteed.
+---
+--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+---
+--@param t Table
+--@returns list of keys
+function vim.tbl_keys(t)
+ assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
+
+ local keys = {}
+ for k, _ in pairs(t) do
+ table.insert(keys, k)
+ end
+ return keys
+end
+
+--- Return a list of all values used in a table.
+--- However, the order of the return table of values is not guaranteed.
+---
+--@param t Table
+--@returns list of values
+function vim.tbl_values(t)
+ assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
+
+ local values = {}
+ for _, v in pairs(t) do
+ table.insert(values, v)
+ end
+ return values
+end
+
--- Checks if a list-like (vector) table contains `value`.
---
--@param t Table to check
--@param value Value to compare
--@returns true if `t` contains `value`
-local function tbl_contains(t, value)
- if type(t) ~= 'table' then
- error('t must be a table')
- end
+function vim.tbl_contains(t, value)
+ vim.validate{t={t,'t'}}
+
for _,v in ipairs(t) do
if v == value then
return true
@@ -114,6 +146,16 @@ local function tbl_contains(t, value)
return false
end
+-- Returns true if the table is empty, and contains no indexed or keyed values.
+--
+--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+--
+--@param t Table to check
+function vim.tbl_isempty(t)
+ assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
+ return next(t) == nil
+end
+
--- Merges two or more map-like tables.
---
--@see |extend()|
@@ -123,7 +165,7 @@ end
--- - "keep": use value from the leftmost map
--- - "force": use value from the rightmost map
--@param ... Two or more map-like tables.
-local function tbl_extend(behavior, ...)
+function vim.tbl_extend(behavior, ...)
if (behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force') then
error('invalid "behavior": '..tostring(behavior))
end
@@ -145,13 +187,77 @@ local function tbl_extend(behavior, ...)
return ret
end
+--- Deep compare values for equality
+function vim.deep_equal(a, b)
+ if a == b then return true end
+ if type(a) ~= type(b) then return false end
+ if type(a) == 'table' then
+ -- TODO improve this algorithm's performance.
+ for k, v in pairs(a) do
+ if not vim.deep_equal(v, b[k]) then
+ return false
+ end
+ end
+ for k, v in pairs(b) do
+ if not vim.deep_equal(v, a[k]) then
+ return false
+ end
+ end
+ return true
+ end
+ return false
+end
+
+--- Add the reverse lookup values to an existing table.
+--- For example:
+--- `tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A = 1 }`
+--
+--Do note that it *modifies* the input.
+--@param o table The table to add the reverse to.
+function vim.tbl_add_reverse_lookup(o)
+ local keys = vim.tbl_keys(o)
+ for _, k in ipairs(keys) do
+ local v = o[k]
+ if o[v] then
+ error(string.format("The reverse lookup found an existing value for %q while processing key %q", tostring(v), tostring(k)))
+ end
+ o[v] = k
+ end
+ return o
+end
+
+--- Extends a list-like table with the values of another list-like table.
+---
+--- NOTE: This mutates dst!
+---
+--@see |vim.tbl_extend()|
+---
+--@param dst list which will be modified and appended to.
+--@param src list from which values will be inserted.
+--@param start Start index on src. defaults to 1
+--@param finish Final index on src. defaults to #src
+--@returns dst
+function vim.list_extend(dst, src, start, finish)
+ vim.validate {
+ dst = {dst, 't'};
+ src = {src, 't'};
+ start = {start, 'n', true};
+ finish = {finish, 'n', true};
+ }
+ for i = start or 1, finish or #src do
+ table.insert(dst, src[i])
+ end
+ return dst
+end
+
--- Creates a copy of a list-like table such that any nested tables are
--- "unrolled" and appended to the result.
---
+--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+---
--@param t List-like table
--@returns Flattened copy of the given list-like table.
-local function tbl_flatten(t)
- -- From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+function vim.tbl_flatten(t)
local result = {}
local function _tbl_flatten(_t)
local n = #_t
@@ -168,13 +274,39 @@ local function tbl_flatten(t)
return result
end
+-- Determine whether a Lua table can be treated as an array.
+---
+--@params Table
+--@returns true: A non-empty array, false: A non-empty table, nil: An empty table
+function vim.tbl_islist(t)
+ if type(t) ~= 'table' then
+ return false
+ end
+
+ local count = 0
+
+ for k, _ in pairs(t) do
+ if type(k) == "number" then
+ count = count + 1
+ else
+ return false
+ end
+ end
+
+ if count > 0 then
+ return true
+ else
+ return nil
+ end
+end
+
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
---
--@see https://www.lua.org/pil/20.2.html
--@param s String to trim
--@returns String with whitespace removed from its beginning and end
-local function trim(s)
- assert(type(s) == 'string', 'Only strings can be trimmed')
+function vim.trim(s)
+ vim.validate{s={s,'s'}}
return s:match('^%s*(.*%S)') or ''
end
@@ -183,19 +315,120 @@ end
--@see https://github.com/rxi/lume
--@param s String to escape
--@returns %-escaped pattern string
-local function pesc(s)
- assert(type(s) == 'string')
+function vim.pesc(s)
+ vim.validate{s={s,'s'}}
return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1')
end
-local module = {
- deepcopy = deepcopy,
- gsplit = gsplit,
- pesc = pesc,
- split = split,
- tbl_contains = tbl_contains,
- tbl_extend = tbl_extend,
- tbl_flatten = tbl_flatten,
- trim = trim,
-}
-return module
+--- Test if `prefix` is a prefix of `s` for strings.
+--
+-- @param s String to check
+-- @param prefix Potential prefix
+-- @return boolean True if prefix is a prefix of s
+function vim.startswith(s, prefix)
+ vim.validate { s = {s, 's'}; prefix = {prefix, 's'}; }
+ return s:sub(1, #prefix) == prefix
+end
+
+--- Test if `suffix` is a suffix of `s` for strings.
+--
+-- @param s String to check
+-- @param suffix Potential suffix
+-- @return boolean True if suffix is a suffix of s
+function vim.endswith(s, suffix)
+ vim.validate { s = {s, 's'}; suffix = {suffix, 's'}; }
+ return #suffix == 0 or s:sub(-#suffix) == suffix
+end
+
+--- Validates a parameter specification (types and values).
+---
+--- Usage example:
+--- <pre>
+--- function user.new(name, age, hobbies)
+--- vim.validate{
+--- name={name, 'string'},
+--- age={age, 'number'},
+--- hobbies={hobbies, 'table'},
+--- }
+--- ...
+--- end
+--- </pre>
+---
+--- Examples with explicit argument values (can be run directly):
+--- <pre>
+--- vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
+--- => NOP (success)
+---
+--- vim.validate{arg1={1, 'table'}}
+--- => error('arg1: expected table, got number')
+---
+--- vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
+--- => error('arg1: expected even number, got 3')
+--- </pre>
+---
+--@param opt Map of parameter names to validations. Each key is a parameter
+--- name; each value is a tuple in one of these forms:
+--- 1. (arg_value, type_name, optional)
+--- - arg_value: argument value
+--- - type_name: string type name, one of: ("table", "t", "string",
+--- "s", "number", "n", "boolean", "b", "function", "f", "nil",
+--- "thread", "userdata")
+--- - optional: (optional) boolean, if true, `nil` is valid
+--- 2. (arg_value, fn, msg)
+--- - arg_value: argument value
+--- - fn: any function accepting one argument, returns true if and
+--- only if the argument is valid
+--- - msg: (optional) error string if validation fails
+function vim.validate(opt) end -- luacheck: no unused
+vim.validate = (function()
+ local type_names = {
+ t='table', s='string', n='number', b='boolean', f='function', c='callable',
+ ['table']='table', ['string']='string', ['number']='number',
+ ['boolean']='boolean', ['function']='function', ['callable']='callable',
+ ['nil']='nil', ['thread']='thread', ['userdata']='userdata',
+ }
+ local function _type_name(t)
+ local tname = type_names[t]
+ if tname == nil then
+ error(string.format('invalid type name: %s', tostring(t)))
+ end
+ return tname
+ end
+ local function _is_type(val, t)
+ return t == 'callable' and vim.is_callable(val) or type(val) == t
+ end
+
+ return function(opt)
+ assert(type(opt) == 'table', string.format('opt: expected table, got %s', type(opt)))
+ for param_name, spec in pairs(opt) do
+ assert(type(spec) == 'table', string.format('%s: expected table, got %s', param_name, type(spec)))
+
+ local val = spec[1] -- Argument value.
+ local t = spec[2] -- Type name, or callable.
+ local optional = (true == spec[3])
+
+ if not vim.is_callable(t) then -- Check type name.
+ if (not optional or val ~= nil) and not _is_type(val, _type_name(t)) then
+ error(string.format("%s: expected %s, got %s", param_name, _type_name(t), type(val)))
+ end
+ elseif not t(val) then -- Check user-provided validation function.
+ error(string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), val))
+ end
+ end
+ return true
+ end
+end)()
+
+--- Returns true if object `f` can be called as a function.
+---
+--@param f Any object
+--@return true if `f` is callable, else false
+function vim.is_callable(f)
+ if type(f) == 'function' then return true end
+ local m = getmetatable(f)
+ if m == nil then return false end
+ return type(m.__call) == 'function'
+end
+
+return vim
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
new file mode 100644
index 0000000000..e0202927bb
--- /dev/null
+++ b/runtime/lua/vim/treesitter.lua
@@ -0,0 +1,73 @@
+local a = vim.api
+
+-- TODO(bfredl): currently we retain parsers for the lifetime of the buffer.
+-- Consider use weak references to release parser if all plugins are done with
+-- it.
+local parsers = {}
+
+local Parser = {}
+Parser.__index = Parser
+
+function Parser:parse()
+ if self.valid then
+ return self.tree
+ end
+ self.tree = self._parser:parse_buf(self.bufnr)
+ self.valid = true
+ return self.tree
+end
+
+function Parser:_on_lines(bufnr, _, start_row, old_stop_row, stop_row, old_byte_size)
+ local start_byte = a.nvim_buf_get_offset(bufnr,start_row)
+ local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row)
+ local old_stop_byte = start_byte + old_byte_size
+ self._parser:edit(start_byte,old_stop_byte,stop_byte,
+ start_row,0,old_stop_row,0,stop_row,0)
+ self.valid = false
+end
+
+local module = {
+ add_language=vim._ts_add_language,
+ inspect_language=vim._ts_inspect_language,
+}
+
+function module.create_parser(bufnr, ft, id)
+ if bufnr == 0 then
+ bufnr = a.nvim_get_current_buf()
+ end
+ local self = setmetatable({bufnr=bufnr, valid=false}, Parser)
+ self._parser = vim._create_ts_parser(ft)
+ self:parse()
+ -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is
+ -- using it.
+ local function lines_cb(_, ...)
+ return self:_on_lines(...)
+ end
+ local detach_cb = nil
+ if id ~= nil then
+ detach_cb = function()
+ if parsers[id] == self then
+ parsers[id] = nil
+ end
+ end
+ end
+ a.nvim_buf_attach(self.bufnr, false, {on_lines=lines_cb, on_detach=detach_cb})
+ return self
+end
+
+function module.get_parser(bufnr, ft)
+ if bufnr == nil or bufnr == 0 then
+ bufnr = a.nvim_get_current_buf()
+ end
+ if ft == nil then
+ ft = a.nvim_buf_get_option(bufnr, "filetype")
+ end
+ local id = tostring(bufnr)..'_'..ft
+
+ if parsers[id] == nil then
+ parsers[id] = module.create_parser(bufnr, ft, id)
+ end
+ return parsers[id]
+end
+
+return module
diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua
new file mode 100644
index 0000000000..1065f84f4c
--- /dev/null
+++ b/runtime/lua/vim/uri.lua
@@ -0,0 +1,94 @@
+--- TODO: This is implemented only for files now.
+-- https://tools.ietf.org/html/rfc3986
+-- https://tools.ietf.org/html/rfc2732
+-- https://tools.ietf.org/html/rfc2396
+
+
+local uri_decode
+do
+ local schar = string.char
+ local function hex_to_char(hex)
+ return schar(tonumber(hex, 16))
+ end
+ uri_decode = function(str)
+ return str:gsub("%%([a-fA-F0-9][a-fA-F0-9])", hex_to_char)
+ end
+end
+
+local uri_encode
+do
+ local PATTERNS = {
+ --- RFC 2396
+ -- https://tools.ietf.org/html/rfc2396#section-2.2
+ rfc2396 = "^A-Za-z0-9%-_.!~*'()";
+ --- RFC 2732
+ -- https://tools.ietf.org/html/rfc2732
+ rfc2732 = "^A-Za-z0-9%-_.!~*'()[]";
+ --- RFC 3986
+ -- https://tools.ietf.org/html/rfc3986#section-2.2
+ rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/";
+ }
+ local sbyte, tohex = string.byte
+ if jit then
+ tohex = require'bit'.tohex
+ else
+ tohex = function(b) return string.format("%02x", b) end
+ end
+ local function percent_encode_char(char)
+ return "%"..tohex(sbyte(char), 2)
+ end
+ uri_encode = function(text, rfc)
+ if not text then return end
+ local pattern = PATTERNS[rfc] or PATTERNS.rfc3986
+ return text:gsub("(["..pattern.."])", percent_encode_char)
+ end
+end
+
+
+local function is_windows_file_uri(uri)
+ return uri:match('^file:///[a-zA-Z]:') ~= nil
+end
+
+local function uri_from_fname(path)
+ local volume_path, fname = path:match("^([a-zA-Z]:)(.*)")
+ local is_windows = volume_path ~= nil
+ if is_windows then
+ path = volume_path..uri_encode(fname:gsub("\\", "/"))
+ else
+ path = uri_encode(path)
+ end
+ local uri_parts = {"file://"}
+ if is_windows then
+ table.insert(uri_parts, "/")
+ end
+ table.insert(uri_parts, path)
+ return table.concat(uri_parts)
+end
+
+local function uri_from_bufnr(bufnr)
+ return uri_from_fname(vim.api.nvim_buf_get_name(bufnr))
+end
+
+local function uri_to_fname(uri)
+ -- TODO improve this.
+ if is_windows_file_uri(uri) then
+ uri = uri:gsub('^file:///', '')
+ uri = uri:gsub('/', '\\')
+ else
+ uri = uri:gsub('^file://', '')
+ end
+ return uri_decode(uri)
+end
+
+-- Return or create a buffer for a uri.
+local function uri_to_bufnr(uri)
+ return vim.fn.bufadd((uri_to_fname(uri)))
+end
+
+return {
+ uri_from_fname = uri_from_fname,
+ uri_from_bufnr = uri_from_bufnr,
+ uri_to_fname = uri_to_fname,
+ uri_to_bufnr = uri_to_bufnr,
+}
+-- vim:sw=2 ts=2 et
diff --git a/runtime/makemenu.vim b/runtime/makemenu.vim
index ff29d59063..27fa46fe25 100644
--- a/runtime/makemenu.vim
+++ b/runtime/makemenu.vim
@@ -1,6 +1,6 @@
" Script to define the syntax menu in synmenu.vim
" Maintainer: Bram Moolenaar <Bram@vim.org>
-" Last Change: 2018 May 17
+" Last Change: 2019 Dec 07
" This is used by "make menu" in the src directory.
edit <sfile>:p:h/synmenu.vim
@@ -101,6 +101,7 @@ SynMenu AB.AYacc:ayacc
SynMenu AB.B:b
SynMenu AB.Baan:baan
+SynMenu AB.Bash:bash
SynMenu AB.Basic.FreeBasic:freebasic
SynMenu AB.Basic.IBasic:ibasic
SynMenu AB.Basic.QBasic:basic
@@ -128,8 +129,9 @@ SynMenu C.Century\ Term:cterm
SynMenu C.CH\ script:ch
SynMenu C.ChaiScript:chaiscript
SynMenu C.ChangeLog:changelog
-SynMenu C.Cheetah\ template:cheetah
SynMenu C.CHILL:chill
+SynMenu C.Cheetah\ template:cheetah
+SynMenu C.Chicken:chicken
SynMenu C.ChordPro:chordpro
SynMenu C.Clean:clean
SynMenu C.Clever:cl
@@ -160,6 +162,7 @@ SynMenu C.Cyn++:cynpp
SynMenu C.Cynlib:cynlib
SynMenu DE.D:d
+SynMenu DE.Dart:dart
SynMenu DE.Datascript:datascript
SynMenu DE.Debian.Debian\ ChangeLog:debchangelog
SynMenu DE.Debian.Debian\ Control:debcontrol
@@ -192,12 +195,14 @@ SynMenu DE.DTD:dtd
SynMenu DE.DTML\ (Zope):dtml
SynMenu DE.DTrace:dtrace
SynMenu DE.Dts/dtsi:dts
+SynMenu DE.Dune:dune
SynMenu DE.Dylan.Dylan:dylan
SynMenu DE.Dylan.Dylan\ interface:dylanintr
SynMenu DE.Dylan.Dylan\ lid:dylanlid
SynMenu DE.EDIF:edif
SynMenu DE.Eiffel:eiffel
+SynMenu DE.Eight:8th
SynMenu DE.Elinks\ config:elinks
SynMenu DE.Elm\ filter\ rules:elmfilt
SynMenu DE.Embedix\ Component\ Description:ecd
@@ -307,6 +312,7 @@ SynMenu HIJK.Java.JavaCC:javacc
SynMenu HIJK.Java.Java\ Server\ Pages:jsp
SynMenu HIJK.Java.Java\ Properties:jproperties
SynMenu HIJK.JavaScript:javascript
+SynMenu HIJK.JavaScriptReact:javascriptreact
SynMenu HIJK.Jess:jess
SynMenu HIJK.Jgraph:jgraph
SynMenu HIJK.Jovial:jovial
@@ -365,6 +371,7 @@ SynMenu M.Mathematica:mma
SynMenu M.Matlab:matlab
SynMenu M.Maxima:maxima
SynMenu M.MEL\ (for\ Maya):mel
+SynMenu M.Meson:meson
SynMenu M.Messages\ (/var/log):messages
SynMenu M.Metafont:mf
SynMenu M.MetaPost:mp
@@ -467,6 +474,7 @@ SynMenu R.R.R\ help:rhelp
SynMenu R.R.R\ noweb:rnoweb
SynMenu R.Racc\ input:racc
SynMenu R.Radiance:radiance
+SynMenu R.Raml:raml
SynMenu R.Ratpoison:ratpoison
SynMenu R.RCS.RCS\ log\ output:rcslog
SynMenu R.RCS.RCS\ file:rcs
@@ -609,6 +617,8 @@ SynMenu T.Trustees:trustees
SynMenu T.TSS.Command\ Line:tsscl
SynMenu T.TSS.Geometry:tssgm
SynMenu T.TSS.Optics:tssop
+SynMenu T.Typescript:typescript
+SynMenu T.TypescriptReact:typescriptreact
SynMenu UV.Udev\ config:udevconf
SynMenu UV.Udev\ permissions:udevperm
@@ -637,6 +647,7 @@ SynMenu UV.VSE\ JCL:vsejcl
SynMenu WXYZ.WEB.CWEB:cweb
SynMenu WXYZ.WEB.WEB:web
SynMenu WXYZ.WEB.WEB\ Changes:change
+SynMenu WXYZ.WebAssembly:wast
SynMenu WXYZ.Webmacro:webmacro
SynMenu WXYZ.Website\ MetaLanguage:wml
SynMenu WXYZ.wDiff:wdiff
diff --git a/runtime/nvim.appdata.xml b/runtime/nvim.appdata.xml
index 32d3c523c6..025de1b5a9 100644
--- a/runtime/nvim.appdata.xml
+++ b/runtime/nvim.appdata.xml
@@ -26,6 +26,10 @@
</screenshots>
<releases>
+ <release date="2019-11-06" version="0.4.3"/>
+ <release date="2019-09-15" version="0.4.2"/>
+ <release date="2019-09-15" version="0.4.1"/>
+ <release date="2019-09-15" version="0.4.0"/>
<release date="2019-07-03" version="0.3.8"/>
<release date="2019-04-29" version="0.3.5"/>
<release date="2019-01-13" version="0.3.4"/>
diff --git a/runtime/optwin.vim b/runtime/optwin.vim
index 51b2bbb583..6b3328a5d4 100644
--- a/runtime/optwin.vim
+++ b/runtime/optwin.vim
@@ -300,6 +300,11 @@ call append("$", "tagstack\ta :tag command will use the tagstack")
call <SID>BinOptionG("tgst", &tgst)
call append("$", "showfulltag\twhen completing tags in Insert mode show more info")
call <SID>BinOptionG("sft", &sft)
+if has("eval")
+ call append("$", "tagfunc\ta function to be used to perform tag searches")
+ call append("$", "\t(local to buffer)")
+ call <SID>OptionL("tfu")
+endif
if has("cscope")
call append("$", "cscopeprg\tcommand for executing cscope")
call <SID>OptionG("csprg", &csprg)
diff --git a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
index a97461ad69..b9fc77dc37 100644
--- a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
+++ b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
@@ -67,8 +67,8 @@ command -nargs=* -complete=file -bang Termdebug call s:StartDebug(<bang>0, <f-ar
command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>)
" Name of the gdb command, defaults to "gdb".
-if !exists('termdebugger')
- let termdebugger = 'gdb'
+if !exists('g:termdebugger')
+ let g:termdebugger = 'gdb'
endif
let s:pc_id = 12
@@ -106,9 +106,14 @@ endfunc
func s:StartDebug_internal(dict)
if exists('s:gdbwin')
- echoerr 'Terminal debugger already running'
+ echoerr 'Terminal debugger already running, cannot run two'
return
endif
+ if !executable(g:termdebugger)
+ echoerr 'Cannot execute debugger program "' .. g:termdebugger .. '"'
+ return
+ endif
+
let s:ptywin = 0
let s:pid = 0
@@ -578,6 +583,7 @@ func s:HandleEvaluate(msg)
endif
let s:evalFromBalloonExprResult = split(s:evalFromBalloonExprResult, '\\n')
call s:OpenHoverPreview(s:evalFromBalloonExprResult, v:null)
+ let s:evalFromBalloonExprResult = ''
else
echomsg '"' . s:evalexpr . '": ' . value
endif
diff --git a/runtime/plugin/man.vim b/runtime/plugin/man.vim
index e18a5528bb..e762eb3664 100644
--- a/runtime/plugin/man.vim
+++ b/runtime/plugin/man.vim
@@ -1,4 +1,4 @@
-" Maintainer: Anmol Sethi <anmol@aubble.com>
+" Maintainer: Anmol Sethi <hi@nhooyr.io>
if exists('g:loaded_man')
finish
diff --git a/runtime/synmenu.vim b/runtime/synmenu.vim
index 70e0e83ba7..3367c68d3b 100644
--- a/runtime/synmenu.vim
+++ b/runtime/synmenu.vim
@@ -87,19 +87,20 @@ an 50.10.580 &Syntax.AB.Awk :cal SetSyn("awk")<CR>
an 50.10.590 &Syntax.AB.AYacc :cal SetSyn("ayacc")<CR>
an 50.10.610 &Syntax.AB.B :cal SetSyn("b")<CR>
an 50.10.620 &Syntax.AB.Baan :cal SetSyn("baan")<CR>
-an 50.10.630 &Syntax.AB.Basic.FreeBasic :cal SetSyn("freebasic")<CR>
-an 50.10.640 &Syntax.AB.Basic.IBasic :cal SetSyn("ibasic")<CR>
-an 50.10.650 &Syntax.AB.Basic.QBasic :cal SetSyn("basic")<CR>
-an 50.10.660 &Syntax.AB.Basic.Visual\ Basic :cal SetSyn("vb")<CR>
-an 50.10.670 &Syntax.AB.Bazaar\ commit\ file :cal SetSyn("bzr")<CR>
-an 50.10.680 &Syntax.AB.Bazel :cal SetSyn("bzl")<CR>
-an 50.10.690 &Syntax.AB.BC\ calculator :cal SetSyn("bc")<CR>
-an 50.10.700 &Syntax.AB.BDF\ font :cal SetSyn("bdf")<CR>
-an 50.10.710 &Syntax.AB.BibTeX.Bibliography\ database :cal SetSyn("bib")<CR>
-an 50.10.720 &Syntax.AB.BibTeX.Bibliography\ Style :cal SetSyn("bst")<CR>
-an 50.10.730 &Syntax.AB.BIND.BIND\ config :cal SetSyn("named")<CR>
-an 50.10.740 &Syntax.AB.BIND.BIND\ zone :cal SetSyn("bindzone")<CR>
-an 50.10.750 &Syntax.AB.Blank :cal SetSyn("blank")<CR>
+an 50.10.630 &Syntax.AB.Bash :cal SetSyn("bash")<CR>
+an 50.10.640 &Syntax.AB.Basic.FreeBasic :cal SetSyn("freebasic")<CR>
+an 50.10.650 &Syntax.AB.Basic.IBasic :cal SetSyn("ibasic")<CR>
+an 50.10.660 &Syntax.AB.Basic.QBasic :cal SetSyn("basic")<CR>
+an 50.10.670 &Syntax.AB.Basic.Visual\ Basic :cal SetSyn("vb")<CR>
+an 50.10.680 &Syntax.AB.Bazaar\ commit\ file :cal SetSyn("bzr")<CR>
+an 50.10.690 &Syntax.AB.Bazel :cal SetSyn("bzl")<CR>
+an 50.10.700 &Syntax.AB.BC\ calculator :cal SetSyn("bc")<CR>
+an 50.10.710 &Syntax.AB.BDF\ font :cal SetSyn("bdf")<CR>
+an 50.10.720 &Syntax.AB.BibTeX.Bibliography\ database :cal SetSyn("bib")<CR>
+an 50.10.730 &Syntax.AB.BibTeX.Bibliography\ Style :cal SetSyn("bst")<CR>
+an 50.10.740 &Syntax.AB.BIND.BIND\ config :cal SetSyn("named")<CR>
+an 50.10.750 &Syntax.AB.BIND.BIND\ zone :cal SetSyn("bindzone")<CR>
+an 50.10.760 &Syntax.AB.Blank :cal SetSyn("blank")<CR>
an 50.20.100 &Syntax.C.C :cal SetSyn("c")<CR>
an 50.20.110 &Syntax.C.C++ :cal SetSyn("cpp")<CR>
an 50.20.120 &Syntax.C.C# :cal SetSyn("cs")<CR>
@@ -113,89 +114,93 @@ an 50.20.190 &Syntax.C.Century\ Term :cal SetSyn("cterm")<CR>
an 50.20.200 &Syntax.C.CH\ script :cal SetSyn("ch")<CR>
an 50.20.210 &Syntax.C.ChaiScript :cal SetSyn("chaiscript")<CR>
an 50.20.220 &Syntax.C.ChangeLog :cal SetSyn("changelog")<CR>
-an 50.20.230 &Syntax.C.Cheetah\ template :cal SetSyn("cheetah")<CR>
-an 50.20.240 &Syntax.C.CHILL :cal SetSyn("chill")<CR>
-an 50.20.250 &Syntax.C.ChordPro :cal SetSyn("chordpro")<CR>
-an 50.20.260 &Syntax.C.Clean :cal SetSyn("clean")<CR>
-an 50.20.270 &Syntax.C.Clever :cal SetSyn("cl")<CR>
-an 50.20.280 &Syntax.C.Clipper :cal SetSyn("clipper")<CR>
-an 50.20.290 &Syntax.C.Clojure :cal SetSyn("clojure")<CR>
-an 50.20.300 &Syntax.C.Cmake :cal SetSyn("cmake")<CR>
-an 50.20.310 &Syntax.C.Cmod :cal SetSyn("cmod")<CR>
-an 50.20.320 &Syntax.C.Cmusrc :cal SetSyn("cmusrc")<CR>
-an 50.20.330 &Syntax.C.Cobol :cal SetSyn("cobol")<CR>
-an 50.20.340 &Syntax.C.Coco/R :cal SetSyn("coco")<CR>
-an 50.20.350 &Syntax.C.Cold\ Fusion :cal SetSyn("cf")<CR>
-an 50.20.360 &Syntax.C.Conary\ Recipe :cal SetSyn("conaryrecipe")<CR>
-an 50.20.370 &Syntax.C.Config.Cfg\ Config\ file :cal SetSyn("cfg")<CR>
-an 50.20.380 &Syntax.C.Config.Configure\.in :cal SetSyn("config")<CR>
-an 50.20.390 &Syntax.C.Config.Generic\ Config\ file :cal SetSyn("conf")<CR>
-an 50.20.400 &Syntax.C.CRM114 :cal SetSyn("crm")<CR>
-an 50.20.410 &Syntax.C.Crontab :cal SetSyn("crontab")<CR>
-an 50.20.420 &Syntax.C.CSDL :cal SetSyn("csdl")<CR>
-an 50.20.430 &Syntax.C.CSP :cal SetSyn("csp")<CR>
-an 50.20.440 &Syntax.C.Ctrl-H :cal SetSyn("ctrlh")<CR>
-an 50.20.450 &Syntax.C.Cucumber :cal SetSyn("cucumber")<CR>
-an 50.20.460 &Syntax.C.CUDA :cal SetSyn("cuda")<CR>
-an 50.20.470 &Syntax.C.CUPL.CUPL :cal SetSyn("cupl")<CR>
-an 50.20.480 &Syntax.C.CUPL.Simulation :cal SetSyn("cuplsim")<CR>
-an 50.20.490 &Syntax.C.CVS.commit\ file :cal SetSyn("cvs")<CR>
-an 50.20.500 &Syntax.C.CVS.cvsrc :cal SetSyn("cvsrc")<CR>
-an 50.20.510 &Syntax.C.Cyn++ :cal SetSyn("cynpp")<CR>
-an 50.20.520 &Syntax.C.Cynlib :cal SetSyn("cynlib")<CR>
+an 50.20.230 &Syntax.C.CHILL :cal SetSyn("chill")<CR>
+an 50.20.240 &Syntax.C.Cheetah\ template :cal SetSyn("cheetah")<CR>
+an 50.20.250 &Syntax.C.Chicken :cal SetSyn("chicken")<CR>
+an 50.20.260 &Syntax.C.ChordPro :cal SetSyn("chordpro")<CR>
+an 50.20.270 &Syntax.C.Clean :cal SetSyn("clean")<CR>
+an 50.20.280 &Syntax.C.Clever :cal SetSyn("cl")<CR>
+an 50.20.290 &Syntax.C.Clipper :cal SetSyn("clipper")<CR>
+an 50.20.300 &Syntax.C.Clojure :cal SetSyn("clojure")<CR>
+an 50.20.310 &Syntax.C.Cmake :cal SetSyn("cmake")<CR>
+an 50.20.320 &Syntax.C.Cmod :cal SetSyn("cmod")<CR>
+an 50.20.330 &Syntax.C.Cmusrc :cal SetSyn("cmusrc")<CR>
+an 50.20.340 &Syntax.C.Cobol :cal SetSyn("cobol")<CR>
+an 50.20.350 &Syntax.C.Coco/R :cal SetSyn("coco")<CR>
+an 50.20.360 &Syntax.C.Cold\ Fusion :cal SetSyn("cf")<CR>
+an 50.20.370 &Syntax.C.Conary\ Recipe :cal SetSyn("conaryrecipe")<CR>
+an 50.20.380 &Syntax.C.Config.Cfg\ Config\ file :cal SetSyn("cfg")<CR>
+an 50.20.390 &Syntax.C.Config.Configure\.in :cal SetSyn("config")<CR>
+an 50.20.400 &Syntax.C.Config.Generic\ Config\ file :cal SetSyn("conf")<CR>
+an 50.20.410 &Syntax.C.CRM114 :cal SetSyn("crm")<CR>
+an 50.20.420 &Syntax.C.Crontab :cal SetSyn("crontab")<CR>
+an 50.20.430 &Syntax.C.CSDL :cal SetSyn("csdl")<CR>
+an 50.20.440 &Syntax.C.CSP :cal SetSyn("csp")<CR>
+an 50.20.450 &Syntax.C.Ctrl-H :cal SetSyn("ctrlh")<CR>
+an 50.20.460 &Syntax.C.Cucumber :cal SetSyn("cucumber")<CR>
+an 50.20.470 &Syntax.C.CUDA :cal SetSyn("cuda")<CR>
+an 50.20.480 &Syntax.C.CUPL.CUPL :cal SetSyn("cupl")<CR>
+an 50.20.490 &Syntax.C.CUPL.Simulation :cal SetSyn("cuplsim")<CR>
+an 50.20.500 &Syntax.C.CVS.commit\ file :cal SetSyn("cvs")<CR>
+an 50.20.510 &Syntax.C.CVS.cvsrc :cal SetSyn("cvsrc")<CR>
+an 50.20.520 &Syntax.C.Cyn++ :cal SetSyn("cynpp")<CR>
+an 50.20.530 &Syntax.C.Cynlib :cal SetSyn("cynlib")<CR>
an 50.30.100 &Syntax.DE.D :cal SetSyn("d")<CR>
-an 50.30.110 &Syntax.DE.Datascript :cal SetSyn("datascript")<CR>
-an 50.30.120 &Syntax.DE.Debian.Debian\ ChangeLog :cal SetSyn("debchangelog")<CR>
-an 50.30.130 &Syntax.DE.Debian.Debian\ Control :cal SetSyn("debcontrol")<CR>
-an 50.30.140 &Syntax.DE.Debian.Debian\ Copyright :cal SetSyn("debcopyright")<CR>
-an 50.30.150 &Syntax.DE.Debian.Debian\ Sources\.list :cal SetSyn("debsources")<CR>
-an 50.30.160 &Syntax.DE.Denyhosts :cal SetSyn("denyhosts")<CR>
-an 50.30.170 &Syntax.DE.Desktop :cal SetSyn("desktop")<CR>
-an 50.30.180 &Syntax.DE.Dict\ config :cal SetSyn("dictconf")<CR>
-an 50.30.190 &Syntax.DE.Dictd\ config :cal SetSyn("dictdconf")<CR>
-an 50.30.200 &Syntax.DE.Diff :cal SetSyn("diff")<CR>
-an 50.30.210 &Syntax.DE.Digital\ Command\ Lang :cal SetSyn("dcl")<CR>
-an 50.30.220 &Syntax.DE.Dircolors :cal SetSyn("dircolors")<CR>
-an 50.30.230 &Syntax.DE.Dirpager :cal SetSyn("dirpager")<CR>
-an 50.30.240 &Syntax.DE.Django\ template :cal SetSyn("django")<CR>
-an 50.30.250 &Syntax.DE.DNS/BIND\ zone :cal SetSyn("bindzone")<CR>
-an 50.30.260 &Syntax.DE.Dnsmasq\ config :cal SetSyn("dnsmasq")<CR>
-an 50.30.270 &Syntax.DE.DocBook.auto-detect :cal SetSyn("docbk")<CR>
-an 50.30.280 &Syntax.DE.DocBook.SGML :cal SetSyn("docbksgml")<CR>
-an 50.30.290 &Syntax.DE.DocBook.XML :cal SetSyn("docbkxml")<CR>
-an 50.30.300 &Syntax.DE.Dockerfile :cal SetSyn("dockerfile")<CR>
-an 50.30.310 &Syntax.DE.Dot :cal SetSyn("dot")<CR>
-an 50.30.320 &Syntax.DE.Doxygen.C\ with\ doxygen :cal SetSyn("c.doxygen")<CR>
-an 50.30.330 &Syntax.DE.Doxygen.C++\ with\ doxygen :cal SetSyn("cpp.doxygen")<CR>
-an 50.30.340 &Syntax.DE.Doxygen.IDL\ with\ doxygen :cal SetSyn("idl.doxygen")<CR>
-an 50.30.350 &Syntax.DE.Doxygen.Java\ with\ doxygen :cal SetSyn("java.doxygen")<CR>
-an 50.30.360 &Syntax.DE.Doxygen.DataScript\ with\ doxygen :cal SetSyn("datascript.doxygen")<CR>
-an 50.30.370 &Syntax.DE.Dracula :cal SetSyn("dracula")<CR>
-an 50.30.380 &Syntax.DE.DSSSL :cal SetSyn("dsl")<CR>
-an 50.30.390 &Syntax.DE.DTD :cal SetSyn("dtd")<CR>
-an 50.30.400 &Syntax.DE.DTML\ (Zope) :cal SetSyn("dtml")<CR>
-an 50.30.410 &Syntax.DE.DTrace :cal SetSyn("dtrace")<CR>
-an 50.30.420 &Syntax.DE.Dts/dtsi :cal SetSyn("dts")<CR>
-an 50.30.430 &Syntax.DE.Dylan.Dylan :cal SetSyn("dylan")<CR>
-an 50.30.440 &Syntax.DE.Dylan.Dylan\ interface :cal SetSyn("dylanintr")<CR>
-an 50.30.450 &Syntax.DE.Dylan.Dylan\ lid :cal SetSyn("dylanlid")<CR>
-an 50.30.470 &Syntax.DE.EDIF :cal SetSyn("edif")<CR>
-an 50.30.480 &Syntax.DE.Eiffel :cal SetSyn("eiffel")<CR>
-an 50.30.490 &Syntax.DE.Elinks\ config :cal SetSyn("elinks")<CR>
-an 50.30.500 &Syntax.DE.Elm\ filter\ rules :cal SetSyn("elmfilt")<CR>
-an 50.30.510 &Syntax.DE.Embedix\ Component\ Description :cal SetSyn("ecd")<CR>
-an 50.30.520 &Syntax.DE.ERicsson\ LANGuage :cal SetSyn("erlang")<CR>
-an 50.30.530 &Syntax.DE.ESMTP\ rc :cal SetSyn("esmtprc")<CR>
-an 50.30.540 &Syntax.DE.ESQL-C :cal SetSyn("esqlc")<CR>
-an 50.30.550 &Syntax.DE.Essbase\ script :cal SetSyn("csc")<CR>
-an 50.30.560 &Syntax.DE.Esterel :cal SetSyn("esterel")<CR>
-an 50.30.570 &Syntax.DE.Eterm\ config :cal SetSyn("eterm")<CR>
-an 50.30.580 &Syntax.DE.Euphoria\ 3 :cal SetSyn("euphoria3")<CR>
-an 50.30.590 &Syntax.DE.Euphoria\ 4 :cal SetSyn("euphoria4")<CR>
-an 50.30.600 &Syntax.DE.Eviews :cal SetSyn("eviews")<CR>
-an 50.30.610 &Syntax.DE.Exim\ conf :cal SetSyn("exim")<CR>
-an 50.30.620 &Syntax.DE.Expect :cal SetSyn("expect")<CR>
-an 50.30.630 &Syntax.DE.Exports :cal SetSyn("exports")<CR>
+an 50.30.110 &Syntax.DE.Dart :cal SetSyn("dart")<CR>
+an 50.30.120 &Syntax.DE.Datascript :cal SetSyn("datascript")<CR>
+an 50.30.130 &Syntax.DE.Debian.Debian\ ChangeLog :cal SetSyn("debchangelog")<CR>
+an 50.30.140 &Syntax.DE.Debian.Debian\ Control :cal SetSyn("debcontrol")<CR>
+an 50.30.150 &Syntax.DE.Debian.Debian\ Copyright :cal SetSyn("debcopyright")<CR>
+an 50.30.160 &Syntax.DE.Debian.Debian\ Sources\.list :cal SetSyn("debsources")<CR>
+an 50.30.170 &Syntax.DE.Denyhosts :cal SetSyn("denyhosts")<CR>
+an 50.30.180 &Syntax.DE.Desktop :cal SetSyn("desktop")<CR>
+an 50.30.190 &Syntax.DE.Dict\ config :cal SetSyn("dictconf")<CR>
+an 50.30.200 &Syntax.DE.Dictd\ config :cal SetSyn("dictdconf")<CR>
+an 50.30.210 &Syntax.DE.Diff :cal SetSyn("diff")<CR>
+an 50.30.220 &Syntax.DE.Digital\ Command\ Lang :cal SetSyn("dcl")<CR>
+an 50.30.230 &Syntax.DE.Dircolors :cal SetSyn("dircolors")<CR>
+an 50.30.240 &Syntax.DE.Dirpager :cal SetSyn("dirpager")<CR>
+an 50.30.250 &Syntax.DE.Django\ template :cal SetSyn("django")<CR>
+an 50.30.260 &Syntax.DE.DNS/BIND\ zone :cal SetSyn("bindzone")<CR>
+an 50.30.270 &Syntax.DE.Dnsmasq\ config :cal SetSyn("dnsmasq")<CR>
+an 50.30.280 &Syntax.DE.DocBook.auto-detect :cal SetSyn("docbk")<CR>
+an 50.30.290 &Syntax.DE.DocBook.SGML :cal SetSyn("docbksgml")<CR>
+an 50.30.300 &Syntax.DE.DocBook.XML :cal SetSyn("docbkxml")<CR>
+an 50.30.310 &Syntax.DE.Dockerfile :cal SetSyn("dockerfile")<CR>
+an 50.30.320 &Syntax.DE.Dot :cal SetSyn("dot")<CR>
+an 50.30.330 &Syntax.DE.Doxygen.C\ with\ doxygen :cal SetSyn("c.doxygen")<CR>
+an 50.30.340 &Syntax.DE.Doxygen.C++\ with\ doxygen :cal SetSyn("cpp.doxygen")<CR>
+an 50.30.350 &Syntax.DE.Doxygen.IDL\ with\ doxygen :cal SetSyn("idl.doxygen")<CR>
+an 50.30.360 &Syntax.DE.Doxygen.Java\ with\ doxygen :cal SetSyn("java.doxygen")<CR>
+an 50.30.370 &Syntax.DE.Doxygen.DataScript\ with\ doxygen :cal SetSyn("datascript.doxygen")<CR>
+an 50.30.380 &Syntax.DE.Dracula :cal SetSyn("dracula")<CR>
+an 50.30.390 &Syntax.DE.DSSSL :cal SetSyn("dsl")<CR>
+an 50.30.400 &Syntax.DE.DTD :cal SetSyn("dtd")<CR>
+an 50.30.410 &Syntax.DE.DTML\ (Zope) :cal SetSyn("dtml")<CR>
+an 50.30.420 &Syntax.DE.DTrace :cal SetSyn("dtrace")<CR>
+an 50.30.430 &Syntax.DE.Dts/dtsi :cal SetSyn("dts")<CR>
+an 50.30.440 &Syntax.DE.Dune :cal SetSyn("dune")<CR>
+an 50.30.450 &Syntax.DE.Dylan.Dylan :cal SetSyn("dylan")<CR>
+an 50.30.460 &Syntax.DE.Dylan.Dylan\ interface :cal SetSyn("dylanintr")<CR>
+an 50.30.470 &Syntax.DE.Dylan.Dylan\ lid :cal SetSyn("dylanlid")<CR>
+an 50.30.490 &Syntax.DE.EDIF :cal SetSyn("edif")<CR>
+an 50.30.500 &Syntax.DE.Eiffel :cal SetSyn("eiffel")<CR>
+an 50.30.510 &Syntax.DE.Eight :cal SetSyn("8th")<CR>
+an 50.30.520 &Syntax.DE.Elinks\ config :cal SetSyn("elinks")<CR>
+an 50.30.530 &Syntax.DE.Elm\ filter\ rules :cal SetSyn("elmfilt")<CR>
+an 50.30.540 &Syntax.DE.Embedix\ Component\ Description :cal SetSyn("ecd")<CR>
+an 50.30.550 &Syntax.DE.ERicsson\ LANGuage :cal SetSyn("erlang")<CR>
+an 50.30.560 &Syntax.DE.ESMTP\ rc :cal SetSyn("esmtprc")<CR>
+an 50.30.570 &Syntax.DE.ESQL-C :cal SetSyn("esqlc")<CR>
+an 50.30.580 &Syntax.DE.Essbase\ script :cal SetSyn("csc")<CR>
+an 50.30.590 &Syntax.DE.Esterel :cal SetSyn("esterel")<CR>
+an 50.30.600 &Syntax.DE.Eterm\ config :cal SetSyn("eterm")<CR>
+an 50.30.610 &Syntax.DE.Euphoria\ 3 :cal SetSyn("euphoria3")<CR>
+an 50.30.620 &Syntax.DE.Euphoria\ 4 :cal SetSyn("euphoria4")<CR>
+an 50.30.630 &Syntax.DE.Eviews :cal SetSyn("eviews")<CR>
+an 50.30.640 &Syntax.DE.Exim\ conf :cal SetSyn("exim")<CR>
+an 50.30.650 &Syntax.DE.Expect :cal SetSyn("expect")<CR>
+an 50.30.660 &Syntax.DE.Exports :cal SetSyn("exports")<CR>
an 50.40.100 &Syntax.FG.Falcon :cal SetSyn("falcon")<CR>
an 50.40.110 &Syntax.FG.Fantom :cal SetSyn("fan")<CR>
an 50.40.120 &Syntax.FG.Fetchmail :cal SetSyn("fetchmail")<CR>
@@ -259,43 +264,44 @@ an 50.50.290 &Syntax.HIJK.HTML.XHTML :cal SetSyn("xhtml")<CR>
an 50.50.300 &Syntax.HIJK.Host\.conf :cal SetSyn("hostconf")<CR>
an 50.50.310 &Syntax.HIJK.Hosts\ access :cal SetSyn("hostsaccess")<CR>
an 50.50.320 &Syntax.HIJK.Hyper\ Builder :cal SetSyn("hb")<CR>
-an 50.50.330 &Syntax.HIJK.Icewm\ menu :cal SetSyn("icemenu")<CR>
-an 50.50.340 &Syntax.HIJK.Icon :cal SetSyn("icon")<CR>
-an 50.50.350 &Syntax.HIJK.IDL\Generic\ IDL :cal SetSyn("idl")<CR>
-an 50.50.360 &Syntax.HIJK.IDL\Microsoft\ IDL :cal SetSyn("msidl")<CR>
-an 50.50.370 &Syntax.HIJK.Indent\ profile :cal SetSyn("indent")<CR>
-an 50.50.380 &Syntax.HIJK.Inform :cal SetSyn("inform")<CR>
-an 50.50.390 &Syntax.HIJK.Informix\ 4GL :cal SetSyn("fgl")<CR>
-an 50.50.400 &Syntax.HIJK.Initng :cal SetSyn("initng")<CR>
-an 50.50.410 &Syntax.HIJK.Inittab :cal SetSyn("inittab")<CR>
-an 50.50.420 &Syntax.HIJK.Inno\ setup :cal SetSyn("iss")<CR>
-an 50.50.430 &Syntax.HIJK.Innovation\ Data\ Processing.Upstream\ dat :cal SetSyn("upstreamdat")<CR>
-an 50.50.440 &Syntax.HIJK.Innovation\ Data\ Processing.Upstream\ log :cal SetSyn("upstreamlog")<CR>
-an 50.50.450 &Syntax.HIJK.Innovation\ Data\ Processing.Upstream\ rpt :cal SetSyn("upstreamrpt")<CR>
-an 50.50.460 &Syntax.HIJK.Innovation\ Data\ Processing.Upstream\ Install\ log :cal SetSyn("upstreaminstalllog")<CR>
-an 50.50.470 &Syntax.HIJK.Innovation\ Data\ Processing.Usserver\ log :cal SetSyn("usserverlog")<CR>
-an 50.50.480 &Syntax.HIJK.Innovation\ Data\ Processing.USW2KAgt\ log :cal SetSyn("usw2kagtlog")<CR>
-an 50.50.490 &Syntax.HIJK.InstallShield\ script :cal SetSyn("ishd")<CR>
-an 50.50.500 &Syntax.HIJK.Interactive\ Data\ Lang :cal SetSyn("idlang")<CR>
-an 50.50.510 &Syntax.HIJK.IPfilter :cal SetSyn("ipfilter")<CR>
-an 50.50.530 &Syntax.HIJK.J :cal SetSyn("j")<CR>
-an 50.50.540 &Syntax.HIJK.JAL :cal SetSyn("jal")<CR>
-an 50.50.550 &Syntax.HIJK.JAM :cal SetSyn("jam")<CR>
-an 50.50.560 &Syntax.HIJK.Jargon :cal SetSyn("jargon")<CR>
-an 50.50.570 &Syntax.HIJK.Java.Java :cal SetSyn("java")<CR>
-an 50.50.580 &Syntax.HIJK.Java.JavaCC :cal SetSyn("javacc")<CR>
-an 50.50.590 &Syntax.HIJK.Java.Java\ Server\ Pages :cal SetSyn("jsp")<CR>
-an 50.50.600 &Syntax.HIJK.Java.Java\ Properties :cal SetSyn("jproperties")<CR>
-an 50.50.610 &Syntax.HIJK.JavaScript :cal SetSyn("javascript")<CR>
-an 50.50.620 &Syntax.HIJK.Jess :cal SetSyn("jess")<CR>
-an 50.50.630 &Syntax.HIJK.Jgraph :cal SetSyn("jgraph")<CR>
-an 50.50.640 &Syntax.HIJK.Jovial :cal SetSyn("jovial")<CR>
-an 50.50.650 &Syntax.HIJK.JSON :cal SetSyn("json")<CR>
-an 50.50.670 &Syntax.HIJK.Kconfig :cal SetSyn("kconfig")<CR>
-an 50.50.680 &Syntax.HIJK.KDE\ script :cal SetSyn("kscript")<CR>
-an 50.50.690 &Syntax.HIJK.Kimwitu++ :cal SetSyn("kwt")<CR>
-an 50.50.700 &Syntax.HIJK.Kivy :cal SetSyn("kivy")<CR>
-an 50.50.710 &Syntax.HIJK.KixTart :cal SetSyn("kix")<CR>
+an 50.50.340 &Syntax.HIJK.Icewm\ menu :cal SetSyn("icemenu")<CR>
+an 50.50.350 &Syntax.HIJK.Icon :cal SetSyn("icon")<CR>
+an 50.50.360 &Syntax.HIJK.IDL\Generic\ IDL :cal SetSyn("idl")<CR>
+an 50.50.370 &Syntax.HIJK.IDL\Microsoft\ IDL :cal SetSyn("msidl")<CR>
+an 50.50.380 &Syntax.HIJK.Indent\ profile :cal SetSyn("indent")<CR>
+an 50.50.390 &Syntax.HIJK.Inform :cal SetSyn("inform")<CR>
+an 50.50.400 &Syntax.HIJK.Informix\ 4GL :cal SetSyn("fgl")<CR>
+an 50.50.410 &Syntax.HIJK.Initng :cal SetSyn("initng")<CR>
+an 50.50.420 &Syntax.HIJK.Inittab :cal SetSyn("inittab")<CR>
+an 50.50.430 &Syntax.HIJK.Inno\ setup :cal SetSyn("iss")<CR>
+an 50.50.440 &Syntax.HIJK.Innovation\ Data\ Processing.Upstream\ dat :cal SetSyn("upstreamdat")<CR>
+an 50.50.450 &Syntax.HIJK.Innovation\ Data\ Processing.Upstream\ log :cal SetSyn("upstreamlog")<CR>
+an 50.50.460 &Syntax.HIJK.Innovation\ Data\ Processing.Upstream\ rpt :cal SetSyn("upstreamrpt")<CR>
+an 50.50.470 &Syntax.HIJK.Innovation\ Data\ Processing.Upstream\ Install\ log :cal SetSyn("upstreaminstalllog")<CR>
+an 50.50.480 &Syntax.HIJK.Innovation\ Data\ Processing.Usserver\ log :cal SetSyn("usserverlog")<CR>
+an 50.50.490 &Syntax.HIJK.Innovation\ Data\ Processing.USW2KAgt\ log :cal SetSyn("usw2kagtlog")<CR>
+an 50.50.500 &Syntax.HIJK.InstallShield\ script :cal SetSyn("ishd")<CR>
+an 50.50.510 &Syntax.HIJK.Interactive\ Data\ Lang :cal SetSyn("idlang")<CR>
+an 50.50.520 &Syntax.HIJK.IPfilter :cal SetSyn("ipfilter")<CR>
+an 50.50.540 &Syntax.HIJK.J :cal SetSyn("j")<CR>
+an 50.50.550 &Syntax.HIJK.JAL :cal SetSyn("jal")<CR>
+an 50.50.560 &Syntax.HIJK.JAM :cal SetSyn("jam")<CR>
+an 50.50.570 &Syntax.HIJK.Jargon :cal SetSyn("jargon")<CR>
+an 50.50.580 &Syntax.HIJK.Java.Java :cal SetSyn("java")<CR>
+an 50.50.590 &Syntax.HIJK.Java.JavaCC :cal SetSyn("javacc")<CR>
+an 50.50.600 &Syntax.HIJK.Java.Java\ Server\ Pages :cal SetSyn("jsp")<CR>
+an 50.50.610 &Syntax.HIJK.Java.Java\ Properties :cal SetSyn("jproperties")<CR>
+an 50.50.620 &Syntax.HIJK.JavaScript :cal SetSyn("javascript")<CR>
+an 50.50.630 &Syntax.HIJK.JavaScriptReact :cal SetSyn("javascriptreact")<CR>
+an 50.50.640 &Syntax.HIJK.Jess :cal SetSyn("jess")<CR>
+an 50.50.650 &Syntax.HIJK.Jgraph :cal SetSyn("jgraph")<CR>
+an 50.50.660 &Syntax.HIJK.Jovial :cal SetSyn("jovial")<CR>
+an 50.50.670 &Syntax.HIJK.JSON :cal SetSyn("json")<CR>
+an 50.50.690 &Syntax.HIJK.Kconfig :cal SetSyn("kconfig")<CR>
+an 50.50.700 &Syntax.HIJK.KDE\ script :cal SetSyn("kscript")<CR>
+an 50.50.710 &Syntax.HIJK.Kimwitu++ :cal SetSyn("kwt")<CR>
+an 50.50.720 &Syntax.HIJK.Kivy :cal SetSyn("kivy")<CR>
+an 50.50.730 &Syntax.HIJK.KixTart :cal SetSyn("kix")<CR>
an 50.60.100 &Syntax.L.Lace :cal SetSyn("lace")<CR>
an 50.60.110 &Syntax.L.LamdaProlog :cal SetSyn("lprolog")<CR>
an 50.60.120 &Syntax.L.Latte :cal SetSyn("latte")<CR>
@@ -343,34 +349,35 @@ an 50.70.240 &Syntax.M.Mathematica :cal SetSyn("mma")<CR>
an 50.70.250 &Syntax.M.Matlab :cal SetSyn("matlab")<CR>
an 50.70.260 &Syntax.M.Maxima :cal SetSyn("maxima")<CR>
an 50.70.270 &Syntax.M.MEL\ (for\ Maya) :cal SetSyn("mel")<CR>
-an 50.70.280 &Syntax.M.Messages\ (/var/log) :cal SetSyn("messages")<CR>
-an 50.70.290 &Syntax.M.Metafont :cal SetSyn("mf")<CR>
-an 50.70.300 &Syntax.M.MetaPost :cal SetSyn("mp")<CR>
-an 50.70.310 &Syntax.M.MGL :cal SetSyn("mgl")<CR>
-an 50.70.320 &Syntax.M.MIX :cal SetSyn("mix")<CR>
-an 50.70.330 &Syntax.M.MMIX :cal SetSyn("mmix")<CR>
-an 50.70.340 &Syntax.M.Modconf :cal SetSyn("modconf")<CR>
-an 50.70.350 &Syntax.M.Model :cal SetSyn("model")<CR>
-an 50.70.360 &Syntax.M.Modsim\ III :cal SetSyn("modsim3")<CR>
-an 50.70.370 &Syntax.M.Modula\ 2 :cal SetSyn("modula2")<CR>
-an 50.70.380 &Syntax.M.Modula\ 3 :cal SetSyn("modula3")<CR>
-an 50.70.390 &Syntax.M.Monk :cal SetSyn("monk")<CR>
-an 50.70.400 &Syntax.M.Motorola\ S-Record :cal SetSyn("srec")<CR>
-an 50.70.410 &Syntax.M.Mplayer\ config :cal SetSyn("mplayerconf")<CR>
-an 50.70.420 &Syntax.M.MOO :cal SetSyn("moo")<CR>
-an 50.70.430 &Syntax.M.Mrxvtrc :cal SetSyn("mrxvtrc")<CR>
-an 50.70.440 &Syntax.M.MS-DOS/Windows.4DOS\ \.bat\ file :cal SetSyn("btm")<CR>
-an 50.70.450 &Syntax.M.MS-DOS/Windows.\.bat\/\.cmd\ file :cal SetSyn("dosbatch")<CR>
-an 50.70.460 &Syntax.M.MS-DOS/Windows.\.ini\ file :cal SetSyn("dosini")<CR>
-an 50.70.470 &Syntax.M.MS-DOS/Windows.Message\ text :cal SetSyn("msmessages")<CR>
-an 50.70.480 &Syntax.M.MS-DOS/Windows.Module\ Definition :cal SetSyn("def")<CR>
-an 50.70.490 &Syntax.M.MS-DOS/Windows.Registry :cal SetSyn("registry")<CR>
-an 50.70.500 &Syntax.M.MS-DOS/Windows.Resource\ file :cal SetSyn("rc")<CR>
-an 50.70.510 &Syntax.M.Msql :cal SetSyn("msql")<CR>
-an 50.70.520 &Syntax.M.MuPAD :cal SetSyn("mupad")<CR>
-an 50.70.530 &Syntax.M.Murphi :cal SetSyn("murphi")<CR>
-an 50.70.540 &Syntax.M.MUSHcode :cal SetSyn("mush")<CR>
-an 50.70.550 &Syntax.M.Muttrc :cal SetSyn("muttrc")<CR>
+an 50.70.280 &Syntax.M.Meson :cal SetSyn("meson")<CR>
+an 50.70.290 &Syntax.M.Messages\ (/var/log) :cal SetSyn("messages")<CR>
+an 50.70.300 &Syntax.M.Metafont :cal SetSyn("mf")<CR>
+an 50.70.310 &Syntax.M.MetaPost :cal SetSyn("mp")<CR>
+an 50.70.320 &Syntax.M.MGL :cal SetSyn("mgl")<CR>
+an 50.70.330 &Syntax.M.MIX :cal SetSyn("mix")<CR>
+an 50.70.340 &Syntax.M.MMIX :cal SetSyn("mmix")<CR>
+an 50.70.350 &Syntax.M.Modconf :cal SetSyn("modconf")<CR>
+an 50.70.360 &Syntax.M.Model :cal SetSyn("model")<CR>
+an 50.70.370 &Syntax.M.Modsim\ III :cal SetSyn("modsim3")<CR>
+an 50.70.380 &Syntax.M.Modula\ 2 :cal SetSyn("modula2")<CR>
+an 50.70.390 &Syntax.M.Modula\ 3 :cal SetSyn("modula3")<CR>
+an 50.70.400 &Syntax.M.Monk :cal SetSyn("monk")<CR>
+an 50.70.410 &Syntax.M.Motorola\ S-Record :cal SetSyn("srec")<CR>
+an 50.70.420 &Syntax.M.Mplayer\ config :cal SetSyn("mplayerconf")<CR>
+an 50.70.430 &Syntax.M.MOO :cal SetSyn("moo")<CR>
+an 50.70.440 &Syntax.M.Mrxvtrc :cal SetSyn("mrxvtrc")<CR>
+an 50.70.450 &Syntax.M.MS-DOS/Windows.4DOS\ \.bat\ file :cal SetSyn("btm")<CR>
+an 50.70.460 &Syntax.M.MS-DOS/Windows.\.bat\/\.cmd\ file :cal SetSyn("dosbatch")<CR>
+an 50.70.470 &Syntax.M.MS-DOS/Windows.\.ini\ file :cal SetSyn("dosini")<CR>
+an 50.70.480 &Syntax.M.MS-DOS/Windows.Message\ text :cal SetSyn("msmessages")<CR>
+an 50.70.490 &Syntax.M.MS-DOS/Windows.Module\ Definition :cal SetSyn("def")<CR>
+an 50.70.500 &Syntax.M.MS-DOS/Windows.Registry :cal SetSyn("registry")<CR>
+an 50.70.510 &Syntax.M.MS-DOS/Windows.Resource\ file :cal SetSyn("rc")<CR>
+an 50.70.520 &Syntax.M.Msql :cal SetSyn("msql")<CR>
+an 50.70.530 &Syntax.M.MuPAD :cal SetSyn("mupad")<CR>
+an 50.70.540 &Syntax.M.Murphi :cal SetSyn("murphi")<CR>
+an 50.70.550 &Syntax.M.MUSHcode :cal SetSyn("mush")<CR>
+an 50.70.560 &Syntax.M.Muttrc :cal SetSyn("muttrc")<CR>
an 50.80.100 &Syntax.NO.N1QL :cal SetSyn("n1ql")<CR>
an 50.80.110 &Syntax.NO.Nanorc :cal SetSyn("nanorc")<CR>
an 50.80.120 &Syntax.NO.Nastran\ input/DMAP :cal SetSyn("nastran")<CR>
@@ -442,25 +449,26 @@ an 50.100.110 &Syntax.R.R.R\ help :cal SetSyn("rhelp")<CR>
an 50.100.120 &Syntax.R.R.R\ noweb :cal SetSyn("rnoweb")<CR>
an 50.100.130 &Syntax.R.Racc\ input :cal SetSyn("racc")<CR>
an 50.100.140 &Syntax.R.Radiance :cal SetSyn("radiance")<CR>
-an 50.100.150 &Syntax.R.Ratpoison :cal SetSyn("ratpoison")<CR>
-an 50.100.160 &Syntax.R.RCS.RCS\ log\ output :cal SetSyn("rcslog")<CR>
-an 50.100.170 &Syntax.R.RCS.RCS\ file :cal SetSyn("rcs")<CR>
-an 50.100.180 &Syntax.R.Readline\ config :cal SetSyn("readline")<CR>
-an 50.100.190 &Syntax.R.Rebol :cal SetSyn("rebol")<CR>
-an 50.100.200 &Syntax.R.ReDIF :cal SetSyn("redif")<CR>
-an 50.100.210 &Syntax.R.Relax\ NG :cal SetSyn("rng")<CR>
-an 50.100.220 &Syntax.R.Remind :cal SetSyn("remind")<CR>
-an 50.100.230 &Syntax.R.Relax\ NG\ compact :cal SetSyn("rnc")<CR>
-an 50.100.240 &Syntax.R.Renderman.Renderman\ Shader\ Lang :cal SetSyn("sl")<CR>
-an 50.100.250 &Syntax.R.Renderman.Renderman\ Interface\ Bytestream :cal SetSyn("rib")<CR>
-an 50.100.260 &Syntax.R.Resolv\.conf :cal SetSyn("resolv")<CR>
-an 50.100.270 &Syntax.R.Reva\ Forth :cal SetSyn("reva")<CR>
-an 50.100.280 &Syntax.R.Rexx :cal SetSyn("rexx")<CR>
-an 50.100.290 &Syntax.R.Robots\.txt :cal SetSyn("robots")<CR>
-an 50.100.300 &Syntax.R.RockLinux\ package\ desc\. :cal SetSyn("desc")<CR>
-an 50.100.310 &Syntax.R.Rpcgen :cal SetSyn("rpcgen")<CR>
-an 50.100.320 &Syntax.R.RPL/2 :cal SetSyn("rpl")<CR>
-an 50.100.330 &Syntax.R.ReStructuredText :cal SetSyn("rst")<CR>
+an 50.100.150 &Syntax.R.Raml :cal SetSyn("raml")<CR>
+an 50.100.160 &Syntax.R.Ratpoison :cal SetSyn("ratpoison")<CR>
+an 50.100.170 &Syntax.R.RCS.RCS\ log\ output :cal SetSyn("rcslog")<CR>
+an 50.100.180 &Syntax.R.RCS.RCS\ file :cal SetSyn("rcs")<CR>
+an 50.100.190 &Syntax.R.Readline\ config :cal SetSyn("readline")<CR>
+an 50.100.200 &Syntax.R.Rebol :cal SetSyn("rebol")<CR>
+an 50.100.210 &Syntax.R.ReDIF :cal SetSyn("redif")<CR>
+an 50.100.220 &Syntax.R.Relax\ NG :cal SetSyn("rng")<CR>
+an 50.100.230 &Syntax.R.Remind :cal SetSyn("remind")<CR>
+an 50.100.240 &Syntax.R.Relax\ NG\ compact :cal SetSyn("rnc")<CR>
+an 50.100.250 &Syntax.R.Renderman.Renderman\ Shader\ Lang :cal SetSyn("sl")<CR>
+an 50.100.260 &Syntax.R.Renderman.Renderman\ Interface\ Bytestream :cal SetSyn("rib")<CR>
+an 50.100.270 &Syntax.R.Resolv\.conf :cal SetSyn("resolv")<CR>
+an 50.100.280 &Syntax.R.Reva\ Forth :cal SetSyn("reva")<CR>
+an 50.100.290 &Syntax.R.Rexx :cal SetSyn("rexx")<CR>
+an 50.100.300 &Syntax.R.Robots\.txt :cal SetSyn("robots")<CR>
+an 50.100.310 &Syntax.R.RockLinux\ package\ desc\. :cal SetSyn("desc")<CR>
+an 50.100.320 &Syntax.R.Rpcgen :cal SetSyn("rpcgen")<CR>
+an 50.100.330 &Syntax.R.RPL/2 :cal SetSyn("rpl")<CR>
+an 50.100.340 &Syntax.R.ReStructuredText :cal SetSyn("rst")<CR>
an 50.110.100 &Syntax.M.ReStructuredText\ with\ R\ statements :cal SetSyn("rrst")<CR>
an 50.120.100 &Syntax.R.RTF :cal SetSyn("rtf")<CR>
an 50.120.110 &Syntax.R.Ruby :cal SetSyn("ruby")<CR>
@@ -581,6 +589,8 @@ an 50.150.370 &Syntax.T.Trustees :cal SetSyn("trustees")<CR>
an 50.150.380 &Syntax.T.TSS.Command\ Line :cal SetSyn("tsscl")<CR>
an 50.150.390 &Syntax.T.TSS.Geometry :cal SetSyn("tssgm")<CR>
an 50.150.400 &Syntax.T.TSS.Optics :cal SetSyn("tssop")<CR>
+an 50.150.410 &Syntax.T.Typescript :cal SetSyn("typescript")<CR>
+an 50.150.420 &Syntax.T.TypescriptReact :cal SetSyn("typescriptreact")<CR>
an 50.160.100 &Syntax.UV.Udev\ config :cal SetSyn("udevconf")<CR>
an 50.160.110 &Syntax.UV.Udev\ permissions :cal SetSyn("udevperm")<CR>
an 50.160.120 &Syntax.UV.Udev\ rules :cal SetSyn("udevrules")<CR>
@@ -607,32 +617,33 @@ an 50.160.330 &Syntax.UV.VSE\ JCL :cal SetSyn("vsejcl")<CR>
an 50.170.100 &Syntax.WXYZ.WEB.CWEB :cal SetSyn("cweb")<CR>
an 50.170.110 &Syntax.WXYZ.WEB.WEB :cal SetSyn("web")<CR>
an 50.170.120 &Syntax.WXYZ.WEB.WEB\ Changes :cal SetSyn("change")<CR>
-an 50.170.130 &Syntax.WXYZ.Webmacro :cal SetSyn("webmacro")<CR>
-an 50.170.140 &Syntax.WXYZ.Website\ MetaLanguage :cal SetSyn("wml")<CR>
-an 50.170.160 &Syntax.WXYZ.wDiff :cal SetSyn("wdiff")<CR>
-an 50.170.180 &Syntax.WXYZ.Wget\ config :cal SetSyn("wget")<CR>
-an 50.170.190 &Syntax.WXYZ.Whitespace\ (add) :cal SetSyn("whitespace")<CR>
-an 50.170.200 &Syntax.WXYZ.WildPackets\ EtherPeek\ Decoder :cal SetSyn("dcd")<CR>
-an 50.170.210 &Syntax.WXYZ.WinBatch/Webbatch :cal SetSyn("winbatch")<CR>
-an 50.170.220 &Syntax.WXYZ.Windows\ Scripting\ Host :cal SetSyn("wsh")<CR>
-an 50.170.230 &Syntax.WXYZ.WSML :cal SetSyn("wsml")<CR>
-an 50.170.240 &Syntax.WXYZ.WvDial :cal SetSyn("wvdial")<CR>
-an 50.170.260 &Syntax.WXYZ.X\ Keyboard\ Extension :cal SetSyn("xkb")<CR>
-an 50.170.270 &Syntax.WXYZ.X\ Pixmap :cal SetSyn("xpm")<CR>
-an 50.170.280 &Syntax.WXYZ.X\ Pixmap\ (2) :cal SetSyn("xpm2")<CR>
-an 50.170.290 &Syntax.WXYZ.X\ resources :cal SetSyn("xdefaults")<CR>
-an 50.170.300 &Syntax.WXYZ.XBL :cal SetSyn("xbl")<CR>
-an 50.170.310 &Syntax.WXYZ.Xinetd\.conf :cal SetSyn("xinetd")<CR>
-an 50.170.320 &Syntax.WXYZ.Xmodmap :cal SetSyn("xmodmap")<CR>
-an 50.170.330 &Syntax.WXYZ.Xmath :cal SetSyn("xmath")<CR>
-an 50.170.340 &Syntax.WXYZ.XML :cal SetSyn("xml")<CR>
-an 50.170.350 &Syntax.WXYZ.XML\ Schema\ (XSD) :cal SetSyn("xsd")<CR>
-an 50.170.360 &Syntax.WXYZ.XQuery :cal SetSyn("xquery")<CR>
-an 50.170.370 &Syntax.WXYZ.Xslt :cal SetSyn("xslt")<CR>
-an 50.170.380 &Syntax.WXYZ.XFree86\ Config :cal SetSyn("xf86conf")<CR>
-an 50.170.400 &Syntax.WXYZ.YAML :cal SetSyn("yaml")<CR>
-an 50.170.410 &Syntax.WXYZ.Yacc :cal SetSyn("yacc")<CR>
-an 50.170.430 &Syntax.WXYZ.Zimbu :cal SetSyn("zimbu")<CR>
+an 50.170.130 &Syntax.WXYZ.WebAssembly :cal SetSyn("wast")<CR>
+an 50.170.140 &Syntax.WXYZ.Webmacro :cal SetSyn("webmacro")<CR>
+an 50.170.150 &Syntax.WXYZ.Website\ MetaLanguage :cal SetSyn("wml")<CR>
+an 50.170.170 &Syntax.WXYZ.wDiff :cal SetSyn("wdiff")<CR>
+an 50.170.190 &Syntax.WXYZ.Wget\ config :cal SetSyn("wget")<CR>
+an 50.170.200 &Syntax.WXYZ.Whitespace\ (add) :cal SetSyn("whitespace")<CR>
+an 50.170.210 &Syntax.WXYZ.WildPackets\ EtherPeek\ Decoder :cal SetSyn("dcd")<CR>
+an 50.170.220 &Syntax.WXYZ.WinBatch/Webbatch :cal SetSyn("winbatch")<CR>
+an 50.170.230 &Syntax.WXYZ.Windows\ Scripting\ Host :cal SetSyn("wsh")<CR>
+an 50.170.240 &Syntax.WXYZ.WSML :cal SetSyn("wsml")<CR>
+an 50.170.250 &Syntax.WXYZ.WvDial :cal SetSyn("wvdial")<CR>
+an 50.170.270 &Syntax.WXYZ.X\ Keyboard\ Extension :cal SetSyn("xkb")<CR>
+an 50.170.280 &Syntax.WXYZ.X\ Pixmap :cal SetSyn("xpm")<CR>
+an 50.170.290 &Syntax.WXYZ.X\ Pixmap\ (2) :cal SetSyn("xpm2")<CR>
+an 50.170.300 &Syntax.WXYZ.X\ resources :cal SetSyn("xdefaults")<CR>
+an 50.170.310 &Syntax.WXYZ.XBL :cal SetSyn("xbl")<CR>
+an 50.170.320 &Syntax.WXYZ.Xinetd\.conf :cal SetSyn("xinetd")<CR>
+an 50.170.330 &Syntax.WXYZ.Xmodmap :cal SetSyn("xmodmap")<CR>
+an 50.170.340 &Syntax.WXYZ.Xmath :cal SetSyn("xmath")<CR>
+an 50.170.350 &Syntax.WXYZ.XML :cal SetSyn("xml")<CR>
+an 50.170.360 &Syntax.WXYZ.XML\ Schema\ (XSD) :cal SetSyn("xsd")<CR>
+an 50.170.370 &Syntax.WXYZ.XQuery :cal SetSyn("xquery")<CR>
+an 50.170.380 &Syntax.WXYZ.Xslt :cal SetSyn("xslt")<CR>
+an 50.170.390 &Syntax.WXYZ.XFree86\ Config :cal SetSyn("xf86conf")<CR>
+an 50.170.410 &Syntax.WXYZ.YAML :cal SetSyn("yaml")<CR>
+an 50.170.420 &Syntax.WXYZ.Yacc :cal SetSyn("yacc")<CR>
+an 50.170.440 &Syntax.WXYZ.Zimbu :cal SetSyn("zimbu")<CR>
" The End Of The Syntax Menu
diff --git a/runtime/syntax/man.vim b/runtime/syntax/man.vim
index 6afe56a6e3..7ac02c3f63 100644
--- a/runtime/syntax/man.vim
+++ b/runtime/syntax/man.vim
@@ -1,4 +1,4 @@
-" Maintainer: Anmol Sethi <anmol@aubble.com>
+" Maintainer: Anmol Sethi <hi@nhooyr.io>
" Previous Maintainer: SungHyun Nam <goweol@gmail.com>
if exists('b:current_syntax')
@@ -30,6 +30,7 @@ endif
if !exists('b:man_sect')
call man#init_pager()
endif
+
if b:man_sect =~# '^[023]'
syntax case match
syntax include @c $VIMRUNTIME/syntax/c.vim
diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim
index b177fe641d..6f0818c845 100644
--- a/runtime/syntax/vim.vim
+++ b/runtime/syntax/vim.vim
@@ -558,7 +558,7 @@ syn match vimHiGuiFontname contained "'[a-zA-Z\-* ]\+'"
syn match vimHiGuiRgb contained "#\x\{6}"
" Highlighting: hi group key=arg ... {{{2
-syn cluster vimHiCluster contains=vimGroup,vimHiGroup,vimHiTerm,vimHiCTerm,vimHiStartStop,vimHiCtermFgBg,vimHiGui,vimHiGuiFont,vimHiGuiFgBg,vimHiKeyError,vimNotation
+syn cluster vimHiCluster contains=vimGroup,vimHiBlend,vimHiGroup,vimHiTerm,vimHiCTerm,vimHiStartStop,vimHiCtermFgBg,vimHiGui,vimHiGuiFont,vimHiGuiFgBg,vimHiKeyError,vimNotation
syn region vimHiKeyList contained oneline start="\i\+" skip="\\\\\|\\|" end="$\||" contains=@vimHiCluster
if !exists("g:vimsyn_noerror") && !exists("g:vimsyn_vimhikeyerror")
syn match vimHiKeyError contained "\i\+="he=e-1
@@ -571,6 +571,7 @@ syn match vimHiGui contained "\cgui="he=e-1 nextgroup=vimHiAttribList
syn match vimHiGuiFont contained "\cfont="he=e-1 nextgroup=vimHiFontname
syn match vimHiGuiFgBg contained "\cgui\%([fb]g\|sp\)="he=e-1 nextgroup=vimHiGroup,vimHiGuiFontname,vimHiGuiRgb,vimFgBgAttrib
syn match vimHiTermcap contained "\S\+" contains=vimNotation
+syn match vimHiBlend contained "\cblend="he=e-1 nextgroup=vimHiNmbr
syn match vimHiNmbr contained '\d\+'
" Highlight: clear {{{2
@@ -850,6 +851,7 @@ if !exists("skip_vim_syntax_inits")
hi def link vimGroupSpecial Special
hi def link vimGroup Type
hi def link vimHiAttrib PreProc
+ hi def link vimHiBlend vimHiTerm
hi def link vimHiClear vimHighlight
hi def link vimHiCtermFgBg vimHiTerm
hi def link vimHiCTerm vimHiTerm
diff --git a/runtime/tutor/en/vim-01-beginner.tutor b/runtime/tutor/en/vim-01-beginner.tutor
index 4e6154b24a..5ae0fde0da 100644
--- a/runtime/tutor/en/vim-01-beginner.tutor
+++ b/runtime/tutor/en/vim-01-beginner.tutor
@@ -91,7 +91,7 @@ NOTE: [:q!](:q) <Enter> discards any changes you made. In a few lessons you
** Press `x`{normal} to delete the character under the cursor. **
- 1. Move the cursor to the line below marked --->.
+ 1. Move the cursor to the line below marked ✗.
2. To fix the errors, move the cursor until it is on top of the
character to be deleted.
@@ -111,7 +111,7 @@ NOTE: As you go through this tutor, do not try to memorize, learn by
** Press `i`{normal} to insert text. **
- 1. Move the cursor to the first line below marked --->.
+ 1. Move the cursor to the first line below marked ✗.
2. To make the first line the same as the second, move the cursor on top
of the first character AFTER where the text is to be inserted.
@@ -130,7 +130,7 @@ There is some text missing from this line.
** Press `A`{normal} to append text. **
- 1. Move the cursor to the first line below marked --->.
+ 1. Move the cursor to the first line below marked ✗.
It does not matter on what character the cursor is in that line.
2. Press [A](A) and type in the necessary additions.
@@ -138,7 +138,7 @@ There is some text missing from this line.
3. As the text has been appended press `<Esc>`{normal} to return to Normal
mode.
- 4. Move the cursor to the second line marked ---> and repeat
+ 4. Move the cursor to the second line marked ✗ and repeat
steps 2 and 3 to correct this sentence.
There is some text missing from th
@@ -211,7 +211,7 @@ Now continue with Lesson 2.
1. Press `<Esc>`{normal} to make sure you are in Normal mode.
- 2. Move the cursor to the line below marked --->.
+ 2. Move the cursor to the line below marked ✗.
3. Move the cursor to the beginning of a word that needs to be deleted.
@@ -227,7 +227,7 @@ There are a some words fun that don't belong paper in this sentence.
1. Press `<Esc>`{normal} to make sure you are in Normal mode.
- 2. Move the cursor to the line below marked --->.
+ 2. Move the cursor to the line below marked ✗.
3. Move the cursor to the end of the correct line (AFTER the first . ).
@@ -263,7 +263,7 @@ NOTE: Pressing just the motion while in Normal mode without an operator
** Typing a number before a motion repeats it that many times. **
- 1. Move the cursor to the start of the line marked ---> below.
+ 1. Move the cursor to the start of the line marked ✓ below.
2. Type `2w`{normal} to move the cursor two words forward.
@@ -285,7 +285,7 @@ In the combination of the delete operator and a motion mentioned above you
insert a count before the motion to delete more:
d number motion
- 1. Move the cursor to the first UPPER CASE word in the line marked --->.
+ 1. Move the cursor to the first UPPER CASE word in the line marked ✗.
2. Type `d2w`{normal} to delete the two UPPER CASE words
@@ -318,7 +318,7 @@ it would be easier to simply type two d's to delete a line.
** Press `u`{normal} to undo the last commands, `U`{normal} to fix a whole line. **
- 1. Move the cursor to the line below marked ---> and place it on the
+ 1. Move the cursor to the line below marked ✗ and place it on the
first error.
2. Type `x`{normal} to delete the first unwanted character.
3. Now type `u`{normal} to undo the last command executed.
@@ -359,7 +359,7 @@ Fiix the errors oon thhis line and reeplace them witth undo.
** Type `p`{normal} to put previously deleted text after the cursor. **
- 1. Move the cursor to the first ---> line below.
+ 1. Move the cursor to the first ✓ line below.
2. Type `dd`{normal} to delete the line and store it in a Vim register.
@@ -378,7 +378,7 @@ a) Roses are red,
** Type `rx`{normal} to replace the character at the cursor with x. **
- 1. Move the cursor to the first line below marked --->.
+ 1. Move the cursor to the first line below marked ✗.
2. Move the cursor so that it is on top of the first error.
@@ -397,7 +397,7 @@ NOTE: Remember that you should be learning by doing, not memorization.
** To change until the end of a word, type `ce`{normal}. **
- 1. Move the cursor to the first line below marked --->.
+ 1. Move the cursor to the first line below marked ✗.
2. Place the cursor on the "u" in "lubw".
@@ -423,7 +423,7 @@ Notice that [c](c)e deletes the word and places you in Insert mode.
2. The motions are the same, such as `w`{normal} (word) and `$`{normal} (end of line).
- 3. Move to the first line below marked --->.
+ 3. Move to the first line below marked ✗.
4. Move the cursor to the first error.
@@ -503,7 +503,7 @@ NOTE: When the search reaches the end of the file it will continue at the
** Type `%`{normal} to find a matching ),], or }. **
- 1. Place the cursor on any (, [, or { in the line below marked --->.
+ 1. Place the cursor on any (, [, or { in the line below marked ✓.
2. Now type the [%](%) character.
@@ -521,7 +521,7 @@ NOTE: This is very useful in debugging a program with unmatched parentheses!
** Type `:s/old/new/g` to substitute "new" for "old". **
- 1. Move the cursor to the line below marked --->.
+ 1. Move the cursor to the line below marked ✗.
2. Type
~~~ cmd
@@ -725,7 +725,7 @@ NOTE: You can also read the output of an external command. For example,
** Type `o`{normal} to open a line below the cursor and place you in Insert mode. **
- 1. Move the cursor to the line below marked --->.
+ 1. Move the cursor to the line below marked ✓.
2. Type the lowercase letter `o`{normal} to [open](o) up a line BELOW the
cursor and place you in Insert mode.
@@ -743,7 +743,7 @@ Open up a line above this by typing O while the cursor is on this line.
** Type `a`{normal} to insert text AFTER the cursor. **
- 1. Move the cursor to the start of the line below marked --->.
+ 1. Move the cursor to the start of the line below marked ✗.
2. Press `e`{normal} until the cursor is on the end of "li".
@@ -766,7 +766,7 @@ NOTE: [a](a), [i](i) and [A](A) all go to the same Insert mode, the only
** Type a capital `R`{normal} to replace more than one character. **
- 1. Move the cursor to the first line below marked --->. Move the cursor to
+ 1. Move the cursor to the first line below marked ✗. Move the cursor to
the beginning of the first "xxx".
2. Now press `R`{normal} ([capital R](R)) and type the number below it in the
@@ -787,7 +787,7 @@ NOTE: Replace mode is like Insert mode, but every typed character deletes an
** Use the `y`{normal} operator to copy text and `p`{normal} to paste it. **
- 1. Go to the line marked with ---> below and place the cursor after "a)".
+ 1. Go to the line marked with ✓ below and place the cursor after "a)".
2. Start Visual mode with `v`{normal} and move the cursor to just before
"first".
@@ -805,7 +805,7 @@ NOTE: Replace mode is like Insert mode, but every typed character deletes an
end of the next line with `j$`{normal} and put the text there with `p`{normal}
a) This is the first item.
- b)
+b)
NOTE: you can use `y`{normal} as an operator: `yw`{normal} yanks one word.
diff --git a/runtime/tutor/en/vim-01-beginner.tutor.json b/runtime/tutor/en/vim-01-beginner.tutor.json
index 2f87d7543f..af22cf2aca 100644
--- a/runtime/tutor/en/vim-01-beginner.tutor.json
+++ b/runtime/tutor/en/vim-01-beginner.tutor.json
@@ -1,43 +1,45 @@
{
- "expect": {
- "24": -1,
- "103": "The cow jumped over the moon.",
- "124": "There is some text missing from this line.",
- "125": "There is some text missing from this line.",
- "144": "There is some text missing from this line.",
- "145": "There is some text missing from this line.",
- "146": "There is also some text missing here.",
- "147": "There is also some text missing here.",
- "220": "There are some words that don't belong in this sentence.",
- "236": "Somebody typed the end of this line twice.",
- "276": -1,
- "295": "This line of words is cleaned up.",
- "309": -1,
- "310": -1,
- "311": -1,
- "312": -1,
- "313": -1,
- "314": -1,
- "315": -1,
- "332": "Fix the errors on this line and replace them with undo.",
- "372": -1,
- "373": -1,
- "374": -1,
- "375": -1,
- "389": "When this line was typed in, someone pressed some wrong keys!",
- "390": "When this line was typed in, someone pressed some wrong keys!",
- "411": "This line has a few words that need changing using the change operator.",
- "412": "This line has a few words that need changing using the change operator.",
- "432": "The end of this line needs to be corrected using the c$ command.",
- "433": "The end of this line needs to be corrected using the c$ command.",
- "497": -1,
- "516": -1,
- "541": "Usually the best time to see the flowers is in the spring.",
- "759": "This line will allow you to practice appending text to a line.",
- "760": "This line will allow you to practice appending text to a line.",
- "780": "Adding 123 to 456 gives you 579.",
- "781": "Adding 123 to 456 gives you 579.",
- "807": "a) This is the first item.",
- "808": " b) This is the second item."
- }
+ "expect": {
+ "24": -1,
+ "103": "The cow jumped over the moon.",
+ "124": "There is some text missing from this line.",
+ "125": "There is some text missing from this line.",
+ "144": "There is some text missing from this line.",
+ "145": "There is some text missing from this line.",
+ "146": "There is also some text missing here.",
+ "147": "There is also some text missing here.",
+ "220": "There are some words that don't belong in this sentence.",
+ "236": "Somebody typed the end of this line twice.",
+ "276": -1,
+ "295": "This line of words is cleaned up.",
+ "309": -1,
+ "310": -1,
+ "311": -1,
+ "312": -1,
+ "313": -1,
+ "314": -1,
+ "315": -1,
+ "332": "Fix the errors on this line and replace them with undo.",
+ "372": -1,
+ "373": -1,
+ "374": -1,
+ "375": -1,
+ "389": "When this line was typed in, someone pressed some wrong keys!",
+ "390": "When this line was typed in, someone pressed some wrong keys!",
+ "411": "This line has a few words that need changing using the change operator.",
+ "412": "This line has a few words that need changing using the change operator.",
+ "432": "The end of this line needs to be corrected using the `c$` command.",
+ "433": "The end of this line needs to be corrected using the `c$` command.",
+ "497": -1,
+ "516": -1,
+ "541": "Usually the best time to see the flowers is in the spring.",
+ "735": -1,
+ "740": -1,
+ "759": "This line will allow you to practice appending text to a line.",
+ "760": "This line will allow you to practice appending text to a line.",
+ "780": "Adding 123 to 456 gives you 579.",
+ "781": "Adding 123 to 456 gives you 579.",
+ "807": "a) This is the first item.",
+ "808": "b) This is the second item."
+ }
}
diff --git a/runtime/tutor/tutor.tutor b/runtime/tutor/tutor.tutor
index c937bd686a..b46fcc4836 100644
--- a/runtime/tutor/tutor.tutor
+++ b/runtime/tutor/tutor.tutor
@@ -118,7 +118,7 @@ and are hidden by default. Links to them look like
\[label\]\(\*anchor\*\)
-6. Add the appropiate link:
+6. Add the appropriate link:
A link to the Links section
A link to the [Links](*links*) section
diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py
index 373a58d11e..4d71d5e15e 100755
--- a/scripts/gen_vimdoc.py
+++ b/scripts/gen_vimdoc.py
@@ -36,11 +36,12 @@ import shutil
import textwrap
import subprocess
import collections
+import msgpack
from xml.dom import minidom
-if sys.version_info[0] < 3:
- print("use Python 3")
+if sys.version_info[0] < 3 or sys.version_info[1] < 5:
+ print("requires Python 3.5+")
sys.exit(1)
DEBUG = ('DEBUG' in os.environ)
@@ -84,7 +85,7 @@ CONFIG = {
'append_only': [],
},
'lua': {
- 'filename': 'if_lua.txt',
+ 'filename': 'lua.txt',
'section_start_token': '*lua-vim*',
'section_order': [
'vim.lua',
@@ -453,7 +454,7 @@ def parse_source_xml(filename, mode):
"""
global xrefs
xrefs = set()
- functions = []
+ functions = {} # Map of func_name:docstring.
deprecated_functions = []
dom = minidom.parse(filename)
@@ -577,11 +578,13 @@ def parse_source_xml(filename, mode):
if 'Deprecated' in xrefs:
deprecated_functions.append(func_doc)
elif name.startswith(CONFIG[mode]['func_name_prefix']):
- functions.append(func_doc)
+ functions[name] = func_doc
xrefs.clear()
- return '\n\n'.join(functions), '\n\n'.join(deprecated_functions)
+ return ('\n\n'.join(list(functions.values())),
+ '\n\n'.join(deprecated_functions),
+ functions)
def delete_lines_below(filename, tokenstr):
@@ -604,6 +607,13 @@ def gen_docs(config):
Doxygen is called and configured through stdin.
"""
for mode in CONFIG:
+ functions = {} # Map of func_name:docstring.
+ mpack_file = os.path.join(
+ base_dir, 'runtime', 'doc',
+ CONFIG[mode]['filename'].replace('.txt', '.mpack'))
+ if os.path.exists(mpack_file):
+ os.remove(mpack_file)
+
output_dir = out_dir.format(mode=mode)
p = subprocess.Popen(['doxygen', '-'], stdin=subprocess.PIPE)
p.communicate(
@@ -645,14 +655,15 @@ def gen_docs(config):
filename = get_text(find_first(compound, 'name'))
if filename.endswith('.c') or filename.endswith('.lua'):
- functions, deprecated = parse_source_xml(
- os.path.join(base, '%s.xml' %
- compound.getAttribute('refid')), mode)
+ functions_text, deprecated_text, fns = parse_source_xml(
+ os.path.join(base, '{}.xml'.format(
+ compound.getAttribute('refid'))), mode)
+ # Collect functions from all modules (for the current `mode`).
+ functions = {**functions, **fns}
- if not functions and not deprecated:
+ if not functions_text and not deprecated_text:
continue
-
- if functions or deprecated:
+ else:
name = os.path.splitext(os.path.basename(filename))[0]
if name == 'ui':
name = name.upper()
@@ -665,12 +676,12 @@ def gen_docs(config):
if intro:
doc += '\n\n' + intro
- if functions:
- doc += '\n\n' + functions
+ if functions_text:
+ doc += '\n\n' + functions_text
- if INCLUDE_DEPRECATED and deprecated:
+ if INCLUDE_DEPRECATED and deprecated_text:
doc += '\n\n\nDeprecated %s Functions: ~\n\n' % name
- doc += deprecated
+ doc += deprecated_text
if doc:
filename = os.path.basename(filename)
@@ -713,6 +724,8 @@ def gen_docs(config):
delete_lines_below(doc_file, CONFIG[mode]['section_start_token'])
with open(doc_file, 'ab') as fp:
fp.write(docs.encode('utf8'))
+ with open(mpack_file, 'wb') as fp:
+ fp.write(msgpack.packb(functions, use_bin_type=True))
shutil.rmtree(output_dir)
diff --git a/scripts/lua2dox.lua b/scripts/lua2dox.lua
index 77cdabcc4b..171621e38d 100644
--- a/scripts/lua2dox.lua
+++ b/scripts/lua2dox.lua
@@ -65,7 +65,7 @@ FILTER_PATTERNS = *.lua=lua2dox_filter
Either add them to the end or find the appropriate entry in Doxyfile.
-There are other lines that you might like to alter, but see futher documentation for details.
+There are other lines that you might like to alter, but see further documentation for details.
<li> When Doxyfile is edited run "doxygen"
@@ -543,7 +543,6 @@ function TLua2DoX_filter.readfile(this,AppStamp,Filename)
local fn = TString_removeCommentFromLine(string_trim(string.sub(line,pos_fn+8)))
if fn_magic then
fn = fn_magic
- fn_magic = nil
end
if string.sub(fn,1,1)=='(' then
@@ -554,49 +553,20 @@ function TLua2DoX_filter.readfile(this,AppStamp,Filename)
-- want to fix for iffy declarations
local open_paren = string.find(fn,'[%({]')
- local fn0 = fn
if open_paren then
- fn0 = string.sub(fn,1,open_paren-1)
-- we might have a missing close paren
if not string.find(fn,'%)') then
fn = fn .. ' ___MissingCloseParenHere___)'
end
end
- local dot = string.find(fn0,'[%.:]')
- if dot then -- it's a method
- local klass = string.sub(fn,1,dot-1)
- local method = string.sub(fn,dot+1)
- --TCore_IO_writeln('function ' .. klass .. '::' .. method .. ftail .. '{}')
- --TCore_IO_writeln(klass .. '::' .. method .. ftail .. '{}')
- outStream:writeln(
- '/*! \\memberof ' .. klass .. ' */ '
- .. method .. '{}'
- )
- else
- -- add vanilla function
-
- outStream:writeln(fn_type .. 'function ' .. fn .. '{}')
- end
+ -- add vanilla function
+ outStream:writeln(fn_type .. 'function ' .. fn .. '{}')
end
else
this:warning(inStream:getLineNo(),'something weird here')
end
fn_magic = nil -- mustn't indavertently use it again
- elseif string.find(line,'=%s*class%(') then
- state = 'in_class' -- it's a class declaration
- local tailComment
- line,tailComment = TString_removeCommentFromLine(line)
- local equals = string.find(line,'=')
- local klass = string_trim(string.sub(line,1,equals-1))
- local tail = string_trim(string.sub(line,equals+1))
- -- class(wibble wibble)
- -- ....v.
- local parent = string.sub(tail,7,-2)
- if #parent>0 then
- parent = ' :public ' .. parent
- end
- outStream:writeln('class ' .. klass .. parent .. '{};')
else
state = '' -- unknown
if #line>0 then -- we don't know what this line means, so just comment it out
diff --git a/scripts/release.sh b/scripts/release.sh
index 29d61370ce..5b4902a2d7 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -80,7 +80,7 @@ _do_bump_commit() {
<release date="'"${__DATE}"'" version="xxx"/>,' runtime/nvim.appdata.xml
rm CMakeLists.txt.bk
rm runtime/nvim.appdata.xml.bk
- nvim +'/NVIM_VERSION' +1new +'exe "norm! iUpdate version numbers!!!\<CR>"' \
+ nvim +'/NVIM_VERSION' +1new +'exe "norm! iUpdate version numbers!!!"' \
-O CMakeLists.txt runtime/nvim.appdata.xml
git add CMakeLists.txt runtime/nvim.appdata.xml
@@ -93,8 +93,8 @@ fi
_do_bump_commit
echo "
Next steps:
- - Double-check NVIM_VERSION_* in CMakeLists.txt
- - Double-check runtime/nvim.appdata.xml
+ - Update runtime/nvim.appdata.xml on _master_
+ - Run tests/CI (version_spec.lua)!
- Push the tag:
git push --follow-tags
- Update the 'stable' tag:
diff --git a/scripts/shadacat.py b/scripts/shadacat.py
index 89846427a5..2b71fc2385 100755
--- a/scripts/shadacat.py
+++ b/scripts/shadacat.py
@@ -66,7 +66,7 @@ except IndexError:
def filt(entry): return True
else:
_filt = filt
- def filt(entry): return eval(_filt, globals(), {'entry': entry})
+ def filt(entry): return eval(_filt, globals(), {'entry': entry}) # noqa
poswidth = len(str(os.stat(fname).st_size or 1000))
diff --git a/scripts/update_version_stamp.lua b/scripts/update_version_stamp.lua
new file mode 100755
index 0000000000..11b521fab6
--- /dev/null
+++ b/scripts/update_version_stamp.lua
@@ -0,0 +1,55 @@
+#!/usr/bin/env lua
+--
+-- Script to update the Git version stamp during build.
+-- This is called via the custom update_version_stamp target in
+-- src/nvim/CMakeLists.txt.
+--
+-- arg[1]: file in which to update the version string
+-- arg[2]: prefix to use always ("vX.Y.Z")
+
+local function die(msg)
+ io.stderr:write(string.format('%s: %s\n', arg[0], msg))
+ -- No error, fall back to using generated "-dev" version.
+ os.exit(0)
+end
+
+local function iswin()
+ return package.config:sub(1,1) == '\\'
+end
+
+if #arg ~= 2 then
+ die(string.format("Expected two args, got %d", #arg))
+end
+
+local versiondeffile = arg[1]
+local prefix = arg[2]
+
+local dev_null = iswin() and 'NUL' or '/dev/null'
+local described = io.popen('git describe --first-parent --dirty 2>'..dev_null):read('*l')
+if not described then
+ described = io.popen('git describe --first-parent --tags --always --dirty'):read('*l')
+end
+if not described then
+ io.open(versiondeffile, 'w'):write('\n')
+ die('git-describe failed, using empty include file.')
+end
+
+-- `git describe` annotates the most recent tagged release; for pre-release
+-- builds we must replace that with the unreleased version.
+local with_prefix = described:gsub("^v%d+%.%d+%.%d+", prefix)
+if described == with_prefix then
+ -- Prepend the prefix always, e.g. with "nightly-12208-g4041b62b9".
+ with_prefix = prefix .. "-" .. described
+end
+
+-- Read existing include file.
+local current = io.open(versiondeffile, 'r')
+if current then
+ current = current:read('*l')
+end
+
+-- Write new include file, if different.
+local new = '#define NVIM_VERSION_MEDIUM "'..with_prefix..'"'
+if current ~= new then
+ io.open(versiondeffile, 'w'):write(new .. '\n')
+end
diff --git a/scripts/vim-patch.sh b/scripts/vim-patch.sh
index 2cc32f0dd0..b6a0df4649 100755
--- a/scripts/vim-patch.sh
+++ b/scripts/vim-patch.sh
@@ -89,7 +89,7 @@ get_vim_sources() {
echo "Cloning Vim into: ${VIM_SOURCE_DIR}"
git clone https://github.com/vim/vim.git "${VIM_SOURCE_DIR}"
cd "${VIM_SOURCE_DIR}"
- else
+ elif [[ "${1-}" == update ]]; then
cd "${VIM_SOURCE_DIR}"
if ! [ -d ".git" ] \
&& ! [ "$(git rev-parse --show-toplevel)" = "${VIM_SOURCE_DIR}" ]; then
@@ -103,6 +103,8 @@ get_vim_sources() {
else
msg_err "Could not update Vim sources; ignoring error."
fi
+ else
+ cd "${VIM_SOURCE_DIR}"
fi
}
@@ -124,7 +126,7 @@ find_git_remote() {
}
# Assign variables for a given Vim tag, patch version, or commit.
-# Might exit in case it cannot be found.
+# Might exit in case it cannot be found, after updating Vim sources.
assign_commit_details() {
local vim_commit_ref
if [[ ${1} =~ v?[0-9]\.[0-9]\.[0-9]{3,4} ]]; then
@@ -146,9 +148,14 @@ assign_commit_details() {
local munge_commit_line=false
fi
- vim_commit=$(git -C "${VIM_SOURCE_DIR}" log -1 --format="%H" "${vim_commit_ref}" --) || {
- >&2 msg_err "Couldn't find Vim revision '${vim_commit_ref}'."
- exit 3
+ local get_vim_commit_cmd="git -C ${VIM_SOURCE_DIR} log -1 --format=%H ${vim_commit_ref} --"
+ vim_commit=$($get_vim_commit_cmd 2>&1) || {
+ # Update Vim sources.
+ get_vim_sources update
+ vim_commit=$($get_vim_commit_cmd 2>&1) || {
+ >&2 msg_err "Couldn't find Vim revision '${vim_commit_ref}': git error: ${vim_commit}."
+ exit 3
+ }
}
vim_commit_url="https://github.com/vim/vim/commit/${vim_commit}"
@@ -366,7 +373,7 @@ submit_pr() {
# Gets all Vim commits since the "start" commit.
list_vim_commits() { (
- cd "${VIM_SOURCE_DIR}" && git log --reverse --format='%H' v8.0.0000..HEAD "$@"
+ cd "${VIM_SOURCE_DIR}" && git log --reverse v8.0.0000..HEAD "$@"
) }
# Prints all (sorted) "vim-patch:xxx" tokens found in the Nvim git log.
@@ -389,6 +396,7 @@ list_vimpatch_numbers() {
}
# Prints a newline-delimited list of Vim commits, for use by scripts.
+# "$1": use extended format?
# "$@" is passed to list_vim_commits, as extra arguments to git-log.
list_missing_vimpatches() {
local token vim_commit vim_tag patch_number
@@ -396,6 +404,13 @@ list_missing_vimpatches() {
declare -A vim_commit_tags
declare -a git_log_args
+ local extended_format=$1; shift
+ if [[ "$extended_format" == 1 ]]; then
+ git_log_args=("--format=%H %s")
+ else
+ git_log_args=("--format=%H")
+ fi
+
# Massage arguments for git-log.
declare -A git_log_replacements=(
[^\(.*/\)?src/nvim/\(.*\)]="\${BASH_REMATCH[1]}src/\${BASH_REMATCH[2]}"
@@ -431,13 +446,27 @@ list_missing_vimpatches() {
# Get missing Vim commits
set +u # Avoid "unbound variable" with bash < 4.4 below.
- for vim_commit in $(list_vim_commits "${git_log_args[@]}"); do
+ local vim_commit info
+ while IFS=' ' read -r line; do
# Check for vim-patch:<commit_hash> (usually runtime updates).
- token="vim-patch:${vim_commit:0:7}"
+ token="vim-patch:${line:0:7}"
if [[ "${tokens[$token]-}" ]]; then
continue
fi
+ # Get commit hash, and optional info from line. This is used in
+ # extended mode, and when using e.g. '--format' manually.
+ vim_commit=${line%% *}
+ if [[ "$vim_commit" == "$line" ]]; then
+ info=
+ else
+ info=${line#* }
+ if [[ -n $info ]]; then
+ # Remove any "patch 8.0.0902: " prefixes, and prefix with ": ".
+ info=": ${info#patch*: }"
+ fi
+ fi
+
vim_tag="${vim_commit_tags[$vim_commit]-}"
if [[ -n "$vim_tag" ]]; then
# Check for vim-patch:<tag> (not commit hash).
@@ -445,26 +474,26 @@ list_missing_vimpatches() {
if [[ "${tokens[$patch_number]-}" ]]; then
continue
fi
- echo "$vim_tag"
+ printf '%s%s\n' "$vim_tag" "$info"
else
- echo "$vim_commit"
+ printf '%s%s\n' "$vim_commit" "$info"
fi
- done
+ done < <(list_vim_commits "${git_log_args[@]}")
set -u
}
# Prints a human-formatted list of Vim commits, with instructional messages.
# Passes "$@" onto list_missing_vimpatches (args for git-log).
show_vimpatches() {
- get_vim_sources
- printf "\nVim patches missing from Neovim:\n"
+ get_vim_sources update
+ printf "Vim patches missing from Neovim:\n"
local -A runtime_commits
for commit in $(git -C "${VIM_SOURCE_DIR}" log --format="%H %D" -- runtime | sed 's/,\? tag: / /g'); do
runtime_commits[$commit]=1
done
- list_missing_vimpatches "$@" | while read -r vim_commit; do
+ list_missing_vimpatches 1 "$@" | while read -r vim_commit; do
if [[ "${runtime_commits[$vim_commit]-}" ]]; then
printf ' • %s (+runtime)\n' "${vim_commit}"
else
@@ -472,18 +501,15 @@ show_vimpatches() {
fi
done
- printf "Instructions:
-
- To port one of the above patches to Neovim, execute
- this script with the patch revision as argument and
- follow the instructions.
+ cat << EOF
- Examples: '%s -p 7.4.487'
- '%s -p 1e8ebf870720e7b671f98f22d653009826304c4f'
+Instructions:
+ To port one of the above patches to Neovim, execute this script with the patch revision as argument and follow the instructions, e.g.
+ '${BASENAME} -p v8.0.1234', or '${BASENAME} -P v8.0.1234'
NOTE: Please port the _oldest_ patch if you possibly can.
- Out-of-order patches increase the possibility of bugs.
-" "${BASENAME}" "${BASENAME}"
+ You can use '${BASENAME} -l path/to/file' to see what patches are missing for a file.
+EOF
}
review_commit() {
@@ -596,7 +622,7 @@ while getopts "hlLMVp:P:g:r:s" opt; do
;;
L)
shift # remove opt
- list_missing_vimpatches "$@"
+ list_missing_vimpatches 0 "$@"
exit 0
;;
M)
@@ -624,7 +650,7 @@ while getopts "hlLMVp:P:g:r:s" opt; do
exit 0
;;
V)
- get_vim_sources
+ get_vim_sources update
exit 0
;;
*)
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 81ffb9adf3..218fdcf649 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,5 +1,6 @@
-name: neovim
-version: git
+name: nvim
+base: core18
+adopt-info: nvim
summary: Vim-fork focused on extensibility and agility.
description: |
Neovim is a project that seeks to aggressively refactor Vim in order to:
@@ -9,22 +10,41 @@ description: |
Enable the implementation of new/modern user interfaces without any modifications to the core source
Improve extensibility with a new plugin architecture
For lots more details, see the wiki!
-confinement: classic
+
+grade: stable # must be 'stable' to release into candidate/stable channels
+confinement: devmode
apps:
- neovim:
- command: usr/local/bin/nvim
- plugs: [network, network-bind, x11]
+ nvim:
+ command: usr/bin/nvim
environment:
HOME: /home/$USER
- VIM: $SNAP/usr/local/share/nvim/runtime
+ VIM: $SNAP/usr/share/nvim
+ VIMRUNTIME: $SNAP/usr/share/nvim/runtime
+ desktop: usr/share/applications/nvim.desktop
parts:
- neovim:
+ nvim:
source: .
+ override-pull: |
+ snapcraftctl pull
+ major="$(awk '/NVIM_VERSION_MAJOR/{gsub(")","",$2); print $2}' CMakeLists.txt)"
+ minor="$(awk '/NVIM_VERSION_MINOR/{gsub(")","",$2); print $2}' CMakeLists.txt)"
+ patch="$(awk '/NVIM_VERSION_PATCH/{gsub(")","",$2); print $2}' CMakeLists.txt)"
+ version_prefix="v$major.$minor.$patch"
+ git_described="$(git describe --first-parent --dirty 2> /dev/null | perl -lpe 's/v\d.\d.\d-//g')"
+ git_described="${git_described:-$(git describe --first-parent --tags --always --dirty)}"
+ snapcraftctl set-version "${version_prefix}-${git_described}"
plugin: make
make-parameters:
- CMAKE_BUILD_TYPE=Release
+ - CMAKE_INSTALL_PREFIX=/usr
+ override-build: |
+ snapcraftctl build
+ # Fix Desktop file
+ sed -i 's|^Exec=nvim|Exec=/snap/bin/nvim.nvim|' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/nvim.desktop
+ sed -i 's|^TryExec=nvim|TryExec=/snap/bin/nvim.nvim|' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/nvim.desktop
+ sed -i 's|^Icon=.*|Icon=${SNAP}/usr/share/pixmaps/nvim.png|' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/nvim.desktop
build-packages:
- ninja-build
- libtool
@@ -33,9 +53,10 @@ parts:
- automake
- cmake
- g++
+ - git
+ - gettext
- pkg-config
- unzip
- snap:
- - usr/local/bin
- - usr/local/share/nvim
- - -usr/local/share/man
+ prime:
+ - -usr/share/man
+
diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt
index aa8100873b..bc8e64dd41 100644
--- a/src/nvim/CMakeLists.txt
+++ b/src/nvim/CMakeLists.txt
@@ -52,7 +52,8 @@ set(UNICODE_TABLES_GENERATOR ${GENERATOR_DIR}/gen_unicode_tables.lua)
set(UNICODE_DIR ${PROJECT_SOURCE_DIR}/unicode)
set(GENERATED_UNICODE_TABLES ${GENERATED_DIR}/unicode_tables.generated.h)
set(VIM_MODULE_FILE ${GENERATED_DIR}/lua/vim_module.generated.h)
-set(VIM_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/src/nvim/lua/vim.lua)
+set(LUA_VIM_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/src/nvim/lua/vim.lua)
+set(LUA_SHARED_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/shared.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")
@@ -85,6 +86,10 @@ file(GLOB NVIM_HEADERS *.h)
file(GLOB XDIFF_SOURCES xdiff/*.c)
file(GLOB XDIFF_HEADERS xdiff/*.h)
+file(GLOB TREESITTER_SOURCES ../tree_sitter/*.c)
+file(GLOB TS_SOURCE_AMALGAM ../tree_sitter/lib.c)
+list(REMOVE_ITEM TREESITTER_SOURCES ${TS_SOURCE_AMALGAM})
+
foreach(subdir
os
api
@@ -141,6 +146,7 @@ set(CONV_SOURCES
ex_cmds.c
ex_docmd.c
fileio.c
+ lua/treesitter.c
mbyte.c
memline.c
message.c
@@ -172,6 +178,9 @@ if(NOT MSVC)
set_source_files_properties(
eval.c PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} -Wno-conversion")
endif()
+
+ # tree-sitter: inlined external project, we don't maintain it. #10124
+ set_source_files_properties(${TREESITTER_SOURCES} PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} -Wno-conversion -Wno-pedantic -Wno-shadow -Wno-missing-prototypes -Wno-unused-variable")
endif()
if(NOT "${MIN_LOG_LEVEL}" MATCHES "^$")
@@ -209,6 +218,34 @@ function(get_preproc_output varname iname)
endif()
endfunction()
+# Handle generating version from Git.
+set(use_git_version 0)
+if(NVIM_VERSION_MEDIUM)
+ message(STATUS "NVIM_VERSION_MEDIUM: ${NVIM_VERSION_MEDIUM}")
+elseif(${CMAKE_VERSION} VERSION_LESS "3.2.0")
+ message(STATUS "Skipping version-string generation (requires CMake 3.2.0+)")
+elseif(EXISTS ${PROJECT_SOURCE_DIR}/.git)
+ find_program(GIT_EXECUTABLE git)
+ if(GIT_EXECUTABLE)
+ message(STATUS "Using NVIM_VERSION_MEDIUM from Git")
+ set(use_git_version 1)
+ else()
+ message(STATUS "Skipping version-string generation (cannot find git)")
+ endif()
+endif()
+if(use_git_version)
+ # Create a update_version_stamp target to update the version during build.
+ file(RELATIVE_PATH relbuild "${PROJECT_SOURCE_DIR}" "${CMAKE_BINARY_DIR}")
+ add_custom_target(update_version_stamp ALL
+ COMMAND ${LUA_PRG} scripts/update_version_stamp.lua
+ ${relbuild}/config/auto/versiondef_git.h
+ "v${NVIM_VERSION_MAJOR}.${NVIM_VERSION_MINOR}.${NVIM_VERSION_PATCH}"
+ WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
+ BYPRODUCTS ${CMAKE_BINARY_DIR}/config/auto/versiondef_git.h)
+else()
+ file(WRITE ${CMAKE_BINARY_DIR}/config/auto/versiondef_git.h "")
+endif()
+
# NVIM_GENERATED_FOR_HEADERS: generated headers to be included in headers
# NVIM_GENERATED_FOR_SOURCES: generated headers to be included in sources
# NVIM_GENERATED_SOURCES: generated source files
@@ -237,12 +274,16 @@ foreach(sfile ${NVIM_SOURCES}
get_preproc_output(PREPROC_OUTPUT ${gf_i})
+ set(depends "${HEADER_GENERATOR}" "${sfile}")
+ if(use_git_version AND "${f}" STREQUAL "version.c")
+ # Ensure auto/versiondef_git.h exists after "make clean".
+ list(APPEND depends update_version_stamp)
+ endif()
add_custom_command(
OUTPUT "${gf_c_h}" "${gf_h_h}"
COMMAND ${CMAKE_C_COMPILER} ${sfile} ${PREPROC_OUTPUT} ${gen_cflags} ${C_FLAGS_ARRAY}
COMMAND "${LUA_PRG}" "${HEADER_GENERATOR}" "${sfile}" "${gf_c_h}" "${gf_h_h}" "${gf_i}"
- DEPENDS "${HEADER_GENERATOR}" "${sfile}"
- )
+ DEPENDS ${depends})
list(APPEND NVIM_GENERATED_FOR_SOURCES "${gf_c_h}")
list(APPEND NVIM_GENERATED_FOR_HEADERS "${gf_h_h}")
if(${d} MATCHES "^api$" AND NOT ${f} MATCHES "^api/helpers.c$")
@@ -277,11 +318,13 @@ add_custom_command(
add_custom_command(
OUTPUT ${VIM_MODULE_FILE}
- COMMAND ${LUA_PRG} ${CHAR_BLOB_GENERATOR} ${VIM_MODULE_SOURCE}
- ${VIM_MODULE_FILE} vim_module
+ COMMAND ${LUA_PRG} ${CHAR_BLOB_GENERATOR} ${VIM_MODULE_FILE}
+ ${LUA_VIM_MODULE_SOURCE} vim_module
+ ${LUA_SHARED_MODULE_SOURCE} shared_module
DEPENDS
${CHAR_BLOB_GENERATOR}
- ${VIM_MODULE_SOURCE}
+ ${LUA_VIM_MODULE_SOURCE}
+ ${LUA_SHARED_MODULE_SOURCE}
)
list(APPEND NVIM_GENERATED_SOURCES
@@ -379,6 +422,7 @@ if(Iconv_LIBRARIES)
endif()
if(WIN32)
+ list(APPEND NVIM_LINK_LIBRARIES netapi32)
list(APPEND NVIM_LINK_LIBRARIES ${WINPTY_LIBRARIES})
endif()
@@ -395,6 +439,7 @@ list(APPEND NVIM_LINK_LIBRARIES
${LIBVTERM_LIBRARIES}
${LIBTERMKEY_LIBRARIES}
${UNIBILIUM_LIBRARIES}
+ ${UTF8PROC_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT}
)
@@ -414,12 +459,13 @@ endif()
add_executable(nvim ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS}
${NVIM_GENERATED_SOURCES} ${NVIM_SOURCES} ${NVIM_HEADERS}
- ${XDIFF_SOURCES} ${XDIFF_HEADERS})
+ ${XDIFF_SOURCES} ${XDIFF_HEADERS} ${TREESITTER_SOURCES})
target_link_libraries(nvim ${NVIM_EXEC_LINK_LIBRARIES})
install_helper(TARGETS nvim)
set_property(TARGET nvim APPEND PROPERTY
INCLUDE_DIRECTORIES ${LUA_PREFERRED_INCLUDE_DIRS})
+set_property(TARGET nvim PROPERTY ENABLE_EXPORTS TRUE)
if(ENABLE_LTO AND (POLICY CMP0069))
include(CheckIPOSupported)
@@ -500,7 +546,7 @@ add_library(
EXCLUDE_FROM_ALL
${NVIM_SOURCES} ${NVIM_GENERATED_SOURCES}
${NVIM_HEADERS} ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS}
- ${XDIFF_SOURCES} ${XDIFF_HEADERS}
+ ${XDIFF_SOURCES} ${XDIFF_HEADERS} ${TREESITTER_SOURCES}
)
set_property(TARGET libnvim APPEND PROPERTY
INCLUDE_DIRECTORIES ${LUA_PREFERRED_INCLUDE_DIRS})
@@ -525,7 +571,7 @@ else()
EXCLUDE_FROM_ALL
${NVIM_SOURCES} ${NVIM_GENERATED_SOURCES}
${NVIM_HEADERS} ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS}
- ${XDIFF_SOURCES} ${XDIFF_HEADERS}
+ ${XDIFF_SOURCES} ${XDIFF_HEADERS} ${TREESITTER_SOURCES}
${UNIT_TEST_FIXTURES}
)
target_link_libraries(nvim-test ${NVIM_TEST_LINK_LIBRARIES})
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c
index 3e1209d1b1..8f5718d97e 100644
--- a/src/nvim/api/buffer.c
+++ b/src/nvim/api/buffer.c
@@ -23,7 +23,10 @@
#include "nvim/memory.h"
#include "nvim/misc1.h"
#include "nvim/ex_cmds.h"
+#include "nvim/map_defs.h"
+#include "nvim/map.h"
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/fileio.h"
#include "nvim/move.h"
#include "nvim/syntax.h"
@@ -101,25 +104,47 @@ String buffer_get_line(Buffer buffer, Integer index, Error *err)
return rv;
}
-/// Activates buffer-update events on a channel, or as lua callbacks.
+/// Activates buffer-update events on a channel, or as Lua callbacks.
+///
+/// Example (Lua): capture buffer updates in a global `events` variable
+/// (use "print(vim.inspect(events))" to see its contents):
+/// <pre>
+/// events = {}
+/// vim.api.nvim_buf_attach(0, false, {
+/// on_lines=function(...) table.insert(events, {...}) end})
+/// </pre>
+///
+/// @see |nvim_buf_detach()|
+/// @see |api-buffer-updates-lua|
///
/// @param channel_id
/// @param buffer Buffer handle, or 0 for current buffer
-/// @param send_buffer Set to true if the initial notification should contain
-/// the whole buffer. If so, the first notification will be a
-/// `nvim_buf_lines_event`. Otherwise, the first notification will be
-/// a `nvim_buf_changedtick_event`. Not used for lua callbacks.
+/// @param send_buffer True if the initial notification should contain the
+/// whole buffer: first notification will be `nvim_buf_lines_event`.
+/// Else the first notification will be `nvim_buf_changedtick_event`.
+/// Not for Lua callbacks.
/// @param opts Optional parameters.
-/// - `on_lines`: lua callback received on change.
-/// - `on_changedtick`: lua callback received on changedtick
-/// increment without text change.
-/// - `utf_sizes`: include UTF-32 and UTF-16 size of
-/// the replaced region.
-/// See |api-buffer-updates-lua| for more information
+/// - on_lines: Lua callback invoked on change.
+/// Return `true` to detach. Args:
+/// - buffer handle
+/// - b:changedtick
+/// - first line that changed (zero-indexed)
+/// - last line that was changed
+/// - last line in the updated range
+/// - byte count of previous contents
+/// - deleted_codepoints (if `utf_sizes` is true)
+/// - deleted_codeunits (if `utf_sizes` is true)
+/// - on_changedtick: Lua callback invoked on changedtick
+/// increment without text change. Args:
+/// - buffer handle
+/// - b:changedtick
+/// - on_detach: Lua callback invoked on detach. Args:
+/// - buffer handle
+/// - utf_sizes: include UTF-32 and UTF-16 size of the replaced
+/// region, as args to `on_lines`.
/// @param[out] err Error details, if any
-/// @return False when updates couldn't be enabled because the buffer isn't
-/// loaded or `opts` contained an invalid key; otherwise True.
-/// TODO: LUA_API_NO_EVAL
+/// @return False if attach failed (invalid parameter, or buffer isn't loaded);
+/// otherwise True. TODO: LUA_API_NO_EVAL
Boolean nvim_buf_attach(uint64_t channel_id,
Buffer buffer,
Boolean send_buffer,
@@ -183,13 +208,14 @@ error:
/// Deactivates buffer-update events on the channel.
///
-/// For Lua callbacks see |api-lua-detach|.
+/// @see |nvim_buf_attach()|
+/// @see |api-lua-detach| for detaching Lua callbacks
///
/// @param channel_id
/// @param buffer Buffer handle, or 0 for current buffer
/// @param[out] err Error details, if any
-/// @return False when updates couldn't be disabled because the buffer
-/// isn't loaded; otherwise True.
+/// @return False if detach failed (because the buffer isn't loaded);
+/// otherwise True.
Boolean nvim_buf_detach(uint64_t channel_id,
Buffer buffer,
Error *err)
@@ -529,7 +555,8 @@ void nvim_buf_set_lines(uint64_t channel_id,
(linenr_T)(end - 1),
MAXLNUM,
(long)extra,
- false);
+ false,
+ kExtmarkUndo);
changed_lines((linenr_T)start, 0, (linenr_T)end, (long)extra, true);
fix_cursor((linenr_T)start, (linenr_T)end, (linenr_T)extra);
@@ -984,6 +1011,254 @@ ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buffer, String name, Error *err)
return rv;
}
+/// Returns position for a given extmark id
+///
+/// @param buffer Buffer handle, or 0 for current buffer
+/// @param ns_id Namespace id from |nvim_create_namespace()|
+/// @param id Extmark id
+/// @param[out] err Error details, if any
+/// @return (row, col) tuple or empty list () if extmark id was absent
+ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id,
+ Integer id, Error *err)
+ FUNC_API_SINCE(7)
+{
+ Array rv = ARRAY_DICT_INIT;
+
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+
+ if (!buf) {
+ return rv;
+ }
+
+ if (!ns_initialized((uint64_t)ns_id)) {
+ api_set_error(err, kErrorTypeValidation, _("Invalid ns_id"));
+ return rv;
+ }
+
+ Extmark *extmark = extmark_from_id(buf, (uint64_t)ns_id, (uint64_t)id);
+ if (!extmark) {
+ return rv;
+ }
+ ADD(rv, INTEGER_OBJ((Integer)extmark->line->lnum-1));
+ ADD(rv, INTEGER_OBJ((Integer)extmark->col-1));
+ return rv;
+}
+
+/// Gets extmarks in "traversal order" from a |charwise| region defined by
+/// buffer positions (inclusive, 0-indexed |api-indexing|).
+///
+/// Region can be given as (row,col) tuples, or valid extmark ids (whose
+/// positions define the bounds). 0 and -1 are understood as (0,0) and (-1,-1)
+/// respectively, thus the following are equivalent:
+///
+/// <pre>
+/// nvim_buf_get_extmarks(0, my_ns, 0, -1, {})
+/// nvim_buf_get_extmarks(0, my_ns, [0,0], [-1,-1], {})
+/// </pre>
+///
+/// If `end` is less than `start`, traversal works backwards. (Useful
+/// with `limit`, to get the first marks prior to a given position.)
+///
+/// Example:
+///
+/// <pre>
+/// local a = vim.api
+/// local pos = a.nvim_win_get_cursor(0)
+/// local ns = a.nvim_create_namespace('my-plugin')
+/// -- Create new extmark at line 1, column 1.
+/// local m1 = a.nvim_buf_set_extmark(0, ns, 0, 0, 0, {})
+/// -- Create new extmark at line 3, column 1.
+/// local m2 = a.nvim_buf_set_extmark(0, ns, 0, 2, 0, {})
+/// -- Get extmarks only from line 3.
+/// local ms = a.nvim_buf_get_extmarks(0, ns, {2,0}, {2,0}, {})
+/// -- Get all marks in this buffer + namespace.
+/// local all = a.nvim_buf_get_extmarks(0, ns, 0, -1, {})
+/// print(vim.inspect(ms))
+/// </pre>
+///
+/// @param buffer Buffer handle, or 0 for current buffer
+/// @param ns_id Namespace id from |nvim_create_namespace()|
+/// @param start Start of range, given as (row, col) or valid extmark id
+/// (whose position defines the bound)
+/// @param end End of range, given as (row, col) or valid extmark id
+/// (whose position defines the bound)
+/// @param opts Optional parameters. Keys:
+/// - limit: Maximum number of marks to return
+/// @param[out] err Error details, if any
+/// @return List of [extmark_id, row, col] tuples in "traversal order".
+Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start,
+ Object end, Dictionary opts, Error *err)
+ FUNC_API_SINCE(7)
+{
+ Array rv = ARRAY_DICT_INIT;
+
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+ if (!buf) {
+ return rv;
+ }
+
+ if (!ns_initialized((uint64_t)ns_id)) {
+ api_set_error(err, kErrorTypeValidation, _("Invalid ns_id"));
+ return rv;
+ }
+ Integer limit = -1;
+
+ for (size_t i = 0; i < opts.size; i++) {
+ String k = opts.items[i].key;
+ Object *v = &opts.items[i].value;
+ if (strequal("limit", k.data)) {
+ if (v->type != kObjectTypeInteger) {
+ api_set_error(err, kErrorTypeValidation, "limit is not an integer");
+ return rv;
+ }
+ limit = v->data.integer;
+ v->data.integer = LUA_NOREF;
+ } else {
+ api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
+ return rv;
+ }
+ }
+
+ if (limit == 0) {
+ return rv;
+ }
+
+
+ bool reverse = false;
+
+ linenr_T l_lnum;
+ colnr_T l_col;
+ if (!extmark_get_index_from_obj(buf, ns_id, start, &l_lnum, &l_col, err)) {
+ return rv;
+ }
+
+ linenr_T u_lnum;
+ colnr_T u_col;
+ if (!extmark_get_index_from_obj(buf, ns_id, end, &u_lnum, &u_col, err)) {
+ return rv;
+ }
+
+ if (l_lnum > u_lnum || (l_lnum == u_lnum && l_col > u_col)) {
+ reverse = true;
+ linenr_T tmp_lnum = l_lnum;
+ l_lnum = u_lnum;
+ u_lnum = tmp_lnum;
+ colnr_T tmp_col = l_col;
+ l_col = u_col;
+ u_col = tmp_col;
+ }
+
+
+ ExtmarkArray marks = extmark_get(buf, (uint64_t)ns_id, l_lnum, l_col, u_lnum,
+ u_col, (int64_t)limit, reverse);
+
+ for (size_t i = 0; i < kv_size(marks); i++) {
+ Array mark = ARRAY_DICT_INIT;
+ Extmark *extmark = kv_A(marks, i);
+ ADD(mark, INTEGER_OBJ((Integer)extmark->mark_id));
+ ADD(mark, INTEGER_OBJ(extmark->line->lnum-1));
+ ADD(mark, INTEGER_OBJ(extmark->col-1));
+ ADD(rv, ARRAY_OBJ(mark));
+ }
+
+ kv_destroy(marks);
+ return rv;
+}
+
+/// Creates or updates an extmark.
+///
+/// To create a new extmark, pass id=0. The extmark id will be returned.
+// To move an existing mark, pass its id.
+///
+/// It is also allowed to create a new mark by passing in a previously unused
+/// id, but the caller must then keep track of existing and unused ids itself.
+/// (Useful over RPC, to avoid waiting for the return value.)
+///
+/// @param buffer Buffer handle, or 0 for current buffer
+/// @param ns_id Namespace id from |nvim_create_namespace()|
+/// @param id Extmark id, or 0 to create new
+/// @param line Line number where to place the mark
+/// @param col Column where to place the mark
+/// @param opts Optional parameters. Currently not used.
+/// @param[out] err Error details, if any
+/// @return Id of the created/updated extmark
+Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer id,
+ Integer line, Integer col,
+ Dictionary opts, Error *err)
+ FUNC_API_SINCE(7)
+{
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+ if (!buf) {
+ return 0;
+ }
+
+ if (!ns_initialized((uint64_t)ns_id)) {
+ api_set_error(err, kErrorTypeValidation, _("Invalid ns_id"));
+ return 0;
+ }
+
+ if (opts.size > 0) {
+ api_set_error(err, kErrorTypeValidation, "opts dict isn't empty");
+ return 0;
+ }
+
+ size_t len = 0;
+ if (line < 0 || line > buf->b_ml.ml_line_count) {
+ api_set_error(err, kErrorTypeValidation, "line value outside range");
+ return 0;
+ } else if (line < buf->b_ml.ml_line_count) {
+ len = STRLEN(ml_get_buf(buf, (linenr_T)line+1, false));
+ }
+
+ if (col == -1) {
+ col = (Integer)len;
+ } else if (col < -1 || col > (Integer)len) {
+ api_set_error(err, kErrorTypeValidation, "col value outside range");
+ return 0;
+ }
+
+ uint64_t id_num;
+ if (id == 0) {
+ id_num = extmark_free_id_get(buf, (uint64_t)ns_id);
+ } else if (id > 0) {
+ id_num = (uint64_t)id;
+ } else {
+ api_set_error(err, kErrorTypeValidation, _("Invalid mark id"));
+ return 0;
+ }
+
+ extmark_set(buf, (uint64_t)ns_id, id_num,
+ (linenr_T)line+1, (colnr_T)col+1, kExtmarkUndo);
+
+ return (Integer)id_num;
+}
+
+/// Removes an extmark.
+///
+/// @param buffer Buffer handle, or 0 for current buffer
+/// @param ns_id Namespace id from |nvim_create_namespace()|
+/// @param id Extmark id
+/// @param[out] err Error details, if any
+/// @return true if the extmark was found, else false
+Boolean nvim_buf_del_extmark(Buffer buffer,
+ Integer ns_id,
+ Integer id,
+ Error *err)
+ FUNC_API_SINCE(7)
+{
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+
+ if (!buf) {
+ return false;
+ }
+ if (!ns_initialized((uint64_t)ns_id)) {
+ api_set_error(err, kErrorTypeValidation, _("Invalid ns_id"));
+ return false;
+ }
+
+ return extmark_del(buf, (uint64_t)ns_id, (uint64_t)id, kExtmarkUndo);
+}
+
/// Adds a highlight to buffer.
///
/// Useful for plugins that dynamically generate highlights to a buffer
@@ -1050,7 +1325,8 @@ Integer nvim_buf_add_highlight(Buffer buffer,
return ns_id;
}
-/// Clears namespaced objects, highlights and virtual text, from a line range
+/// Clears namespaced objects (highlights, extmarks, virtual text) from
+/// a region.
///
/// Lines are 0-indexed. |api-indexing| To clear the namespace in the entire
/// buffer, specify line_start=0 and line_end=-1.
@@ -1082,6 +1358,10 @@ void nvim_buf_clear_namespace(Buffer buffer,
}
bufhl_clear_line_range(buf, (int)ns_id, (int)line_start+1, (int)line_end);
+ extmark_clear(buf, ns_id == -1 ? 0 : (uint64_t)ns_id,
+ (linenr_T)line_start+1,
+ (linenr_T)line_end,
+ kExtmarkUndo);
}
/// Clears highlights and virtual text from namespace and range of lines
@@ -1192,6 +1472,56 @@ free_exit:
return 0;
}
+/// Get the virtual text (annotation) for a buffer line.
+///
+/// The virtual text is returned as list of lists, whereas the inner lists have
+/// either one or two elements. The first element is the actual text, the
+/// optional second element is the highlight group.
+///
+/// The format is exactly the same as given to nvim_buf_set_virtual_text().
+///
+/// If there is no virtual text associated with the given line, an empty list
+/// is returned.
+///
+/// @param buffer Buffer handle, or 0 for current buffer
+/// @param line Line to get the virtual text from (zero-indexed)
+/// @param[out] err Error details, if any
+/// @return List of virtual text chunks
+Array nvim_buf_get_virtual_text(Buffer buffer, Integer lnum, Error *err)
+ FUNC_API_SINCE(7)
+{
+ Array chunks = ARRAY_DICT_INIT;
+
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+ if (!buf) {
+ return chunks;
+ }
+
+ if (lnum < 0 || lnum >= MAXLNUM) {
+ api_set_error(err, kErrorTypeValidation, "Line number outside range");
+ return chunks;
+ }
+
+ BufhlLine *lineinfo = bufhl_tree_ref(&buf->b_bufhl_info, (linenr_T)(lnum + 1),
+ false);
+ if (!lineinfo) {
+ return chunks;
+ }
+
+ for (size_t i = 0; i < lineinfo->virt_text.size; i++) {
+ Array chunk = ARRAY_DICT_INIT;
+ VirtTextChunk *vtc = &lineinfo->virt_text.items[i];
+ ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text)));
+ if (vtc->hl_id > 0) {
+ ADD(chunk, STRING_OBJ(cstr_to_string(
+ (const char *)syn_id2name(vtc->hl_id))));
+ }
+ ADD(chunks, ARRAY_OBJ(chunk));
+ }
+
+ return chunks;
+}
+
Dictionary nvim__buf_stats(Buffer buffer, Error *err)
{
Dictionary rv = ARRAY_DICT_INIT;
diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c
index 2056cb07e3..b8d62e42a1 100644
--- a/src/nvim/api/private/helpers.c
+++ b/src/nvim/api/private/helpers.c
@@ -10,6 +10,7 @@
#include "nvim/api/private/helpers.h"
#include "nvim/api/private/defs.h"
#include "nvim/api/private/handle.h"
+#include "nvim/api/vim.h"
#include "nvim/msgpack_rpc/helpers.h"
#include "nvim/lua/executor.h"
#include "nvim/ascii.h"
@@ -23,6 +24,7 @@
#include "nvim/eval/typval.h"
#include "nvim/map_defs.h"
#include "nvim/map.h"
+#include "nvim/mark_extended.h"
#include "nvim/option.h"
#include "nvim/option_defs.h"
#include "nvim/version.h"
@@ -1505,3 +1507,127 @@ ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf)
return mappings;
}
+
+// Returns an extmark given an id or a positional index
+// If throw == true then an error will be raised if nothing
+// was found
+// Returns NULL if something went wrong
+Extmark *extmark_from_id_or_pos(Buffer buffer, Integer ns, Object id,
+ Error *err, bool throw)
+{
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+
+ if (!buf) {
+ return NULL;
+ }
+
+ Extmark *extmark = NULL;
+ if (id.type == kObjectTypeArray) {
+ if (id.data.array.size != 2) {
+ api_set_error(err, kErrorTypeValidation,
+ _("Position must have 2 elements"));
+ return NULL;
+ }
+ linenr_T row = (linenr_T)id.data.array.items[0].data.integer;
+ colnr_T col = (colnr_T)id.data.array.items[1].data.integer;
+ if (row < 1 || col < 1) {
+ if (throw) {
+ api_set_error(err, kErrorTypeValidation, _("Row and column MUST be > 0"));
+ }
+ return NULL;
+ }
+ extmark = extmark_from_pos(buf, (uint64_t)ns, row, col);
+ } else if (id.type != kObjectTypeInteger) {
+ if (throw) {
+ api_set_error(err, kErrorTypeValidation,
+ _("Mark id must be an int or [row, col]"));
+ }
+ return NULL;
+ } else if (id.data.integer < 0) {
+ if (throw) {
+ api_set_error(err, kErrorTypeValidation, _("Mark id must be positive"));
+ }
+ return NULL;
+ } else {
+ extmark = extmark_from_id(buf,
+ (uint64_t)ns,
+ (uint64_t)id.data.integer);
+ }
+
+ if (!extmark) {
+ if (throw) {
+ api_set_error(err, kErrorTypeValidation, _("Mark doesn't exist"));
+ }
+ return NULL;
+ }
+ return extmark;
+}
+
+// Is the Namespace in use?
+bool ns_initialized(uint64_t ns)
+{
+ if (ns < 1) {
+ return false;
+ }
+ return ns < (uint64_t)next_namespace_id;
+}
+
+/// Gets the line and column of an extmark.
+///
+/// Extmarks may be queried by position, name or even special names
+/// in the future such as "cursor".
+///
+/// @param[out] lnum extmark line
+/// @param[out] colnr extmark column
+///
+/// @return true if the extmark was found, else false
+bool extmark_get_index_from_obj(buf_T *buf, Integer ns, Object obj, linenr_T
+ *lnum, colnr_T *colnr, Error *err)
+{
+ // Check if it is mark id
+ if (obj.type == kObjectTypeInteger) {
+ Integer id = obj.data.integer;
+ if (id == 0) {
+ *lnum = 1;
+ *colnr = 1;
+ return true;
+ } else if (id == -1) {
+ *lnum = MAXLNUM;
+ *colnr = MAXCOL;
+ return true;
+ } else if (id < 0) {
+ api_set_error(err, kErrorTypeValidation, _("Mark id must be positive"));
+ return false;
+ }
+
+ Extmark *extmark = extmark_from_id(buf, (uint64_t)ns, (uint64_t)id);
+ if (extmark) {
+ *lnum = extmark->line->lnum;
+ *colnr = extmark->col;
+ return true;
+ } else {
+ api_set_error(err, kErrorTypeValidation, _("No mark with requested id"));
+ return false;
+ }
+
+ // Check if it is a position
+ } else if (obj.type == kObjectTypeArray) {
+ Array pos = obj.data.array;
+ if (pos.size != 2
+ || pos.items[0].type != kObjectTypeInteger
+ || pos.items[1].type != kObjectTypeInteger) {
+ api_set_error(err, kErrorTypeValidation,
+ _("Position must have 2 integer elements"));
+ return false;
+ }
+ Integer line = pos.items[0].data.integer;
+ Integer col = pos.items[1].data.integer;
+ *lnum = (linenr_T)(line >= 0 ? line + 1 : MAXLNUM);
+ *colnr = (colnr_T)(col >= 0 ? col + 1 : MAXCOL);
+ return true;
+ } else {
+ api_set_error(err, kErrorTypeValidation,
+ _("Position must be a mark id Integer or position Array"));
+ return false;
+ }
+}
diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h
index 0ea7667428..8930f252f6 100644
--- a/src/nvim/api/private/helpers.h
+++ b/src/nvim/api/private/helpers.h
@@ -102,6 +102,20 @@ typedef struct {
int did_emsg;
} TryState;
+// `msg_list` controls the collection of abort-causing non-exception errors,
+// which would otherwise be ignored. This pattern is from do_cmdline().
+//
+// TODO(bfredl): prepare error-handling at "top level" (nv_event).
+#define TRY_WRAP(code) \
+ do { \
+ struct msglist **saved_msg_list = msg_list; \
+ struct msglist *private_msg_list; \
+ msg_list = &private_msg_list; \
+ private_msg_list = NULL; \
+ code \
+ msg_list = saved_msg_list; /* Restore the exception context. */ \
+ } while (0)
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "api/private/helpers.h.generated.h"
#endif
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c
index 602733fd31..19601b6539 100644
--- a/src/nvim/api/vim.c
+++ b/src/nvim/api/vim.c
@@ -25,6 +25,7 @@
#include "nvim/highlight.h"
#include "nvim/window.h"
#include "nvim/types.h"
+#include "nvim/ex_cmds2.h"
#include "nvim/ex_docmd.h"
#include "nvim/screen.h"
#include "nvim/memline.h"
@@ -39,6 +40,7 @@
#include "nvim/ops.h"
#include "nvim/option.h"
#include "nvim/state.h"
+#include "nvim/mark_extended.h"
#include "nvim/syntax.h"
#include "nvim/getchar.h"
#include "nvim/os/input.h"
@@ -53,20 +55,6 @@
# include "api/vim.c.generated.h"
#endif
-// `msg_list` controls the collection of abort-causing non-exception errors,
-// which would otherwise be ignored. This pattern is from do_cmdline().
-//
-// TODO(bfredl): prepare error-handling at "top level" (nv_event).
-#define TRY_WRAP(code) \
- do { \
- struct msglist **saved_msg_list = msg_list; \
- struct msglist *private_msg_list; \
- msg_list = &private_msg_list; \
- private_msg_list = NULL; \
- code \
- msg_list = saved_msg_list; /* Restore the exception context. */ \
- } while (0)
-
void api_vim_init(void)
FUNC_API_NOEXPORT
{
@@ -85,10 +73,70 @@ void api_vim_free_all_mem(void)
map_free(String, handle_T)(namespace_ids);
}
+/// Executes Vimscript (multiline block of Ex-commands), like anonymous
+/// |:source|.
+///
+/// Unlike |nvim_command()| this function supports heredocs, script-scope (s:),
+/// etc.
+///
+/// On execution error: fails with VimL error, does not update v:errmsg.
+///
+/// @see |execute()|
+/// @see |nvim_command()|
+///
+/// @param src Vimscript code
+/// @param output Capture and return all (non-error, non-shell |:!|) output
+/// @param[out] err Error details (Vim error), if any
+/// @return Output (non-error, non-shell |:!|) if `output` is true,
+/// else empty string.
+String nvim_exec(String src, Boolean output, Error *err)
+ FUNC_API_SINCE(7)
+{
+ const int save_msg_silent = msg_silent;
+ garray_T *const save_capture_ga = capture_ga;
+ garray_T capture_local;
+ if (output) {
+ ga_init(&capture_local, 1, 80);
+ capture_ga = &capture_local;
+ }
+
+ try_start();
+ msg_silent++;
+ do_source_str(src.data, "nvim_exec()");
+ capture_ga = save_capture_ga;
+ msg_silent = save_msg_silent;
+ try_end(err);
+
+ if (ERROR_SET(err)) {
+ goto theend;
+ }
+
+ if (output && capture_local.ga_len > 1) {
+ String s = (String){
+ .data = capture_local.ga_data,
+ .size = (size_t)capture_local.ga_len,
+ };
+ // redir usually (except :echon) prepends a newline.
+ if (s.data[0] == '\n') {
+ memmove(s.data, s.data + 1, s.size - 1);
+ s.data[s.size - 1] = '\0';
+ s.size = s.size - 1;
+ }
+ return s; // Caller will free the memory.
+ }
+theend:
+ if (output) {
+ ga_clear(&capture_local);
+ }
+ return (String)STRING_INIT;
+}
+
/// Executes an ex-command.
///
/// On execution error: fails with VimL error, does not update v:errmsg.
///
+/// @see |nvim_exec()|
+///
/// @param command Ex-command string
/// @param[out] err Error details (Vim error), if any
void nvim_command(String command, Error *err)
@@ -345,53 +393,16 @@ String nvim_replace_termcodes(String str, Boolean from_part, Boolean do_lt,
return cstr_as_string(ptr);
}
-/// Executes an ex-command and returns its (non-error) output.
-/// Shell |:!| output is not captured.
-///
-/// On execution error: fails with VimL error, does not update v:errmsg.
-///
-/// @param command Ex-command string
-/// @param[out] err Error details (Vim error), if any
+/// @deprecated
+/// @see nvim_exec
String nvim_command_output(String command, Error *err)
FUNC_API_SINCE(1)
+ FUNC_API_DEPRECATED_SINCE(7)
{
- const int save_msg_silent = msg_silent;
- garray_T *const save_capture_ga = capture_ga;
- garray_T capture_local;
- ga_init(&capture_local, 1, 80);
-
- try_start();
- msg_silent++;
- capture_ga = &capture_local;
- do_cmdline_cmd(command.data);
- capture_ga = save_capture_ga;
- msg_silent = save_msg_silent;
- try_end(err);
-
- if (ERROR_SET(err)) {
- goto theend;
- }
-
- if (capture_local.ga_len > 1) {
- String s = (String){
- .data = capture_local.ga_data,
- .size = (size_t)capture_local.ga_len,
- };
- // redir usually (except :echon) prepends a newline.
- if (s.data[0] == '\n') {
- memmove(s.data, s.data + 1, s.size - 1);
- s.data[s.size - 1] = '\0';
- s.size = s.size - 1;
- }
- return s; // Caller will free the memory.
- }
-
-theend:
- ga_clear(&capture_local);
- return (String)STRING_INIT;
+ return nvim_exec(command, true, err);
}
-/// Evaluates a VimL expression (:help expression).
+/// Evaluates a VimL |expression|.
/// Dictionaries and Lists are recursively expanded.
///
/// On execution error: fails with VimL error, does not update v:errmsg.
@@ -436,6 +447,15 @@ Object nvim_eval(String expr, Error *err)
return rv;
}
+/// @deprecated Use nvim_exec_lua() instead.
+Object nvim_execute_lua(String code, Array args, Error *err)
+ FUNC_API_SINCE(3)
+ FUNC_API_DEPRECATED_SINCE(7)
+ FUNC_API_REMOTE_ONLY
+{
+ return executor_exec_lua_api(code, args, err);
+}
+
/// Execute Lua code. Parameters (if any) are available as `...` inside the
/// chunk. The chunk can return a value.
///
@@ -448,8 +468,9 @@ Object nvim_eval(String expr, Error *err)
/// or executing the Lua code.
///
/// @return Return value of Lua code if present or NIL.
-Object nvim_execute_lua(String code, Array args, Error *err)
- FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY
+Object nvim_exec_lua(String code, Array args, Error *err)
+ FUNC_API_SINCE(7)
+ FUNC_API_REMOTE_ONLY
{
return executor_exec_lua_api(code, args, err);
}
@@ -1054,10 +1075,9 @@ fail:
/// @param enter Enter the window (make it the current window)
/// @param config Map defining the window configuration. Keys:
/// - `relative`: Sets the window layout to "floating", placed at (row,col)
-/// coordinates relative to one of:
+/// coordinates relative to:
/// - "editor" The global editor grid
-/// - "win" Window given by the `win` field, or current window by
-/// default.
+/// - "win" Window given by the `win` field, or current window.
/// - "cursor" Cursor position in current window.
/// - `win`: |window-ID| for relative="win".
/// - `anchor`: Decides which corner of the float to place at (row,col):
@@ -1088,9 +1108,10 @@ fail:
/// float where the text should not be edited. Disables
/// 'number', 'relativenumber', 'cursorline', 'cursorcolumn',
/// 'foldcolumn', 'spell' and 'list' options. 'signcolumn'
-/// is changed to `auto`. The end-of-buffer region is hidden
-/// by setting `eob` flag of 'fillchars' to a space char,
-/// and clearing the |EndOfBuffer| region in 'winhighlight'.
+/// is changed to `auto` and 'colorcolumn' is cleared. The
+/// end-of-buffer region is hidden by setting `eob` flag of
+/// 'fillchars' to a space char, and clearing the
+/// |EndOfBuffer| region in 'winhighlight'.
/// @param[out] err Error details, if any
///
/// @return Window handle, or 0 on error
@@ -1109,6 +1130,10 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dictionary config,
if (enter) {
win_enter(wp, false);
}
+ if (!win_valid(wp)) {
+ api_set_error(err, kErrorTypeException, "Window was closed immediately");
+ return 0;
+ }
if (buffer > 0) {
nvim_win_set_buf(wp->handle, buffer, err);
}
@@ -1260,13 +1285,13 @@ Boolean nvim_paste(String data, Boolean crlf, Integer phase, Error *err)
Array lines = string_to_array(data, crlf);
ADD(args, ARRAY_OBJ(lines));
ADD(args, INTEGER_OBJ(phase));
- rv = nvim_execute_lua(STATIC_CSTR_AS_STRING("return vim.paste(...)"), args,
- err);
+ rv = nvim_exec_lua(STATIC_CSTR_AS_STRING("return vim.paste(...)"), args,
+ err);
if (ERROR_SET(err)) {
draining = true;
goto theend;
}
- if (!(State & CMDLINE) && !(State & INSERT) && (phase == -1 || phase == 1)) {
+ if (!(State & (CMDLINE | INSERT)) && (phase == -1 || phase == 1)) {
ResetRedobuff();
AppendCharToRedobuff('a'); // Dot-repeat.
}
@@ -1284,7 +1309,7 @@ Boolean nvim_paste(String data, Boolean crlf, Integer phase, Error *err)
}
}
}
- if (!(State & CMDLINE) && !(State & INSERT) && (phase == -1 || phase == 3)) {
+ if (!(State & (CMDLINE | INSERT)) && (phase == -1 || phase == 3)) {
AppendCharToRedobuff(ESC); // Dot-repeat.
}
theend:
@@ -1304,7 +1329,7 @@ theend:
/// @param lines |readfile()|-style list of lines. |channel-lines|
/// @param type Edit behavior: any |getregtype()| result, or:
/// - "b" |blockwise-visual| mode (may include width, e.g. "b3")
-/// - "c" |characterwise| mode
+/// - "c" |charwise| mode
/// - "l" |linewise| mode
/// - "" guess by contents, see |setreg()|
/// @param after Insert after cursor (like |p|), or before (like |P|).
@@ -2395,7 +2420,7 @@ Array nvim_get_proc_children(Integer pid, Error *err)
Array a = ARRAY_DICT_INIT;
ADD(a, INTEGER_OBJ(pid));
String s = cstr_to_string("return vim._os_proc_children(select(1, ...))");
- Object o = nvim_execute_lua(s, a, err);
+ Object o = nvim_exec_lua(s, a, err);
api_free_string(s);
api_free_array(a);
if (o.type == kObjectTypeArray) {
@@ -2441,7 +2466,7 @@ Object nvim_get_proc(Integer pid, Error *err)
Array a = ARRAY_DICT_INIT;
ADD(a, INTEGER_OBJ(pid));
String s = cstr_to_string("return vim._os_proc_info(select(1, ...))");
- Object o = nvim_execute_lua(s, a, err);
+ Object o = nvim_exec_lua(s, a, err);
api_free_string(s);
api_free_array(a);
if (o.type == kObjectTypeArray && o.data.array.size == 0) {
diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua
index c223679596..a52789c795 100644
--- a/src/nvim/auevents.lua
+++ b/src/nvim/auevents.lua
@@ -21,9 +21,9 @@ return {
'BufWritePre', -- before writing a buffer
'ChanInfo', -- info was received about channel
'ChanOpen', -- channel was opened
- 'CmdLineChanged', -- command line was modified
- 'CmdLineEnter', -- after entering cmdline mode
- 'CmdLineLeave', -- before leaving cmdline mode
+ 'CmdlineChanged', -- command line was modified
+ 'CmdlineEnter', -- after entering cmdline mode
+ 'CmdlineLeave', -- before leaving cmdline mode
'CmdUndefined', -- command undefined
'CmdWinEnter', -- after entering the cmdline window
'CmdWinLeave', -- before leaving the cmdline window
diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c
index b50c764ac3..33ffff39f6 100644
--- a/src/nvim/buffer.c
+++ b/src/nvim/buffer.c
@@ -53,6 +53,7 @@
#include "nvim/indent_c.h"
#include "nvim/main.h"
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
@@ -410,11 +411,11 @@ bool buf_valid(buf_T *buf)
/// caller should get a new buffer very soon!
/// The 'bufhidden' option can force freeing and deleting.
/// @param abort_if_last
-/// If TRUE, do not close the buffer if autocommands cause
+/// If true, do not close the buffer if autocommands cause
/// there to be only one window with this buffer. e.g. when
/// ":quit" is supposed to close the window but autocommands
/// close all other windows.
-void close_buffer(win_T *win, buf_T *buf, int action, int abort_if_last)
+void close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last)
{
bool unload_buf = (action != 0);
bool del_buf = (action == DOBUF_DEL || action == DOBUF_WIPE);
@@ -816,6 +817,7 @@ static void free_buffer_stuff(buf_T *buf, int free_flags)
}
uc_clear(&buf->b_ucmds); // clear local user commands
buf_delete_signs(buf, (char_u *)"*"); // delete any signs
+ extmark_free_all(buf); // delete any extmarks
bufhl_clear_all(buf); // delete any highligts
map_clear_int(buf, MAP_ALL_MODES, true, false); // clear local mappings
map_clear_int(buf, MAP_ALL_MODES, true, true); // clear local abbrevs
@@ -929,7 +931,7 @@ void handle_swap_exists(bufref_T *old_curbuf)
// User selected Recover at ATTENTION prompt.
msg_scroll = true;
- ml_recover();
+ ml_recover(false);
MSG_PUTS("\n"); // don't overwrite the last message
cmdline_row = msg_row;
do_modelines(0);
@@ -1236,7 +1238,7 @@ do_buffer(
return FAIL;
}
} else {
- EMSG2(_("E89: %s will be killed(add ! to override)"),
+ EMSG2(_("E89: %s will be killed (add ! to override)"),
(char *)buf->b_fname);
return FAIL;
}
@@ -1583,10 +1585,12 @@ void enter_buffer(buf_T *buf)
open_buffer(false, NULL, 0);
} else {
- if (!msg_silent) {
+ if (!msg_silent && !shortmess(SHM_FILEINFO)) {
need_fileinfo = true; // display file info after redraw
}
- (void)buf_check_timestamp(curbuf, false); // check if file changed
+ // check if file changed
+ (void)buf_check_timestamp(curbuf, false);
+
curwin->w_topline = 1;
curwin->w_topfill = 0;
apply_autocmds(EVENT_BUFENTER, NULL, NULL, false, curbuf);
@@ -1947,6 +1951,7 @@ void free_buf_options(buf_T *buf, int free_p_ff)
clear_string_option(&buf->b_p_path);
clear_string_option(&buf->b_p_tags);
clear_string_option(&buf->b_p_tc);
+ clear_string_option(&buf->b_p_tfu);
clear_string_option(&buf->b_p_dict);
clear_string_option(&buf->b_p_tsr);
clear_string_option(&buf->b_p_qe);
@@ -2689,7 +2694,7 @@ setfname(
buf_T *buf,
char_u *ffname,
char_u *sfname,
- int message // give message when buffer already exists
+ bool message // give message when buffer already exists
)
{
buf_T *obuf = NULL;
@@ -3796,14 +3801,20 @@ int build_stl_str_hl(
buf_T *const save_curbuf = curbuf;
win_T *const save_curwin = curwin;
+ const int save_VIsual_active = VIsual_active;
curwin = wp;
curbuf = wp->w_buffer;
+ // Visual mode is only valid in the current window.
+ if (curwin != save_curwin) {
+ VIsual_active = false;
+ }
// Note: The result stored in `t` is unused.
str = eval_to_string_safe(out_p, &t, use_sandbox);
curwin = save_curwin;
curbuf = save_curbuf;
+ VIsual_active = save_VIsual_active;
// Remove the variable we just stored
do_unlet(S_LEN("g:actual_curbuf"), true);
@@ -4629,7 +4640,8 @@ do_arg_all(
if (i < alist->al_ga.ga_len
&& (AARGLIST(alist)[i].ae_fnum == buf->b_fnum
|| path_full_compare(alist_name(&AARGLIST(alist)[i]),
- buf->b_ffname, true) & kEqualFiles)) {
+ buf->b_ffname,
+ true, true) & kEqualFiles)) {
int weight = 1;
if (old_curtab == curtab) {
@@ -5179,6 +5191,13 @@ bool bt_help(const buf_T *const buf)
return buf != NULL && buf->b_help;
}
+// Return true if "buf" is a normal buffer, 'buftype' is empty.
+bool bt_normal(const buf_T *const buf)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ return buf != NULL && buf->b_p_bt[0] == NUL;
+}
+
// Return true if "buf" is the quickfix buffer.
bool bt_quickfix(const buf_T *const buf)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
@@ -5487,6 +5506,7 @@ void bufhl_clear_line_range(buf_T *buf,
linenr_T line_start,
linenr_T line_end)
{
+ // TODO(bfredl): implement kb_itr_interval to jump directly to the first line
kbitr_t(bufhl) itr;
BufhlLine *l, t = BUFHLLINE_INIT(line_start);
if (!kb_itr_get(bufhl, &buf->b_bufhl_info, &t, &itr)) {
@@ -5614,6 +5634,86 @@ void bufhl_mark_adjust(buf_T* buf,
}
}
+/// Adjust a placed highlight for column changes and joined/broken lines
+bool bufhl_mark_col_adjust(buf_T *buf,
+ linenr_T lnum,
+ colnr_T mincol,
+ long lnum_amount,
+ long col_amount)
+{
+ bool moved = false;
+ BufhlLine *lineinfo = bufhl_tree_ref(&buf->b_bufhl_info, lnum, false);
+ if (!lineinfo) {
+ // Old line empty, nothing to do
+ return false;
+ }
+ // Create the new line below only if needed
+ BufhlLine *lineinfo2 = NULL;
+
+ colnr_T delcol = MAXCOL;
+ if (lnum_amount == 0 && col_amount < 0) {
+ delcol = mincol+(int)col_amount;
+ }
+
+ size_t newidx = 0;
+ for (size_t i = 0; i < kv_size(lineinfo->items); i++) {
+ BufhlItem *item = &kv_A(lineinfo->items, i);
+ bool delete = false;
+ if (item->start >= mincol) {
+ moved = true;
+ item->start += (int)col_amount;
+ if (item->stop < MAXCOL) {
+ item->stop += (int)col_amount;
+ }
+ if (lnum_amount != 0) {
+ if (lineinfo2 == NULL) {
+ lineinfo2 = bufhl_tree_ref(&buf->b_bufhl_info,
+ lnum+lnum_amount, true);
+ }
+ kv_push(lineinfo2->items, *item);
+ delete = true;
+ }
+ } else {
+ if (item->start >= delcol) {
+ moved = true;
+ item->start = delcol;
+ }
+ if (item->stop == MAXCOL || item->stop+1 >= mincol) {
+ if (item->stop == MAXCOL) {
+ if (delcol < MAXCOL
+ && delcol > (colnr_T)STRLEN(ml_get_buf(buf, lnum, false))) {
+ delete = true;
+ }
+ } else {
+ moved = true;
+ item->stop += (int)col_amount;
+ }
+ assert(lnum_amount >= 0);
+ if (lnum_amount > 0) {
+ item->stop = MAXCOL;
+ }
+ } else if (item->stop+1 >= delcol) {
+ moved = true;
+ item->stop = delcol-1;
+ }
+ // we covered the entire range with a visual delete or something
+ if (item->stop < item->start) {
+ delete = true;
+ }
+ }
+
+ if (!delete) {
+ if (i != newidx) {
+ kv_A(lineinfo->items, newidx) = kv_A(lineinfo->items, i);
+ }
+ newidx++;
+ }
+ }
+ kv_size(lineinfo->items) = newidx;
+
+ return moved;
+}
+
/// Get highlights to display at a specific line
///
diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h
index 16c7804be0..700d8b82e6 100644
--- a/src/nvim/buffer_defs.h
+++ b/src/nvim/buffer_defs.h
@@ -115,14 +115,18 @@ typedef uint16_t disptick_T; // display tick type
#include "nvim/os/fs_defs.h" // for FileID
#include "nvim/terminal.h" // for Terminal
+#include "nvim/lib/kbtree.h"
+#include "nvim/mark_extended.h"
+
/*
* The taggy struct is used to store the information about a :tag command.
*/
typedef struct taggy {
- char_u *tagname; /* tag name */
- fmark_T fmark; /* cursor position BEFORE ":tag" */
- int cur_match; /* match number */
- int cur_fnum; /* buffer number used for cur_match */
+ char_u *tagname; // tag name
+ fmark_T fmark; // cursor position BEFORE ":tag"
+ int cur_match; // match number
+ int cur_fnum; // buffer number used for cur_match
+ char_u *user_data; // used with tagfunc
} taggy_T;
typedef struct buffblock buffblock_T;
@@ -647,6 +651,7 @@ struct file_buffer {
char_u *b_p_cpt; ///< 'complete'
char_u *b_p_cfu; ///< 'completefunc'
char_u *b_p_ofu; ///< 'omnifunc'
+ char_u *b_p_tfu; ///< 'tagfunc'
int b_p_eol; ///< 'endofline'
int b_p_fixeol; ///< 'fixendofline'
int b_p_et; ///< 'expandtab'
@@ -803,6 +808,10 @@ struct file_buffer {
kvec_t(BufhlLine *) b_bufhl_move_space; // temporary space for highlights
+ PMap(uint64_t) *b_extmark_ns; // extmark namespaces
+ kbtree_t(extmarklines) b_extlines; // extmarks
+ kvec_t(ExtmarkLine *) b_extmark_move_space; // temp space for extmarks
+
// array of channel_id:s which have asked to receive updates for this
// buffer.
kvec_t(uint64_t) update_channels;
@@ -909,19 +918,19 @@ typedef struct w_line {
* or row (FR_ROW) layout or is a leaf, which has a window.
*/
struct frame_S {
- char fr_layout; /* FR_LEAF, FR_COL or FR_ROW */
+ char fr_layout; // FR_LEAF, FR_COL or FR_ROW
int fr_width;
- int fr_newwidth; /* new width used in win_equal_rec() */
+ int fr_newwidth; // new width used in win_equal_rec()
int fr_height;
- int fr_newheight; /* new height used in win_equal_rec() */
- frame_T *fr_parent; /* containing frame or NULL */
- frame_T *fr_next; /* frame right or below in same parent, NULL
- for first */
- frame_T *fr_prev; /* frame left or above in same parent, NULL
- for last */
- /* fr_child and fr_win are mutually exclusive */
- frame_T *fr_child; /* first contained frame */
- win_T *fr_win; /* window that fills this frame */
+ int fr_newheight; // new height used in win_equal_rec()
+ frame_T *fr_parent; // containing frame or NULL
+ frame_T *fr_next; // frame right or below in same parent, NULL
+ // for last
+ frame_T *fr_prev; // frame left or above in same parent, NULL
+ // for first
+ // fr_child and fr_win are mutually exclusive
+ frame_T *fr_child; // first contained frame
+ win_T *fr_win; // window that fills this frame
};
#define FR_LEAF 0 /* frame is a leaf */
diff --git a/src/nvim/change.c b/src/nvim/change.c
index ba80e71ae6..8a782c2b20 100644
--- a/src/nvim/change.c
+++ b/src/nvim/change.c
@@ -17,6 +17,7 @@
#include "nvim/indent.h"
#include "nvim/indent_c.h"
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/memline.h"
#include "nvim/misc1.h"
#include "nvim/move.h"
@@ -358,6 +359,24 @@ void changed_bytes(linenr_T lnum, colnr_T col)
}
}
+/// insert/delete bytes at column
+///
+/// Like changed_bytes() but also adjust extmark for "added" bytes.
+/// When "added" is negative text was deleted.
+static void inserted_bytes(linenr_T lnum, colnr_T col, int added)
+{
+ if (added > 0) {
+ extmark_col_adjust(curbuf, lnum, col+1, 0, added, kExtmarkUndo);
+ } else if (added < 0) {
+ // TODO(bfredl): next revision of extmarks should handle both these
+ // with the same entry point. Also with more sane params..
+ extmark_col_adjust_delete(curbuf, lnum, col+2,
+ col+(-added)+1, kExtmarkUndo, 0);
+ }
+
+ changed_bytes(lnum, col);
+}
+
/// Appended "count" lines below line "lnum" in the current buffer.
/// Must be called AFTER the change and after mark_adjust().
/// Takes care of marking the buffer to be redrawn and sets the changed flag.
@@ -372,7 +391,7 @@ void appended_lines_mark(linenr_T lnum, long count)
// Skip mark_adjust when adding a line after the last one, there can't
// be marks there. But it's still needed in diff mode.
if (lnum + count < curbuf->b_ml.ml_line_count || curwin->w_p_diff) {
- mark_adjust(lnum + 1, (linenr_T)MAXLNUM, count, 0L, false);
+ mark_adjust(lnum + 1, (linenr_T)MAXLNUM, count, 0L, false, kExtmarkUndo);
}
changed_lines(lnum + 1, 0, lnum + 1, count, true);
}
@@ -390,7 +409,8 @@ void deleted_lines(linenr_T lnum, long count)
/// be triggered to display the cursor.
void deleted_lines_mark(linenr_T lnum, long count)
{
- mark_adjust(lnum, (linenr_T)(lnum + count - 1), (long)MAXLNUM, -count, false);
+ mark_adjust(lnum, (linenr_T)(lnum + count - 1), (long)MAXLNUM, -count, false,
+ kExtmarkUndo);
changed_lines(lnum, 0, lnum + count, -count, true);
}
@@ -628,7 +648,7 @@ void ins_char_bytes(char_u *buf, size_t charlen)
ml_replace(lnum, newp, false);
// mark the buffer as changed and prepare for displaying
- changed_bytes(lnum, (colnr_T)col);
+ inserted_bytes(lnum, (colnr_T)col, (int)(newlen - oldlen));
// If we're in Insert or Replace mode and 'showmatch' is set, then briefly
// show the match for right parens and braces.
@@ -674,7 +694,7 @@ void ins_str(char_u *s)
assert(bytes >= 0);
memmove(newp + col + newlen, oldp + col, (size_t)bytes);
ml_replace(lnum, newp, false);
- changed_bytes(lnum, col);
+ inserted_bytes(lnum, col, newlen);
curwin->w_cursor.col += newlen;
}
@@ -795,7 +815,7 @@ int del_bytes(colnr_T count, bool fixpos_arg, bool use_delcombine)
}
// mark the buffer as changed and prepare for displaying
- changed_bytes(lnum, curwin->w_cursor.col);
+ inserted_bytes(lnum, col, -count);
return OK;
}
@@ -951,6 +971,9 @@ int open_line(
bool did_append; // appended a new line
int saved_pi = curbuf->b_p_pi; // copy of preserveindent setting
+ linenr_T lnum = curwin->w_cursor.lnum;
+ colnr_T mincol = curwin->w_cursor.col + 1;
+
// make a copy of the current line so we can mess with it
char_u *saved_line = vim_strsave(get_cursor_line_ptr());
@@ -1574,7 +1597,8 @@ int open_line(
// be marks there. But still needed in diff mode.
if (curwin->w_cursor.lnum + 1 < curbuf->b_ml.ml_line_count
|| curwin->w_p_diff) {
- mark_adjust(curwin->w_cursor.lnum + 1, (linenr_T)MAXLNUM, 1L, 0L, false);
+ mark_adjust(curwin->w_cursor.lnum + 1, (linenr_T)MAXLNUM, 1L, 0L, false,
+ kExtmarkUndo);
}
did_append = true;
} else {
@@ -1663,8 +1687,12 @@ int open_line(
if (flags & OPENLINE_MARKFIX) {
mark_col_adjust(curwin->w_cursor.lnum,
curwin->w_cursor.col + less_cols_off,
- 1L, (long)-less_cols, 0);
+ 1L, (long)-less_cols, 0, kExtmarkNOOP);
}
+ // Always move extmarks - Here we move only the line where the
+ // cursor is, the previous mark_adjust takes care of the lines after
+ extmark_col_adjust(curbuf, lnum, mincol, 1L, (long)-less_cols,
+ kExtmarkUndo);
} else {
changed_bytes(curwin->w_cursor.lnum, curwin->w_cursor.col);
}
diff --git a/src/nvim/channel.c b/src/nvim/channel.c
index f9102fa0e2..2bb568f025 100644
--- a/src/nvim/channel.c
+++ b/src/nvim/channel.c
@@ -272,11 +272,35 @@ static void close_cb(Stream *stream, void *data)
channel_decref(data);
}
+
+/// Starts a job and returns the associated channel
+///
+/// @param[in] argv Arguments vector specifying the command to run,
+/// NULL-terminated
+/// @param[in] on_stdout Callback to read the job's stdout
+/// @param[in] on_stderr Callback to read the job's stderr
+/// @param[in] on_exit Callback to receive the job's exit status
+/// @param[in] pty True if the job should run attached to a pty
+/// @param[in] rpc True to communicate with the job using msgpack-rpc,
+/// `on_stdout` is ignored
+/// @param[in] detach True if the job should not be killed when nvim exits,
+/// ignored if `pty` is true
+/// @param[in] cwd Initial working directory for the job. Nvim's working
+/// directory if `cwd` is NULL
+/// @param[in] pty_width Width of the pty, ignored if `pty` is false
+/// @param[in] pty_height Height of the pty, ignored if `pty` is false
+/// @param[in] term_name `$TERM` for the pty
+/// @param[in] env Nvim's configured environment is used if this is NULL,
+/// otherwise defines all environment variables
+/// @param[out] status_out 0 for invalid arguments, > 0 for the channel id,
+/// < 0 if the job can't start
+///
+/// @returns [allocated] channel
Channel *channel_job_start(char **argv, CallbackReader on_stdout,
CallbackReader on_stderr, Callback on_exit,
bool pty, bool rpc, bool detach, const char *cwd,
uint16_t pty_width, uint16_t pty_height,
- char *term_name, varnumber_T *status_out)
+ char *term_name, char **env, varnumber_T *status_out)
{
assert(cwd == NULL || os_isdir_executable(cwd));
@@ -314,6 +338,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
proc->events = chan->events;
proc->detach = detach;
proc->cwd = cwd;
+ proc->env = env;
char *cmd = xstrdup(proc->argv[0]);
bool has_out, has_err;
@@ -328,6 +353,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
if (status) {
EMSG3(_(e_jobspawn), os_strerror(status), cmd);
xfree(cmd);
+ os_free_fullenv(proc->env);
if (proc->type == kProcessTypePty) {
xfree(chan->stream.pty.term_name);
}
@@ -336,6 +362,8 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
return NULL;
}
xfree(cmd);
+ os_free_fullenv(proc->env);
+
wstream_init(&proc->in, 0);
if (has_out) {
diff --git a/src/nvim/context.c b/src/nvim/context.c
index 2f872ff2bf..1ae0510762 100644
--- a/src/nvim/context.c
+++ b/src/nvim/context.c
@@ -254,7 +254,7 @@ static inline void ctx_save_funcs(Context *ctx, bool scriptonly)
size_t cmd_len = sizeof("func! ") + STRLEN(name);
char *cmd = xmalloc(cmd_len);
snprintf(cmd, cmd_len, "func! %s", name);
- String func_body = nvim_command_output(cstr_as_string(cmd), &err);
+ String func_body = nvim_exec(cstr_as_string(cmd), true, &err);
xfree(cmd);
if (!ERROR_SET(&err)) {
ADD(ctx->funcs, STRING_OBJ(func_body));
diff --git a/src/nvim/cursor.c b/src/nvim/cursor.c
index f2b3cfe690..036ae32589 100644
--- a/src/nvim/cursor.c
+++ b/src/nvim/cursor.c
@@ -93,11 +93,12 @@ int coladvance(colnr_T wcol)
static int coladvance2(
pos_T *pos,
- bool addspaces, /* change the text to achieve our goal? */
- bool finetune, /* change char offset for the exact column */
- colnr_T wcol /* column to move to */
+ bool addspaces, // change the text to achieve our goal?
+ bool finetune, // change char offset for the exact column
+ colnr_T wcol_arg // column to move to (can be negative)
)
{
+ colnr_T wcol = wcol_arg;
int idx;
char_u *ptr;
char_u *line;
@@ -165,6 +166,7 @@ static int coladvance2(
if (virtual_active()
&& addspaces
+ && wcol >= 0
&& ((col != wcol && col != wcol + 1) || csize > 1)) {
/* 'virtualedit' is set: The difference between wcol and col is
* filled with spaces. */
@@ -244,8 +246,9 @@ static int coladvance2(
mark_mb_adjustpos(curbuf, pos);
}
- if (col < wcol)
+ if (wcol < 0 || col < wcol) {
return FAIL;
+ }
return OK;
}
diff --git a/src/nvim/diff.c b/src/nvim/diff.c
index db3ef7ac47..dccde01d29 100644
--- a/src/nvim/diff.c
+++ b/src/nvim/diff.c
@@ -44,7 +44,7 @@
#include "nvim/os/shell.h"
static int diff_busy = false; // using diff structs, don't change them
-static int diff_need_update = false; // ex_diffupdate needs to be called
+static bool diff_need_update = false; // ex_diffupdate needs to be called
// Flags obtained from the 'diffopt' option
#define DIFF_FILLER 0x001 // display filler lines
@@ -57,8 +57,9 @@ static int diff_need_update = false; // ex_diffupdate needs to be called
#define DIFF_VERTICAL 0x080 // vertical splits
#define DIFF_HIDDEN_OFF 0x100 // diffoff when hidden
#define DIFF_INTERNAL 0x200 // use internal xdiff algorithm
+#define DIFF_CLOSE_OFF 0x400 // diffoff when closing window
#define ALL_WHITE_DIFF (DIFF_IWHITE | DIFF_IWHITEALL | DIFF_IWHITEEOL)
-static int diff_flags = DIFF_INTERNAL | DIFF_FILLER;
+static int diff_flags = DIFF_INTERNAL | DIFF_FILLER | DIFF_CLOSE_OFF;
static long diff_algorithm = 0;
@@ -490,7 +491,8 @@ static void diff_mark_adjust_tp(tabpage_T *tp, int idx, linenr_T line1,
}
if (tp == curtab) {
- diff_redraw(true);
+ // Don't redraw right away, this updates the diffs, which can be slow.
+ need_diff_redraw = true;
// Need to recompute the scroll binding, may remove or add filler
// lines (e.g., when adding lines above w_topline). But it's slow when
@@ -634,8 +636,9 @@ static int diff_check_sanity(tabpage_T *tp, diff_T *dp)
/// Mark all diff buffers in the current tab page for redraw.
///
/// @param dofold Also recompute the folds
-static void diff_redraw(int dofold)
+void diff_redraw(bool dofold)
{
+ need_diff_redraw = false;
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
if (!wp->w_p_diff) {
continue;
@@ -723,8 +726,8 @@ static int diff_write_buffer(buf_T *buf, diffin_T *din)
char_u cbuf[MB_MAXBYTES + 1];
c = PTR2CHAR(s);
- c = enc_utf8 ? utf_fold(c) : TOLOWER_LOC(c);
- orig_len = MB_PTR2LEN(s);
+ c = utf_fold(c);
+ orig_len = utfc_ptr2len(s);
if (utf_char2bytes(c, cbuf) != orig_len) {
// TODO(Bram): handle byte length difference
memmove(ptr + len, s, orig_len);
@@ -1472,6 +1475,13 @@ void ex_diffoff(exarg_T *eap)
diff_buf_clear();
}
+ if (!diffwin) {
+ diff_need_update = false;
+ curtab->tp_diff_invalid = false;
+ curtab->tp_diff_update = false;
+ diff_clear(curtab);
+ }
+
// Remove "hor" from from 'scrollopt' if there are no diff windows left.
if (!diffwin && (vim_strchr(p_sbo, 'h') != NULL)) {
do_cmdline_cmd("set sbo-=hor");
@@ -1712,6 +1722,7 @@ static void diff_copy_entry(diff_T *dprev, diff_T *dp, int idx_orig,
///
/// @param tp
void diff_clear(tabpage_T *tp)
+ FUNC_ATTR_NONNULL_ALL
{
diff_T *p;
diff_T *next_p;
@@ -2141,6 +2152,9 @@ int diffopt_changed(void)
} else if (STRNCMP(p, "hiddenoff", 9) == 0) {
p += 9;
diff_flags_new |= DIFF_HIDDEN_OFF;
+ } else if (STRNCMP(p, "closeoff", 8) == 0) {
+ p += 8;
+ diff_flags_new |= DIFF_CLOSE_OFF;
} else if (STRNCMP(p, "indent-heuristic", 16) == 0) {
p += 16;
diff_indent_heuristic = XDF_INDENT_HEURISTIC;
@@ -2216,6 +2230,13 @@ bool diffopt_hiddenoff(void)
return (diff_flags & DIFF_HIDDEN_OFF) != 0;
}
+// Return true if 'diffopt' contains "closeoff".
+bool diffopt_closeoff(void)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ return (diff_flags & DIFF_CLOSE_OFF) != 0;
+}
+
/// Find the difference within a changed line.
///
/// @param wp window whose current buffer to check
@@ -2690,7 +2711,8 @@ void ex_diffgetput(exarg_T *eap)
// Adjust marks. This will change the following entries!
if (added != 0) {
- mark_adjust(lnum, lnum + count - 1, (long)MAXLNUM, (long)added, false);
+ mark_adjust(lnum, lnum + count - 1, (long)MAXLNUM, (long)added, false,
+ kExtmarkUndo);
if (curwin->w_cursor.lnum >= lnum) {
// Adjust the cursor position if it's in/after the changed
// lines.
diff --git a/src/nvim/diff.h b/src/nvim/diff.h
index 3624ce29bb..99a60381bd 100644
--- a/src/nvim/diff.h
+++ b/src/nvim/diff.h
@@ -4,6 +4,13 @@
#include "nvim/pos.h"
#include "nvim/ex_cmds_defs.h"
+// Value set from 'diffopt'.
+EXTERN int diff_context INIT(= 6); // context for folds
+EXTERN int diff_foldcolumn INIT(= 2); // 'foldcolumn' for diff mode
+EXTERN bool diff_need_scrollbind INIT(= false);
+
+EXTERN bool need_diff_redraw INIT(= false); // need to call diff_redraw()
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "diff.h.generated.h"
#endif
diff --git a/src/nvim/digraph.c b/src/nvim/digraph.c
index f6ec350488..65d95ff158 100644
--- a/src/nvim/digraph.c
+++ b/src/nvim/digraph.c
@@ -128,101 +128,154 @@ static digr_T digraphdefault[] =
{ 'P', 'M', 0x9e },
{ 'A', 'C', 0x9f },
{ 'N', 'S', 0xa0 },
+#define DG_START_LATIN 0xa1
{ '!', 'I', 0xa1 },
+ { '~', '!', 0xa1 }, // ¡ Vim 5.x compatible
{ 'C', 't', 0xa2 },
+ { 'c', '|', 0xa2 }, // ¢ Vim 5.x compatible
{ 'P', 'd', 0xa3 },
+ { '$', '$', 0xa3 }, // £ Vim 5.x compatible
{ 'C', 'u', 0xa4 },
+ { 'o', 'x', 0xa4 }, // ¤ Vim 5.x compatible
{ 'Y', 'e', 0xa5 },
+ { 'Y', '-', 0xa5 }, // ¥ Vim 5.x compatible
{ 'B', 'B', 0xa6 },
+ { '|', '|', 0xa6 }, // ¦ Vim 5.x compatible
{ 'S', 'E', 0xa7 },
{ '\'', ':', 0xa8 },
{ 'C', 'o', 0xa9 },
+ { 'c', 'O', 0xa9 }, // © Vim 5.x compatible
{ '-', 'a', 0xaa },
{ '<', '<', 0xab },
{ 'N', 'O', 0xac },
+ { '-', ',', 0xac }, // ¬ Vim 5.x compatible
{ '-', '-', 0xad },
{ 'R', 'g', 0xae },
{ '\'', 'm', 0xaf },
+ { '-', '=', 0xaf }, // ¯ Vim 5.x compatible
{ 'D', 'G', 0xb0 },
+ { '~', 'o', 0xb0 }, // ° Vim 5.x compatible
{ '+', '-', 0xb1 },
{ '2', 'S', 0xb2 },
+ { '2', '2', 0xb2 }, // ² Vim 5.x compatible
{ '3', 'S', 0xb3 },
+ { '3', '3', 0xb3 }, // ³ Vim 5.x compatible
{ '\'', '\'', 0xb4 },
{ 'M', 'y', 0xb5 },
{ 'P', 'I', 0xb6 },
+ { 'p', 'p', 0xb6 }, // ¶ Vim 5.x compatible
{ '.', 'M', 0xb7 },
+ { '~', '.', 0xb7 }, // · Vim 5.x compatible
{ '\'', ',', 0xb8 },
{ '1', 'S', 0xb9 },
+ { '1', '1', 0xb9 }, // ¹ Vim 5.x compatible
{ '-', 'o', 0xba },
{ '>', '>', 0xbb },
{ '1', '4', 0xbc },
{ '1', '2', 0xbd },
{ '3', '4', 0xbe },
{ '?', 'I', 0xbf },
+ { '~', '?', 0xbf }, // ¿ Vim 5.x compatible
{ 'A', '!', 0xc0 },
+ { 'A', '`', 0xc0 }, // À Vim 5.x compatible
{ 'A', '\'', 0xc1 },
{ 'A', '>', 0xc2 },
+ { 'A', '^', 0xc2 }, // Â Vim 5.x compatible
{ 'A', '?', 0xc3 },
+ { 'A', '~', 0xc3 }, // Ã Vim 5.x compatible
{ 'A', ':', 0xc4 },
+ { 'A', '"', 0xc4 }, // Ä Vim 5.x compatible
{ 'A', 'A', 0xc5 },
+ { 'A', '@', 0xc5 }, // Ã… Vim 5.x compatible
{ 'A', 'E', 0xc6 },
{ 'C', ',', 0xc7 },
{ 'E', '!', 0xc8 },
+ { 'E', '`', 0xc8 }, // È Vim 5.x compatible
{ 'E', '\'', 0xc9 },
{ 'E', '>', 0xca },
+ { 'E', '^', 0xca }, // Ê Vim 5.x compatible
{ 'E', ':', 0xcb },
+ { 'E', '"', 0xcb }, // Ë Vim 5.x compatible
{ 'I', '!', 0xcc },
+ { 'I', '`', 0xcc }, // Ì Vim 5.x compatible
{ 'I', '\'', 0xcd },
{ 'I', '>', 0xce },
+ { 'I', '^', 0xce }, // ÃŽ Vim 5.x compatible
{ 'I', ':', 0xcf },
+ { 'I', '"', 0xcf }, // Ã Vim 5.x compatible
{ 'D', '-', 0xd0 },
{ 'N', '?', 0xd1 },
+ { 'N', '~', 0xd1 }, // Ñ Vim 5.x compatible
{ 'O', '!', 0xd2 },
+ { 'O', '`', 0xd2 }, // Ã’ Vim 5.x compatible
{ 'O', '\'', 0xd3 },
{ 'O', '>', 0xd4 },
+ { 'O', '^', 0xd4 }, // Ô Vim 5.x compatible
{ 'O', '?', 0xd5 },
+ { 'O', '~', 0xd5 }, // Õ Vim 5.x compatible
{ 'O', ':', 0xd6 },
{ '*', 'X', 0xd7 },
+ { '/', '\\', 0xd7 }, // × Vim 5.x compatible
{ 'O', '/', 0xd8 },
{ 'U', '!', 0xd9 },
+ { 'U', '`', 0xd9 }, // Ù Vim 5.x compatible
{ 'U', '\'', 0xda },
{ 'U', '>', 0xdb },
+ { 'U', '^', 0xdb }, // Û Vim 5.x compatible
{ 'U', ':', 0xdc },
{ 'Y', '\'', 0xdd },
{ 'T', 'H', 0xde },
+ { 'I', 'p', 0xde }, // Þ Vim 5.x compatible
{ 's', 's', 0xdf },
{ 'a', '!', 0xe0 },
+ { 'a', '`', 0xe0 }, // à Vim 5.x compatible
{ 'a', '\'', 0xe1 },
{ 'a', '>', 0xe2 },
+ { 'a', '^', 0xe2 }, // â Vim 5.x compatible
{ 'a', '?', 0xe3 },
+ { 'a', '~', 0xe3 }, // ã Vim 5.x compatible
{ 'a', ':', 0xe4 },
+ { 'a', '"', 0xe4 }, // ä Vim 5.x compatible
{ 'a', 'a', 0xe5 },
+ { 'a', '@', 0xe5 }, // å Vim 5.x compatible
{ 'a', 'e', 0xe6 },
{ 'c', ',', 0xe7 },
{ 'e', '!', 0xe8 },
+ { 'e', '`', 0xe8 }, // è Vim 5.x compatible
{ 'e', '\'', 0xe9 },
{ 'e', '>', 0xea },
+ { 'e', '^', 0xea }, // ê Vim 5.x compatible
{ 'e', ':', 0xeb },
+ { 'e', '"', 0xeb }, // ë Vim 5.x compatible
{ 'i', '!', 0xec },
+ { 'i', '`', 0xec }, // ì Vim 5.x compatible
{ 'i', '\'', 0xed },
{ 'i', '>', 0xee },
+ { 'i', '^', 0xee }, // î Vim 5.x compatible
{ 'i', ':', 0xef },
{ 'd', '-', 0xf0 },
{ 'n', '?', 0xf1 },
+ { 'n', '~', 0xf1 }, // ñ Vim 5.x compatible
{ 'o', '!', 0xf2 },
+ { 'o', '`', 0xf2 }, // ò Vim 5.x compatible
{ 'o', '\'', 0xf3 },
{ 'o', '>', 0xf4 },
+ { 'o', '^', 0xf4 }, // ô Vim 5.x compatible
{ 'o', '?', 0xf5 },
+ { 'o', '~', 0xf5 }, // õ Vim 5.x compatible
{ 'o', ':', 0xf6 },
{ '-', ':', 0xf7 },
{ 'o', '/', 0xf8 },
{ 'u', '!', 0xf9 },
+ { 'u', '`', 0xf9 }, // ù Vim 5.x compatible
{ 'u', '\'', 0xfa },
{ 'u', '>', 0xfb },
+ { 'u', '^', 0xfb }, // û Vim 5.x compatible
{ 'u', ':', 0xfc },
{ 'y', '\'', 0xfd },
{ 't', 'h', 0xfe },
{ 'y', ':', 0xff },
+ { 'y', '"', 0xff }, // x XX Vim 5.x compatible
{ 'A', '-', 0x0100 },
{ 'a', '-', 0x0101 },
@@ -397,6 +450,7 @@ static digr_T digraphdefault[] =
{ '\'', '0', 0x02da },
{ '\'', ';', 0x02db },
{ '\'', '"', 0x02dd },
+#define DG_START_GREEK 0x0386
{ 'A', '%', 0x0386 },
{ 'E', '%', 0x0388 },
{ 'Y', '%', 0x0389 },
@@ -478,6 +532,7 @@ static digr_T digraphdefault[] =
{ 'p', '3', 0x03e1 },
{ '\'', '%', 0x03f4 },
{ 'j', '3', 0x03f5 },
+#define DG_START_CYRILLIC 0x0401
{ 'I', 'O', 0x0401 },
{ 'D', '%', 0x0402 },
{ 'G', '%', 0x0403 },
@@ -582,6 +637,7 @@ static digr_T digraphdefault[] =
{ 'c', '3', 0x0481 },
{ 'G', '3', 0x0490 },
{ 'g', '3', 0x0491 },
+#define DG_START_HEBREW 0x05d0
{ 'A', '+', 0x05d0 },
{ 'B', '+', 0x05d1 },
{ 'G', '+', 0x05d2 },
@@ -609,6 +665,7 @@ static digr_T digraphdefault[] =
{ 'R', '+', 0x05e8 },
{ 'S', 'h', 0x05e9 },
{ 'T', '+', 0x05ea },
+#define DG_START_ARABIC 0x060c
{ ',', '+', 0x060c },
{ ';', '+', 0x061b },
{ '?', '+', 0x061f },
@@ -671,6 +728,7 @@ static digr_T digraphdefault[] =
{ '7', 'a', 0x06f7 },
{ '8', 'a', 0x06f8 },
{ '9', 'a', 0x06f9 },
+#define DG_START_LATIN_EXTENDED 0x1e02
{ 'B', '.', 0x1e02 },
{ 'b', '.', 0x1e03 },
{ 'B', '_', 0x1e06 },
@@ -722,7 +780,9 @@ static digr_T digraphdefault[] =
{ 'V', '?', 0x1e7c },
{ 'v', '?', 0x1e7d },
{ 'W', '!', 0x1e80 },
+ { 'W', '`', 0x1e80 }, // extra alternative, easier to remember
{ 'w', '!', 0x1e81 },
+ { 'w', '`', 0x1e81 }, // extra alternative, easier to remember
{ 'W', '\'', 0x1e82 },
{ 'w', '\'', 0x1e83 },
{ 'W', ':', 0x1e84 },
@@ -756,11 +816,14 @@ static digr_T digraphdefault[] =
{ 'U', '2', 0x1ee6 },
{ 'u', '2', 0x1ee7 },
{ 'Y', '!', 0x1ef2 },
+ { 'Y', '`', 0x1ef2 }, // extra alternative, easier to remember
{ 'y', '!', 0x1ef3 },
+ { 'y', '`', 0x1ef3 }, // extra alternative, easier to remember
{ 'Y', '2', 0x1ef6 },
{ 'y', '2', 0x1ef7 },
{ 'Y', '?', 0x1ef8 },
{ 'y', '?', 0x1ef9 },
+#define DG_START_GREEK_EXTENDED 0x1f00
{ ';', '\'', 0x1f00 },
{ ',', '\'', 0x1f01 },
{ ';', '!', 0x1f02 },
@@ -769,6 +832,7 @@ static digr_T digraphdefault[] =
{ '?', ',', 0x1f05 },
{ '!', ':', 0x1f06 },
{ '?', ':', 0x1f07 },
+#define DG_START_PUNCTUATION 0x2002
{ '1', 'N', 0x2002 },
{ '1', 'M', 0x2003 },
{ '3', 'M', 0x2004 },
@@ -807,6 +871,7 @@ static digr_T digraphdefault[] =
{ ':', 'X', 0x203b },
{ '\'', '-', 0x203e },
{ '/', 'f', 0x2044 },
+#define DG_START_SUB_SUPER 0x2070
{ '0', 'S', 0x2070 },
{ '4', 'S', 0x2074 },
{ '5', 'S', 0x2075 },
@@ -835,13 +900,15 @@ static digr_T digraphdefault[] =
{ '=', 's', 0x208c },
{ '(', 's', 0x208d },
{ ')', 's', 0x208e },
+#define DG_START_CURRENCY 0x20a4
{ 'L', 'i', 0x20a4 },
{ 'P', 't', 0x20a7 },
{ 'W', '=', 0x20a9 },
- { '=', 'e', 0x20ac }, // euro
- { 'E', 'u', 0x20ac }, // euro
- { '=', 'R', 0x20bd }, // rouble
- { '=', 'P', 0x20bd }, // rouble
+ { '=', 'e', 0x20ac }, // euro
+ { 'E', 'u', 0x20ac }, // euro
+ { '=', 'R', 0x20bd }, // rouble
+ { '=', 'P', 0x20bd }, // rouble
+#define DG_START_OTHER1 0x2103
{ 'o', 'C', 0x2103 },
{ 'c', 'o', 0x2105 },
{ 'o', 'F', 0x2109 },
@@ -864,6 +931,7 @@ static digr_T digraphdefault[] =
{ '3', '8', 0x215c },
{ '5', '8', 0x215d },
{ '7', '8', 0x215e },
+#define DG_START_ROMAN 0x2160
{ '1', 'R', 0x2160 },
{ '2', 'R', 0x2161 },
{ '3', 'R', 0x2162 },
@@ -888,6 +956,7 @@ static digr_T digraphdefault[] =
{ 'a', 'r', 0x2179 },
{ 'b', 'r', 0x217a },
{ 'c', 'r', 0x217b },
+#define DG_START_ARROWS 0x2190
{ '<', '-', 0x2190 },
{ '-', '!', 0x2191 },
{ '-', '>', 0x2192 },
@@ -897,6 +966,7 @@ static digr_T digraphdefault[] =
{ '<', '=', 0x21d0 },
{ '=', '>', 0x21d2 },
{ '=', '=', 0x21d4 },
+#define DG_START_MATH 0x2200
{ 'F', 'A', 0x2200 },
{ 'd', 'P', 0x2202 },
{ 'T', 'E', 0x2203 },
@@ -954,6 +1024,7 @@ static digr_T digraphdefault[] =
{ '.', 'P', 0x22c5 },
{ ':', '3', 0x22ee },
{ '.', '3', 0x22ef },
+#define DG_START_TECHNICAL 0x2302
{ 'E', 'h', 0x2302 },
{ '<', '7', 0x2308 },
{ '>', '7', 0x2309 },
@@ -966,6 +1037,7 @@ static digr_T digraphdefault[] =
{ 'I', 'l', 0x2321 },
{ '<', '/', 0x2329 },
{ '/', '>', 0x232a },
+#define DG_START_OTHER2 0x2423
{ 'V', 's', 0x2423 },
{ '1', 'h', 0x2440 },
{ '3', 'h', 0x2441 },
@@ -984,6 +1056,7 @@ static digr_T digraphdefault[] =
{ '7', '.', 0x248e },
{ '8', '.', 0x248f },
{ '9', '.', 0x2490 },
+#define DG_START_DRAWING 0x2500
{ 'h', 'h', 0x2500 },
{ 'H', 'H', 0x2501 },
{ 'v', 'v', 0x2502 },
@@ -1034,6 +1107,7 @@ static digr_T digraphdefault[] =
{ 'V', 'H', 0x254b },
{ 'F', 'D', 0x2571 },
{ 'B', 'D', 0x2572 },
+#define DG_START_BLOCK 0x2580
{ 'T', 'B', 0x2580 },
{ 'L', 'B', 0x2584 },
{ 'F', 'B', 0x2588 },
@@ -1042,6 +1116,7 @@ static digr_T digraphdefault[] =
{ '.', 'S', 0x2591 },
{ ':', 'S', 0x2592 },
{ '?', 'S', 0x2593 },
+#define DG_START_SHAPES 0x25a0
{ 'f', 'S', 0x25a0 },
{ 'O', 'S', 0x25a1 },
{ 'R', 'O', 0x25a2 },
@@ -1075,6 +1150,7 @@ static digr_T digraphdefault[] =
{ 'I', 'c', 0x25d9 },
{ 'F', 'd', 0x25e2 },
{ 'B', 'd', 0x25e3 },
+#define DG_START_SYMBOLS 0x2605
{ '*', '2', 0x2605 },
{ '*', '1', 0x2606 },
{ '<', 'H', 0x261c },
@@ -1094,9 +1170,11 @@ static digr_T digraphdefault[] =
{ 'M', 'b', 0x266d },
{ 'M', 'x', 0x266e },
{ 'M', 'X', 0x266f },
+#define DG_START_DINGBATS 0x2713
{ 'O', 'K', 0x2713 },
{ 'X', 'X', 0x2717 },
{ '-', 'X', 0x2720 },
+#define DG_START_CJK_SYMBOLS 0x3000
{ 'I', 'S', 0x3000 },
{ ',', '_', 0x3001 },
{ '.', '_', 0x3002 },
@@ -1120,6 +1198,7 @@ static digr_T digraphdefault[] =
{ '(', 'I', 0x3016 },
{ ')', 'I', 0x3017 },
{ '-', '?', 0x301c },
+#define DG_START_HIRAGANA 0x3041
{ 'A', '5', 0x3041 },
{ 'a', '5', 0x3042 },
{ 'I', '5', 0x3043 },
@@ -1208,6 +1287,7 @@ static digr_T digraphdefault[] =
{ '0', '5', 0x309c },
{ '*', '5', 0x309d },
{ '+', '5', 0x309e },
+#define DG_START_KATAKANA 0x30a1
{ 'a', '6', 0x30a1 },
{ 'A', '6', 0x30a2 },
{ 'i', '6', 0x30a3 },
@@ -1302,6 +1382,7 @@ static digr_T digraphdefault[] =
{ '-', '6', 0x30fc },
{ '*', '6', 0x30fd },
{ '+', '6', 0x30fe },
+#define DG_START_BOPOMOFO 0x3105
{ 'b', '4', 0x3105 },
{ 'p', '4', 0x3106 },
{ 'm', '4', 0x3107 },
@@ -1341,6 +1422,7 @@ static digr_T digraphdefault[] =
{ 'v', '4', 0x312a },
{ 'n', 'G', 0x312b },
{ 'g', 'n', 0x312c },
+#define DG_START_OTHER3 0x3220
{ '1', 'c', 0x3220 },
{ '2', 'c', 0x3221 },
{ '3', 'c', 0x3222 },
@@ -1359,66 +1441,6 @@ static digr_T digraphdefault[] =
{ 'f', 't', 0xfb05 },
{ 's', 't', 0xfb06 },
- // extra alternatives, easier to remember
- { 'W', '`', 0x1e80 },
- { 'w', '`', 0x1e81 },
- { 'Y', '`', 0x1ef2 },
- { 'y', '`', 0x1ef3 },
-
- // Vim 5.x compatible digraphs that don't conflict with the above
- { '~', '!', 161 }, // ¡
- { 'c', '|', 162 }, // ¢
- { '$', '$', 163 }, // £
- { 'o', 'x', 164 }, // ¤ - currency symbol in ISO 8859-1
- { 'Y', '-', 165 }, // ¥
- { '|', '|', 166 }, // ¦
- { 'c', 'O', 169 }, // ©
- { '-', ',', 172 }, // ¬
- { '-', '=', 175 }, // ¯
- { '~', 'o', 176 }, // °
- { '2', '2', 178 }, // ²
- { '3', '3', 179 }, // ³
- { 'p', 'p', 182 }, // ¶
- { '~', '.', 183 }, // ·
- { '1', '1', 185 }, // ¹
- { '~', '?', 191 }, // ¿
- { 'A', '`', 192 }, // À
- { 'A', '^', 194 }, // Â
- { 'A', '~', 195 }, // Ã
- { 'A', '"', 196 }, // Ä
- { 'A', '@', 197 }, // Ã…
- { 'E', '`', 200 }, // È
- { 'E', '^', 202 }, // Ê
- { 'E', '"', 203 }, // Ë
- { 'I', '`', 204 }, // Ì
- { 'I', '^', 206 }, // ÃŽ
- { 'I', '"', 207 }, // Ã
- { 'N', '~', 209 }, // Ñ
- { 'O', '`', 210 }, // Ã’
- { 'O', '^', 212 }, // Ô
- { 'O', '~', 213 }, // Õ
- { '/', '\\', 215 }, // × - multiplication symbol in ISO 8859-1
- { 'U', '`', 217 }, // Ù
- { 'U', '^', 219 }, // Û
- { 'I', 'p', 222 }, // Þ
- { 'a', '`', 224 }, // à
- { 'a', '^', 226 }, // â
- { 'a', '~', 227 }, // ã
- { 'a', '"', 228 }, // ä
- { 'a', '@', 229 }, // å
- { 'e', '`', 232 }, // è
- { 'e', '^', 234 }, // ê
- { 'e', '"', 235 }, // ë
- { 'i', '`', 236 }, // ì
- { 'i', '^', 238 }, // î
- { 'n', '~', 241 }, // ñ
- { 'o', '`', 242 }, // ò
- { 'o', '^', 244 }, // ô
- { 'o', '~', 245 }, // õ
- { 'u', '`', 249 }, // ù
- { 'u', '^', 251 }, // û
- { 'y', '"', 255 }, // x XX
-
{ NUL, NUL, NUL }
};
@@ -1436,7 +1458,7 @@ int do_digraph(int c)
backspaced = -1;
} else if (p_dg) {
if (backspaced >= 0) {
- c = getdigraph(backspaced, c, FALSE);
+ c = getdigraph(backspaced, c, false);
}
backspaced = -1;
@@ -1450,8 +1472,9 @@ int do_digraph(int c)
/// Find a digraph for "val". If found return the string to display it.
/// If not found return NULL.
-char_u *get_digraph_for_char(int val)
+char_u *get_digraph_for_char(int val_arg)
{
+ const int val = val_arg;
digr_T *dp;
static char_u r[3];
@@ -1508,7 +1531,7 @@ int get_digraph(int cmdline)
if (cc != ESC) {
// ESC cancels CTRL-K
- return getdigraph(c, cc, TRUE);
+ return getdigraph(c, cc, true);
}
}
return NUL;
@@ -1520,9 +1543,9 @@ int get_digraph(int cmdline)
/// @param char2
/// @param meta_char
///
-/// @return If no match, return "char2". If "meta_char" is TRUE and "char1"
+/// @return If no match, return "char2". If "meta_char" is true and "char1"
// is a space, return "char2" | 0x80.
-static int getexactdigraph(int char1, int char2, int meta_char)
+static int getexactdigraph(int char1, int char2, bool meta_char)
{
int retval = 0;
@@ -1572,7 +1595,7 @@ static int getexactdigraph(int char1, int char2, int meta_char)
/// @param meta_char
///
/// @return The digraph.
-int getdigraph(int char1, int char2, int meta_char)
+int getdigraph(int char1, int char2, bool meta_char)
{
int retval;
@@ -1642,9 +1665,20 @@ void putdigraph(char_u *str)
}
}
-void listdigraphs(void)
+static void digraph_header(const char *msg)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (msg_col > 0) {
+ msg_putchar('\n');
+ }
+ msg_outtrans_attr((const char_u *)msg, HL_ATTR(HLF_CM));
+ msg_putchar('\n');
+}
+
+void listdigraphs(bool use_headers)
{
digr_T *dp;
+ result_T previous = 0;
msg_putchar('\n');
@@ -1656,25 +1690,63 @@ void listdigraphs(void)
// May need to convert the result to 'encoding'.
tmp.char1 = dp->char1;
tmp.char2 = dp->char2;
- tmp.result = getexactdigraph(tmp.char1, tmp.char2, FALSE);
+ tmp.result = getexactdigraph(tmp.char1, tmp.char2, false);
if ((tmp.result != 0)
&& (tmp.result != tmp.char2)) {
- printdigraph(&tmp);
+ printdigraph(&tmp, use_headers ? &previous : NULL);
}
dp++;
fast_breakcheck();
}
dp = (digr_T *)user_digraphs.ga_data;
- for (int i = 0; i < user_digraphs.ga_len && !got_int; ++i) {
- printdigraph(dp);
+ for (int i = 0; i < user_digraphs.ga_len && !got_int; i++) {
+ if (previous >= 0 && use_headers) {
+ digraph_header(_("Custom"));
+ }
+ previous = -1;
+ printdigraph(dp, NULL);
fast_breakcheck();
dp++;
}
}
-static void printdigraph(digr_T *dp)
+struct dg_header_entry {
+ int dg_start;
+ const char *dg_header;
+} header_table[] = {
+ { DG_START_LATIN, N_("Latin supplement") },
+ { DG_START_GREEK, N_("Greek and Coptic") },
+ { DG_START_CYRILLIC, N_("Cyrillic") },
+ { DG_START_HEBREW, N_("Hebrew") },
+ { DG_START_ARABIC, N_("Arabic") },
+ { DG_START_LATIN_EXTENDED, N_("Latin extended") },
+ { DG_START_GREEK_EXTENDED, N_("Greek extended") },
+ { DG_START_PUNCTUATION, N_("Punctuation") },
+ { DG_START_SUB_SUPER, N_("Super- and subscripts") },
+ { DG_START_CURRENCY, N_("Currency") },
+ { DG_START_OTHER1, N_("Other") },
+ { DG_START_ROMAN, N_("Roman numbers") },
+ { DG_START_ARROWS, N_("Arrows") },
+ { DG_START_MATH, N_("Mathematical operators") },
+ { DG_START_TECHNICAL, N_("Technical") },
+ { DG_START_OTHER2, N_("Other") },
+ { DG_START_DRAWING, N_("Box drawing") },
+ { DG_START_BLOCK, N_("Block elements") },
+ { DG_START_SHAPES, N_("Geometric shapes") },
+ { DG_START_SYMBOLS, N_("Symbols") },
+ { DG_START_DINGBATS, N_("Dingbats") },
+ { DG_START_CJK_SYMBOLS, N_("CJK symbols and punctuation") },
+ { DG_START_HIRAGANA, N_("Hiragana") },
+ { DG_START_KATAKANA, N_("Katakana") },
+ { DG_START_BOPOMOFO, N_("Bopomofo") },
+ { DG_START_OTHER3, N_("Other") },
+ { 0xfffffff, NULL },
+};
+
+static void printdigraph(const digr_T *dp, result_T *previous)
+ FUNC_ATTR_NONNULL_ARG(1)
{
char_u buf[30];
char_u *p;
@@ -1684,6 +1756,17 @@ static void printdigraph(digr_T *dp)
list_width = 13;
if (dp->result != 0) {
+ if (previous != NULL) {
+ for (int i = 0; header_table[i].dg_header != NULL; i++) {
+ if (*previous < header_table[i].dg_start
+ && dp->result >= header_table[i].dg_start
+ && dp->result < header_table[i + 1].dg_start) {
+ digraph_header(_(header_table[i].dg_header));
+ break;
+ }
+ }
+ *previous = dp->result;
+ }
if (msg_col > Columns - list_width) {
msg_putchar('\n');
}
@@ -1804,7 +1887,7 @@ void ex_loadkeymap(exarg_T *eap)
// Get each line of the sourced file, break at the end.
for (;;) {
- line = eap->getline(0, eap->cookie, 0);
+ line = eap->getline(0, eap->cookie, 0, true);
if (line == NULL) {
break;
diff --git a/src/nvim/edit.c b/src/nvim/edit.c
index 16c4882975..eecea03a19 100644
--- a/src/nvim/edit.c
+++ b/src/nvim/edit.c
@@ -28,6 +28,7 @@
#include "nvim/indent.h"
#include "nvim/indent_c.h"
#include "nvim/main.h"
+#include "nvim/mark_extended.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
@@ -1837,6 +1838,13 @@ change_indent (
xfree(new_line);
}
+
+ // change_indent seems to bec called twice, this combination only triggers
+ // once for both calls
+ if (new_cursor_col - vcol != 0) {
+ extmark_col_adjust(curbuf, curwin->w_cursor.lnum, 0, 0, amount,
+ kExtmarkUndo);
+ }
}
/*
@@ -3048,7 +3056,9 @@ static void ins_compl_clear(void)
XFREE_CLEAR(compl_orig_text);
compl_enter_selects = false;
// clear v:completed_item
- set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc());
+ dict_T *const d = tv_dict_alloc();
+ d->dv_lock = VAR_FIXED;
+ set_vim_var_dict(VV_COMPLETED_ITEM, d);
}
/// Check that Insert completion is active.
@@ -4047,12 +4057,14 @@ static int ins_compl_get_exp(pos_T *ini)
// Find up to TAG_MANY matches. Avoids that an enormous number
// of matches is found when compl_pattern is empty
+ g_tag_at_cursor = true;
if (find_tags(compl_pattern, &num_matches, &matches,
TAG_REGEXP | TAG_NAMES | TAG_NOIC | TAG_INS_COMP
| (l_ctrl_x_mode != CTRL_X_NORMAL ? TAG_VERBOSE : 0),
TAG_MANY, curbuf->b_ffname) == OK && num_matches > 0) {
ins_compl_add_matches(num_matches, matches, p_ic);
}
+ g_tag_at_cursor = false;
p_ic = save_p_ic;
break;
@@ -4116,7 +4128,7 @@ static int ins_compl_get_exp(pos_T *ini)
compl_direction,
compl_pattern, 1L,
SEARCH_KEEP + SEARCH_NFMSG,
- RE_LAST, (linenr_T)0, NULL, NULL);
+ RE_LAST, NULL);
}
msg_silent--;
if (!compl_started || set_match_pos) {
@@ -4303,7 +4315,9 @@ static void ins_compl_delete(void)
// causes flicker, thus we can't do that.
changed_cline_bef_curs();
// clear v:completed_item
- set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc());
+ dict_T *const d = tv_dict_alloc();
+ d->dv_lock = VAR_FIXED;
+ set_vim_var_dict(VV_COMPLETED_ITEM, d);
}
// Insert the new text being completed.
@@ -4325,6 +4339,7 @@ static dict_T *ins_compl_dict_alloc(compl_T *match)
{
// { word, abbr, menu, kind, info }
dict_T *dict = tv_dict_alloc();
+ dict->dv_lock = VAR_FIXED;
tv_dict_add_str(
dict, S_LEN("word"),
(const char *)EMPTY_IF_NULL(match->cp_str));
@@ -5479,10 +5494,10 @@ insertchar (
if (c == NUL) /* only formatting was wanted */
return;
- /* Check whether this character should end a comment. */
+ // Check whether this character should end a comment.
if (did_ai && c == end_comment_pending) {
char_u *line;
- char_u lead_end[COM_MAX_LEN]; /* end-comment string */
+ char_u lead_end[COM_MAX_LEN]; // end-comment string
int middle_len, end_len;
int i;
@@ -5490,39 +5505,40 @@ insertchar (
* Need to remove existing (middle) comment leader and insert end
* comment leader. First, check what comment leader we can find.
*/
- i = get_leader_len(line = get_cursor_line_ptr(), &p, FALSE, TRUE);
- if (i > 0 && vim_strchr(p, COM_MIDDLE) != NULL) { /* Just checking */
- /* Skip middle-comment string */
- while (*p && p[-1] != ':') /* find end of middle flags */
- ++p;
+ i = get_leader_len(line = get_cursor_line_ptr(), &p, false, true);
+ if (i > 0 && vim_strchr(p, COM_MIDDLE) != NULL) { // Just checking
+ // Skip middle-comment string
+ while (*p && p[-1] != ':') { // find end of middle flags
+ p++;
+ }
middle_len = copy_option_part(&p, lead_end, COM_MAX_LEN, ",");
- /* Don't count trailing white space for middle_len */
- while (middle_len > 0 && ascii_iswhite(lead_end[middle_len - 1]))
- --middle_len;
+ // Don't count trailing white space for middle_len
+ while (middle_len > 0 && ascii_iswhite(lead_end[middle_len - 1])) {
+ middle_len--;
+ }
- /* Find the end-comment string */
- while (*p && p[-1] != ':') /* find end of end flags */
- ++p;
+ // Find the end-comment string
+ while (*p && p[-1] != ':') { // find end of end flags
+ p++;
+ }
end_len = copy_option_part(&p, lead_end, COM_MAX_LEN, ",");
- /* Skip white space before the cursor */
+ // Skip white space before the cursor
i = curwin->w_cursor.col;
while (--i >= 0 && ascii_iswhite(line[i]))
;
i++;
- /* Skip to before the middle leader */
+ // Skip to before the middle leader
i -= middle_len;
- /* Check some expected things before we go on */
+ // Check some expected things before we go on
if (i >= 0 && lead_end[end_len - 1] == end_comment_pending) {
- /* Backspace over all the stuff we want to replace */
+ // Backspace over all the stuff we want to replace
backspace_until_column(i);
- /*
- * Insert the end-comment string, except for the last
- * character, which will get inserted as normal later.
- */
+ // Insert the end-comment string, except for the last
+ // character, which will get inserted as normal later.
ins_bytes_len(lead_end, end_len - 1);
}
}
@@ -5603,10 +5619,11 @@ insertchar (
AppendCharToRedobuff(c);
} else {
ins_char(c);
- if (flags & INSCHAR_CTRLV)
+ if (flags & INSCHAR_CTRLV) {
redo_literal(c);
- else
+ } else {
AppendCharToRedobuff(c);
+ }
}
}
}
@@ -6888,8 +6905,9 @@ static void mb_replace_pop_ins(int cc)
for (i = 1; i < n; ++i)
buf[i] = replace_pop();
ins_bytes_len(buf, n);
- } else
+ } else {
ins_char(cc);
+ }
if (enc_utf8)
/* Handle composing chars. */
@@ -7999,9 +8017,9 @@ static bool ins_bs(int c, int mode, int *inserted_space_p)
Insstart_orig.col = curwin->w_cursor.col;
}
- if (State & VREPLACE_FLAG)
+ if (State & VREPLACE_FLAG) {
ins_char(' ');
- else {
+ } else {
ins_str((char_u *)" ");
if ((State & REPLACE_FLAG))
replace_push(NUL);
@@ -8479,6 +8497,7 @@ static bool ins_tab(void)
} else { // otherwise use "tabstop"
temp = (int)curbuf->b_p_ts;
}
+
temp -= get_nolist_virtcol() % temp;
/*
@@ -8488,12 +8507,13 @@ static bool ins_tab(void)
*/
ins_char(' ');
while (--temp > 0) {
- if (State & VREPLACE_FLAG)
+ if (State & VREPLACE_FLAG) {
ins_char(' ');
- else {
+ } else {
ins_str((char_u *)" ");
- if (State & REPLACE_FLAG) /* no char replaced */
+ if (State & REPLACE_FLAG) { // no char replaced
replace_push(NUL);
+ }
}
}
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index e39f36957c..a556f0bbbe 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -422,6 +422,7 @@ static struct vimvar {
VV(VV_TYPE_BOOL, "t_bool", VAR_NUMBER, VV_RO),
VV(VV_ECHOSPACE, "echospace", VAR_NUMBER, VV_RO),
VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO),
+ VV(VV_LUA, "lua", VAR_PARTIAL, VV_RO),
};
#undef VV
@@ -433,11 +434,14 @@ static struct vimvar {
#define vv_str vv_di.di_tv.vval.v_string
#define vv_list vv_di.di_tv.vval.v_list
#define vv_dict vv_di.di_tv.vval.v_dict
+#define vv_partial vv_di.di_tv.vval.v_partial
#define vv_tv vv_di.di_tv
/// Variable used for v:
static ScopeDictDictItem vimvars_var;
+static partial_T *vvlua_partial;
+
/// v: hashtab
#define vimvarht vimvardict.dv_hashtab
@@ -639,6 +643,13 @@ void eval_init(void)
set_vim_var_nr(VV_ECHOSPACE, sc_col - 1);
+ vimvars[VV_LUA].vv_type = VAR_PARTIAL;
+ vvlua_partial = xcalloc(1, sizeof(partial_T));
+ vimvars[VV_LUA].vv_partial = vvlua_partial;
+ // this value shouldn't be printed, but if it is, do not crash
+ vvlua_partial->pt_name = xmallocz(0);
+ vvlua_partial->pt_refcount++;
+
set_reg_var(0); // default for v:register is not 0 but '"'
}
@@ -1313,12 +1324,25 @@ int call_vim_function(
{
int doesrange;
int ret;
+ int len = (int)STRLEN(func);
+ partial_T *pt = NULL;
+
+ if (len >= 6 && !memcmp(func, "v:lua.", 6)) {
+ func += 6;
+ len = check_luafunc_name((const char *)func, false);
+ if (len == 0) {
+ ret = FAIL;
+ goto fail;
+ }
+ pt = vvlua_partial;
+ }
rettv->v_type = VAR_UNKNOWN; // tv_clear() uses this.
- ret = call_func(func, (int)STRLEN(func), rettv, argc, argv, NULL,
+ ret = call_func(func, len, rettv, argc, argv, NULL,
curwin->w_cursor.lnum, curwin->w_cursor.lnum,
- &doesrange, true, NULL, NULL);
+ &doesrange, true, pt, NULL);
+fail:
if (ret == FAIL) {
tv_clear(rettv);
}
@@ -1504,6 +1528,116 @@ void ex_const(exarg_T *eap)
ex_let_const(eap, true);
}
+// Get a list of lines from a HERE document. The here document is a list of
+// lines surrounded by a marker.
+// cmd << {marker}
+// {line1}
+// {line2}
+// ....
+// {marker}
+//
+// The {marker} is a string. If the optional 'trim' word is supplied before the
+// marker, then the leading indentation before the lines (matching the
+// indentation in the 'cmd' line) is stripped.
+// Returns a List with {lines} or NULL.
+static list_T *
+heredoc_get(exarg_T *eap, char_u *cmd)
+{
+ char_u *marker;
+ char_u *p;
+ int marker_indent_len = 0;
+ int text_indent_len = 0;
+ char_u *text_indent = NULL;
+
+ if (eap->getline == NULL) {
+ EMSG(_("E991: cannot use =<< here"));
+ return NULL;
+ }
+
+ // Check for the optional 'trim' word before the marker
+ cmd = skipwhite(cmd);
+ if (STRNCMP(cmd, "trim", 4) == 0
+ && (cmd[4] == NUL || ascii_iswhite(cmd[4]))) {
+ cmd = skipwhite(cmd + 4);
+
+ // Trim the indentation from all the lines in the here document.
+ // The amount of indentation trimmed is the same as the indentation of
+ // the first line after the :let command line. To find the end marker
+ // the indent of the :let command line is trimmed.
+ p = *eap->cmdlinep;
+ while (ascii_iswhite(*p)) {
+ p++;
+ marker_indent_len++;
+ }
+ text_indent_len = -1;
+ }
+
+ // The marker is the next word.
+ if (*cmd != NUL && *cmd != '"') {
+ marker = skipwhite(cmd);
+ p = skiptowhite(marker);
+ if (*skipwhite(p) != NUL && *skipwhite(p) != '"') {
+ EMSG(_(e_trailing));
+ return NULL;
+ }
+ *p = NUL;
+ if (islower(*marker)) {
+ EMSG(_("E221: Marker cannot start with lower case letter"));
+ return NULL;
+ }
+ } else {
+ EMSG(_("E172: Missing marker"));
+ return NULL;
+ }
+
+ list_T *l = tv_list_alloc(0);
+ for (;;) {
+ int mi = 0;
+ int ti = 0;
+
+ char_u *theline = eap->getline(NUL, eap->cookie, 0, false);
+ if (theline == NULL) {
+ EMSG2(_("E990: Missing end marker '%s'"), marker);
+ break;
+ }
+
+ // with "trim": skip the indent matching the :let line to find the
+ // marker
+ if (marker_indent_len > 0
+ && STRNCMP(theline, *eap->cmdlinep, marker_indent_len) == 0) {
+ mi = marker_indent_len;
+ }
+ if (STRCMP(marker, theline + mi) == 0) {
+ xfree(theline);
+ break;
+ }
+ if (text_indent_len == -1 && *theline != NUL) {
+ // set the text indent from the first line.
+ p = theline;
+ text_indent_len = 0;
+ while (ascii_iswhite(*p)) {
+ p++;
+ text_indent_len++;
+ }
+ text_indent = vim_strnsave(theline, text_indent_len);
+ }
+ // with "trim": skip the indent matching the first line
+ if (text_indent != NULL) {
+ for (ti = 0; ti < text_indent_len; ti++) {
+ if (theline[ti] != text_indent[ti]) {
+ break;
+ }
+ }
+ }
+
+ tv_list_append_string(l, (char *)(theline + ti), -1);
+ xfree(theline);
+ }
+ xfree(text_indent);
+
+ return l;
+}
+
// ":let" list all variable values
// ":let var1 var2" list variable values
// ":let var = expr" assignment command.
@@ -1560,6 +1694,19 @@ static void ex_let_const(exarg_T *eap, const bool is_const)
list_vim_vars(&first);
}
eap->nextcmd = check_nextcmd(arg);
+ } else if (expr[0] == '=' && expr[1] == '<' && expr[2] == '<') {
+ // HERE document
+ list_T *l = heredoc_get(eap, expr + 3);
+ if (l != NULL) {
+ tv_list_set_ret(&rettv, l);
+ if (!eap->skip) {
+ op[0] = '=';
+ op[1] = NUL;
+ (void)ex_let_vars(eap->arg, &rettv, false, semicolon, var_count,
+ is_const, op);
+ }
+ tv_clear(&rettv);
+ }
} else {
op[0] = '=';
op[1] = NUL;
@@ -2339,6 +2486,13 @@ static char_u *get_lval(char_u *const name, typval_T *const rettv,
}
}
+ if (lp->ll_di != NULL && tv_is_luafunc(&lp->ll_di->di_tv)
+ && len == -1 && rettv == NULL) {
+ tv_clear(&var1);
+ EMSG2(e_illvar, "v:['lua']");
+ return NULL;
+ }
+
if (lp->ll_di == NULL) {
// Can't add "v:" or "a:" variable.
if (lp->ll_dict == &vimvardict
@@ -2718,7 +2872,7 @@ void set_context_for_expression(expand_T *xp, char_u *arg, cmdidx_T cmdidx)
int c;
char_u *p;
- if (cmdidx == CMD_let) {
+ if (cmdidx == CMD_let || cmdidx == CMD_const) {
xp->xp_context = EXPAND_USER_VARS;
if (vim_strpbrk(arg, (char_u *)"\"'+-*/%.=!?~|&$([<>,#") == NULL) {
/* ":let var1 var2 ...": find last space. */
@@ -4576,7 +4730,7 @@ eval_index(
if (evaluate) {
n1 = 0;
- if (!empty1 && rettv->v_type != VAR_DICT) {
+ if (!empty1 && rettv->v_type != VAR_DICT && !tv_is_luafunc(rettv)) {
n1 = tv_get_number(&var1);
tv_clear(&var1);
}
@@ -4700,7 +4854,7 @@ eval_index(
if (len == -1) {
tv_clear(&var1);
}
- if (item == NULL) {
+ if (item == NULL || tv_is_luafunc(&item->di_tv)) {
return FAIL;
}
@@ -5746,13 +5900,13 @@ static int dict_get_tv(char_u **arg, typval_T *rettv, int evaluate)
goto failret;
}
item = tv_dict_item_alloc((const char *)key);
- tv_clear(&tvkey);
item->di_tv = tv;
item->di_tv.v_lock = 0;
if (tv_dict_add(d, item) == FAIL) {
tv_dict_item_free(item);
}
}
+ tv_clear(&tvkey);
if (**arg == '}')
break;
@@ -6211,7 +6365,7 @@ static char_u *deref_func_name(const char *name, int *lenp,
*/
static int
get_func_tv(
- char_u *name, // name of the function
+ const char_u *name, // name of the function
int len, // length of "name"
typval_T *rettv,
char_u **arg, // argument, pointing to the '('
@@ -6467,7 +6621,15 @@ call_func(
rettv->vval.v_number = 0;
error = ERROR_UNKNOWN;
- if (!builtin_function((const char *)rfname, -1)) {
+ if (partial == vvlua_partial) {
+ if (len > 0) {
+ error = ERROR_NONE;
+ executor_call_lua((const char *)funcname, len,
+ argvars, argcount, rettv);
+ } else {
+ error = ERROR_UNKNOWN;
+ }
+ } else if (!builtin_function((const char *)rfname, -1)) {
// User defined function.
if (partial != NULL && partial->pt_func != NULL) {
fp = partial->pt_func;
@@ -6584,14 +6746,14 @@ call_func(
///
/// @param ermsg must be passed without translation (use N_() instead of _()).
/// @param name function name
-static void emsg_funcname(char *ermsg, char_u *name)
+static void emsg_funcname(char *ermsg, const char_u *name)
{
char_u *p;
if (*name == K_SPECIAL) {
p = concat_str((char_u *)"<SNR>", name + 3);
} else {
- p = name;
+ p = (char_u *)name;
}
EMSG2(_(ermsg), p);
@@ -6947,7 +7109,8 @@ static void assert_error(garray_T *gap)
(const char *)gap->ga_data, (ptrdiff_t)gap->ga_len);
}
-static void assert_equal_common(typval_T *argvars, assert_type_T atype)
+static int assert_equal_common(typval_T *argvars, assert_type_T atype)
+ FUNC_ATTR_NONNULL_ALL
{
garray_T ga;
@@ -6958,13 +7121,70 @@ static void assert_equal_common(typval_T *argvars, assert_type_T atype)
&argvars[0], &argvars[1], atype);
assert_error(&ga);
ga_clear(&ga);
+ return 1;
+ }
+ return 0;
+}
+
+static int assert_equalfile(typval_T *argvars)
+ FUNC_ATTR_NONNULL_ALL
+{
+ char buf1[NUMBUFLEN];
+ char buf2[NUMBUFLEN];
+ const char *const fname1 = tv_get_string_buf_chk(&argvars[0], buf1);
+ const char *const fname2 = tv_get_string_buf_chk(&argvars[1], buf2);
+ garray_T ga;
+
+ if (fname1 == NULL || fname2 == NULL) {
+ return 0;
+ }
+
+ IObuff[0] = NUL;
+ FILE *const fd1 = os_fopen(fname1, READBIN);
+ if (fd1 == NULL) {
+ snprintf((char *)IObuff, IOSIZE, (char *)e_notread, fname1);
+ } else {
+ FILE *const fd2 = os_fopen(fname2, READBIN);
+ if (fd2 == NULL) {
+ fclose(fd1);
+ snprintf((char *)IObuff, IOSIZE, (char *)e_notread, fname2);
+ } else {
+ for (int64_t count = 0; ; count++) {
+ const int c1 = fgetc(fd1);
+ const int c2 = fgetc(fd2);
+ if (c1 == EOF) {
+ if (c2 != EOF) {
+ STRCPY(IObuff, "first file is shorter");
+ }
+ break;
+ } else if (c2 == EOF) {
+ STRCPY(IObuff, "second file is shorter");
+ break;
+ } else if (c1 != c2) {
+ snprintf((char *)IObuff, IOSIZE,
+ "difference at byte %" PRId64, count);
+ break;
+ }
+ }
+ fclose(fd1);
+ fclose(fd2);
+ }
+ }
+ if (IObuff[0] != NUL) {
+ prepare_assert_error(&ga);
+ ga_concat(&ga, IObuff);
+ assert_error(&ga);
+ ga_clear(&ga);
+ return 1;
}
+ return 0;
}
static void f_assert_beeps(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
const char *const cmd = tv_get_string_chk(&argvars[0]);
garray_T ga;
+ int ret = 0;
called_vim_beep = false;
suppress_errthrow = true;
@@ -6976,22 +7196,30 @@ static void f_assert_beeps(typval_T *argvars, typval_T *rettv, FunPtr fptr)
ga_concat(&ga, (const char_u *)cmd);
assert_error(&ga);
ga_clear(&ga);
+ ret = 1;
}
suppress_errthrow = false;
emsg_on_display = false;
+ rettv->vval.v_number = ret;
}
// "assert_equal(expected, actual[, msg])" function
static void f_assert_equal(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
- assert_equal_common(argvars, ASSERT_EQUAL);
+ rettv->vval.v_number = assert_equal_common(argvars, ASSERT_EQUAL);
+}
+
+// "assert_equalfile(fname-one, fname-two)" function
+static void f_assert_equalfile(typval_T *argvars, typval_T *rettv, FunPtr fptr)
+{
+ rettv->vval.v_number = assert_equalfile(argvars);
}
// "assert_notequal(expected, actual[, msg])" function
static void f_assert_notequal(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
- assert_equal_common(argvars, ASSERT_NOTEQUAL);
+ rettv->vval.v_number = assert_equal_common(argvars, ASSERT_NOTEQUAL);
}
/// "assert_report(msg)
@@ -7003,34 +7231,21 @@ static void f_assert_report(typval_T *argvars, typval_T *rettv, FunPtr fptr)
ga_concat(&ga, (const char_u *)tv_get_string(&argvars[0]));
assert_error(&ga);
ga_clear(&ga);
+ rettv->vval.v_number = 1;
}
/// "assert_exception(string[, msg])" function
static void f_assert_exception(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
- garray_T ga;
-
- const char *const error = tv_get_string_chk(&argvars[0]);
- if (vimvars[VV_EXCEPTION].vv_str == NULL) {
- prepare_assert_error(&ga);
- ga_concat(&ga, (char_u *)"v:exception is not set");
- assert_error(&ga);
- ga_clear(&ga);
- } else if (error != NULL
- && strstr((char *)vimvars[VV_EXCEPTION].vv_str, error) == NULL) {
- prepare_assert_error(&ga);
- fill_assert_error(&ga, &argvars[1], NULL, &argvars[0],
- &vimvars[VV_EXCEPTION].vv_tv, ASSERT_OTHER);
- assert_error(&ga);
- ga_clear(&ga);
- }
+ rettv->vval.v_number = assert_exception(argvars);
}
-/// "assert_fails(cmd [, error])" function
+/// "assert_fails(cmd [, error [, msg]])" function
static void f_assert_fails(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
const char *const cmd = tv_get_string_chk(&argvars[0]);
garray_T ga;
+ int ret = 0;
int save_trylevel = trylevel;
// trylevel must be zero for a ":throw" command to be considered failed
@@ -7043,9 +7258,17 @@ static void f_assert_fails(typval_T *argvars, typval_T *rettv, FunPtr fptr)
if (!called_emsg) {
prepare_assert_error(&ga);
ga_concat(&ga, (const char_u *)"command did not fail: ");
- ga_concat(&ga, (const char_u *)cmd);
+ if (argvars[1].v_type != VAR_UNKNOWN
+ && argvars[2].v_type != VAR_UNKNOWN) {
+ char *const tofree = encode_tv2echo(&argvars[2], NULL);
+ ga_concat(&ga, (char_u *)tofree);
+ xfree(tofree);
+ } else {
+ ga_concat(&ga, (const char_u *)cmd);
+ }
assert_error(&ga);
ga_clear(&ga);
+ ret = 1;
} else if (argvars[1].v_type != VAR_UNKNOWN) {
char buf[NUMBUFLEN];
const char *const error = tv_get_string_buf_chk(&argvars[1], buf);
@@ -7057,6 +7280,7 @@ static void f_assert_fails(typval_T *argvars, typval_T *rettv, FunPtr fptr)
&vimvars[VV_ERRMSG].vv_tv, ASSERT_OTHER);
assert_error(&ga);
ga_clear(&ga);
+ ret = 1;
}
}
@@ -7066,9 +7290,11 @@ static void f_assert_fails(typval_T *argvars, typval_T *rettv, FunPtr fptr)
emsg_silent = false;
emsg_on_display = false;
set_vim_var_string(VV_ERRMSG, NULL, 0);
+ rettv->vval.v_number = ret;
}
-void assert_inrange(typval_T *argvars)
+static int assert_inrange(typval_T *argvars)
+ FUNC_ATTR_NONNULL_ALL
{
bool error = false;
const varnumber_T lower = tv_get_number_chk(&argvars[0], &error);
@@ -7076,7 +7302,7 @@ void assert_inrange(typval_T *argvars)
const varnumber_T actual = tv_get_number_chk(&argvars[2], &error);
if (error) {
- return;
+ return 0;
}
if (actual < lower || actual > upper) {
garray_T ga;
@@ -7090,11 +7316,14 @@ void assert_inrange(typval_T *argvars)
ASSERT_INRANGE);
assert_error(&ga);
ga_clear(&ga);
+ return 1;
}
+ return 0;
}
// Common for assert_true() and assert_false().
-static void assert_bool(typval_T *argvars, bool is_true)
+static int assert_bool(typval_T *argvars, bool is_true)
+ FUNC_ATTR_NONNULL_ALL
{
bool error = false;
garray_T ga;
@@ -7113,16 +7342,43 @@ static void assert_bool(typval_T *argvars, bool is_true)
NULL, &argvars[0], ASSERT_OTHER);
assert_error(&ga);
ga_clear(&ga);
+ return 1;
+ }
+ return 0;
+}
+
+static int assert_exception(typval_T *argvars)
+ FUNC_ATTR_NONNULL_ALL
+{
+ garray_T ga;
+
+ const char *const error = tv_get_string_chk(&argvars[0]);
+ if (vimvars[VV_EXCEPTION].vv_str == NULL) {
+ prepare_assert_error(&ga);
+ ga_concat(&ga, (char_u *)"v:exception is not set");
+ assert_error(&ga);
+ ga_clear(&ga);
+ return 1;
+ } else if (error != NULL
+ && strstr((char *)vimvars[VV_EXCEPTION].vv_str, error) == NULL) {
+ prepare_assert_error(&ga);
+ fill_assert_error(&ga, &argvars[1], NULL, &argvars[0],
+ &vimvars[VV_EXCEPTION].vv_tv, ASSERT_OTHER);
+ assert_error(&ga);
+ ga_clear(&ga);
+ return 1;
}
+ return 0;
}
// "assert_false(actual[, msg])" function
static void f_assert_false(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
- assert_bool(argvars, false);
+ rettv->vval.v_number = assert_bool(argvars, false);
}
-static void assert_match_common(typval_T *argvars, assert_type_T atype)
+static int assert_match_common(typval_T *argvars, assert_type_T atype)
+ FUNC_ATTR_NONNULL_ALL
{
char buf1[NUMBUFLEN];
char buf2[NUMBUFLEN];
@@ -7138,31 +7394,33 @@ static void assert_match_common(typval_T *argvars, assert_type_T atype)
fill_assert_error(&ga, &argvars[2], NULL, &argvars[0], &argvars[1], atype);
assert_error(&ga);
ga_clear(&ga);
+ return 1;
}
+ return 0;
}
/// "assert_inrange(lower, upper[, msg])" function
static void f_assert_inrange(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
- assert_inrange(argvars);
+ rettv->vval.v_number = assert_inrange(argvars);
}
/// "assert_match(pattern, actual[, msg])" function
static void f_assert_match(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
- assert_match_common(argvars, ASSERT_MATCH);
+ rettv->vval.v_number = assert_match_common(argvars, ASSERT_MATCH);
}
/// "assert_notmatch(pattern, actual[, msg])" function
static void f_assert_notmatch(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
- assert_match_common(argvars, ASSERT_NOTMATCH);
+ rettv->vval.v_number = assert_match_common(argvars, ASSERT_NOTMATCH);
}
// "assert_true(actual[, msg])" function
static void f_assert_true(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
- assert_bool(argvars, true);
+ rettv->vval.v_number = assert_bool(argvars, true);
}
/*
@@ -8458,18 +8716,25 @@ static void f_environ(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
tv_dict_alloc_ret(rettv);
- for (int i = 0; ; i++) {
- // TODO(justinmk): use os_copyfullenv from #7202 ?
- char *envname = os_getenvname_at_index((size_t)i);
- if (envname == NULL) {
- break;
- }
- const char *value = os_getenv(envname);
+ size_t env_size = os_get_fullenv_size();
+ char **env = xmalloc(sizeof(*env) * (env_size + 1));
+ env[env_size] = NULL;
+
+ os_copy_fullenv(env, env_size);
+
+ for (size_t i = 0; i < env_size; i++) {
+ const char * str = env[i];
+ const char * const end = strchr(str + (str[0] == '=' ? 1 : 0),
+ '=');
+ assert(end != NULL);
+ ptrdiff_t len = end - str;
+ assert(len > 0);
+ const char * value = str + len + 1;
tv_dict_add_str(rettv->vval.v_dict,
- (char *)envname, STRLEN((char *)envname),
- value == NULL ? "" : value);
- xfree(envname);
+ str, len,
+ value);
}
+ os_free_fullenv(env);
}
/*
@@ -8492,7 +8757,7 @@ static void f_getenv(typval_T *argvars, typval_T *rettv, FunPtr fptr)
if (p == NULL) {
rettv->v_type = VAR_SPECIAL;
- rettv->vval.v_number = kSpecialVarNull;
+ rettv->vval.v_special = kSpecialVarNull;
return;
}
rettv->vval.v_string = p;
@@ -8546,7 +8811,7 @@ typedef struct {
const listitem_T *li;
} GetListLineCookie;
-static char_u *get_list_line(int c, void *cookie, int indent)
+static char_u *get_list_line(int c, void *cookie, int indent, bool do_concat)
{
GetListLineCookie *const p = (GetListLineCookie *)cookie;
@@ -11245,6 +11510,9 @@ static void f_glob2regpat(typval_T *argvars, typval_T *rettv, FunPtr fptr)
static void f_has(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
static const char *const has_list[] = {
+#if defined(BSD) && !defined(__APPLE__)
+ "bsd",
+#endif
#ifdef UNIX
"unix",
#endif
@@ -12316,6 +12584,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
bool executable = true;
char **argv = tv_to_argv(&argvars[0], NULL, &executable);
+ char **env = NULL;
if (!argv) {
rettv->vval.v_number = executable ? 0 : -1;
return; // Did error message in tv_to_argv.
@@ -12333,6 +12602,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
bool detach = false;
bool rpc = false;
bool pty = false;
+ bool clear_env = false;
CallbackReader on_stdout = CALLBACK_READER_INIT,
on_stderr = CALLBACK_READER_INIT;
Callback on_exit = CALLBACK_NONE;
@@ -12343,6 +12613,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
detach = tv_dict_get_number(job_opts, "detach") != 0;
rpc = tv_dict_get_number(job_opts, "rpc") != 0;
pty = tv_dict_get_number(job_opts, "pty") != 0;
+ clear_env = tv_dict_get_number(job_opts, "clear_env") != 0;
if (pty && rpc) {
EMSG2(_(e_invarg2), "job cannot have both 'pty' and 'rpc' options set");
shell_free_argv(argv);
@@ -12359,6 +12630,45 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
return;
}
}
+ dictitem_T *job_env = tv_dict_find(job_opts, S_LEN("env"));
+ if (job_env) {
+ if (job_env->di_tv.v_type != VAR_DICT) {
+ EMSG2(_(e_invarg2), "env");
+ shell_free_argv(argv);
+ return;
+ }
+
+ size_t custom_env_size = (size_t)tv_dict_len(job_env->di_tv.vval.v_dict);
+ size_t i = 0;
+ size_t env_size = 0;
+
+ if (clear_env) {
+ // + 1 for last null entry
+ env = xmalloc((custom_env_size + 1) * sizeof(*env));
+ env_size = 0;
+ } else {
+ env_size = os_get_fullenv_size();
+
+ env = xmalloc((custom_env_size + env_size + 1) * sizeof(*env));
+
+ os_copy_fullenv(env, env_size);
+ i = env_size;
+ }
+ assert(env); // env must be allocated at this point
+
+ TV_DICT_ITER(job_env->di_tv.vval.v_dict, var, {
+ const char *str = tv_get_string(&var->di_tv);
+ assert(str);
+ size_t len = STRLEN(var->di_key) + strlen(str) + strlen("=") + 1;
+ env[i] = xmalloc(len);
+ snprintf(env[i], len, "%s=%s", (char *)var->di_key, str);
+ i++;
+ });
+
+ // must be null terminated
+ env[env_size + custom_env_size] = NULL;
+ }
+
if (!common_job_callbacks(job_opts, &on_stdout, &on_stderr, &on_exit)) {
shell_free_argv(argv);
@@ -12376,8 +12686,8 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
}
Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit, pty,
- rpc, detach, cwd, width, height, term_name,
- &rettv->vval.v_number);
+ rpc, detach, cwd, width, height,
+ term_name, env, &rettv->vval.v_number);
if (chan) {
channel_create_event(chan, NULL);
}
@@ -14398,6 +14708,7 @@ static int search_cmn(typval_T *argvars, pos_T *match_pos, int *flagsp)
long time_limit = 0;
int options = SEARCH_KEEP;
int subpatnum;
+ searchit_arg_T sia;
const char *const pat = tv_get_string(&argvars[0]);
dir = get_search_arg(&argvars[1], flagsp); // May set p_ws.
@@ -14445,8 +14756,11 @@ static int search_cmn(typval_T *argvars, pos_T *match_pos, int *flagsp)
}
pos = save_cursor = curwin->w_cursor;
+ memset(&sia, 0, sizeof(sia));
+ sia.sa_stop_lnum = (linenr_T)lnum_stop;
+ sia.sa_tm = &tm;
subpatnum = searchit(curwin, curbuf, &pos, NULL, dir, (char_u *)pat, 1,
- options, RE_SEARCH, (linenr_T)lnum_stop, &tm, NULL);
+ options, RE_SEARCH, &sia);
if (subpatnum != FAIL) {
if (flags & SP_SUBPAT)
retval = subpatnum;
@@ -14668,7 +14982,7 @@ static void f_rpcstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
Channel *chan = channel_job_start(argv, CALLBACK_READER_INIT,
CALLBACK_READER_INIT, CALLBACK_NONE,
- false, true, false, NULL, 0, 0, NULL,
+ false, true, false, NULL, 0, 0, NULL, NULL,
&rettv->vval.v_number);
if (chan) {
channel_create_event(chan, NULL);
@@ -15018,8 +15332,13 @@ do_searchpair(
clearpos(&foundpos);
pat = pat3;
for (;; ) {
+ searchit_arg_T sia;
+ memset(&sia, 0, sizeof(sia));
+ sia.sa_stop_lnum = lnum_stop;
+ sia.sa_tm = &tm;
+
n = searchit(curwin, curbuf, &pos, NULL, dir, pat, 1L,
- options, RE_SEARCH, lnum_stop, &tm, NULL);
+ options, RE_SEARCH, &sia);
if (n == FAIL || (firstpos.lnum != 0 && equalpos(pos, firstpos))) {
// didn't find it or found the first match again: FAIL
break;
@@ -15406,7 +15725,7 @@ static void f_setcharsearch(typval_T *argvars, typval_T *rettv, FunPtr fptr)
}
else
set_last_csearch(PTR2CHAR(csearch),
- csearch, MB_PTR2LEN(csearch));
+ csearch, utfc_ptr2len(csearch));
}
di = tv_dict_find(d, S_LEN("forward"));
@@ -15441,7 +15760,7 @@ static void f_setenv(typval_T *argvars, typval_T *rettv, FunPtr fptr)
const char *name = tv_get_string_buf(&argvars[0], namebuf);
if (argvars[1].v_type == VAR_SPECIAL
- && argvars[1].vval.v_number == kSpecialVarNull) {
+ && argvars[1].vval.v_special == kSpecialVarNull) {
os_unsetenv(name);
} else {
os_setenv(name, tv_get_string_buf(&argvars[1], valbuf), 1);
@@ -18050,7 +18369,7 @@ static void f_termopen(typval_T *argvars, typval_T *rettv, FunPtr fptr)
Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit,
true, false, false, cwd,
term_width, curwin->w_height_inner,
- xstrdup("xterm-256color"),
+ xstrdup("xterm-256color"), NULL,
&rettv->vval.v_number);
if (rettv->vval.v_number <= 0) {
return;
@@ -18350,6 +18669,7 @@ static void timer_due_cb(TimeWatcher *tw, void *data)
timer_T *timer = (timer_T *)data;
int save_did_emsg = did_emsg;
int save_called_emsg = called_emsg;
+ const bool save_ex_pressedreturn = get_pressedreturn();
if (timer->stopped || timer->paused) {
return;
@@ -18378,6 +18698,7 @@ static void timer_due_cb(TimeWatcher *tw, void *data)
}
did_emsg = save_did_emsg;
called_emsg = save_called_emsg;
+ set_pressedreturn(save_ex_pressedreturn);
if (timer->emsg_count >= 3) {
timer_stop(timer);
@@ -18747,8 +19068,9 @@ static void f_visualmode(typval_T *argvars, typval_T *rettv, FunPtr fptr)
*/
static void f_wildmenumode(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
- if (wild_menu_showing)
+ if (wild_menu_showing || ((State & CMDLINE) && pum_visible())) {
rettv->vval.v_number = 1;
+ }
}
/// "win_findbuf()" function
@@ -19937,6 +20259,26 @@ static void check_vars(const char *name, size_t len)
}
}
+/// check if special v:lua value for calling lua functions
+static bool tv_is_luafunc(typval_T *tv)
+{
+ return tv->v_type == VAR_PARTIAL && tv->vval.v_partial == vvlua_partial;
+}
+
+/// check the function name after "v:lua."
+static int check_luafunc_name(const char *str, bool paren)
+{
+ const char *p = str;
+ while (ASCII_ISALNUM(*p) || *p == '_' || *p == '.') {
+ p++;
+ }
+ if (*p != (paren ? '(' : NUL)) {
+ return 0;
+ } else {
+ return (int)(p-str);
+ }
+}
+
/// Handle expr[expr], expr[expr:expr] subscript and .name lookup.
/// Also handle function call with Funcref variable: func(expr)
/// Can all be combined: dict.func(expr)[idx]['func'](expr)
@@ -19950,9 +20292,30 @@ handle_subscript(
{
int ret = OK;
dict_T *selfdict = NULL;
- char_u *s;
+ const char_u *s;
int len;
typval_T functv;
+ int slen = 0;
+ bool lua = false;
+
+ if (tv_is_luafunc(rettv)) {
+ if (**arg != '.') {
+ tv_clear(rettv);
+ ret = FAIL;
+ } else {
+ (*arg)++;
+
+ lua = true;
+ s = (char_u *)(*arg);
+ slen = check_luafunc_name(*arg, true);
+ if (slen == 0) {
+ tv_clear(rettv);
+ ret = FAIL;
+ }
+ (*arg) += slen;
+ }
+ }
+
while (ret == OK
&& (**arg == '['
@@ -19969,14 +20332,16 @@ handle_subscript(
// Invoke the function. Recursive!
if (functv.v_type == VAR_PARTIAL) {
pt = functv.vval.v_partial;
- s = partial_name(pt);
+ if (!lua) {
+ s = partial_name(pt);
+ }
} else {
s = functv.vval.v_string;
}
} else {
s = (char_u *)"";
}
- ret = get_func_tv(s, (int)STRLEN(s), rettv, (char_u **)arg,
+ ret = get_func_tv(s, lua ? slen : (int)STRLEN(s), rettv, (char_u **)arg,
curwin->w_cursor.lnum, curwin->w_cursor.lnum,
&len, evaluate, pt, selfdict);
@@ -20211,7 +20576,7 @@ static hashtab_T *get_funccal_local_ht(void)
return &get_funccal()->l_vars.dv_hashtab;
}
-/// Find the dict and hashtable used for a variable
+/// Finds the dict (g:, l:, s:, …) and hashtable used for a variable.
///
/// @param[in] name Variable name, possibly with scope prefix.
/// @param[in] name_len Variable name length.
@@ -21077,7 +21442,6 @@ void ex_function(exarg_T *eap)
bool overwrite = false;
int indent;
int nesting;
- char_u *skip_until = NULL;
dictitem_T *v;
funcdict_T fudi;
static int func_nr = 0; /* number for nameless function */
@@ -21085,8 +21449,13 @@ void ex_function(exarg_T *eap)
hashtab_T *ht;
int todo;
hashitem_T *hi;
- int sourcing_lnum_off;
+ linenr_T sourcing_lnum_off;
+ linenr_T sourcing_lnum_top;
+ bool is_heredoc = false;
+ char_u *skip_until = NULL;
+ char_u *heredoc_trimmed = NULL;
bool show_block = false;
+ bool do_concat = true;
/*
* ":function" without argument: list functions.
@@ -21333,15 +21702,17 @@ void ex_function(exarg_T *eap)
cmdline_row = msg_row;
}
+ // Save the starting line number.
+ sourcing_lnum_top = sourcing_lnum;
+
indent = 2;
nesting = 0;
for (;; ) {
if (KeyTyped) {
- msg_scroll = TRUE;
- saved_wait_return = FALSE;
+ msg_scroll = true;
+ saved_wait_return = false;
}
- need_wait_return = FALSE;
- sourcing_lnum_off = sourcing_lnum;
+ need_wait_return = false;
if (line_arg != NULL) {
/* Use eap->arg, split up in parts by line breaks. */
@@ -21356,9 +21727,9 @@ void ex_function(exarg_T *eap)
} else {
xfree(line_to_free);
if (eap->getline == NULL) {
- theline = getcmdline(':', 0L, indent);
+ theline = getcmdline(':', 0L, indent, do_concat);
} else {
- theline = eap->getline(':', eap->cookie, indent);
+ theline = eap->getline(':', eap->cookie, indent, do_concat);
}
line_to_free = theline;
}
@@ -21374,17 +21745,37 @@ void ex_function(exarg_T *eap)
ui_ext_cmdline_block_append((size_t)indent, (const char *)theline);
}
- /* Detect line continuation: sourcing_lnum increased more than one. */
- if (sourcing_lnum > sourcing_lnum_off + 1)
- sourcing_lnum_off = sourcing_lnum - sourcing_lnum_off - 1;
- else
+ // Detect line continuation: sourcing_lnum increased more than one.
+ sourcing_lnum_off = get_sourced_lnum(eap->getline, eap->cookie);
+ if (sourcing_lnum < sourcing_lnum_off) {
+ sourcing_lnum_off -= sourcing_lnum;
+ } else {
sourcing_lnum_off = 0;
+ }
if (skip_until != NULL) {
- /* between ":append" and "." and between ":python <<EOF" and "EOF"
- * don't check for ":endfunc". */
- if (STRCMP(theline, skip_until) == 0) {
- XFREE_CLEAR(skip_until);
+ // Don't check for ":endfunc" between
+ // * ":append" and "."
+ // * ":python <<EOF" and "EOF"
+ // * ":let {var-name} =<< [trim] {marker}" and "{marker}"
+ if (heredoc_trimmed == NULL
+ || (is_heredoc && skipwhite(theline) == theline)
+ || STRNCMP(theline, heredoc_trimmed,
+ STRLEN(heredoc_trimmed)) == 0) {
+ if (heredoc_trimmed == NULL) {
+ p = theline;
+ } else if (is_heredoc) {
+ p = skipwhite(theline) == theline
+ ? theline : theline + STRLEN(heredoc_trimmed);
+ } else {
+ p = theline + STRLEN(heredoc_trimmed);
+ }
+ if (STRCMP(p, skip_until) == 0) {
+ XFREE_CLEAR(skip_until);
+ XFREE_CLEAR(heredoc_trimmed);
+ do_concat = true;
+ is_heredoc = false;
+ }
}
} else {
/* skip ':' and blanks*/
@@ -21483,6 +21874,34 @@ void ex_function(exarg_T *eap)
else
skip_until = vim_strsave(p);
}
+
+ // Check for ":let v =<< [trim] EOF"
+ // and ":let [a, b] =<< [trim] EOF"
+ arg = skipwhite(skiptowhite(p));
+ if (*arg == '[') {
+ arg = vim_strchr(arg, ']');
+ }
+ if (arg != NULL) {
+ arg = skipwhite(skiptowhite(arg));
+ if (arg[0] == '='
+ && arg[1] == '<'
+ && arg[2] =='<'
+ && (p[0] == 'l'
+ && p[1] == 'e'
+ && (!ASCII_ISALNUM(p[2])
+ || (p[2] == 't' && !ASCII_ISALNUM(p[3]))))) {
+ p = skipwhite(arg + 3);
+ if (STRNCMP(p, "trim", 4) == 0) {
+ // Ignore leading white space.
+ p = skipwhite(p + 4);
+ heredoc_trimmed =
+ vim_strnsave(theline, (int)(skipwhite(theline) - theline));
+ }
+ skip_until = vim_strnsave(p, (int)(skiptowhite(p) - p));
+ do_concat = false;
+ is_heredoc = true;
+ }
+ }
}
/* Add the line to the function. */
@@ -21652,7 +22071,8 @@ void ex_function(exarg_T *eap)
fp->uf_flags = flags;
fp->uf_calls = 0;
fp->uf_script_ctx = current_sctx;
- fp->uf_script_ctx.sc_lnum += sourcing_lnum - newlines.ga_len - 1;
+ fp->uf_script_ctx.sc_lnum += sourcing_lnum_top;
+
goto ret_free;
erret:
@@ -21669,7 +22089,7 @@ ret_free:
if (show_block) {
ui_ext_cmdline_block_leave();
}
-}
+} // NOLINT(readability/fn_size)
/// Get a function name, translating "<SID>" and "<SNR>".
/// Also handles a Funcref in a List or Dictionary.
@@ -21753,8 +22173,19 @@ trans_function_name(
*pp = (char_u *)end;
} else if (lv.ll_tv->v_type == VAR_PARTIAL
&& lv.ll_tv->vval.v_partial != NULL) {
- name = vim_strsave(partial_name(lv.ll_tv->vval.v_partial));
- *pp = (char_u *)end;
+ if (lv.ll_tv->vval.v_partial == vvlua_partial && *end == '.') {
+ len = check_luafunc_name((const char *)end+1, true);
+ if (len == 0) {
+ EMSG2(e_invexpr2, "v:lua");
+ goto theend;
+ }
+ name = xmallocz(len);
+ memcpy(name, end+1, len);
+ *pp = (char_u *)end+1+len;
+ } else {
+ name = vim_strsave(partial_name(lv.ll_tv->vval.v_partial));
+ *pp = (char_u *)end;
+ }
if (partial != NULL) {
*partial = lv.ll_tv->vval.v_partial;
}
@@ -22172,7 +22603,7 @@ void func_dump_profile(FILE *fd)
.channel_id = 0,
};
char_u *p = get_scriptname(last_set, &should_free);
- fprintf(fd, " Defined: %s line %" PRIdLINENR "\n",
+ fprintf(fd, " Defined: %s:%" PRIdLINENR "\n",
p, fp->uf_script_ctx.sc_lnum);
if (should_free) {
xfree(p);
@@ -23305,7 +23736,7 @@ char_u *get_return_cmd(void *rettv)
* Called by do_cmdline() to get the next line.
* Returns allocated string, or NULL for end of function.
*/
-char_u *get_func_line(int c, void *cookie, int indent)
+char_u *get_func_line(int c, void *cookie, int indent, bool do_concat)
{
funccall_T *fcp = (funccall_T *)cookie;
ufunc_T *fp = fcp->func;
@@ -23831,22 +24262,47 @@ repeat:
* - for second :e: before the current fname
* - otherwise: The last '.'
*/
- if (src[*usedlen + 1] == 'e' && *fnamep > tail)
+ const bool is_second_e = *fnamep > tail;
+ if (src[*usedlen + 1] == 'e' && is_second_e) {
s = *fnamep - 2;
- else
+ } else {
s = *fnamep + *fnamelen - 1;
- for (; s > tail; --s)
- if (s[0] == '.')
+ }
+
+ for (; s > tail; s--) {
+ if (s[0] == '.') {
break;
- if (src[*usedlen + 1] == 'e') { /* :e */
- if (s > tail) {
- *fnamelen += (size_t)(*fnamep - (s + 1));
- *fnamep = s + 1;
- } else if (*fnamep <= tail)
+ }
+ }
+ if (src[*usedlen + 1] == 'e') {
+ if (s > tail || (0 && is_second_e && s == tail)) {
+ // we stopped at a '.' (so anchor to &'.' + 1)
+ char_u *newstart = s + 1;
+ size_t distance_stepped_back = *fnamep - newstart;
+ *fnamelen += distance_stepped_back;
+ *fnamep = newstart;
+ } else if (*fnamep <= tail) {
*fnamelen = 0;
- } else { /* :r */
- if (s > tail) /* remove one extension */
+ }
+ } else {
+ // :r - Remove one extension
+ //
+ // Ensure that `s` doesn't go before `*fnamep`,
+ // since then we're taking too many roots:
+ //
+ // "path/to/this.file.ext" :e:e:r:r
+ // ^ ^-------- *fnamep
+ // +------------- tail
+ //
+ // Also ensure `s` doesn't go before `tail`,
+ // since then we're taking too many roots again:
+ //
+ // "path/to/this.file.ext" :r:r:r
+ // ^ ^------------- tail
+ // +--------------------- *fnamep
+ if (s > MAX(tail, *fnamep)) {
*fnamelen = (size_t)(s - *fnamep);
+ }
}
*usedlen += 2;
}
@@ -23952,8 +24408,8 @@ char_u *do_string_sub(char_u *str, char_u *pat, char_u *sub,
/* Skip empty match except for first match. */
if (regmatch.startp[0] == regmatch.endp[0]) {
if (zero_width == regmatch.startp[0]) {
- /* avoid getting stuck on a match with an empty string */
- int i = MB_PTR2LEN(tail);
+ // avoid getting stuck on a match with an empty string
+ int i = utfc_ptr2len(tail);
memmove((char_u *)ga.ga_data + ga.ga_len, tail, (size_t)i);
ga.ga_len += i;
tail += i;
diff --git a/src/nvim/eval.h b/src/nvim/eval.h
index e099de831a..2aa08e2074 100644
--- a/src/nvim/eval.h
+++ b/src/nvim/eval.h
@@ -117,6 +117,7 @@ typedef enum {
VV_TYPE_BOOL,
VV_ECHOSPACE,
VV_EXITING,
+ VV_LUA,
} VimVarIndex;
/// All recognized msgpack types
diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua
index ab5ff57c2f..0ae250e626 100644
--- a/src/nvim/eval.lua
+++ b/src/nvim/eval.lua
@@ -28,8 +28,9 @@ return {
asin={args=1, func="float_op_wrapper", data="&asin"}, -- WJMc
assert_beeps={args={1, 2}},
assert_equal={args={2, 3}},
+ assert_equalfile={args=2},
assert_exception={args={1, 2}},
- assert_fails={args={1, 2}},
+ assert_fails={args={1, 3}},
assert_false={args={1, 2}},
assert_inrange={args={3, 4}},
assert_match={args={2, 3}},
diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c
index 106b8f6eed..72ee45a03a 100644
--- a/src/nvim/eval/typval.c
+++ b/src/nvim/eval/typval.c
@@ -1200,6 +1200,7 @@ void tv_dict_watcher_notify(dict_T *const dict, const char *const key,
typval_T rettv;
+ dict->dv_refcount++;
QUEUE *w;
QUEUE_FOREACH(w, &dict->watchers) {
DictWatcher *watcher = tv_dict_watcher_node_data(w);
@@ -1211,6 +1212,7 @@ void tv_dict_watcher_notify(dict_T *const dict, const char *const key,
tv_clear(&rettv);
}
}
+ tv_dict_unref(dict);
for (size_t i = 1; i < ARRAY_SIZE(argv); i++) {
tv_clear(argv + i);
diff --git a/src/nvim/event/libuv_process.c b/src/nvim/event/libuv_process.c
index 63efee59a8..37b9f73ba4 100644
--- a/src/nvim/event/libuv_process.c
+++ b/src/nvim/event/libuv_process.c
@@ -41,7 +41,7 @@ int libuv_process_spawn(LibuvProcess *uvproc)
#endif
uvproc->uvopts.exit_cb = exit_cb;
uvproc->uvopts.cwd = proc->cwd;
- uvproc->uvopts.env = NULL; // Inherits the parent (nvim) env.
+ uvproc->uvopts.env = proc->env;
uvproc->uvopts.stdio = uvproc->uvstdio;
uvproc->uvopts.stdio_count = 3;
uvproc->uvstdio[0].flags = UV_IGNORE;
diff --git a/src/nvim/event/loop.c b/src/nvim/event/loop.c
index a01bbc9888..c2be472acd 100644
--- a/src/nvim/event/loop.c
+++ b/src/nvim/event/loop.c
@@ -162,10 +162,6 @@ size_t loop_size(Loop *loop)
return rv;
}
-void loop_dummy_event(void **argv)
-{
-}
-
static void async_cb(uv_async_t *handle)
{
Loop *l = handle->loop->data;
diff --git a/src/nvim/event/process.h b/src/nvim/event/process.h
index ef9d953ab7..b677b80bfe 100644
--- a/src/nvim/event/process.h
+++ b/src/nvim/event/process.h
@@ -23,6 +23,7 @@ struct process {
uint64_t stopped_time; // process_stop() timestamp
const char *cwd;
char **argv;
+ char **env;
Stream in, out, err;
process_exit_cb cb;
internal_process_cb internal_exit_cb, internal_close_cb;
diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c
index 16487ce447..0c3b467612 100644
--- a/src/nvim/ex_cmds.c
+++ b/src/nvim/ex_cmds.c
@@ -39,6 +39,7 @@
#include "nvim/buffer_updates.h"
#include "nvim/main.h"
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/message.h"
@@ -658,10 +659,10 @@ void ex_sort(exarg_T *eap)
deleted = (long)(count - (lnum - eap->line2));
if (deleted > 0) {
mark_adjust(eap->line2 - deleted, eap->line2, (long)MAXLNUM, -deleted,
- false);
+ false, kExtmarkUndo);
msgmore(-deleted);
} else if (deleted < 0) {
- mark_adjust(eap->line2, MAXLNUM, -deleted, 0L, false);
+ mark_adjust(eap->line2, MAXLNUM, -deleted, 0L, false, kExtmarkUndo);
}
if (change_occurred || deleted != 0) {
changed_lines(eap->line1, 0, eap->line2 + 1, -deleted, true);
@@ -874,10 +875,12 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest)
* their final destination at the new text position -- webb
*/
last_line = curbuf->b_ml.ml_line_count;
- mark_adjust_nofold(line1, line2, last_line - line2, 0L, true);
+ mark_adjust_nofold(line1, line2, last_line - line2, 0L, true, kExtmarkNoUndo);
+ extmark_adjust(curbuf, line1, line2, last_line - line2, 0L, kExtmarkNoUndo,
+ true);
changed_lines(last_line - num_lines + 1, 0, last_line + 1, num_lines, false);
if (dest >= line2) {
- mark_adjust_nofold(line2 + 1, dest, -num_lines, 0L, false);
+ mark_adjust_nofold(line2 + 1, dest, -num_lines, 0L, false, kExtmarkNoUndo);
FOR_ALL_TAB_WINDOWS(tab, win) {
if (win->w_buffer == curbuf) {
foldMoveRange(&win->w_folds, line1, line2, dest);
@@ -886,7 +889,8 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest)
curbuf->b_op_start.lnum = dest - num_lines + 1;
curbuf->b_op_end.lnum = dest;
} else {
- mark_adjust_nofold(dest + 1, line1 - 1, num_lines, 0L, false);
+ mark_adjust_nofold(dest + 1, line1 - 1, num_lines, 0L, false,
+ kExtmarkNoUndo);
FOR_ALL_TAB_WINDOWS(tab, win) {
if (win->w_buffer == curbuf) {
foldMoveRange(&win->w_folds, dest + 1, line1 - 1, line2);
@@ -897,7 +901,9 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest)
}
curbuf->b_op_start.col = curbuf->b_op_end.col = 0;
mark_adjust_nofold(last_line - num_lines + 1, last_line,
- -(last_line - dest - extra), 0L, true);
+ -(last_line - dest - extra), 0L, true, kExtmarkNoUndo);
+
+ u_extmark_move(curbuf, line1, line2, last_line, dest, num_lines, extra);
changed_lines(last_line - num_lines + 1, 0, last_line + 1, -extra, false);
// send update regarding the new lines that were added
@@ -1281,12 +1287,14 @@ static void do_filter(
if (cmdmod.keepmarks || vim_strchr(p_cpo, CPO_REMMARK) == NULL) {
if (read_linecount >= linecount) {
// move all marks from old lines to new lines
- mark_adjust(line1, line2, linecount, 0L, false);
+ mark_adjust(line1, line2, linecount, 0L, false, kExtmarkUndo);
} else {
// move marks from old lines to new lines, delete marks
// that are in deleted lines
- mark_adjust(line1, line1 + read_linecount - 1, linecount, 0L, false);
- mark_adjust(line1 + read_linecount, line2, MAXLNUM, 0L, false);
+ mark_adjust(line1, line1 + read_linecount - 1, linecount, 0L, false,
+ kExtmarkUndo);
+ mark_adjust(line1 + read_linecount, line2, MAXLNUM, 0L, false,
+ kExtmarkUndo);
}
}
@@ -1579,7 +1587,7 @@ int rename_buffer(char_u *new_fname)
xfname = curbuf->b_fname;
curbuf->b_ffname = NULL;
curbuf->b_sfname = NULL;
- if (setfname(curbuf, new_fname, NULL, TRUE) == FAIL) {
+ if (setfname(curbuf, new_fname, NULL, true) == FAIL) {
curbuf->b_ffname = fname;
curbuf->b_sfname = sfname;
return FAIL;
@@ -2767,7 +2775,7 @@ void ex_append(exarg_T *eap)
State = CMDLINE;
theline = eap->getline(
eap->cstack->cs_looplevel > 0 ? -1 :
- NUL, eap->cookie, indent);
+ NUL, eap->cookie, indent, true);
State = save_State;
}
lines_left = Rows - 1;
@@ -3214,6 +3222,189 @@ static char_u *sub_parse_flags(char_u *cmd, subflags_T *subflags,
return cmd;
}
+static void extmark_move_regmatch_single(lpos_T startpos,
+ lpos_T endpos,
+ linenr_T lnum,
+ int sublen)
+{
+ colnr_T mincol;
+ colnr_T endcol;
+ colnr_T col_amount;
+
+ mincol = startpos.col + 1;
+ endcol = endpos.col + 1;
+
+ // There are cases such as :s/^/x/ where this happens
+ // a delete is simply not required.
+ if (mincol + 1 <= endcol) {
+ extmark_col_adjust_delete(curbuf,
+ lnum, mincol + 1, endcol, kExtmarkUndo, 0);
+ }
+
+ // Insert, sublen seems to be the value we need but + 1...
+ col_amount = sublen - 1;
+ extmark_col_adjust(curbuf, lnum, mincol, 0, col_amount, kExtmarkUndo);
+}
+
+static void extmark_move_regmatch_multi(ExtmarkSubMulti s, long i)
+{
+ colnr_T mincol;
+ linenr_T u_lnum;
+ mincol = s.startpos.col + 1;
+
+ linenr_T n_u_lnum = s.lnum + s.endpos.lnum - s.startpos.lnum;
+ colnr_T n_after_newline_in_pat = s.endpos.col;
+ colnr_T n_before_newline_in_pat = mincol - s.cm_start.col;
+ long n_after_newline_in_sub;
+ if (!s.newline_in_sub) {
+ n_after_newline_in_sub = s.cm_end.col - s.cm_start.col;
+ } else {
+ n_after_newline_in_sub = s.cm_end.col;
+ }
+
+ if (s.newline_in_pat && !s.newline_in_sub) {
+ // -- Delete Pattern --
+ // 1. Move marks in the pattern
+ mincol = s.startpos.col + 1;
+ u_lnum = n_u_lnum;
+ assert(n_u_lnum == u_lnum);
+ extmark_copy_and_place(curbuf,
+ s.lnum, mincol,
+ u_lnum, n_after_newline_in_pat,
+ s.lnum, mincol,
+ kExtmarkUndo, true, NULL);
+ // 2. Move marks on last newline
+ mincol = mincol - n_before_newline_in_pat;
+ extmark_col_adjust(curbuf,
+ u_lnum,
+ n_after_newline_in_pat + 1,
+ -s.newline_in_pat,
+ mincol - n_after_newline_in_pat,
+ kExtmarkUndo);
+ // Take care of the lines after
+ extmark_adjust(curbuf,
+ u_lnum,
+ u_lnum,
+ MAXLNUM,
+ -s.newline_in_pat,
+ kExtmarkUndo,
+ false);
+ // 1. first insert the text in the substitutaion
+ extmark_col_adjust(curbuf,
+ s.lnum,
+ mincol + 1,
+ s.newline_in_sub,
+ n_after_newline_in_sub,
+ kExtmarkUndo);
+
+ } else {
+ // The data in sub_obj is as if the substituons above had already taken
+ // place. For our extmarks they haven't as we work from the bottom of the
+ // buffer up. Readjust the data.
+ n_u_lnum = s.lnum + s.endpos.lnum - s.startpos.lnum;
+ n_u_lnum = n_u_lnum - s.lnum_added;
+
+ // adjusted = L - (i-1)N
+ // where L = lnum value, N= lnum_added and i = iteration
+ linenr_T a_l_lnum = s.cm_start.lnum - ((i -1) * s.lnum_added);
+ linenr_T a_u_lnum = a_l_lnum + s.endpos.lnum;
+ assert(s.startpos.lnum == 0);
+
+ mincol = s.startpos.col + 1;
+ u_lnum = n_u_lnum;
+
+ if (!s.newline_in_pat && s.newline_in_sub) {
+ // -- Delete Pattern --
+ // 1. Move marks in the pattern
+ extmark_col_adjust_delete(curbuf,
+ a_l_lnum,
+ mincol + 1,
+ s.endpos.col + 1,
+ kExtmarkUndo,
+ s.eol);
+
+ extmark_adjust(curbuf,
+ a_u_lnum + 1,
+ MAXLNUM,
+ (long)s.newline_in_sub,
+ 0,
+ kExtmarkUndo,
+ false);
+ // 3. Insert
+ extmark_col_adjust(curbuf,
+ a_l_lnum,
+ mincol,
+ s.newline_in_sub,
+ (long)-mincol + 1 + n_after_newline_in_sub,
+ kExtmarkUndo);
+ } else if (s.newline_in_pat && s.newline_in_sub) {
+ if (s.lnum_added >= 0) {
+ linenr_T u_col = n_after_newline_in_pat == 0
+ ? 1 : n_after_newline_in_pat;
+ extmark_copy_and_place(curbuf,
+ a_l_lnum, mincol,
+ a_u_lnum, u_col,
+ a_l_lnum, mincol,
+ kExtmarkUndo, true, NULL);
+ // 2. Move marks on last newline
+ mincol = mincol - (colnr_T)n_before_newline_in_pat;
+ extmark_col_adjust(curbuf,
+ a_u_lnum,
+ (colnr_T)(n_after_newline_in_pat + 1),
+ -s.newline_in_pat,
+ mincol - n_after_newline_in_pat,
+ kExtmarkUndo);
+ // TODO(timeyyy): nothing to do here if lnum_added = 0
+ extmark_adjust(curbuf,
+ a_u_lnum + 1,
+ MAXLNUM,
+ (long)s.lnum_added,
+ 0,
+ kExtmarkUndo,
+ false);
+
+ extmark_col_adjust(curbuf,
+ a_l_lnum,
+ mincol + 1,
+ s.newline_in_sub,
+ (long)-mincol + n_after_newline_in_sub,
+ kExtmarkUndo);
+ } else {
+ mincol = s.startpos.col + 1;
+ a_l_lnum = s.startpos.lnum + 1;
+ a_u_lnum = s.endpos.lnum + 1;
+ extmark_copy_and_place(curbuf,
+ a_l_lnum, mincol,
+ a_u_lnum, n_after_newline_in_pat,
+ a_l_lnum, mincol,
+ kExtmarkUndo, true, NULL);
+ // 2. Move marks on last newline
+ mincol = mincol - (colnr_T)n_before_newline_in_pat;
+ extmark_col_adjust(curbuf,
+ a_u_lnum,
+ (colnr_T)(n_after_newline_in_pat + 1),
+ -s.newline_in_pat,
+ mincol - n_after_newline_in_pat,
+ kExtmarkUndo);
+ extmark_adjust(curbuf,
+ a_u_lnum,
+ a_u_lnum,
+ MAXLNUM,
+ s.lnum_added,
+ kExtmarkUndo,
+ false);
+ // 3. Insert
+ extmark_col_adjust(curbuf,
+ a_l_lnum,
+ mincol + 1,
+ s.newline_in_sub,
+ (long)-mincol + n_after_newline_in_sub,
+ kExtmarkUndo);
+ }
+ }
+ }
+}
+
/// Perform a substitution from line eap->line1 to line eap->line2 using the
/// command pointed to by eap->arg which should be of the form:
///
@@ -3260,6 +3451,17 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
int save_ma = 0;
int save_b_changed = curbuf->b_changed;
bool preview = (State & CMDPREVIEW);
+ extmark_sub_multi_vec_t extmark_sub_multi = KV_INITIAL_VALUE;
+ extmark_sub_single_vec_t extmark_sub_single = KV_INITIAL_VALUE;
+ linenr_T no_of_lines_changed = 0;
+ linenr_T newline_in_pat = 0;
+ linenr_T newline_in_sub = 0;
+
+ // inccommand tests fail without this check
+ if (!preview) {
+ // Required for Undo to work for extmarks.
+ u_save_cursor();
+ }
if (!global_busy) {
sub_nsubs = 0;
@@ -3418,6 +3620,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
// Check for a match on each line.
// If preview: limit to max('cmdwinheight', viewport).
linenr_T line2 = eap->line2;
+
for (linenr_T lnum = eap->line1;
lnum <= line2 && !got_quit && !aborting()
&& (!preview || preview_lines.lines_needed <= (linenr_T)p_cwh
@@ -3524,6 +3727,11 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
// Note: If not first match on a line, column can't be known here
current_match.start.lnum = sub_firstlnum;
+ // Match might be after the last line for "\n\zs" matching at
+ // the end of the last line.
+ if (lnum > curbuf->b_ml.ml_line_count) {
+ break;
+ }
if (sub_firstline == NULL) {
sub_firstline = vim_strsave(ml_get(sub_firstlnum));
}
@@ -3630,7 +3838,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
for (; i <= (long)ec; ++i)
msg_putchar('^');
- resp = getexmodeline('?', NULL, 0);
+ resp = getexmodeline('?', NULL, 0, true);
if (resp != NULL) {
typed = *resp;
xfree(resp);
@@ -3871,6 +4079,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
ADJUST_SUB_FIRSTLNUM();
+
// Now the trick is to replace CTRL-M chars with a real line
// break. This would make it impossible to insert a CTRL-M in
// the text. The line break can be avoided by preceding the
@@ -3885,7 +4094,9 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
*p1 = NUL; // truncate up to the CR
ml_append(lnum - 1, new_start,
(colnr_T)(p1 - new_start + 1), false);
- mark_adjust(lnum + 1, (linenr_T)MAXLNUM, 1L, 0L, false);
+ mark_adjust(lnum + 1, (linenr_T)MAXLNUM, 1L, 0L, false,
+ kExtmarkNOOP);
+
if (subflags.do_ask) {
appended_lines(lnum - 1, 1L);
} else {
@@ -3912,6 +4123,44 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout,
current_match.end.lnum = lnum;
}
+ // Adjust extmarks, by delete and then insert
+ if (!preview) {
+ newline_in_pat = (regmatch.endpos[0].lnum
+ - regmatch.startpos[0].lnum);
+ newline_in_sub = current_match.end.lnum - current_match.start.lnum;
+ if (newline_in_pat || newline_in_sub) {
+ ExtmarkSubMulti sub_multi;
+ no_of_lines_changed = newline_in_sub - newline_in_pat;
+
+ sub_multi.newline_in_pat = newline_in_pat;
+ sub_multi.newline_in_sub = newline_in_sub;
+ sub_multi.lnum = lnum;
+ sub_multi.lnum_added = no_of_lines_changed;
+ sub_multi.cm_start = current_match.start;
+ sub_multi.cm_end = current_match.end;
+
+ sub_multi.startpos = regmatch.startpos[0];
+ sub_multi.endpos = regmatch.endpos[0];
+ sub_multi.eol = extmark_eol_col(curbuf, lnum);
+
+ kv_push(extmark_sub_multi, sub_multi);
+ // Collect information required for moving extmarks WITHOUT \n, \r
+ } else {
+ no_of_lines_changed = 0;
+
+ if (regmatch.startpos[0].col != -1) {
+ ExtmarkSubSingle sub_single;
+ sub_single.sublen = sublen;
+ sub_single.lnum = lnum;
+ sub_single.startpos = regmatch.startpos[0];
+ sub_single.endpos = regmatch.endpos[0];
+
+ kv_push(extmark_sub_single, sub_single);
+ }
+ }
+ }
+
+
// 4. If subflags.do_all is set, find next match.
// Prevent endless loop with patterns that match empty
// strings, e.g. :s/$/pat/g or :s/[a-z]* /(&)/g.
@@ -3978,7 +4227,7 @@ skip:
ml_delete(lnum, false);
}
mark_adjust(lnum, lnum + nmatch_tl - 1,
- (long)MAXLNUM, -nmatch_tl, false);
+ (long)MAXLNUM, -nmatch_tl, false, kExtmarkNOOP);
if (subflags.do_ask) {
deleted_lines(lnum, nmatch_tl);
}
@@ -4154,6 +4403,35 @@ skip:
}
}
}
+ if (newline_in_pat || newline_in_sub) {
+ long n = (long)kv_size(extmark_sub_multi);
+ ExtmarkSubMulti sub_multi;
+ if (no_of_lines_changed < 0) {
+ for (i = 0; i < n; i++) {
+ sub_multi = kv_A(extmark_sub_multi, i);
+ extmark_move_regmatch_multi(sub_multi, i);
+ }
+ } else {
+ // Move extmarks in reverse order to avoid moving marks we just moved...
+ for (i = 0; i < n; i++) {
+ sub_multi = kv_Z(extmark_sub_multi, i);
+ extmark_move_regmatch_multi(sub_multi, n - i);
+ }
+ }
+ kv_destroy(extmark_sub_multi);
+ } else {
+ long n = (long)kv_size(extmark_sub_single);
+ ExtmarkSubSingle sub_single;
+ for (i = 0; i < n; i++) {
+ sub_single = kv_Z(extmark_sub_single, i);
+ extmark_move_regmatch_single(sub_single.startpos,
+ sub_single.endpos,
+ sub_single.lnum,
+ sub_single.sublen);
+ }
+
+ kv_destroy(extmark_sub_single);
+ }
kv_destroy(preview_lines.subresults);
@@ -4739,11 +5017,19 @@ int find_help_tags(const char_u *arg, int *num_matches, char_u ***matches,
if (STRNICMP(arg, "expr-", 5) == 0) {
// When the string starting with "expr-" and containing '?' and matches
- // the table, it is taken literally. Otherwise '?' is recognized as a
- // wildcard.
+ // the table, it is taken literally (but ~ is escaped). Otherwise '?'
+ // is recognized as a wildcard.
for (i = (int)ARRAY_SIZE(expr_table); --i >= 0; ) {
if (STRCMP(arg + 5, expr_table[i]) == 0) {
- STRCPY(d, arg);
+ for (int si = 0, di = 0; ; si++) {
+ if (arg[si] == '~') {
+ d[di++] = '\\';
+ }
+ d[di++] = arg[si];
+ if (arg[si] == NUL) {
+ break;
+ }
+ }
break;
}
}
@@ -4897,7 +5183,7 @@ int find_help_tags(const char_u *arg, int *num_matches, char_u ***matches,
*matches = NULL;
*num_matches = 0;
- int flags = TAG_HELP | TAG_REGEXP | TAG_NAMES | TAG_VERBOSE;
+ int flags = TAG_HELP | TAG_REGEXP | TAG_NAMES | TAG_VERBOSE | TAG_NO_TAGFUNC;
if (keep_lang) {
flags |= TAG_KEEP_LANG;
}
@@ -5023,7 +5309,7 @@ void fix_help_buffer(void)
copy_option_part(&p, NameBuff, MAXPATHL, ",");
char_u *const rt = (char_u *)vim_getenv("VIMRUNTIME");
if (rt != NULL
- && path_full_compare(rt, NameBuff, false) != kEqualFiles) {
+ && path_full_compare(rt, NameBuff, false, true) != kEqualFiles) {
int fcount;
char_u **fnames;
char_u *s;
@@ -5233,7 +5519,7 @@ static void helptags_one(char_u *const dir, const char_u *const ext,
ga_init(&ga, (int)sizeof(char_u *), 100);
if (add_help_tags
|| path_full_compare((char_u *)"$VIMRUNTIME/doc",
- dir, false) == kEqualFiles) {
+ dir, false, true) == kEqualFiles) {
s = xmalloc(18 + STRLEN(tagfname));
sprintf((char *)s, "help-tags\t%s\t1\n", tagfname);
GA_APPEND(char_u *, &ga, s);
@@ -5527,6 +5813,7 @@ static buf_T *show_sub(exarg_T *eap, pos_T old_cusr,
// We keep a special-purpose buffer around, but don't assume it exists.
buf_T *preview_buf = bufnr ? buflist_findnr(bufnr) : 0;
+ cmdmod.split = 0; // disable :leftabove/botright modifiers
cmdmod.tab = 0; // disable :tab modifier
cmdmod.noswapfile = true; // disable swap for preview buffer
// disable file info message
@@ -5573,6 +5860,9 @@ static buf_T *show_sub(exarg_T *eap, pos_T old_cusr,
highest_num_line = kv_last(lines.subresults).end.lnum;
col_width = log10(highest_num_line) + 1 + 3;
}
+ } else {
+ // Failed to split the window, don't show 'inccommand' preview.
+ preview_buf = NULL;
}
char *str = NULL; // construct the line to show in here
@@ -5585,7 +5875,7 @@ static buf_T *show_sub(exarg_T *eap, pos_T old_cusr,
for (size_t matchidx = 0; matchidx < lines.subresults.size; matchidx++) {
SubResult match = lines.subresults.items[matchidx];
- if (split && preview_buf) {
+ if (preview_buf) {
lpos_T p_start = { 0, match.start.col }; // match starts here in preview
lpos_T p_end = { 0, match.end.col }; // ... and ends here
diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua
index 8c0d22809f..f7aa8a994a 100644
--- a/src/nvim/ex_cmds.lua
+++ b/src/nvim/ex_cmds.lua
@@ -323,6 +323,12 @@ return {
func='ex_abclear',
},
{
+ command='cabove',
+ flags=bit.bor(RANGE, TRLBAR),
+ addr_type=ADDR_OTHER ,
+ func='ex_cbelow',
+ },
+ {
command='caddbuffer',
flags=bit.bor(RANGE, NOTADR, WORD1, TRLBAR),
addr_type=ADDR_LINES,
@@ -359,6 +365,12 @@ return {
func='ex_cbuffer',
},
{
+ command='cbelow',
+ flags=bit.bor(RANGE, TRLBAR),
+ addr_type=ADDR_OTHER ,
+ func='ex_cbelow',
+ },
+ {
command='cbottom',
flags=bit.bor(TRLBAR),
addr_type=ADDR_LINES,
@@ -758,7 +770,7 @@ return {
},
{
command='digraphs',
- flags=bit.bor(EXTRA, TRLBAR, CMDWIN),
+ flags=bit.bor(BANG, EXTRA, TRLBAR, CMDWIN),
addr_type=ADDR_LINES,
func='ex_digraphs',
},
@@ -1273,6 +1285,12 @@ return {
func='ex_last',
},
{
+ command='labove',
+ flags=bit.bor(RANGE, TRLBAR),
+ addr_type=ADDR_OTHER ,
+ func='ex_cbelow',
+ },
+ {
command='language',
flags=bit.bor(EXTRA, TRLBAR, CMDWIN),
addr_type=ADDR_LINES,
@@ -1309,6 +1327,12 @@ return {
func='ex_cbuffer',
},
{
+ command='lbelow',
+ flags=bit.bor(RANGE, TRLBAR),
+ addr_type=ADDR_OTHER ,
+ func='ex_cbelow',
+ },
+ {
command='lbottom',
flags=bit.bor(TRLBAR),
addr_type=ADDR_LINES,
@@ -1632,7 +1656,7 @@ return {
command='marks',
flags=bit.bor(EXTRA, TRLBAR, CMDWIN),
addr_type=ADDR_LINES,
- func='do_marks',
+ func='ex_marks',
},
{
command='match',
diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c
index 813d1a9b0b..496aecfb27 100644
--- a/src/nvim/ex_cmds2.c
+++ b/src/nvim/ex_cmds2.c
@@ -97,10 +97,11 @@ typedef struct sn_prl_S {
struct source_cookie {
FILE *fp; ///< opened file for sourcing
char_u *nextline; ///< if not NULL: line that was read ahead
+ linenr_T sourcing_lnum; ///< line number of the source file
int finished; ///< ":finish" used
#if defined(USE_CRNL)
int fileformat; ///< EOL_UNKNOWN, EOL_UNIX or EOL_DOS
- bool error; ///< true if LF found after CR-LF
+ bool error; ///< true if LF found after CR-LF
#endif
linenr_T breakpoint; ///< next line with breakpoint or zero
char_u *fname; ///< name of sourced file
@@ -1743,7 +1744,7 @@ static bool editing_arg_idx(win_T *win)
&& (win->w_buffer->b_ffname == NULL
|| !(path_full_compare(
alist_name(&WARGLIST(win)[win->w_arg_idx]),
- win->w_buffer->b_ffname, true) & kEqualFiles))));
+ win->w_buffer->b_ffname, true, true) & kEqualFiles))));
}
/// Check if window "win" is editing the w_arg_idx file in its argument list.
@@ -1761,7 +1762,7 @@ void check_arg_idx(win_T *win)
&& (win->w_buffer->b_fnum == GARGLIST[GARGCOUNT - 1].ae_fnum
|| (win->w_buffer->b_ffname != NULL
&& (path_full_compare(alist_name(&GARGLIST[GARGCOUNT - 1]),
- win->w_buffer->b_ffname, true)
+ win->w_buffer->b_ffname, true, true)
& kEqualFiles)))) {
arg_had_last = true;
}
@@ -3014,11 +3015,78 @@ static FILE *fopen_noinh_readbin(char *filename)
return fdopen(fd_tmp, READBIN);
}
+typedef struct {
+ char_u *buf;
+ size_t offset;
+} GetStrLineCookie;
-/// Read the file "fname" and execute its lines as EX commands.
+/// Get one full line from a sourced string (in-memory, no file).
+/// Called by do_cmdline() when it's called from do_source_str().
+///
+/// @return pointer to allocated line, or NULL for end-of-file or
+/// some error.
+static char_u *get_str_line(int c, void *cookie, int indent, bool do_concat)
+{
+ GetStrLineCookie *p = cookie;
+ size_t i = p->offset;
+ if (strlen((char *)p->buf) <= p->offset) {
+ return NULL;
+ }
+ while (!(p->buf[i] == '\n' || p->buf[i] == '\0')) {
+ i++;
+ }
+ char buf[2046];
+ char *dst;
+ dst = xstpncpy(buf, (char *)p->buf + p->offset, i - p->offset);
+ if ((uint32_t)(dst - buf) != i - p->offset) {
+ smsg(_(":source error parsing command %s"), p->buf);
+ return NULL;
+ }
+ buf[i - p->offset] = '\0';
+ p->offset = i + 1;
+ return (char_u *)xstrdup(buf);
+}
+
+/// Executes lines in `src` as Ex commands.
+///
+/// @see do_source()
+int do_source_str(const char *cmd, const char *traceback_name)
+{
+ char_u *save_sourcing_name = sourcing_name;
+ linenr_T save_sourcing_lnum = sourcing_lnum;
+ char_u sourcing_name_buf[256];
+ if (save_sourcing_name == NULL) {
+ sourcing_name = (char_u *)traceback_name;
+ } else {
+ snprintf((char *)sourcing_name_buf, sizeof(sourcing_name_buf),
+ "%s called at %s:%"PRIdLINENR, traceback_name, save_sourcing_name,
+ save_sourcing_lnum);
+ sourcing_name = sourcing_name_buf;
+ }
+ sourcing_lnum = 0;
+
+ GetStrLineCookie cookie = {
+ .buf = (char_u *)cmd,
+ .offset = 0,
+ };
+ const sctx_T save_current_sctx = current_sctx;
+ current_sctx.sc_sid = SID_STR;
+ current_sctx.sc_seq = 0;
+ current_sctx.sc_lnum = save_sourcing_lnum;
+ int retval = do_cmdline(NULL, get_str_line, (void *)&cookie,
+ DOCMD_VERBOSE | DOCMD_NOWAIT | DOCMD_REPEAT);
+ current_sctx = save_current_sctx;
+ sourcing_lnum = save_sourcing_lnum;
+ sourcing_name = save_sourcing_name;
+ return retval;
+}
+
+/// Reads the file `fname` and executes its lines as Ex commands.
///
/// This function may be called recursively!
///
+/// @see do_source_str
+///
/// @param fname
/// @param check_other check for .vimrc and _vimrc
/// @param is_vimrc DOSO_ value
@@ -3124,6 +3192,7 @@ int do_source(char_u *fname, int check_other, int is_vimrc)
#endif
cookie.nextline = NULL;
+ cookie.sourcing_lnum = 0;
cookie.finished = false;
// Check if this script has a breakpoint.
@@ -3218,7 +3287,7 @@ int do_source(char_u *fname, int check_other, int is_vimrc)
cookie.conv.vc_type = CONV_NONE; // no conversion
// Read the first line so we can check for a UTF-8 BOM.
- firstline = getsourceline(0, (void *)&cookie, 0);
+ firstline = getsourceline(0, (void *)&cookie, 0, true);
if (firstline != NULL && STRLEN(firstline) >= 3 && firstline[0] == 0xef
&& firstline[1] == 0xbb && firstline[2] == 0xbf) {
// Found BOM; setup conversion, skip over BOM and recode the line.
@@ -3358,6 +3427,8 @@ char_u *get_scriptname(LastSet last_set, bool *should_free)
_("API client (channel id %" PRIu64 ")"),
last_set.channel_id);
return IObuff;
+ case SID_STR:
+ return (char_u *)_("anonymous :source");
default:
*should_free = true;
return home_replace_save(NULL,
@@ -3375,13 +3446,20 @@ void free_scriptnames(void)
}
# endif
+linenr_T get_sourced_lnum(LineGetter fgetline, void *cookie)
+{
+ return fgetline == getsourceline
+ ? ((struct source_cookie *)cookie)->sourcing_lnum
+ : sourcing_lnum;
+}
+
/// Get one full line from a sourced file.
/// Called by do_cmdline() when it's called from do_source().
///
/// @return pointer to the line in allocated memory, or NULL for end-of-file or
/// some error.
-char_u *getsourceline(int c, void *cookie, int indent)
+char_u *getsourceline(int c, void *cookie, int indent, bool do_concat)
{
struct source_cookie *sp = (struct source_cookie *)cookie;
char_u *line;
@@ -3395,6 +3473,8 @@ char_u *getsourceline(int c, void *cookie, int indent)
if (do_profiling == PROF_YES) {
script_line_end();
}
+ // Set the current sourcing line number.
+ sourcing_lnum = sp->sourcing_lnum + 1;
// Get current line. If there is a read-ahead line, use it, otherwise get
// one now.
if (sp->finished) {
@@ -3404,7 +3484,7 @@ char_u *getsourceline(int c, void *cookie, int indent)
} else {
line = sp->nextline;
sp->nextline = NULL;
- sourcing_lnum++;
+ sp->sourcing_lnum++;
}
if (line != NULL && do_profiling == PROF_YES) {
script_line_start();
@@ -3412,9 +3492,9 @@ char_u *getsourceline(int c, void *cookie, int indent)
// Only concatenate lines starting with a \ when 'cpoptions' doesn't
// contain the 'C' flag.
- if (line != NULL && (vim_strchr(p_cpo, CPO_CONCAT) == NULL)) {
+ if (line != NULL && do_concat && (vim_strchr(p_cpo, CPO_CONCAT) == NULL)) {
// compensate for the one line read-ahead
- sourcing_lnum--;
+ sp->sourcing_lnum--;
// Get the next line and concatenate it when it starts with a
// backslash. We always need to read the next line, keep it in
@@ -3492,7 +3572,7 @@ static char_u *get_one_sourceline(struct source_cookie *sp)
ga_init(&ga, 1, 250);
// Loop until there is a finished line (or end-of-file).
- sourcing_lnum++;
+ sp->sourcing_lnum++;
for (;; ) {
// make room to read at least 120 (more) characters
ga_grow(&ga, 120);
@@ -3559,7 +3639,7 @@ retry:
// len&c parities (is faster than ((len-c)%2 == 0)) -- Acevedo
for (c = len - 2; c >= 0 && buf[c] == Ctrl_V; c--) {}
if ((len & 1) != (c & 1)) { // escaped NL, read more
- sourcing_lnum++;
+ sp->sourcing_lnum++;
continue;
}
diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h
index 42ba1060e9..3a9fd01dd9 100644
--- a/src/nvim/ex_cmds_defs.h
+++ b/src/nvim/ex_cmds_defs.h
@@ -88,7 +88,7 @@ typedef struct exarg exarg_T;
typedef void (*ex_func_T)(exarg_T *eap);
-typedef char_u *(*LineGetter)(int, void *, int);
+typedef char_u *(*LineGetter)(int, void *, int, bool);
/// Structure for command definition.
typedef struct cmdname {
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index 72d39adb3e..30c1373445 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -77,7 +77,7 @@
#include "nvim/api/private/helpers.h"
static int quitmore = 0;
-static int ex_pressedreturn = FALSE;
+static bool ex_pressedreturn = false;
/// Whether ":lcd" or ":tcd" was produced for a session.
static int did_lcd;
@@ -117,11 +117,11 @@ typedef struct {
* reads more lines that may come from the while/for loop.
*/
struct loop_cookie {
- garray_T *lines_gap; /* growarray with line info */
- int current_line; /* last read line from growarray */
- int repeating; /* TRUE when looping a second time */
- /* When "repeating" is FALSE use "getline" and "cookie" to get lines */
- char_u *(*getline)(int, void *, int);
+ garray_T *lines_gap; // growarray with line info
+ int current_line; // last read line from growarray
+ int repeating; // TRUE when looping a second time
+ // When "repeating" is FALSE use "getline" and "cookie" to get lines
+ char_u *(*getline)(int, void *, int, bool);
void *cookie;
};
@@ -140,6 +140,31 @@ struct dbg_stuff {
except_T *current_exception;
};
+typedef struct {
+ // parsed results
+ exarg_T *eap;
+ char_u *parsed_upto; // local we've parsed up to so far
+ char_u *cmd; // start of command
+ char_u *after_modifier;
+
+ // errors
+ char_u *errormsg;
+
+ // globals that need to be updated
+ cmdmod_T cmdmod;
+ int sandbox;
+ int msg_silent;
+ int emsg_silent;
+ bool ex_pressedreturn;
+ long p_verbose;
+
+ // other side-effects
+ bool set_eventignore;
+ long verbose_save;
+ int save_msg_silent;
+ int did_esilent;
+ bool did_sandbox;
+} parse_state_T;
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ex_docmd.c.generated.h"
@@ -271,25 +296,23 @@ int do_cmdline_cmd(const char *cmd)
DOCMD_NOWAIT|DOCMD_KEYTYPED);
}
-/*
- * do_cmdline(): execute one Ex command line
- *
- * 1. Execute "cmdline" when it is not NULL.
- * If "cmdline" is NULL, or more lines are needed, fgetline() is used.
- * 2. Split up in parts separated with '|'.
- *
- * This function can be called recursively!
- *
- * flags:
- * DOCMD_VERBOSE - The command will be included in the error message.
- * DOCMD_NOWAIT - Don't call wait_return() and friends.
- * DOCMD_REPEAT - Repeat execution until fgetline() returns NULL.
- * DOCMD_KEYTYPED - Don't reset KeyTyped.
- * DOCMD_EXCRESET - Reset the exception environment (used for debugging).
- * DOCMD_KEEPLINE - Store first typed line (for repeating with ".").
- *
- * return FAIL if cmdline could not be executed, OK otherwise
- */
+/// do_cmdline(): execute one Ex command line
+///
+/// 1. Execute "cmdline" when it is not NULL.
+/// If "cmdline" is NULL, or more lines are needed, fgetline() is used.
+/// 2. Split up in parts separated with '|'.
+///
+/// This function can be called recursively!
+///
+/// flags:
+/// DOCMD_VERBOSE - The command will be included in the error message.
+/// DOCMD_NOWAIT - Don't call wait_return() and friends.
+/// DOCMD_REPEAT - Repeat execution until fgetline() returns NULL.
+/// DOCMD_KEYTYPED - Don't reset KeyTyped.
+/// DOCMD_EXCRESET - Reset the exception environment (used for debugging).
+/// DOCMD_KEEPLINE - Store first typed line (for repeating with ".").
+///
+/// @return FAIL if cmdline could not be executed, OK otherwise
int do_cmdline(char_u *cmdline, LineGetter fgetline,
void *cookie, /* argument for fgetline() */
int flags)
@@ -313,8 +336,8 @@ int do_cmdline(char_u *cmdline, LineGetter fgetline,
struct msglist **saved_msg_list = NULL;
struct msglist *private_msg_list;
- /* "fgetline" and "cookie" passed to do_one_cmd() */
- char_u *(*cmd_getline)(int, void *, int);
+ // "fgetline" and "cookie" passed to do_one_cmd()
+ char_u *(*cmd_getline)(int, void *, int, bool);
void *cmd_cookie;
struct loop_cookie cmd_loop_cookie;
void *real_cookie;
@@ -396,13 +419,12 @@ int do_cmdline(char_u *cmdline, LineGetter fgetline,
// If force_abort is set, we cancel everything.
did_emsg = false;
- /*
- * KeyTyped is only set when calling vgetc(). Reset it here when not
- * calling vgetc() (sourced command lines).
- */
+ // KeyTyped is only set when calling vgetc(). Reset it here when not
+ // calling vgetc() (sourced command lines).
if (!(flags & DOCMD_KEYTYPED)
- && !getline_equal(fgetline, cookie, getexline))
+ && !getline_equal(fgetline, cookie, getexline)) {
KeyTyped = false;
+ }
/*
* Continue executing command lines:
@@ -507,17 +529,20 @@ int do_cmdline(char_u *cmdline, LineGetter fgetline,
* Need to set msg_didout for the first line after an ":if",
* otherwise the ":if" will be overwritten.
*/
- if (count == 1 && getline_equal(fgetline, cookie, getexline))
- msg_didout = TRUE;
- if (fgetline == NULL || (next_cmdline = fgetline(':', cookie,
- cstack.cs_idx <
- 0 ? 0 : (cstack.cs_idx + 1) * 2
- )) == NULL) {
- /* Don't call wait_return for aborted command line. The NULL
- * returned for the end of a sourced file or executed function
- * doesn't do this. */
- if (KeyTyped && !(flags & DOCMD_REPEAT))
- need_wait_return = FALSE;
+ if (count == 1 && getline_equal(fgetline, cookie, getexline)) {
+ msg_didout = true;
+ }
+ if (fgetline == NULL
+ || (next_cmdline = fgetline(':', cookie,
+ cstack.cs_idx <
+ 0 ? 0 : (cstack.cs_idx + 1) * 2,
+ true)) == NULL) {
+ // Don't call wait_return for aborted command line. The NULL
+ // returned for the end of a sourced file or executed function
+ // doesn't do this.
+ if (KeyTyped && !(flags & DOCMD_REPEAT)) {
+ need_wait_return = false;
+ }
retval = FAIL;
break;
}
@@ -951,7 +976,7 @@ int do_cmdline(char_u *cmdline, LineGetter fgetline,
/*
* Obtain a line when inside a ":while" or ":for" loop.
*/
-static char_u *get_loop_line(int c, void *cookie, int indent)
+static char_u *get_loop_line(int c, void *cookie, int indent, bool do_concat)
{
struct loop_cookie *cp = (struct loop_cookie *)cookie;
wcmd_T *wp;
@@ -961,11 +986,12 @@ static char_u *get_loop_line(int c, void *cookie, int indent)
if (cp->repeating)
return NULL; /* trying to read past ":endwhile"/":endfor" */
- /* First time inside the ":while"/":for": get line normally. */
- if (cp->getline == NULL)
- line = getcmdline(c, 0L, indent);
- else
- line = cp->getline(c, cp->cookie, indent);
+ // First time inside the ":while"/":for": get line normally.
+ if (cp->getline == NULL) {
+ line = getcmdline(c, 0L, indent, do_concat);
+ } else {
+ line = cp->getline(c, cp->cookie, indent, do_concat);
+ }
if (line != NULL) {
store_loop_line(cp->lines_gap, line);
++cp->current_line;
@@ -1197,69 +1223,74 @@ static void get_wincmd_addr_type(char_u *arg, exarg_T *eap)
}
}
-/*
- * Execute one Ex command.
- *
- * If 'sourcing' is TRUE, the command will be included in the error message.
- *
- * 1. skip comment lines and leading space
- * 2. handle command modifiers
- * 3. skip over the range to find the command
- * 4. parse the range
- * 5. parse the command
- * 6. parse arguments
- * 7. switch on command name
- *
- * Note: "fgetline" can be NULL.
- *
- * This function may be called recursively!
- */
-static char_u * do_one_cmd(char_u **cmdlinep,
- int flags,
- struct condstack *cstack,
- LineGetter fgetline,
- void *cookie /* argument for fgetline() */
- )
+/// Skip colons and trailing whitespace, returning a pointer to the first
+/// non-colon, non-whitespace character.
+//
+/// @param skipleadingwhite Skip leading whitespace too
+static char_u *skip_colon_white(const char_u *p, bool skipleadingwhite)
{
- char_u *p;
- linenr_T lnum;
- long n;
- char_u *errormsg = NULL; /* error message */
- exarg_T ea; /* Ex command arguments */
- long verbose_save = -1;
- int save_msg_scroll = msg_scroll;
- int save_msg_silent = -1;
- int did_esilent = 0;
- int did_sandbox = FALSE;
- cmdmod_T save_cmdmod;
- const int save_reg_executing = reg_executing;
- char_u *cmd;
- int address_count = 1;
+ if (skipleadingwhite) {
+ p = skipwhite(p);
+ }
- memset(&ea, 0, sizeof(ea));
- ea.line1 = 1;
- ea.line2 = 1;
- ex_nesting_level++;
+ while (*p == ':') {
+ p = skipwhite(p + 1);
+ }
- /* When the last file has not been edited :q has to be typed twice. */
- if (quitmore
- /* avoid that a function call in 'statusline' does this */
- && !getline_equal(fgetline, cookie, get_func_line)
- /* avoid that an autocommand, e.g. QuitPre, does this */
- && !getline_equal(fgetline, cookie, getnextac)
- )
- --quitmore;
+ return (char_u *)p;
+}
- /*
- * Reset browse, confirm, etc.. They are restored when returning, for
- * recursive calls.
- */
- save_cmdmod = cmdmod;
- memset(&cmdmod, 0, sizeof(cmdmod));
+static void parse_state_to_global(const parse_state_T *parse_state)
+{
+ cmdmod = parse_state->cmdmod;
+ sandbox = parse_state->sandbox;
+ msg_silent = parse_state->msg_silent;
+ emsg_silent = parse_state->emsg_silent;
+ ex_pressedreturn = parse_state->ex_pressedreturn;
+ p_verbose = parse_state->p_verbose;
- /* "#!anything" is handled like a comment. */
- if ((*cmdlinep)[0] == '#' && (*cmdlinep)[1] == '!')
- goto doend;
+ if (parse_state->set_eventignore) {
+ set_string_option_direct(
+ (char_u *)"ei", -1, (char_u *)"all", OPT_FREE, SID_NONE);
+ }
+}
+
+static void parse_state_from_global(parse_state_T *parse_state)
+{
+ memset(parse_state, 0, sizeof(*parse_state));
+ parse_state->cmdmod = cmdmod;
+ parse_state->sandbox = sandbox;
+ parse_state->msg_silent = msg_silent;
+ parse_state->emsg_silent = emsg_silent;
+ parse_state->ex_pressedreturn = ex_pressedreturn;
+ parse_state->p_verbose = p_verbose;
+}
+
+//
+// Parse one Ex command.
+//
+// This has no side-effects, except for modifying parameters
+// passed in by pointer.
+//
+// The `out` should be zeroed, and its `ea` member initialised,
+// before calling this function.
+//
+static bool parse_one_cmd(
+ char_u **cmdlinep,
+ parse_state_T *const out,
+ LineGetter fgetline,
+ void *fgetline_cookie)
+{
+ exarg_T ea = {
+ .line1 = 1,
+ .line2 = 1,
+ };
+ *out->eap = ea;
+
+ // "#!anything" is handled like a comment.
+ if ((*cmdlinep)[0] == '#' && (*cmdlinep)[1] == '!') {
+ return false;
+ }
/*
* Repeat until no more command modifiers are found.
@@ -1269,70 +1300,76 @@ static char_u * do_one_cmd(char_u **cmdlinep,
/*
* 1. Skip comment lines and leading white space and colons.
*/
- while (*ea.cmd == ' ' || *ea.cmd == '\t' || *ea.cmd == ':')
- ++ea.cmd;
+ while (*ea.cmd == ' '
+ || *ea.cmd == '\t'
+ || *ea.cmd == ':') {
+ ea.cmd++;
+ }
- /* in ex mode, an empty line works like :+ */
+ // in ex mode, an empty line works like :+
if (*ea.cmd == NUL && exmode_active
- && (getline_equal(fgetline, cookie, getexmodeline)
- || getline_equal(fgetline, cookie, getexline))
+ && (getline_equal(fgetline, fgetline_cookie, getexmodeline)
+ || getline_equal(fgetline, fgetline_cookie, getexline))
&& curwin->w_cursor.lnum < curbuf->b_ml.ml_line_count) {
ea.cmd = (char_u *)"+";
- ex_pressedreturn = TRUE;
+ out->ex_pressedreturn = true;
}
- /* ignore comment and empty lines */
- if (*ea.cmd == '"')
- goto doend;
+ // ignore comment and empty lines
+ if (*ea.cmd == '"') {
+ return false;
+ }
if (*ea.cmd == NUL) {
- ex_pressedreturn = TRUE;
- goto doend;
+ out->ex_pressedreturn = true;
+ return false;
}
/*
* 2. Handle command modifiers.
*/
- p = skip_range(ea.cmd, NULL);
+ char_u *p = skip_range(ea.cmd, NULL);
switch (*p) {
- /* When adding an entry, also modify cmd_exists(). */
+ // When adding an entry, also modify cmd_exists().
case 'a': if (!checkforcmd(&ea.cmd, "aboveleft", 3))
break;
- cmdmod.split |= WSP_ABOVE;
+ out->cmdmod.split |= WSP_ABOVE;
continue;
case 'b': if (checkforcmd(&ea.cmd, "belowright", 3)) {
- cmdmod.split |= WSP_BELOW;
+ out->cmdmod.split |= WSP_BELOW;
continue;
}
if (checkforcmd(&ea.cmd, "browse", 3)) {
- cmdmod.browse = true;
+ out->cmdmod.browse = true;
continue;
}
- if (!checkforcmd(&ea.cmd, "botright", 2))
+ if (!checkforcmd(&ea.cmd, "botright", 2)) {
break;
- cmdmod.split |= WSP_BOT;
+ }
+ out->cmdmod.split |= WSP_BOT;
continue;
case 'c': if (!checkforcmd(&ea.cmd, "confirm", 4))
break;
- cmdmod.confirm = true;
+ out->cmdmod.confirm = true;
continue;
case 'k': if (checkforcmd(&ea.cmd, "keepmarks", 3)) {
- cmdmod.keepmarks = true;
+ out->cmdmod.keepmarks = true;
continue;
}
if (checkforcmd(&ea.cmd, "keepalt", 5)) {
- cmdmod.keepalt = true;
+ out->cmdmod.keepalt = true;
continue;
}
if (checkforcmd(&ea.cmd, "keeppatterns", 5)) {
- cmdmod.keeppatterns = true;
+ out->cmdmod.keeppatterns = true;
continue;
}
- if (!checkforcmd(&ea.cmd, "keepjumps", 5))
+ if (!checkforcmd(&ea.cmd, "keepjumps", 5)) {
break;
- cmdmod.keepjumps = true;
+ }
+ out->cmdmod.keepjumps = true;
continue;
case 'f': { // only accept ":filter {pat} cmd"
@@ -1342,7 +1379,7 @@ static char_u * do_one_cmd(char_u **cmdlinep,
break;
}
if (*p == '!') {
- cmdmod.filter_force = true;
+ out->cmdmod.filter_force = true;
p = skipwhite(p + 1);
if (*p == NUL || ends_excmd(*p)) {
break;
@@ -1352,134 +1389,217 @@ static char_u * do_one_cmd(char_u **cmdlinep,
if (p == NULL || *p == NUL) {
break;
}
- cmdmod.filter_regmatch.regprog = vim_regcomp(reg_pat, RE_MAGIC);
- if (cmdmod.filter_regmatch.regprog == NULL) {
+ out->cmdmod.filter_regmatch.regprog = vim_regcomp(reg_pat, RE_MAGIC);
+ if (out->cmdmod.filter_regmatch.regprog == NULL) {
break;
}
ea.cmd = p;
continue;
}
- /* ":hide" and ":hide | cmd" are not modifiers */
+ // ":hide" and ":hide | cmd" are not modifiers
case 'h': if (p != ea.cmd || !checkforcmd(&p, "hide", 3)
|| *p == NUL || ends_excmd(*p))
break;
ea.cmd = p;
- cmdmod.hide = true;
+ out->cmdmod.hide = true;
continue;
case 'l': if (checkforcmd(&ea.cmd, "lockmarks", 3)) {
- cmdmod.lockmarks = true;
+ out->cmdmod.lockmarks = true;
continue;
}
- if (!checkforcmd(&ea.cmd, "leftabove", 5))
+ if (!checkforcmd(&ea.cmd, "leftabove", 5)) {
break;
- cmdmod.split |= WSP_ABOVE;
+ }
+ out->cmdmod.split |= WSP_ABOVE;
continue;
case 'n':
if (checkforcmd(&ea.cmd, "noautocmd", 3)) {
- if (cmdmod.save_ei == NULL) {
- /* Set 'eventignore' to "all". Restore the
- * existing option value later. */
- cmdmod.save_ei = vim_strsave(p_ei);
- set_string_option_direct(
- (char_u *)"ei", -1, (char_u *)"all", OPT_FREE, SID_NONE);
+ if (out->cmdmod.save_ei == NULL) {
+ // Set 'eventignore' to "all". Restore the
+ // existing option value later.
+ out->cmdmod.save_ei = vim_strsave(p_ei);
+ out->set_eventignore = true;
}
continue;
}
if (!checkforcmd(&ea.cmd, "noswapfile", 3)) {
break;
}
- cmdmod.noswapfile = true;
+ out->cmdmod.noswapfile = true;
continue;
case 'r': if (!checkforcmd(&ea.cmd, "rightbelow", 6))
break;
- cmdmod.split |= WSP_BELOW;
+ out->cmdmod.split |= WSP_BELOW;
continue;
case 's': if (checkforcmd(&ea.cmd, "sandbox", 3)) {
- if (!did_sandbox)
- ++sandbox;
- did_sandbox = TRUE;
+ if (!out->did_sandbox) {
+ out->sandbox++;
+ }
+ out->did_sandbox = true;
continue;
}
- if (!checkforcmd(&ea.cmd, "silent", 3))
+ if (!checkforcmd(&ea.cmd, "silent", 3)) {
break;
- if (save_msg_silent == -1)
- save_msg_silent = msg_silent;
- ++msg_silent;
+ }
+ if (out->save_msg_silent == -1) {
+ out->save_msg_silent = out->msg_silent;
+ }
+ out->msg_silent++;
if (*ea.cmd == '!' && !ascii_iswhite(ea.cmd[-1])) {
- /* ":silent!", but not "silent !cmd" */
+ // ":silent!", but not "silent !cmd"
ea.cmd = skipwhite(ea.cmd + 1);
- ++emsg_silent;
- ++did_esilent;
+ out->emsg_silent++;
+ out->did_esilent++;
}
continue;
case 't': if (checkforcmd(&p, "tab", 3)) {
- long tabnr = get_address(&ea, &ea.cmd, ADDR_TABS, ea.skip, false, 1);
+ long tabnr = get_address(
+ &ea, &ea.cmd, ADDR_TABS, ea.skip, false, 1);
+
if (tabnr == MAXLNUM) {
- cmdmod.tab = tabpage_index(curtab) + 1;
+ out->cmdmod.tab = tabpage_index(curtab) + 1;
} else {
if (tabnr < 0 || tabnr > LAST_TAB_NR) {
- errormsg = (char_u *)_(e_invrange);
- goto doend;
+ out->errormsg = (char_u *)_(e_invrange);
+ return false;
}
- cmdmod.tab = tabnr + 1;
+ out->cmdmod.tab = tabnr + 1;
}
ea.cmd = p;
continue;
}
- if (!checkforcmd(&ea.cmd, "topleft", 2))
+ if (!checkforcmd(&ea.cmd, "topleft", 2)) {
break;
- cmdmod.split |= WSP_TOP;
+ }
+ out->cmdmod.split |= WSP_TOP;
continue;
case 'u': if (!checkforcmd(&ea.cmd, "unsilent", 3))
break;
- if (save_msg_silent == -1)
- save_msg_silent = msg_silent;
- msg_silent = 0;
+ if (out->save_msg_silent == -1) {
+ out->save_msg_silent = out->msg_silent;
+ }
+ out->msg_silent = 0;
continue;
case 'v': if (checkforcmd(&ea.cmd, "vertical", 4)) {
- cmdmod.split |= WSP_VERT;
+ out->cmdmod.split |= WSP_VERT;
continue;
}
if (!checkforcmd(&p, "verbose", 4))
break;
- if (verbose_save < 0)
- verbose_save = p_verbose;
- if (ascii_isdigit(*ea.cmd))
- p_verbose = atoi((char *)ea.cmd);
- else
- p_verbose = 1;
+ if (out->verbose_save < 0) {
+ out->verbose_save = out->p_verbose;
+ }
+ if (ascii_isdigit(*ea.cmd)) {
+ out->p_verbose = atoi((char *)ea.cmd);
+ } else {
+ out->p_verbose = 1;
+ }
ea.cmd = p;
continue;
}
break;
}
- char_u *after_modifier = ea.cmd;
-
- ea.skip = (did_emsg
- || got_int
- || current_exception
- || (cstack->cs_idx >= 0
- && !(cstack->cs_flags[cstack->cs_idx] & CSF_ACTIVE)));
+ out->after_modifier = ea.cmd;
// 3. Skip over the range to find the command. Let "p" point to after it.
//
// We need the command to know what kind of range it uses.
- cmd = ea.cmd;
+ out->cmd = ea.cmd;
ea.cmd = skip_range(ea.cmd, NULL);
if (*ea.cmd == '*') {
ea.cmd = skipwhite(ea.cmd + 1);
}
- p = find_command(&ea, NULL);
+ out->parsed_upto = find_command(&ea, NULL);
+
+ *out->eap = ea;
+
+ return true;
+}
+
+/*
+ * Execute one Ex command.
+ *
+ * If 'sourcing' is TRUE, the command will be included in the error message.
+ *
+ * 1. skip comment lines and leading space
+ * 2. handle command modifiers
+ * 3. skip over the range to find the command
+ * 4. parse the range
+ * 5. parse the command
+ * 6. parse arguments
+ * 7. switch on command name
+ *
+ * Note: "fgetline" can be NULL.
+ *
+ * This function may be called recursively!
+ */
+static char_u * do_one_cmd(char_u **cmdlinep,
+ int flags,
+ struct condstack *cstack,
+ LineGetter fgetline,
+ void *cookie /* argument for fgetline() */
+ )
+{
+ char_u *p;
+ linenr_T lnum;
+ long n;
+ char_u *errormsg = NULL; // error message
+ exarg_T ea;
+ int save_msg_scroll = msg_scroll;
+ parse_state_T parsed;
+ cmdmod_T save_cmdmod;
+ const int save_reg_executing = reg_executing;
+
+ ex_nesting_level++;
+
+ /* When the last file has not been edited :q has to be typed twice. */
+ if (quitmore
+ /* avoid that a function call in 'statusline' does this */
+ && !getline_equal(fgetline, cookie, get_func_line)
+ /* avoid that an autocommand, e.g. QuitPre, does this */
+ && !getline_equal(fgetline, cookie, getnextac)
+ )
+ --quitmore;
+
+ /*
+ * Reset browse, confirm, etc.. They are restored when returning, for
+ * recursive calls.
+ */
+ save_cmdmod = cmdmod;
+ memset(&cmdmod, 0, sizeof(cmdmod));
+
+ parse_state_from_global(&parsed);
+ parsed.eap = &ea;
+ parsed.verbose_save = -1;
+ parsed.save_msg_silent = -1;
+ parsed.did_esilent = 0;
+ parsed.did_sandbox = false;
+ bool parse_success = parse_one_cmd(cmdlinep, &parsed, fgetline, cookie);
+ parse_state_to_global(&parsed);
+
+ // Update locals from parse_one_cmd()
+ errormsg = parsed.errormsg;
+ p = parsed.parsed_upto;
+
+ if (!parse_success) {
+ goto doend;
+ }
+
+ ea.skip = (did_emsg
+ || got_int
+ || current_exception
+ || (cstack->cs_idx >= 0
+ && !(cstack->cs_flags[cstack->cs_idx] & CSF_ACTIVE)));
// Count this line for profiling if skip is TRUE.
if (do_profiling == PROF_YES
@@ -1550,148 +1670,9 @@ static char_u * do_one_cmd(char_u **cmdlinep,
}
}
- /* repeat for all ',' or ';' separated addresses */
- ea.cmd = cmd;
- for (;; ) {
- ea.line1 = ea.line2;
- switch (ea.addr_type) {
- case ADDR_LINES:
- // default is current line number
- ea.line2 = curwin->w_cursor.lnum;
- break;
- case ADDR_WINDOWS:
- ea.line2 = CURRENT_WIN_NR;
- break;
- case ADDR_ARGUMENTS:
- ea.line2 = curwin->w_arg_idx + 1;
- if (ea.line2 > ARGCOUNT) {
- ea.line2 = ARGCOUNT;
- }
- break;
- case ADDR_LOADED_BUFFERS:
- case ADDR_BUFFERS:
- ea.line2 = curbuf->b_fnum;
- break;
- case ADDR_TABS:
- ea.line2 = CURRENT_TAB_NR;
- break;
- case ADDR_TABS_RELATIVE:
- ea.line2 = 1;
- break;
- case ADDR_QUICKFIX:
- ea.line2 = qf_get_cur_valid_idx(&ea);
- break;
- }
- ea.cmd = skipwhite(ea.cmd);
- lnum = get_address(&ea, &ea.cmd, ea.addr_type, ea.skip,
- ea.addr_count == 0, address_count++);
- if (ea.cmd == NULL) { // error detected
- goto doend;
- }
- if (lnum == MAXLNUM) {
- if (*ea.cmd == '%') { /* '%' - all lines */
- ++ea.cmd;
- switch (ea.addr_type) {
- case ADDR_LINES:
- ea.line1 = 1;
- ea.line2 = curbuf->b_ml.ml_line_count;
- break;
- case ADDR_LOADED_BUFFERS: {
- buf_T *buf = firstbuf;
- while (buf->b_next != NULL && buf->b_ml.ml_mfp == NULL) {
- buf = buf->b_next;
- }
- ea.line1 = buf->b_fnum;
- buf = lastbuf;
- while (buf->b_prev != NULL && buf->b_ml.ml_mfp == NULL) {
- buf = buf->b_prev;
- }
- ea.line2 = buf->b_fnum;
- break;
- }
- case ADDR_BUFFERS:
- ea.line1 = firstbuf->b_fnum;
- ea.line2 = lastbuf->b_fnum;
- break;
- case ADDR_WINDOWS:
- case ADDR_TABS:
- if (IS_USER_CMDIDX(ea.cmdidx)) {
- ea.line1 = 1;
- ea.line2 =
- ea.addr_type == ADDR_WINDOWS ? LAST_WIN_NR : LAST_TAB_NR;
- } else {
- // there is no Vim command which uses '%' and
- // ADDR_WINDOWS or ADDR_TABS
- errormsg = (char_u *)_(e_invrange);
- goto doend;
- }
- break;
- case ADDR_TABS_RELATIVE:
- errormsg = (char_u *)_(e_invrange);
- goto doend;
- break;
- case ADDR_ARGUMENTS:
- if (ARGCOUNT == 0) {
- ea.line1 = ea.line2 = 0;
- } else {
- ea.line1 = 1;
- ea.line2 = ARGCOUNT;
- }
- break;
- case ADDR_QUICKFIX:
- ea.line1 = 1;
- ea.line2 = qf_get_size(&ea);
- if (ea.line2 == 0) {
- ea.line2 = 1;
- }
- break;
- }
- ++ea.addr_count;
- }
- /* '*' - visual area */
- else if (*ea.cmd == '*') {
- pos_T *fp;
-
- if (ea.addr_type != ADDR_LINES) {
- errormsg = (char_u *)_(e_invrange);
- goto doend;
- }
-
- ++ea.cmd;
- if (!ea.skip) {
- fp = getmark('<', FALSE);
- if (check_mark(fp) == FAIL)
- goto doend;
- ea.line1 = fp->lnum;
- fp = getmark('>', FALSE);
- if (check_mark(fp) == FAIL)
- goto doend;
- ea.line2 = fp->lnum;
- ++ea.addr_count;
- }
- }
- } else
- ea.line2 = lnum;
- ea.addr_count++;
-
- if (*ea.cmd == ';') {
- if (!ea.skip) {
- curwin->w_cursor.lnum = ea.line2;
- // don't leave the cursor on an illegal line or column
- check_cursor();
- }
- } else if (*ea.cmd != ',') {
- break;
- }
- ea.cmd++;
- }
-
- /* One address given: set start and end lines */
- if (ea.addr_count == 1) {
- ea.line1 = ea.line2;
- /* ... but only implicit: really no address given */
- if (lnum == MAXLNUM)
- ea.addr_count = 0;
+ ea.cmd = parsed.cmd;
+ if (parse_cmd_address(&ea, &errormsg) == FAIL) {
+ goto doend;
}
/*
@@ -1701,9 +1682,7 @@ static char_u * do_one_cmd(char_u **cmdlinep,
/*
* Skip ':' and any white space
*/
- ea.cmd = skipwhite(ea.cmd);
- while (*ea.cmd == ':')
- ea.cmd = skipwhite(ea.cmd + 1);
+ ea.cmd = skip_colon_white(ea.cmd, true);
/*
* If we got a line, but no command, then go to the line.
@@ -1772,8 +1751,8 @@ static char_u * do_one_cmd(char_u **cmdlinep,
if (!(flags & DOCMD_VERBOSE)) {
// If the modifier was parsed OK the error must be in the following
// command
- if (after_modifier != NULL) {
- append_command(after_modifier);
+ if (parsed.after_modifier != NULL) {
+ append_command(parsed.after_modifier);
} else {
append_command(*cmdlinep);
}
@@ -2144,6 +2123,7 @@ static char_u * do_one_cmd(char_u **cmdlinep,
case CMD_browse:
case CMD_call:
case CMD_confirm:
+ case CMD_const:
case CMD_delfunction:
case CMD_djump:
case CMD_dlist:
@@ -2168,6 +2148,7 @@ static char_u * do_one_cmd(char_u **cmdlinep,
case CMD_leftabove:
case CMD_let:
case CMD_lockmarks:
+ case CMD_lockvar:
case CMD_lua:
case CMD_match:
case CMD_mzscheme:
@@ -2196,6 +2177,7 @@ static char_u * do_one_cmd(char_u **cmdlinep,
case CMD_tilde:
case CMD_topleft:
case CMD_unlet:
+ case CMD_unlockvar:
case CMD_verbose:
case CMD_vertical:
case CMD_wincmd:
@@ -2241,12 +2223,12 @@ static char_u * do_one_cmd(char_u **cmdlinep,
// The :try command saves the emsg_silent flag, reset it here when
// ":silent! try" was used, it should only apply to :try itself.
- if (ea.cmdidx == CMD_try && did_esilent > 0) {
- emsg_silent -= did_esilent;
+ if (ea.cmdidx == CMD_try && parsed.did_esilent > 0) {
+ emsg_silent -= parsed.did_esilent;
if (emsg_silent < 0) {
emsg_silent = 0;
}
- did_esilent = 0;
+ parsed.did_esilent = 0;
}
// 7. Execute the command.
@@ -2312,8 +2294,9 @@ doend:
? cmdnames[(int)ea.cmdidx].cmd_name
: (char_u *)NULL);
- if (verbose_save >= 0)
- p_verbose = verbose_save;
+ if (parsed.verbose_save >= 0) {
+ p_verbose = parsed.verbose_save;
+ }
if (cmdmod.save_ei != NULL) {
/* Restore 'eventignore' to the value before ":noautocmd". */
set_string_option_direct((char_u *)"ei", -1, cmdmod.save_ei,
@@ -2328,16 +2311,18 @@ doend:
cmdmod = save_cmdmod;
reg_executing = save_reg_executing;
- if (save_msg_silent != -1) {
- /* messages could be enabled for a serious error, need to check if the
- * counters don't become negative */
- if (!did_emsg || msg_silent > save_msg_silent)
- msg_silent = save_msg_silent;
- emsg_silent -= did_esilent;
- if (emsg_silent < 0)
+ if (parsed.save_msg_silent != -1) {
+ // messages could be enabled for a serious error, need to check if the
+ // counters don't become negative
+ if (!did_emsg || msg_silent > parsed.save_msg_silent) {
+ msg_silent = parsed.save_msg_silent;
+ }
+ emsg_silent -= parsed.did_esilent;
+ if (emsg_silent < 0) {
emsg_silent = 0;
- /* Restore msg_scroll, it's set by file I/O commands, even when no
- * message is actually displayed. */
+ }
+ // Restore msg_scroll, it's set by file I/O commands, even when no
+ // message is actually displayed.
msg_scroll = save_msg_scroll;
/* "silent reg" or "silent echo x" inside "redir" leaves msg_col
@@ -2346,8 +2331,9 @@ doend:
msg_col = 0;
}
- if (did_sandbox)
- --sandbox;
+ if (parsed.did_sandbox) {
+ sandbox--;
+ }
if (ea.nextcmd && *ea.nextcmd == NUL) /* not really a next command */
ea.nextcmd = NULL;
@@ -2357,6 +2343,160 @@ doend:
return ea.nextcmd;
}
+// Parse the address range, if any, in "eap".
+// Return FAIL and set "errormsg" or return OK.
+int parse_cmd_address(exarg_T *eap, char_u **errormsg)
+ FUNC_ATTR_NONNULL_ALL
+{
+ int address_count = 1;
+ linenr_T lnum;
+
+ // Repeat for all ',' or ';' separated addresses.
+ for (;;) {
+ eap->line1 = eap->line2;
+ switch (eap->addr_type) {
+ case ADDR_LINES:
+ // default is current line number
+ eap->line2 = curwin->w_cursor.lnum;
+ break;
+ case ADDR_WINDOWS:
+ eap->line2 = CURRENT_WIN_NR;
+ break;
+ case ADDR_ARGUMENTS:
+ eap->line2 = curwin->w_arg_idx + 1;
+ if (eap->line2 > ARGCOUNT) {
+ eap->line2 = ARGCOUNT;
+ }
+ break;
+ case ADDR_LOADED_BUFFERS:
+ case ADDR_BUFFERS:
+ eap->line2 = curbuf->b_fnum;
+ break;
+ case ADDR_TABS:
+ eap->line2 = CURRENT_TAB_NR;
+ break;
+ case ADDR_TABS_RELATIVE:
+ eap->line2 = 1;
+ break;
+ case ADDR_QUICKFIX:
+ eap->line2 = qf_get_cur_valid_idx(eap);
+ break;
+ }
+ eap->cmd = skipwhite(eap->cmd);
+ lnum = get_address(eap, &eap->cmd, eap->addr_type, eap->skip,
+ eap->addr_count == 0, address_count++);
+ if (eap->cmd == NULL) { // error detected
+ return FAIL;
+ }
+ if (lnum == MAXLNUM) {
+ if (*eap->cmd == '%') { // '%' - all lines
+ eap->cmd++;
+ switch (eap->addr_type) {
+ case ADDR_LINES:
+ eap->line1 = 1;
+ eap->line2 = curbuf->b_ml.ml_line_count;
+ break;
+ case ADDR_LOADED_BUFFERS: {
+ buf_T *buf = firstbuf;
+
+ while (buf->b_next != NULL && buf->b_ml.ml_mfp == NULL) {
+ buf = buf->b_next;
+ }
+ eap->line1 = buf->b_fnum;
+ buf = lastbuf;
+ while (buf->b_prev != NULL && buf->b_ml.ml_mfp == NULL) {
+ buf = buf->b_prev;
+ }
+ eap->line2 = buf->b_fnum;
+ break;
+ }
+ case ADDR_BUFFERS:
+ eap->line1 = firstbuf->b_fnum;
+ eap->line2 = lastbuf->b_fnum;
+ break;
+ case ADDR_WINDOWS:
+ case ADDR_TABS:
+ if (IS_USER_CMDIDX(eap->cmdidx)) {
+ eap->line1 = 1;
+ eap->line2 = eap->addr_type == ADDR_WINDOWS
+ ? LAST_WIN_NR : LAST_TAB_NR;
+ } else {
+ // there is no Vim command which uses '%' and
+ // ADDR_WINDOWS or ADDR_TABS
+ *errormsg = (char_u *)_(e_invrange);
+ return FAIL;
+ }
+ break;
+ case ADDR_TABS_RELATIVE:
+ *errormsg = (char_u *)_(e_invrange);
+ return FAIL;
+ case ADDR_ARGUMENTS:
+ if (ARGCOUNT == 0) {
+ eap->line1 = eap->line2 = 0;
+ } else {
+ eap->line1 = 1;
+ eap->line2 = ARGCOUNT;
+ }
+ break;
+ case ADDR_QUICKFIX:
+ eap->line1 = 1;
+ eap->line2 = qf_get_size(eap);
+ if (eap->line2 == 0) {
+ eap->line2 = 1;
+ }
+ break;
+ }
+ eap->addr_count++;
+ } else if (*eap->cmd == '*') {
+ // '*' - visual area
+ if (eap->addr_type != ADDR_LINES) {
+ *errormsg = (char_u *)_(e_invrange);
+ return FAIL;
+ }
+
+ eap->cmd++;
+ if (!eap->skip) {
+ pos_T *fp = getmark('<', false);
+ if (check_mark(fp) == FAIL) {
+ return FAIL;
+ }
+ eap->line1 = fp->lnum;
+ fp = getmark('>', false);
+ if (check_mark(fp) == FAIL) {
+ return FAIL;
+ }
+ eap->line2 = fp->lnum;
+ eap->addr_count++;
+ }
+ }
+ } else {
+ eap->line2 = lnum;
+ }
+ eap->addr_count++;
+
+ if (*eap->cmd == ';') {
+ if (!eap->skip) {
+ curwin->w_cursor.lnum = eap->line2;
+ // don't leave the cursor on an illegal line or column
+ check_cursor();
+ }
+ } else if (*eap->cmd != ',') {
+ break;
+ }
+ eap->cmd++;
+ }
+
+ // One address given: set start and end lines.
+ if (eap->addr_count == 1) {
+ eap->line1 = eap->line2;
+ // ... but only implicit: really no address given
+ if (lnum == MAXLNUM) {
+ eap->addr_count = 0;
+ }
+ }
+ return OK;
+}
+
/*
* Check for an Ex command with optional tail.
* If there is a match advance "pp" to the argument and return TRUE.
@@ -3297,6 +3437,7 @@ const char * set_one_cmd_context(
case CMD_syntax:
set_context_in_syntax_cmd(xp, arg);
break;
+ case CMD_const:
case CMD_let:
case CMD_if:
case CMD_elseif:
@@ -3537,15 +3678,13 @@ const char * set_one_cmd_context(
return NULL;
}
-/*
- * skip a range specifier of the form: addr [,addr] [;addr] ..
- *
- * Backslashed delimiters after / or ? will be skipped, and commands will
- * not be expanded between /'s and ?'s or after "'".
- *
- * Also skip white space and ":" characters.
- * Returns the "cmd" pointer advanced to beyond the range.
- */
+// Skip a range specifier of the form: addr [,addr] [;addr] ..
+//
+// Backslashed delimiters after / or ? will be skipped, and commands will
+// not be expanded between /'s and ?'s or after "'".
+//
+// Also skip white space and ":" characters.
+// Returns the "cmd" pointer advanced to beyond the range.
char_u *skip_range(
const char_u *cmd,
int *ctx // pointer to xp_context or NULL
@@ -3576,9 +3715,8 @@ char_u *skip_range(
++cmd;
}
- /* Skip ":" and white space. */
- while (*cmd == ':')
- cmd = skipwhite(cmd + 1);
+ // Skip ":" and white space.
+ cmd = skip_colon_white(cmd, false);
return (char_u *)cmd;
}
@@ -3746,8 +3884,7 @@ static linenr_T get_address(exarg_T *eap,
curwin->w_cursor.col = 0;
}
searchcmdlen = 0;
- if (!do_search(NULL, c, cmd, 1L,
- SEARCH_HIS | SEARCH_MSG, NULL, NULL)) {
+ if (!do_search(NULL, c, cmd, 1L, SEARCH_HIS | SEARCH_MSG, NULL)) {
curwin->w_cursor = pos;
cmd = NULL;
goto error;
@@ -3784,8 +3921,7 @@ static linenr_T get_address(exarg_T *eap,
pos.coladd = 0;
if (searchit(curwin, curbuf, &pos, NULL,
*cmd == '?' ? BACKWARD : FORWARD,
- (char_u *)"", 1L, SEARCH_MSG,
- i, (linenr_T)0, NULL, NULL) != FAIL) {
+ (char_u *)"", 1L, SEARCH_MSG, i, NULL) != FAIL) {
lnum = pos.lnum;
} else {
cmd = NULL;
@@ -4998,9 +5134,11 @@ static void uc_list(char_u *name, size_t name_len)
ucmd_T *cmd;
int len;
uint32_t a;
- garray_T *gap;
- gap = &curbuf->b_ucmds;
+ // In cmdwin, the alternative buffer should be used.
+ garray_T *gap = (cmdwin_type != 0 && get_cmdline_type() == NUL)
+ ? &prevwin->w_buffer->b_ucmds
+ : &curbuf->b_ucmds;
for (;; ) {
for (i = 0; i < gap->ga_len; ++i) {
cmd = USER_CMD_GA(gap, i);
@@ -5849,13 +5987,21 @@ char_u *get_user_cmd_addr_type(expand_T *xp, int idx)
/*
* Function given to ExpandGeneric() to obtain the list of user command names.
*/
-char_u *get_user_commands(expand_T *xp, int idx)
+char_u *get_user_commands(expand_T *xp FUNC_ATTR_UNUSED, int idx)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
- if (idx < curbuf->b_ucmds.ga_len)
- return USER_CMD_GA(&curbuf->b_ucmds, idx)->uc_name;
- idx -= curbuf->b_ucmds.ga_len;
- if (idx < ucmds.ga_len)
+ // In cmdwin, the alternative buffer should be used.
+ const buf_T *const buf = (cmdwin_type != 0 && get_cmdline_type() == NUL)
+ ? prevwin->w_buffer
+ : curbuf;
+
+ if (idx < buf->b_ucmds.ga_len) {
+ return USER_CMD_GA(&buf->b_ucmds, idx)->uc_name;
+ }
+ idx -= buf->b_ucmds.ga_len;
+ if (idx < ucmds.ga_len) {
return USER_CMD(idx)->uc_name;
+ }
return NULL;
}
@@ -6066,9 +6212,11 @@ static bool before_quit_autocmds(win_T *wp, bool quit_all, int forceit)
if (quit_all
|| (check_more(false, forceit) == OK && only_one_window())) {
apply_autocmds(EVENT_EXITPRE, NULL, NULL, false, curbuf);
- // Refuse to quit when locked or when the buffer in the last window is
- // being closed (can only happen in autocommands).
- if (curbuf_locked()
+ // Refuse to quit when locked or when the window was closed or the
+ // buffer in the last window is being closed (can only happen in
+ // autocommands).
+ if (!win_valid(wp)
+ || curbuf_locked()
|| (curbuf->b_nwindows == 1 && curbuf->b_locked > 0)) {
return true;
}
@@ -6708,17 +6856,18 @@ static void ex_preserve(exarg_T *eap)
/// ":recover".
static void ex_recover(exarg_T *eap)
{
- /* Set recoverymode right away to avoid the ATTENTION prompt. */
- recoverymode = TRUE;
+ // Set recoverymode right away to avoid the ATTENTION prompt.
+ recoverymode = true;
if (!check_changed(curbuf, (p_awa ? CCGD_AW : 0)
| CCGD_MULTWIN
| (eap->forceit ? CCGD_FORCEIT : 0)
| CCGD_EXCMD)
&& (*eap->arg == NUL
- || setfname(curbuf, eap->arg, NULL, TRUE) == OK))
- ml_recover();
- recoverymode = FALSE;
+ || setfname(curbuf, eap->arg, NULL, true) == OK)) {
+ ml_recover(true);
+ }
+ recoverymode = false;
}
/*
@@ -8188,6 +8337,7 @@ static void ex_normal(exarg_T *eap)
int save_insertmode = p_im;
int save_finish_op = finish_op;
long save_opcount = opcount;
+ const int save_reg_executing = reg_executing;
char_u *arg = NULL;
int l;
char_u *p;
@@ -8282,7 +8432,8 @@ static void ex_normal(exarg_T *eap)
p_im = save_insertmode;
finish_op = save_finish_op;
opcount = save_opcount;
- msg_didout |= save_msg_didout; /* don't reset msg_didout now */
+ reg_executing = save_reg_executing;
+ msg_didout |= save_msg_didout; // don't reset msg_didout now
/* Restore the state (needed when called from a function executed for
* 'indentexpr'). Update the mouse and cursor, they may have changed. */
@@ -9553,7 +9704,7 @@ put_view(
*/
if ((*flagp & SSOP_FOLDS)
&& wp->w_buffer->b_ffname != NULL
- && (*wp->w_buffer->b_p_bt == NUL || bt_help(wp->w_buffer))
+ && (bt_normal(wp->w_buffer) || bt_help(wp->w_buffer))
) {
if (put_folds(fd, wp) == FAIL)
return FAIL;
@@ -10010,10 +10161,11 @@ static void ex_setfiletype(exarg_T *eap)
static void ex_digraphs(exarg_T *eap)
{
- if (*eap->arg != NUL)
+ if (*eap->arg != NUL) {
putdigraph(eap->arg);
- else
- listdigraphs();
+ } else {
+ listdigraphs(eap->forceit);
+ }
}
static void ex_set(exarg_T *eap)
@@ -10131,6 +10283,28 @@ static void ex_folddo(exarg_T *eap)
ml_clearmarked(); // clear rest of the marks
}
+// Returns true if the supplied Ex cmdidx is for a location list command
+// instead of a quickfix command.
+bool is_loclist_cmd(int cmdidx)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ if (cmdidx < 0 || cmdidx >= CMD_SIZE) {
+ return false;
+ }
+ return cmdnames[cmdidx].cmd_name[0] == 'l';
+}
+
+bool get_pressedreturn(void)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ return ex_pressedreturn;
+}
+
+void set_pressedreturn(bool val)
+{
+ ex_pressedreturn = val;
+}
+
static void ex_terminal(exarg_T *eap)
{
char ex_cmd[1024];
@@ -10178,10 +10352,13 @@ bool cmd_can_preview(char_u *cmd)
return false;
}
+ // Ignore additional colons at the start...
+ cmd = skip_colon_white(cmd, true);
+
// Ignore any leading modifiers (:keeppatterns, :verbose, etc.)
for (int len = modifier_len(cmd); len != 0; len = modifier_len(cmd)) {
cmd += len;
- cmd = skipwhite(cmd);
+ cmd = skip_colon_white(cmd, true);
}
exarg_T ea;
diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c
index 3f7bc45c2e..8065d764b9 100644
--- a/src/nvim/ex_getln.c
+++ b/src/nvim/ex_getln.c
@@ -878,7 +878,9 @@ static int command_line_execute(VimState *state, int key)
}
if (s->c == cedit_key || s->c == K_CMDWIN) {
- if (ex_normal_busy == 0 && got_int == false) {
+ // TODO(vim): why is ex_normal_busy checked here?
+ if ((s->c == K_CMDWIN || ex_normal_busy == 0)
+ && got_int == false) {
// Open a window to edit the command line (and history).
s->c = open_cmdwin();
s->some_key_typed = true;
@@ -1075,7 +1077,7 @@ static void command_line_next_incsearch(CommandLineState *s, bool next_match)
int found = searchit(curwin, curbuf, &t, NULL,
next_match ? FORWARD : BACKWARD,
pat, s->count, search_flags,
- RE_SEARCH, 0, NULL, NULL);
+ RE_SEARCH, NULL);
emsg_off--;
ui_busy_stop();
if (found) {
@@ -1818,6 +1820,7 @@ static int command_line_changed(CommandLineState *s)
if (p_is && !cmd_silent && (s->firstc == '/' || s->firstc == '?')) {
pos_T end_pos;
proftime_T tm;
+ searchit_arg_T sia;
// if there is a character waiting, search and redraw later
if (char_avail()) {
@@ -1844,8 +1847,10 @@ static int command_line_changed(CommandLineState *s)
if (!p_hls) {
search_flags += SEARCH_KEEP;
}
+ memset(&sia, 0, sizeof(sia));
+ sia.sa_tm = &tm;
i = do_search(NULL, s->firstc, ccline.cmdbuff, s->count,
- search_flags, &tm, NULL);
+ search_flags, &sia);
emsg_off--;
// if interrupted while searching, behave like it failed
if (got_int) {
@@ -1924,7 +1929,9 @@ static int command_line_changed(CommandLineState *s)
// - Immediately undo the effects.
State |= CMDPREVIEW;
emsg_silent++; // Block error reporting as the command may be incomplete
+ msg_silent++; // Block messages, namely ones that prompt
do_cmdline(ccline.cmdbuff, NULL, NULL, DOCMD_KEEPLINE|DOCMD_NOWAIT);
+ msg_silent--; // Unblock messages
emsg_silent--; // Unblock error reporting
// Restore the window "view".
@@ -1991,7 +1998,8 @@ char_u *
getcmdline (
int firstc,
long count, // only used for incremental search
- int indent // indent for inside conditionals
+ int indent, // indent for inside conditionals
+ bool do_concat // unused
)
{
// Be prepared for situations where cmdline can be invoked recursively.
@@ -2167,17 +2175,18 @@ static void correct_screencol(int idx, int cells, int *col)
* Get an Ex command line for the ":" command.
*/
char_u *
-getexline (
- int c, /* normally ':', NUL for ":append" */
+getexline(
+ int c, // normally ':', NUL for ":append"
void *cookie,
- int indent /* indent for inside conditionals */
+ int indent, // indent for inside conditionals
+ bool do_concat
)
{
/* When executing a register, remove ':' that's in front of each line. */
if (exec_from_reg && vpeekc() == ':')
(void)vgetc();
- return getcmdline(c, 1L, indent);
+ return getcmdline(c, 1L, indent, do_concat);
}
/*
@@ -2187,11 +2196,12 @@ getexline (
* Returns a string in allocated memory or NULL.
*/
char_u *
-getexmodeline (
- int promptc, /* normally ':', NUL for ":append" and '?' for
- :s prompt */
+getexmodeline(
+ int promptc, // normally ':', NUL for ":append" and '?'
+ // for :s prompt
void *cookie,
- int indent /* indent for inside conditionals */
+ int indent, // indent for inside conditionals
+ bool do_concat
)
{
garray_T line_ga;
@@ -2320,7 +2330,7 @@ redraw:
} while (++vcol % 8);
p++;
} else {
- len = MB_PTR2LEN(p);
+ len = utfc_ptr2len(p);
msg_outtrans_len(p, len);
vcol += ptr2cells(p);
p += len;
@@ -5000,19 +5010,24 @@ static void expand_shellcmd(char_u *filepat, int *num_file, char_u ***file,
hashtab_T found_ht;
hash_init(&found_ht);
for (s = path; ; s = e) {
+ e = vim_strchr(s, ENV_SEPCHAR);
+ if (e == NULL) {
+ e = s + STRLEN(s);
+ }
+
if (*s == NUL) {
if (did_curdir) {
break;
}
// Find directories in the current directory, path is empty.
did_curdir = true;
- } else if (*s == '.') {
+ flags |= EW_DIR;
+ } else if (STRNCMP(s, ".", e - s) == 0) {
did_curdir = true;
- }
-
- e = vim_strchr(s, ENV_SEPCHAR);
- if (e == NULL) {
- e = s + STRLEN(s);
+ flags |= EW_DIR;
+ } else {
+ // Do not match directories inside a $PATH item.
+ flags &= ~EW_DIR;
}
l = (size_t)(e - s);
@@ -6065,12 +6080,9 @@ static int open_cmdwin(void)
set_bufref(&old_curbuf, curbuf);
- /* Save current window sizes. */
+ // Save current window sizes.
win_size_save(&winsizes);
- /* Don't execute autocommands while creating the window. */
- block_autocmds();
-
// When using completion in Insert mode with <C-R>=<C-F> one can open the
// command line window, but we don't want the popup menu then.
pum_undisplay(true);
@@ -6079,10 +6091,9 @@ static int open_cmdwin(void)
cmdmod.tab = 0;
cmdmod.noswapfile = 1;
- /* Create a window for the command-line buffer. */
+ // Create a window for the command-line buffer.
if (win_split((int)p_cwh, WSP_BOT) == FAIL) {
beep_flush();
- unblock_autocmds();
return K_IGNORE;
}
cmdwin_type = get_cmdline_type();
@@ -6097,13 +6108,11 @@ static int open_cmdwin(void)
curbuf->b_p_ma = true;
curwin->w_p_fen = false;
- // Do execute autocommands for setting the filetype (load syntax).
- unblock_autocmds();
- // But don't allow switching to another buffer.
+ // Don't allow switching to another buffer.
curbuf_lock++;
- /* Showing the prompt may have set need_wait_return, reset it. */
- need_wait_return = FALSE;
+ // Showing the prompt may have set need_wait_return, reset it.
+ need_wait_return = false;
const int histtype = hist_char2type(cmdwin_type);
if (histtype == HIST_CMD || histtype == HIST_DEBUG) {
@@ -6115,11 +6124,11 @@ static int open_cmdwin(void)
}
curbuf_lock--;
- /* Reset 'textwidth' after setting 'filetype' (the Vim filetype plugin
- * sets 'textwidth' to 78). */
+ // Reset 'textwidth' after setting 'filetype' (the Vim filetype plugin
+ // sets 'textwidth' to 78).
curbuf->b_p_tw = 0;
- /* Fill the buffer with the history. */
+ // Fill the buffer with the history.
init_history();
if (hislen > 0 && histtype != HIST_INVALID) {
i = hisidx[histtype];
@@ -6160,9 +6169,10 @@ static int open_cmdwin(void)
// Trigger CmdwinEnter autocommands.
typestr[0] = (char_u)cmdwin_type;
typestr[1] = NUL;
- apply_autocmds(EVENT_CMDWINENTER, typestr, typestr, FALSE, curbuf);
- if (restart_edit != 0) /* autocmd with ":startinsert" */
+ apply_autocmds(EVENT_CMDWINENTER, typestr, typestr, false, curbuf);
+ if (restart_edit != 0) { // autocmd with ":startinsert"
stuffcharReadbuff(K_NOP);
+ }
i = RedrawingDisabled;
RedrawingDisabled = 0;
@@ -6179,10 +6189,10 @@ static int open_cmdwin(void)
const bool save_KeyTyped = KeyTyped;
- /* Trigger CmdwinLeave autocommands. */
- apply_autocmds(EVENT_CMDWINLEAVE, typestr, typestr, FALSE, curbuf);
+ // Trigger CmdwinLeave autocommands.
+ apply_autocmds(EVENT_CMDWINLEAVE, typestr, typestr, false, curbuf);
- /* Restore KeyTyped in case it is modified by autocommands */
+ // Restore KeyTyped in case it is modified by autocommands
KeyTyped = save_KeyTyped;
// Restore the command line info.
@@ -6241,9 +6251,7 @@ static int open_cmdwin(void)
}
}
- /* Don't execute autocommands while deleting the window. */
- block_autocmds();
- // Avoid command-line window first character being concealed
+ // Avoid command-line window first character being concealed.
curwin->w_p_cole = 0;
wp = curwin;
set_bufref(&bufref, curbuf);
@@ -6256,10 +6264,8 @@ static int open_cmdwin(void)
close_buffer(NULL, bufref.br_buf, DOBUF_WIPE, false);
}
- /* Restore window sizes. */
+ // Restore window sizes.
win_size_restore(&winsizes);
-
- unblock_autocmds();
}
ga_clear(&winsizes);
@@ -6308,7 +6314,7 @@ char *script_get(exarg_T *const eap, size_t *const lenp)
for (;;) {
char *const theline = (char *)eap->getline(
eap->cstack->cs_looplevel > 0 ? -1 :
- NUL, eap->cookie, 0);
+ NUL, eap->cookie, 0, true);
if (theline == NULL || strcmp(end_pattern, theline) == 0) {
xfree(theline);
diff --git a/src/nvim/ex_getln.h b/src/nvim/ex_getln.h
index 051564fbe1..99d5a7786d 100644
--- a/src/nvim/ex_getln.h
+++ b/src/nvim/ex_getln.h
@@ -27,6 +27,8 @@
#define WILD_ESCAPE 0x80
#define WILD_ICASE 0x100
#define WILD_ALLLINKS 0x200
+#define WILD_IGNORE_COMPLETESLASH 0x400
+#define WILD_NOERROR 0x800 // sets EW_NOERROR
/// Present history tables
typedef enum {
diff --git a/src/nvim/file_search.c b/src/nvim/file_search.c
index ad6a481bc5..47272df2f0 100644
--- a/src/nvim/file_search.c
+++ b/src/nvim/file_search.c
@@ -1102,8 +1102,8 @@ static bool ff_wc_equal(char_u *s1, char_u *s2)
prev2 = prev1;
prev1 = c1;
- i += MB_PTR2LEN(s1 + i);
- j += MB_PTR2LEN(s2 + j);
+ i += utfc_ptr2len(s1 + i);
+ j += utfc_ptr2len(s2 + j);
}
return s1[i] == s2[j];
}
diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c
index 4cf42b41e9..f518e59acc 100644
--- a/src/nvim/fileio.c
+++ b/src/nvim/fileio.c
@@ -2650,6 +2650,7 @@ buf_write(
*/
if (!(append && *p_pm == NUL) && !filtering && perm >= 0 && dobackup) {
FileInfo file_info;
+ const bool no_prepend_dot = false;
if ((bkc & BKC_YES) || append) { /* "yes" */
backup_copy = TRUE;
@@ -2737,6 +2738,7 @@ buf_write(
int some_error = false;
char_u *dirp;
char_u *rootname;
+ char_u *p;
/*
* Try to make the backup in each directory in the 'bdir' option.
@@ -2756,6 +2758,17 @@ buf_write(
* Isolate one directory name, using an entry in 'bdir'.
*/
(void)copy_option_part(&dirp, IObuff, IOSIZE, ",");
+ p = IObuff + STRLEN(IObuff);
+ if (after_pathsep((char *)IObuff, (char *)p) && p[-1] == p[-2]) {
+ // Ends with '//', Use Full path
+ if ((p = (char_u *)make_percent_swname((char *)IObuff, (char *)fname))
+ != NULL) {
+ backup = (char_u *)modname((char *)p, (char *)backup_ext,
+ no_prepend_dot);
+ xfree(p);
+ }
+ }
+
rootname = get_file_in_dir(fname, IObuff);
if (rootname == NULL) {
some_error = TRUE; /* out of memory */
@@ -2764,10 +2777,14 @@ buf_write(
FileInfo file_info_new;
{
- /*
- * Make backup file name.
- */
- backup = (char_u *)modname((char *)rootname, (char *)backup_ext, FALSE);
+ //
+ // Make the backup file name.
+ //
+ if (backup == NULL) {
+ backup = (char_u *)modname((char *)rootname, (char *)backup_ext,
+ no_prepend_dot);
+ }
+
if (backup == NULL) {
xfree(rootname);
some_error = TRUE; /* out of memory */
@@ -2893,12 +2910,26 @@ nobackup:
* Isolate one directory name and make the backup file name.
*/
(void)copy_option_part(&dirp, IObuff, IOSIZE, ",");
- rootname = get_file_in_dir(fname, IObuff);
- if (rootname == NULL)
- backup = NULL;
- else {
- backup = (char_u *)modname((char *)rootname, (char *)backup_ext, FALSE);
- xfree(rootname);
+ p = IObuff + STRLEN(IObuff);
+ if (after_pathsep((char *)IObuff, (char *)p) && p[-1] == p[-2]) {
+ // path ends with '//', use full path
+ if ((p = (char_u *)make_percent_swname((char *)IObuff, (char *)fname))
+ != NULL) {
+ backup = (char_u *)modname((char *)p, (char *)backup_ext,
+ no_prepend_dot);
+ xfree(p);
+ }
+ }
+
+ if (backup == NULL) {
+ rootname = get_file_in_dir(fname, IObuff);
+ if (rootname == NULL) {
+ backup = NULL;
+ } else {
+ backup = (char_u *)modname((char *)rootname, (char *)backup_ext,
+ no_prepend_dot);
+ xfree(rootname);
+ }
}
if (backup != NULL) {
@@ -3700,8 +3731,9 @@ static int set_rw_fname(char_u *fname, char_u *sfname)
return FAIL;
}
- if (setfname(curbuf, fname, sfname, FALSE) == OK)
+ if (setfname(curbuf, fname, sfname, false) == OK) {
curbuf->b_flags |= BF_NOTEDITED;
+ }
/* ....and a new named one is created */
apply_autocmds(EVENT_BUFNEW, NULL, NULL, FALSE, curbuf);
@@ -4834,7 +4866,7 @@ buf_check_timestamp(
if (buf->terminal
|| buf->b_ffname == NULL
|| buf->b_ml.ml_mfp == NULL
- || *buf->b_p_bt != NUL
+ || !bt_normal(buf)
|| buf->b_saving
|| busy
)
@@ -7100,12 +7132,10 @@ auto_next_pat(
}
}
-/*
- * Get next autocommand command.
- * Called by do_cmdline() to get the next line for ":if".
- * Returns allocated string, or NULL for end of autocommands.
- */
-char_u *getnextac(int c, void *cookie, int indent)
+/// Get next autocommand command.
+/// Called by do_cmdline() to get the next line for ":if".
+/// @return allocated string, or NULL for end of autocommands.
+char_u *getnextac(int c, void *cookie, int indent, bool do_concat)
{
AutoPatCmd *acp = (AutoPatCmd *)cookie;
char_u *retval;
diff --git a/src/nvim/fold.c b/src/nvim/fold.c
index 5ce953e626..b193b4005c 100644
--- a/src/nvim/fold.c
+++ b/src/nvim/fold.c
@@ -771,6 +771,11 @@ void foldUpdate(win_T *wp, linenr_T top, linenr_T bot)
return;
}
+ if (need_diff_redraw) {
+ // will update later
+ return;
+ }
+
// Mark all folds from top to bot as maybe-small.
fold_T *fp;
(void)foldFind(&wp->w_folds, top, &fp);
diff --git a/src/nvim/generators/gen_char_blob.lua b/src/nvim/generators/gen_char_blob.lua
index 1702add2e4..a7dad50d48 100644
--- a/src/nvim/generators/gen_char_blob.lua
+++ b/src/nvim/generators/gen_char_blob.lua
@@ -1,49 +1,59 @@
if arg[1] == '--help' then
print('Usage:')
- print(' gencharblob.lua source target varname')
+ print(' '..arg[0]..' target source varname [source varname]...')
print('')
print('Generates C file with big uint8_t blob.')
print('Blob will be stored in a static const array named varname.')
os.exit()
end
-assert(#arg == 3)
+assert(#arg >= 3 and (#arg - 1) % 2 == 0)
-local source_file = arg[1]
-local target_file = arg[2]
-local varname = arg[3]
-
-local source = io.open(source_file, 'r')
+local target_file = arg[1] or error('Need a target file')
local target = io.open(target_file, 'w')
target:write('#include <stdint.h>\n\n')
-target:write(('static const uint8_t %s[] = {\n'):format(varname))
-
-local num_bytes = 0
-local MAX_NUM_BYTES = 15 -- 78 / 5: maximum number of bytes on one line
-target:write(' ')
-
-local increase_num_bytes
-increase_num_bytes = function()
- num_bytes = num_bytes + 1
- if num_bytes == MAX_NUM_BYTES then
- num_bytes = 0
- target:write('\n ')
+
+local varnames = {}
+for argi = 2, #arg, 2 do
+ local source_file = arg[argi]
+ local varname = arg[argi + 1]
+ if varnames[varname] then
+ error(string.format("varname %q is already specified for file %q", varname, varnames[varname]))
end
-end
+ varnames[varname] = source_file
+
+ local source = io.open(source_file, 'r')
+ or error(string.format("source_file %q doesn't exist", source_file))
+
+ target:write(('static const uint8_t %s[] = {\n'):format(varname))
-for line in source:lines() do
- for i = 1,string.len(line) do
- local byte = string.byte(line, i)
- assert(byte ~= 0)
- target:write(string.format(' %3u,', byte))
+ local num_bytes = 0
+ local MAX_NUM_BYTES = 15 -- 78 / 5: maximum number of bytes on one line
+ target:write(' ')
+
+ local increase_num_bytes
+ increase_num_bytes = function()
+ num_bytes = num_bytes + 1
+ if num_bytes == MAX_NUM_BYTES then
+ num_bytes = 0
+ target:write('\n ')
+ end
+ end
+
+ for line in source:lines() do
+ for i = 1, string.len(line) do
+ local byte = line:byte(i)
+ assert(byte ~= 0)
+ target:write(string.format(' %3u,', byte))
+ increase_num_bytes()
+ end
+ target:write(string.format(' %3u,', string.byte('\n', 1)))
increase_num_bytes()
end
- target:write(string.format(' %3u,', string.byte('\n', 1)))
- increase_num_bytes()
-end
-target:write(' 0};\n')
+ target:write(' 0};\n')
+ source:close()
+end
-source:close()
target:close()
diff --git a/src/nvim/generators/gen_declarations.lua b/src/nvim/generators/gen_declarations.lua
index ad44613f42..0782c8115d 100755
--- a/src/nvim/generators/gen_declarations.lua
+++ b/src/nvim/generators/gen_declarations.lua
@@ -207,9 +207,7 @@ preproc_f:close()
local header = [[
-#ifndef DEFINE_FUNC_ATTRIBUTES
-# define DEFINE_FUNC_ATTRIBUTES
-#endif
+#define DEFINE_FUNC_ATTRIBUTES
#include "nvim/func_attr.h"
#undef DEFINE_FUNC_ATTRIBUTES
]]
diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c
index 2469bb5baa..c038977127 100644
--- a/src/nvim/getchar.c
+++ b/src/nvim/getchar.c
@@ -1165,12 +1165,12 @@ void free_typebuf(void)
if (typebuf.tb_buf == typebuf_init) {
internal_error("Free typebuf 1");
} else {
- xfree(typebuf.tb_buf);
+ XFREE_CLEAR(typebuf.tb_buf);
}
if (typebuf.tb_noremap == noremapbuf_init) {
internal_error("Free typebuf 2");
} else {
- xfree(typebuf.tb_noremap);
+ XFREE_CLEAR(typebuf.tb_noremap);
}
}
@@ -1833,13 +1833,13 @@ static int vgetorpeek(int advance)
char_u *p1 = mp->m_keys;
char_u *p2 = (char_u *)mb_unescape((const char **)&p1);
- if (has_mbyte && p2 != NULL && MB_BYTE2LEN(c1) > MB_PTR2LEN(p2))
+ if (p2 != NULL && MB_BYTE2LEN(c1) > utfc_ptr2len(p2)) {
mlen = 0;
- /*
- * Check an entry whether it matches.
- * - Full match: mlen == keylen
- * - Partly match: mlen == typebuf.tb_len
- */
+ }
+
+ // Check an entry whether it matches.
+ // - Full match: mlen == keylen
+ // - Partly match: mlen == typebuf.tb_len
keylen = mp->m_keylen;
if (mlen == keylen
|| (mlen == typebuf.tb_len
@@ -2409,7 +2409,6 @@ int inchar(
did_outofmem_msg = FALSE; /* display out of memory message (again) */
did_swapwrite_msg = FALSE; /* display swap file write error again */
}
- undo_off = FALSE; /* restart undo now */
// Get a character from a script file if there is one.
// If interrupted: Stop reading script files, close them all.
@@ -4419,7 +4418,7 @@ mapblock_T *get_maphash(int index, buf_T *buf)
}
/// Get command argument for <Cmd> key
-char_u * getcmdkeycmd(int promptc, void *cookie, int indent)
+char_u * getcmdkeycmd(int promptc, void *cookie, int indent, bool do_concat)
{
garray_T line_ga;
int c1 = -1, c2;
diff --git a/src/nvim/globals.h b/src/nvim/globals.h
index 5237c621f9..172c190df2 100644
--- a/src/nvim/globals.h
+++ b/src/nvim/globals.h
@@ -331,6 +331,7 @@ EXTERN int garbage_collect_at_exit INIT(= false);
#define SID_NONE -6 // don't set scriptID
#define SID_LUA -7 // for Lua scripts/chunks
#define SID_API_CLIENT -8 // for API clients
+#define SID_STR -9 // for sourcing a string
// Script CTX being sourced or was sourced to define the current function.
EXTERN sctx_T current_sctx INIT(= { 0 COMMA 0 COMMA 0 });
@@ -400,11 +401,6 @@ EXTERN bool mouse_past_eol INIT(= false); /* mouse right of line */
EXTERN int mouse_dragging INIT(= 0); /* extending Visual area with
mouse dragging */
-/* Value set from 'diffopt'. */
-EXTERN int diff_context INIT(= 6); /* context for folds */
-EXTERN int diff_foldcolumn INIT(= 2); /* 'foldcolumn' for diff mode */
-EXTERN int diff_need_scrollbind INIT(= FALSE);
-
/* The root of the menu hierarchy. */
EXTERN vimmenu_T *root_menu INIT(= NULL);
/*
@@ -768,7 +764,6 @@ EXTERN int did_outofmem_msg INIT(= false);
// set after out of memory msg
EXTERN int did_swapwrite_msg INIT(= false);
// set after swap write error msg
-EXTERN int undo_off INIT(= false); // undo switched off for now
EXTERN int global_busy INIT(= 0); // set when :global is executing
EXTERN int listcmd_busy INIT(= false); // set when :argdo, :windo or
// :bufdo is executing
@@ -787,7 +782,11 @@ EXTERN int postponed_split_flags INIT(= 0); /* args for win_split() */
EXTERN int postponed_split_tab INIT(= 0); /* cmdmod.tab */
EXTERN int g_do_tagpreview INIT(= 0); /* for tag preview commands:
height of preview window */
-EXTERN int replace_offset INIT(= 0); /* offset for replace_push() */
+EXTERN int g_tag_at_cursor INIT(= false); // whether the tag command comes
+ // from the command line (0) or was
+ // invoked as a normal command (1)
+
+EXTERN int replace_offset INIT(= 0); // offset for replace_push()
EXTERN char_u *escape_chars INIT(= (char_u *)" \t\\\"|");
/* need backslash in cmd line */
diff --git a/src/nvim/hashtab.h b/src/nvim/hashtab.h
index a70a8bea63..19633d455f 100644
--- a/src/nvim/hashtab.h
+++ b/src/nvim/hashtab.h
@@ -81,10 +81,10 @@ typedef struct hashtable_S {
size_t hi##todo_ = hi##ht_->ht_used; \
for (hashitem_T *hi = hi##ht_->ht_array; hi##todo_; hi++) { \
if (!HASHITEM_EMPTY(hi)) { \
+ hi##todo_--; \
{ \
code \
} \
- hi##todo_--; \
} \
} \
} while (0)
diff --git a/src/nvim/highlight.c b/src/nvim/highlight.c
index 093cc4923b..c96f07ed89 100644
--- a/src/nvim/highlight.c
+++ b/src/nvim/highlight.c
@@ -308,23 +308,39 @@ int hl_combine_attr(int char_attr, int prim_attr)
// start with low-priority attribute, and override colors if present below.
HlAttrs new_en = char_aep;
- new_en.cterm_ae_attr |= spell_aep.cterm_ae_attr;
- new_en.rgb_ae_attr |= spell_aep.rgb_ae_attr;
+ if (spell_aep.cterm_ae_attr & HL_NOCOMBINE) {
+ new_en.cterm_ae_attr = spell_aep.cterm_ae_attr;
+ } else {
+ new_en.cterm_ae_attr |= spell_aep.cterm_ae_attr;
+ }
+ if (spell_aep.rgb_ae_attr & HL_NOCOMBINE) {
+ new_en.rgb_ae_attr = spell_aep.rgb_ae_attr;
+ } else {
+ new_en.rgb_ae_attr |= spell_aep.rgb_ae_attr;
+ }
if (spell_aep.cterm_fg_color > 0) {
new_en.cterm_fg_color = spell_aep.cterm_fg_color;
+ new_en.rgb_ae_attr &= ((~HL_FG_INDEXED)
+ | (spell_aep.rgb_ae_attr & HL_FG_INDEXED));
}
if (spell_aep.cterm_bg_color > 0) {
new_en.cterm_bg_color = spell_aep.cterm_bg_color;
+ new_en.rgb_ae_attr &= ((~HL_BG_INDEXED)
+ | (spell_aep.rgb_ae_attr & HL_BG_INDEXED));
}
if (spell_aep.rgb_fg_color >= 0) {
new_en.rgb_fg_color = spell_aep.rgb_fg_color;
+ new_en.rgb_ae_attr &= ((~HL_FG_INDEXED)
+ | (spell_aep.rgb_ae_attr & HL_FG_INDEXED));
}
if (spell_aep.rgb_bg_color >= 0) {
new_en.rgb_bg_color = spell_aep.rgb_bg_color;
+ new_en.rgb_ae_attr &= ((~HL_BG_INDEXED)
+ | (spell_aep.rgb_ae_attr & HL_BG_INDEXED));
}
if (spell_aep.rgb_sp_color >= 0) {
@@ -414,6 +430,7 @@ int hl_blend_attrs(int back_attr, int front_attr, bool *through)
cattrs.cterm_bg_color = fattrs.cterm_bg_color;
cattrs.cterm_fg_color = cterm_blend(ratio, battrs.cterm_fg_color,
fattrs.cterm_bg_color);
+ cattrs.rgb_ae_attr &= ~(HL_FG_INDEXED | HL_BG_INDEXED);
} else {
cattrs = fattrs;
if (ratio >= 50) {
@@ -427,6 +444,8 @@ int hl_blend_attrs(int back_attr, int front_attr, bool *through)
} else {
cattrs.rgb_sp_color = -1;
}
+
+ cattrs.rgb_ae_attr &= ~HL_BG_INDEXED;
}
cattrs.rgb_bg_color = rgb_blend(ratio, battrs.rgb_bg_color,
fattrs.rgb_bg_color);
@@ -603,6 +622,14 @@ Dictionary hlattrs2dict(HlAttrs ae, bool use_rgb)
}
if (use_rgb) {
+ if (mask & HL_FG_INDEXED) {
+ PUT(hl, "fg_indexed", BOOLEAN_OBJ(true));
+ }
+
+ if (mask & HL_BG_INDEXED) {
+ PUT(hl, "bg_indexed", BOOLEAN_OBJ(true));
+ }
+
if (ae.rgb_fg_color != -1) {
PUT(hl, "foreground", INTEGER_OBJ(ae.rgb_fg_color));
}
diff --git a/src/nvim/highlight_defs.h b/src/nvim/highlight_defs.h
index f9c2c03fc6..36f3181674 100644
--- a/src/nvim/highlight_defs.h
+++ b/src/nvim/highlight_defs.h
@@ -18,6 +18,9 @@ typedef enum {
HL_UNDERCURL = 0x10,
HL_STANDOUT = 0x20,
HL_STRIKETHROUGH = 0x40,
+ HL_NOCOMBINE = 0x80,
+ HL_BG_INDEXED = 0x0100,
+ HL_FG_INDEXED = 0x0200,
} HlAttrFlags;
/// Stores a complete highlighting entry, including colors and attributes
diff --git a/src/nvim/lib/kbtree.h b/src/nvim/lib/kbtree.h
index 33aeff1d89..bef37f8ba9 100644
--- a/src/nvim/lib/kbtree.h
+++ b/src/nvim/lib/kbtree.h
@@ -25,6 +25,12 @@
* SUCH DAMAGE.
*/
+// Gotchas
+// -------
+//
+// if you delete from a kbtree while iterating over it you must use
+// kb_del_itr and not kb_del otherwise the iterator might point to freed memory.
+
#ifndef NVIM_LIB_KBTREE_H
#define NVIM_LIB_KBTREE_H
diff --git a/src/nvim/log.c b/src/nvim/log.c
index db36611933..225e40cdb4 100644
--- a/src/nvim/log.c
+++ b/src/nvim/log.c
@@ -93,6 +93,7 @@ static bool log_path_init(void)
void log_init(void)
{
uv_mutex_init(&mutex);
+ log_path_init();
}
void log_lock(void)
diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c
index 9665655e74..09d1a68898 100644
--- a/src/nvim/lua/converter.c
+++ b/src/nvim/lua/converter.c
@@ -377,6 +377,19 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv)
nlua_pop_typval_table_processing_end:
break;
}
+ case LUA_TUSERDATA: {
+ nlua_pushref(lstate, nlua_nil_ref);
+ bool is_nil = lua_rawequal(lstate, -2, -1);
+ lua_pop(lstate, 1);
+ if (is_nil) {
+ cur.tv->v_type = VAR_SPECIAL;
+ cur.tv->vval.v_special = kSpecialVarNull;
+ } else {
+ EMSG(_("E5101: Cannot convert given lua type"));
+ ret = false;
+ }
+ break;
+ }
default: {
EMSG(_("E5101: Cannot convert given lua type"));
ret = false;
@@ -401,10 +414,18 @@ nlua_pop_typval_table_processing_end:
return ret;
}
+static bool typval_conv_special = false;
+
#define TYPVAL_ENCODE_ALLOW_SPECIALS true
#define TYPVAL_ENCODE_CONV_NIL(tv) \
- lua_pushnil(lstate)
+ do { \
+ if (typval_conv_special) { \
+ lua_pushnil(lstate); \
+ } else { \
+ nlua_pushref(lstate, nlua_nil_ref); \
+ } \
+ } while (0)
#define TYPVAL_ENCODE_CONV_BOOL(tv, num) \
lua_pushboolean(lstate, (bool)(num))
@@ -439,7 +460,13 @@ nlua_pop_typval_table_processing_end:
lua_createtable(lstate, 0, 0)
#define TYPVAL_ENCODE_CONV_EMPTY_DICT(tv, dict) \
- nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary)
+ do { \
+ if (typval_conv_special) { \
+ nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary); \
+ } else { \
+ lua_createtable(lstate, 0, 0); \
+ } \
+ } while (0)
#define TYPVAL_ENCODE_CONV_LIST_START(tv, len) \
do { \
@@ -548,9 +575,11 @@ nlua_pop_typval_table_processing_end:
/// @param[in] tv typval_T to convert.
///
/// @return true in case of success, false otherwise.
-bool nlua_push_typval(lua_State *lstate, typval_T *const tv)
+bool nlua_push_typval(lua_State *lstate, typval_T *const tv, bool special)
{
+ typval_conv_special = special;
const int initial_size = lua_gettop(lstate);
+
if (!lua_checkstack(lstate, initial_size + 2)) {
emsgf(_("E1502: Lua failed to grow stack to %i"), initial_size + 4);
return false;
@@ -708,7 +737,11 @@ void nlua_push_Object(lua_State *lstate, const Object obj, bool special)
{
switch (obj.type) {
case kObjectTypeNil: {
- lua_pushnil(lstate);
+ if (special) {
+ lua_pushnil(lstate);
+ } else {
+ nlua_pushref(lstate, nlua_nil_ref);
+ }
break;
}
case kObjectTypeLuaRef: {
@@ -1142,6 +1175,19 @@ Object nlua_pop_Object(lua_State *const lstate, bool ref, Error *const err)
break;
}
+ case LUA_TUSERDATA: {
+ nlua_pushref(lstate, nlua_nil_ref);
+ bool is_nil = lua_rawequal(lstate, -2, -1);
+ lua_pop(lstate, 1);
+ if (is_nil) {
+ *cur.obj = NIL;
+ } else {
+ api_set_error(err, kErrorTypeValidation,
+ "Cannot convert userdata");
+ }
+ break;
+ }
+
default: {
type_error:
api_set_error(err, kErrorTypeValidation,
@@ -1179,7 +1225,7 @@ GENERATE_INDEX_FUNCTION(Tabpage)
#undef GENERATE_INDEX_FUNCTION
-/// Record some auxilary values in vim module
+/// Record some auxiliary values in vim module
///
/// Assumes that module table is on top of the stack.
///
diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c
index f51aa3c6d4..25f4be1c4d 100644
--- a/src/nvim/lua/executor.c
+++ b/src/nvim/lua/executor.c
@@ -12,6 +12,7 @@
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/vim.h"
+#include "nvim/msgpack_rpc/channel.h"
#include "nvim/vim.h"
#include "nvim/ex_getln.h"
#include "nvim/ex_cmds2.h"
@@ -31,6 +32,7 @@
#include "nvim/lua/executor.h"
#include "nvim/lua/converter.h"
+#include "nvim/lua/treesitter.h"
#include "luv/luv.h"
@@ -46,9 +48,6 @@ typedef struct {
# include "lua/executor.c.generated.h"
#endif
-/// Name of the run code for use in messages
-#define NLUA_EVAL_NAME "<VimL compiled string>"
-
/// Convert lua error into a Vim error message
///
/// @param lstate Lua interpreter state.
@@ -269,12 +268,7 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
#endif
// vim
- const char *code = (char *)&vim_module[0];
- if (luaL_loadbuffer(lstate, code, strlen(code), "@vim.lua")
- || lua_pcall(lstate, 0, LUA_MULTRET, 0)) {
- nlua_error(lstate, _("E5106: Error while creating vim module: %.*s"));
- return 1;
- }
+ lua_newtable(lstate);
// vim.api
nlua_add_api_functions(lstate);
// vim.types, vim.type_idx, vim.val_idx
@@ -294,6 +288,17 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
// in_fast_event
lua_pushcfunction(lstate, &nlua_in_fast_event);
lua_setfield(lstate, -2, "in_fast_event");
+ // call
+ lua_pushcfunction(lstate, &nlua_call);
+ lua_setfield(lstate, -2, "call");
+
+ // rpcrequest
+ lua_pushcfunction(lstate, &nlua_rpcrequest);
+ lua_setfield(lstate, -2, "rpcrequest");
+
+ // rpcnotify
+ lua_pushcfunction(lstate, &nlua_rpcnotify);
+ lua_setfield(lstate, -2, "rpcnotify");
// vim.loop
luv_set_loop(lstate, &main_loop.uv);
@@ -310,7 +315,38 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
lua_setfield(lstate, -2, "luv");
lua_pop(lstate, 3);
+ // vim.NIL
+ lua_newuserdata(lstate, 0);
+ lua_createtable(lstate, 0, 0);
+ lua_pushcfunction(lstate, &nlua_nil_tostring);
+ lua_setfield(lstate, -2, "__tostring");
+ lua_setmetatable(lstate, -2);
+ nlua_nil_ref = nlua_ref(lstate, -1);
+ lua_setfield(lstate, -2, "NIL");
+
+ // internal vim._treesitter... API
+ nlua_add_treesitter(lstate);
+
lua_setglobal(lstate, "vim");
+
+ {
+ const char *code = (char *)&shared_module[0];
+ if (luaL_loadbuffer(lstate, code, strlen(code), "@shared.lua")
+ || lua_pcall(lstate, 0, 0, 0)) {
+ nlua_error(lstate, _("E5106: Error while creating shared module: %.*s"));
+ return 1;
+ }
+ }
+
+ {
+ const char *code = (char *)&vim_module[0];
+ if (luaL_loadbuffer(lstate, code, strlen(code), "@vim.lua")
+ || lua_pcall(lstate, 0, 0, 0)) {
+ nlua_error(lstate, _("E5106: Error while creating vim module: %.*s"));
+ return 1;
+ }
+ }
+
return 0;
}
@@ -371,29 +407,6 @@ static lua_State *nlua_enter(void)
return lstate;
}
-/// Execute lua string
-///
-/// @param[in] str String to execute.
-/// @param[out] ret_tv Location where result will be saved.
-///
-/// @return Result of the execution.
-void executor_exec_lua(const String str, typval_T *const ret_tv)
- FUNC_ATTR_NONNULL_ALL
-{
- lua_State *const lstate = nlua_enter();
-
- if (luaL_loadbuffer(lstate, str.data, str.size, NLUA_EVAL_NAME)) {
- nlua_error(lstate, _("E5104: Error while creating lua chunk: %.*s"));
- return;
- }
- if (lua_pcall(lstate, 0, 1, 0)) {
- nlua_error(lstate, _("E5105: Error while calling lua chunk: %.*s"));
- return;
- }
-
- nlua_pop_typval(lstate, ret_tv);
-}
-
static void nlua_print_event(void **argv)
{
char *str = argv[0];
@@ -534,6 +547,125 @@ int nlua_in_fast_event(lua_State *lstate)
return 1;
}
+int nlua_call(lua_State *lstate)
+{
+ Error err = ERROR_INIT;
+ size_t name_len;
+ const char_u *name = (const char_u *)luaL_checklstring(lstate, 1, &name_len);
+ if (!nlua_is_deferred_safe(lstate)) {
+ return luaL_error(lstate, e_luv_api_disabled, "vimL function");
+ }
+
+ int nargs = lua_gettop(lstate)-1;
+ if (nargs > MAX_FUNC_ARGS) {
+ return luaL_error(lstate, "Function called with too many arguments");
+ }
+
+ typval_T vim_args[MAX_FUNC_ARGS + 1];
+ int i = 0; // also used for freeing the variables
+ for (; i < nargs; i++) {
+ lua_pushvalue(lstate, (int)i+2);
+ if (!nlua_pop_typval(lstate, &vim_args[i])) {
+ api_set_error(&err, kErrorTypeException,
+ "error converting argument %d", i+1);
+ goto free_vim_args;
+ }
+ }
+
+ TRY_WRAP({
+ // TODO(bfredl): this should be simplified in error handling refactor
+ force_abort = false;
+ suppress_errthrow = false;
+ current_exception = NULL;
+ did_emsg = false;
+
+ try_start();
+ typval_T rettv;
+ int dummy;
+ // call_func() retval is deceptive, ignore it. Instead we set `msg_list`
+ // (TRY_WRAP) to capture abort-causing non-exception errors.
+ (void)call_func(name, (int)name_len, &rettv, nargs,
+ vim_args, NULL, curwin->w_cursor.lnum, curwin->w_cursor.lnum,
+ &dummy, true, NULL, NULL);
+ if (!try_end(&err)) {
+ nlua_push_typval(lstate, &rettv, false);
+ }
+ tv_clear(&rettv);
+ });
+
+free_vim_args:
+ while (i > 0) {
+ tv_clear(&vim_args[--i]);
+ }
+ if (ERROR_SET(&err)) {
+ lua_pushstring(lstate, err.msg);
+ api_clear_error(&err);
+ return lua_error(lstate);
+ }
+ return 1;
+}
+
+static int nlua_rpcrequest(lua_State *lstate)
+{
+ if (!nlua_is_deferred_safe(lstate)) {
+ return luaL_error(lstate, e_luv_api_disabled, "rpcrequest");
+ }
+ return nlua_rpc(lstate, true);
+}
+
+static int nlua_rpcnotify(lua_State *lstate)
+{
+ return nlua_rpc(lstate, false);
+}
+
+static int nlua_rpc(lua_State *lstate, bool request)
+{
+ size_t name_len;
+ uint64_t chan_id = (uint64_t)luaL_checkinteger(lstate, 1);
+ const char *name = luaL_checklstring(lstate, 2, &name_len);
+ int nargs = lua_gettop(lstate)-2;
+ Error err = ERROR_INIT;
+ Array args = ARRAY_DICT_INIT;
+
+ for (int i = 0; i < nargs; i++) {
+ lua_pushvalue(lstate, (int)i+3);
+ ADD(args, nlua_pop_Object(lstate, false, &err));
+ if (ERROR_SET(&err)) {
+ api_free_array(args);
+ goto check_err;
+ }
+ }
+
+ if (request) {
+ Object result = rpc_send_call(chan_id, name, args, &err);
+ if (!ERROR_SET(&err)) {
+ nlua_push_Object(lstate, result, false);
+ api_free_object(result);
+ }
+ } else {
+ if (!rpc_send_event(chan_id, name, args)) {
+ api_set_error(&err, kErrorTypeValidation,
+ "Invalid channel: %"PRIu64, chan_id);
+ }
+ }
+
+check_err:
+ if (ERROR_SET(&err)) {
+ lua_pushstring(lstate, err.msg);
+ api_clear_error(&err);
+ return lua_error(lstate);
+ }
+
+ return request ? 1 : 0;
+}
+
+static int nlua_nil_tostring(lua_State *lstate)
+{
+ lua_pushstring(lstate, "vim.NIL");
+ return 1;
+}
+
+
#ifdef WIN32
/// os.getenv: override os.getenv to maintain coherency. #9681
///
@@ -587,10 +719,6 @@ void executor_eval_lua(const String str, typval_T *const arg,
typval_T *const ret_tv)
FUNC_ATTR_NONNULL_ALL
{
- lua_State *const lstate = nlua_enter();
-
- garray_T str_ga;
- ga_init(&str_ga, 1, 80);
#define EVALHEADER "local _A=select(1,...) return ("
const size_t lcmd_len = sizeof(EVALHEADER) - 1 + str.size + 1;
char *lcmd;
@@ -603,35 +731,76 @@ void executor_eval_lua(const String str, typval_T *const arg,
memcpy(lcmd + sizeof(EVALHEADER) - 1, str.data, str.size);
lcmd[lcmd_len - 1] = ')';
#undef EVALHEADER
- if (luaL_loadbuffer(lstate, lcmd, lcmd_len, NLUA_EVAL_NAME)) {
- nlua_error(lstate,
- _("E5107: Error while creating lua chunk for luaeval(): %.*s"));
- if (lcmd != (char *)IObuff) {
- xfree(lcmd);
- }
- return;
- }
+ typval_exec_lua(lcmd, lcmd_len, "luaeval()", arg, 1, true, ret_tv);
+
if (lcmd != (char *)IObuff) {
xfree(lcmd);
}
+}
- if (arg->v_type == VAR_UNKNOWN) {
- lua_pushnil(lstate);
+void executor_call_lua(const char *str, size_t len, typval_T *const args,
+ int argcount, typval_T *ret_tv)
+ FUNC_ATTR_NONNULL_ALL
+{
+#define CALLHEADER "return "
+#define CALLSUFFIX "(...)"
+ const size_t lcmd_len = sizeof(CALLHEADER) - 1 + len + sizeof(CALLSUFFIX) - 1;
+ char *lcmd;
+ if (lcmd_len < IOSIZE) {
+ lcmd = (char *)IObuff;
} else {
- nlua_push_typval(lstate, arg);
+ lcmd = xmalloc(lcmd_len);
+ }
+ memcpy(lcmd, CALLHEADER, sizeof(CALLHEADER) - 1);
+ memcpy(lcmd + sizeof(CALLHEADER) - 1, str, len);
+ memcpy(lcmd + sizeof(CALLHEADER) - 1 + len, CALLSUFFIX,
+ sizeof(CALLSUFFIX) - 1);
+#undef CALLHEADER
+#undef CALLSUFFIX
+
+ typval_exec_lua(lcmd, lcmd_len, "v:lua", args, argcount, false, ret_tv);
+
+ if (lcmd != (char *)IObuff) {
+ xfree(lcmd);
}
- if (lua_pcall(lstate, 1, 1, 0)) {
- nlua_error(lstate,
- _("E5108: Error while calling lua chunk for luaeval(): %.*s"));
+}
+
+static void typval_exec_lua(const char *lcmd, size_t lcmd_len, const char *name,
+ typval_T *const args, int argcount, bool special,
+ typval_T *ret_tv)
+{
+ if (check_restricted() || check_secure()) {
+ ret_tv->v_type = VAR_NUMBER;
+ ret_tv->vval.v_number = 0;
return;
}
- nlua_pop_typval(lstate, ret_tv);
+ lua_State *const lstate = nlua_enter();
+ if (luaL_loadbuffer(lstate, lcmd, lcmd_len, name)) {
+ nlua_error(lstate, _("E5107: Error loading lua %.*s"));
+ return;
+ }
+
+ for (int i = 0; i < argcount; i++) {
+ if (args[i].v_type == VAR_UNKNOWN) {
+ lua_pushnil(lstate);
+ } else {
+ nlua_push_typval(lstate, &args[i], special);
+ }
+ }
+ if (lua_pcall(lstate, argcount, ret_tv ? 1 : 0, 0)) {
+ nlua_error(lstate, _("E5108: Error executing lua %.*s"));
+ return;
+ }
+
+ if (ret_tv) {
+ nlua_pop_typval(lstate, ret_tv);
+ }
}
-/// Execute lua string
+/// Execute Lua string
///
-/// Used for nvim_execute_lua().
+/// Used for nvim_exec_lua().
///
/// @param[in] str String to execute.
/// @param[in] args array of ... args
@@ -712,9 +881,8 @@ void ex_lua(exarg_T *const eap)
xfree(code);
return;
}
- typval_T tv = { .v_type = VAR_UNKNOWN };
- executor_exec_lua((String) { .data = code, .size = len }, &tv);
- tv_clear(&tv);
+ typval_exec_lua(code, len, ":lua", NULL, 0, false, NULL);
+
xfree(code);
}
@@ -752,8 +920,8 @@ void ex_luado(exarg_T *const eap)
#undef DOSTART
#undef DOEND
- if (luaL_loadbuffer(lstate, lcmd, lcmd_len, NLUA_EVAL_NAME)) {
- nlua_error(lstate, _("E5109: Error while creating lua chunk: %.*s"));
+ if (luaL_loadbuffer(lstate, lcmd, lcmd_len, ":luado")) {
+ nlua_error(lstate, _("E5109: Error loading lua: %.*s"));
if (lcmd_len >= IOSIZE) {
xfree(lcmd);
}
@@ -763,7 +931,7 @@ void ex_luado(exarg_T *const eap)
xfree(lcmd);
}
if (lua_pcall(lstate, 0, 1, 0)) {
- nlua_error(lstate, _("E5110: Error while creating lua function: %.*s"));
+ nlua_error(lstate, _("E5110: Error executing lua: %.*s"));
return;
}
for (linenr_T l = eap->line1; l <= eap->line2; l++) {
@@ -774,7 +942,7 @@ void ex_luado(exarg_T *const eap)
lua_pushstring(lstate, (const char *)ml_get_buf(curbuf, l, false));
lua_pushnumber(lstate, (lua_Number)l);
if (lua_pcall(lstate, 2, 1, 0)) {
- nlua_error(lstate, _("E5111: Error while calling lua function: %.*s"));
+ nlua_error(lstate, _("E5111: Error calling lua: %.*s"));
break;
}
if (lua_isstring(lstate, -1)) {
@@ -816,3 +984,27 @@ void ex_luafile(exarg_T *const eap)
return;
}
}
+
+static int create_tslua_parser(lua_State *L)
+{
+ if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) {
+ return luaL_error(L, "string expected");
+ }
+
+ const char *lang_name = lua_tostring(L, 1);
+ return tslua_push_parser(L, lang_name);
+}
+
+static void nlua_add_treesitter(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
+{
+ tslua_init(lstate);
+
+ lua_pushcfunction(lstate, create_tslua_parser);
+ lua_setfield(lstate, -2, "_create_ts_parser");
+
+ lua_pushcfunction(lstate, tslua_register_lang);
+ lua_setfield(lstate, -2, "_ts_add_language");
+
+ lua_pushcfunction(lstate, tslua_inspect_lang);
+ lua_setfield(lstate, -2, "_ts_inspect_language");
+}
diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h
index 8d356a5600..32f66b629c 100644
--- a/src/nvim/lua/executor.h
+++ b/src/nvim/lua/executor.h
@@ -12,6 +12,8 @@
// Generated by msgpack-gen.lua
void nlua_add_api_functions(lua_State *lstate) REAL_FATTR_NONNULL_ALL;
+EXTERN LuaRef nlua_nil_ref INIT(= LUA_NOREF);
+
#define set_api_error(s, err) \
do { \
Error *err_ = (err); \
diff --git a/src/nvim/lua/treesitter.c b/src/nvim/lua/treesitter.c
new file mode 100644
index 0000000000..d2072402bb
--- /dev/null
+++ b/src/nvim/lua/treesitter.c
@@ -0,0 +1,652 @@
+// 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
+
+// lua bindings for tree-sitter.
+// NB: this file mostly contains a generic lua interface for tree-sitter
+// trees and nodes, and could be broken out as a reusable lua package
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+#include <assert.h>
+
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#include "tree_sitter/api.h"
+
+#include "nvim/lua/treesitter.h"
+#include "nvim/api/private/handle.h"
+#include "nvim/memline.h"
+
+typedef struct {
+ TSParser *parser;
+ TSTree *tree; // internal tree, used for editing/reparsing
+} TSLua_parser;
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "lua/treesitter.c.generated.h"
+#endif
+
+static struct luaL_Reg parser_meta[] = {
+ { "__gc", parser_gc },
+ { "__tostring", parser_tostring },
+ { "parse_buf", parser_parse_buf },
+ { "edit", parser_edit },
+ { "tree", parser_tree },
+ { NULL, NULL }
+};
+
+static struct luaL_Reg tree_meta[] = {
+ { "__gc", tree_gc },
+ { "__tostring", tree_tostring },
+ { "root", tree_root },
+ { NULL, NULL }
+};
+
+static struct luaL_Reg node_meta[] = {
+ { "__tostring", node_tostring },
+ { "__eq", node_eq },
+ { "__len", node_child_count },
+ { "range", node_range },
+ { "start", node_start },
+ { "end_", node_end },
+ { "type", node_type },
+ { "symbol", node_symbol },
+ { "named", node_named },
+ { "missing", node_missing },
+ { "has_error", node_has_error },
+ { "sexpr", node_sexpr },
+ { "child_count", node_child_count },
+ { "named_child_count", node_named_child_count },
+ { "child", node_child },
+ { "named_child", node_named_child },
+ { "descendant_for_range", node_descendant_for_range },
+ { "named_descendant_for_range", node_named_descendant_for_range },
+ { "parent", node_parent },
+ { NULL, NULL }
+};
+
+static PMap(cstr_t) *langs;
+
+static void build_meta(lua_State *L, const char *tname, const luaL_Reg *meta)
+{
+ if (luaL_newmetatable(L, tname)) { // [meta]
+ for (size_t i = 0; meta[i].name != NULL; i++) {
+ lua_pushcfunction(L, meta[i].func); // [meta, func]
+ lua_setfield(L, -2, meta[i].name); // [meta]
+ }
+
+ lua_pushvalue(L, -1); // [meta, meta]
+ lua_setfield(L, -2, "__index"); // [meta]
+ }
+ lua_pop(L, 1); // [] (don't use it now)
+}
+
+/// init the tslua library
+///
+/// all global state is stored in the regirstry of the lua_State
+void tslua_init(lua_State *L)
+{
+ langs = pmap_new(cstr_t)();
+
+ // type metatables
+ build_meta(L, "treesitter_parser", parser_meta);
+ build_meta(L, "treesitter_tree", tree_meta);
+ build_meta(L, "treesitter_node", node_meta);
+}
+
+int tslua_register_lang(lua_State *L)
+{
+ if (lua_gettop(L) < 2 || !lua_isstring(L, 1) || !lua_isstring(L, 2)) {
+ return luaL_error(L, "string expected");
+ }
+
+ const char *path = lua_tostring(L, 1);
+ const char *lang_name = lua_tostring(L, 2);
+
+ if (pmap_has(cstr_t)(langs, lang_name)) {
+ return 0;
+ }
+
+#define BUFSIZE 128
+ char symbol_buf[BUFSIZE];
+ snprintf(symbol_buf, BUFSIZE, "tree_sitter_%s", lang_name);
+#undef BUFSIZE
+
+ uv_lib_t lib;
+ if (uv_dlopen(path, &lib)) {
+ snprintf((char *)IObuff, IOSIZE, "Failed to load parser: uv_dlopen: %s",
+ uv_dlerror(&lib));
+ uv_dlclose(&lib);
+ lua_pushstring(L, (char *)IObuff);
+ return lua_error(L);
+ }
+
+ TSLanguage *(*lang_parser)(void);
+ if (uv_dlsym(&lib, symbol_buf, (void **)&lang_parser)) {
+ snprintf((char *)IObuff, IOSIZE, "Failed to load parser: uv_dlsym: %s",
+ uv_dlerror(&lib));
+ uv_dlclose(&lib);
+ lua_pushstring(L, (char *)IObuff);
+ return lua_error(L);
+ }
+
+ TSLanguage *lang = lang_parser();
+ if (lang == NULL) {
+ return luaL_error(L, "Failed to load parser: internal error");
+ }
+
+ pmap_put(cstr_t)(langs, xstrdup(lang_name), lang);
+
+ lua_pushboolean(L, true);
+ return 1;
+}
+
+int tslua_inspect_lang(lua_State *L)
+{
+ if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) {
+ return luaL_error(L, "string expected");
+ }
+ const char *lang_name = lua_tostring(L, 1);
+
+ TSLanguage *lang = pmap_get(cstr_t)(langs, lang_name);
+ if (!lang) {
+ return luaL_error(L, "no such language: %s", lang_name);
+ }
+
+ lua_createtable(L, 0, 2); // [retval]
+
+ size_t nsymbols = (size_t)ts_language_symbol_count(lang);
+
+ lua_createtable(L, nsymbols-1, 1); // [retval, symbols]
+ for (size_t i = 0; i < nsymbols; i++) {
+ TSSymbolType t = ts_language_symbol_type(lang, i);
+ if (t == TSSymbolTypeAuxiliary) {
+ // not used by the API
+ continue;
+ }
+ lua_createtable(L, 2, 0); // [retval, symbols, elem]
+ lua_pushstring(L, ts_language_symbol_name(lang, i));
+ lua_rawseti(L, -2, 1);
+ lua_pushboolean(L, t == TSSymbolTypeRegular);
+ lua_rawseti(L, -2, 2); // [retval, symbols, elem]
+ lua_rawseti(L, -2, i); // [retval, symbols]
+ }
+
+ lua_setfield(L, -2, "symbols"); // [retval]
+
+ size_t nfields = (size_t)ts_language_field_count(lang);
+ lua_createtable(L, nfields-1, 1); // [retval, fields]
+ for (size_t i = 0; i < nfields; i++) {
+ lua_pushstring(L, ts_language_field_name_for_id(lang, i));
+ lua_rawseti(L, -2, i); // [retval, fields]
+ }
+
+ lua_setfield(L, -2, "fields"); // [retval]
+ return 1;
+}
+
+int tslua_push_parser(lua_State *L, const char *lang_name)
+{
+ TSLanguage *lang = pmap_get(cstr_t)(langs, lang_name);
+ if (!lang) {
+ return luaL_error(L, "no such language: %s", lang_name);
+ }
+
+ TSParser *parser = ts_parser_new();
+ ts_parser_set_language(parser, lang);
+ TSLua_parser *p = lua_newuserdata(L, sizeof(TSLua_parser)); // [udata]
+ p->parser = parser;
+ p->tree = NULL;
+
+ lua_getfield(L, LUA_REGISTRYINDEX, "treesitter_parser"); // [udata, meta]
+ lua_setmetatable(L, -2); // [udata]
+ return 1;
+}
+
+static TSLua_parser *parser_check(lua_State *L)
+{
+ return luaL_checkudata(L, 1, "treesitter_parser");
+}
+
+static int parser_gc(lua_State *L)
+{
+ TSLua_parser *p = parser_check(L);
+ if (!p) {
+ return 0;
+ }
+
+ ts_parser_delete(p->parser);
+ if (p->tree) {
+ ts_tree_delete(p->tree);
+ }
+
+ return 0;
+}
+
+static int parser_tostring(lua_State *L)
+{
+ lua_pushstring(L, "<parser>");
+ return 1;
+}
+
+static const char *input_cb(void *payload, uint32_t byte_index,
+ TSPoint position, uint32_t *bytes_read)
+{
+ buf_T *bp = payload;
+#define BUFSIZE 256
+ static char buf[BUFSIZE];
+
+ if ((linenr_T)position.row >= bp->b_ml.ml_line_count) {
+ *bytes_read = 0;
+ return "";
+ }
+ char_u *line = ml_get_buf(bp, position.row+1, false);
+ size_t len = STRLEN(line);
+ size_t tocopy = MIN(len-position.column, BUFSIZE);
+
+ memcpy(buf, line+position.column, tocopy);
+ // Translate embedded \n to NUL
+ memchrsub(buf, '\n', '\0', tocopy);
+ *bytes_read = (uint32_t)tocopy;
+ if (tocopy < BUFSIZE) {
+ // now add the final \n. If it didn't fit, input_cb will be called again
+ // on the same line with advanced column.
+ buf[tocopy] = '\n';
+ (*bytes_read)++;
+ }
+ return buf;
+#undef BUFSIZE
+}
+
+static int parser_parse_buf(lua_State *L)
+{
+ TSLua_parser *p = parser_check(L);
+ if (!p) {
+ return 0;
+ }
+
+ long bufnr = lua_tointeger(L, 2);
+ void *payload = handle_get_buffer(bufnr);
+ if (!payload) {
+ return luaL_error(L, "invalid buffer handle: %d", bufnr);
+ }
+ TSInput input = { payload, input_cb, TSInputEncodingUTF8 };
+ TSTree *new_tree = ts_parser_parse(p->parser, p->tree, input);
+ if (p->tree) {
+ ts_tree_delete(p->tree);
+ }
+ p->tree = new_tree;
+
+ tslua_push_tree(L, p->tree);
+ return 1;
+}
+
+static int parser_tree(lua_State *L)
+{
+ TSLua_parser *p = parser_check(L);
+ if (!p) {
+ return 0;
+ }
+
+ tslua_push_tree(L, p->tree);
+ return 1;
+}
+
+static int parser_edit(lua_State *L)
+{
+ if (lua_gettop(L) < 10) {
+ lua_pushstring(L, "not enough args to parser:edit()");
+ return lua_error(L);
+ }
+
+ TSLua_parser *p = parser_check(L);
+ if (!p) {
+ return 0;
+ }
+
+ if (!p->tree) {
+ return 0;
+ }
+
+ long start_byte = lua_tointeger(L, 2);
+ long old_end_byte = lua_tointeger(L, 3);
+ long new_end_byte = lua_tointeger(L, 4);
+ TSPoint start_point = { lua_tointeger(L, 5), lua_tointeger(L, 6) };
+ TSPoint old_end_point = { lua_tointeger(L, 7), lua_tointeger(L, 8) };
+ TSPoint new_end_point = { lua_tointeger(L, 9), lua_tointeger(L, 10) };
+
+ TSInputEdit edit = { start_byte, old_end_byte, new_end_byte,
+ start_point, old_end_point, new_end_point };
+
+ ts_tree_edit(p->tree, &edit);
+
+ return 0;
+}
+
+
+// Tree methods
+
+/// push tree interface on lua stack.
+///
+/// This makes a copy of the tree, so ownership of the argument is unaffected.
+void tslua_push_tree(lua_State *L, TSTree *tree)
+{
+ if (tree == NULL) {
+ lua_pushnil(L);
+ return;
+ }
+ TSTree **ud = lua_newuserdata(L, sizeof(TSTree *)); // [udata]
+ *ud = ts_tree_copy(tree);
+ lua_getfield(L, LUA_REGISTRYINDEX, "treesitter_tree"); // [udata, meta]
+ lua_setmetatable(L, -2); // [udata]
+
+ // table used for node wrappers to keep a reference to tree wrapper
+ // NB: in lua 5.3 the uservalue for the node could just be the tree, but
+ // in lua 5.1 the uservalue (fenv) must be a table.
+ lua_createtable(L, 1, 0); // [udata, reftable]
+ lua_pushvalue(L, -2); // [udata, reftable, udata]
+ lua_rawseti(L, -2, 1); // [udata, reftable]
+ lua_setfenv(L, -2); // [udata]
+}
+
+static TSTree *tree_check(lua_State *L)
+{
+ TSTree **ud = luaL_checkudata(L, 1, "treesitter_tree");
+ return *ud;
+}
+
+static int tree_gc(lua_State *L)
+{
+ TSTree *tree = tree_check(L);
+ if (!tree) {
+ return 0;
+ }
+
+ ts_tree_delete(tree);
+ return 0;
+}
+
+static int tree_tostring(lua_State *L)
+{
+ lua_pushstring(L, "<tree>");
+ return 1;
+}
+
+static int tree_root(lua_State *L)
+{
+ TSTree *tree = tree_check(L);
+ if (!tree) {
+ return 0;
+ }
+ TSNode root = ts_tree_root_node(tree);
+ push_node(L, root);
+ return 1;
+}
+
+// Node methods
+
+/// push node interface on lua stack
+///
+/// top of stack must either be the tree this node belongs to or another node
+/// of the same tree! This value is not popped. Can only be called inside a
+/// cfunction with the tslua environment.
+static void push_node(lua_State *L, TSNode node)
+{
+ if (ts_node_is_null(node)) {
+ lua_pushnil(L); // [src, nil]
+ return;
+ }
+ TSNode *ud = lua_newuserdata(L, sizeof(TSNode)); // [src, udata]
+ *ud = node;
+ lua_getfield(L, LUA_REGISTRYINDEX, "treesitter_node"); // [src, udata, meta]
+ lua_setmetatable(L, -2); // [src, udata]
+ lua_getfenv(L, -2); // [src, udata, reftable]
+ lua_setfenv(L, -2); // [src, udata]
+}
+
+static bool node_check(lua_State *L, TSNode *res)
+{
+ TSNode *ud = luaL_checkudata(L, 1, "treesitter_node");
+ if (ud) {
+ *res = *ud;
+ return true;
+ }
+ return false;
+}
+
+
+static int node_tostring(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ lua_pushstring(L, "<node ");
+ lua_pushstring(L, ts_node_type(node));
+ lua_pushstring(L, ">");
+ lua_concat(L, 3);
+ return 1;
+}
+
+static int node_eq(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ // This should only be called if both x and y in "x == y" has the
+ // treesitter_node metatable. So it is ok to error out otherwise.
+ TSNode *ud = luaL_checkudata(L, 2, "treesitter_node");
+ if (!ud) {
+ return 0;
+ }
+ TSNode node2 = *ud;
+ lua_pushboolean(L, ts_node_eq(node, node2));
+ return 1;
+}
+
+static int node_range(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ TSPoint start = ts_node_start_point(node);
+ TSPoint end = ts_node_end_point(node);
+ lua_pushnumber(L, start.row);
+ lua_pushnumber(L, start.column);
+ lua_pushnumber(L, end.row);
+ lua_pushnumber(L, end.column);
+ return 4;
+}
+
+static int node_start(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ TSPoint start = ts_node_start_point(node);
+ uint32_t start_byte = ts_node_start_byte(node);
+ lua_pushnumber(L, start.row);
+ lua_pushnumber(L, start.column);
+ lua_pushnumber(L, start_byte);
+ return 3;
+}
+
+static int node_end(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ TSPoint end = ts_node_end_point(node);
+ uint32_t end_byte = ts_node_end_byte(node);
+ lua_pushnumber(L, end.row);
+ lua_pushnumber(L, end.column);
+ lua_pushnumber(L, end_byte);
+ return 3;
+}
+
+static int node_child_count(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ uint32_t count = ts_node_child_count(node);
+ lua_pushnumber(L, count);
+ return 1;
+}
+
+static int node_named_child_count(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ uint32_t count = ts_node_named_child_count(node);
+ lua_pushnumber(L, count);
+ return 1;
+}
+
+static int node_type(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ lua_pushstring(L, ts_node_type(node));
+ return 1;
+}
+
+static int node_symbol(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ TSSymbol symbol = ts_node_symbol(node);
+ lua_pushnumber(L, symbol);
+ return 1;
+}
+
+static int node_named(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ lua_pushboolean(L, ts_node_is_named(node));
+ return 1;
+}
+
+static int node_sexpr(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ char *allocated = ts_node_string(node);
+ lua_pushstring(L, allocated);
+ xfree(allocated);
+ return 1;
+}
+
+static int node_missing(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ lua_pushboolean(L, ts_node_is_missing(node));
+ return 1;
+}
+
+static int node_has_error(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ lua_pushboolean(L, ts_node_has_error(node));
+ return 1;
+}
+
+static int node_child(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ long num = lua_tointeger(L, 2);
+ TSNode child = ts_node_child(node, (uint32_t)num);
+
+ lua_pushvalue(L, 1);
+ push_node(L, child);
+ return 1;
+}
+
+static int node_named_child(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ long num = lua_tointeger(L, 2);
+ TSNode child = ts_node_named_child(node, (uint32_t)num);
+
+ lua_pushvalue(L, 1);
+ push_node(L, child);
+ return 1;
+}
+
+static int node_descendant_for_range(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ TSPoint start = { (uint32_t)lua_tointeger(L, 2),
+ (uint32_t)lua_tointeger(L, 3) };
+ TSPoint end = { (uint32_t)lua_tointeger(L, 4),
+ (uint32_t)lua_tointeger(L, 5) };
+ TSNode child = ts_node_descendant_for_point_range(node, start, end);
+
+ lua_pushvalue(L, 1);
+ push_node(L, child);
+ return 1;
+}
+
+static int node_named_descendant_for_range(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ TSPoint start = { (uint32_t)lua_tointeger(L, 2),
+ (uint32_t)lua_tointeger(L, 3) };
+ TSPoint end = { (uint32_t)lua_tointeger(L, 4),
+ (uint32_t)lua_tointeger(L, 5) };
+ TSNode child = ts_node_named_descendant_for_point_range(node, start, end);
+
+ lua_pushvalue(L, 1);
+ push_node(L, child);
+ return 1;
+}
+
+static int node_parent(lua_State *L)
+{
+ TSNode node;
+ if (!node_check(L, &node)) {
+ return 0;
+ }
+ TSNode parent = ts_node_parent(node);
+ push_node(L, parent);
+ return 1;
+}
+
diff --git a/src/nvim/lua/treesitter.h b/src/nvim/lua/treesitter.h
new file mode 100644
index 0000000000..812166f67b
--- /dev/null
+++ b/src/nvim/lua/treesitter.h
@@ -0,0 +1,14 @@
+#ifndef NVIM_LUA_TREESITTER_H
+#define NVIM_LUA_TREESITTER_H
+
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#include "tree_sitter/api.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "lua/treesitter.h.generated.h"
+#endif
+
+#endif // NVIM_LUA_TREESITTER_H
diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua
index a03e97490d..e7c5458102 100644
--- a/src/nvim/lua/vim.lua
+++ b/src/nvim/lua/vim.lua
@@ -33,35 +33,35 @@
-- - https://github.com/bakpakin/Fennel (pretty print, repl)
-- - https://github.com/howl-editor/howl/tree/master/lib/howl/util
+local vim = vim
+assert(vim)
-- Internal-only until comments in #8107 are addressed.
-- Returns:
-- {errcode}, {output}
-local function _system(cmd)
- local out = vim.api.nvim_call_function('system', { cmd })
- local err = vim.api.nvim_get_vvar('shell_error')
+function vim._system(cmd)
+ local out = vim.fn.system(cmd)
+ local err = vim.v.shell_error
return err, out
end
-- Gets process info from the `ps` command.
-- Used by nvim_get_proc() as a fallback.
-local function _os_proc_info(pid)
+function vim._os_proc_info(pid)
if pid == nil or pid <= 0 or type(pid) ~= 'number' then
error('invalid pid')
end
local cmd = { 'ps', '-p', pid, '-o', 'comm=', }
- local err, name = _system(cmd)
- if 1 == err and string.gsub(name, '%s*', '') == '' then
+ local err, name = vim._system(cmd)
+ if 1 == err and vim.trim(name) == '' then
return {} -- Process not found.
elseif 0 ~= err then
- local args_str = vim.api.nvim_call_function('string', { cmd })
- error('command failed: '..args_str)
+ error('command failed: '..vim.fn.string(cmd))
end
- local _, ppid = _system({ 'ps', '-p', pid, '-o', 'ppid=', })
+ local _, ppid = vim._system({ 'ps', '-p', pid, '-o', 'ppid=', })
-- Remove trailing whitespace.
- name = string.gsub(string.gsub(name, '%s+$', ''), '^.*/', '')
- ppid = string.gsub(ppid, '%s+$', '')
- ppid = tonumber(ppid) == nil and -1 or tonumber(ppid)
+ name = vim.trim(name):gsub('^.*/', '')
+ ppid = tonumber(ppid) or -1
return {
name = name,
pid = pid,
@@ -71,20 +71,19 @@ end
-- Gets process children from the `pgrep` command.
-- Used by nvim_get_proc_children() as a fallback.
-local function _os_proc_children(ppid)
+function vim._os_proc_children(ppid)
if ppid == nil or ppid <= 0 or type(ppid) ~= 'number' then
error('invalid ppid')
end
local cmd = { 'pgrep', '-P', ppid, }
- local err, rv = _system(cmd)
- if 1 == err and string.gsub(rv, '%s*', '') == '' then
+ local err, rv = vim._system(cmd)
+ if 1 == err and vim.trim(rv) == '' then
return {} -- Process not found.
elseif 0 ~= err then
- local args_str = vim.api.nvim_call_function('string', { cmd })
- error('command failed: '..args_str)
+ error('command failed: '..vim.fn.string(cmd))
end
local children = {}
- for s in string.gmatch(rv, '%S+') do
+ for s in rv:gmatch('%S+') do
local i = tonumber(s)
if i ~= nil then
table.insert(children, i)
@@ -98,7 +97,7 @@ end
-- Last inserted paths. Used to clear out items from package.[c]path when they
-- are no longer in &runtimepath.
local last_nvim_paths = {}
-local function _update_package_paths()
+function vim._update_package_paths()
local cur_nvim_paths = {}
local rtps = vim.api.nvim_list_runtime_paths()
local sep = package.config:sub(1, 1)
@@ -162,22 +161,35 @@ local function inspect(object, options) -- luacheck: no unused
error(object, options) -- Stub for gen_vimdoc.py
end
---- Paste handler, invoked by |nvim_paste()| when a conforming UI
---- (such as the |TUI|) pastes text into the editor.
----
---@see |paste|
----
---@param lines |readfile()|-style list of lines to paste. |channel-lines|
---@param phase -1: "non-streaming" paste: the call contains all lines.
---- If paste is "streamed", `phase` indicates the stream state:
---- - 1: starts the paste (exactly once)
---- - 2: continues the paste (zero or more times)
---- - 3: ends the paste (exactly once)
---@returns false if client should cancel the paste.
-local function paste(lines, phase) end -- luacheck: no unused
-paste = (function()
+do
local tdots, tick, got_line1 = 0, 0, false
- return function(lines, phase)
+
+ --- Paste handler, invoked by |nvim_paste()| when a conforming UI
+ --- (such as the |TUI|) pastes text into the editor.
+ ---
+ --- Example: To remove ANSI color codes when pasting:
+ --- <pre>
+ --- vim.paste = (function(overridden)
+ --- return function(lines, phase)
+ --- for i,line in ipairs(lines) do
+ --- -- Scrub ANSI color codes from paste input.
+ --- lines[i] = line:gsub('\27%[[0-9;mK]+', '')
+ --- end
+ --- overridden(lines, phase)
+ --- end
+ --- end)(vim.paste)
+ --- </pre>
+ ---
+ --@see |paste|
+ ---
+ --@param lines |readfile()|-style list of lines to paste. |channel-lines|
+ --@param phase -1: "non-streaming" paste: the call contains all lines.
+ --- If paste is "streamed", `phase` indicates the stream state:
+ --- - 1: starts the paste (exactly once)
+ --- - 2: continues the paste (zero or more times)
+ --- - 3: ends the paste (exactly once)
+ --@returns false if client should cancel the paste.
+ function vim.paste(lines, phase)
local call = vim.api.nvim_call_function
local now = vim.loop.now()
local mode = call('mode', {}):sub(1,1)
@@ -189,11 +201,14 @@ paste = (function()
if mode == 'c' and not got_line1 then -- cmdline-mode: paste only 1 line.
got_line1 = (#lines > 1)
vim.api.nvim_set_option('paste', true) -- For nvim_input().
- local line1, _ = string.gsub(lines[1], '[\r\n\012\027]', ' ') -- Scrub.
+ local line1 = lines[1]:gsub('<', '<lt>'):gsub('[\r\n\012\027]', ' ') -- Scrub.
vim.api.nvim_input(line1)
vim.api.nvim_set_option('paste', false)
- elseif mode ~= 'c' then -- Else: discard remaining cmdline-mode chunks.
- if phase < 2 and mode ~= 'i' and mode ~= 'R' and mode ~= 't' then
+ elseif mode ~= 'c' then
+ if phase < 2 and mode:find('^[vV\22sS\19]') then
+ vim.api.nvim_command([[exe "normal! \<Del>"]])
+ vim.api.nvim_put(lines, 'c', false, true)
+ elseif phase < 2 and not mode:find('^[iRt]') then
vim.api.nvim_put(lines, 'c', true, true)
-- XXX: Normal-mode: workaround bad cursor-placement after first chunk.
vim.api.nvim_command('normal! a')
@@ -214,42 +229,157 @@ paste = (function()
end
return true -- Paste will not continue if not returning `true`.
end
-end)()
+end
--- Defers callback `cb` until the Nvim API is safe to call.
---
---@see |lua-loop-callbacks|
---@see |vim.schedule()|
---@see |vim.in_fast_event()|
-local function schedule_wrap(cb)
+function vim.schedule_wrap(cb)
return (function (...)
local args = {...}
vim.schedule(function() cb(unpack(args)) end)
end)
end
+-- vim.fn.{func}(...)
+vim.fn = setmetatable({}, {
+ __index = function(t, key)
+ local function _fn(...)
+ return vim.call(key, ...)
+ end
+ t[key] = _fn
+ return _fn
+ end
+})
+
+-- These are for loading runtime modules lazily since they aren't available in
+-- the nvim binary as specified in executor.c
local function __index(t, key)
if key == 'inspect' then
t.inspect = require('vim.inspect')
return t.inspect
- elseif require('vim.shared')[key] ~= nil then
- -- Expose all `vim.shared` functions on the `vim` module.
- t[key] = require('vim.shared')[key]
+ elseif key == 'treesitter' then
+ t.treesitter = require('vim.treesitter')
+ return t.treesitter
+ elseif require('vim.uri')[key] ~= nil then
+ -- Expose all `vim.uri` functions on the `vim` module.
+ t[key] = require('vim.uri')[key]
return t[key]
+ elseif key == 'lsp' then
+ t.lsp = require('vim.lsp')
+ return t.lsp
end
end
-local module = {
- _update_package_paths = _update_package_paths,
- _os_proc_children = _os_proc_children,
- _os_proc_info = _os_proc_info,
- _system = _system,
- paste = paste,
- schedule_wrap = schedule_wrap,
-}
-
-setmetatable(module, {
+setmetatable(vim, {
__index = __index
})
+-- An easier alias for commands.
+vim.cmd = vim.api.nvim_command
+
+-- These are the vim.env/v/g/o/bo/wo variable magic accessors.
+do
+ local a = vim.api
+ local validate = vim.validate
+ local function make_meta_accessor(get, set, del)
+ validate {
+ get = {get, 'f'};
+ set = {set, 'f'};
+ del = {del, 'f', true};
+ }
+ local mt = {}
+ if del then
+ function mt:__newindex(k, v)
+ if v == nil then
+ return del(k)
+ end
+ return set(k, v)
+ end
+ else
+ function mt:__newindex(k, v)
+ return set(k, v)
+ end
+ end
+ function mt:__index(k)
+ return get(k)
+ end
+ return setmetatable({}, mt)
+ end
+ local function pcall_ret(status, ...)
+ if status then return ... end
+ end
+ local function nil_wrap(fn)
+ return function(...)
+ return pcall_ret(pcall(fn, ...))
+ end
+ end
+ vim.g = make_meta_accessor(nil_wrap(a.nvim_get_var), a.nvim_set_var, a.nvim_del_var)
+ vim.v = make_meta_accessor(nil_wrap(a.nvim_get_vvar), a.nvim_set_vvar)
+ vim.o = make_meta_accessor(a.nvim_get_option, a.nvim_set_option)
+ local function getenv(k)
+ local v = vim.fn.getenv(k)
+ if v == vim.NIL then
+ return nil
+ end
+ return v
+ end
+ vim.env = make_meta_accessor(getenv, vim.fn.setenv)
+ -- TODO(ashkan) if/when these are available from an API, generate them
+ -- instead of hardcoding.
+ local window_options = {
+ arab = true; arabic = true; breakindent = true; breakindentopt = true;
+ bri = true; briopt = true; cc = true; cocu = true;
+ cole = true; colorcolumn = true; concealcursor = true; conceallevel = true;
+ crb = true; cuc = true; cul = true; cursorbind = true;
+ cursorcolumn = true; cursorline = true; diff = true; fcs = true;
+ fdc = true; fde = true; fdi = true; fdl = true;
+ fdm = true; fdn = true; fdt = true; fen = true;
+ fillchars = true; fml = true; fmr = true; foldcolumn = true;
+ foldenable = true; foldexpr = true; foldignore = true; foldlevel = true;
+ foldmarker = true; foldmethod = true; foldminlines = true; foldnestmax = true;
+ foldtext = true; lbr = true; lcs = true; linebreak = true;
+ list = true; listchars = true; nu = true; number = true;
+ numberwidth = true; nuw = true; previewwindow = true; pvw = true;
+ relativenumber = true; rightleft = true; rightleftcmd = true; rl = true;
+ rlc = true; rnu = true; scb = true; scl = true;
+ scr = true; scroll = true; scrollbind = true; signcolumn = true;
+ spell = true; statusline = true; stl = true; wfh = true;
+ wfw = true; winbl = true; winblend = true; winfixheight = true;
+ winfixwidth = true; winhighlight = true; winhl = true; wrap = true;
+ }
+ local function new_buf_opt_accessor(bufnr)
+ local function get(k)
+ if window_options[k] then
+ return a.nvim_err_writeln(k.." is a window option, not a buffer option")
+ end
+ if bufnr == nil and type(k) == "number" then
+ return new_buf_opt_accessor(k)
+ end
+ return a.nvim_buf_get_option(bufnr or 0, k)
+ end
+ local function set(k, v)
+ if window_options[k] then
+ return a.nvim_err_writeln(k.." is a window option, not a buffer option")
+ end
+ return a.nvim_buf_set_option(bufnr or 0, k, v)
+ end
+ return make_meta_accessor(get, set)
+ end
+ vim.bo = new_buf_opt_accessor(nil)
+ local function new_win_opt_accessor(winnr)
+ local function get(k)
+ if winnr == nil and type(k) == "number" then
+ return new_win_opt_accessor(k)
+ end
+ return a.nvim_win_get_option(winnr or 0, k)
+ end
+ local function set(k, v) return a.nvim_win_set_option(winnr or 0, k, v) end
+ return make_meta_accessor(get, set)
+ end
+ vim.wo = new_win_opt_accessor(nil)
+end
+
return module
diff --git a/src/nvim/macros.h b/src/nvim/macros.h
index d11507fa6a..f6c8c0a4a0 100644
--- a/src/nvim/macros.h
+++ b/src/nvim/macros.h
@@ -110,8 +110,6 @@
// MB_COPY_CHAR(f, t): copy one char from "f" to "t" and advance the pointers.
// PTR2CHAR(): get character from pointer.
-// Get the length of the character p points to, including composing chars.
-# define MB_PTR2LEN(p) mb_ptr2len(p)
// Advance multi-byte pointer, skip over composing chars.
# define MB_PTR_ADV(p) (p += mb_ptr2len((char_u *)p))
// Advance multi-byte pointer, do not skip over composing chars.
diff --git a/src/nvim/main.c b/src/nvim/main.c
index be1f08bb46..c7011f4f4e 100644
--- a/src/nvim/main.c
+++ b/src/nvim/main.c
@@ -27,6 +27,7 @@
#include "nvim/highlight.h"
#include "nvim/iconv.h"
#include "nvim/if_cscope.h"
+#include "nvim/lua/executor.h"
#ifdef HAVE_LOCALE_H
# include <locale.h>
#endif
@@ -143,7 +144,6 @@ static const char *err_extra_cmd =
void event_init(void)
{
- log_init();
loop_init(&main_loop, NULL);
resize_events = multiqueue_new_child(main_loop.events);
@@ -219,6 +219,7 @@ void early_init(void)
// First find out the home directory, needed to expand "~" in options.
init_homedir(); // find real value of $HOME
set_init_1();
+ log_init();
TIME_MSG("inits 1");
set_lang_var(); // set v:lang and v:ctype
@@ -957,6 +958,7 @@ static void command_line_scan(mparm_T *parmp)
case 'r': // "-r" recovery mode
case 'L': { // "-L" recovery mode
recoverymode = 1;
+ headless_mode = true;
break;
}
case 's': {
@@ -1460,12 +1462,13 @@ static void create_windows(mparm_T *parmp)
} else
parmp->window_count = 1;
- if (recoverymode) { /* do recover */
- msg_scroll = TRUE; /* scroll message up */
- ml_recover();
- if (curbuf->b_ml.ml_mfp == NULL) /* failed */
+ if (recoverymode) { // do recover
+ msg_scroll = true; // scroll message up
+ ml_recover(true);
+ if (curbuf->b_ml.ml_mfp == NULL) { // failed
getout(1);
- do_modelines(0); /* do modelines */
+ }
+ do_modelines(0); // do modelines
} else {
// Open a buffer for windows that don't have one yet.
// Commands in the vimrc might have loaded a file or split the window.
@@ -1513,7 +1516,7 @@ static void create_windows(mparm_T *parmp)
/* We can't close the window, it would disturb what
* happens next. Clear the file name and set the arg
* index to -1 to delete it later. */
- setfname(curbuf, NULL, NULL, FALSE);
+ setfname(curbuf, NULL, NULL, false);
curwin->w_arg_idx = -1;
swap_exists_action = SEA_NONE;
} else
@@ -1778,7 +1781,8 @@ static bool do_user_initialization(void)
if (do_source(user_vimrc, true, DOSO_VIMRC) != FAIL) {
do_exrc = p_exrc;
if (do_exrc) {
- do_exrc = (path_full_compare((char_u *)VIMRC_FILE, user_vimrc, false)
+ do_exrc = (path_full_compare((char_u *)VIMRC_FILE, user_vimrc,
+ false, true)
!= kEqualFiles);
}
xfree(user_vimrc);
@@ -1805,7 +1809,7 @@ static bool do_user_initialization(void)
do_exrc = p_exrc;
if (do_exrc) {
do_exrc = (path_full_compare((char_u *)VIMRC_FILE, (char_u *)vimrc,
- false) != kEqualFiles);
+ false, true) != kEqualFiles);
}
xfree(vimrc);
xfree(config_dirs);
diff --git a/src/nvim/mark.c b/src/nvim/mark.c
index e103d3cb55..93bc497cf0 100644
--- a/src/nvim/mark.c
+++ b/src/nvim/mark.c
@@ -178,6 +178,16 @@ void setpcmark(void)
curwin->w_pcmark.lnum = 1;
}
+ if (jop_flags & JOP_STACK) {
+ // If we're somewhere in the middle of the jumplist discard everything
+ // after the current index.
+ if (curwin->w_jumplistidx < curwin->w_jumplistlen - 1) {
+ // Discard the rest of the jumplist by cutting the length down to
+ // contain nothing beyond the current index.
+ curwin->w_jumplistlen = curwin->w_jumplistidx + 1;
+ }
+ }
+
/* If jumplist is full: remove oldest entry */
if (++curwin->w_jumplistlen > JUMPLISTSIZE) {
curwin->w_jumplistlen = JUMPLISTSIZE;
@@ -296,17 +306,17 @@ pos_T *movechangelist(int count)
* - NULL if there is no mark called 'c'.
* - -1 if mark is in other file and jumped there (only if changefile is TRUE)
*/
-pos_T *getmark_buf(buf_T *buf, int c, int changefile)
+pos_T *getmark_buf(buf_T *buf, int c, bool changefile)
{
return getmark_buf_fnum(buf, c, changefile, NULL);
}
-pos_T *getmark(int c, int changefile)
+pos_T *getmark(int c, bool changefile)
{
return getmark_buf_fnum(curbuf, c, changefile, NULL);
}
-pos_T *getmark_buf_fnum(buf_T *buf, int c, int changefile, int *fnum)
+pos_T *getmark_buf_fnum(buf_T *buf, int c, bool changefile, int *fnum)
{
pos_T *posp;
pos_T *startp, *endp;
@@ -616,7 +626,7 @@ static char_u *mark_line(pos_T *mp, int lead_len)
/*
* print the marks
*/
-void do_marks(exarg_T *eap)
+void ex_marks(exarg_T *eap)
{
char_u *arg = eap->arg;
int i;
@@ -905,9 +915,10 @@ void mark_adjust(linenr_T line1,
linenr_T line2,
long amount,
long amount_after,
- bool end_temp)
+ bool end_temp,
+ ExtmarkOp op)
{
- mark_adjust_internal(line1, line2, amount, amount_after, true, end_temp);
+ mark_adjust_internal(line1, line2, amount, amount_after, true, end_temp, op);
}
// mark_adjust_nofold() does the same as mark_adjust() but without adjusting
@@ -916,14 +927,16 @@ void mark_adjust(linenr_T line1,
// calling foldMarkAdjust() with arguments line1, line2, amount, amount_after,
// for an example of why this may be necessary, see do_move().
void mark_adjust_nofold(linenr_T line1, linenr_T line2, long amount,
- long amount_after, bool end_temp)
+ long amount_after, bool end_temp,
+ ExtmarkOp op)
{
- mark_adjust_internal(line1, line2, amount, amount_after, false, end_temp);
+ mark_adjust_internal(line1, line2, amount, amount_after, false, end_temp, op);
}
static void mark_adjust_internal(linenr_T line1, linenr_T line2,
long amount, long amount_after,
- bool adjust_folds, bool end_temp)
+ bool adjust_folds, bool end_temp,
+ ExtmarkOp op)
{
int i;
int fnum = curbuf->b_fnum;
@@ -979,6 +992,9 @@ static void mark_adjust_internal(linenr_T line1, linenr_T line2,
sign_mark_adjust(line1, line2, amount, amount_after);
bufhl_mark_adjust(curbuf, line1, line2, amount, amount_after, end_temp);
+ if (op != kExtmarkNOOP) {
+ extmark_adjust(curbuf, line1, line2, amount, amount_after, op, end_temp);
+ }
}
/* previous context mark */
@@ -1090,7 +1106,7 @@ static void mark_adjust_internal(linenr_T line1, linenr_T line2,
// cursor is inside them.
void mark_col_adjust(
linenr_T lnum, colnr_T mincol, long lnum_amount, long col_amount,
- int spaces_removed)
+ int spaces_removed, ExtmarkOp op)
{
int i;
int fnum = curbuf->b_fnum;
@@ -1110,6 +1126,13 @@ void mark_col_adjust(
col_adjust(&(namedfm[i].fmark.mark));
}
+ // Extmarks
+ if (op != kExtmarkNOOP) {
+ // TODO(timeyyy): consider spaces_removed? (behave like a delete)
+ extmark_col_adjust(curbuf, lnum, mincol, lnum_amount, col_amount,
+ kExtmarkUndo);
+ }
+
/* last Insert position */
col_adjust(&(curbuf->b_last_insert.mark));
@@ -1191,7 +1214,20 @@ void cleanup_jumplist(win_T *wp, bool checktail)
break;
}
}
- if (i >= wp->w_jumplistlen) { // no duplicate
+ bool mustfree;
+ if (i >= wp->w_jumplistlen) { // not duplicate
+ mustfree = false;
+ } else if (i > from + 1) { // non-adjacent duplicate
+ // When the jump options include "stack", duplicates are only removed from
+ // the jumplist when they are adjacent.
+ mustfree = !(jop_flags & JOP_STACK);
+ } else { // adjacent duplicate
+ mustfree = true;
+ }
+
+ if (mustfree) {
+ xfree(wp->w_jumplist[from].fname);
+ } else {
if (to != from) {
// Not using wp->w_jumplist[to++] = wp->w_jumplist[from] because
// this way valgrind complains about overlapping source and destination
@@ -1199,8 +1235,6 @@ void cleanup_jumplist(win_T *wp, bool checktail)
wp->w_jumplist[to] = wp->w_jumplist[from];
}
to++;
- } else {
- xfree(wp->w_jumplist[from].fname);
}
}
if (wp->w_jumplistidx == wp->w_jumplistlen) {
diff --git a/src/nvim/mark_extended.c b/src/nvim/mark_extended.c
new file mode 100644
index 0000000000..17776d438a
--- /dev/null
+++ b/src/nvim/mark_extended.c
@@ -0,0 +1,1139 @@
+// This is an open source non-commercial project. Dear PVS-Studio, please check
+// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+
+// Implements extended marks for plugins. Each mark exists in a btree of
+// lines containing btrees of columns.
+//
+// The btree provides efficient range lookups.
+// A map of pointers to the marks is used for fast lookup by mark id.
+//
+// Marks are moved by calls to: extmark_col_adjust, extmark_adjust, or
+// extmark_col_adjust_delete which are based on col_adjust and mark_adjust from
+// mark.c
+//
+// Undo/Redo of marks is implemented by storing the call arguments to
+// extmark_col_adjust or extmark_adjust. The list of arguments
+// is applied in extmark_apply_undo. The only case where we have to
+// copy extmarks is for the area being effected by a delete.
+//
+// Marks live in namespaces that allow plugins/users to segregate marks
+// from other users.
+//
+// For possible ideas for efficency improvements see:
+// http://blog.atom.io/2015/06/16/optimizing-an-important-atom-primitive.html
+// TODO(bfredl): These ideas could be used for an enhanced btree, which
+// wouldn't need separate line and column layers.
+// Other implementations exist in gtk and tk toolkits.
+//
+// Deleting marks only happens when explicitly calling extmark_del, deleteing
+// over a range of marks will only move the marks. Deleting on a mark will
+// leave it in same position unless it is on the EOL of a line.
+
+#include <assert.h>
+#include "nvim/vim.h"
+#include "charset.h"
+#include "nvim/mark_extended.h"
+#include "nvim/memline.h"
+#include "nvim/pos.h"
+#include "nvim/globals.h"
+#include "nvim/map.h"
+#include "nvim/lib/kbtree.h"
+#include "nvim/undo.h"
+#include "nvim/buffer.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "mark_extended.c.generated.h"
+#endif
+
+
+/// Create or update an extmark
+///
+/// must not be used during iteration!
+/// @returns whether a new mark was created
+int extmark_set(buf_T *buf, uint64_t ns, uint64_t id,
+ linenr_T lnum, colnr_T col, ExtmarkOp op)
+{
+ Extmark *extmark = extmark_from_id(buf, ns, id);
+ if (!extmark) {
+ extmark_create(buf, ns, id, lnum, col, op);
+ return true;
+ } else {
+ ExtmarkLine *extmarkline = extmark->line;
+ extmark_update(extmark, buf, ns, id, lnum, col, op, NULL);
+ if (kb_size(&extmarkline->items) == 0) {
+ kb_del(extmarklines, &buf->b_extlines, extmarkline);
+ extmarkline_free(extmarkline);
+ }
+ return false;
+ }
+}
+
+// Remove an extmark
+// Returns 0 on missing id
+int extmark_del(buf_T *buf, uint64_t ns, uint64_t id, ExtmarkOp op)
+{
+ Extmark *extmark = extmark_from_id(buf, ns, id);
+ if (!extmark) {
+ return 0;
+ }
+ return extmark_delete(extmark, buf, ns, id, op);
+}
+
+// Free extmarks in a ns between lines
+// if ns = 0, it means clear all namespaces
+void extmark_clear(buf_T *buf, uint64_t ns,
+ linenr_T l_lnum, linenr_T u_lnum, ExtmarkOp undo)
+{
+ if (!buf->b_extmark_ns) {
+ return;
+ }
+
+ bool marks_cleared = false;
+ if (undo == kExtmarkUndo) {
+ // Copy marks that would be effected by clear
+ u_extmark_copy(buf, ns, l_lnum, 0, u_lnum, MAXCOL);
+ }
+
+ bool all_ns = ns == 0 ? true : false;
+ ExtmarkNs *ns_obj;
+ if (!all_ns) {
+ ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ if (!ns_obj) {
+ // nothing to do
+ return;
+ }
+ }
+
+ FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, {
+ FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, 0, MAXCOL, {
+ if (extmark->ns_id == ns || all_ns) {
+ marks_cleared = true;
+ if (all_ns) {
+ ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, extmark->ns_id);
+ } else {
+ ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ }
+ pmap_del(uint64_t)(ns_obj->map, extmark->mark_id);
+ kb_del_itr(markitems, &extmarkline->items, &mitr);
+ }
+ });
+ if (kb_size(&extmarkline->items) == 0) {
+ kb_del_itr(extmarklines, &buf->b_extlines, &itr);
+ extmarkline_free(extmarkline);
+ }
+ });
+
+ // Record the undo for the actual move
+ if (marks_cleared && undo == kExtmarkUndo) {
+ u_extmark_clear(buf, ns, l_lnum, u_lnum);
+ }
+}
+
+// Returns the position of marks between a range,
+// marks found at the start or end index will be included,
+// if upper_lnum or upper_col are negative the buffer
+// will be searched to the start, or end
+// dir can be set to control the order of the array
+// amount = amount of marks to find or -1 for all
+ExtmarkArray extmark_get(buf_T *buf, uint64_t ns,
+ linenr_T l_lnum, colnr_T l_col,
+ linenr_T u_lnum, colnr_T u_col,
+ int64_t amount, bool reverse)
+{
+ ExtmarkArray array = KV_INITIAL_VALUE;
+ // Find all the marks
+ if (!reverse) {
+ FOR_ALL_EXTMARKS(buf, ns, l_lnum, l_col, u_lnum, u_col, {
+ if (extmark->ns_id == ns) {
+ kv_push(array, extmark);
+ if (kv_size(array) == (size_t)amount) {
+ return array;
+ }
+ }
+ })
+ } else {
+ FOR_ALL_EXTMARKS_PREV(buf, ns, l_lnum, l_col, u_lnum, u_col, {
+ if (extmark->ns_id == ns) {
+ kv_push(array, extmark);
+ if (kv_size(array) == (size_t)amount) {
+ return array;
+ }
+ }
+ })
+ }
+ return array;
+}
+
+static void extmark_create(buf_T *buf, uint64_t ns, uint64_t id,
+ linenr_T lnum, colnr_T col, ExtmarkOp op)
+{
+ if (!buf->b_extmark_ns) {
+ buf->b_extmark_ns = pmap_new(uint64_t)();
+ }
+ ExtmarkNs *ns_obj = NULL;
+ ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ // Initialize a new namespace for this buffer
+ if (!ns_obj) {
+ ns_obj = xmalloc(sizeof(ExtmarkNs));
+ ns_obj->map = pmap_new(uint64_t)();
+ pmap_put(uint64_t)(buf->b_extmark_ns, ns, ns_obj);
+ }
+
+ // Create or get a line
+ ExtmarkLine *extmarkline = extmarkline_ref(buf, lnum, true);
+ // Create and put mark on the line
+ extmark_put(col, id, extmarkline, ns);
+
+ // Marks do not have stable address so we have to look them up
+ // by using the line instead of the mark
+ pmap_put(uint64_t)(ns_obj->map, id, extmarkline);
+ if (op != kExtmarkNoUndo) {
+ u_extmark_set(buf, ns, id, lnum, col, kExtmarkSet);
+ }
+
+ // Set a free id so extmark_free_id_get works
+ extmark_free_id_set(ns_obj, id);
+}
+
+// update the position of an extmark
+// to update while iterating pass the markitems itr
+static void extmark_update(Extmark *extmark, buf_T *buf,
+ uint64_t ns, uint64_t id,
+ linenr_T lnum, colnr_T col,
+ ExtmarkOp op, kbitr_t(markitems) *mitr)
+{
+ assert(op != kExtmarkNOOP);
+ if (op != kExtmarkNoUndo) {
+ u_extmark_update(buf, ns, id, extmark->line->lnum, extmark->col,
+ lnum, col);
+ }
+ ExtmarkLine *old_line = extmark->line;
+ // Move the mark to a new line and update column
+ if (old_line->lnum != lnum) {
+ ExtmarkLine *ref_line = extmarkline_ref(buf, lnum, true);
+ extmark_put(col, id, ref_line, ns);
+ // Update the hashmap
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ pmap_put(uint64_t)(ns_obj->map, id, ref_line);
+ // Delete old mark
+ if (mitr != NULL) {
+ kb_del_itr(markitems, &(old_line->items), mitr);
+ } else {
+ kb_del(markitems, &old_line->items, *extmark);
+ }
+ // Just update the column
+ } else {
+ if (mitr != NULL) {
+ // The btree stays organized during iteration with kbitr_t
+ extmark->col = col;
+ } else {
+ // Keep the btree in order
+ kb_del(markitems, &old_line->items, *extmark);
+ extmark_put(col, id, old_line, ns);
+ }
+ }
+}
+
+static int extmark_delete(Extmark *extmark,
+ buf_T *buf,
+ uint64_t ns,
+ uint64_t id,
+ ExtmarkOp op)
+{
+ if (op != kExtmarkNoUndo) {
+ u_extmark_set(buf, ns, id, extmark->line->lnum, extmark->col,
+ kExtmarkDel);
+ }
+
+ // Remove our key from the namespace
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ pmap_del(uint64_t)(ns_obj->map, id);
+
+ // Remove the mark mark from the line
+ ExtmarkLine *extmarkline = extmark->line;
+ kb_del(markitems, &extmarkline->items, *extmark);
+ // Remove the line if there are no more marks in the line
+ if (kb_size(&extmarkline->items) == 0) {
+ kb_del(extmarklines, &buf->b_extlines, extmarkline);
+ extmarkline_free(extmarkline);
+ }
+ return true;
+}
+
+// Lookup an extmark by id
+Extmark *extmark_from_id(buf_T *buf, uint64_t ns, uint64_t id)
+{
+ if (!buf->b_extmark_ns) {
+ return NULL;
+ }
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ if (!ns_obj || !kh_size(ns_obj->map->table)) {
+ return NULL;
+ }
+ ExtmarkLine *extmarkline = pmap_get(uint64_t)(ns_obj->map, id);
+ if (!extmarkline) {
+ return NULL;
+ }
+
+ FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, 0, MAXCOL, {
+ if (extmark->ns_id == ns
+ && extmark->mark_id == id) {
+ return extmark;
+ }
+ })
+ return NULL;
+}
+
+// Lookup an extmark by position
+Extmark *extmark_from_pos(buf_T *buf, uint64_t ns, linenr_T lnum, colnr_T col)
+{
+ if (!buf->b_extmark_ns) {
+ return NULL;
+ }
+ FOR_ALL_EXTMARKS(buf, ns, lnum, col, lnum, col, {
+ if (extmark->ns_id == ns) {
+ if (extmark->col == col) {
+ return extmark;
+ }
+ }
+ })
+ return NULL;
+}
+
+// Returns an available id in a namespace
+uint64_t extmark_free_id_get(buf_T *buf, uint64_t ns)
+{
+ if (!buf->b_extmark_ns) {
+ return 1;
+ }
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns);
+ if (!ns_obj) {
+ return 1;
+ }
+ return ns_obj->free_id;
+}
+
+// Set the next free id in a namesapce
+static void extmark_free_id_set(ExtmarkNs *ns_obj, uint64_t id)
+{
+ // Simply Heurstic, the largest id + 1
+ ns_obj->free_id = id + 1;
+}
+
+// free extmarks from the buffer
+void extmark_free_all(buf_T *buf)
+{
+ if (!buf->b_extmark_ns) {
+ return;
+ }
+
+ uint64_t ns;
+ ExtmarkNs *ns_obj;
+
+ FOR_ALL_EXTMARKLINES(buf, 1, MAXLNUM, {
+ kb_del_itr(extmarklines, &buf->b_extlines, &itr);
+ extmarkline_free(extmarkline);
+ })
+
+ map_foreach(buf->b_extmark_ns, ns, ns_obj, {
+ (void)ns;
+ pmap_free(uint64_t)(ns_obj->map);
+ xfree(ns_obj);
+ });
+
+ pmap_free(uint64_t)(buf->b_extmark_ns);
+ buf->b_extmark_ns = NULL;
+
+ // k?_init called to set pointers to NULL
+ kb_destroy(extmarklines, (&buf->b_extlines));
+ kb_init(&buf->b_extlines);
+
+ kv_destroy(buf->b_extmark_move_space);
+ kv_init(buf->b_extmark_move_space);
+}
+
+
+// Save info for undo/redo of set marks
+static void u_extmark_set(buf_T *buf, uint64_t ns, uint64_t id,
+ linenr_T lnum, colnr_T col, UndoObjectType undo_type)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ExtmarkSet set;
+ set.ns_id = ns;
+ set.mark_id = id;
+ set.lnum = lnum;
+ set.col = col;
+
+ ExtmarkUndoObject undo = { .type = undo_type,
+ .data.set = set };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// Save info for undo/redo of deleted marks
+static void u_extmark_update(buf_T *buf, uint64_t ns, uint64_t id,
+ linenr_T old_lnum, colnr_T old_col,
+ linenr_T lnum, colnr_T col)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ExtmarkUpdate update;
+ update.ns_id = ns;
+ update.mark_id = id;
+ update.old_lnum = old_lnum;
+ update.old_col = old_col;
+ update.lnum = lnum;
+ update.col = col;
+
+ ExtmarkUndoObject undo = { .type = kExtmarkUpdate,
+ .data.update = update };
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// Hueristic works only for when the user is typing in insert mode
+// - Instead of 1 undo object for each char inserted,
+// we create 1 undo objet for all text inserted before the user hits esc
+// Return True if we compacted else False
+static bool u_compact_col_adjust(buf_T *buf, linenr_T lnum, colnr_T mincol,
+ long lnum_amount, long col_amount)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return false;
+ }
+
+ if (kv_size(uhp->uh_extmark) < 1) {
+ return false;
+ }
+ // Check the last action
+ ExtmarkUndoObject object = kv_last(uhp->uh_extmark);
+
+ if (object.type != kColAdjust) {
+ return false;
+ }
+ ColAdjust undo = object.data.col_adjust;
+ bool compactable = false;
+
+ if (!undo.lnum_amount && !lnum_amount) {
+ if (undo.lnum == lnum) {
+ if ((undo.mincol + undo.col_amount) >= mincol) {
+ compactable = true;
+ } } }
+
+ if (!compactable) {
+ return false;
+ }
+
+ undo.col_amount = undo.col_amount + col_amount;
+ ExtmarkUndoObject new_undo = { .type = kColAdjust,
+ .data.col_adjust = undo };
+ kv_last(uhp->uh_extmark) = new_undo;
+ return true;
+}
+
+// Save col_adjust info so we can undo/redo
+void u_extmark_col_adjust(buf_T *buf, linenr_T lnum, colnr_T mincol,
+ long lnum_amount, long col_amount)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ if (!u_compact_col_adjust(buf, lnum, mincol, lnum_amount, col_amount)) {
+ ColAdjust col_adjust;
+ col_adjust.lnum = lnum;
+ col_adjust.mincol = mincol;
+ col_adjust.lnum_amount = lnum_amount;
+ col_adjust.col_amount = col_amount;
+
+ ExtmarkUndoObject undo = { .type = kColAdjust,
+ .data.col_adjust = col_adjust };
+
+ kv_push(uhp->uh_extmark, undo);
+ }
+}
+
+// Save col_adjust_delete info so we can undo/redo
+void u_extmark_col_adjust_delete(buf_T *buf, linenr_T lnum,
+ colnr_T mincol, colnr_T endcol, int eol)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ColAdjustDelete col_adjust_delete;
+ col_adjust_delete.lnum = lnum;
+ col_adjust_delete.mincol = mincol;
+ col_adjust_delete.endcol = endcol;
+ col_adjust_delete.eol = eol;
+
+ ExtmarkUndoObject undo = { .type = kColAdjustDelete,
+ .data.col_adjust_delete = col_adjust_delete };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// Save adjust info so we can undo/redo
+static void u_extmark_adjust(buf_T * buf, linenr_T line1, linenr_T line2,
+ long amount, long amount_after)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ Adjust adjust;
+ adjust.line1 = line1;
+ adjust.line2 = line2;
+ adjust.amount = amount;
+ adjust.amount_after = amount_after;
+
+ ExtmarkUndoObject undo = { .type = kLineAdjust,
+ .data.adjust = adjust };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// save info to undo/redo a :move
+void u_extmark_move(buf_T *buf, linenr_T line1, linenr_T line2,
+ linenr_T last_line, linenr_T dest, linenr_T num_lines,
+ linenr_T extra)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ AdjustMove move;
+ move.line1 = line1;
+ move.line2 = line2;
+ move.last_line = last_line;
+ move.dest = dest;
+ move.num_lines = num_lines;
+ move.extra = extra;
+
+ ExtmarkUndoObject undo = { .type = kAdjustMove,
+ .data.move = move };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// copy extmarks data between range, useful when we cannot simply reverse
+// the operation. This will do nothing on redo, enforces correct position when
+// undo.
+// if ns = 0, it means copy all namespaces
+void u_extmark_copy(buf_T *buf, uint64_t ns,
+ linenr_T l_lnum, colnr_T l_col,
+ linenr_T u_lnum, colnr_T u_col)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ bool all_ns = ns == 0 ? true : false;
+
+ ExtmarkCopy copy;
+ ExtmarkUndoObject undo;
+ FOR_ALL_EXTMARKS(buf, 1, l_lnum, l_col, u_lnum, u_col, {
+ if (all_ns || extmark->ns_id == ns) {
+ copy.ns_id = extmark->ns_id;
+ copy.mark_id = extmark->mark_id;
+ copy.lnum = extmark->line->lnum;
+ copy.col = extmark->col;
+
+ undo.data.copy = copy;
+ undo.type = kExtmarkCopy;
+ kv_push(uhp->uh_extmark, undo);
+ }
+ });
+}
+
+void u_extmark_copy_place(buf_T *buf,
+ linenr_T l_lnum, colnr_T l_col,
+ linenr_T u_lnum, colnr_T u_col,
+ linenr_T p_lnum, colnr_T p_col)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ExtmarkCopyPlace copy_place;
+ copy_place.l_lnum = l_lnum;
+ copy_place.l_col = l_col;
+ copy_place.u_lnum = u_lnum;
+ copy_place.u_col = u_col;
+ copy_place.p_lnum = p_lnum;
+ copy_place.p_col = p_col;
+
+ ExtmarkUndoObject undo = { .type = kExtmarkCopyPlace,
+ .data.copy_place = copy_place };
+
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// Save info for undo/redo of extmark_clear
+static void u_extmark_clear(buf_T *buf, uint64_t ns,
+ linenr_T l_lnum, linenr_T u_lnum)
+{
+ u_header_T *uhp = u_force_get_undo_header(buf);
+ if (!uhp) {
+ return;
+ }
+
+ ExtmarkClear clear;
+ clear.ns_id = ns;
+ clear.l_lnum = l_lnum;
+ clear.u_lnum = u_lnum;
+
+ ExtmarkUndoObject undo = { .type = kExtmarkClear,
+ .data.clear = clear };
+ kv_push(uhp->uh_extmark, undo);
+}
+
+// undo or redo an extmark operation
+void extmark_apply_undo(ExtmarkUndoObject undo_info, bool undo)
+{
+ linenr_T lnum;
+ colnr_T mincol;
+ long lnum_amount;
+ long col_amount;
+ linenr_T line1;
+ linenr_T line2;
+ long amount;
+ long amount_after;
+
+ // use extmark_col_adjust
+ if (undo_info.type == kColAdjust) {
+ // Undo
+ if (undo) {
+ lnum = (undo_info.data.col_adjust.lnum
+ + undo_info.data.col_adjust.lnum_amount);
+ lnum_amount = -undo_info.data.col_adjust.lnum_amount;
+ col_amount = -undo_info.data.col_adjust.col_amount;
+ mincol = (undo_info.data.col_adjust.mincol
+ + (colnr_T)undo_info.data.col_adjust.col_amount);
+ // Redo
+ } else {
+ lnum = undo_info.data.col_adjust.lnum;
+ col_amount = undo_info.data.col_adjust.col_amount;
+ lnum_amount = undo_info.data.col_adjust.lnum_amount;
+ mincol = undo_info.data.col_adjust.mincol;
+ }
+ extmark_col_adjust(curbuf,
+ lnum, mincol, lnum_amount, col_amount, kExtmarkNoUndo);
+ // use extmark_col_adjust_delete
+ } else if (undo_info.type == kColAdjustDelete) {
+ if (undo) {
+ mincol = undo_info.data.col_adjust_delete.mincol;
+ col_amount = (undo_info.data.col_adjust_delete.endcol
+ - undo_info.data.col_adjust_delete.mincol) + 1;
+ extmark_col_adjust(curbuf,
+ undo_info.data.col_adjust_delete.lnum,
+ mincol,
+ 0,
+ col_amount,
+ kExtmarkNoUndo);
+ // Redo
+ } else {
+ extmark_col_adjust_delete(curbuf,
+ undo_info.data.col_adjust_delete.lnum,
+ undo_info.data.col_adjust_delete.mincol,
+ undo_info.data.col_adjust_delete.endcol,
+ kExtmarkNoUndo,
+ undo_info.data.col_adjust_delete.eol);
+ }
+ // use extmark_adjust
+ } else if (undo_info.type == kLineAdjust) {
+ if (undo) {
+ // Undo - call signature type one - insert now
+ if (undo_info.data.adjust.amount == MAXLNUM) {
+ line1 = undo_info.data.adjust.line1;
+ line2 = MAXLNUM;
+ amount = -undo_info.data.adjust.amount_after;
+ amount_after = 0;
+ // Undo - call singature type two - delete now
+ } else if (undo_info.data.adjust.line2 == MAXLNUM) {
+ line1 = undo_info.data.adjust.line1;
+ line2 = undo_info.data.adjust.line2;
+ amount = -undo_info.data.adjust.amount;
+ amount_after = undo_info.data.adjust.amount_after;
+ // Undo - call signature three - move lines
+ } else {
+ line1 = (undo_info.data.adjust.line1
+ + undo_info.data.adjust.amount);
+ line2 = (undo_info.data.adjust.line2
+ + undo_info.data.adjust.amount);
+ amount = -undo_info.data.adjust.amount;
+ amount_after = -undo_info.data.adjust.amount_after;
+ }
+ // redo
+ } else {
+ line1 = undo_info.data.adjust.line1;
+ line2 = undo_info.data.adjust.line2;
+ amount = undo_info.data.adjust.amount;
+ amount_after = undo_info.data.adjust.amount_after;
+ }
+ extmark_adjust(curbuf,
+ line1, line2, amount, amount_after, kExtmarkNoUndo, false);
+ // kExtmarkCopy
+ } else if (undo_info.type == kExtmarkCopy) {
+ // Redo should be handled by kColAdjustDelete or kExtmarkCopyPlace
+ if (undo) {
+ extmark_set(curbuf,
+ undo_info.data.copy.ns_id,
+ undo_info.data.copy.mark_id,
+ undo_info.data.copy.lnum,
+ undo_info.data.copy.col,
+ kExtmarkNoUndo);
+ }
+ // uses extmark_copy_and_place
+ } else if (undo_info.type == kExtmarkCopyPlace) {
+ // Redo, undo is handle by kExtmarkCopy
+ if (!undo) {
+ extmark_copy_and_place(curbuf,
+ undo_info.data.copy_place.l_lnum,
+ undo_info.data.copy_place.l_col,
+ undo_info.data.copy_place.u_lnum,
+ undo_info.data.copy_place.u_col,
+ undo_info.data.copy_place.p_lnum,
+ undo_info.data.copy_place.p_col,
+ kExtmarkNoUndo, true, NULL);
+ }
+ // kExtmarkClear
+ } else if (undo_info.type == kExtmarkClear) {
+ // Redo, undo is handle by kExtmarkCopy
+ if (!undo) {
+ extmark_clear(curbuf,
+ undo_info.data.clear.ns_id,
+ undo_info.data.clear.l_lnum,
+ undo_info.data.clear.u_lnum,
+ kExtmarkNoUndo);
+ }
+ // kAdjustMove
+ } else if (undo_info.type == kAdjustMove) {
+ apply_undo_move(undo_info, undo);
+ // extmark_set
+ } else if (undo_info.type == kExtmarkSet) {
+ if (undo) {
+ extmark_del(curbuf,
+ undo_info.data.set.ns_id,
+ undo_info.data.set.mark_id,
+ kExtmarkNoUndo);
+ // Redo
+ } else {
+ extmark_set(curbuf,
+ undo_info.data.set.ns_id,
+ undo_info.data.set.mark_id,
+ undo_info.data.set.lnum,
+ undo_info.data.set.col,
+ kExtmarkNoUndo);
+ }
+ // extmark_set into update
+ } else if (undo_info.type == kExtmarkUpdate) {
+ if (undo) {
+ extmark_set(curbuf,
+ undo_info.data.update.ns_id,
+ undo_info.data.update.mark_id,
+ undo_info.data.update.old_lnum,
+ undo_info.data.update.old_col,
+ kExtmarkNoUndo);
+ // Redo
+ } else {
+ extmark_set(curbuf,
+ undo_info.data.update.ns_id,
+ undo_info.data.update.mark_id,
+ undo_info.data.update.lnum,
+ undo_info.data.update.col,
+ kExtmarkNoUndo);
+ }
+ // extmark_del
+ } else if (undo_info.type == kExtmarkDel) {
+ if (undo) {
+ extmark_set(curbuf,
+ undo_info.data.set.ns_id,
+ undo_info.data.set.mark_id,
+ undo_info.data.set.lnum,
+ undo_info.data.set.col,
+ kExtmarkNoUndo);
+ // Redo
+ } else {
+ extmark_del(curbuf,
+ undo_info.data.set.ns_id,
+ undo_info.data.set.mark_id,
+ kExtmarkNoUndo);
+ }
+ }
+}
+
+// undo/redo an kExtmarkMove operation
+static void apply_undo_move(ExtmarkUndoObject undo_info, bool undo)
+{
+ // 3 calls are required , see comment in function do_move (ex_cmds.c)
+ linenr_T line1 = undo_info.data.move.line1;
+ linenr_T line2 = undo_info.data.move.line2;
+ linenr_T last_line = undo_info.data.move.last_line;
+ linenr_T dest = undo_info.data.move.dest;
+ linenr_T num_lines = undo_info.data.move.num_lines;
+ linenr_T extra = undo_info.data.move.extra;
+
+ if (undo) {
+ if (dest >= line2) {
+ extmark_adjust(curbuf, dest - num_lines + 1, dest,
+ last_line - dest + num_lines - 1, 0L, kExtmarkNoUndo,
+ true);
+ extmark_adjust(curbuf, dest - line2, dest - line1,
+ dest - line2, 0L, kExtmarkNoUndo, false);
+ } else {
+ extmark_adjust(curbuf, line1-num_lines, line2-num_lines,
+ last_line - (line1-num_lines), 0L, kExtmarkNoUndo, true);
+ extmark_adjust(curbuf, (line1-num_lines) + 1, (line2-num_lines) + 1,
+ -num_lines, 0L, kExtmarkNoUndo, false);
+ }
+ extmark_adjust(curbuf, last_line, last_line + num_lines - 1,
+ line1 - last_line, 0L, kExtmarkNoUndo, true);
+ // redo
+ } else {
+ extmark_adjust(curbuf, line1, line2,
+ last_line - line2, 0L, kExtmarkNoUndo, true);
+ if (dest >= line2) {
+ extmark_adjust(curbuf, line2 + 1, dest,
+ -num_lines, 0L, kExtmarkNoUndo, false);
+ } else {
+ extmark_adjust(curbuf, dest + 1, line1 - 1,
+ num_lines, 0L, kExtmarkNoUndo, false);
+ }
+ extmark_adjust(curbuf, last_line - num_lines + 1, last_line,
+ -(last_line - dest - extra), 0L, kExtmarkNoUndo, true);
+ }
+}
+
+
+/// Get the column position for EOL on a line
+///
+/// If the lnum doesn't exist, returns 0
+colnr_T extmark_eol_col(buf_T *buf, linenr_T lnum)
+{
+ if (lnum > buf->b_ml.ml_line_count) {
+ return 0;
+ }
+ return (colnr_T)STRLEN(ml_get_buf(buf, lnum, false)) + 1;
+}
+
+
+// Adjust columns and rows for extmarks
+// based off mark_col_adjust in mark.c
+// returns true if something was moved otherwise false
+static bool extmark_col_adjust_impl(buf_T *buf, linenr_T lnum,
+ colnr_T mincol, long lnum_amount,
+ bool for_delete,
+ long update_col)
+{
+ bool marks_exist = false;
+
+ ExtmarkLine *extmarkline = extmarkline_ref(buf, lnum, false);
+ if (!extmarkline) {
+ return false;
+ }
+
+ FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, mincol, MAXCOL, {
+ marks_exist = true;
+
+ // Calculate desired col amount where the adjustment should take place
+ // (not taking) eol into account
+ long col_amount;
+ if (for_delete) {
+ if (extmark->col < update_col) {
+ // When mark inside range
+ colnr_T start_effected_range = mincol - 1;
+ col_amount = -(extmark->col - start_effected_range);
+ } else {
+ // Mark outside of range
+ // -1 because a delete of width 0 should still move marks
+ col_amount = -(update_col - mincol) - 1;
+ }
+ } else {
+ // for anything other than deletes
+ col_amount = update_col;
+ }
+
+ // No update required for this guy
+ if (col_amount == 0 && lnum_amount == 0) {
+ continue;
+ }
+
+ // Set mark to start of line
+ if (col_amount < 0
+ && extmark->col <= (colnr_T)-col_amount) {
+ extmark_update(extmark, buf, extmark->ns_id, extmark->mark_id,
+ extmarkline->lnum + lnum_amount,
+ 1, kExtmarkNoUndo, &mitr);
+ // Update the mark
+ } else {
+ // Note: The undo is handled by u_extmark_col_adjust, NoUndo here
+ extmark_update(extmark, buf, extmark->ns_id, extmark->mark_id,
+ extmarkline->lnum + lnum_amount,
+ extmark->col + (colnr_T)col_amount, kExtmarkNoUndo, &mitr);
+ }
+ })
+
+ if (kb_size(&extmarkline->items) == 0) {
+ kb_del(extmarklines, &buf->b_extlines, extmarkline);
+ extmarkline_free(extmarkline);
+ }
+
+ return marks_exist;
+}
+
+// Adjust columns and rows for extmarks
+//
+// based off mark_col_adjust in mark.c
+// use extmark_col_adjust_impl to move columns by inserting
+// Doesn't take the eol into consideration (possible to put marks in invalid
+// positions)
+void extmark_col_adjust(buf_T *buf, linenr_T lnum,
+ colnr_T mincol, long lnum_amount,
+ long col_amount, ExtmarkOp undo)
+{
+ assert(col_amount > INT_MIN && col_amount <= INT_MAX);
+
+ bool marks_moved = extmark_col_adjust_impl(buf, lnum, mincol, lnum_amount,
+ false, col_amount);
+
+ marks_moved |= bufhl_mark_col_adjust(buf, lnum, mincol,
+ lnum_amount, col_amount);
+
+ if (undo == kExtmarkUndo && marks_moved) {
+ u_extmark_col_adjust(buf, lnum, mincol, lnum_amount, col_amount);
+ }
+}
+
+// Adjust marks after a delete on a line
+//
+// Automatically readjusts to take the eol into account
+// TODO(timeyyy): change mincol to be for the mark to be copied, not moved
+//
+// @param mincol First column that needs to be moved (start of delete range) + 1
+// @param endcol Last column which needs to be copied (end of delete range + 1)
+void extmark_col_adjust_delete(buf_T *buf, linenr_T lnum,
+ colnr_T mincol, colnr_T endcol,
+ ExtmarkOp undo, int _eol)
+{
+ colnr_T start_effected_range = mincol;
+
+ bool marks_moved;
+ if (undo == kExtmarkUndo) {
+ // Copy marks that would be effected by delete
+ // -1 because we need to restore if a mark existed at the start pos
+ u_extmark_copy(buf, 0, lnum, start_effected_range, lnum, endcol);
+ }
+
+ marks_moved = extmark_col_adjust_impl(buf, lnum, mincol, 0,
+ true, (long)endcol);
+
+ marks_moved |= bufhl_mark_col_adjust(buf, lnum, endcol, 0, mincol-(endcol+1));
+ // Deletes at the end of the line have different behaviour than the normal
+ // case when deleted.
+ // Cleanup any marks that are floating beyond the end of line.
+ // we allow this to be passed in as well because the buffer may have already
+ // been mutated.
+ int eol = _eol;
+ if (!eol) {
+ eol = extmark_eol_col(buf, lnum);
+ }
+ FOR_ALL_EXTMARKS(buf, 1, lnum, eol, lnum, -1, {
+ extmark_update(extmark, buf, extmark->ns_id, extmark->mark_id,
+ extmarkline->lnum, (colnr_T)eol, kExtmarkNoUndo, &mitr);
+ })
+
+ // Record the undo for the actual move
+ if (marks_moved && undo == kExtmarkUndo) {
+ u_extmark_col_adjust_delete(buf, lnum, mincol, endcol, eol);
+ }
+}
+
+// Adjust extmark row for inserted/deleted rows (columns stay fixed).
+void extmark_adjust(buf_T *buf,
+ linenr_T line1,
+ linenr_T line2,
+ long amount,
+ long amount_after,
+ ExtmarkOp undo,
+ bool end_temp)
+{
+ ExtmarkLine *_extline;
+
+ // btree needs to be kept ordered to work, so far only :move requires this
+ // 2nd call with end_temp = true unpack the lines from the temp position
+ if (end_temp && amount < 0) {
+ for (size_t i = 0; i < kv_size(buf->b_extmark_move_space); i++) {
+ _extline = kv_A(buf->b_extmark_move_space, i);
+ _extline->lnum += amount;
+ kb_put(extmarklines, &buf->b_extlines, _extline);
+ }
+ kv_size(buf->b_extmark_move_space) = 0;
+ return;
+ }
+
+ bool marks_exist = false;
+ linenr_T *lp;
+
+ linenr_T adj_start = line1;
+ if (amount == MAXLNUM) {
+ // Careful! marks from deleted region can end up on en extisting extmarkline
+ // that is goinig to be adjusted to the target position.
+ linenr_T join_num = line1 - amount_after;
+ ExtmarkLine *joinline = (join_num > line2
+ ? extmarkline_ref(buf, join_num, false) : NULL);
+
+ // extmark_adjust is already redoable, the copy should only be for undo
+ marks_exist = extmark_copy_and_place(curbuf,
+ line1, 1,
+ line2, MAXCOL,
+ line1, 1,
+ kExtmarkUndoNoRedo, true, joinline);
+ adj_start = line2+1;
+ }
+ FOR_ALL_EXTMARKLINES(buf, adj_start, MAXLNUM, {
+ marks_exist = true;
+ lp = &(extmarkline->lnum);
+ if (*lp <= line2) {
+ // 1st call with end_temp = true, store the lines in a temp position
+ if (end_temp && amount > 0) {
+ kb_del_itr_extmarklines(&buf->b_extlines, &itr);
+ kv_push(buf->b_extmark_move_space, extmarkline);
+ }
+
+ *lp += amount;
+ } else if (amount_after && *lp > line2) {
+ *lp += amount_after;
+ }
+ })
+
+ if (undo == kExtmarkUndo && marks_exist) {
+ u_extmark_adjust(buf, line1, line2, amount, amount_after);
+ }
+}
+
+/// Range points to copy
+///
+/// if part of a larger iteration we can't delete, then the caller
+/// must check for empty lines.
+bool extmark_copy_and_place(buf_T *buf,
+ linenr_T l_lnum, colnr_T l_col,
+ linenr_T u_lnum, colnr_T u_col,
+ linenr_T p_lnum, colnr_T p_col,
+ ExtmarkOp undo, bool delete,
+ ExtmarkLine *destline)
+
+{
+ bool marks_moved = false;
+ if (undo == kExtmarkUndo || undo == kExtmarkUndoNoRedo) {
+ // Copy marks that would be effected by delete
+ u_extmark_copy(buf, 0, l_lnum, l_col, u_lnum, u_col);
+ }
+
+ // Move extmarks to their final position
+ // Careful: if we move items within the same line, we might change order of
+ // marks within the same extmarkline. Too keep it simple, first delete all
+ // items from the extmarkline and put them back in the right order.
+ FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, {
+ kvec_t(Extmark) temp_space = KV_INITIAL_VALUE;
+ bool same_line = extmarkline == destline;
+ FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items,
+ (extmarkline->lnum > l_lnum) ? 0 : l_col,
+ (extmarkline->lnum < u_lnum) ? MAXCOL : u_col, {
+ if (!destline) {
+ destline = extmarkline_ref(buf, p_lnum, true);
+ same_line = extmarkline == destline;
+ }
+ marks_moved = true;
+ if (!same_line) {
+ extmark_put(p_col, extmark->mark_id, destline, extmark->ns_id);
+ ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns,
+ extmark->ns_id);
+ pmap_put(uint64_t)(ns_obj->map, extmark->mark_id, destline);
+ } else {
+ kv_push(temp_space, *extmark);
+ }
+ // Delete old mark
+ kb_del_itr(markitems, &extmarkline->items, &mitr);
+ })
+ if (same_line) {
+ for (size_t i = 0; i < kv_size(temp_space); i++) {
+ Extmark mark = kv_A(temp_space, i);
+ extmark_put(p_col, mark.mark_id, extmarkline, mark.ns_id);
+ }
+ kv_destroy(temp_space);
+ } else if (delete && kb_size(&extmarkline->items) == 0) {
+ kb_del_itr(extmarklines, &buf->b_extlines, &itr);
+ extmarkline_free(extmarkline);
+ }
+ })
+
+ // Record the undo for the actual move
+ if (marks_moved && undo == kExtmarkUndo) {
+ u_extmark_copy_place(buf, l_lnum, l_col, u_lnum, u_col, p_lnum, p_col);
+ }
+
+ return marks_moved;
+}
+
+// Get reference to line in kbtree_t, allocating it if neccessary.
+ExtmarkLine *extmarkline_ref(buf_T *buf, linenr_T lnum, bool put)
+{
+ kbtree_t(extmarklines) *b = &buf->b_extlines;
+ ExtmarkLine t, **pp;
+ t.lnum = lnum;
+
+ pp = kb_get(extmarklines, b, &t);
+ if (!pp) {
+ if (!put) {
+ return NULL;
+ }
+ ExtmarkLine *p = xcalloc(sizeof(ExtmarkLine), 1);
+ p->lnum = lnum;
+ // p->items zero initialized
+ kb_put(extmarklines, b, p);
+ return p;
+ }
+ // Return existing
+ return *pp;
+}
+
+void extmarkline_free(ExtmarkLine *extmarkline)
+{
+ kb_destroy(markitems, (&extmarkline->items));
+ xfree(extmarkline);
+}
+
+/// Put an extmark into a line,
+///
+/// caller must ensure combination of id and ns_id isn't in use.
+void extmark_put(colnr_T col, uint64_t id,
+ ExtmarkLine *extmarkline, uint64_t ns)
+{
+ Extmark t;
+ t.col = col;
+ t.mark_id = id;
+ t.line = extmarkline;
+ t.ns_id = ns;
+
+ kbtree_t(markitems) *b = &(extmarkline->items);
+ // kb_put requries the key to not be there
+ assert(!kb_getp(markitems, b, &t));
+
+ kb_put(markitems, b, t);
+}
+
+
diff --git a/src/nvim/mark_extended.h b/src/nvim/mark_extended.h
new file mode 100644
index 0000000000..ee1da26875
--- /dev/null
+++ b/src/nvim/mark_extended.h
@@ -0,0 +1,282 @@
+#ifndef NVIM_MARK_EXTENDED_H
+#define NVIM_MARK_EXTENDED_H
+
+#include "nvim/mark_extended_defs.h"
+#include "nvim/buffer_defs.h" // for buf_T
+
+
+// Macro Documentation: FOR_ALL_?
+// Search exclusively using the range values given.
+// Use MAXCOL/MAXLNUM for the start and end of the line/col.
+// The ns parameter: Unless otherwise stated, this is only a starting point
+// for the btree to searched in, the results being itterated over will
+// still contain extmarks from other namespaces.
+
+// see FOR_ALL_? for documentation
+#define FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, code)\
+ kbitr_t(extmarklines) itr;\
+ ExtmarkLine t;\
+ t.lnum = l_lnum;\
+ if (!kb_itr_get(extmarklines, &buf->b_extlines, &t, &itr)) { \
+ kb_itr_next(extmarklines, &buf->b_extlines, &itr);\
+ }\
+ ExtmarkLine *extmarkline;\
+ for (; kb_itr_valid(&itr); kb_itr_next(extmarklines, \
+ &buf->b_extlines, &itr)) { \
+ extmarkline = kb_itr_key(&itr);\
+ if (extmarkline->lnum > u_lnum) { \
+ break;\
+ }\
+ code;\
+ }
+
+// see FOR_ALL_? for documentation
+#define FOR_ALL_EXTMARKLINES_PREV(buf, l_lnum, u_lnum, code)\
+ kbitr_t(extmarklines) itr;\
+ ExtmarkLine t;\
+ t.lnum = u_lnum;\
+ if (!kb_itr_get(extmarklines, &buf->b_extlines, &t, &itr)) { \
+ kb_itr_prev(extmarklines, &buf->b_extlines, &itr);\
+ }\
+ ExtmarkLine *extmarkline;\
+ for (; kb_itr_valid(&itr); kb_itr_prev(extmarklines, \
+ &buf->b_extlines, &itr)) { \
+ extmarkline = kb_itr_key(&itr);\
+ if (extmarkline->lnum < l_lnum) { \
+ break;\
+ }\
+ code;\
+ }
+
+// see FOR_ALL_? for documentation
+#define FOR_ALL_EXTMARKS(buf, ns, l_lnum, l_col, u_lnum, u_col, code)\
+ kbitr_t(markitems) mitr;\
+ Extmark mt;\
+ mt.ns_id = ns;\
+ mt.mark_id = 0;\
+ mt.line = NULL;\
+ FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, { \
+ mt.col = (extmarkline->lnum != l_lnum) ? MINCOL : l_col;\
+ if (!kb_itr_get(markitems, &extmarkline->items, mt, &mitr)) { \
+ kb_itr_next(markitems, &extmarkline->items, &mitr);\
+ } \
+ Extmark *extmark;\
+ for (; \
+ kb_itr_valid(&mitr); \
+ kb_itr_next(markitems, &extmarkline->items, &mitr)) { \
+ extmark = &kb_itr_key(&mitr);\
+ if (extmark->line->lnum == u_lnum \
+ && extmark->col > u_col) { \
+ break;\
+ }\
+ code;\
+ }\
+ })
+
+
+// see FOR_ALL_? for documentation
+#define FOR_ALL_EXTMARKS_PREV(buf, ns, l_lnum, l_col, u_lnum, u_col, code)\
+ kbitr_t(markitems) mitr;\
+ Extmark mt;\
+ mt.mark_id = sizeof(uint64_t);\
+ mt.ns_id = ns;\
+ FOR_ALL_EXTMARKLINES_PREV(buf, l_lnum, u_lnum, { \
+ mt.col = (extmarkline->lnum != u_lnum) ? MAXCOL : u_col;\
+ if (!kb_itr_get(markitems, &extmarkline->items, mt, &mitr)) { \
+ kb_itr_prev(markitems, &extmarkline->items, &mitr);\
+ } \
+ Extmark *extmark;\
+ for (; \
+ kb_itr_valid(&mitr); \
+ kb_itr_prev(markitems, &extmarkline->items, &mitr)) { \
+ extmark = &kb_itr_key(&mitr);\
+ if (extmark->line->lnum == l_lnum \
+ && extmark->col < l_col) { \
+ break;\
+ }\
+ code;\
+ }\
+ })
+
+
+#define FOR_ALL_EXTMARKS_IN_LINE(items, l_col, u_col, code)\
+ kbitr_t(markitems) mitr;\
+ Extmark mt;\
+ mt.ns_id = 0;\
+ mt.mark_id = 0;\
+ mt.line = NULL;\
+ mt.col = l_col;\
+ colnr_T extmarkline_u_col = u_col;\
+ if (!kb_itr_get(markitems, &items, mt, &mitr)) { \
+ kb_itr_next(markitems, &items, &mitr);\
+ } \
+ Extmark *extmark;\
+ for (; kb_itr_valid(&mitr); kb_itr_next(markitems, &items, &mitr)) { \
+ extmark = &kb_itr_key(&mitr);\
+ if (extmark->col > extmarkline_u_col) { \
+ break;\
+ }\
+ code;\
+ }
+
+
+typedef struct ExtmarkNs { // For namespacing extmarks
+ PMap(uint64_t) *map; // For fast lookup
+ uint64_t free_id; // For automatically assigning id's
+} ExtmarkNs;
+
+
+typedef kvec_t(Extmark *) ExtmarkArray;
+
+
+// Undo/redo extmarks
+
+typedef enum {
+ kExtmarkNOOP, // Extmarks shouldn't be moved
+ kExtmarkUndo, // Operation should be reversable/undoable
+ kExtmarkNoUndo, // Operation should not be reversable
+ kExtmarkUndoNoRedo, // Operation should be undoable, but not redoable
+} ExtmarkOp;
+
+
+// adjust line numbers only, corresponding to mark_adjust call
+typedef struct {
+ linenr_T line1;
+ linenr_T line2;
+ long amount;
+ long amount_after;
+} Adjust;
+
+// adjust columns after split/join line, like mark_col_adjust
+typedef struct {
+ linenr_T lnum;
+ colnr_T mincol;
+ long col_amount;
+ long lnum_amount;
+} ColAdjust;
+
+// delete the columns between mincol and endcol
+typedef struct {
+ linenr_T lnum;
+ colnr_T mincol;
+ colnr_T endcol;
+ int eol;
+} ColAdjustDelete;
+
+// adjust linenumbers after :move operation
+typedef struct {
+ linenr_T line1;
+ linenr_T line2;
+ linenr_T last_line;
+ linenr_T dest;
+ linenr_T num_lines;
+ linenr_T extra;
+} AdjustMove;
+
+// TODO(bfredl): reconsider if we really should track mark creation/updating
+// itself, these are not really "edit" operation.
+// extmark was created
+typedef struct {
+ uint64_t ns_id;
+ uint64_t mark_id;
+ linenr_T lnum;
+ colnr_T col;
+} ExtmarkSet;
+
+// extmark was updated
+typedef struct {
+ uint64_t ns_id;
+ uint64_t mark_id;
+ linenr_T old_lnum;
+ colnr_T old_col;
+ linenr_T lnum;
+ colnr_T col;
+} ExtmarkUpdate;
+
+// copied mark before deletion (as operation is destructive)
+typedef struct {
+ uint64_t ns_id;
+ uint64_t mark_id;
+ linenr_T lnum;
+ colnr_T col;
+} ExtmarkCopy;
+
+// also used as part of :move operation? probably can be simplified to one
+// event.
+typedef struct {
+ linenr_T l_lnum;
+ colnr_T l_col;
+ linenr_T u_lnum;
+ colnr_T u_col;
+ linenr_T p_lnum;
+ colnr_T p_col;
+} ExtmarkCopyPlace;
+
+// extmark was cleared.
+// TODO(bfredl): same reconsideration as for ExtmarkSet/ExtmarkUpdate
+typedef struct {
+ uint64_t ns_id;
+ linenr_T l_lnum;
+ linenr_T u_lnum;
+} ExtmarkClear;
+
+
+typedef enum {
+ kLineAdjust,
+ kColAdjust,
+ kColAdjustDelete,
+ kAdjustMove,
+ kExtmarkSet,
+ kExtmarkDel,
+ kExtmarkUpdate,
+ kExtmarkCopy,
+ kExtmarkCopyPlace,
+ kExtmarkClear,
+} UndoObjectType;
+
+// TODO(bfredl): reduce the number of undo action types
+struct undo_object {
+ UndoObjectType type;
+ union {
+ Adjust adjust;
+ ColAdjust col_adjust;
+ ColAdjustDelete col_adjust_delete;
+ AdjustMove move;
+ ExtmarkSet set;
+ ExtmarkUpdate update;
+ ExtmarkCopy copy;
+ ExtmarkCopyPlace copy_place;
+ ExtmarkClear clear;
+ } data;
+};
+
+
+// For doing move of extmarks in substitutions
+typedef struct {
+ lpos_T startpos;
+ lpos_T endpos;
+ linenr_T lnum;
+ int sublen;
+} ExtmarkSubSingle;
+
+// For doing move of extmarks in substitutions
+typedef struct {
+ lpos_T startpos;
+ lpos_T endpos;
+ linenr_T lnum;
+ linenr_T newline_in_pat;
+ linenr_T newline_in_sub;
+ linenr_T lnum_added;
+ lpos_T cm_start; // start of the match
+ lpos_T cm_end; // end of the match
+ int eol; // end of the match
+} ExtmarkSubMulti;
+
+typedef kvec_t(ExtmarkSubSingle) extmark_sub_single_vec_t;
+typedef kvec_t(ExtmarkSubMulti) extmark_sub_multi_vec_t;
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "mark_extended.h.generated.h"
+#endif
+
+#endif // NVIM_MARK_EXTENDED_H
diff --git a/src/nvim/mark_extended_defs.h b/src/nvim/mark_extended_defs.h
new file mode 100644
index 0000000000..565c599d06
--- /dev/null
+++ b/src/nvim/mark_extended_defs.h
@@ -0,0 +1,54 @@
+#ifndef NVIM_MARK_EXTENDED_DEFS_H
+#define NVIM_MARK_EXTENDED_DEFS_H
+
+#include "nvim/pos.h" // for colnr_T
+#include "nvim/map.h" // for uint64_t
+#include "nvim/lib/kbtree.h"
+#include "nvim/lib/kvec.h"
+
+struct ExtmarkLine;
+
+typedef struct Extmark
+{
+ uint64_t ns_id;
+ uint64_t mark_id;
+ struct ExtmarkLine *line;
+ colnr_T col;
+} Extmark;
+
+
+// We only need to compare columns as rows are stored in a different tree.
+// Marks are ordered by: position, namespace, mark_id
+// This improves moving marks but slows down all other use cases (searches)
+static inline int extmark_cmp(Extmark a, Extmark b)
+{
+ int cmp = kb_generic_cmp(a.col, b.col);
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = kb_generic_cmp(a.ns_id, b.ns_id);
+ if (cmp != 0) {
+ return cmp;
+ }
+ return kb_generic_cmp(a.mark_id, b.mark_id);
+}
+
+
+#define markitems_cmp(a, b) (extmark_cmp((a), (b)))
+KBTREE_INIT(markitems, Extmark, markitems_cmp, 10)
+
+typedef struct ExtmarkLine
+{
+ linenr_T lnum;
+ kbtree_t(markitems) items;
+} ExtmarkLine;
+
+#define EXTMARKLINE_CMP(a, b) (kb_generic_cmp((a)->lnum, (b)->lnum))
+KBTREE_INIT(extmarklines, ExtmarkLine *, EXTMARKLINE_CMP, 10)
+
+
+typedef struct undo_object ExtmarkUndoObject;
+typedef kvec_t(ExtmarkUndoObject) extmark_undo_vec_t;
+
+
+#endif // NVIM_MARK_EXTENDED_DEFS_H
diff --git a/src/nvim/memline.c b/src/nvim/memline.c
index 15dd2767a2..e5ba17a0a7 100644
--- a/src/nvim/memline.c
+++ b/src/nvim/memline.c
@@ -523,9 +523,9 @@ void ml_open_file(buf_T *buf)
}
}
- if (mfp->mf_fname == NULL) { /* Failed! */
- need_wait_return = TRUE; /* call wait_return later */
- ++no_wait_return;
+ if (*p_dir != NUL && mfp->mf_fname == NULL) {
+ need_wait_return = true; // call wait_return later
+ no_wait_return++;
(void)EMSG2(_(
"E303: Unable to open swap file for \"%s\", recovery impossible"),
buf_spname(buf) != NULL ? buf_spname(buf) : buf->b_fname);
@@ -540,7 +540,7 @@ void ml_open_file(buf_T *buf)
/// file, or reading into an existing buffer, create a swap file now.
///
/// @param newfile reading file into new buffer
-void check_need_swap(int newfile)
+void check_need_swap(bool newfile)
{
int old_msg_silent = msg_silent; // might be reset by an E325 message
msg_silent = 0; // If swap dialog prompts for input, user needs to see it!
@@ -738,10 +738,10 @@ static void add_b0_fenc(ZERO_BL *b0p, buf_T *buf)
}
-/*
- * Try to recover curbuf from the .swp file.
- */
-void ml_recover(void)
+/// Try to recover curbuf from the .swp file.
+/// @param checkext If true, check the extension and detect whether it is a
+/// swap file.
+void ml_recover(bool checkext)
{
buf_T *buf = NULL;
memfile_T *mfp = NULL;
@@ -785,7 +785,7 @@ void ml_recover(void)
if (fname == NULL) /* When there is no file name */
fname = (char_u *)"";
len = (int)STRLEN(fname);
- if (len >= 4
+ if (checkext && len >= 4
&& STRNICMP(fname + len - 4, ".s", 2) == 0
&& vim_strchr((char_u *)"abcdefghijklmnopqrstuvw",
TOLOWER_ASC(fname[len - 2])) != NULL
@@ -937,8 +937,9 @@ void ml_recover(void)
*/
if (directly) {
expand_env(b0p->b0_fname, NameBuff, MAXPATHL);
- if (setfname(curbuf, NameBuff, NULL, TRUE) == FAIL)
+ if (setfname(curbuf, NameBuff, NULL, true) == FAIL) {
goto theend;
+ }
}
home_replace(NULL, mfp->mf_fname, NameBuff, MAXPATHL, TRUE);
@@ -1375,7 +1376,9 @@ recover_names (
if (curbuf->b_ml.ml_mfp != NULL
&& (p = curbuf->b_ml.ml_mfp->mf_fname) != NULL) {
for (int i = 0; i < num_files; i++) {
- if (path_full_compare(p, files[i], true) & kEqualFiles) {
+ // Do not expand wildcards, on Windows would try to expand
+ // "%tmp%" in "%tmp%file"
+ if (path_full_compare(p, files[i], true, false) & kEqualFiles) {
// Remove the name from files[i]. Move further entries
// down. When the array becomes empty free it here, since
// FreeWild() won't be called below.
@@ -1435,7 +1438,7 @@ recover_names (
* Append the full path to name with path separators made into percent
* signs, to dir. An unnamed buffer is handled as "" (<currentdir>/"")
*/
-static char *make_percent_swname(const char *dir, char *name)
+char *make_percent_swname(const char *dir, char *name)
FUNC_ATTR_NONNULL_ARG(1)
{
char *d = NULL;
@@ -1927,6 +1930,7 @@ int ml_append_buf(
colnr_T len, // length of new line, including NUL, or 0
bool newfile // flag, see above
)
+ FUNC_ATTR_NONNULL_ARG(1)
{
if (buf->b_ml.ml_mfp == NULL)
return FAIL;
diff --git a/src/nvim/memory.c b/src/nvim/memory.c
index 64aae71433..9bc6b23ce3 100644
--- a/src/nvim/memory.c
+++ b/src/nvim/memory.c
@@ -693,6 +693,8 @@ void free_all_mem(void)
clear_hl_tables(false);
list_free_log();
+
+ check_quickfix_busy();
}
#endif
diff --git a/src/nvim/message.c b/src/nvim/message.c
index 30e906cd5f..067f1c3283 100644
--- a/src/nvim/message.c
+++ b/src/nvim/message.c
@@ -233,12 +233,15 @@ void msg_multiline_attr(const char *s, int attr, bool check_int)
{
const char *next_spec = s;
- while (next_spec != NULL && (!check_int || !got_int)) {
+ while (next_spec != NULL) {
+ if (check_int && got_int) {
+ return;
+ }
next_spec = strpbrk(s, "\t\n\r");
if (next_spec != NULL) {
// Printing all char that are before the char found by strpbrk
- msg_outtrans_len_attr((char_u *)s, next_spec - s, attr);
+ msg_outtrans_len_attr((const char_u *)s, next_spec - s, attr);
if (*next_spec != TAB) {
msg_clr_eos();
@@ -1375,7 +1378,7 @@ static void msg_home_replace_attr(char_u *fname, int attr)
/*
* Output 'len' characters in 'str' (including NULs) with translation
- * if 'len' is -1, output upto a NUL character.
+ * if 'len' is -1, output up to a NUL character.
* Use attributes 'attr'.
* Return the number of characters it takes on the screen.
*/
@@ -1384,12 +1387,12 @@ int msg_outtrans(char_u *str)
return msg_outtrans_attr(str, 0);
}
-int msg_outtrans_attr(char_u *str, int attr)
+int msg_outtrans_attr(const char_u *str, int attr)
{
return msg_outtrans_len_attr(str, (int)STRLEN(str), attr);
}
-int msg_outtrans_len(char_u *str, int len)
+int msg_outtrans_len(const char_u *str, int len)
{
return msg_outtrans_len_attr(str, len, 0);
}
@@ -1402,7 +1405,7 @@ char_u *msg_outtrans_one(char_u *p, int attr)
{
int l;
- if (has_mbyte && (l = (*mb_ptr2len)(p)) > 1) {
+ if ((l = utfc_ptr2len(p)) > 1) {
msg_outtrans_len_attr(p, l, attr);
return p + l;
}
@@ -1410,7 +1413,7 @@ char_u *msg_outtrans_one(char_u *p, int attr)
return p + 1;
}
-int msg_outtrans_len_attr(char_u *msgstr, int len, int attr)
+int msg_outtrans_len_attr(const char_u *msgstr, int len, int attr)
{
int retval = 0;
const char *str = (const char *)msgstr;
@@ -1498,7 +1501,7 @@ void msg_make(char_u *arg)
}
}
-/// Output the string 'str' upto a NUL character.
+/// Output the string 'str' up to a NUL character.
/// Return the number of characters it takes on the screen.
///
/// If K_SPECIAL is encountered, then it is taken in conjunction with the
diff --git a/src/nvim/misc1.c b/src/nvim/misc1.c
index ab3520dd73..a871d424c6 100644
--- a/src/nvim/misc1.c
+++ b/src/nvim/misc1.c
@@ -30,7 +30,6 @@
#include "nvim/indent_c.h"
#include "nvim/buffer_updates.h"
#include "nvim/main.h"
-#include "nvim/mark.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
@@ -484,25 +483,39 @@ int plines_win_col(win_T *wp, linenr_T lnum, long column)
return lines;
}
+/// Get the number of screen lines lnum takes up. This takes care of
+/// both folds and topfill, and limits to the current window height.
+///
+/// @param[in] wp window line is in
+/// @param[in] lnum line number
+/// @param[out] nextp if not NULL, the line after a fold
+/// @param[out] foldedp if not NULL, whether lnum is on a fold
+/// @param[in] cache whether to use the window's cache for folds
+///
+/// @return the total number of screen lines
+int plines_win_full(win_T *wp, linenr_T lnum, linenr_T *const nextp,
+ bool *const foldedp, const bool cache)
+{
+ bool folded = hasFoldingWin(wp, lnum, NULL, nextp, cache, NULL);
+ if (foldedp) {
+ *foldedp = folded;
+ }
+ if (folded) {
+ return 1;
+ } else if (lnum == wp->w_topline) {
+ return plines_win_nofill(wp, lnum, true) + wp->w_topfill;
+ }
+ return plines_win(wp, lnum, true);
+}
+
int plines_m_win(win_T *wp, linenr_T first, linenr_T last)
{
int count = 0;
while (first <= last) {
- // Check if there are any really folded lines, but also included lines
- // that are maybe folded.
- linenr_T x = foldedCount(wp, first, NULL);
- if (x > 0) {
- ++count; /* count 1 for "+-- folded" line */
- first += x;
- } else {
- if (first == wp->w_topline) {
- count += plines_win_nofill(wp, first, true) + wp->w_topfill;
- } else {
- count += plines_win(wp, first, true);
- }
- first++;
- }
+ linenr_T next = first;
+ count += plines_win_full(wp, first, &next, NULL, false);
+ first = next + 1;
}
return count;
}
diff --git a/src/nvim/mouse.c b/src/nvim/mouse.c
index d0aa0653cb..deb7ee6342 100644
--- a/src/nvim/mouse.c
+++ b/src/nvim/mouse.c
@@ -508,31 +508,30 @@ static win_T *mouse_find_grid_win(int *gridp, int *rowp, int *colp)
return NULL;
}
-/*
- * setmouse() - switch mouse on/off depending on current mode and 'mouse'
- */
+/// Set UI mouse depending on current mode and 'mouse'.
+///
+/// Emits mouse_on/mouse_off UI event (unless 'mouse' is empty).
void setmouse(void)
{
- int checkfor;
-
ui_cursor_shape();
- /* be quick when mouse is off */
- if (*p_mouse == NUL)
+ // Be quick when mouse is off.
+ if (*p_mouse == NUL) {
return;
+ }
- if (VIsual_active)
+ int checkfor = MOUSE_NORMAL; // assume normal mode
+ if (VIsual_active) {
checkfor = MOUSE_VISUAL;
- else if (State == HITRETURN || State == ASKMORE || State == SETWSIZE)
+ } else if (State == HITRETURN || State == ASKMORE || State == SETWSIZE) {
checkfor = MOUSE_RETURN;
- else if (State & INSERT)
+ } else if (State & INSERT) {
checkfor = MOUSE_INSERT;
- else if (State & CMDLINE)
+ } else if (State & CMDLINE) {
checkfor = MOUSE_COMMAND;
- else if (State == CONFIRM || State == EXTERNCMD)
- checkfor = ' '; /* don't use mouse for ":confirm" or ":!cmd" */
- else
- checkfor = MOUSE_NORMAL; /* assume normal mode */
+ } else if (State == CONFIRM || State == EXTERNCMD) {
+ checkfor = ' '; // don't use mouse for ":confirm" or ":!cmd"
+ }
if (mouse_has(checkfor)) {
ui_call_mouse_on();
diff --git a/src/nvim/move.c b/src/nvim/move.c
index e6fee9999f..efbc548620 100644
--- a/src/nvim/move.c
+++ b/src/nvim/move.c
@@ -65,17 +65,10 @@ static void comp_botline(win_T *wp)
done = 0;
}
- for (; lnum <= wp->w_buffer->b_ml.ml_line_count; ++lnum) {
- int n;
+ for (; lnum <= wp->w_buffer->b_ml.ml_line_count; lnum++) {
linenr_T last = lnum;
- bool folded = hasFoldingWin(wp, lnum, NULL, &last, true, NULL);
- if (folded) {
- n = 1;
- } else if (lnum == wp->w_topline) {
- n = plines_win_nofill(wp, lnum, true) + wp->w_topfill;
- } else {
- n = plines_win(wp, lnum, true);
- }
+ bool folded;
+ int n = plines_win_full(wp, lnum, &last, &folded, true);
if (lnum <= wp->w_cursor.lnum && last >= wp->w_cursor.lnum) {
wp->w_cline_row = done;
wp->w_cline_height = n;
@@ -571,18 +564,14 @@ static void curs_rows(win_T *wp)
break;
wp->w_cline_row += wp->w_lines[i].wl_size;
} else {
- long fold_count = foldedCount(wp, lnum, NULL);
- if (fold_count) {
- lnum += fold_count;
- if (lnum > wp->w_cursor.lnum)
- break;
- ++wp->w_cline_row;
- } else if (lnum == wp->w_topline) {
- wp->w_cline_row += plines_win_nofill(wp, lnum++, true)
- + wp->w_topfill;
- } else {
- wp->w_cline_row += plines_win(wp, lnum++, true);
+ linenr_T last = lnum;
+ bool folded;
+ int n = plines_win_full(wp, lnum, &last, &folded, false);
+ lnum = last + 1;
+ if (folded && lnum > wp->w_cursor.lnum) {
+ break;
}
+ wp->w_cline_row += n;
}
}
@@ -593,18 +582,13 @@ static void curs_rows(win_T *wp)
|| (i < wp->w_lines_valid
&& (!wp->w_lines[i].wl_valid
|| wp->w_lines[i].wl_lnum != wp->w_cursor.lnum))) {
- if (wp->w_cursor.lnum == wp->w_topline)
- wp->w_cline_height = plines_win_nofill(wp, wp->w_cursor.lnum,
- true) + wp->w_topfill;
- else
- wp->w_cline_height = plines_win(wp, wp->w_cursor.lnum, true);
- wp->w_cline_folded = hasFoldingWin(wp, wp->w_cursor.lnum,
- NULL, NULL, true, NULL);
+ wp->w_cline_height = plines_win_full(wp, wp->w_cursor.lnum, NULL,
+ &wp->w_cline_folded, true);
} else if (i > wp->w_lines_valid) {
/* a line that is too long to fit on the last screen line */
wp->w_cline_height = 0;
- wp->w_cline_folded = hasFoldingWin(wp, wp->w_cursor.lnum,
- NULL, NULL, true, NULL);
+ wp->w_cline_folded = hasFoldingWin(wp, wp->w_cursor.lnum, NULL,
+ NULL, true, NULL);
} else {
wp->w_cline_height = wp->w_lines[i].wl_size;
wp->w_cline_folded = wp->w_lines[i].wl_folded;
@@ -646,12 +630,9 @@ static void validate_cheight(void)
{
check_cursor_moved(curwin);
if (!(curwin->w_valid & VALID_CHEIGHT)) {
- if (curwin->w_cursor.lnum == curwin->w_topline)
- curwin->w_cline_height = plines_nofill(curwin->w_cursor.lnum)
- + curwin->w_topfill;
- else
- curwin->w_cline_height = plines(curwin->w_cursor.lnum);
- curwin->w_cline_folded = hasFolding(curwin->w_cursor.lnum, NULL, NULL);
+ curwin->w_cline_height = plines_win_full(curwin, curwin->w_cursor.lnum,
+ NULL, &curwin->w_cline_folded,
+ true);
curwin->w_valid |= VALID_CHEIGHT;
}
}
diff --git a/src/nvim/normal.c b/src/nvim/normal.c
index e7e6d2b365..b9dbcc6805 100644
--- a/src/nvim/normal.c
+++ b/src/nvim/normal.c
@@ -1422,12 +1422,12 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
if (oap->motion_type == kMTLineWise) {
oap->inclusive = false;
} else if (oap->motion_type == kMTCharWise) {
- // If the motion already was characterwise, toggle "inclusive"
+ // If the motion already was charwise, toggle "inclusive"
oap->inclusive = !oap->inclusive;
}
oap->motion_type = kMTCharWise;
} else if (oap->motion_force == Ctrl_V) {
- // Change line- or characterwise motion into Visual block mode.
+ // Change line- or charwise motion into Visual block mode.
if (!VIsual_active) {
VIsual_active = true;
VIsual = oap->start;
@@ -1516,7 +1516,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
}
// In Select mode, a linewise selection is operated upon like a
- // characterwise selection.
+ // charwise selection.
// Special case: gH<Del> deletes the last line.
if (VIsual_select && VIsual_mode == 'V'
&& cap->oap->op_type != OP_DELETE) {
@@ -1553,9 +1553,11 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
if (!VIsual_active) {
if (hasFolding(oap->start.lnum, &oap->start.lnum, NULL))
oap->start.col = 0;
- if (hasFolding(curwin->w_cursor.lnum, NULL,
- &curwin->w_cursor.lnum))
+ if ((curwin->w_cursor.col > 0 || oap->inclusive)
+ && hasFolding(curwin->w_cursor.lnum, NULL,
+ &curwin->w_cursor.lnum)) {
curwin->w_cursor.col = (colnr_T)STRLEN(get_cursor_line_ptr());
+ }
}
oap->end = curwin->w_cursor;
curwin->w_cursor = oap->start;
@@ -3792,7 +3794,7 @@ find_decl (
valid = false;
(void)valid; // Avoid "dead assignment" warning.
t = searchit(curwin, curbuf, &curwin->w_cursor, NULL, FORWARD,
- pat, 1L, searchflags, RE_LAST, (linenr_T)0, NULL, NULL);
+ pat, 1L, searchflags, RE_LAST, NULL);
if (curwin->w_cursor.lnum >= old_pos.lnum) {
t = false; // match after start is failure too
}
@@ -3923,18 +3925,20 @@ static bool nv_screengo(oparg_T *oap, int dir, long dist)
n = ((linelen - width1 - 1) / width2 + 1) * width2 + width1;
else
n = width1;
- if (curwin->w_curswant > (colnr_T)n + 1)
- curwin->w_curswant -= ((curwin->w_curswant - n) / width2 + 1)
- * width2;
+ if (curwin->w_curswant >= n) {
+ curwin->w_curswant = n - 1;
+ }
}
while (dist--) {
if (dir == BACKWARD) {
- if ((long)curwin->w_curswant >= width2)
- /* move back within line */
+ if (curwin->w_curswant >= width1) {
+ // Move back within the line. This can give a negative value
+ // for w_curswant if width1 < width2 (with cpoptions+=n),
+ // which will get clipped to column 0.
curwin->w_curswant -= width2;
- else {
- /* to previous line */
+ } else {
+ // to previous line
if (curwin->w_cursor.lnum == 1) {
retval = false;
break;
@@ -3971,6 +3975,13 @@ static bool nv_screengo(oparg_T *oap, int dir, long dist)
}
curwin->w_cursor.lnum++;
curwin->w_curswant %= width2;
+ // Check if the cursor has moved below the number display
+ // when width1 < width2 (with cpoptions+=n). Subtract width2
+ // to get a negative value for w_curswant, which will get
+ // clipped to column 0.
+ if (curwin->w_curswant >= width1) {
+ curwin->w_curswant -= width2;
+ }
linelen = linetabsize(get_cursor_line_ptr());
}
}
@@ -4577,7 +4588,7 @@ static void nv_colon(cmdarg_T *cap)
nv_operator(cap);
} else {
if (cap->oap->op_type != OP_NOP) {
- // Using ":" as a movement is characterwise exclusive.
+ // Using ":" as a movement is charwise exclusive.
cap->oap->motion_type = kMTCharWise;
cap->oap->inclusive = false;
} else if (cap->count0 && !is_cmdkey) {
@@ -4678,9 +4689,8 @@ static void nv_ctrlo(cmdarg_T *cap)
}
}
-/*
- * CTRL-^ command, short for ":e #"
- */
+// CTRL-^ command, short for ":e #". Works even when the alternate buffer is
+// not named.
static void nv_hat(cmdarg_T *cap)
{
if (!checkclearopq(cap->oap))
@@ -4926,9 +4936,12 @@ static void nv_ident(cmdarg_T *cap)
/* put pattern in search history */
init_history();
add_to_history(HIST_SEARCH, (char_u *)buf, true, NUL);
- (void)normal_search(cap, cmdchar == '*' ? '/' : '?', (char_u *)buf, 0);
+ (void)normal_search(cap, cmdchar == '*' ? '/' : '?', (char_u *)buf, 0,
+ NULL);
} else {
+ g_tag_at_cursor = true;
do_cmdline_cmd(buf);
+ g_tag_at_cursor = false;
}
xfree(buf);
@@ -5125,14 +5138,10 @@ static void nv_right(cmdarg_T *cap)
break;
} else if (PAST_LINE) {
curwin->w_set_curswant = true;
- if (virtual_active())
+ if (virtual_active()) {
oneright();
- else {
- if (has_mbyte)
- curwin->w_cursor.col +=
- (*mb_ptr2len)(get_cursor_pos_ptr());
- else
- ++curwin->w_cursor.col;
+ } else {
+ curwin->w_cursor.col += (*mb_ptr2len)(get_cursor_pos_ptr());
}
}
}
@@ -5346,7 +5355,7 @@ static void nv_search(cmdarg_T *cap)
// When using 'incsearch' the cursor may be moved to set a different search
// start position.
- cap->searchbuf = getcmdline(cap->cmdchar, cap->count1, 0);
+ cap->searchbuf = getcmdline(cap->cmdchar, cap->count1, 0, true);
if (cap->searchbuf == NULL) {
clearop(oap);
@@ -5355,7 +5364,7 @@ static void nv_search(cmdarg_T *cap)
(void)normal_search(cap, cap->cmdchar, cap->searchbuf,
(cap->arg || !equalpos(save_cursor, curwin->w_cursor))
- ? 0 : SEARCH_MARK);
+ ? 0 : SEARCH_MARK, NULL);
}
/*
@@ -5365,14 +5374,15 @@ static void nv_search(cmdarg_T *cap)
static void nv_next(cmdarg_T *cap)
{
pos_T old = curwin->w_cursor;
- int i = normal_search(cap, 0, NULL, SEARCH_MARK | cap->arg);
+ int wrapped = false;
+ int i = normal_search(cap, 0, NULL, SEARCH_MARK | cap->arg, &wrapped);
- if (i == 1 && equalpos(old, curwin->w_cursor)) {
+ if (i == 1 && !wrapped && equalpos(old, curwin->w_cursor)) {
// Avoid getting stuck on the current cursor position, which can happen when
// an offset is given and the cursor is on the last char in the buffer:
// Repeat with count + 1.
cap->count1 += 1;
- (void)normal_search(cap, 0, NULL, SEARCH_MARK | cap->arg);
+ (void)normal_search(cap, 0, NULL, SEARCH_MARK | cap->arg, NULL);
cap->count1 -= 1;
}
}
@@ -5386,18 +5396,24 @@ static int normal_search(
cmdarg_T *cap,
int dir,
char_u *pat,
- int opt /* extra flags for do_search() */
+ int opt, // extra flags for do_search()
+ int *wrapped
)
{
int i;
+ searchit_arg_T sia;
cap->oap->motion_type = kMTCharWise;
cap->oap->inclusive = false;
cap->oap->use_reg_one = true;
curwin->w_set_curswant = true;
+ memset(&sia, 0, sizeof(sia));
i = do_search(cap->oap, dir, pat, cap->count1,
- opt | SEARCH_OPT | SEARCH_ECHO | SEARCH_MSG, NULL, NULL);
+ opt | SEARCH_OPT | SEARCH_ECHO | SEARCH_MSG, &sia);
+ if (wrapped != NULL) {
+ *wrapped = sia.sa_wrapped;
+ }
if (i == 0) {
clearop(cap->oap);
} else {
@@ -6356,8 +6372,8 @@ static void nv_visual(cmdarg_T *cap)
if (cap->cmdchar == Ctrl_Q)
cap->cmdchar = Ctrl_V;
- /* 'v', 'V' and CTRL-V can be used while an operator is pending to make it
- * characterwise, linewise, or blockwise. */
+ // 'v', 'V' and CTRL-V can be used while an operator is pending to make it
+ // charwise, linewise, or blockwise.
if (cap->oap->op_type != OP_NOP) {
motion_force = cap->oap->motion_force = cap->cmdchar;
finish_op = false; // operator doesn't finish now but later
@@ -6733,6 +6749,22 @@ static void nv_g_cmd(cmdarg_T *cap)
curwin->w_set_curswant = true;
break;
+ case 'M':
+ {
+ const char_u *const ptr = get_cursor_line_ptr();
+
+ oap->motion_type = kMTCharWise;
+ oap->inclusive = false;
+ i = (int)mb_string2cells_len(ptr, STRLEN(ptr));
+ if (cap->count0 > 0 && cap->count0 <= 100) {
+ coladvance((colnr_T)(i * cap->count0 / 100));
+ } else {
+ coladvance((colnr_T)(i / 2));
+ }
+ curwin->w_set_curswant = true;
+ }
+ break;
+
case '_':
/* "g_": to the last non-blank character in the line or <count> lines
* downward. */
@@ -6797,10 +6829,14 @@ static void nv_g_cmd(cmdarg_T *cap)
} else if (nv_screengo(oap, FORWARD, cap->count1 - 1) == false)
clearopbeep(oap);
} else {
+ if (cap->count1 > 1) {
+ // if it fails, let the cursor still move to the last char
+ cursor_down(cap->count1 - 1, false);
+ }
i = curwin->w_leftcol + curwin->w_width_inner - col_off - 1;
coladvance((colnr_T)i);
- /* Make sure we stick in this column. */
+ // Make sure we stick in this column.
validate_virtcol();
curwin->w_curswant = curwin->w_virtcol;
curwin->w_set_curswant = false;
@@ -7851,15 +7887,17 @@ static void nv_put_opt(cmdarg_T *cap, bool fix_indent)
cap->oap->regname = regname;
}
- /* When deleted a linewise Visual area, put the register as
- * lines to avoid it joined with the next line. When deletion was
- * characterwise, split a line when putting lines. */
- if (VIsual_mode == 'V')
+ // When deleted a linewise Visual area, put the register as
+ // lines to avoid it joined with the next line. When deletion was
+ // charwise, split a line when putting lines.
+ if (VIsual_mode == 'V') {
flags |= PUT_LINE;
- else if (VIsual_mode == 'v')
+ } else if (VIsual_mode == 'v') {
flags |= PUT_LINE_SPLIT;
- if (VIsual_mode == Ctrl_V && dir == FORWARD)
+ }
+ if (VIsual_mode == Ctrl_V && dir == FORWARD) {
flags |= PUT_LINE_FORWARD;
+ }
dir = BACKWARD;
if ((VIsual_mode != 'V'
&& curwin->w_cursor.col < curbuf->b_op_start.col)
diff --git a/src/nvim/ops.c b/src/nvim/ops.c
index 4f0c1b5cb9..294c65ca03 100644
--- a/src/nvim/ops.c
+++ b/src/nvim/ops.c
@@ -49,6 +49,7 @@
#include "nvim/undo.h"
#include "nvim/macros.h"
#include "nvim/window.h"
+#include "nvim/lib/kvec.h"
#include "nvim/os/input.h"
#include "nvim/os/time.h"
@@ -270,20 +271,21 @@ void shift_line(
int left,
int round,
int amount,
- int call_changed_bytes /* call changed_bytes() */
+ int call_changed_bytes // call changed_bytes()
)
{
int count;
int i, j;
int p_sw = get_sw_value(curbuf);
- count = get_indent(); /* get current indent */
+ count = get_indent(); // get current indent
- if (round) { /* round off indent */
- i = count / p_sw; /* number of p_sw rounded down */
- j = count % p_sw; /* extra spaces */
- if (j && left) /* first remove extra spaces */
- --amount;
+ if (round) { // round off indent
+ i = count / p_sw; // number of p_sw rounded down
+ j = count % p_sw; // extra spaces
+ if (j && left) { // first remove extra spaces
+ amount--;
+ }
if (left) {
i -= amount;
if (i < 0)
@@ -291,7 +293,7 @@ void shift_line(
} else
i += amount;
count = i * p_sw;
- } else { /* original vi indent */
+ } else { // original vi indent
if (left) {
count -= p_sw * amount;
if (count < 0)
@@ -300,11 +302,21 @@ void shift_line(
count += p_sw * amount;
}
- /* Set new indent */
- if (State & VREPLACE_FLAG)
- change_indent(INDENT_SET, count, FALSE, NUL, call_changed_bytes);
- else
+ // Set new indent
+ if (State & VREPLACE_FLAG) {
+ change_indent(INDENT_SET, count, false, NUL, call_changed_bytes);
+ } else {
(void)set_indent(count, call_changed_bytes ? SIN_CHANGED : 0);
+
+ colnr_T mincol = (curwin->w_cursor.col + 1) -p_sw;
+ colnr_T col_amount = left ? -p_sw : p_sw;
+ extmark_col_adjust(curbuf,
+ curwin->w_cursor.lnum,
+ mincol,
+ 0,
+ col_amount,
+ kExtmarkUndo);
+ }
}
/*
@@ -477,6 +489,10 @@ static void shift_block(oparg_T *oap, int amount)
State = oldstate;
curwin->w_cursor.col = oldcol;
p_ri = old_p_ri;
+
+ colnr_T col_amount = left ? -p_sw : p_sw;
+ extmark_col_adjust(curbuf, curwin->w_cursor.lnum,
+ curwin->w_cursor.col, 0, col_amount, kExtmarkUndo);
}
/*
@@ -621,10 +637,19 @@ void op_reindent(oparg_T *oap, Indenter how)
amount = how(); /* get the indent for this line */
if (amount >= 0 && set_indent(amount, SIN_UNDO)) {
- /* did change the indent, call changed_lines() later */
- if (first_changed == 0)
+ // did change the indent, call changed_lines() later
+ if (first_changed == 0) {
first_changed = curwin->w_cursor.lnum;
+ }
last_changed = curwin->w_cursor.lnum;
+
+ // Adjust extmarks
+ extmark_col_adjust(curbuf,
+ curwin->w_cursor.lnum,
+ 0, // mincol
+ 0, // lnum_amount
+ amount, // col_amount
+ kExtmarkUndo);
}
}
++curwin->w_cursor.lnum;
@@ -671,13 +696,15 @@ int get_expr_register(void)
{
char_u *new_line;
- new_line = getcmdline('=', 0L, 0);
- if (new_line == NULL)
+ new_line = getcmdline('=', 0L, 0, true);
+ if (new_line == NULL) {
return NUL;
- if (*new_line == NUL) /* use previous line */
+ }
+ if (*new_line == NUL) { // use previous line
xfree(new_line);
- else
+ } else {
set_expr_line(new_line);
+ }
return '=';
}
@@ -1558,6 +1585,7 @@ int op_delete(oparg_T *oap)
oap->end = curwin->w_cursor;
curwin->w_cursor = oap->start;
}
+ mb_adjust_opend(oap);
}
if (oap->line_count == 1) { /* delete characters within one line */
@@ -1627,10 +1655,35 @@ setmarks:
if (oap->motion_type == kMTBlockWise) {
curbuf->b_op_end.lnum = oap->end.lnum;
curbuf->b_op_end.col = oap->start.col;
- } else
+ } else {
curbuf->b_op_end = oap->start;
+ }
curbuf->b_op_start = oap->start;
+ // TODO(timeyyy): refactor: Move extended marks
+ // + 1 to change to buf mode,
+ // and + 1 because we only move marks after the deleted col
+ colnr_T mincol = oap->start.col + 1 + 1;
+ colnr_T endcol;
+ if (oap->motion_type == kMTBlockWise) {
+ // TODO(timeyyy): refactor extmark_col_adjust to take lnumstart, lnum_end ?
+ endcol = bd.end_vcol + 1;
+ for (lnum = curwin->w_cursor.lnum; lnum <= oap->end.lnum; lnum++) {
+ extmark_col_adjust_delete(curbuf, lnum, mincol, endcol,
+ kExtmarkUndo, 0);
+ }
+
+ // Delete characters within one line,
+ // The case with multiple lines is handled by do_join
+ } else if (oap->motion_type == kMTCharWise && oap->line_count == 1) {
+ // + 1 to change to buf mode, then plus 1 to fit function requirements
+ endcol = oap->end.col + 1 + 1;
+
+ lnum = curwin->w_cursor.lnum;
+ if (oap->is_VIsual == false) {
+ endcol = MAX(endcol - 1, mincol);
+ }
+ }
return OK;
}
@@ -2026,8 +2079,8 @@ bool swapchar(int op_type, pos_T *pos)
pos_T sp = curwin->w_cursor;
curwin->w_cursor = *pos;
- /* don't use del_char(), it also removes composing chars */
- del_bytes(utf_ptr2len(get_cursor_pos_ptr()), FALSE, FALSE);
+ // don't use del_char(), it also removes composing chars
+ del_bytes(utf_ptr2len(get_cursor_pos_ptr()), false, false);
ins_char(nc);
curwin->w_cursor = sp;
} else {
@@ -2100,8 +2153,9 @@ void op_insert(oparg_T *oap, long count1)
* values in "bd". */
if (u_save_cursor() == FAIL)
return;
- for (i = 0; i < bd.endspaces; i++)
+ for (i = 0; i < bd.endspaces; i++) {
ins_char(' ');
+ }
bd.textlen += bd.endspaces;
}
} else {
@@ -2219,6 +2273,10 @@ void op_insert(oparg_T *oap, long count1)
xfree(ins_text);
}
}
+ colnr_T col = oap->start.col;
+ for (linenr_T lnum = oap->start.lnum; lnum <= oap->end.lnum; lnum++) {
+ extmark_col_adjust(curbuf, lnum, col, 0, 1, kExtmarkUndo);
+ }
}
/*
@@ -2689,6 +2747,27 @@ static void do_autocmd_textyankpost(oparg_T *oap, yankreg_T *reg)
}
+static void extmarks_do_put(int dir,
+ size_t totlen,
+ MotionType y_type,
+ linenr_T lnum,
+ colnr_T col)
+{
+ // adjust extmarks
+ colnr_T col_amount = (colnr_T)(dir == FORWARD ? totlen-1 : totlen);
+ // Move extmark with char put
+ if (y_type == kMTCharWise) {
+ extmark_col_adjust(curbuf, lnum, col, 0, col_amount, kExtmarkUndo);
+ // Move extmark with blockwise put
+ } else if (y_type == kMTBlockWise) {
+ for (lnum = curbuf->b_op_start.lnum;
+ lnum <= curbuf->b_op_end.lnum;
+ lnum++) {
+ extmark_col_adjust(curbuf, lnum, col, 0, col_amount, kExtmarkUndo);
+ }
+ }
+}
+
/*
* Put contents of register "regname" into the text.
* Caller must check "regname" to be valid!
@@ -2703,8 +2782,8 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags)
char_u *oldp;
int yanklen;
size_t totlen = 0; // init for gcc
- linenr_T lnum;
- colnr_T col;
+ linenr_T lnum = 0;
+ colnr_T col = 0;
size_t i; // index in y_array[]
MotionType y_type;
size_t y_size;
@@ -3281,11 +3360,11 @@ error:
curbuf->b_op_start.lnum++;
}
// Skip mark_adjust when adding lines after the last one, there
- // can't be marks there. But still needed in diff mode.
+ // can't be marks there.
if (curbuf->b_op_start.lnum + (y_type == kMTCharWise) - 1 + nr_lines
- < curbuf->b_ml.ml_line_count || curwin->w_p_diff) {
+ < curbuf->b_ml.ml_line_count) {
mark_adjust(curbuf->b_op_start.lnum + (y_type == kMTCharWise),
- (linenr_T)MAXLNUM, nr_lines, 0L, false);
+ (linenr_T)MAXLNUM, nr_lines, 0L, false, kExtmarkUndo);
}
// note changed text for displaying and folding
@@ -3347,6 +3426,8 @@ end:
/* If the cursor is past the end of the line put it at the end. */
adjust_cursor_eol();
+
+ extmarks_do_put(dir, totlen, y_type, lnum, col);
}
/*
@@ -3689,7 +3770,10 @@ int do_join(size_t count,
if (insert_space && t > 0) {
curr = skipwhite(curr);
- if (*curr != ')' && currsize != 0 && endcurr1 != TAB
+ if (*curr != NUL
+ && *curr != ')'
+ && currsize != 0
+ && endcurr1 != TAB
&& (!has_format_option(FO_MBYTE_JOIN)
|| (utf_ptr2char(curr) < 0x100 && endcurr1 < 0x100))
&& (!has_format_option(FO_MBYTE_JOIN2)
@@ -3740,6 +3824,7 @@ int do_join(size_t count,
* column. This is not Vi compatible, but Vi deletes the marks, thus that
* should not really be a problem.
*/
+
for (t = (linenr_T)count - 1;; t--) {
cend -= currsize;
memmove(cend, curr, (size_t)currsize);
@@ -3751,12 +3836,18 @@ int do_join(size_t count,
// If deleting more spaces than adding, the cursor moves no more than
// what is added if it is inside these spaces.
const int spaces_removed = (int)((curr - curr_start) - spaces[t]);
+ linenr_T lnum = curwin->w_cursor.lnum + t;
+ colnr_T mincol = (colnr_T)0;
+ long lnum_amount = (linenr_T)-t;
+ long col_amount = (long)(cend - newp - spaces_removed);
+
+ mark_col_adjust(lnum, mincol, lnum_amount, col_amount, spaces_removed,
+ kExtmarkUndo);
- mark_col_adjust(curwin->w_cursor.lnum + t, (colnr_T)0, (linenr_T)-t,
- (long)(cend - newp - spaces_removed), spaces_removed);
if (t == 0) {
break;
}
+
curr = curr_start = ml_get((linenr_T)(curwin->w_cursor.lnum + t - 1));
if (remove_comments)
curr += comments[t - 1];
@@ -3764,6 +3855,7 @@ int do_join(size_t count,
curr = skipwhite(curr);
currsize = (int)STRLEN(curr);
}
+
ml_replace(curwin->w_cursor.lnum, newp, false);
if (setmark) {
@@ -4184,14 +4276,14 @@ format_lines(
if (next_leader_len > 0) {
(void)del_bytes(next_leader_len, false, false);
mark_col_adjust(curwin->w_cursor.lnum, (colnr_T)0, 0L,
- (long)-next_leader_len, 0);
+ (long)-next_leader_len, 0, kExtmarkNOOP);
} else if (second_indent > 0) { // the "leader" for FO_Q_SECOND
int indent = (int)getwhitecols_curline();
if (indent > 0) {
- (void)del_bytes(indent, FALSE, FALSE);
+ (void)del_bytes(indent, false, false);
mark_col_adjust(curwin->w_cursor.lnum,
- (colnr_T)0, 0L, (long)-indent, 0);
+ (colnr_T)0, 0L, (long)-indent, 0, kExtmarkNOOP);
}
}
curwin->w_cursor.lnum--;
@@ -4279,15 +4371,13 @@ int paragraph_start(linenr_T lnum)
return TRUE; /* after empty line */
do_comments = has_format_option(FO_Q_COMS);
- if (fmt_check_par(lnum - 1
- , &leader_len, &leader_flags, do_comments
- ))
- return TRUE; /* after non-paragraph line */
+ if (fmt_check_par(lnum - 1, &leader_len, &leader_flags, do_comments)) {
+ return true; // after non-paragraph line
+ }
- if (fmt_check_par(lnum
- , &next_leader_len, &next_leader_flags, do_comments
- ))
- return TRUE; /* "lnum" is not a paragraph line */
+ if (fmt_check_par(lnum, &next_leader_len, &next_leader_flags, do_comments)) {
+ return true; // "lnum" is not a paragraph line
+ }
if (has_format_option(FO_WHITE_PAR) && !ends_in_white(lnum - 1))
return TRUE; /* missing trailing space in previous line. */
@@ -4536,7 +4626,7 @@ void op_addsub(oparg_T *oap, linenr_T Prenum1, bool g_cmd)
int do_addsub(int op_type, pos_T *pos, int length, linenr_T Prenum1)
{
int col;
- char_u *buf1;
+ char_u *buf1 = NULL;
char_u buf2[NUMBUFLEN];
int pre; // 'X' or 'x': hex; '0': octal; 'B' or 'b': bin
static bool hexupper = false; // 0xABC
@@ -4641,7 +4731,7 @@ int do_addsub(int op_type, pos_T *pos, int length, linenr_T Prenum1)
if (visual) {
while (ptr[col] != NUL && length > 0 && !ascii_isdigit(ptr[col])
&& !(doalp && ASCII_ISALPHA(ptr[col]))) {
- int mb_len = MB_PTR2LEN(ptr + col);
+ int mb_len = utfc_ptr2len(ptr + col);
col += mb_len;
length -= mb_len;
@@ -4845,7 +4935,6 @@ int do_addsub(int op_type, pos_T *pos, int length, linenr_T Prenum1)
*ptr = NUL;
STRCAT(buf1, buf2);
ins_str(buf1); // insert the new number
- xfree(buf1);
endpos = curwin->w_cursor;
if (curwin->w_cursor.col) {
curwin->w_cursor.col--;
@@ -4860,6 +4949,7 @@ int do_addsub(int op_type, pos_T *pos, int length, linenr_T Prenum1)
}
theend:
+ xfree(buf1);
if (visual) {
curwin->w_cursor = save_cursor;
} else if (did_change) {
@@ -5114,8 +5204,7 @@ void write_reg_contents_lst(int name, char_u **strings,
/// write_reg_contents_ex - store `str` in register `name`
///
-/// If `str` ends in '\n' or '\r', use linewise, otherwise use
-/// characterwise.
+/// If `str` ends in '\n' or '\r', use linewise, otherwise use charwise.
///
/// @warning when `name` is '/', `len` and `must_append` are ignored. This
/// means that `str` MUST be NUL-terminated.
diff --git a/src/nvim/option.c b/src/nvim/option.c
index 3ccc67eb14..e48ed201e8 100644
--- a/src/nvim/option.c
+++ b/src/nvim/option.c
@@ -133,6 +133,7 @@ static char_u *p_cms;
static char_u *p_cpt;
static char_u *p_cfu;
static char_u *p_ofu;
+static char_u *p_tfu;
static int p_eol;
static int p_fixeol;
static int p_et;
@@ -2183,6 +2184,7 @@ static void didset_options(void)
(void)opt_strings_flags(p_tc, p_tc_values, &tc_flags, false);
(void)opt_strings_flags(p_ve, p_ve_values, &ve_flags, true);
(void)opt_strings_flags(p_wop, p_wop_values, &wop_flags, true);
+ (void)opt_strings_flags(p_jop, p_jop_values, &jop_flags, true);
(void)spell_check_msm();
(void)spell_check_sps();
(void)compile_cap_prog(curwin->w_s);
@@ -2204,10 +2206,10 @@ static void didset_options2(void)
(void)opt_strings_flags(p_cb, p_cb_values, &cb_flags, true);
// Parse default for 'fillchars'.
- (void)set_chars_option(curwin, &curwin->w_p_fcs);
+ (void)set_chars_option(curwin, &curwin->w_p_fcs, true);
// Parse default for 'listchars'.
- (void)set_chars_option(curwin, &curwin->w_p_lcs);
+ (void)set_chars_option(curwin, &curwin->w_p_lcs, true);
// Parse default for 'wildmode'.
check_opt_wim();
@@ -2273,6 +2275,7 @@ void check_buf_options(buf_T *buf)
check_string_option(&buf->b_p_ep);
check_string_option(&buf->b_p_path);
check_string_option(&buf->b_p_tags);
+ check_string_option(&buf->b_p_tfu);
check_string_option(&buf->b_p_tc);
check_string_option(&buf->b_p_dict);
check_string_option(&buf->b_p_tsr);
@@ -2630,6 +2633,10 @@ did_set_string_option(
if (strcmp((char *)(*varp), HIGHLIGHT_INIT) != 0) {
errmsg = e_unsupportedoption;
}
+ } else if (varp == &p_jop) { // 'jumpoptions'
+ if (opt_strings_flags(p_jop, p_jop_values, &jop_flags, true) != OK) {
+ errmsg = e_invarg;
+ }
} else if (gvarp == &p_nf) { // 'nrformats'
if (check_opt_strings(*varp, p_nf_values, true) != OK) {
errmsg = e_invarg;
@@ -2661,11 +2668,11 @@ did_set_string_option(
errmsg = e_invarg;
} else {
FOR_ALL_TAB_WINDOWS(tp, wp) {
- if (set_chars_option(wp, &wp->w_p_lcs) != NULL) {
+ if (set_chars_option(wp, &wp->w_p_lcs, true) != NULL) {
errmsg = (char_u *)_("E834: Conflicts with value of 'listchars'");
goto ambw_end;
}
- if (set_chars_option(wp, &wp->w_p_fcs) != NULL) {
+ if (set_chars_option(wp, &wp->w_p_fcs, true) != NULL) {
errmsg = (char_u *)_("E835: Conflicts with value of 'fillchars'");
goto ambw_end;
}
@@ -2866,10 +2873,26 @@ ambw_end:
}
s = skip_to_option_part(s);
}
- } else if (varp == &curwin->w_p_lcs) { // 'listchars'
- errmsg = set_chars_option(curwin, varp);
- } else if (varp == &curwin->w_p_fcs) { // 'fillchars'
- errmsg = set_chars_option(curwin, varp);
+ } else if (varp == &p_lcs) { // 'listchars'
+ errmsg = set_chars_option(curwin, varp, false);
+ if (!errmsg) {
+ FOR_ALL_TAB_WINDOWS(tp, wp) {
+ set_chars_option(wp, &wp->w_p_lcs, true);
+ }
+ }
+ redraw_all_later(NOT_VALID);
+ } else if (varp == &curwin->w_p_lcs) { // local 'listchars'
+ errmsg = set_chars_option(curwin, varp, true);
+ } else if (varp == &p_fcs) { // 'fillchars'
+ errmsg = set_chars_option(curwin, varp, false);
+ if (!errmsg) {
+ FOR_ALL_TAB_WINDOWS(tp, wp) {
+ set_chars_option(wp, &wp->w_p_fcs, true);
+ }
+ }
+ redraw_all_later(NOT_VALID);
+ } else if (varp == &curwin->w_p_fcs) { // local 'fillchars'
+ errmsg = set_chars_option(curwin, varp, true);
} else if (varp == &p_cedit) { // 'cedit'
errmsg = check_cedit();
} else if (varp == &p_vfile) { // 'verbosefile'
@@ -3499,7 +3522,7 @@ skip:
///
/// @param varp either &curwin->w_p_lcs or &curwin->w_p_fcs
/// @return error message, NULL if it's OK.
-static char_u *set_chars_option(win_T *wp, char_u **varp)
+static char_u *set_chars_option(win_T *wp, char_u **varp, bool set)
{
int round, i, len, entries;
char_u *p, *s;
@@ -3534,12 +3557,18 @@ static char_u *set_chars_option(win_T *wp, char_u **varp)
{ &wp->w_p_lcs_chars.conceal, "conceal", NUL },
};
- if (varp == &wp->w_p_lcs) {
+ if (varp == &p_lcs || varp == &wp->w_p_lcs) {
tab = lcs_tab;
entries = ARRAY_SIZE(lcs_tab);
+ if (varp == &wp->w_p_lcs && wp->w_p_lcs[0] == NUL) {
+ varp = &p_lcs;
+ }
} else {
tab = fcs_tab;
entries = ARRAY_SIZE(fcs_tab);
+ if (varp == &wp->w_p_fcs && wp->w_p_fcs[0] == NUL) {
+ varp = &p_fcs;
+ }
if (*p_ambw == 'd') {
// XXX: If ambiwidth=double then "|" and "·" take 2 columns, which is
// forbidden (TUI limitation?). Set old defaults.
@@ -3552,7 +3581,7 @@ static char_u *set_chars_option(win_T *wp, char_u **varp)
}
// first round: check for valid value, second round: assign values
- for (round = 0; round <= 1; round++) {
+ for (round = 0; round <= set ? 1 : 0; round++) {
if (round > 0) {
// After checking that the value is valid: set defaults
for (i = 0; i < entries; i++) {
@@ -3560,7 +3589,7 @@ static char_u *set_chars_option(win_T *wp, char_u **varp)
*(tab[i].cp) = tab[i].def;
}
}
- if (varp == &wp->w_p_lcs) {
+ if (varp == &p_lcs || varp == &wp->w_p_lcs) {
wp->w_p_lcs_chars.tab1 = NUL;
wp->w_p_lcs_chars.tab3 = NUL;
}
@@ -4306,6 +4335,10 @@ static char *set_num_option(int opt_idx, char_u *varp, long value,
if (value < 0) {
errmsg = e_positive;
}
+ } else if (pp == &p_wd) {
+ if (value < 0) {
+ errmsg = e_positive;
+ }
}
// Don't change the value and return early if validation failed.
@@ -5167,6 +5200,13 @@ void ui_refresh_options(void)
}
ui_call_option_set(name, value);
}
+ if (p_mouse != NULL) {
+ if (*p_mouse == NUL) {
+ ui_call_mouse_off();
+ } else {
+ setmouse();
+ }
+ }
}
/*
@@ -5556,6 +5596,16 @@ void unset_global_local_option(char *name, void *from)
case PV_MENC:
clear_string_option(&buf->b_p_menc);
break;
+ case PV_LCS:
+ clear_string_option(&((win_T *)from)->w_p_lcs);
+ set_chars_option((win_T *)from, &((win_T *)from)->w_p_lcs, true);
+ redraw_win_later((win_T *)from, NOT_VALID);
+ break;
+ case PV_FCS:
+ clear_string_option(&((win_T *)from)->w_p_fcs);
+ set_chars_option((win_T *)from, &((win_T *)from)->w_p_fcs, true);
+ redraw_win_later((win_T *)from, NOT_VALID);
+ break;
}
}
@@ -5586,11 +5636,14 @@ static char_u *get_varp_scope(vimoption_T *p, int opt_flags)
case PV_INC: return (char_u *)&(curbuf->b_p_inc);
case PV_DICT: return (char_u *)&(curbuf->b_p_dict);
case PV_TSR: return (char_u *)&(curbuf->b_p_tsr);
+ case PV_TFU: return (char_u *)&(curbuf->b_p_tfu);
case PV_STL: return (char_u *)&(curwin->w_p_stl);
case PV_UL: return (char_u *)&(curbuf->b_p_ul);
case PV_LW: return (char_u *)&(curbuf->b_p_lw);
case PV_BKC: return (char_u *)&(curbuf->b_p_bkc);
case PV_MENC: return (char_u *)&(curbuf->b_p_menc);
+ case PV_FCS: return (char_u *)&(curwin->w_p_fcs);
+ case PV_LCS: return (char_u *)&(curwin->w_p_lcs);
}
return NULL; // "cannot happen"
}
@@ -5649,6 +5702,10 @@ static char_u *get_varp(vimoption_T *p)
? (char_u *)&(curbuf->b_p_lw) : p->var;
case PV_MENC: return *curbuf->b_p_menc != NUL
? (char_u *)&(curbuf->b_p_menc) : p->var;
+ case PV_FCS: return *curwin->w_p_fcs != NUL
+ ? (char_u *)&(curwin->w_p_fcs) : p->var;
+ case PV_LCS: return *curwin->w_p_lcs != NUL
+ ? (char_u *)&(curwin->w_p_lcs) : p->var;
case PV_ARAB: return (char_u *)&(curwin->w_p_arab);
case PV_LIST: return (char_u *)&(curwin->w_p_list);
@@ -5738,6 +5795,7 @@ static char_u *get_varp(vimoption_T *p)
case PV_SPF: return (char_u *)&(curwin->w_s->b_p_spf);
case PV_SPL: return (char_u *)&(curwin->w_s->b_p_spl);
case PV_SW: return (char_u *)&(curbuf->b_p_sw);
+ case PV_TFU: return (char_u *)&(curbuf->b_p_tfu);
case PV_TS: return (char_u *)&(curbuf->b_p_ts);
case PV_TW: return (char_u *)&(curbuf->b_p_tw);
case PV_UDF: return (char_u *)&(curbuf->b_p_udf);
@@ -5745,8 +5803,6 @@ static char_u *get_varp(vimoption_T *p)
case PV_KMAP: return (char_u *)&(curbuf->b_p_keymap);
case PV_SCL: return (char_u *)&(curwin->w_p_scl);
case PV_WINHL: return (char_u *)&(curwin->w_p_winhl);
- case PV_FCS: return (char_u *)&(curwin->w_p_fcs);
- case PV_LCS: return (char_u *)&(curwin->w_p_lcs);
case PV_WINBL: return (char_u *)&(curwin->w_p_winbl);
default: IEMSG(_("E356: get_varp ERROR"));
}
@@ -5888,8 +5944,8 @@ void didset_window_options(win_T *wp)
{
check_colorcolumn(wp);
briopt_check(wp);
- set_chars_option(wp, &wp->w_p_fcs);
- set_chars_option(wp, &wp->w_p_lcs);
+ set_chars_option(wp, &wp->w_p_fcs, true);
+ set_chars_option(wp, &wp->w_p_lcs, true);
parse_winhl_opt(wp); // sets w_hl_needs_update also for w_p_winbl
wp->w_grid.blending = wp->w_p_winbl > 0;
}
@@ -6000,6 +6056,7 @@ void buf_copy_options(buf_T *buf, int flags)
buf->b_p_cpt = vim_strsave(p_cpt);
buf->b_p_cfu = vim_strsave(p_cfu);
buf->b_p_ofu = vim_strsave(p_ofu);
+ buf->b_p_tfu = vim_strsave(p_tfu);
buf->b_p_sts = p_sts;
buf->b_p_sts_nopaste = p_sts_nopaste;
buf->b_p_com = vim_strsave(p_com);
diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h
index 08df495250..4f9f32794b 100644
--- a/src/nvim/option_defs.h
+++ b/src/nvim/option_defs.h
@@ -473,6 +473,12 @@ EXTERN char_u *p_isf; // 'isfname'
EXTERN char_u *p_isi; // 'isident'
EXTERN char_u *p_isp; // 'isprint'
EXTERN int p_js; // 'joinspaces'
+EXTERN char_u *p_jop; // 'jumpooptions'
+EXTERN unsigned jop_flags;
+#ifdef IN_OPTION_C
+static char *(p_jop_values[]) = { "stack", NULL };
+#endif
+#define JOP_STACK 0x01
EXTERN char_u *p_kp; // 'keywordprg'
EXTERN char_u *p_km; // 'keymodel'
EXTERN char_u *p_langmap; // 'langmap'
@@ -484,6 +490,7 @@ EXTERN long p_linespace; // 'linespace'
EXTERN char_u *p_lispwords; // 'lispwords'
EXTERN long p_ls; // 'laststatus'
EXTERN long p_stal; // 'showtabline'
+EXTERN char_u *p_lcs; // 'listchars'
EXTERN int p_lz; // 'lazyredraw'
EXTERN int p_lpl; // 'loadplugins'
@@ -518,10 +525,18 @@ EXTERN long p_pyx; // 'pyxversion'
EXTERN char_u *p_rdb; // 'redrawdebug'
EXTERN unsigned rdb_flags;
# ifdef IN_OPTION_C
-static char *(p_rdb_values[]) = { "compositor", "nothrottle", NULL };
+static char *(p_rdb_values[]) = {
+ "compositor",
+ "nothrottle",
+ "invalid",
+ "nodelta",
+ NULL
+};
# endif
# define RDB_COMPOSITOR 0x001
# define RDB_NOTHROTTLE 0x002
+# define RDB_INVALID 0x004
+# define RDB_NODELTA 0x008
EXTERN long p_rdt; // 'redrawtime'
EXTERN int p_remap; // 'remap'
@@ -603,13 +618,14 @@ EXTERN char_u *p_swb; // 'switchbuf'
EXTERN unsigned swb_flags;
#ifdef IN_OPTION_C
static char *(p_swb_values[]) =
- { "useopen", "usetab", "split", "newtab", "vsplit", NULL };
+ { "useopen", "usetab", "split", "newtab", "vsplit", "uselast", NULL };
#endif
#define SWB_USEOPEN 0x001
#define SWB_USETAB 0x002
#define SWB_SPLIT 0x004
#define SWB_NEWTAB 0x008
#define SWB_VSPLIT 0x010
+#define SWB_USELAST 0x020
EXTERN int p_tbs; ///< 'tagbsearch'
EXTERN char_u *p_tc; ///< 'tagcase'
EXTERN unsigned tc_flags; ///< flags from 'tagcase'
@@ -644,6 +660,7 @@ EXTERN long p_ul; ///< 'undolevels'
EXTERN long p_ur; ///< 'undoreload'
EXTERN long p_uc; ///< 'updatecount'
EXTERN long p_ut; ///< 'updatetime'
+EXTERN char_u *p_fcs; ///< 'fillchar'
EXTERN char_u *p_shada; ///< 'shada'
EXTERN char *p_shadafile; ///< 'shadafile'
EXTERN char_u *p_vdir; ///< 'viewdir'
@@ -772,6 +789,7 @@ enum {
, BV_SUA
, BV_SW
, BV_SWF
+ , BV_TFU
, BV_TAGS
, BV_TC
, BV_TS
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
index 52e788944b..93bfc1c0b1 100644
--- a/src/nvim/options.lua
+++ b/src/nvim/options.lua
@@ -612,7 +612,7 @@ return {
alloced=true,
redraw={'current_window'},
varname='p_dip',
- defaults={if_true={vi="internal,filler"}}
+ defaults={if_true={vi="internal,filler,closeoff"}}
},
{
full_name='digraph', abbreviation='dg',
@@ -804,11 +804,12 @@ return {
},
{
full_name='fillchars', abbreviation='fcs',
- type='string', list='onecomma', scope={'window'},
+ type='string', list='onecomma', scope={'global', 'window'},
deny_duplicates=true,
vi_def=true,
alloced=true,
redraw={'current_window'},
+ varname='p_fcs',
defaults={if_true={vi=''}}
},
{
@@ -1299,6 +1300,14 @@ return {
defaults={if_true={vi=true}}
},
{
+ full_name='jumpoptions', abbreviation='jop',
+ type='string', list='onecomma', scope={'global'},
+ deny_duplicates=true,
+ varname='p_jop',
+ vim=true,
+ defaults={if_true={vim=''}}
+ },
+ {
full_name='keymap', abbreviation='kmp',
type='string', scope={'buffer'},
normal_fname_chars=true,
@@ -1420,11 +1429,12 @@ return {
},
{
full_name='listchars', abbreviation='lcs',
- type='string', list='onecomma', scope={'window'},
+ type='string', list='onecomma', scope={'global', 'window'},
deny_duplicates=true,
vim=true,
alloced=true,
redraw={'current_window'},
+ varname='p_lcs',
defaults={if_true={vi="eol:$", vim="tab:> ,trail:-,nbsp:+"}}
},
{
@@ -2323,9 +2333,9 @@ return {
full_name='startofline', abbreviation='sol',
type='bool', scope={'global'},
vi_def=true,
- vim=true,
+ vim=false,
varname='p_sol',
- defaults={if_true={vi=true}}
+ defaults={if_true={vi=false}}
},
{
full_name='statusline', abbreviation='stl',
@@ -2389,6 +2399,14 @@ return {
defaults={if_true={vi=""}}
},
{
+ full_name='tagfunc', abbreviation='tfu',
+ type='string', scope={'buffer'},
+ vim=true,
+ vi_def=true,
+ varname='p_tfu',
+ defaults={if_true={vi=""}}
+ },
+ {
full_name='tabline', abbreviation='tal',
type='string', scope={'global'},
vi_def=true,
diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c
index f5dbf0694e..360609c50d 100644
--- a/src/nvim/os/env.c
+++ b/src/nvim/os/env.c
@@ -44,6 +44,16 @@ void env_init(void)
uv_mutex_init(&mutex);
}
+void os_env_var_lock(void)
+{
+ uv_mutex_lock(&mutex);
+}
+
+void os_env_var_unlock(void)
+{
+ uv_mutex_unlock(&mutex);
+}
+
/// Like getenv(), but returns NULL if the variable is empty.
/// @see os_env_exists
const char *os_getenv(const char *name)
@@ -55,6 +65,7 @@ const char *os_getenv(const char *name)
return NULL;
}
uv_mutex_lock(&mutex);
+ int r = 0;
if (pmap_has(cstr_t)(envmap, name)
&& !!(e = (char *)pmap_get(cstr_t)(envmap, name))) {
if (e[0] != '\0') {
@@ -67,7 +78,7 @@ const char *os_getenv(const char *name)
pmap_del2(envmap, name);
}
e = xmalloc(size);
- int r = uv_os_getenv(name, e, &size);
+ r = uv_os_getenv(name, e, &size);
if (r == UV_ENOBUFS) {
e = xrealloc(e, size);
r = uv_os_getenv(name, e, &size);
@@ -75,14 +86,15 @@ const char *os_getenv(const char *name)
if (r != 0 || size == 0 || e[0] == '\0') {
xfree(e);
e = NULL;
- if (r != 0 && r != UV_ENOENT && r != UV_UNKNOWN) {
- ELOG("uv_os_getenv(%s) failed: %d %s", name, r, uv_err_name(r));
- }
goto end;
}
pmap_put(cstr_t)(envmap, xstrdup(name), e);
end:
+ // Must do this before ELOG, log.c may call os_setenv.
uv_mutex_unlock(&mutex);
+ if (r != 0 && r != UV_ENOENT && r != UV_UNKNOWN) {
+ ELOG("uv_os_getenv(%s) failed: %d %s", name, r, uv_err_name(r));
+ }
return (e == NULL || size == 0 || e[0] == '\0') ? NULL : e;
}
@@ -102,9 +114,6 @@ bool os_env_exists(const char *name)
assert(r != UV_EINVAL);
if (r != 0 && r != UV_ENOENT && r != UV_ENOBUFS) {
ELOG("uv_os_getenv(%s) failed: %d %s", name, r, uv_err_name(r));
-#ifdef WIN32
- return (r == UV_UNKNOWN);
-#endif
}
return (r == 0 || r == UV_ENOBUFS);
}
@@ -135,15 +144,25 @@ int os_setenv(const char *name, const char *value, int overwrite)
}
#endif
uv_mutex_lock(&mutex);
- int r = uv_os_setenv(name, value);
+ int r;
+#ifdef WIN32
+ // libintl uses getenv() for LC_ALL/LANG/etc., so we must use _putenv_s().
+ if (striequal(name, "LC_ALL") || striequal(name, "LANGUAGE")
+ || striequal(name, "LANG") || striequal(name, "LC_MESSAGES")) {
+ r = _putenv_s(name, value); // NOLINT
+ assert(r == 0);
+ }
+#endif
+ r = uv_os_setenv(name, value);
assert(r != UV_EINVAL);
// Destroy the old map item. Do this AFTER uv_os_setenv(), because `value`
// could be a previous os_getenv() result.
pmap_del2(envmap, name);
+ // Must do this before ELOG, log.c may call os_setenv.
+ uv_mutex_unlock(&mutex);
if (r != 0) {
ELOG("uv_os_setenv(%s) failed: %d %s", name, r, uv_err_name(r));
}
- uv_mutex_unlock(&mutex);
return r == 0 ? 0 : -1;
}
@@ -157,46 +176,150 @@ int os_unsetenv(const char *name)
uv_mutex_lock(&mutex);
pmap_del2(envmap, name);
int r = uv_os_unsetenv(name);
+ // Must do this before ELOG, log.c may call os_setenv.
+ uv_mutex_unlock(&mutex);
if (r != 0) {
ELOG("uv_os_unsetenv(%s) failed: %d %s", name, r, uv_err_name(r));
}
- uv_mutex_unlock(&mutex);
return r == 0 ? 0 : -1;
}
+/// Returns number of variables in the current environment variables block
+size_t os_get_fullenv_size(void)
+{
+ size_t len = 0;
+#ifdef _WIN32
+ wchar_t *envstrings = GetEnvironmentStringsW();
+ wchar_t *p = envstrings;
+ size_t l;
+ if (!envstrings) {
+ return len;
+ }
+ // GetEnvironmentStringsW() result has this format:
+ // var1=value1\0var2=value2\0...varN=valueN\0\0
+ while ((l = wcslen(p)) != 0) {
+ p += l + 1;
+ len++;
+ }
+
+ FreeEnvironmentStringsW(envstrings);
+#else
+# if defined(HAVE__NSGETENVIRON)
+ char **environ = *_NSGetEnviron();
+# else
+ extern char **environ;
+# endif
+
+ while (environ[len] != NULL) {
+ len++;
+ }
+
+#endif
+ return len;
+}
+
+void os_free_fullenv(char **env)
+{
+ if (!env) { return; }
+ for (char **it = env; *it; it++) {
+ XFREE_CLEAR(*it);
+ }
+ xfree(env);
+}
+
+/// Copies the current environment variables into the given array, `env`. Each
+/// array element is of the form "NAME=VALUE".
+/// Result must be freed by the caller.
+///
+/// @param[out] env array to populate with environment variables
+/// @param env_size size of `env`, @see os_fullenv_size
+void os_copy_fullenv(char **env, size_t env_size)
+{
+#ifdef _WIN32
+ wchar_t *envstrings = GetEnvironmentStringsW();
+ if (!envstrings) {
+ return;
+ }
+ wchar_t *p = envstrings;
+ size_t i = 0;
+ size_t l;
+ // GetEnvironmentStringsW() result has this format:
+ // var1=value1\0var2=value2\0...varN=valueN\0\0
+ while ((l = wcslen(p)) != 0 && i < env_size) {
+ char *utf8_str;
+ int conversion_result = utf16_to_utf8(p, -1, &utf8_str);
+ if (conversion_result != 0) {
+ EMSG2("utf16_to_utf8 failed: %d", conversion_result);
+ break;
+ }
+ p += l + 1;
+
+ env[i] = utf8_str;
+ i++;
+ }
+
+ FreeEnvironmentStringsW(envstrings);
+#else
+# if defined(HAVE__NSGETENVIRON)
+ char **environ = *_NSGetEnviron();
+# else
+ extern char **environ;
+# endif
+
+ size_t i = 0;
+ while (environ[i] != NULL && i < env_size) {
+ env[i] = xstrdup(environ[i]);
+ i++;
+ }
+#endif
+}
+
+/// Copy value of the environment variable at `index` in the current
+/// environment variables block.
+/// Result must be freed by the caller.
+///
+/// @param index nth item in environment variables block
+/// @return [allocated] environment variable's value, or NULL
char *os_getenvname_at_index(size_t index)
{
#ifdef _WIN32
- wchar_t *env = GetEnvironmentStringsW();
- if (!env) {
+ wchar_t *envstrings = GetEnvironmentStringsW();
+ if (!envstrings) {
return NULL;
}
+ wchar_t *p = envstrings;
char *name = NULL;
- size_t current_index = 0;
+ size_t i = 0;
+ size_t l;
// GetEnvironmentStringsW() result has this format:
// var1=value1\0var2=value2\0...varN=valueN\0\0
- for (wchar_t *it = env; *it != L'\0' || *(it + 1) != L'\0'; it++) {
- if (index == current_index) {
+ while ((l = wcslen(p)) != 0 && i <= index) {
+ if (i == index) {
char *utf8_str;
- int conversion_result = utf16_to_utf8(it, -1, &utf8_str);
+ int conversion_result = utf16_to_utf8(p, -1, &utf8_str);
if (conversion_result != 0) {
EMSG2("utf16_to_utf8 failed: %d", conversion_result);
break;
}
- size_t namesize = 0;
- while (utf8_str[namesize] != '=' && utf8_str[namesize] != NUL) {
- namesize++;
- }
- name = (char *)vim_strnsave((char_u *)utf8_str, namesize);
+
+ // Some Windows env vars start with =, so skip over that to find the
+ // separator between name/value
+ const char * const end = strchr(utf8_str + (utf8_str[0] == '=' ? 1 : 0),
+ '=');
+ assert(end != NULL);
+ ptrdiff_t len = end - utf8_str;
+ assert(len > 0);
+ name = xstrndup(utf8_str, (size_t)len);
xfree(utf8_str);
break;
}
- if (*it == L'\0') {
- current_index++;
- }
+
+ // Advance past the name and NUL
+ p += l + 1;
+ i++;
}
- FreeEnvironmentStringsW(env);
+ FreeEnvironmentStringsW(envstrings);
return name;
#else
# if defined(HAVE__NSGETENVIRON)
@@ -204,19 +327,20 @@ char *os_getenvname_at_index(size_t index)
# else
extern char **environ;
# endif
- // Check if index is inside the environ array and is not the last element.
+
+ // check if index is inside the environ array
for (size_t i = 0; i <= index; i++) {
if (environ[i] == NULL) {
return NULL;
}
}
char *str = environ[index];
- size_t namesize = 0;
- while (str[namesize] != '=' && str[namesize] != NUL) {
- namesize++;
- }
- char *name = (char *)vim_strnsave((char_u *)str, namesize);
- return name;
+ assert(str != NULL);
+ const char * const end = strchr(str, '=');
+ assert(end != NULL);
+ ptrdiff_t len = end - str;
+ assert(len > 0);
+ return xstrndup(str, (size_t)len);
#endif
}
diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c
index 6956410401..f4377b1457 100644
--- a/src/nvim/os/shell.c
+++ b/src/nvim/os/shell.c
@@ -423,10 +423,11 @@ static bool out_data_decide_throttle(size_t size)
pulse_msg[1] = (tick > 1) ? '.' : ' ';
pulse_msg[2] = (tick > 2) ? '.' : ' ';
if (visit == 1) {
- msg_putchar('\n');
+ msg_puts("[...]\n");
}
msg_putchar('\r'); // put cursor at start of line
msg_puts(pulse_msg);
+ msg_putchar('\r');
ui_flush();
return true;
}
diff --git a/src/nvim/os/tty.c b/src/nvim/os/tty.c
index bd5b9b4506..4f525bed9a 100644
--- a/src/nvim/os/tty.c
+++ b/src/nvim/os/tty.c
@@ -6,6 +6,7 @@
//
#include "nvim/os/os.h"
+#include "nvim/os/tty.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "os/tty.c.generated.h"
diff --git a/src/nvim/os/users.c b/src/nvim/os/users.c
index c6463c2c92..9374693550 100644
--- a/src/nvim/os/users.c
+++ b/src/nvim/os/users.c
@@ -13,6 +13,25 @@
#ifdef HAVE_PWD_H
# include <pwd.h>
#endif
+#ifdef WIN32
+# include <lm.h>
+#endif
+
+// Add a user name to the list of users in garray_T *users.
+// Do nothing if user name is NULL or empty.
+static void add_user(garray_T *users, char *user, bool need_copy)
+{
+ char *user_copy = (user != NULL && need_copy)
+ ? xstrdup(user) : user;
+
+ if (user_copy == NULL || *user_copy == NUL) {
+ if (need_copy) {
+ xfree(user);
+ }
+ return;
+ }
+ GA_APPEND(char *, users, user_copy);
+}
// Initialize users garray and fill it with os usernames.
// Return Ok for success, FAIL for failure.
@@ -24,16 +43,66 @@ int os_get_usernames(garray_T *users)
ga_init(users, sizeof(char *), 20);
# if defined(HAVE_GETPWENT) && defined(HAVE_PWD_H)
- struct passwd *pw;
+ {
+ struct passwd *pw;
+
+ setpwent();
+ while ((pw = getpwent()) != NULL) {
+ add_user(users, pw->pw_name, true);
+ }
+ endpwent();
+ }
+# elif defined(WIN32)
+ {
+ DWORD nusers = 0, ntotal = 0, i;
+ PUSER_INFO_0 uinfo;
+
+ if (NetUserEnum(NULL, 0, 0, (LPBYTE *)&uinfo, MAX_PREFERRED_LENGTH,
+ &nusers, &ntotal, NULL) == NERR_Success) {
+ for (i = 0; i < nusers; i++) {
+ char *user;
+ int conversion_result = utf16_to_utf8(uinfo[i].usri0_name, -1, &user);
+ if (conversion_result != 0) {
+ EMSG2("utf16_to_utf8 failed: %d", conversion_result);
+ break;
+ }
+ add_user(users, user, false);
+ }
+
+ NetApiBufferFree(uinfo);
+ }
+ }
+# endif
+# if defined(HAVE_GETPWNAM)
+ {
+ const char *user_env = os_getenv("USER");
+
+ // The $USER environment variable may be a valid remote user name (NIS,
+ // LDAP) not already listed by getpwent(), as getpwent() only lists
+ // local user names. If $USER is not already listed, check whether it
+ // is a valid remote user name using getpwnam() and if it is, add it to
+ // the list of user names.
+
+ if (user_env != NULL && *user_env != NUL) {
+ int i;
+
+ for (i = 0; i < users->ga_len; i++) {
+ char *local_user = ((char **)users->ga_data)[i];
+
+ if (STRCMP(local_user, user_env) == 0) {
+ break;
+ }
+ }
+
+ if (i == users->ga_len) {
+ struct passwd *pw = getpwnam(user_env); // NOLINT
- setpwent();
- while ((pw = getpwent()) != NULL) {
- // pw->pw_name shouldn't be NULL but just in case...
- if (pw->pw_name != NULL) {
- GA_APPEND(char *, users, xstrdup(pw->pw_name));
+ if (pw != NULL) {
+ add_user(users, pw->pw_name, true);
+ }
+ }
}
}
- endpwent();
# endif
return OK;
diff --git a/src/nvim/os_unix.c b/src/nvim/os_unix.c
index ded575529f..0f44df2188 100644
--- a/src/nvim/os_unix.c
+++ b/src/nvim/os_unix.c
@@ -412,7 +412,7 @@ int mch_expand_wildcards(int num_pat, char_u **pat, int *num_file,
fseek(fd, 0L, SEEK_SET);
buffer = xmalloc(len + 1);
// fread() doesn't terminate buffer with NUL;
- // appropiate termination (not always NUL) is done below.
+ // appropriate termination (not always NUL) is done below.
size_t readlen = fread((char *)buffer, 1, len, fd);
fclose(fd);
os_remove((char *)tempname);
diff --git a/src/nvim/path.c b/src/nvim/path.c
index 1c787e3a1d..a53870acb8 100644
--- a/src/nvim/path.c
+++ b/src/nvim/path.c
@@ -51,9 +51,10 @@
/// expanded.
/// @param s2 Second file name.
/// @param checkname When both files don't exist, only compare their names.
+/// @param expandenv Whether to expand environment variables in file names.
/// @return Enum of type FileComparison. @see FileComparison.
FileComparison path_full_compare(char_u *const s1, char_u *const s2,
- const bool checkname)
+ const bool checkname, const bool expandenv)
{
assert(s1 && s2);
char_u exp1[MAXPATHL];
@@ -61,7 +62,11 @@ FileComparison path_full_compare(char_u *const s1, char_u *const s2,
char_u full2[MAXPATHL];
FileID file_id_1, file_id_2;
- expand_env(s1, exp1, MAXPATHL);
+ if (expandenv) {
+ expand_env(s1, exp1, MAXPATHL);
+ } else {
+ xstrlcpy((char *)exp1, (const char *)s1, MAXPATHL - 1);
+ }
bool id_ok_1 = os_fileid((char *)exp1, &file_id_1);
bool id_ok_2 = os_fileid((char *)s2, &file_id_2);
if (!id_ok_1 && !id_ok_2) {
@@ -333,9 +338,9 @@ int path_fnamencmp(const char *const fname1, const char *const fname2,
&& (p_fic ? (c1 != c2 && CH_FOLD(c1) != CH_FOLD(c2)) : c1 != c2)) {
break;
}
- len -= (size_t)MB_PTR2LEN((const char_u *)p1);
- p1 += MB_PTR2LEN((const char_u *)p1);
- p2 += MB_PTR2LEN((const char_u *)p2);
+ len -= (size_t)utfc_ptr2len((const char_u *)p1);
+ p1 += utfc_ptr2len((const char_u *)p1);
+ p2 += utfc_ptr2len((const char_u *)p2);
}
return c1 - c2;
#else
@@ -1203,7 +1208,7 @@ int gen_expand_wildcards(int num_pat, char_u **pat, int *num_file,
}
} else {
// First expand environment variables, "~/" and "~user/".
- if (has_env_var(p) || *p == '~') {
+ if ((has_env_var(p) && !(flags & EW_NOTENV)) || *p == '~') {
p = expand_env_save_opt(p, true);
if (p == NULL)
p = pat[i];
@@ -1905,16 +1910,16 @@ int pathcmp(const char *p, const char *q, int maxlen)
: c1 - c2; // no match
}
- i += MB_PTR2LEN((char_u *)p + i);
- j += MB_PTR2LEN((char_u *)q + j);
+ i += utfc_ptr2len((char_u *)p + i);
+ j += utfc_ptr2len((char_u *)q + j);
}
if (s == NULL) { // "i" or "j" ran into "maxlen"
return 0;
}
c1 = PTR2CHAR((char_u *)s + i);
- c2 = PTR2CHAR((char_u *)s + i + MB_PTR2LEN((char_u *)s + i));
- /* ignore a trailing slash, but not "//" or ":/" */
+ c2 = PTR2CHAR((char_u *)s + i + utfc_ptr2len((char_u *)s + i));
+ // ignore a trailing slash, but not "//" or ":/"
if (c2 == NUL
&& i > 0
&& !after_pathsep((char *)s, (char *)s + i)
@@ -1923,10 +1928,12 @@ int pathcmp(const char *p, const char *q, int maxlen)
#else
&& c1 == '/'
#endif
- )
- return 0; /* match with trailing slash */
- if (s == q)
- return -1; /* no match */
+ ) {
+ return 0; // match with trailing slash
+ }
+ if (s == q) {
+ return -1; // no match
+ }
return 1;
}
diff --git a/src/nvim/path.h b/src/nvim/path.h
index 4e466d1b71..15abd19646 100644
--- a/src/nvim/path.h
+++ b/src/nvim/path.h
@@ -20,11 +20,12 @@
#define EW_KEEPDOLLAR 0x800 /* do not escape $, $var is expanded */
/* Note: mostly EW_NOTFOUND and EW_SILENT are mutually exclusive: EW_NOTFOUND
* is used when executing commands and EW_SILENT for interactive expanding. */
-#define EW_ALLLINKS 0x1000 // also links not pointing to existing file
-#define EW_SHELLCMD 0x2000 // called from expand_shellcmd(), don't check
- // if executable is in $PATH
-#define EW_DODOT 0x4000 // also files starting with a dot
-#define EW_EMPTYOK 0x8000 // no matches is not an error
+#define EW_ALLLINKS 0x1000 // also links not pointing to existing file
+#define EW_SHELLCMD 0x2000 // called from expand_shellcmd(), don't check
+ // if executable is in $PATH
+#define EW_DODOT 0x4000 // also files starting with a dot
+#define EW_EMPTYOK 0x8000 // no matches is not an error
+#define EW_NOTENV 0x10000 // do not expand environment variables
/// Return value for the comparison of two files. Also @see path_full_compare.
typedef enum file_comparison {
diff --git a/src/nvim/po/af.po b/src/nvim/po/af.po
index 61ad72d7e0..79048eac39 100644
--- a/src/nvim/po/af.po
+++ b/src/nvim/po/af.po
@@ -100,7 +100,7 @@ msgid "E88: Cannot go before first buffer"
msgstr "E88: Kan nie vóór eerste buffer gaan nie"
#, fuzzy, c-format
-#~ msgid "E89: %s will be killed(add ! to override)"
+#~ msgid "E89: %s will be killed (add ! to override)"
#~ msgstr "E189: \"%s\" bestaan (gebruik ! om te dwing)"
#, c-format
diff --git a/src/nvim/po/check.vim b/src/nvim/po/check.vim
index eae27ef74d..650c6155e2 100644
--- a/src/nvim/po/check.vim
+++ b/src/nvim/po/check.vim
@@ -47,6 +47,17 @@ let wsv = winsaveview()
let error = 0
while 1
+ let lnum = line('.')
+ if getline(lnum) =~ 'msgid "Text;.*;"'
+ if getline(lnum + 1) !~ '^msgstr "\([^;]\+;\)\+"'
+ echomsg 'Mismatching ; in line ' . (lnum + 1)
+ echomsg 'Did you forget the trailing semicolon?'
+ if error == 0
+ let error = lnum + 1
+ endif
+ endif
+ endif
+
if getline(line('.') - 1) !~ "no-c-format"
" go over the "msgid" and "msgid_plural" lines
let prevfromline = 'foobar'
diff --git a/src/nvim/po/fi.po b/src/nvim/po/fi.po
index 4612988c95..f568a34b3c 100644
--- a/src/nvim/po/fi.po
+++ b/src/nvim/po/fi.po
@@ -323,7 +323,7 @@ msgid "E88: Cannot go before first buffer"
msgstr "E88: Ensimmäisen puskurin ohi ei voi edetä"
#, fuzzy, c-format
-#~ msgid "E89: %s will be killed(add ! to override)"
+#~ msgid "E89: %s will be killed (add ! to override)"
#~ msgstr "E189: %s on jo olemassa (lisää komentoon ! ohittaaksesi)"
#, fuzzy, c-format
diff --git a/src/nvim/po/uk.po b/src/nvim/po/uk.po
index 211d38e53a..19ea8e897a 100644
--- a/src/nvim/po/uk.po
+++ b/src/nvim/po/uk.po
@@ -87,8 +87,8 @@ msgid "E88: Cannot go before first buffer"
msgstr "E88: Це вже найперший буфер"
#, c-format
-msgid "E89: %s will be killed(add ! to override)"
-msgstr "E89: «%s» буде вбито(! щоб не зважати)"
+msgid "E89: %s will be killed (add ! to override)"
+msgstr "E89: «%s» буде вбито (! щоб не зважати)"
#, c-format
msgid ""
diff --git a/src/nvim/pos.h b/src/nvim/pos.h
index 47d253e083..8e86ea08c5 100644
--- a/src/nvim/pos.h
+++ b/src/nvim/pos.h
@@ -14,6 +14,10 @@ typedef int colnr_T;
enum { MAXLNUM = 0x7fffffff };
/// Maximal column number, 31 bits
enum { MAXCOL = 0x7fffffff };
+// Minimum line number
+enum { MINLNUM = 1 };
+// minimum column number
+enum { MINCOL = 1 };
/*
* position in file or buffer
diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c
index 301f72fa79..5d30ca624f 100644
--- a/src/nvim/quickfix.c
+++ b/src/nvim/quickfix.c
@@ -1,9 +1,7 @@
// 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
-/*
- * quickfix.c: functions for quickfix mode, using a file with error messages
- */
+// quickfix.c: functions for quickfix mode, using a file with error messages
#include <assert.h>
#include <inttypes.h>
@@ -53,9 +51,7 @@ struct dir_stack_T {
char_u *dirname;
};
-/*
- * For each error the next struct is allocated and linked in a list.
- */
+// For each error the next struct is allocated and linked in a list.
typedef struct qfline_S qfline_T;
struct qfline_S {
qfline_T *qf_next; ///< pointer to next error in the list
@@ -74,12 +70,19 @@ struct qfline_S {
char_u qf_valid; ///< valid error message detected
};
-/*
- * There is a stack of error lists.
- */
+// There is a stack of error lists.
#define LISTCOUNT 10
#define INVALID_QFIDX (-1)
+/// Quickfix list type.
+typedef enum
+{
+ QFLT_QUICKFIX, ///< Quickfix list - global list
+ QFLT_LOCATION, ///< Location list - per window list
+ QFLT_INTERNAL ///< Internal - Temporary list used by
+ // getqflist()/getloclist()
+} qfltype_T;
+
/// Quickfix/Location list definition
///
/// Usually the list contains one or more entries. But an empty list can be
@@ -87,6 +90,7 @@ struct qfline_S {
/// information and entries can be added later using setqflist()/setloclist().
typedef struct qf_list_S {
unsigned qf_id; ///< Unique identifier for this list
+ qfltype_T qfl_type;
qfline_T *qf_start; ///< pointer to the first error
qfline_T *qf_last; ///< pointer to the last error
qfline_T *qf_ptr; ///< pointer to the current error
@@ -110,16 +114,15 @@ typedef struct qf_list_S {
/// Quickfix/Location list stack definition
/// Contains a list of quickfix/location lists (qf_list_T)
struct qf_info_S {
- /*
- * Count of references to this list. Used only for location lists.
- * When a location list window reference this list, qf_refcount
- * will be 2. Otherwise, qf_refcount will be 1. When qf_refcount
- * reaches 0, the list is freed.
- */
+ // Count of references to this list. Used only for location lists.
+ // When a location list window reference this list, qf_refcount
+ // will be 2. Otherwise, qf_refcount will be 1. When qf_refcount
+ // reaches 0, the list is freed.
int qf_refcount;
- int qf_listcount; /* current number of lists */
- int qf_curlist; /* current error list */
+ int qf_listcount; // current number of lists
+ int qf_curlist; // current error list
qf_list_T qf_lists[LISTCOUNT];
+ qfltype_T qfl_type; // type of list
};
static qf_info_T ql_info; // global quickfix list
@@ -127,33 +130,38 @@ static unsigned last_qf_id = 0; // Last Used quickfix list id
#define FMT_PATTERNS 11 // maximum number of % recognized
-/*
- * Structure used to hold the info of one part of 'errorformat'
- */
+// Structure used to hold the info of one part of 'errorformat'
typedef struct efm_S efm_T;
struct efm_S {
- regprog_T *prog; /* pre-formatted part of 'errorformat' */
- efm_T *next; /* pointer to next (NULL if last) */
- char_u addr[FMT_PATTERNS]; /* indices of used % patterns */
- char_u prefix; /* prefix of this format line: */
- /* 'D' enter directory */
- /* 'X' leave directory */
- /* 'A' start of multi-line message */
- /* 'E' error message */
- /* 'W' warning message */
- /* 'I' informational message */
- /* 'C' continuation line */
- /* 'Z' end of multi-line message */
- /* 'G' general, unspecific message */
- /* 'P' push file (partial) message */
- /* 'Q' pop/quit file (partial) message */
- /* 'O' overread (partial) message */
- char_u flags; /* additional flags given in prefix */
- /* '-' do not include this line */
- /* '+' include whole line in message */
- int conthere; /* %> used */
+ regprog_T *prog; // pre-formatted part of 'errorformat'
+ efm_T *next; // pointer to next (NULL if last)
+ char_u addr[FMT_PATTERNS]; // indices of used % patterns
+ char_u prefix; // prefix of this format line:
+ // 'D' enter directory
+ // 'X' leave directory
+ // 'A' start of multi-line message
+ // 'E' error message
+ // 'W' warning message
+ // 'I' informational message
+ // 'C' continuation line
+ // 'Z' end of multi-line message
+ // 'G' general, unspecific message
+ // 'P' push file (partial) message
+ // 'Q' pop/quit file (partial) message
+ // 'O' overread (partial) message
+ char_u flags; // additional flags given in prefix
+ // '-' do not include this line
+ // '+' include whole line in message
+ int conthere; // %> used
};
+/// List of location lists to be deleted.
+/// Used to delay the deletion of locations lists by autocmds.
+typedef struct qf_delq_S {
+ struct qf_delq_S *next;
+ qf_info_T *qi;
+} qf_delq_T;
+
enum {
QF_FAIL = 0,
QF_OK = 1,
@@ -163,6 +171,8 @@ enum {
QF_MULTISCAN = 5,
};
+/// State information used to parse lines and add entries to a quickfix/location
+/// list.
typedef struct {
char_u *linebuf;
size_t linelen;
@@ -196,14 +206,19 @@ typedef struct {
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "quickfix.c.generated.h"
#endif
-/* Quickfix window check helper macro */
+
+static char_u *e_no_more_items = (char_u *)N_("E553: No more items");
+
+// Quickfix window check helper macro
#define IS_QF_WINDOW(wp) (bt_quickfix(wp->w_buffer) && wp->w_llist_ref == NULL)
-/* Location list window check helper macro */
+// Location list window check helper macro
#define IS_LL_WINDOW(wp) (bt_quickfix(wp->w_buffer) && wp->w_llist_ref != NULL)
// Quickfix and location list stack check helper macros
-#define IS_QF_STACK(qi) (qi == &ql_info)
-#define IS_LL_STACK(qi) (qi != &ql_info)
+#define IS_QF_STACK(qi) (qi->qfl_type == QFLT_QUICKFIX)
+#define IS_LL_STACK(qi) (qi->qfl_type == QFLT_LOCATION)
+#define IS_QF_LIST(qfl) (qfl->qfl_type == QFLT_QUICKFIX)
+#define IS_LL_LIST(qfl) (qfl->qfl_type == QFLT_LOCATION)
//
// Return location list for window 'wp'
@@ -211,6 +226,14 @@ typedef struct {
//
#define GET_LOC_LIST(wp) (IS_LL_WINDOW(wp) ? wp->w_llist_ref : wp->w_llist)
+// Macro to loop through all the items in a quickfix list
+// Quickfix item index starts from 1, so i below starts at 1
+#define FOR_ALL_QFL_ITEMS(qfl, qfp, i) \
+ for (i = 1, qfp = qfl->qf_start; /* NOLINT(readability/braces) */ \
+ !got_int && i <= qfl->qf_count && qfp != NULL; \
+ i++, qfp = qfp->qf_next)
+
+
// Looking up a buffer can be slow if there are many. Remember the last one
// to make this a lot faster if there are multiple matches in the same file.
static char_u *qf_last_bufname = NULL;
@@ -218,6 +241,50 @@ static bufref_T qf_last_bufref = { NULL, 0, 0 };
static char *e_loc_list_changed = N_("E926: Current location list was changed");
+// Counter to prevent autocmds from freeing up location lists when they are
+// still being used.
+static int quickfix_busy = 0;
+static qf_delq_T *qf_delq_head = NULL;
+
+/// Process the next line from a file/buffer/list/string and add it
+/// to the quickfix list 'qfl'.
+static int qf_init_process_nextline(qf_list_T *qfl,
+ efm_T *fmt_first,
+ qfstate_T *state,
+ qffields_T *fields)
+{
+ int status;
+
+ // Get the next line from a file/buffer/list/string
+ status = qf_get_nextline(state);
+ if (status != QF_OK) {
+ return status;
+ }
+
+ status = qf_parse_line(qfl, state->linebuf, state->linelen,
+ fmt_first, fields);
+ if (status != QF_OK) {
+ return status;
+ }
+
+ return qf_add_entry(qfl,
+ qfl->qf_directory,
+ (*fields->namebuf || qfl->qf_directory != NULL)
+ ? fields->namebuf
+ : ((qfl->qf_currfile != NULL && fields->valid)
+ ? qfl->qf_currfile : (char_u *)NULL),
+ fields->module,
+ 0,
+ fields->errmsg,
+ fields->lnum,
+ fields->col,
+ fields->use_viscol,
+ fields->pattern,
+ fields->enr,
+ fields->type,
+ fields->valid);
+}
+
/// Read the errorfile "efile" into memory, line by line, building the error
/// list. Set the error list's title to qf_title.
///
@@ -229,8 +296,9 @@ static char *e_loc_list_changed = N_("E926: Current location list was changed");
/// @params enc If non-NULL, encoding used to parse errors
///
/// @returns -1 for error, number of errors for success.
-int qf_init(win_T *wp, char_u *efile, char_u *errorformat, int newlist,
- char_u *qf_title, char_u *enc)
+int qf_init(win_T *wp, const char_u *restrict efile,
+ char_u *restrict errorformat, int newlist,
+ const char_u *restrict qf_title, char_u *restrict enc)
{
qf_info_T *qi = &ql_info;
@@ -264,15 +332,151 @@ static struct fmtpattern
{ 'o', ".\\+" }
};
+/// Convert an errorformat pattern to a regular expression pattern.
+/// See fmt_pat definition above for the list of supported patterns. The
+/// pattern specifier is supplied in "efmpat". The converted pattern is stored
+/// in "regpat". Returns a pointer to the location after the pattern.
+static char_u * efmpat_to_regpat(
+ const char_u *efmpat,
+ char_u *regpat,
+ efm_T *efminfo,
+ int idx,
+ int round,
+ char_u *errmsg,
+ size_t errmsglen)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (efminfo->addr[idx]) {
+ // Each errorformat pattern can occur only once
+ snprintf((char *)errmsg, errmsglen,
+ _("E372: Too many %%%c in format string"), *efmpat);
+ EMSG(errmsg);
+ return NULL;
+ }
+ if ((idx && idx < 6
+ && vim_strchr((char_u *)"DXOPQ", efminfo->prefix) != NULL)
+ || (idx == 6
+ && vim_strchr((char_u *)"OPQ", efminfo->prefix) == NULL)) {
+ snprintf((char *)errmsg, errmsglen,
+ _("E373: Unexpected %%%c in format string"), *efmpat);
+ EMSG(errmsg);
+ return NULL;
+ }
+ efminfo->addr[idx] = (char_u)++round;
+ *regpat++ = '\\';
+ *regpat++ = '(';
+#ifdef BACKSLASH_IN_FILENAME
+ if (*efmpat == 'f') {
+ // Also match "c:" in the file name, even when
+ // checking for a colon next: "%f:".
+ // "\%(\a:\)\="
+ STRCPY(regpat, "\\%(\\a:\\)\\=");
+ regpat += 10;
+ }
+#endif
+ if (*efmpat == 'f' && efmpat[1] != NUL) {
+ if (efmpat[1] != '\\' && efmpat[1] != '%') {
+ // A file name may contain spaces, but this isn't
+ // in "\f". For "%f:%l:%m" there may be a ":" in
+ // the file name. Use ".\{-1,}x" instead (x is
+ // the next character), the requirement that :999:
+ // follows should work.
+ STRCPY(regpat, ".\\{-1,}");
+ regpat += 7;
+ } else {
+ // File name followed by '\\' or '%': include as
+ // many file name chars as possible.
+ STRCPY(regpat, "\\f\\+");
+ regpat += 4;
+ }
+ } else {
+ char_u *srcptr = (char_u *)fmt_pat[idx].pattern;
+ while ((*regpat = *srcptr++) != NUL) {
+ regpat++;
+ }
+ }
+ *regpat++ = '\\';
+ *regpat++ = ')';
+
+ return regpat;
+}
+
+/// Convert a scanf like format in 'errorformat' to a regular expression.
+/// Returns a pointer to the location after the pattern.
+static char_u * scanf_fmt_to_regpat(
+ const char_u **pefmp,
+ const char_u *efm,
+ int len,
+ char_u *regpat,
+ char_u *errmsg,
+ size_t errmsglen)
+ FUNC_ATTR_NONNULL_ALL
+{
+ const char_u *efmp = *pefmp;
+
+ if (*efmp == '[' || *efmp == '\\') {
+ if ((*regpat++ = *efmp) == '[') { // %*[^a-z0-9] etc.
+ if (efmp[1] == '^') {
+ *regpat++ = *++efmp;
+ }
+ if (efmp < efm + len) {
+ *regpat++ = *++efmp; // could be ']'
+ while (efmp < efm + len && (*regpat++ = *++efmp) != ']') {
+ }
+ if (efmp == efm + len) {
+ EMSG(_("E374: Missing ] in format string"));
+ return NULL;
+ }
+ }
+ } else if (efmp < efm + len) { // %*\D, %*\s etc.
+ *regpat++ = *++efmp;
+ }
+ *regpat++ = '\\';
+ *regpat++ = '+';
+ } else {
+ // TODO(vim): scanf()-like: %*ud, %*3c, %*f, ... ?
+ snprintf((char *)errmsg, errmsglen,
+ _("E375: Unsupported %%%c in format string"), *efmp);
+ EMSG(errmsg);
+ return NULL;
+ }
+
+ *pefmp = efmp;
+
+ return regpat;
+}
+
+/// Analyze/parse an errorformat prefix.
+static const char_u *efm_analyze_prefix(const char_u *efmp, efm_T *efminfo,
+ char_u *errmsg, size_t errmsglen)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (vim_strchr((char_u *)"+-", *efmp) != NULL) {
+ efminfo->flags = *efmp++;
+ }
+ if (vim_strchr((char_u *)"DXAEWICZGOPQ", *efmp) != NULL) {
+ efminfo->prefix = *efmp;
+ } else {
+ snprintf((char *)errmsg, errmsglen,
+ _("E376: Invalid %%%c in format string prefix"), *efmp);
+ EMSG(errmsg);
+ return NULL;
+ }
+
+ return efmp;
+}
+
+
// Converts a 'errorformat' string to regular expression pattern
-static int efm_to_regpat(char_u *efm, int len, efm_T *fmt_ptr,
- char_u *regpat, char_u *errmsg)
+static int efm_to_regpat(const char_u *efm, int len, efm_T *fmt_ptr,
+ char_u *regpat, char_u *errmsg, size_t errmsglen)
+ FUNC_ATTR_NONNULL_ALL
{
// Build regexp pattern from current 'errorformat' option
char_u *ptr = regpat;
*ptr++ = '^';
int round = 0;
- for (char_u *efmp = efm; efmp < efm + len; efmp++) {
+ for (const char_u *efmp = efm; efmp < efm + len; efmp++) {
if (*efmp == '%') {
efmp++;
int idx;
@@ -282,90 +486,17 @@ static int efm_to_regpat(char_u *efm, int len, efm_T *fmt_ptr,
}
}
if (idx < FMT_PATTERNS) {
- if (fmt_ptr->addr[idx]) {
- snprintf((char *)errmsg, CMDBUFFSIZE + 1,
- _("E372: Too many %%%c in format string"), *efmp);
- EMSG(errmsg);
- return -1;
- }
- if ((idx
- && idx < 6
- && vim_strchr((char_u *)"DXOPQ", fmt_ptr->prefix) != NULL)
- || (idx == 6
- && vim_strchr((char_u *)"OPQ", fmt_ptr->prefix) == NULL)) {
- snprintf((char *)errmsg, CMDBUFFSIZE + 1,
- _("E373: Unexpected %%%c in format string"), *efmp);
- EMSG(errmsg);
- return -1;
+ ptr = efmpat_to_regpat(efmp, ptr, fmt_ptr, idx, round, errmsg,
+ errmsglen);
+ if (ptr == NULL) {
+ return FAIL;
}
round++;
- fmt_ptr->addr[idx] = (char_u)round;
- *ptr++ = '\\';
- *ptr++ = '(';
-#ifdef BACKSLASH_IN_FILENAME
- if (*efmp == 'f') {
- // Also match "c:" in the file name, even when
- // checking for a colon next: "%f:".
- // "\%(\a:\)\="
- STRCPY(ptr, "\\%(\\a:\\)\\=");
- ptr += 10;
- }
-#endif
- if (*efmp == 'f' && efmp[1] != NUL) {
- if (efmp[1] != '\\' && efmp[1] != '%') {
- // A file name may contain spaces, but this isn't
- // in "\f". For "%f:%l:%m" there may be a ":" in
- // the file name. Use ".\{-1,}x" instead (x is
- // the next character), the requirement that :999:
- // follows should work.
- STRCPY(ptr, ".\\{-1,}");
- ptr += 7;
- } else {
- // File name followed by '\\' or '%': include as
- // many file name chars as possible.
- STRCPY(ptr, "\\f\\+");
- ptr += 4;
- }
- } else {
- char_u *srcptr = (char_u *)fmt_pat[idx].pattern;
- while ((*ptr = *srcptr++) != NUL) {
- ptr++;
- }
- }
- *ptr++ = '\\';
- *ptr++ = ')';
} else if (*efmp == '*') {
- if (*++efmp == '[' || *efmp == '\\') {
- if ((*ptr++ = *efmp) == '[') { // %*[^a-z0-9] etc.
- if (efmp[1] == '^') {
- *ptr++ = *++efmp;
- }
- if (efmp < efm + len) {
- efmp++;
- *ptr++ = *efmp; // could be ']'
- while (efmp < efm + len) {
- efmp++;
- if ((*ptr++ = *efmp) == ']') {
- break;
- }
- }
- if (efmp == efm + len) {
- EMSG(_("E374: Missing ] in format string"));
- return -1;
- }
- }
- } else if (efmp < efm + len) { // %*\D, %*\s etc.
- efmp++;
- *ptr++ = *efmp;
- }
- *ptr++ = '\\';
- *ptr++ = '+';
- } else {
- // TODO(vim): scanf()-like: %*ud, %*3c, %*f, ... ?
- snprintf((char *)errmsg, CMDBUFFSIZE + 1,
- _("E375: Unsupported %%%c in format string"), *efmp);
- EMSG(errmsg);
- return -1;
+ efmp++;
+ ptr = scanf_fmt_to_regpat(&efmp, efm, len, ptr, errmsg, errmsglen);
+ if (ptr == NULL) {
+ return FAIL;
}
} else if (vim_strchr((char_u *)"%\\.^$~[", *efmp) != NULL) {
*ptr++ = *efmp; // regexp magic characters
@@ -374,22 +505,17 @@ static int efm_to_regpat(char_u *efm, int len, efm_T *fmt_ptr,
} else if (*efmp == '>') {
fmt_ptr->conthere = true;
} else if (efmp == efm + 1) { // analyse prefix
- if (vim_strchr((char_u *)"+-", *efmp) != NULL) {
- fmt_ptr->flags = *efmp++;
- }
- if (vim_strchr((char_u *)"DXAEWICZGOPQ", *efmp) != NULL) {
- fmt_ptr->prefix = *efmp;
- } else {
- snprintf((char *)errmsg, CMDBUFFSIZE + 1,
- _("E376: Invalid %%%c in format string prefix"), *efmp);
- EMSG(errmsg);
- return -1;
+ // prefix is allowed only at the beginning of the errorformat
+ // option part
+ efmp = efm_analyze_prefix(efmp, fmt_ptr, errmsg, errmsglen);
+ if (efmp == NULL) {
+ return FAIL;
}
} else {
snprintf((char *)errmsg, CMDBUFFSIZE + 1,
_("E377: Invalid %%%c in format string"), *efmp);
EMSG(errmsg);
- return -1;
+ return FAIL;
}
} else { // copy normal character
if (*efmp == '\\' && efmp + 1 < efm + len) {
@@ -405,7 +531,7 @@ static int efm_to_regpat(char_u *efm, int len, efm_T *fmt_ptr,
*ptr++ = '$';
*ptr = NUL;
- return 0;
+ return OK;
}
static efm_T *fmt_start = NULL; // cached across qf_parse_line() calls
@@ -421,7 +547,42 @@ static void free_efm_list(efm_T **efm_first)
fmt_start = NULL;
}
-// Parse 'errorformat' option
+/// Compute the size of the buffer used to convert a 'errorformat' pattern into
+/// a regular expression pattern.
+static size_t efm_regpat_bufsz(char_u *efm)
+{
+ size_t sz;
+
+ sz = (FMT_PATTERNS * 3) + (STRLEN(efm) << 2);
+ for (int i = FMT_PATTERNS - 1; i >= 0; ) {
+ sz += STRLEN(fmt_pat[i--].pattern);
+ }
+#ifdef BACKSLASH_IN_FILENAME
+ sz += 12; // "%f" can become twelve chars longer (see efm_to_regpat)
+#else
+ sz += 2; // "%f" can become two chars longer
+#endif
+
+ return sz;
+}
+
+/// Return the length of a 'errorformat' option part (separated by ",").
+static int efm_option_part_len(char_u *efm)
+{
+ int len;
+
+ for (len = 0; efm[len] != NUL && efm[len] != ','; len++) {
+ if (efm[len] == '\\' && efm[len + 1] != NUL) {
+ len++;
+ }
+ }
+
+ return len;
+}
+
+/// Parse the 'errorformat' option. Multiple parts in the 'errorformat' option
+/// are parsed and converted to regular expressions. Returns information about
+/// the parsed 'errorformat' option.
static efm_T * parse_efm_option(char_u *efm)
{
efm_T *fmt_ptr = NULL;
@@ -433,16 +594,8 @@ static efm_T * parse_efm_option(char_u *efm)
char_u *errmsg = xmalloc(errmsglen);
// Get some space to modify the format string into.
- size_t i = (FMT_PATTERNS * 3) + (STRLEN(efm) << 2);
- for (int round = FMT_PATTERNS - 1; round >= 0; ) {
- i += STRLEN(fmt_pat[round--].pattern);
- }
-#ifdef BACKSLASH_IN_FILENAME
- i += 12; // "%f" can become twelve chars longer (see efm_to_regpat)
-#else
- i += 2; // "%f" can become two chars longer
-#endif
- char_u *fmtstr = xmalloc(i);
+ size_t sz = efm_regpat_bufsz(efm);
+ char_u *fmtstr = xmalloc(sz);
while (efm[0] != NUL) {
// Allocate a new eformat structure and put it at the end of the list
@@ -455,13 +608,9 @@ static efm_T * parse_efm_option(char_u *efm)
fmt_last = fmt_ptr;
// Isolate one part in the 'errorformat' option
- for (len = 0; efm[len] != NUL && efm[len] != ','; len++) {
- if (efm[len] == '\\' && efm[len + 1] != NUL) {
- len++;
- }
- }
+ len = efm_option_part_len(efm);
- if (efm_to_regpat(efm, len, fmt_ptr, fmtstr, errmsg) == -1) {
+ if (efm_to_regpat(efm, len, fmt_ptr, fmtstr, errmsg, errmsglen) == FAIL) {
goto parse_efm_error;
}
if ((fmt_ptr->prog = vim_regcomp(fmtstr, RE_MAGIC + RE_STRING)) == NULL) {
@@ -487,6 +636,7 @@ parse_efm_end:
return fmt_first;
}
+/// Allocate more memory for the line buffer used for parsing lines.
static char_u *qf_grow_linebuf(qfstate_T *state, size_t newsz)
{
// If the line exceeds LINE_MAXLEN exclude the last
@@ -728,25 +878,41 @@ static int qf_get_nextline(qfstate_T *state)
return QF_OK;
}
-// Returns true if the specified quickfix/location list is empty.
-static bool qf_list_empty(const qf_info_T *qi, int qf_idx)
+/// Returns true if the specified quickfix/location stack is empty
+static bool qf_stack_empty(const qf_info_T *qi)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
- if (qi == NULL || qf_idx < 0 || qf_idx >= LISTCOUNT) {
- return true;
- }
- return qi->qf_lists[qf_idx].qf_count <= 0;
+ return qi == NULL || qi->qf_listcount <= 0;
+}
+
+/// Returns true if the specified quickfix/location list is empty.
+static bool qf_list_empty(qf_list_T *qfl)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ return qfl == NULL || qfl->qf_count <= 0;
+}
+
+/// Returns true if the specified quickfix/location list is not empty and
+/// has valid entries.
+static bool qf_list_has_valid_entries(qf_list_T *qfl)
+{
+ return !qf_list_empty(qfl) && !qfl->qf_nonevalid;
+}
+
+/// Return a pointer to a list in the specified quickfix stack
+static qf_list_T * qf_get_list(qf_info_T *qi, int idx)
+{
+ return &qi->qf_lists[idx];
}
/// Parse a line and get the quickfix fields.
/// Return the QF_ status.
-static int qf_parse_line(qf_info_T *qi, int qf_idx, char_u *linebuf,
+static int qf_parse_line(qf_list_T *qfl, char_u *linebuf,
size_t linelen, efm_T *fmt_first, qffields_T *fields)
{
efm_T *fmt_ptr;
int idx = 0;
char_u *tail = NULL;
- qf_list_T *qfl = &qi->qf_lists[qf_idx];
int status;
restofline:
@@ -802,7 +968,7 @@ restofline:
qfl->qf_multiignore = false; // reset continuation
} else if (vim_strchr((char_u *)"CZ", idx) != NULL) {
// continuation of multi-line msg
- status = qf_parse_multiline_pfx(qi, qf_idx, idx, qfl, fields);
+ status = qf_parse_multiline_pfx(idx, qfl, fields);
if (status != QF_OK) {
return status;
}
@@ -825,6 +991,79 @@ restofline:
return QF_OK;
}
+// Allocate the fields used for parsing lines and populating a quickfix list.
+static void qf_alloc_fields(qffields_T *pfields)
+ FUNC_ATTR_NONNULL_ALL
+{
+ pfields->namebuf = xmalloc(CMDBUFFSIZE + 1);
+ pfields->module = xmalloc(CMDBUFFSIZE + 1);
+ pfields->errmsglen = CMDBUFFSIZE + 1;
+ pfields->errmsg = xmalloc(pfields->errmsglen);
+ pfields->pattern = xmalloc(CMDBUFFSIZE + 1);
+}
+
+// Free the fields used for parsing lines and populating a quickfix list.
+static void qf_free_fields(qffields_T *pfields)
+ FUNC_ATTR_NONNULL_ALL
+{
+ xfree(pfields->namebuf);
+ xfree(pfields->module);
+ xfree(pfields->errmsg);
+ xfree(pfields->pattern);
+}
+
+// Setup the state information used for parsing lines and populating a
+// quickfix list.
+static int qf_setup_state(
+ qfstate_T *pstate,
+ char_u *restrict enc,
+ const char_u *restrict efile,
+ typval_T *tv,
+ buf_T *buf,
+ linenr_T lnumfirst,
+ linenr_T lnumlast)
+ FUNC_ATTR_NONNULL_ARG(1)
+{
+ pstate->vc.vc_type = CONV_NONE;
+ if (enc != NULL && *enc != NUL) {
+ convert_setup(&pstate->vc, enc, p_enc);
+ }
+
+ if (efile != NULL
+ && (pstate->fd = os_fopen((const char *)efile, "r")) == NULL) {
+ EMSG2(_(e_openerrf), efile);
+ return FAIL;
+ }
+
+ if (tv != NULL) {
+ if (tv->v_type == VAR_STRING) {
+ pstate->p_str = tv->vval.v_string;
+ } else if (tv->v_type == VAR_LIST) {
+ pstate->p_li = tv_list_first(tv->vval.v_list);
+ }
+ pstate->tv = tv;
+ }
+ pstate->buf = buf;
+ pstate->buflnum = lnumfirst;
+ pstate->lnumlast = lnumlast;
+
+ return OK;
+}
+
+// Cleanup the state information used for parsing lines and populating a
+// quickfix list.
+static void qf_cleanup_state(qfstate_T *pstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (pstate->fd != NULL) {
+ fclose(pstate->fd);
+ }
+ xfree(pstate->growbuf);
+ if (pstate->vc.vc_type != CONV_NONE) {
+ convert_setup(&pstate->vc, NULL, NULL);
+ }
+}
+
// Read the errorfile "efile" into memory, line by line, building the error
// list.
// Alternative: when "efile" is NULL read errors from buffer "buf".
@@ -837,19 +1076,20 @@ static int
qf_init_ext(
qf_info_T *qi,
int qf_idx,
- char_u *efile,
+ const char_u *restrict efile,
buf_T *buf,
typval_T *tv,
- char_u *errorformat,
- int newlist, // TRUE: start a new error list
+ char_u *restrict errorformat,
+ bool newlist, // true: start a new error list
linenr_T lnumfirst, // first line number to use
linenr_T lnumlast, // last line number to use
- char_u *qf_title,
- char_u *enc
+ const char_u *restrict qf_title,
+ char_u *restrict enc
)
+ FUNC_ATTR_NONNULL_ARG(1)
{
- qfstate_T state;
- qffields_T fields;
+ qfstate_T state = { 0 };
+ qffields_T fields = { 0 };
qfline_T *old_last = NULL;
bool adding = false;
static efm_T *fmt_first = NULL;
@@ -861,21 +1101,9 @@ qf_init_ext(
// Do not used the cached buffer, it may have been wiped out.
XFREE_CLEAR(qf_last_bufname);
- memset(&state, 0, sizeof(state));
- memset(&fields, 0, sizeof(fields));
- state.vc.vc_type = CONV_NONE;
- if (enc != NULL && *enc != NUL) {
- convert_setup(&state.vc, enc, p_enc);
- }
-
- fields.namebuf = xmalloc(CMDBUFFSIZE + 1);
- fields.module = xmalloc(CMDBUFFSIZE + 1);
- fields.errmsglen = CMDBUFFSIZE + 1;
- fields.errmsg = xmalloc(fields.errmsglen);
- fields.pattern = xmalloc(CMDBUFFSIZE + 1);
-
- if (efile != NULL && (state.fd = os_fopen((char *)efile, "r")) == NULL) {
- EMSG2(_(e_openerrf), efile);
+ qf_alloc_fields(&fields);
+ if (qf_setup_state(&state, enc, efile, tv, buf,
+ lnumfirst, lnumlast) == FAIL) {
goto qf_init_end;
}
@@ -886,12 +1114,12 @@ qf_init_ext(
} else {
// Adding to existing list, use last entry.
adding = true;
- if (qi->qf_lists[qf_idx].qf_count > 0) {
+ if (!qf_list_empty(qf_get_list(qi, qf_idx) )) {
old_last = qi->qf_lists[qf_idx].qf_last;
}
}
- qf_list_T *qfl = &qi->qf_lists[qf_idx];
+ qf_list_T *qfl = qf_get_list(qi, qf_idx);
// Use the local value of 'errorformat' if it's set.
if (errorformat == p_efm && tv == NULL && buf && *buf->b_p_efm != NUL) {
@@ -918,63 +1146,21 @@ qf_init_ext(
goto error2;
}
- /*
- * got_int is reset here, because it was probably set when killing the
- * ":make" command, but we still want to read the errorfile then.
- */
- got_int = FALSE;
+ // got_int is reset here, because it was probably set when killing the
+ // ":make" command, but we still want to read the errorfile then.
+ got_int = false;
- if (tv != NULL) {
- if (tv->v_type == VAR_STRING) {
- state.p_str = tv->vval.v_string;
- } else if (tv->v_type == VAR_LIST) {
- state.p_list = tv->vval.v_list;
- state.p_li = tv_list_first(tv->vval.v_list);
- }
- state.tv = tv;
- }
- state.buf = buf;
- state.buflnum = lnumfirst;
- state.lnumlast = lnumlast;
-
- /*
- * Read the lines in the error file one by one.
- * Try to recognize one of the error formats in each line.
- */
+ // Read the lines in the error file one by one.
+ // Try to recognize one of the error formats in each line.
while (!got_int) {
- // Get the next line from a file/buffer/list/string
- status = qf_get_nextline(&state);
+ status = qf_init_process_nextline(qfl, fmt_first, &state, &fields);
if (status == QF_END_OF_INPUT) { // end of input
break;
}
-
- status = qf_parse_line(qi, qf_idx, state.linebuf, state.linelen,
- fmt_first, &fields);
if (status == QF_FAIL) {
goto error2;
}
- if (status == QF_IGNORE_LINE) {
- continue;
- }
- if (qf_add_entry(qi,
- qf_idx,
- qfl->qf_directory,
- (*fields.namebuf || qfl->qf_directory)
- ? fields.namebuf : ((qfl->qf_currfile && fields.valid)
- ? qfl->qf_currfile : (char_u *)NULL),
- fields.module,
- 0,
- fields.errmsg,
- fields.lnum,
- fields.col,
- fields.use_viscol,
- fields.pattern,
- fields.enr,
- fields.type,
- fields.valid) == FAIL) {
- goto error2;
- }
line_breakcheck();
}
if (state.fd == NULL || !ferror(state.fd)) {
@@ -997,45 +1183,34 @@ qf_init_ext(
error2:
if (!adding) {
// Error when creating a new list. Free the new list
- qf_free(qi, qi->qf_curlist);
+ qf_free(qfl);
qi->qf_listcount--;
if (qi->qf_curlist > 0) {
qi->qf_curlist--;
}
}
qf_init_end:
- if (state.fd != NULL) {
- fclose(state.fd);
- }
- xfree(fields.namebuf);
- xfree(fields.module);
- xfree(fields.errmsg);
- xfree(fields.pattern);
- xfree(state.growbuf);
-
if (qf_idx == qi->qf_curlist) {
qf_update_buffer(qi, old_last);
}
-
- if (state.vc.vc_type != CONV_NONE) {
- convert_setup(&state.vc, NULL, NULL);
- }
+ qf_cleanup_state(&state);
+ qf_free_fields(&fields);
return retval;
}
/// Set the title of the specified quickfix list. Frees the previous title.
/// Prepends ':' to the title.
-static void qf_store_title(qf_info_T *qi, int qf_idx, const char_u *title)
+static void qf_store_title(qf_list_T *qfl, const char_u *title)
FUNC_ATTR_NONNULL_ARG(1)
{
- XFREE_CLEAR(qi->qf_lists[qf_idx].qf_title);
+ XFREE_CLEAR(qfl->qf_title);
if (title != NULL) {
size_t len = STRLEN(title) + 1;
char_u *p = xmallocz(len);
- qi->qf_lists[qf_idx].qf_title = p;
+ qfl->qf_title = p;
xstrlcpy((char *)p, (const char *)title, len + 1);
}
}
@@ -1053,35 +1228,254 @@ static char_u * qf_cmdtitle(char_u *cmd)
return qftitle_str;
}
-// Prepare for adding a new quickfix list. If the current list is in the
-// middle of the stack, then all the following lists are freed and then
-// the new list is added.
-static void qf_new_list(qf_info_T *qi, char_u *qf_title)
+/// Return a pointer to the current list in the specified quickfix stack
+static qf_list_T * qf_get_curlist(qf_info_T *qi)
+{
+ return qf_get_list(qi, qi->qf_curlist);
+}
+
+/// Prepare for adding a new quickfix list. If the current list is in the
+/// middle of the stack, then all the following lists are freed and then
+/// the new list is added.
+static void qf_new_list(qf_info_T *qi, const char_u *qf_title)
{
int i;
+ qf_list_T *qfl;
// If the current entry is not the last entry, delete entries beyond
// the current entry. This makes it possible to browse in a tree-like
// way with ":grep'.
- while (qi->qf_listcount > qi->qf_curlist + 1)
- qf_free(qi, --qi->qf_listcount);
+ while (qi->qf_listcount > qi->qf_curlist + 1) {
+ qf_free(&qi->qf_lists[--qi->qf_listcount]);
+ }
- /*
- * When the stack is full, remove to oldest entry
- * Otherwise, add a new entry.
- */
+ // When the stack is full, remove to oldest entry
+ // Otherwise, add a new entry.
if (qi->qf_listcount == LISTCOUNT) {
- qf_free(qi, 0);
- for (i = 1; i < LISTCOUNT; ++i)
+ qf_free(&qi->qf_lists[0]);
+ for (i = 1; i < LISTCOUNT; i++) {
qi->qf_lists[i - 1] = qi->qf_lists[i];
+ }
qi->qf_curlist = LISTCOUNT - 1;
} else
qi->qf_curlist = qi->qf_listcount++;
- memset(&qi->qf_lists[qi->qf_curlist], 0, (size_t)(sizeof(qf_list_T)));
- qf_store_title(qi, qi->qf_curlist, qf_title);
- qi->qf_lists[qi->qf_curlist].qf_id = ++last_qf_id;
+ qfl = qf_get_curlist(qi);
+ memset(qfl, 0, sizeof(qf_list_T));
+ qf_store_title(qfl, qf_title);
+ qfl->qfl_type = qi->qfl_type;
+ qfl->qf_id = ++last_qf_id;
+}
+
+/// Parse the match for filename ('%f') pattern in regmatch.
+/// Return the matched value in "fields->namebuf".
+static int qf_parse_fmt_f(regmatch_T *rmp,
+ int midx,
+ qffields_T *fields,
+ int prefix)
+{
+ char_u c;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+
+ // Expand ~/file and $HOME/file to full path.
+ c = *rmp->endp[midx];
+ *rmp->endp[midx] = NUL;
+ expand_env(rmp->startp[midx], fields->namebuf, CMDBUFFSIZE);
+ *rmp->endp[midx] = c;
+
+ // For separate filename patterns (%O, %P and %Q), the specified file
+ // should exist.
+ if (vim_strchr((char_u *)"OPQ", prefix) != NULL
+ && !os_path_exists(fields->namebuf)) {
+ return QF_FAIL;
+ }
+
+ return QF_OK;
+}
+
+/// Parse the match for error number ('%n') pattern in regmatch.
+/// Return the matched value in "fields->enr".
+static int qf_parse_fmt_n(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->enr = (int)atol((char *)rmp->startp[midx]);
+ return QF_OK;
+}
+
+/// Parse the match for line number (%l') pattern in regmatch.
+/// Return the matched value in "fields->lnum".
+static int qf_parse_fmt_l(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->lnum = atol((char *)rmp->startp[midx]);
+ return QF_OK;
+}
+
+/// Parse the match for column number ('%c') pattern in regmatch.
+/// Return the matched value in "fields->col".
+static int qf_parse_fmt_c(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->col = (int)atol((char *)rmp->startp[midx]);
+ return QF_OK;
+}
+
+/// Parse the match for error type ('%t') pattern in regmatch.
+/// Return the matched value in "fields->type".
+static int qf_parse_fmt_t(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->type = *rmp->startp[midx];
+ return QF_OK;
+}
+
+/// Parse the match for '%+' format pattern. The whole matching line is included
+/// in the error string. Return the matched line in "fields->errmsg".
+static int qf_parse_fmt_plus(char_u *linebuf,
+ size_t linelen,
+ qffields_T *fields)
+{
+ if (linelen >= fields->errmsglen) {
+ // linelen + null terminator
+ fields->errmsg = xrealloc(fields->errmsg, linelen + 1);
+ fields->errmsglen = linelen + 1;
+ }
+ STRLCPY(fields->errmsg, linebuf, linelen + 1);
+ return QF_OK;
+}
+
+/// Parse the match for error message ('%m') pattern in regmatch.
+/// Return the matched value in "fields->errmsg".
+static int qf_parse_fmt_m(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ size_t len;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ len = (size_t)(rmp->endp[midx] - rmp->startp[midx]);
+ if (len >= fields->errmsglen) {
+ // len + null terminator
+ fields->errmsg = xrealloc(fields->errmsg, len + 1);
+ fields->errmsglen = len + 1;
+ }
+ STRLCPY(fields->errmsg, rmp->startp[midx], len + 1);
+ return QF_OK;
}
+/// Parse the match for rest of a single-line file message ('%r') pattern.
+/// Return the matched value in "tail".
+static int qf_parse_fmt_r(regmatch_T *rmp, int midx, char_u **tail)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ *tail = rmp->startp[midx];
+ return QF_OK;
+}
+
+/// Parse the match for the pointer line ('%p') pattern in regmatch.
+/// Return the matched value in "fields->col".
+static int qf_parse_fmt_p(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ char_u *match_ptr;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->col = 0;
+ for (match_ptr = rmp->startp[midx]; match_ptr != rmp->endp[midx];
+ match_ptr++) {
+ fields->col++;
+ if (*match_ptr == TAB) {
+ fields->col += 7;
+ fields->col -= fields->col % 8;
+ }
+ }
+ fields->col++;
+ fields->use_viscol = true;
+ return QF_OK;
+}
+
+/// Parse the match for the virtual column number ('%v') pattern in regmatch.
+/// Return the matched value in "fields->col".
+static int qf_parse_fmt_v(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ if (rmp->startp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ fields->col = (int)atol((char *)rmp->startp[midx]);
+ fields->use_viscol = true;
+ return QF_OK;
+}
+
+/// Parse the match for the search text ('%s') pattern in regmatch.
+/// Return the matched value in "fields->pattern".
+static int qf_parse_fmt_s(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ size_t len;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ len = (size_t)(rmp->endp[midx] - rmp->startp[midx]);
+ if (len > CMDBUFFSIZE - 5) {
+ len = CMDBUFFSIZE - 5;
+ }
+ STRCPY(fields->pattern, "^\\V");
+ xstrlcat((char *)fields->pattern, (char *)rmp->startp[midx], len + 4);
+ fields->pattern[len + 3] = '\\';
+ fields->pattern[len + 4] = '$';
+ fields->pattern[len + 5] = NUL;
+ return QF_OK;
+}
+
+/// Parse the match for the module ('%o') pattern in regmatch.
+/// Return the matched value in "fields->module".
+static int qf_parse_fmt_o(regmatch_T *rmp, int midx, qffields_T *fields)
+{
+ size_t len;
+ size_t dsize;
+
+ if (rmp->startp[midx] == NULL || rmp->endp[midx] == NULL) {
+ return QF_FAIL;
+ }
+ len = (size_t)(rmp->endp[midx] - rmp->startp[midx]);
+ dsize = STRLEN(fields->module) + len + 1;
+ if (dsize > CMDBUFFSIZE) {
+ dsize = CMDBUFFSIZE;
+ }
+ xstrlcat((char *)fields->module, (char *)rmp->startp[midx], dsize);
+ return QF_OK;
+}
+
+/// 'errorformat' format pattern parser functions.
+/// The '%f' and '%r' formats are parsed differently from other formats.
+/// See qf_parse_match() for details.
+static int (*qf_parse_fmt[FMT_PATTERNS])(regmatch_T *, int, qffields_T *) = {
+ NULL,
+ qf_parse_fmt_n,
+ qf_parse_fmt_l,
+ qf_parse_fmt_c,
+ qf_parse_fmt_t,
+ qf_parse_fmt_m,
+ NULL,
+ qf_parse_fmt_p,
+ qf_parse_fmt_v,
+ qf_parse_fmt_s,
+ qf_parse_fmt_o
+};
+
/// Parse the error format matches in 'regmatch' and set the values in 'fields'.
/// fmt_ptr contains the 'efm' format specifiers/prefixes that have a match.
/// Returns QF_OK if all the matches are successfully parsed. On failure,
@@ -1092,7 +1486,8 @@ static int qf_parse_match(char_u *linebuf, size_t linelen, efm_T *fmt_ptr,
{
char_u idx = fmt_ptr->prefix;
int i;
- size_t len;
+ int midx;
+ int status;
if ((idx == 'C' || idx == 'Z') && !qf_multiline) {
return QF_FAIL;
@@ -1106,118 +1501,26 @@ static int qf_parse_match(char_u *linebuf, size_t linelen, efm_T *fmt_ptr,
// Extract error message data from matched line.
// We check for an actual submatch, because "\[" and "\]" in
// the 'errorformat' may cause the wrong submatch to be used.
- if ((i = (int)fmt_ptr->addr[0]) > 0) { // %f
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
-
- // Expand ~/file and $HOME/file to full path.
- char_u c = *regmatch->endp[i];
- *regmatch->endp[i] = NUL;
- expand_env(regmatch->startp[i], fields->namebuf, CMDBUFFSIZE);
- *regmatch->endp[i] = c;
-
- if (vim_strchr((char_u *)"OPQ", idx) != NULL
- && !os_path_exists(fields->namebuf)) {
- return QF_FAIL;
- }
- }
- if ((i = (int)fmt_ptr->addr[1]) > 0) { // %n
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->enr = (int)atol((char *)regmatch->startp[i]);
- }
- if ((i = (int)fmt_ptr->addr[2]) > 0) { // %l
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->lnum = atol((char *)regmatch->startp[i]);
- }
- if ((i = (int)fmt_ptr->addr[3]) > 0) { // %c
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->col = (int)atol((char *)regmatch->startp[i]);
- }
- if ((i = (int)fmt_ptr->addr[4]) > 0) { // %t
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->type = *regmatch->startp[i];
- }
- if (fmt_ptr->flags == '+' && !qf_multiscan) { // %+
- if (linelen >= fields->errmsglen) {
- // linelen + null terminator
- fields->errmsg = xrealloc(fields->errmsg, linelen + 1);
- fields->errmsglen = linelen + 1;
- }
- STRLCPY(fields->errmsg, linebuf, linelen + 1);
- } else if ((i = (int)fmt_ptr->addr[5]) > 0) { // %m
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
- len = (size_t)(regmatch->endp[i] - regmatch->startp[i]);
- if (len >= fields->errmsglen) {
- // len + null terminator
- fields->errmsg = xrealloc(fields->errmsg, len + 1);
- fields->errmsglen = len + 1;
- }
- STRLCPY(fields->errmsg, regmatch->startp[i], len + 1);
- }
- if ((i = (int)fmt_ptr->addr[6]) > 0) { // %r
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- *tail = regmatch->startp[i];
- }
- if ((i = (int)fmt_ptr->addr[7]) > 0) { // %p
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
- fields->col = 0;
- char_u *match_ptr;
- for (match_ptr = regmatch->startp[i]; match_ptr != regmatch->endp[i];
- match_ptr++) {
- fields->col++;
- if (*match_ptr == TAB) {
- fields->col += 7;
- fields->col -= fields->col % 8;
+ for (i = 0; i < FMT_PATTERNS; i++) {
+ status = QF_OK;
+ midx = (int)fmt_ptr->addr[i];
+ if (i == 0 && midx > 0) { // %f
+ status = qf_parse_fmt_f(regmatch, midx, fields, idx);
+ } else if (i == 5) {
+ if (fmt_ptr->flags == '+' && !qf_multiscan) { // %+
+ status = qf_parse_fmt_plus(linebuf, linelen, fields);
+ } else if (midx > 0) { // %m
+ status = qf_parse_fmt_m(regmatch, midx, fields);
}
+ } else if (i == 6 && midx > 0) { // %r
+ status = qf_parse_fmt_r(regmatch, midx, tail);
+ } else if (midx > 0) { // others
+ status = (qf_parse_fmt[i])(regmatch, midx, fields);
}
- fields->col++;
- fields->use_viscol = true;
- }
- if ((i = (int)fmt_ptr->addr[8]) > 0) { // %v
- if (regmatch->startp[i] == NULL) {
- return QF_FAIL;
- }
- fields->col = (int)atol((char *)regmatch->startp[i]);
- fields->use_viscol = true;
- }
- if ((i = (int)fmt_ptr->addr[9]) > 0) { // %s
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
- len = (size_t)(regmatch->endp[i] - regmatch->startp[i]);
- if (len > CMDBUFFSIZE - 5) {
- len = CMDBUFFSIZE - 5;
- }
- STRCPY(fields->pattern, "^\\V");
- STRNCAT(fields->pattern, regmatch->startp[i], len);
- fields->pattern[len + 3] = '\\';
- fields->pattern[len + 4] = '$';
- fields->pattern[len + 5] = NUL;
- }
- if ((i = (int)fmt_ptr->addr[10]) > 0) { // %o
- if (regmatch->startp[i] == NULL || regmatch->endp[i] == NULL) {
- return QF_FAIL;
- }
- len = (size_t)(regmatch->endp[i] - regmatch->startp[i]);
- if (len > CMDBUFFSIZE) {
- len = CMDBUFFSIZE;
+
+ if (status != QF_OK) {
+ return status;
}
- STRNCAT(fields->module, regmatch->startp[i], len);
}
return QF_OK;
@@ -1329,8 +1632,7 @@ static int qf_parse_line_nomatch(char_u *linebuf, size_t linelen,
}
/// Parse multi-line error format prefixes (%C and %Z)
-static int qf_parse_multiline_pfx(qf_info_T *qi, int qf_idx, int idx,
- qf_list_T *qfl, qffields_T *fields)
+static int qf_parse_multiline_pfx(int idx, qf_list_T *qfl, qffields_T *fields)
{
if (!qfl->qf_multiignore) {
qfline_T *qfprev = qfl->qf_last;
@@ -1361,7 +1663,7 @@ static int qf_parse_multiline_pfx(qf_info_T *qi, int qf_idx, int idx,
}
qfprev->qf_viscol = fields->use_viscol;
if (!qfprev->qf_fnum) {
- qfprev->qf_fnum = qf_get_fnum(qi, qf_idx, qfl->qf_directory,
+ qfprev->qf_fnum = qf_get_fnum(qfl, qfl->qf_directory,
*fields->namebuf || qfl->qf_directory
? fields->namebuf
: qfl->qf_currfile && fields->valid
@@ -1376,7 +1678,18 @@ static int qf_parse_multiline_pfx(qf_info_T *qi, int qf_idx, int idx,
return QF_IGNORE_LINE;
}
-/// Free a location list.
+/// Queue location list stack delete request.
+static void locstack_queue_delreq(qf_info_T *qi)
+{
+ qf_delq_T *q;
+
+ q = xmalloc(sizeof(qf_delq_T));
+ q->qi = qi;
+ q->next = qf_delq_head;
+ qf_delq_head = q;
+}
+
+/// Free a location list stack
static void ll_free_all(qf_info_T **pqi)
{
int i;
@@ -1385,14 +1698,21 @@ static void ll_free_all(qf_info_T **pqi)
qi = *pqi;
if (qi == NULL)
return;
- *pqi = NULL; /* Remove reference to this list */
+ *pqi = NULL; // Remove reference to this list
qi->qf_refcount--;
if (qi->qf_refcount < 1) {
- /* No references to this location list */
- for (i = 0; i < qi->qf_listcount; ++i)
- qf_free(qi, i);
- xfree(qi);
+ // No references to this location list.
+ // If the location list is still in use, then queue the delete request
+ // to be processed later.
+ if (quickfix_busy > 0) {
+ locstack_queue_delreq(qi);
+ } else {
+ for (i = 0; i < qi->qf_listcount; i++) {
+ qf_free(qf_get_list(qi, i));
+ }
+ xfree(qi);
+ }
}
}
@@ -1403,19 +1723,64 @@ void qf_free_all(win_T *wp)
qf_info_T *qi = &ql_info;
if (wp != NULL) {
- /* location list */
+ // location list
ll_free_all(&wp->w_llist);
ll_free_all(&wp->w_llist_ref);
- } else
- /* quickfix list */
- for (i = 0; i < qi->qf_listcount; ++i)
- qf_free(qi, i);
+ } else {
+ // quickfix list
+ for (i = 0; i < qi->qf_listcount; i++) {
+ qf_free(qf_get_list(qi, i));
+ }
+ }
+}
+
+/// Delay freeing of location list stacks when the quickfix code is running.
+/// Used to avoid problems with autocmds freeing location list stacks when the
+/// quickfix code is still referencing the stack.
+/// Must always call decr_quickfix_busy() exactly once after this.
+static void incr_quickfix_busy(void)
+{
+ quickfix_busy++;
+}
+
+/// Safe to free location list stacks. Process any delayed delete requests.
+static void decr_quickfix_busy(void)
+{
+ quickfix_busy--;
+ if (quickfix_busy == 0) {
+ // No longer referencing the location lists. Process all the pending
+ // delete requests.
+ while (qf_delq_head != NULL) {
+ qf_delq_T *q = qf_delq_head;
+
+ qf_delq_head = q->next;
+ ll_free_all(&q->qi);
+ xfree(q);
+ }
+ }
+#ifdef ABORT_ON_INTERNAL_ERROR
+ if (quickfix_busy < 0) {
+ EMSG("quickfix_busy has become negative");
+ abort();
+ }
+#endif
+}
+
+#if defined(EXITFREE)
+void check_quickfix_busy(void)
+{
+ if (quickfix_busy != 0) {
+ EMSGN("quickfix_busy not zero on exit: %ld", (long)quickfix_busy);
+# ifdef ABORT_ON_INTERNAL_ERROR
+ abort();
+# endif
+ }
}
+#endif
/// Add an entry to the end of the list of errors.
///
-/// @param qi quickfix list
-/// @param qf_idx list index
+/// @param qfl quickfix list entry
/// @param dir optional directory name
/// @param fname file name or NULL
/// @param module module name or NULL
@@ -1429,8 +1794,8 @@ void qf_free_all(win_T *wp)
/// @param type type character
/// @param valid valid entry
///
-/// @returns OK or FAIL.
-static int qf_add_entry(qf_info_T *qi, int qf_idx, char_u *dir, char_u *fname,
+/// @returns QF_OK or QF_FAIL.
+static int qf_add_entry(qf_list_T *qfl, char_u *dir, char_u *fname,
char_u *module, int bufnum, char_u *mesg, long lnum,
int col, char_u vis_col, char_u *pattern, int nr,
char_u type, char_u valid)
@@ -1444,10 +1809,10 @@ static int qf_add_entry(qf_info_T *qi, int qf_idx, char_u *dir, char_u *fname,
qfp->qf_fnum = bufnum;
if (buf != NULL) {
buf->b_has_qf_entry |=
- IS_QF_STACK(qi) ? BUF_HAS_QF_ENTRY : BUF_HAS_LL_ENTRY;
+ IS_QF_LIST(qfl) ? BUF_HAS_QF_ENTRY : BUF_HAS_LL_ENTRY;
}
} else {
- qfp->qf_fnum = qf_get_fnum(qi, qf_idx, dir, fname);
+ qfp->qf_fnum = qf_get_fnum(qfl, dir, fname);
}
qfp->qf_text = vim_strsave(mesg);
qfp->qf_lnum = lnum;
@@ -1470,12 +1835,12 @@ static int qf_add_entry(qf_info_T *qi, int qf_idx, char_u *dir, char_u *fname,
qfp->qf_type = (char_u)type;
qfp->qf_valid = valid;
- lastp = &qi->qf_lists[qf_idx].qf_last;
- if (qi->qf_lists[qf_idx].qf_count == 0) {
+ lastp = &qfl->qf_last;
+ if (qf_list_empty(qfl)) {
// first element in the list
- qi->qf_lists[qf_idx].qf_start = qfp;
- qi->qf_lists[qf_idx].qf_ptr = qfp;
- qi->qf_lists[qf_idx].qf_index = 0;
+ qfl->qf_start = qfp;
+ qfl->qf_ptr = qfp;
+ qfl->qf_index = 0;
qfp->qf_prev = NULL;
} else {
assert(*lastp);
@@ -1485,164 +1850,211 @@ static int qf_add_entry(qf_info_T *qi, int qf_idx, char_u *dir, char_u *fname,
qfp->qf_next = NULL;
qfp->qf_cleared = false;
*lastp = qfp;
- qi->qf_lists[qf_idx].qf_count++;
- if (qi->qf_lists[qf_idx].qf_index == 0 && qfp->qf_valid) {
+ qfl->qf_count++;
+ if (qfl->qf_index == 0 && qfp->qf_valid) {
// first valid entry
- qi->qf_lists[qf_idx].qf_index = qi->qf_lists[qf_idx].qf_count;
- qi->qf_lists[qf_idx].qf_ptr = qfp;
+ qfl->qf_index = qfl->qf_count;
+ qfl->qf_ptr = qfp;
}
- return OK;
+ return QF_OK;
}
-/*
- * Allocate a new location list
- */
-static qf_info_T *ll_new_list(void)
+/// Allocate a new quickfix/location list stack
+static qf_info_T *qf_alloc_stack(qfltype_T qfltype)
+ FUNC_ATTR_NONNULL_RET
{
qf_info_T *qi = xcalloc(1, sizeof(qf_info_T));
qi->qf_refcount++;
+ qi->qfl_type = qfltype;
return qi;
}
-/*
- * Return the location list for window 'wp'.
- * If not present, allocate a location list
- */
+/// Return the location list stack for window 'wp'.
+/// If not present, allocate a location list stack
static qf_info_T *ll_get_or_alloc_list(win_T *wp)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET
{
- if (IS_LL_WINDOW(wp))
- /* For a location list window, use the referenced location list */
+ if (IS_LL_WINDOW(wp)) {
+ // For a location list window, use the referenced location list
return wp->w_llist_ref;
+ }
- /*
- * For a non-location list window, w_llist_ref should not point to a
- * location list.
- */
+ // For a non-location list window, w_llist_ref should not point to a
+ // location list.
ll_free_all(&wp->w_llist_ref);
- if (wp->w_llist == NULL)
- wp->w_llist = ll_new_list(); /* new location list */
+ if (wp->w_llist == NULL) {
+ wp->w_llist = qf_alloc_stack(QFLT_LOCATION); // new location list
+ }
return wp->w_llist;
}
-/*
- * Copy the location list from window "from" to window "to".
- */
-void copy_loclist(win_T *from, win_T *to)
+/// Get the quickfix/location list stack to use for the specified Ex command.
+/// For a location list command, returns the stack for the current window. If
+/// the location list is not found, then returns NULL and prints an error
+/// message if 'print_emsg' is TRUE.
+static qf_info_T * qf_cmd_get_stack(exarg_T *eap, int print_emsg)
+{
+ qf_info_T *qi = &ql_info;
+
+ if (is_loclist_cmd(eap->cmdidx)) {
+ qi = GET_LOC_LIST(curwin);
+ if (qi == NULL) {
+ if (print_emsg) {
+ EMSG(_(e_loclist));
+ }
+ return NULL;
+ }
+ }
+
+ return qi;
+}
+
+/// Get the quickfix/location list stack to use for the specified Ex command.
+/// For a location list command, returns the stack for the current window.
+/// If the location list is not present, then allocates a new one.
+/// For a location list command, sets 'pwinp' to curwin.
+static qf_info_T *qf_cmd_get_or_alloc_stack(const exarg_T *eap, win_T **pwinp)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET
+{
+ qf_info_T *qi = &ql_info;
+
+ if (is_loclist_cmd(eap->cmdidx)) {
+ qi = ll_get_or_alloc_list(curwin);
+ *pwinp = curwin;
+ }
+
+ return qi;
+}
+
+/// Copy location list entries from 'from_qfl' to 'to_qfl'.
+static int copy_loclist_entries(const qf_list_T *from_qfl, qf_list_T *to_qfl)
+ FUNC_ATTR_NONNULL_ALL
{
- qf_info_T *qi;
- int idx;
int i;
+ qfline_T *from_qfp;
+
+ // copy all the location entries in this list
+ FOR_ALL_QFL_ITEMS(from_qfl, from_qfp, i) {
+ if (qf_add_entry(to_qfl,
+ NULL,
+ NULL,
+ from_qfp->qf_module,
+ 0,
+ from_qfp->qf_text,
+ from_qfp->qf_lnum,
+ from_qfp->qf_col,
+ from_qfp->qf_viscol,
+ from_qfp->qf_pattern,
+ from_qfp->qf_nr,
+ 0,
+ from_qfp->qf_valid) == QF_FAIL) {
+ return FAIL;
+ }
- /*
- * When copying from a location list window, copy the referenced
- * location list. For other windows, copy the location list for
- * that window.
- */
- if (IS_LL_WINDOW(from))
+ // qf_add_entry() will not set the qf_num field, as the
+ // directory and file names are not supplied. So the qf_fnum
+ // field is copied here.
+ qfline_T *const prevp = to_qfl->qf_last;
+ prevp->qf_fnum = from_qfp->qf_fnum; // file number
+ prevp->qf_type = from_qfp->qf_type; // error type
+ if (from_qfl->qf_ptr == from_qfp) {
+ to_qfl->qf_ptr = prevp; // current location
+ }
+ }
+
+ return OK;
+}
+
+/// Copy the specified location list 'from_qfl' to 'to_qfl'.
+static int copy_loclist(const qf_list_T *from_qfl, qf_list_T *to_qfl)
+ FUNC_ATTR_NONNULL_ALL
+{
+ // Some of the fields are populated by qf_add_entry()
+ to_qfl->qfl_type = from_qfl->qfl_type;
+ to_qfl->qf_nonevalid = from_qfl->qf_nonevalid;
+ to_qfl->qf_count = 0;
+ to_qfl->qf_index = 0;
+ to_qfl->qf_start = NULL;
+ to_qfl->qf_last = NULL;
+ to_qfl->qf_ptr = NULL;
+ if (from_qfl->qf_title != NULL) {
+ to_qfl->qf_title = vim_strsave(from_qfl->qf_title);
+ } else {
+ to_qfl->qf_title = NULL;
+ }
+ if (from_qfl->qf_ctx != NULL) {
+ to_qfl->qf_ctx = xcalloc(1, sizeof(*to_qfl->qf_ctx));
+ tv_copy(from_qfl->qf_ctx, to_qfl->qf_ctx);
+ } else {
+ to_qfl->qf_ctx = NULL;
+ }
+
+ if (from_qfl->qf_count) {
+ if (copy_loclist_entries(from_qfl, to_qfl) == FAIL) {
+ return FAIL;
+ }
+ }
+
+ to_qfl->qf_index = from_qfl->qf_index; // current index in the list
+
+ // Assign a new ID for the location list
+ to_qfl->qf_id = ++last_qf_id;
+ to_qfl->qf_changedtick = 0L;
+
+ // When no valid entries are present in the list, qf_ptr points to
+ // the first item in the list
+ if (to_qfl->qf_nonevalid) {
+ to_qfl->qf_ptr = to_qfl->qf_start;
+ to_qfl->qf_index = 1;
+ }
+
+ return OK;
+}
+
+// Copy the location list stack 'from' window to 'to' window.
+void copy_loclist_stack(win_T *from, win_T *to)
+ FUNC_ATTR_NONNULL_ALL
+{
+ qf_info_T *qi;
+
+ // When copying from a location list window, copy the referenced
+ // location list. For other windows, copy the location list for
+ // that window.
+ if (IS_LL_WINDOW(from)) {
qi = from->w_llist_ref;
- else
+ } else {
qi = from->w_llist;
+ }
- if (qi == NULL) /* no location list to copy */
+ if (qi == NULL) { // no location list to copy
return;
+ }
- /* allocate a new location list */
- to->w_llist = ll_new_list();
+ // allocate a new location list
+ to->w_llist = qf_alloc_stack(QFLT_LOCATION);
to->w_llist->qf_listcount = qi->qf_listcount;
- /* Copy the location lists one at a time */
- for (idx = 0; idx < qi->qf_listcount; idx++) {
- qf_list_T *from_qfl;
- qf_list_T *to_qfl;
-
+ // Copy the location lists one at a time
+ for (int idx = 0; idx < qi->qf_listcount; idx++) {
to->w_llist->qf_curlist = idx;
- from_qfl = &qi->qf_lists[idx];
- to_qfl = &to->w_llist->qf_lists[idx];
-
- /* Some of the fields are populated by qf_add_entry() */
- to_qfl->qf_nonevalid = from_qfl->qf_nonevalid;
- to_qfl->qf_count = 0;
- to_qfl->qf_index = 0;
- to_qfl->qf_start = NULL;
- to_qfl->qf_last = NULL;
- to_qfl->qf_ptr = NULL;
- if (from_qfl->qf_title != NULL)
- to_qfl->qf_title = vim_strsave(from_qfl->qf_title);
- else
- to_qfl->qf_title = NULL;
-
- if (from_qfl->qf_ctx != NULL) {
- to_qfl->qf_ctx = xcalloc(1, sizeof(typval_T));
- tv_copy(from_qfl->qf_ctx, to_qfl->qf_ctx);
- } else {
- to_qfl->qf_ctx = NULL;
- }
-
- if (from_qfl->qf_count) {
- qfline_T *from_qfp;
- qfline_T *prevp;
-
- // copy all the location entries in this list
- for (i = 0, from_qfp = from_qfl->qf_start;
- i < from_qfl->qf_count && from_qfp != NULL;
- i++, from_qfp = from_qfp->qf_next) {
- if (qf_add_entry(to->w_llist,
- to->w_llist->qf_curlist,
- NULL,
- NULL,
- from_qfp->qf_module,
- 0,
- from_qfp->qf_text,
- from_qfp->qf_lnum,
- from_qfp->qf_col,
- from_qfp->qf_viscol,
- from_qfp->qf_pattern,
- from_qfp->qf_nr,
- 0,
- from_qfp->qf_valid) == FAIL) {
- qf_free_all(to);
- return;
- }
- /*
- * qf_add_entry() will not set the qf_num field, as the
- * directory and file names are not supplied. So the qf_fnum
- * field is copied here.
- */
- prevp = to->w_llist->qf_lists[to->w_llist->qf_curlist].qf_last;
- prevp->qf_fnum = from_qfp->qf_fnum; // file number
- prevp->qf_type = from_qfp->qf_type; // error type
- if (from_qfl->qf_ptr == from_qfp) {
- to_qfl->qf_ptr = prevp; // current location
- }
- }
- }
-
- to_qfl->qf_index = from_qfl->qf_index; /* current index in the list */
-
- // Assign a new ID for the location list
- to_qfl->qf_id = ++last_qf_id;
- to_qfl->qf_changedtick = 0L;
-
- /* When no valid entries are present in the list, qf_ptr points to
- * the first item in the list */
- if (to_qfl->qf_nonevalid) {
- to_qfl->qf_ptr = to_qfl->qf_start;
- to_qfl->qf_index = 1;
+ if (copy_loclist(qf_get_list(qi, idx),
+ qf_get_list(to->w_llist, idx)) == FAIL) {
+ qf_free_all(to);
+ return;
}
}
- to->w_llist->qf_curlist = qi->qf_curlist; /* current list */
+ to->w_llist->qf_curlist = qi->qf_curlist; // current list
}
-// Get buffer number for file "directory/fname".
-// Also sets the b_has_qf_entry flag.
-static int qf_get_fnum(qf_info_T *qi, int qf_idx, char_u *directory,
- char_u *fname)
+/// Get buffer number for file "directory/fname".
+/// Also sets the b_has_qf_entry flag.
+static int qf_get_fnum(qf_list_T *qfl, char_u *directory, char_u *fname )
{
char_u *ptr = NULL;
char_u *bufname;
@@ -1665,7 +2077,7 @@ static int qf_get_fnum(qf_info_T *qi, int qf_idx, char_u *directory,
// directory change.
if (!os_path_exists(ptr)) {
xfree(ptr);
- directory = qf_guess_filepath(qi, qf_idx, fname);
+ directory = qf_guess_filepath(qfl, fname);
if (directory) {
ptr = (char_u *)concat_fnames((char *)directory, (char *)fname, true);
} else {
@@ -1693,7 +2105,7 @@ static int qf_get_fnum(qf_info_T *qi, int qf_idx, char_u *directory,
return 0;
}
buf->b_has_qf_entry =
- IS_QF_STACK(qi) ? BUF_HAS_QF_ENTRY : BUF_HAS_LL_ENTRY;
+ IS_QF_LIST(qfl) ? BUF_HAS_QF_ENTRY : BUF_HAS_LL_ENTRY;
return buf->b_fnum;
}
@@ -1704,22 +2116,21 @@ static char_u *qf_push_dir(char_u *dirbuf, struct dir_stack_T **stackptr,
{
struct dir_stack_T *ds_ptr;
- /* allocate new stack element and hook it in */
+ // allocate new stack element and hook it in
struct dir_stack_T *ds_new = xmalloc(sizeof(struct dir_stack_T));
ds_new->next = *stackptr;
*stackptr = ds_new;
- /* store directory on the stack */
+ // store directory on the stack
if (vim_isAbsName(dirbuf)
|| (*stackptr)->next == NULL
- || (*stackptr && is_file_stack))
+ || (*stackptr && is_file_stack)) {
(*stackptr)->dirname = vim_strsave(dirbuf);
- else {
- /* Okay we don't have an absolute path.
- * dirbuf must be a subdir of one of the directories on the stack.
- * Let's search...
- */
+ } else {
+ // Okay we don't have an absolute path.
+ // dirbuf must be a subdir of one of the directories on the stack.
+ // Let's search...
ds_new = (*stackptr)->next;
(*stackptr)->dirname = NULL;
while (ds_new) {
@@ -1732,7 +2143,7 @@ static char_u *qf_push_dir(char_u *dirbuf, struct dir_stack_T **stackptr,
ds_new = ds_new->next;
}
- /* clean up all dirs we already left */
+ // clean up all dirs we already left
while ((*stackptr)->next != ds_new) {
ds_ptr = (*stackptr)->next;
(*stackptr)->next = (*stackptr)->next->next;
@@ -1740,7 +2151,7 @@ static char_u *qf_push_dir(char_u *dirbuf, struct dir_stack_T **stackptr,
xfree(ds_ptr);
}
- /* Nothing found -> it must be on top level */
+ // Nothing found -> it must be on top level
if (ds_new == NULL) {
xfree((*stackptr)->dirname);
(*stackptr)->dirname = vim_strsave(dirbuf);
@@ -1758,18 +2169,16 @@ static char_u *qf_push_dir(char_u *dirbuf, struct dir_stack_T **stackptr,
}
-/*
- * pop dirbuf from the directory stack and return previous directory or NULL if
- * stack is empty
- */
+// pop dirbuf from the directory stack and return previous directory or NULL if
+// stack is empty
static char_u *qf_pop_dir(struct dir_stack_T **stackptr)
{
struct dir_stack_T *ds_ptr;
- /* TODO: Should we check if dirbuf is the directory on top of the stack?
- * What to do if it isn't? */
+ // TODO(vim): Should we check if dirbuf is the directory on top of the stack?
+ // What to do if it isn't?
- /* pop top element and free it */
+ // pop top element and free it
if (*stackptr != NULL) {
ds_ptr = *stackptr;
*stackptr = (*stackptr)->next;
@@ -1777,13 +2186,11 @@ static char_u *qf_pop_dir(struct dir_stack_T **stackptr)
xfree(ds_ptr);
}
- /* return NEW top element as current dir or NULL if stack is empty*/
+ // return NEW top element as current dir or NULL if stack is empty
return *stackptr ? (*stackptr)->dirname : NULL;
}
-/*
- * clean up directory stack
- */
+// clean up directory stack
static void qf_clean_dir_stack(struct dir_stack_T **stackptr)
{
struct dir_stack_T *ds_ptr;
@@ -1795,32 +2202,29 @@ static void qf_clean_dir_stack(struct dir_stack_T **stackptr)
}
}
-/*
- * Check in which directory of the directory stack the given file can be
- * found.
- * Returns a pointer to the directory name or NULL if not found.
- * Cleans up intermediate directory entries.
- *
- * TODO: How to solve the following problem?
- * If we have the this directory tree:
- * ./
- * ./aa
- * ./aa/bb
- * ./bb
- * ./bb/x.c
- * and make says:
- * making all in aa
- * making all in bb
- * x.c:9: Error
- * Then qf_push_dir thinks we are in ./aa/bb, but we are in ./bb.
- * qf_guess_filepath will return NULL.
- */
-static char_u *qf_guess_filepath(qf_info_T *qi, int qf_idx, char_u *filename)
+/// Check in which directory of the directory stack the given file can be
+/// found.
+/// Returns a pointer to the directory name or NULL if not found.
+/// Cleans up intermediate directory entries.
+///
+/// TODO(vim): How to solve the following problem?
+/// If we have this directory tree:
+/// ./
+/// ./aa
+/// ./aa/bb
+/// ./bb
+/// ./bb/x.c
+/// and make says:
+/// making all in aa
+/// making all in bb
+/// x.c:9: Error
+/// Then qf_push_dir thinks we are in ./aa/bb, but we are in ./bb.
+/// qf_guess_filepath will return NULL.
+static char_u *qf_guess_filepath(qf_list_T *qfl, char_u *filename)
{
struct dir_stack_T *ds_ptr;
struct dir_stack_T *ds_tmp;
char_u *fullname;
- qf_list_T *qfl = &qi->qf_lists[qf_idx];
// no dirs on the stack - there's nothing we can do
if (qfl->qf_dir_stack == NULL) {
@@ -1876,24 +2280,21 @@ static bool qflist_valid(win_T *wp, unsigned int qf_id)
/// When loading a file from the quickfix, the autocommands may modify it.
/// This may invalidate the current quickfix entry. This function checks
-/// whether a entry is still present in the quickfix.
+/// whether an entry is still present in the quickfix list.
/// Similar to location list.
-static bool is_qf_entry_present(qf_info_T *qi, qfline_T *qf_ptr)
+static bool is_qf_entry_present(qf_list_T *qfl, qfline_T *qf_ptr)
{
- qf_list_T *qfl;
qfline_T *qfp;
int i;
- qfl = &qi->qf_lists[qi->qf_curlist];
-
// Search for the entry in the current list
- for (i = 0, qfp = qfl->qf_start; i < qfl->qf_count; i++, qfp = qfp->qf_next) {
- if (qfp == NULL || qfp == qf_ptr) {
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
+ if (qfp == qf_ptr) {
break;
}
}
- if (i == qfl->qf_count) { // Entry is not found
+ if (i > qfl->qf_count) { // Entry is not found
return false;
}
@@ -1902,20 +2303,19 @@ static bool is_qf_entry_present(qf_info_T *qi, qfline_T *qf_ptr)
/// Get the next valid entry in the current quickfix/location list. The search
/// starts from the current entry. Returns NULL on failure.
-static qfline_T *get_next_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
+static qfline_T *get_next_valid_entry(qf_list_T *qfl, qfline_T *qf_ptr,
int *qf_index, int dir)
{
int idx = *qf_index;
int old_qf_fnum = qf_ptr->qf_fnum;
do {
- if (idx == qi->qf_lists[qi->qf_curlist].qf_count
- || qf_ptr->qf_next == NULL) {
+ if (idx == qfl->qf_count || qf_ptr->qf_next == NULL) {
return NULL;
}
idx++;
qf_ptr = qf_ptr->qf_next;
- } while ((!qi->qf_lists[qi->qf_curlist].qf_nonevalid && !qf_ptr->qf_valid)
+ } while ((!qfl->qf_nonevalid && !qf_ptr->qf_valid)
|| (dir == FORWARD_FILE && qf_ptr->qf_fnum == old_qf_fnum));
*qf_index = idx;
@@ -1924,7 +2324,7 @@ static qfline_T *get_next_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
/// Get the previous valid entry in the current quickfix/location list. The
/// search starts from the current entry. Returns NULL on failure.
-static qfline_T *get_prev_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
+static qfline_T *get_prev_valid_entry(qf_list_T *qfl, qfline_T *qf_ptr,
int *qf_index, int dir)
{
int idx = *qf_index;
@@ -1936,7 +2336,7 @@ static qfline_T *get_prev_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
}
idx--;
qf_ptr = qf_ptr->qf_prev;
- } while ((!qi->qf_lists[qi->qf_curlist].qf_nonevalid && !qf_ptr->qf_valid)
+ } while ((!qfl->qf_nonevalid && !qf_ptr->qf_valid)
|| (dir == BACKWARD_FILE && qf_ptr->qf_fnum == old_qf_fnum));
*qf_index = idx;
@@ -1947,12 +2347,11 @@ static qfline_T *get_prev_valid_entry(qf_info_T *qi, qfline_T *qf_ptr,
/// the quickfix list.
/// dir == FORWARD or FORWARD_FILE: next valid entry
/// dir == BACKWARD or BACKWARD_FILE: previous valid entry
-static qfline_T *get_nth_valid_entry(qf_info_T *qi, int errornr,
+static qfline_T *get_nth_valid_entry(qf_list_T *qfl, int errornr,
qfline_T *qf_ptr, int *qf_index, int dir)
{
qfline_T *prev_qf_ptr;
int prev_index;
- static char_u *e_no_more_items = (char_u *)N_("E553: No more items");
char_u *err = e_no_more_items;
while (errornr--) {
@@ -1960,9 +2359,9 @@ static qfline_T *get_nth_valid_entry(qf_info_T *qi, int errornr,
prev_index = *qf_index;
if (dir == FORWARD || dir == FORWARD_FILE) {
- qf_ptr = get_next_valid_entry(qi, qf_ptr, qf_index, dir);
+ qf_ptr = get_next_valid_entry(qfl, qf_ptr, qf_index, dir);
} else {
- qf_ptr = get_prev_valid_entry(qi, qf_ptr, qf_index, dir);
+ qf_ptr = get_prev_valid_entry(qfl, qf_ptr, qf_index, dir);
}
if (qf_ptr == NULL) {
@@ -1982,7 +2381,7 @@ static qfline_T *get_nth_valid_entry(qf_info_T *qi, int errornr,
}
/// Get n'th (errornr) quickfix entry
-static qfline_T *get_nth_entry(qf_info_T *qi, int errornr, qfline_T *qf_ptr,
+static qfline_T *get_nth_entry(qf_list_T *qfl, int errornr, qfline_T *qf_ptr,
int *cur_qfidx)
{
int qf_idx = *cur_qfidx;
@@ -1995,7 +2394,7 @@ static qfline_T *get_nth_entry(qf_info_T *qi, int errornr, qfline_T *qf_ptr,
// New error number is greater than the current error number
while (errornr > qf_idx
- && qf_idx < qi->qf_lists[qi->qf_curlist].qf_count
+ && qf_idx < qfl->qf_count
&& qf_ptr->qf_next != NULL) {
qf_idx++;
qf_ptr = qf_ptr->qf_next;
@@ -2005,6 +2404,25 @@ static qfline_T *get_nth_entry(qf_info_T *qi, int errornr, qfline_T *qf_ptr,
return qf_ptr;
}
+// Find a window displaying a Vim help file.
+static win_T *qf_find_help_win(void)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
+ if (bt_help(wp->w_buffer)) {
+ return wp;
+ }
+ }
+ return NULL;
+}
+
+/// Set the location list for the specified window to 'qi'.
+static void win_set_loclist(win_T *wp, qf_info_T *qi)
+{
+ wp->w_llist = qi;
+ qi->qf_refcount++;
+}
+
/// Find a help window or open one.
static int jump_to_help_window(qf_info_T *qi, int *opened_window)
{
@@ -2013,12 +2431,7 @@ static int jump_to_help_window(qf_info_T *qi, int *opened_window)
if (cmdmod.tab != 0) {
wp = NULL;
} else {
- FOR_ALL_WINDOWS_IN_TAB(wp2, curtab) {
- if (bt_help(wp2->w_buffer)) {
- wp = wp2;
- break;
- }
- }
+ wp = qf_find_help_win();
}
if (wp != NULL && wp->w_buffer->b_nwindows > 0) {
@@ -2049,8 +2462,7 @@ static int jump_to_help_window(qf_info_T *qi, int *opened_window)
if (IS_LL_STACK(qi)) { // not a quickfix list
// The new window should use the supplied location list
- curwin->w_llist = qi;
- qi->qf_refcount++;
+ win_set_loclist(curwin, qi);
}
}
@@ -2061,146 +2473,197 @@ static int jump_to_help_window(qf_info_T *qi, int *opened_window)
return OK;
}
-/// Find a suitable window for opening a file (qf_fnum) and jump to it.
-/// If the file is already opened in a window, jump to it.
-static int qf_jump_to_usable_window(int qf_fnum, int *opened_window)
+// Find a non-quickfix window using the given location list.
+// Returns NULL if a matching window is not found.
+static win_T *qf_find_win_with_loclist(const qf_info_T *ll)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
- win_T *usable_win_ptr = NULL;
- int usable_win;
- int flags;
- win_T *win;
- win_T *altwin;
+ FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
+ if (wp->w_llist == ll && !bt_quickfix(wp->w_buffer)) {
+ return wp;
+ }
+ }
+ return NULL;
+}
- usable_win = 0;
+// Find a window containing a normal buffer
+static win_T *qf_find_win_with_normal_buf(void)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
+ if (bt_normal(wp->w_buffer)) {
+ return wp;
+ }
+ }
+ return NULL;
+}
- qf_info_T *ll_ref = curwin->w_llist_ref;
+// Go to a window in any tabpage containing the specified file. Returns true
+// if successfully jumped to the window. Otherwise returns FALSE.
+static bool qf_goto_tabwin_with_file(int fnum)
+{
+ FOR_ALL_TAB_WINDOWS(tp, wp) {
+ if (wp->w_buffer->b_fnum == fnum) {
+ goto_tabpage_win(tp, wp);
+ return true;
+ }
+ }
+ return false;
+}
+
+// Create a new window to show a file above the quickfix window. Called when
+// only the quickfix window is present.
+static int qf_open_new_file_win(qf_info_T *ll_ref)
+{
+ int flags = WSP_ABOVE;
if (ll_ref != NULL) {
- // Find a window using the same location list that is not a
- // quickfix window.
- FOR_ALL_WINDOWS_IN_TAB(usable_win_ptr2, curtab) {
- if (usable_win_ptr2->w_llist == ll_ref
- && !bt_quickfix(usable_win_ptr2->w_buffer)) {
- usable_win_ptr = usable_win_ptr2;
- usable_win = 1;
+ flags |= WSP_NEWLOC;
+ }
+ if (win_split(0, flags) == FAIL) {
+ return FAIL; // not enough room for window
+ }
+ p_swb = empty_option; // don't split again
+ swb_flags = 0;
+ RESET_BINDING(curwin);
+ if (ll_ref != NULL) {
+ // The new window should use the location list from the
+ // location list window
+ win_set_loclist(curwin, ll_ref);
+ }
+ return OK;
+}
+
+// Go to a window that shows the right buffer. If the window is not found, go
+// to the window just above the location list window. This is used for opening
+// a file from a location window and not from a quickfix window. If some usable
+// window is previously found, then it is supplied in 'use_win'.
+static void qf_goto_win_with_ll_file(win_T *use_win, int qf_fnum,
+ qf_info_T *ll_ref)
+{
+ win_T *win = use_win;
+
+ if (win == NULL) {
+ // Find the window showing the selected file
+ FOR_ALL_WINDOWS_IN_TAB(win2, curtab) {
+ if (win2->w_buffer->b_fnum == qf_fnum) {
+ win = win2;
break;
}
}
+ if (win == NULL) {
+ // Find a previous usable window
+ win = curwin;
+ do {
+ if (bt_normal(win->w_buffer)) {
+ break;
+ }
+ if (win->w_prev == NULL) {
+ win = lastwin; // wrap around the top
+ } else {
+ win = win->w_prev; // go to previous window
+ }
+ } while (win != curwin);
+ }
+ }
+ win_goto(win);
+
+ // If the location list for the window is not set, then set it
+ // to the location list from the location window
+ if (win->w_llist == NULL && ll_ref != NULL) {
+ // The new window should use the location list from the
+ // location list window
+ win_set_loclist(win, ll_ref);
+ }
+}
+
+// Go to a window that shows the specified file. If a window is not found, go
+// to the window just above the quickfix window. This is used for opening a
+// file from a quickfix window and not from a location window.
+static void qf_goto_win_with_qfl_file(int qf_fnum)
+{
+ win_T *win = curwin;
+ win_T *altwin = NULL;
+ for (;;) {
+ if (win->w_buffer->b_fnum == qf_fnum) {
+ break;
+ }
+ if (win->w_prev == NULL) {
+ win = lastwin; // wrap around the top
+ } else {
+ win = win->w_prev; // go to previous window
+ }
+
+ if (IS_QF_WINDOW(win)) {
+ // Didn't find it, go to the window before the quickfix
+ // window, unless 'switchbuf' contains 'uselast': in this case we
+ // try to jump to the previously used window first.
+ if ((swb_flags & SWB_USELAST) && win_valid(prevwin)) {
+ win = prevwin;
+ } else if (altwin != NULL) {
+ win = altwin;
+ } else if (curwin->w_prev != NULL) {
+ win = curwin->w_prev;
+ } else {
+ win = curwin->w_next;
+ }
+ break;
+ }
+
+ // Remember a usable window.
+ if (altwin == NULL
+ && !win->w_p_pvw
+ && bt_normal(win->w_buffer)) {
+ altwin = win;
+ }
+ }
+
+ win_goto(win);
+}
+
+// Find a suitable window for opening a file (qf_fnum) from the
+// quickfix/location list and jump to it. If the file is already opened in a
+// window, jump to it. Otherwise open a new window to display the file. This is
+// called from either a quickfix or a location list window.
+static int qf_jump_to_usable_window(int qf_fnum, int *opened_window)
+{
+ win_T *usable_win_ptr = NULL;
+ bool usable_win = false;
+
+ qf_info_T *ll_ref = curwin->w_llist_ref;
+ if (ll_ref != NULL) {
+ // Find a non-quickfix window with this location list
+ usable_win_ptr = qf_find_win_with_loclist(ll_ref);
+ if (usable_win_ptr != NULL) {
+ usable_win = true;
+ }
}
if (!usable_win) {
// Locate a window showing a normal buffer
- FOR_ALL_WINDOWS_IN_TAB(win2, curtab) {
- if (win2->w_buffer->b_p_bt[0] == NUL) {
- usable_win = 1;
- break;
- }
+ win_T *win = qf_find_win_with_normal_buf();
+ if (win != NULL) {
+ usable_win = true;
}
}
// If no usable window is found and 'switchbuf' contains "usetab"
// then search in other tabs.
if (!usable_win && (swb_flags & SWB_USETAB)) {
- FOR_ALL_TAB_WINDOWS(tp, wp) {
- if (wp->w_buffer->b_fnum == qf_fnum) {
- goto_tabpage_win(tp, wp);
- usable_win = 1;
- goto win_found;
- }
- }
+ usable_win = qf_goto_tabwin_with_file(qf_fnum);
}
-win_found:
// If there is only one window and it is the quickfix window, create a
// new one above the quickfix window.
if ((ONE_WINDOW && bt_quickfix(curbuf)) || !usable_win) {
- flags = WSP_ABOVE;
- if (ll_ref != NULL) {
- flags |= WSP_NEWLOC;
- }
- if (win_split(0, flags) == FAIL) {
- return FAIL; // not enough room for window
+ if (qf_open_new_file_win(ll_ref) != OK) {
+ return FAIL;
}
*opened_window = true; // close it when fail
- p_swb = empty_option; // don't split again
- swb_flags = 0;
- RESET_BINDING(curwin);
- if (ll_ref != NULL) {
- // The new window should use the location list from the
- // location list window
- curwin->w_llist = ll_ref;
- ll_ref->qf_refcount++;
- }
} else {
- if (curwin->w_llist_ref != NULL) {
- // In a location window
- win = usable_win_ptr;
- if (win == NULL) {
- // Find the window showing the selected file
- FOR_ALL_WINDOWS_IN_TAB(win2, curtab) {
- if (win2->w_buffer->b_fnum == qf_fnum) {
- win = win2;
- break;
- }
- }
- if (win == NULL) {
- // Find a previous usable window
- win = curwin;
- do {
- if (win->w_buffer->b_p_bt[0] == NUL) {
- break;
- }
- if (win->w_prev == NULL) {
- win = lastwin; // wrap around the top
- } else {
- win = win->w_prev; // go to previous window
- }
- } while (win != curwin);
- }
- }
- win_goto(win);
-
- // If the location list for the window is not set, then set it
- // to the location list from the location window
- if (win->w_llist == NULL) {
- win->w_llist = ll_ref;
- if (ll_ref != NULL) {
- ll_ref->qf_refcount++;
- }
- }
- } else {
- // Try to find a window that shows the right buffer.
- // Default to the window just above the quickfix buffer.
- win = curwin;
- altwin = NULL;
- for (;;) {
- if (win->w_buffer->b_fnum == qf_fnum) {
- break;
- }
- if (win->w_prev == NULL) {
- win = lastwin; // wrap around the top
- } else {
- win = win->w_prev; // go to previous window
- }
- if (IS_QF_WINDOW(win)) {
- // Didn't find it, go to the window before the quickfix window.
- if (altwin != NULL) {
- win = altwin;
- } else if (curwin->w_prev != NULL) {
- win = curwin->w_prev;
- } else {
- win = curwin->w_next;
- }
- break;
- }
-
- // Remember a usable window.
- if (altwin == NULL && !win->w_p_pvw
- && win->w_buffer->b_p_bt[0] == NUL) {
- altwin = win;
- }
- }
-
- win_goto(win);
+ if (curwin->w_llist_ref != NULL) { // In a location window
+ qf_goto_win_with_ll_file(usable_win_ptr, qf_fnum, ll_ref);
+ } else { // In a quickfix window
+ qf_goto_win_with_qfl_file(qf_fnum);
}
}
@@ -2211,6 +2674,8 @@ win_found:
static int qf_jump_edit_buffer(qf_info_T *qi, qfline_T *qf_ptr, int forceit,
win_T *oldwin, int *opened_window, int *abort)
{
+ qf_list_T *qfl = qf_get_curlist(qi);
+ qfltype_T qfl_type = qfl->qfl_type;
int retval = OK;
if (qf_ptr->qf_type == 1) {
@@ -2225,12 +2690,12 @@ static int qf_jump_edit_buffer(qf_info_T *qi, qfline_T *qf_ptr, int forceit,
oldwin == curwin ? curwin : NULL);
}
} else {
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qfl->qf_id;
retval = buflist_getfile(qf_ptr->qf_fnum, (linenr_T)1,
GETF_SETMARK | GETF_SWITCH, forceit);
- if (IS_LL_STACK(qi)) {
+ if (qfl_type == QFLT_LOCATION) {
// Location list. Check whether the associated window is still
// present and the list is still valid.
if (!win_valid_any_tab(oldwin)) {
@@ -2241,8 +2706,8 @@ static int qf_jump_edit_buffer(qf_info_T *qi, qfline_T *qf_ptr, int forceit,
EMSG(_(e_loc_list_changed));
*abort = true;
}
- } else if (!is_qf_entry_present(qi, qf_ptr)) {
- if (IS_QF_STACK(qi)) {
+ } else if (!is_qf_entry_present(qfl, qf_ptr)) {
+ if (qfl_type == QFLT_QUICKFIX) {
EMSG(_("E925: Current quickfix was changed"));
} else {
EMSG(_(e_loc_list_changed));
@@ -2258,15 +2723,12 @@ static int qf_jump_edit_buffer(qf_info_T *qi, qfline_T *qf_ptr, int forceit,
return retval;
}
-/// Goto the error line in the current file using either line/column number or a
-/// search pattern.
+/// Go to the error line in the current file using either line/column number or
+/// a search pattern.
static void qf_jump_goto_line(linenr_T qf_lnum, int qf_col, char_u qf_viscol,
char_u *qf_pattern)
{
linenr_T i;
- char_u *line;
- colnr_T screen_col;
- colnr_T char_col;
if (qf_pattern == NULL) {
// Go to line with error, unless qf_lnum is 0.
@@ -2278,26 +2740,11 @@ static void qf_jump_goto_line(linenr_T qf_lnum, int qf_col, char_u qf_viscol,
curwin->w_cursor.lnum = i;
}
if (qf_col > 0) {
- curwin->w_cursor.col = qf_col - 1;
curwin->w_cursor.coladd = 0;
if (qf_viscol == true) {
- // Check each character from the beginning of the error
- // line up to the error column. For each tab character
- // found, reduce the error column value by the length of
- // a tab character.
- line = get_cursor_line_ptr();
- screen_col = 0;
- for (char_col = 0; char_col < curwin->w_cursor.col; char_col++) {
- if (*line == NUL) {
- break;
- }
- if (*line++ == '\t') {
- curwin->w_cursor.col -= 7 - (screen_col % 8);
- screen_col += 8 - (screen_col % 8);
- } else {
- screen_col++;
- }
- }
+ coladvance(qf_col - 1);
+ } else {
+ curwin->w_cursor.col = qf_col - 1;
}
curwin->w_set_curswant = true;
check_cursor();
@@ -2308,7 +2755,7 @@ static void qf_jump_goto_line(linenr_T qf_lnum, int qf_col, char_u qf_viscol,
// Move the cursor to the first line in the buffer
pos_T save_cursor = curwin->w_cursor;
curwin->w_cursor.lnum = 0;
- if (!do_search(NULL, '/', qf_pattern, (long)1, SEARCH_KEEP, NULL, NULL)) {
+ if (!do_search(NULL, '/', qf_pattern, (long)1, SEARCH_KEEP, NULL)) {
curwin->w_cursor = save_cursor;
}
}
@@ -2324,7 +2771,7 @@ static void qf_jump_print_msg(qf_info_T *qi, int qf_index, qfline_T *qf_ptr,
update_topline_redraw();
}
snprintf((char *)IObuff, IOSIZE, _("(%d of %d)%s%s: "), qf_index,
- qi->qf_lists[qi->qf_curlist].qf_count,
+ qf_get_curlist(qi)->qf_count,
qf_ptr->qf_cleared ? _(" (line deleted)") : "",
(char *)qf_types(qf_ptr->qf_type, qf_ptr->qf_nr));
// Add the message, skipping leading whitespace and newlines.
@@ -2354,6 +2801,7 @@ static void qf_jump_print_msg(qf_info_T *qi, int qf_index, qfline_T *qf_ptr,
/// else go to entry "errornr"
void qf_jump(qf_info_T *qi, int dir, int errornr, int forceit)
{
+ qf_list_T *qfl;
qfline_T *qf_ptr;
qfline_T *old_qf_ptr;
int qf_index;
@@ -2371,32 +2819,34 @@ void qf_jump(qf_info_T *qi, int dir, int errornr, int forceit)
if (qi == NULL)
qi = &ql_info;
- if (qi->qf_curlist >= qi->qf_listcount
- || qi->qf_lists[qi->qf_curlist].qf_count == 0) {
+ if (qf_stack_empty(qi) || qf_list_empty(qf_get_curlist(qi))) {
EMSG(_(e_quickfix));
return;
}
- qf_ptr = qi->qf_lists[qi->qf_curlist].qf_ptr;
+ qfl = qf_get_curlist(qi);
+
+ qf_ptr = qfl->qf_ptr;
old_qf_ptr = qf_ptr;
- qf_index = qi->qf_lists[qi->qf_curlist].qf_index;
+ qf_index = qfl->qf_index;
old_qf_index = qf_index;
if (dir != 0) { // next/prev valid entry
- qf_ptr = get_nth_valid_entry(qi, errornr, qf_ptr, &qf_index, dir);
+ qf_ptr = get_nth_valid_entry(qfl, errornr, qf_ptr, &qf_index, dir);
if (qf_ptr == NULL) {
qf_ptr = old_qf_ptr;
qf_index = old_qf_index;
goto theend; // The horror... the horror...
}
} else if (errornr != 0) { // go to specified number
- qf_ptr = get_nth_entry(qi, errornr, qf_ptr, &qf_index);
+ qf_ptr = get_nth_entry(qfl, errornr, qf_ptr, &qf_index);
}
- qi->qf_lists[qi->qf_curlist].qf_index = qf_index;
- if (qf_win_pos_update(qi, old_qf_index))
- /* No need to print the error message if it's visible in the error
- * window */
- print_message = FALSE;
+ qfl->qf_index = qf_index;
+ if (qf_win_pos_update(qi, old_qf_index)) {
+ // No need to print the error message if it's visible in the error
+ // window
+ print_message = false;
+ }
// For ":helpgrep" find a help window or open one.
if (qf_ptr->qf_type == 1 && (!bt_help(curwin->w_buffer) || cmdmod.tab != 0)) {
@@ -2418,10 +2868,8 @@ void qf_jump(qf_info_T *qi, int dir, int errornr, int forceit)
}
}
- /*
- * If there is a file name,
- * read the wanted file if needed, and check autowrite etc.
- */
+ // If there is a file name,
+ // read the wanted file if needed, and check autowrite etc.
old_curbuf = curbuf;
old_lnum = curwin->w_cursor.lnum;
@@ -2465,12 +2913,12 @@ failed:
}
theend:
if (qi != NULL) {
- qi->qf_lists[qi->qf_curlist].qf_ptr = qf_ptr;
- qi->qf_lists[qi->qf_curlist].qf_index = qf_index;
+ qfl->qf_ptr = qf_ptr;
+ qfl->qf_index = qf_index;
}
if (p_swb != old_swb && opened_window) {
- /* Restore old 'switchbuf' value, but not when an autocommand or
- * modeline has changed the value. */
+ // Restore old 'switchbuf' value, but not when an autocommand or
+ // modeline has changed the value.
if (p_swb == empty_option) {
p_swb = old_swb;
swb_flags = old_swb_flags;
@@ -2479,36 +2927,115 @@ theend:
}
}
-/*
- * ":clist": list all errors
- * ":llist": list all locations
- */
+
+// Highlight attributes used for displaying entries from the quickfix list.
+static int qfFileAttr;
+static int qfSepAttr;
+static int qfLineAttr;
+
+/// Display information about a single entry from the quickfix/location list.
+/// Used by ":clist/:llist" commands.
+/// 'cursel' will be set to true for the currently selected entry in the
+/// quickfix list.
+static void qf_list_entry(qfline_T *qfp, int qf_idx, bool cursel)
+{
+ char_u *fname;
+ buf_T *buf;
+
+ fname = NULL;
+ if (qfp->qf_module != NULL && *qfp->qf_module != NUL) {
+ vim_snprintf((char *)IObuff, IOSIZE, "%2d %s", qf_idx,
+ (char *)qfp->qf_module);
+ } else {
+ if (qfp->qf_fnum != 0
+ && (buf = buflist_findnr(qfp->qf_fnum)) != NULL) {
+ fname = buf->b_fname;
+ if (qfp->qf_type == 1) { // :helpgrep
+ fname = path_tail(fname);
+ }
+ }
+ if (fname == NULL) {
+ snprintf((char *)IObuff, IOSIZE, "%2d", qf_idx);
+ } else {
+ vim_snprintf((char *)IObuff, IOSIZE, "%2d %s",
+ qf_idx, (char *)fname);
+ }
+ }
+
+ // Support for filtering entries using :filter /pat/ clist
+ // Match against the module name, file name, search pattern and
+ // text of the entry.
+ bool filter_entry = true;
+ if (qfp->qf_module != NULL && *qfp->qf_module != NUL) {
+ filter_entry &= message_filtered(qfp->qf_module);
+ }
+ if (filter_entry && fname != NULL) {
+ filter_entry &= message_filtered(fname);
+ }
+ if (filter_entry && qfp->qf_pattern != NULL) {
+ filter_entry &= message_filtered(qfp->qf_pattern);
+ }
+ if (filter_entry) {
+ filter_entry &= message_filtered(qfp->qf_text);
+ }
+ if (filter_entry) {
+ return;
+ }
+
+ msg_putchar('\n');
+ msg_outtrans_attr(IObuff, cursel ? HL_ATTR(HLF_QFL) : qfFileAttr);
+
+ if (qfp->qf_lnum != 0) {
+ msg_puts_attr(":", qfSepAttr);
+ }
+ if (qfp->qf_lnum == 0) {
+ IObuff[0] = NUL;
+ } else if (qfp->qf_col == 0) {
+ vim_snprintf((char *)IObuff, IOSIZE, "%" PRIdLINENR, qfp->qf_lnum);
+ } else {
+ vim_snprintf((char *)IObuff, IOSIZE, "%" PRIdLINENR " col %d",
+ qfp->qf_lnum, qfp->qf_col);
+ }
+ vim_snprintf((char *)IObuff + STRLEN(IObuff), IOSIZE, "%s",
+ (char *)qf_types(qfp->qf_type, qfp->qf_nr));
+ msg_puts_attr((const char *)IObuff, qfLineAttr);
+ msg_puts_attr(":", qfSepAttr);
+ if (qfp->qf_pattern != NULL) {
+ qf_fmt_text(qfp->qf_pattern, IObuff, IOSIZE);
+ msg_puts((const char *)IObuff);
+ msg_puts_attr(":", qfSepAttr);
+ }
+ msg_puts(" ");
+
+ // Remove newlines and leading whitespace from the text. For an
+ // unrecognized line keep the indent, the compiler may mark a word
+ // with ^^^^. */
+ qf_fmt_text((fname != NULL || qfp->qf_lnum != 0)
+ ? skipwhite(qfp->qf_text) : qfp->qf_text,
+ IObuff, IOSIZE);
+ msg_prt_line(IObuff, false);
+ ui_flush(); // show one line at a time
+}
+
+// ":clist": list all errors
+// ":llist": list all locations
void qf_list(exarg_T *eap)
{
- buf_T *buf;
- char_u *fname;
- qfline_T *qfp;
+ qf_list_T *qfl;
+ qfline_T *qfp;
int i;
int idx1 = 1;
int idx2 = -1;
char_u *arg = eap->arg;
- int qfFileAttr;
- int qfSepAttr;
- int qfLineAttr;
int all = eap->forceit; // if not :cl!, only show
// recognised errors
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_llist) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
- if (qi->qf_curlist >= qi->qf_listcount
- || qi->qf_lists[qi->qf_curlist].qf_count == 0) {
+ if (qf_stack_empty(qi) || qf_list_empty(qf_get_curlist(qi))) {
EMSG(_(e_quickfix));
return;
}
@@ -2522,12 +3049,13 @@ void qf_list(exarg_T *eap)
EMSG(_(e_trailing));
return;
}
+ qfl = qf_get_curlist(qi);
if (plus) {
- i = qi->qf_lists[qi->qf_curlist].qf_index;
+ i = qfl->qf_index;
idx2 = i + idx1;
idx1 = i;
} else {
- i = qi->qf_lists[qi->qf_curlist].qf_count;
+ i = qfl->qf_count;
if (idx1 < 0) {
idx1 = (-idx1 > i) ? 0 : idx1 + i + 1;
}
@@ -2554,107 +3082,25 @@ void qf_list(exarg_T *eap)
qfLineAttr = HL_ATTR(HLF_N);
}
- if (qi->qf_lists[qi->qf_curlist].qf_nonevalid) {
+ if (qfl->qf_nonevalid) {
all = true;
}
- qfp = qi->qf_lists[qi->qf_curlist].qf_start;
- for (i = 1; !got_int && i <= qi->qf_lists[qi->qf_curlist].qf_count; ) {
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
if ((qfp->qf_valid || all) && idx1 <= i && i <= idx2) {
- if (got_int) {
- break;
- }
-
- fname = NULL;
- if (qfp->qf_module != NULL && *qfp->qf_module != NUL) {
- vim_snprintf((char *)IObuff, IOSIZE, "%2d %s", i,
- (char *)qfp->qf_module);
- } else {
- if (qfp->qf_fnum != 0 && (buf = buflist_findnr(qfp->qf_fnum)) != NULL) {
- fname = buf->b_fname;
- if (qfp->qf_type == 1) { // :helpgrep
- fname = path_tail(fname);
- }
- }
- if (fname == NULL) {
- snprintf((char *)IObuff, IOSIZE, "%2d", i);
- } else {
- vim_snprintf((char *)IObuff, IOSIZE, "%2d %s", i, (char *)fname);
- }
- }
-
- // Support for filtering entries using :filter /pat/ clist
- // Match against the module name, file name, search pattern and
- // text of the entry.
- bool filter_entry = true;
- if (qfp->qf_module != NULL && *qfp->qf_module != NUL) {
- filter_entry &= message_filtered(qfp->qf_module);
- }
- if (filter_entry && fname != NULL) {
- filter_entry &= message_filtered(fname);
- }
- if (filter_entry && qfp->qf_pattern != NULL) {
- filter_entry &= message_filtered(qfp->qf_pattern);
- }
- if (filter_entry) {
- filter_entry &= message_filtered(qfp->qf_text);
- }
- if (filter_entry) {
- goto next_entry;
- }
- msg_putchar('\n');
- msg_outtrans_attr(IObuff, i == qi->qf_lists[qi->qf_curlist].qf_index
- ? HL_ATTR(HLF_QFL) : qfFileAttr);
-
- if (qfp->qf_lnum != 0) {
- msg_puts_attr(":", qfSepAttr);
- }
- if (qfp->qf_lnum == 0) {
- IObuff[0] = NUL;
- } else if (qfp->qf_col == 0) {
- vim_snprintf((char *)IObuff, IOSIZE, "%" PRIdLINENR, qfp->qf_lnum);
- } else {
- vim_snprintf((char *)IObuff, IOSIZE, "%" PRIdLINENR " col %d",
- qfp->qf_lnum, qfp->qf_col);
- }
- vim_snprintf((char *)IObuff + STRLEN(IObuff), IOSIZE, "%s",
- (char *)qf_types(qfp->qf_type, qfp->qf_nr));
- msg_puts_attr((const char *)IObuff, qfLineAttr);
- msg_puts_attr(":", qfSepAttr);
- if (qfp->qf_pattern != NULL) {
- qf_fmt_text(qfp->qf_pattern, IObuff, IOSIZE);
- msg_puts((const char *)IObuff);
- msg_puts_attr(":", qfSepAttr);
- }
- msg_puts(" ");
-
- /* Remove newlines and leading whitespace from the text. For an
- * unrecognized line keep the indent, the compiler may mark a word
- * with ^^^^. */
- qf_fmt_text((fname != NULL || qfp->qf_lnum != 0)
- ? skipwhite(qfp->qf_text) : qfp->qf_text,
- IObuff, IOSIZE);
- msg_prt_line(IObuff, FALSE);
- ui_flush(); /* show one line at a time */
+ qf_list_entry(qfp, i, i == qfl->qf_index);
}
-
-next_entry:
- qfp = qfp->qf_next;
- if (qfp == NULL) {
- break;
- }
- i++;
os_breakcheck();
}
}
-/*
- * Remove newlines and leading whitespace from an error message.
- * Put the result in "buf[bufsize]".
- */
-static void qf_fmt_text(char_u *text, char_u *buf, int bufsize)
+// Remove newlines and leading whitespace from an error message.
+// Put the result in "buf[bufsize]".
+static void qf_fmt_text(const char_u *restrict text, char_u *restrict buf,
+ int bufsize)
+ FUNC_ATTR_NONNULL_ALL
{
int i;
- char_u *p = text;
+ const char_u *p = text;
for (i = 0; *p != NUL && i < bufsize - 1; ++i) {
if (*p == '\n') {
@@ -2695,23 +3141,17 @@ static void qf_msg(qf_info_T *qi, int which, char *lead)
msg(buf);
}
-/*
- * ":colder [count]": Up in the quickfix stack.
- * ":cnewer [count]": Down in the quickfix stack.
- * ":lolder [count]": Up in the location list stack.
- * ":lnewer [count]": Down in the location list stack.
- */
+/// ":colder [count]": Up in the quickfix stack.
+/// ":cnewer [count]": Down in the quickfix stack.
+/// ":lolder [count]": Up in the location list stack.
+/// ":lnewer [count]": Down in the location list stack.
void qf_age(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
int count;
- if (eap->cmdidx == CMD_lolder || eap->cmdidx == CMD_lnewer) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
if (eap->addr_count != 0) {
@@ -2742,14 +3182,10 @@ void qf_age(exarg_T *eap)
/// Display the information about all the quickfix/location lists in the stack.
void qf_history(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi = qf_cmd_get_stack(eap, false);
int i;
- if (eap->cmdidx == CMD_lhistory) {
- qi = GET_LOC_LIST(curwin);
- }
- if (qi == NULL || (qi->qf_listcount == 0
- && qi->qf_lists[qi->qf_curlist].qf_count == 0)) {
+ if (qf_stack_empty(qi) || qf_list_empty(qf_get_curlist(qi))) {
MSG(_("No entries"));
} else {
for (i = 0; i < qi->qf_listcount; i++) {
@@ -2760,12 +3196,11 @@ void qf_history(exarg_T *eap)
/// Free all the entries in the error list "idx". Note that other information
/// associated with the list like context and title are not freed.
-static void qf_free_items(qf_info_T *qi, int idx)
+static void qf_free_items(qf_list_T *qfl)
{
qfline_T *qfp;
qfline_T *qfpnext;
bool stop = false;
- qf_list_T *qfl = &qi->qf_lists[idx];
while (qfl->qf_count && qfl->qf_start != NULL) {
qfp = qfl->qf_start;
@@ -2806,10 +3241,9 @@ static void qf_free_items(qf_info_T *qi, int idx)
/// Free error list "idx". Frees all the entries in the quickfix list,
/// associated context information and the title.
-static void qf_free(qf_info_T *qi, int idx)
+static void qf_free(qf_list_T *qfl)
{
- qf_list_T *qfl = &qi->qf_lists[idx];
- qf_free_items(qi, idx);
+ qf_free_items(qfl);
XFREE_CLEAR(qfl->qf_title);
tv_free(qfl->qf_ctx);
@@ -2818,9 +3252,7 @@ static void qf_free(qf_info_T *qi, int idx)
qfl->qf_changedtick = 0L;
}
-/*
- * qf_mark_adjust: adjust marks
- */
+// qf_mark_adjust: adjust marks
bool qf_mark_adjust(win_T *wp, linenr_T line1, linenr_T line2, long amount,
long amount_after)
{
@@ -2841,11 +3273,10 @@ bool qf_mark_adjust(win_T *wp, linenr_T line1, linenr_T line2, long amount,
qi = wp->w_llist;
}
- for (idx = 0; idx < qi->qf_listcount; ++idx)
- if (qi->qf_lists[idx].qf_count)
- for (i = 0, qfp = qi->qf_lists[idx].qf_start;
- i < qi->qf_lists[idx].qf_count && qfp != NULL;
- i++, qfp = qfp->qf_next) {
+ for (idx = 0; idx < qi->qf_listcount; idx++) {
+ qf_list_T *qfl = qf_get_list(qi, idx);
+ if (!qf_list_empty(qfl)) {
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
if (qfp->qf_fnum == curbuf->b_fnum) {
found_one = true;
if (qfp->qf_lnum >= line1 && qfp->qf_lnum <= line2) {
@@ -2857,25 +3288,25 @@ bool qf_mark_adjust(win_T *wp, linenr_T line1, linenr_T line2, long amount,
qfp->qf_lnum += amount_after;
}
}
+ }
+ }
return found_one;
}
-/*
- * Make a nice message out of the error character and the error number:
- * char number message
- * e or E 0 " error"
- * w or W 0 " warning"
- * i or I 0 " info"
- * 0 0 ""
- * other 0 " c"
- * e or E n " error n"
- * w or W n " warning n"
- * i or I n " info n"
- * 0 n " error n"
- * other n " c n"
- * 1 x "" :helpgrep
- */
+// Make a nice message out of the error character and the error number:
+// char number message
+// e or E 0 " error"
+// w or W 0 " warning"
+// i or I 0 " info"
+// 0 0 ""
+// other 0 " c"
+// e or E n " error n"
+// w or W n " warning n"
+// i or I n " info n"
+// 0 n " error n"
+// other n " c n"
+// 1 x "" :helpgrep
static char_u *qf_types(int c, int nr)
{
static char_u buf[20];
@@ -2916,7 +3347,7 @@ void qf_view_result(bool split)
if (IS_LL_WINDOW(curwin)) {
qi = GET_LOC_LIST(curwin);
}
- if (qf_list_empty(qi, qi->qf_curlist)) {
+ if (qf_list_empty(qf_get_curlist(qi))) {
EMSG(_(e_quickfix));
return;
}
@@ -2936,83 +3367,175 @@ void qf_view_result(bool split)
do_cmdline_cmd((IS_LL_WINDOW(curwin) ? ".ll" : ".cc"));
}
-/*
- * ":cwindow": open the quickfix window if we have errors to display,
- * close it if not.
- * ":lwindow": open the location list window if we have locations to display,
- * close it if not.
- */
+// ":cwindow": open the quickfix window if we have errors to display,
+// close it if not.
+// ":lwindow": open the location list window if we have locations to display,
+// close it if not.
void ex_cwindow(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
+ qf_list_T *qfl;
win_T *win;
- if (eap->cmdidx == CMD_lwindow) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL)
- return;
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
- /* Look for an existing quickfix window. */
+ qfl = qf_get_curlist(qi);
+
+ // Look for an existing quickfix window.
win = qf_find_win(qi);
- /*
- * If a quickfix window is open but we have no errors to display,
- * close the window. If a quickfix window is not open, then open
- * it if we have errors; otherwise, leave it closed.
- */
- if (qi->qf_lists[qi->qf_curlist].qf_nonevalid
- || qi->qf_lists[qi->qf_curlist].qf_count == 0
- || qi->qf_curlist >= qi->qf_listcount) {
- if (win != NULL)
+ // If a quickfix window is open but we have no errors to display,
+ // close the window. If a quickfix window is not open, then open
+ // it if we have errors; otherwise, leave it closed.
+ if (qf_stack_empty(qi)
+ || qfl->qf_nonevalid
+ || qf_list_empty(qf_get_curlist(qi))) {
+ if (win != NULL) {
ex_cclose(eap);
- } else if (win == NULL)
+ }
+ } else if (win == NULL) {
ex_copen(eap);
+ }
}
-/*
- * ":cclose": close the window showing the list of errors.
- * ":lclose": close the window showing the location list
- */
+// ":cclose": close the window showing the list of errors.
+// ":lclose": close the window showing the location list
void ex_cclose(exarg_T *eap)
{
- win_T *win = NULL;
- qf_info_T *qi = &ql_info;
+ win_T *win = NULL;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_lclose || eap->cmdidx == CMD_lwindow) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL)
- return;
+ if ((qi = qf_cmd_get_stack(eap, false)) == NULL) {
+ return;
}
- /* Find existing quickfix window and close it. */
+ // Find existing quickfix window and close it.
win = qf_find_win(qi);
if (win != NULL) {
win_close(win, false);
}
}
-/*
- * ":copen": open a window that shows the list of errors.
- * ":lopen": open a window that shows the location list.
- */
+// Goto a quickfix or location list window (if present).
+// Returns OK if the window is found, FAIL otherwise.
+static int qf_goto_cwindow(const qf_info_T *qi, bool resize, int sz,
+ bool vertsplit)
+{
+ win_T *const win = qf_find_win(qi);
+ if (win == NULL) {
+ return FAIL;
+ }
+
+ win_goto(win);
+ if (resize) {
+ if (vertsplit) {
+ if (sz != win->w_width) {
+ win_setwidth(sz);
+ }
+ } else if (sz != win->w_height
+ && (win->w_height + win->w_status_height + tabline_height()
+ < cmdline_row)) {
+ win_setheight(sz);
+ }
+ }
+
+ return OK;
+}
+
+// Open a new quickfix or location list window, load the quickfix buffer and
+// set the appropriate options for the window.
+// Returns FAIL if the window could not be opened.
+static int qf_open_new_cwindow(const qf_info_T *qi, int height)
+{
+ win_T *oldwin = curwin;
+ const tabpage_T *const prevtab = curtab;
+ int flags = 0;
+
+ const buf_T *const qf_buf = qf_find_buf(qi);
+
+ // The current window becomes the previous window afterwards.
+ win_T *const win = curwin;
+
+ if (IS_QF_STACK(qi) && cmdmod.split == 0) {
+ // Create the new quickfix window at the very bottom, except when
+ // :belowright or :aboveleft is used.
+ win_goto(lastwin);
+ }
+ // Default is to open the window below the current window
+ if (cmdmod.split == 0) {
+ flags = WSP_BELOW;
+ }
+ flags |= WSP_NEWLOC;
+ if (win_split(height, flags) == FAIL) {
+ return FAIL; // not enough room for window
+ }
+ RESET_BINDING(curwin);
+
+ if (IS_LL_STACK(qi)) {
+ // For the location list window, create a reference to the
+ // location list from the window 'win'.
+ curwin->w_llist_ref = win->w_llist;
+ win->w_llist->qf_refcount++;
+ }
+
+ if (oldwin != curwin) {
+ oldwin = NULL; // don't store info when in another window
+ }
+ if (qf_buf != NULL) {
+ // Use the existing quickfix buffer
+ (void)do_ecmd(qf_buf->b_fnum, NULL, NULL, NULL, ECMD_ONE,
+ ECMD_HIDE + ECMD_OLDBUF, oldwin);
+ } else {
+ // Create a new quickfix buffer
+ (void)do_ecmd(0, NULL, NULL, NULL, ECMD_ONE, ECMD_HIDE, oldwin);
+
+ // switch off 'swapfile'
+ set_option_value("swf", 0L, NULL, OPT_LOCAL);
+ set_option_value("bt", 0L, "quickfix", OPT_LOCAL);
+ set_option_value("bh", 0L, "wipe", OPT_LOCAL);
+ RESET_BINDING(curwin);
+ curwin->w_p_diff = false;
+ set_option_value("fdm", 0L, "manual", OPT_LOCAL);
+ }
+
+ // Only set the height when still in the same tab page and there is no
+ // window to the side.
+ if (curtab == prevtab && curwin->w_width == Columns) {
+ win_setheight(height);
+ }
+ curwin->w_p_wfh = true; // set 'winfixheight'
+ if (win_valid(win)) {
+ prevwin = win;
+ }
+ return OK;
+}
+
+/// Set "w:quickfix_title" if "qi" has a title.
+static void qf_set_title_var(qf_list_T *qfl)
+{
+ if (qfl->qf_title != NULL) {
+ set_internal_string_var((char_u *)"w:quickfix_title", qfl->qf_title);
+ }
+}
+
+/// ":copen": open a window that shows the list of errors.
+/// ":lopen": open a window that shows the location list.
void ex_copen(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
+ qf_list_T *qfl;
int height;
- win_T *win;
- tabpage_T *prevtab = curtab;
- buf_T *qf_buf;
- win_T *oldwin = curwin;
+ int status = FAIL;
+ int lnum;
- if (eap->cmdidx == CMD_lopen || eap->cmdidx == CMD_lwindow) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
+ incr_quickfix_busy();
+
if (eap->addr_count != 0) {
assert(eap->line2 <= INT_MAX);
height = (int)eap->line2;
@@ -3021,94 +3544,33 @@ void ex_copen(exarg_T *eap)
}
reset_VIsual_and_resel(); // stop Visual mode
- /*
- * Find existing quickfix window, or open a new one.
- */
- win = qf_find_win(qi);
-
- if (win != NULL && cmdmod.tab == 0) {
- win_goto(win);
- if (eap->addr_count != 0) {
- if (cmdmod.split & WSP_VERT) {
- if (height != win->w_width) {
- win_setwidth(height);
- }
- } else {
- if (height != win->w_height) {
- win_setheight(height);
- }
- }
- }
- } else {
- int flags = 0;
-
- qf_buf = qf_find_buf(qi);
-
- /* The current window becomes the previous window afterwards. */
- win = curwin;
-
- if ((eap->cmdidx == CMD_copen || eap->cmdidx == CMD_cwindow)
- && cmdmod.split == 0)
- // Create the new quickfix window at the very bottom, except when
- // :belowright or :aboveleft is used.
- win_goto(lastwin);
- // Default is to open the window below the current window
- if (cmdmod.split == 0) {
- flags = WSP_BELOW;
- }
- flags |= WSP_NEWLOC;
- if (win_split(height, flags) == FAIL) {
- return; // not enough room for window
+ // Find an existing quickfix window, or open a new one.
+ if (cmdmod.tab == 0) {
+ status = qf_goto_cwindow(qi, eap->addr_count != 0, height,
+ cmdmod.split & WSP_VERT);
+ }
+ if (status == FAIL) {
+ if (qf_open_new_cwindow(qi, height) == FAIL) {
+ decr_quickfix_busy();
+ return;
}
- RESET_BINDING(curwin);
+ }
- if (eap->cmdidx == CMD_lopen || eap->cmdidx == CMD_lwindow) {
- /*
- * For the location list window, create a reference to the
- * location list from the window 'win'.
- */
- curwin->w_llist_ref = win->w_llist;
- win->w_llist->qf_refcount++;
- }
-
- if (oldwin != curwin)
- oldwin = NULL; /* don't store info when in another window */
- if (qf_buf != NULL)
- /* Use the existing quickfix buffer */
- (void)do_ecmd(qf_buf->b_fnum, NULL, NULL, NULL, ECMD_ONE,
- ECMD_HIDE + ECMD_OLDBUF, oldwin);
- else {
- /* Create a new quickfix buffer */
- (void)do_ecmd(0, NULL, NULL, NULL, ECMD_ONE, ECMD_HIDE, oldwin);
- // Switch off 'swapfile'.
- set_option_value("swf", 0L, NULL, OPT_LOCAL);
- set_option_value("bt", 0L, "quickfix", OPT_LOCAL);
- set_option_value("bh", 0L, "wipe", OPT_LOCAL);
- RESET_BINDING(curwin);
- curwin->w_p_diff = false;
- set_option_value("fdm", 0L, "manual", OPT_LOCAL);
- }
-
- /* Only set the height when still in the same tab page and there is no
- * window to the side. */
- if (curtab == prevtab
- && curwin->w_width == Columns
- )
- win_setheight(height);
- curwin->w_p_wfh = TRUE; /* set 'winfixheight' */
- if (win_valid(win))
- prevwin = win;
- }
-
- qf_set_title_var(qi);
+ qfl = qf_get_curlist(qi);
+ qf_set_title_var(qfl);
+ // Save the current index here, as updating the quickfix buffer may free
+ // the quickfix list
+ lnum = qfl->qf_index;
// Fill the buffer with the quickfix list.
- qf_fill_buffer(qi, curbuf, NULL);
+ qf_fill_buffer(qfl, curbuf, NULL);
+
+ decr_quickfix_busy();
- curwin->w_cursor.lnum = qi->qf_lists[qi->qf_curlist].qf_index;
+ curwin->w_cursor.lnum = lnum;
curwin->w_cursor.col = 0;
check_cursor();
- update_topline(); /* scroll to show the line */
+ update_topline(); // scroll to show the line
}
// Move the cursor in the quickfix window to "lnum".
@@ -3129,17 +3591,13 @@ static void qf_win_goto(win_T *win, linenr_T lnum)
curbuf = curwin->w_buffer;
}
-// :cbottom/:lbottom command.
+/// :cbottom/:lbottom command.
void ex_cbottom(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_lbottom) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
win_T *win = qf_find_win(qi);
@@ -3149,38 +3607,32 @@ void ex_cbottom(exarg_T *eap)
}
}
-/*
- * Return the number of the current entry (line number in the quickfix
- * window).
- */
+// Return the number of the current entry (line number in the quickfix
+// window).
linenr_T qf_current_entry(win_T *wp)
{
qf_info_T *qi = &ql_info;
- if (IS_LL_WINDOW(wp))
- /* In the location list window, use the referenced location list */
+ if (IS_LL_WINDOW(wp)) {
+ // In the location list window, use the referenced location list
qi = wp->w_llist_ref;
+ }
- return qi->qf_lists[qi->qf_curlist].qf_index;
+ return qf_get_curlist(qi)->qf_index;
}
-/*
- * Update the cursor position in the quickfix window to the current error.
- * Return TRUE if there is a quickfix window.
- */
-static int
-qf_win_pos_update (
+// Update the cursor position in the quickfix window to the current error.
+// Return TRUE if there is a quickfix window.
+static int qf_win_pos_update(
qf_info_T *qi,
- int old_qf_index /* previous qf_index or zero */
+ int old_qf_index // previous qf_index or zero
)
{
win_T *win;
- int qf_index = qi->qf_lists[qi->qf_curlist].qf_index;
+ int qf_index = qf_get_curlist(qi)->qf_index;
- /*
- * Put the cursor on the current error in the quickfix window, so that
- * it's viewable.
- */
+ // Put the cursor on the current error in the quickfix window, so that
+ // it's viewable.
win = qf_find_win(qi);
if (win != NULL
&& qf_index <= win->w_buffer->b_ml.ml_line_count
@@ -3198,8 +3650,9 @@ qf_win_pos_update (
}
/// Checks whether the given window is displaying the specified
-/// quickfix/location list buffer.
-static int is_qf_win(win_T *win, qf_info_T *qi)
+/// quickfix/location stack.
+static int is_qf_win(const win_T *win, const qf_info_T *qi)
+ FUNC_ATTR_NONNULL_ARG(2) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
//
// A window displaying the quickfix buffer will have the w_llist_ref field
@@ -3217,9 +3670,10 @@ static int is_qf_win(win_T *win, qf_info_T *qi)
return false;
}
-/// Find a window displaying the quickfix/location list 'qi'
+/// Find a window displaying the quickfix/location stack 'qi'
/// Only searches in the current tabpage.
-static win_T *qf_find_win(qf_info_T *qi)
+static win_T *qf_find_win(const qf_info_T *qi)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
FOR_ALL_WINDOWS_IN_TAB(win, curtab) {
if (is_qf_win(win, qi)) {
@@ -3230,11 +3684,10 @@ static win_T *qf_find_win(qf_info_T *qi)
return NULL;
}
-/*
- * Find a quickfix buffer.
- * Searches in windows opened in all the tabs.
- */
-static buf_T *qf_find_buf(qf_info_T *qi)
+// Find a quickfix buffer.
+// Searches in windows opened in all the tabs.
+static buf_T *qf_find_buf(const qf_info_T *qi)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
FOR_ALL_TAB_WINDOWS(tp, win) {
if (is_qf_win(win, qi)) {
@@ -3253,21 +3706,19 @@ static void qf_update_win_titlevar(qf_info_T *qi)
if ((win = qf_find_win(qi)) != NULL) {
win_T *curwin_save = curwin;
curwin = win;
- qf_set_title_var(qi);
+ qf_set_title_var(qf_get_curlist(qi));
curwin = curwin_save;
}
}
-/*
- * Find the quickfix buffer. If it exists, update the contents.
- */
+// Find the quickfix buffer. If it exists, update the contents.
static void qf_update_buffer(qf_info_T *qi, qfline_T *old_last)
{
buf_T *buf;
win_T *win;
aco_save_T aco;
- /* Check if a buffer for the quickfix list exists. Update it. */
+ // Check if a buffer for the quickfix list exists. Update it.
buf = qf_find_buf(qi);
if (buf != NULL) {
linenr_T old_line_count = buf->b_ml.ml_line_count;
@@ -3279,7 +3730,7 @@ static void qf_update_buffer(qf_info_T *qi, qfline_T *old_last)
qf_update_win_titlevar(qi);
- qf_fill_buffer(qi, buf, old_last);
+ qf_fill_buffer(qf_get_curlist(qi), buf, old_last);
buf_inc_changedtick(buf);
if (old_last == NULL) {
@@ -3297,26 +3748,82 @@ static void qf_update_buffer(qf_info_T *qi, qfline_T *old_last)
}
}
-// Set "w:quickfix_title" if "qi" has a title.
-static void qf_set_title_var(qf_info_T *qi)
+// Add an error line to the quickfix buffer.
+static int qf_buf_add_line(buf_T *buf, linenr_T lnum, const qfline_T *qfp,
+ char_u *dirname)
+ FUNC_ATTR_NONNULL_ALL
{
- if (qi->qf_lists[qi->qf_curlist].qf_title != NULL) {
- set_internal_string_var((char_u *)"w:quickfix_title",
- qi->qf_lists[qi->qf_curlist].qf_title);
+ int len;
+ buf_T *errbuf;
+
+ if (qfp->qf_module != NULL) {
+ STRCPY(IObuff, qfp->qf_module);
+ len = (int)STRLEN(IObuff);
+ } else if (qfp->qf_fnum != 0
+ && (errbuf = buflist_findnr(qfp->qf_fnum)) != NULL
+ && errbuf->b_fname != NULL) {
+ if (qfp->qf_type == 1) { // :helpgrep
+ STRLCPY(IObuff, path_tail(errbuf->b_fname), sizeof(IObuff));
+ } else {
+ // shorten the file name if not done already
+ if (errbuf->b_sfname == NULL
+ || path_is_absolute(errbuf->b_sfname)) {
+ if (*dirname == NUL) {
+ os_dirname(dirname, MAXPATHL);
+ }
+ shorten_buf_fname(errbuf, dirname, false);
+ }
+ STRLCPY(IObuff, errbuf->b_fname, sizeof(IObuff));
+ }
+ len = (int)STRLEN(IObuff);
+ } else {
+ len = 0;
}
+ IObuff[len++] = '|';
+
+ if (qfp->qf_lnum > 0) {
+ snprintf((char *)IObuff + len, sizeof(IObuff), "%" PRId64,
+ (int64_t)qfp->qf_lnum);
+ len += (int)STRLEN(IObuff + len);
+
+ if (qfp->qf_col > 0) {
+ snprintf((char *)IObuff + len, sizeof(IObuff), " col %d", qfp->qf_col);
+ len += (int)STRLEN(IObuff + len);
+ }
+
+ snprintf((char *)IObuff + len, sizeof(IObuff), "%s",
+ (char *)qf_types(qfp->qf_type, qfp->qf_nr));
+ len += (int)STRLEN(IObuff + len);
+ } else if (qfp->qf_pattern != NULL) {
+ qf_fmt_text(qfp->qf_pattern, IObuff + len, IOSIZE - len);
+ len += (int)STRLEN(IObuff + len);
+ }
+ IObuff[len++] = '|';
+ IObuff[len++] = ' ';
+
+ // Remove newlines and leading whitespace from the text.
+ // For an unrecognized line keep the indent, the compiler may
+ // mark a word with ^^^^.
+ qf_fmt_text(len > 3 ? skipwhite(qfp->qf_text) : qfp->qf_text,
+ IObuff + len, IOSIZE - len);
+
+ if (ml_append_buf(buf, lnum, IObuff,
+ (colnr_T)STRLEN(IObuff) + 1, false) == FAIL) {
+ return FAIL;
+ }
+ return OK;
}
-// Fill current buffer with quickfix errors, replacing any previous contents.
-// curbuf must be the quickfix buffer!
-// If "old_last" is not NULL append the items after this one.
-// When "old_last" is NULL then "buf" must equal "curbuf"! Because ml_delete()
-// is used and autocommands will be triggered.
-static void qf_fill_buffer(qf_info_T *qi, buf_T *buf, qfline_T *old_last)
+/// Fill current buffer with quickfix errors, replacing any previous contents.
+/// curbuf must be the quickfix buffer!
+/// If "old_last" is not NULL append the items after this one.
+/// When "old_last" is NULL then "buf" must equal "curbuf"! Because ml_delete()
+/// is used and autocommands will be triggered.
+static void qf_fill_buffer(qf_list_T *qfl, buf_T *buf, qfline_T *old_last)
+ FUNC_ATTR_NONNULL_ARG(2)
{
linenr_T lnum;
qfline_T *qfp;
- buf_T *errbuf;
- int len;
const bool old_KeyTyped = KeyTyped;
if (old_last == NULL) {
@@ -3331,73 +3838,22 @@ static void qf_fill_buffer(qf_info_T *qi, buf_T *buf, qfline_T *old_last)
}
}
- /* Check if there is anything to display */
- if (qi->qf_curlist < qi->qf_listcount) {
+ // Check if there is anything to display
+ if (qfl != NULL) {
char_u dirname[MAXPATHL];
*dirname = NUL;
// Add one line for each error
if (old_last == NULL) {
- qfp = qi->qf_lists[qi->qf_curlist].qf_start;
+ qfp = qfl->qf_start;
lnum = 0;
} else {
qfp = old_last->qf_next;
lnum = buf->b_ml.ml_line_count;
}
- while (lnum < qi->qf_lists[qi->qf_curlist].qf_count) {
- if (qfp->qf_module != NULL) {
- STRCPY(IObuff, qfp->qf_module);
- len = (int)STRLEN(IObuff);
- } else if (qfp->qf_fnum != 0
- && (errbuf = buflist_findnr(qfp->qf_fnum)) != NULL
- && errbuf->b_fname != NULL) {
- if (qfp->qf_type == 1) { // :helpgrep
- STRLCPY(IObuff, path_tail(errbuf->b_fname), sizeof(IObuff));
- } else {
- // shorten the file name if not done already
- if (errbuf->b_sfname == NULL
- || path_is_absolute(errbuf->b_sfname)) {
- if (*dirname == NUL) {
- os_dirname(dirname, MAXPATHL);
- }
- shorten_buf_fname(errbuf, dirname, false);
- }
- STRLCPY(IObuff, errbuf->b_fname, sizeof(IObuff));
- }
- len = (int)STRLEN(IObuff);
- } else {
- len = 0;
- }
- IObuff[len++] = '|';
-
- if (qfp->qf_lnum > 0) {
- sprintf((char *)IObuff + len, "%" PRId64, (int64_t)qfp->qf_lnum);
- len += (int)STRLEN(IObuff + len);
-
- if (qfp->qf_col > 0) {
- sprintf((char *)IObuff + len, " col %d", qfp->qf_col);
- len += (int)STRLEN(IObuff + len);
- }
-
- sprintf((char *)IObuff + len, "%s",
- (char *)qf_types(qfp->qf_type, qfp->qf_nr));
- len += (int)STRLEN(IObuff + len);
- } else if (qfp->qf_pattern != NULL) {
- qf_fmt_text(qfp->qf_pattern, IObuff + len, IOSIZE - len);
- len += (int)STRLEN(IObuff + len);
- }
- IObuff[len++] = '|';
- IObuff[len++] = ' ';
-
- /* Remove newlines and leading whitespace from the text.
- * For an unrecognized line keep the indent, the compiler may
- * mark a word with ^^^^. */
- qf_fmt_text(len > 3 ? skipwhite(qfp->qf_text) : qfp->qf_text,
- IObuff + len, IOSIZE - len);
-
- if (ml_append_buf(buf, lnum, IObuff, (colnr_T)STRLEN(IObuff) + 1, false)
- == FAIL) {
+ while (lnum < qfl->qf_count) {
+ if (qf_buf_add_line(buf, lnum, qfp, dirname) == FAIL) {
break;
}
lnum++;
@@ -3435,13 +3891,13 @@ static void qf_fill_buffer(qf_info_T *qi, buf_T *buf, qfline_T *old_last)
redraw_curbuf_later(NOT_VALID);
}
- /* Restore KeyTyped, setting 'filetype' may reset it. */
+ // Restore KeyTyped, setting 'filetype' may reset it.
KeyTyped = old_KeyTyped;
}
-static void qf_list_changed(qf_info_T *qi, int qf_idx)
+static void qf_list_changed(qf_list_T *qfl)
{
- qi->qf_lists[qf_idx].qf_changedtick++;
+ qfl->qf_changedtick++;
}
/// Return the quickfix/location list number with the given identifier.
@@ -3457,15 +3913,15 @@ static int qf_id2nr(const qf_info_T *const qi, const unsigned qfid)
return INVALID_QFIDX;
}
-// If the current list is not "save_qfid" and we can find the list with that ID
-// then make it the current list.
-// This is used when autocommands may have changed the current list.
-// Returns OK if successfully restored the list. Returns FAIL if the list with
-// the specified identifier (save_qfid) is not found in the stack.
+/// If the current list is not "save_qfid" and we can find the list with that ID
+/// then make it the current list.
+/// This is used when autocommands may have changed the current list.
+/// Returns OK if successfully restored the list. Returns FAIL if the list with
+/// the specified identifier (save_qfid) is not found in the stack.
static int qf_restore_list(qf_info_T *qi, unsigned save_qfid)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
- if (qi->qf_lists[qi->qf_curlist].qf_id != save_qfid) {
+ if (qf_get_curlist(qi)->qf_id != save_qfid) {
const int curlist = qf_id2nr(qi, save_qfid);
if (curlist < 0) {
// list is not present
@@ -3484,14 +3940,12 @@ static void qf_jump_first(qf_info_T *qi, unsigned save_qfid, int forceit)
return;
}
// Autocommands might have cleared the list, check for that
- if (!qf_list_empty(qi, qi->qf_curlist)) {
+ if (!qf_list_empty(qf_get_curlist(qi))) {
qf_jump(qi, 0, 0, forceit);
}
}
-/*
- * Return TRUE when using ":vimgrep" for ":grep".
- */
+// Return TRUE when using ":vimgrep" for ":grep".
int grep_internal(cmdidx_T cmdidx)
{
return (cmdidx == CMD_grep
@@ -3502,33 +3956,74 @@ int grep_internal(cmdidx_T cmdidx)
*curbuf->b_p_gp == NUL ? p_gp : curbuf->b_p_gp) == 0;
}
-/*
- * Used for ":make", ":lmake", ":grep", ":lgrep", ":grepadd", and ":lgrepadd"
- */
+// Return the make/grep autocmd name.
+static char_u *make_get_auname(cmdidx_T cmdidx)
+ FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ switch (cmdidx) {
+ case CMD_make:
+ return (char_u *)"make";
+ case CMD_lmake:
+ return (char_u *)"lmake";
+ case CMD_grep:
+ return (char_u *)"grep";
+ case CMD_lgrep:
+ return (char_u *)"lgrep";
+ case CMD_grepadd:
+ return (char_u *)"grepadd";
+ case CMD_lgrepadd:
+ return (char_u *)"lgrepadd";
+ default:
+ return NULL;
+ }
+}
+
+// Form the complete command line to invoke 'make'/'grep'. Quote the command
+// using 'shellquote' and append 'shellpipe'. Echo the fully formed command.
+static char *make_get_fullcmd(const char_u *makecmd, const char_u *fname)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET
+{
+ size_t len = STRLEN(p_shq) * 2 + STRLEN(makecmd) + 1;
+ if (*p_sp != NUL) {
+ len += STRLEN(p_sp) + STRLEN(fname) + 3;
+ }
+ char *const cmd = xmalloc(len);
+ snprintf(cmd, len, "%s%s%s", (char *)p_shq, (char *)makecmd, (char *)p_shq);
+
+ // If 'shellpipe' empty: don't redirect to 'errorfile'.
+ if (*p_sp != NUL) {
+ append_redir(cmd, len, (char *)p_sp, (char *)fname);
+ }
+
+ // Display the fully formed command. Output a newline if there's something
+ // else than the :make command that was typed (in which case the cursor is
+ // in column 0).
+ if (msg_col == 0) {
+ msg_didout = false;
+ }
+ msg_start();
+ MSG_PUTS(":!");
+ msg_outtrans((char_u *)cmd); // show what we are doing
+
+ return cmd;
+}
+
+// Used for ":make", ":lmake", ":grep", ":lgrep", ":grepadd", and ":lgrepadd"
void ex_make(exarg_T *eap)
{
char_u *fname;
win_T *wp = NULL;
qf_info_T *qi = &ql_info;
int res;
- char_u *au_name = NULL;
char_u *enc = (*curbuf->b_p_menc != NUL) ? curbuf->b_p_menc : p_menc;
- /* Redirect ":grep" to ":vimgrep" if 'grepprg' is "internal". */
+ // Redirect ":grep" to ":vimgrep" if 'grepprg' is "internal".
if (grep_internal(eap->cmdidx)) {
ex_vimgrep(eap);
return;
}
- switch (eap->cmdidx) {
- case CMD_make: au_name = (char_u *)"make"; break;
- case CMD_lmake: au_name = (char_u *)"lmake"; break;
- case CMD_grep: au_name = (char_u *)"grep"; break;
- case CMD_lgrep: au_name = (char_u *)"lgrep"; break;
- case CMD_grepadd: au_name = (char_u *)"grepadd"; break;
- case CMD_lgrepadd: au_name = (char_u *)"lgrepadd"; break;
- default: break;
- }
+ char_u *const au_name = make_get_auname(eap->cmdidx);
if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name,
curbuf->b_fname, true, curbuf)) {
if (aborting()) {
@@ -3536,9 +4031,9 @@ void ex_make(exarg_T *eap)
}
}
- if (eap->cmdidx == CMD_lmake || eap->cmdidx == CMD_lgrep
- || eap->cmdidx == CMD_lgrepadd)
+ if (is_loclist_cmd(eap->cmdidx)) {
wp = curwin;
+ }
autowrite_all();
fname = get_mef_name();
@@ -3546,28 +4041,11 @@ void ex_make(exarg_T *eap)
return;
os_remove((char *)fname); // in case it's not unique
- // If 'shellpipe' empty: don't redirect to 'errorfile'.
- const size_t len = (STRLEN(p_shq) * 2 + STRLEN(eap->arg) + 1
- + (*p_sp == NUL
- ? 0
- : STRLEN(p_sp) + STRLEN(fname) + 3));
- char *const cmd = xmalloc(len);
- snprintf(cmd, len, "%s%s%s", (char *)p_shq, (char *)eap->arg,
- (char *)p_shq);
- if (*p_sp != NUL) {
- append_redir(cmd, len, (char *) p_sp, (char *) fname);
- }
- // Output a newline if there's something else than the :make command that
- // was typed (in which case the cursor is in column 0).
- if (msg_col == 0) {
- msg_didout = false;
- }
- msg_start();
- MSG_PUTS(":!");
- msg_outtrans((char_u *)cmd); // show what we are doing
+ char *const cmd = make_get_fullcmd(eap->arg, fname);
do_shell((char_u *)cmd, 0);
+ incr_quickfix_busy();
res = qf_init(wp, fname, (eap->cmdidx != CMD_make
&& eap->cmdidx != CMD_lmake) ? p_gefm : p_efm,
@@ -3580,11 +4058,11 @@ void ex_make(exarg_T *eap)
}
}
if (res >= 0) {
- qf_list_changed(qi, qi->qf_curlist);
+ qf_list_changed(qf_get_curlist(qi));
}
// Remember the current quickfix list identifier, so that we can
// check for autocommands changing the current quickfix list.
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
if (au_name != NULL) {
apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name, curbuf->b_fname, true,
curbuf);
@@ -3595,16 +4073,15 @@ void ex_make(exarg_T *eap)
}
cleanup:
+ decr_quickfix_busy();
os_remove((char *)fname);
xfree(fname);
xfree(cmd);
}
-/*
- * Return the name for the errorfile, in allocated memory.
- * Find a new unique name when 'makeef' contains "##".
- * Returns NULL for error.
- */
+// Return the name for the errorfile, in allocated memory.
+// Find a new unique name when 'makeef' contains "##".
+// Returns NULL for error.
static char_u *get_mef_name(void)
{
char_u *p;
@@ -3626,7 +4103,7 @@ static char_u *get_mef_name(void)
if (*p == NUL)
return vim_strsave(p_mef);
- /* Keep trying until the name doesn't exist yet. */
+ // Keep trying until the name doesn't exist yet.
for (;; ) {
if (start == -1) {
start = (int)os_get_pid();
@@ -3652,23 +4129,20 @@ static char_u *get_mef_name(void)
size_t qf_get_size(exarg_T *eap)
FUNC_ATTR_NONNULL_ALL
{
- qf_info_T *qi = &ql_info;
- if (eap->cmdidx == CMD_ldo || eap->cmdidx == CMD_lfdo) {
- // Location list.
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- return 0;
- }
+ qf_info_T *qi;
+ qf_list_T *qfl;
+
+ if ((qi = qf_cmd_get_stack(eap, false)) == NULL) {
+ return 0;
}
int prev_fnum = 0;
size_t sz = 0;
qfline_T *qfp;
- size_t i;
- assert(qi->qf_lists[qi->qf_curlist].qf_count >= 0);
- for (i = 0, qfp = qi->qf_lists[qi->qf_curlist].qf_start;
- i < (size_t)qi->qf_lists[qi->qf_curlist].qf_count && qfp != NULL;
- i++, qfp = qfp->qf_next) {
+ int i;
+ assert(qf_get_curlist(qi)->qf_count >= 0);
+ qfl = qf_get_curlist(qi);
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
if (!qfp->qf_valid) {
continue;
}
@@ -3691,18 +4165,14 @@ size_t qf_get_size(exarg_T *eap)
size_t qf_get_cur_idx(exarg_T *eap)
FUNC_ATTR_NONNULL_ALL
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_ldo || eap->cmdidx == CMD_lfdo) {
- // Location list.
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- return 0;
- }
+ if ((qi = qf_cmd_get_stack(eap, false)) == NULL) {
+ return 0;
}
- assert(qi->qf_lists[qi->qf_curlist].qf_index >= 0);
- return (size_t)qi->qf_lists[qi->qf_curlist].qf_index;
+ assert(qf_get_curlist(qi)->qf_index >= 0);
+ return (size_t)qf_get_curlist(qi)->qf_index;
}
/// Returns the current index in the quickfix/location list,
@@ -3711,20 +4181,16 @@ size_t qf_get_cur_idx(exarg_T *eap)
int qf_get_cur_valid_idx(exarg_T *eap)
FUNC_ATTR_NONNULL_ALL
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_ldo || eap->cmdidx == CMD_lfdo) {
- // Location list.
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- return 1;
- }
+ if ((qi = qf_cmd_get_stack(eap, false)) == NULL) {
+ return 1;
}
- qf_list_T *qfl = &qi->qf_lists[qi->qf_curlist];
+ qf_list_T *qfl = qf_get_curlist(qi);
// Check if the list has valid errors.
- if (qfl->qf_count <= 0 || qfl->qf_nonevalid) {
+ if (!qf_list_has_valid_entries(qfl)) {
return 1;
}
@@ -3759,24 +4225,20 @@ int qf_get_cur_valid_idx(exarg_T *eap)
/// Used by :cdo, :ldo, :cfdo and :lfdo commands.
/// For :cdo and :ldo, returns the 'n'th valid error entry.
/// For :cfdo and :lfdo, returns the 'n'th valid file entry.
-static size_t qf_get_nth_valid_entry(qf_info_T *qi, size_t n, bool fdo)
+static size_t qf_get_nth_valid_entry(qf_list_T *qfl, size_t n, int fdo)
FUNC_ATTR_NONNULL_ALL
{
- qf_list_T *qfl = &qi->qf_lists[qi->qf_curlist];
-
// Check if the list has valid errors.
- if (qfl->qf_count <= 0 || qfl->qf_nonevalid) {
+ if (!qf_list_has_valid_entries(qfl)) {
return 1;
}
int prev_fnum = 0;
size_t eidx = 0;
- size_t i;
+ int i;
qfline_T *qfp;
assert(qfl->qf_count >= 0);
- for (i = 1, qfp = qfl->qf_start;
- i <= (size_t)qfl->qf_count && qfp != NULL;
- i++, qfp = qfp->qf_next) {
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
if (qfp->qf_valid) {
if (fdo) {
if (qfp->qf_fnum > 0 && qfp->qf_fnum != prev_fnum) {
@@ -3794,41 +4256,39 @@ static size_t qf_get_nth_valid_entry(qf_info_T *qi, size_t n, bool fdo)
}
}
- return i <= (size_t)qfl->qf_count ? i : 1;
+ return i <= qfl->qf_count ? (size_t)i : 1;
}
-/*
- * ":cc", ":crewind", ":cfirst" and ":clast".
- * ":ll", ":lrewind", ":lfirst" and ":llast".
- * ":cdo", ":ldo", ":cfdo" and ":lfdo".
- */
+/// ":cc", ":crewind", ":cfirst" and ":clast".
+/// ":ll", ":lrewind", ":lfirst" and ":llast".
+/// ":cdo", ":ldo", ":cfdo" and ":lfdo".
void ex_cc(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_ll
- || eap->cmdidx == CMD_lrewind
- || eap->cmdidx == CMD_lfirst
- || eap->cmdidx == CMD_llast
- || eap->cmdidx == CMD_ldo
- || eap->cmdidx == CMD_lfdo) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
int errornr;
if (eap->addr_count > 0) {
errornr = (int)eap->line2;
- } else if (eap->cmdidx == CMD_cc || eap->cmdidx == CMD_ll) {
- errornr = 0;
- } else if (eap->cmdidx == CMD_crewind || eap->cmdidx == CMD_lrewind
- || eap->cmdidx == CMD_cfirst || eap->cmdidx == CMD_lfirst) {
- errornr = 1;
} else {
- errornr = 32767;
+ switch (eap->cmdidx) {
+ case CMD_cc:
+ case CMD_ll:
+ errornr = 0;
+ break;
+ case CMD_crewind:
+ case CMD_lrewind:
+ case CMD_cfirst:
+ case CMD_lfirst:
+ errornr = 1;
+ break;
+ default:
+ errornr = 32767;
+ break;
+ }
}
// For cdo and ldo commands, jump to the nth valid error.
@@ -3842,8 +4302,9 @@ void ex_cc(exarg_T *eap)
} else {
n = 1;
}
- size_t valid_entry = qf_get_nth_valid_entry(qi, n,
- eap->cmdidx == CMD_cfdo || eap->cmdidx == CMD_lfdo);
+ size_t valid_entry = qf_get_nth_valid_entry(
+ qf_get_curlist(qi), n,
+ eap->cmdidx == CMD_cfdo || eap->cmdidx == CMD_lfdo);
assert(valid_entry <= INT_MAX);
errornr = (int)valid_entry;
}
@@ -3851,28 +4312,15 @@ void ex_cc(exarg_T *eap)
qf_jump(qi, 0, errornr, eap->forceit);
}
-/*
- * ":cnext", ":cnfile", ":cNext" and ":cprevious".
- * ":lnext", ":lNext", ":lprevious", ":lnfile", ":lNfile" and ":lpfile".
- * ":cdo", ":ldo", ":cfdo" and ":lfdo".
- */
+/// ":cnext", ":cnfile", ":cNext" and ":cprevious".
+/// ":lnext", ":lNext", ":lprevious", ":lnfile", ":lNfile" and ":lpfile".
+/// ":cdo", ":ldo", ":cfdo" and ":lfdo".
void ex_cnext(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
+ qf_info_T *qi;
- if (eap->cmdidx == CMD_lnext
- || eap->cmdidx == CMD_lNext
- || eap->cmdidx == CMD_lprevious
- || eap->cmdidx == CMD_lnfile
- || eap->cmdidx == CMD_lNfile
- || eap->cmdidx == CMD_lpfile
- || eap->cmdidx == CMD_ldo
- || eap->cmdidx == CMD_lfdo) {
- qi = GET_LOC_LIST(curwin);
- if (qi == NULL) {
- EMSG(_(e_loclist));
- return;
- }
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
}
int errornr;
@@ -3884,51 +4332,353 @@ void ex_cnext(exarg_T *eap)
errornr = 1;
}
- qf_jump(qi, (eap->cmdidx == CMD_cnext || eap->cmdidx == CMD_lnext
- || eap->cmdidx == CMD_cdo || eap->cmdidx == CMD_ldo)
- ? FORWARD
- : (eap->cmdidx == CMD_cnfile || eap->cmdidx == CMD_lnfile
- || eap->cmdidx == CMD_cfdo || eap->cmdidx == CMD_lfdo)
- ? FORWARD_FILE
- : (eap->cmdidx == CMD_cpfile || eap->cmdidx == CMD_lpfile
- || eap->cmdidx == CMD_cNfile || eap->cmdidx == CMD_lNfile)
- ? BACKWARD_FILE
- : BACKWARD,
- errornr, eap->forceit);
+ // Depending on the command jump to either next or previous entry/file.
+ Direction dir;
+ switch (eap->cmdidx) {
+ case CMD_cprevious:
+ case CMD_lprevious:
+ case CMD_cNext:
+ case CMD_lNext:
+ dir = BACKWARD;
+ break;
+ case CMD_cnfile:
+ case CMD_lnfile:
+ case CMD_cfdo:
+ case CMD_lfdo:
+ dir = FORWARD_FILE;
+ break;
+ case CMD_cpfile:
+ case CMD_lpfile:
+ case CMD_cNfile:
+ case CMD_lNfile:
+ dir = BACKWARD_FILE;
+ break;
+ case CMD_cnext:
+ case CMD_lnext:
+ case CMD_cdo:
+ case CMD_ldo:
+ default:
+ dir = FORWARD;
+ break;
+ }
+
+ qf_jump(qi, dir, errornr, eap->forceit);
+}
+
+/// Find the first entry in the quickfix list 'qfl' from buffer 'bnr'.
+/// The index of the entry is stored in 'errornr'.
+/// Returns NULL if an entry is not found.
+static qfline_T *qf_find_first_entry_in_buf(qf_list_T *qfl,
+ int bnr,
+ int *errornr)
+{
+ qfline_T *qfp = NULL;
+ int idx = 0;
+
+ // Find the first entry in this file
+ FOR_ALL_QFL_ITEMS(qfl, qfp, idx) {
+ if (qfp->qf_fnum == bnr) {
+ break;
+ }
+ }
+
+ *errornr = idx;
+ return qfp;
+}
+
+/// Find the first quickfix entry on the same line as 'entry'. Updates 'errornr'
+/// with the error number for the first entry. Assumes the entries are sorted in
+/// the quickfix list by line number.
+static qfline_T * qf_find_first_entry_on_line(qfline_T *entry, int *errornr)
+{
+ while (!got_int
+ && entry->qf_prev != NULL
+ && entry->qf_fnum == entry->qf_prev->qf_fnum
+ && entry->qf_lnum == entry->qf_prev->qf_lnum) {
+ entry = entry->qf_prev;
+ (*errornr)--;
+ }
+
+ return entry;
+}
+
+/// Find the last quickfix entry on the same line as 'entry'. Updates 'errornr'
+/// with the error number for the last entry. Assumes the entries are sorted in
+/// the quickfix list by line number.
+static qfline_T * qf_find_last_entry_on_line(qfline_T *entry, int *errornr)
+{
+ while (!got_int
+ && entry->qf_next != NULL
+ && entry->qf_fnum == entry->qf_next->qf_fnum
+ && entry->qf_lnum == entry->qf_next->qf_lnum) {
+ entry = entry->qf_next;
+ (*errornr)++;
+ }
+
+ return entry;
}
-/*
- * ":cfile"/":cgetfile"/":caddfile" commands.
- * ":lfile"/":lgetfile"/":laddfile" commands.
- */
+/// Find the first quickfix entry below line 'lnum' in buffer 'bnr'.
+/// 'qfp' points to the very first entry in the buffer and 'errornr' is the
+/// index of the very first entry in the quickfix list.
+/// Returns NULL if an entry is not found after 'lnum'.
+static qfline_T *qf_find_entry_on_next_line(int bnr,
+ linenr_T lnum,
+ qfline_T *qfp,
+ int *errornr)
+{
+ if (qfp->qf_lnum > lnum) {
+ // First entry is after line 'lnum'
+ return qfp;
+ }
+
+ // Find the entry just before or at the line 'lnum'
+ while (qfp->qf_next != NULL
+ && qfp->qf_next->qf_fnum == bnr
+ && qfp->qf_next->qf_lnum <= lnum) {
+ qfp = qfp->qf_next;
+ (*errornr)++;
+ }
+
+ if (qfp->qf_next == NULL || qfp->qf_next->qf_fnum != bnr) {
+ // No entries found after 'lnum'
+ return NULL;
+ }
+
+ // Use the entry just after line 'lnum'
+ qfp = qfp->qf_next;
+ (*errornr)++;
+
+ return qfp;
+}
+
+/// Find the first quickfix entry before line 'lnum' in buffer 'bnr'.
+/// 'qfp' points to the very first entry in the buffer and 'errornr' is the
+/// index of the very first entry in the quickfix list.
+/// Returns NULL if an entry is not found before 'lnum'.
+static qfline_T *qf_find_entry_on_prev_line(int bnr,
+ linenr_T lnum,
+ qfline_T *qfp,
+ int *errornr)
+{
+ // Find the entry just before the line 'lnum'
+ while (qfp->qf_next != NULL
+ && qfp->qf_next->qf_fnum == bnr
+ && qfp->qf_next->qf_lnum < lnum) {
+ qfp = qfp->qf_next;
+ (*errornr)++;
+ }
+
+ if (qfp->qf_lnum >= lnum) { // entry is after 'lnum'
+ return NULL;
+ }
+
+ // If multiple entries are on the same line, then use the first entry
+ qfp = qf_find_first_entry_on_line(qfp, errornr);
+
+ return qfp;
+}
+
+/// Find a quickfix entry in 'qfl' closest to line 'lnum' in buffer 'bnr' in
+/// the direction 'dir'.
+static qfline_T *qf_find_closest_entry(qf_list_T *qfl,
+ int bnr,
+ linenr_T lnum,
+ int dir,
+ int *errornr)
+{
+ qfline_T *qfp;
+
+ *errornr = 0;
+
+ // Find the first entry in this file
+ qfp = qf_find_first_entry_in_buf(qfl, bnr, errornr);
+ if (qfp == NULL) {
+ return NULL; // no entry in this file
+ }
+
+ if (dir == FORWARD) {
+ qfp = qf_find_entry_on_next_line(bnr, lnum, qfp, errornr);
+ } else {
+ qfp = qf_find_entry_on_prev_line(bnr, lnum, qfp, errornr);
+ }
+
+ return qfp;
+}
+
+/// Get the nth quickfix entry below the specified entry treating multiple
+/// entries on a single line as one. Searches forward in the list.
+static void qf_get_nth_below_entry(qfline_T *entry,
+ int *errornr,
+ linenr_T n)
+{
+ while (n-- > 0 && !got_int) {
+ qfline_T *first_entry = entry;
+ int first_errornr = *errornr;
+
+ // Treat all the entries on the same line in this file as one
+ entry = qf_find_last_entry_on_line(entry, errornr);
+
+ if (entry->qf_next == NULL
+ || entry->qf_next->qf_fnum != entry->qf_fnum) {
+ // If multiple entries are on the same line, then use the first
+ // entry
+ entry = first_entry;
+ *errornr = first_errornr;
+ break;
+ }
+
+ entry = entry->qf_next;
+ (*errornr)++;
+ }
+}
+
+/// Get the nth quickfix entry above the specified entry treating multiple
+/// entries on a single line as one. Searches backwards in the list.
+static void qf_get_nth_above_entry(qfline_T *entry,
+ int *errornr,
+ linenr_T n)
+{
+ while (n-- > 0 && !got_int) {
+ if (entry->qf_prev == NULL
+ || entry->qf_prev->qf_fnum != entry->qf_fnum) {
+ break;
+ }
+
+ entry = entry->qf_prev;
+ (*errornr)--;
+
+ // If multiple entries are on the same line, then use the first entry
+ entry = qf_find_first_entry_on_line(entry, errornr);
+ }
+}
+
+/// Find the n'th quickfix entry adjacent to line 'lnum' in buffer 'bnr' in the
+/// specified direction.
+/// Returns the error number in the quickfix list or 0 if an entry is not found.
+static int qf_find_nth_adj_entry(qf_list_T *qfl,
+ int bnr,
+ linenr_T lnum,
+ linenr_T n,
+ int dir)
+{
+ qfline_T *adj_entry;
+ int errornr;
+
+ // Find an entry closest to the specified line
+ adj_entry = qf_find_closest_entry(qfl, bnr, lnum, dir, &errornr);
+ if (adj_entry == NULL) {
+ return 0;
+ }
+
+ if (--n > 0) {
+ // Go to the n'th entry in the current buffer
+ if (dir == FORWARD) {
+ qf_get_nth_below_entry(adj_entry, &errornr, n);
+ } else {
+ qf_get_nth_above_entry(adj_entry, &errornr, n);
+ }
+ }
+
+ return errornr;
+}
+
+/// Jump to a quickfix entry in the current file nearest to the current line.
+/// ":cabove", ":cbelow", ":labove" and ":lbelow" commands
+void ex_cbelow(exarg_T *eap)
+{
+ qf_info_T *qi;
+ qf_list_T *qfl;
+ int dir;
+ int buf_has_flag;
+ int errornr = 0;
+
+ if (eap->addr_count > 0 && eap->line2 <= 0) {
+ EMSG(_(e_invrange));
+ return;
+ }
+
+ // Check whether the current buffer has any quickfix entries
+ if (eap->cmdidx == CMD_cabove || eap->cmdidx == CMD_cbelow) {
+ buf_has_flag = BUF_HAS_QF_ENTRY;
+ } else {
+ buf_has_flag = BUF_HAS_LL_ENTRY;
+ }
+ if (!(curbuf->b_has_qf_entry & buf_has_flag)) {
+ EMSG(_(e_quickfix));
+ return;
+ }
+
+ if ((qi = qf_cmd_get_stack(eap, true)) == NULL) {
+ return;
+ }
+
+ qfl = qf_get_curlist(qi);
+ // check if the list has valid errors
+ if (!qf_list_has_valid_entries(qfl)) {
+ EMSG(_(e_quickfix));
+ return;
+ }
+
+ if (eap->cmdidx == CMD_cbelow || eap->cmdidx == CMD_lbelow) {
+ dir = FORWARD;
+ } else {
+ dir = BACKWARD;
+ }
+
+ errornr = qf_find_nth_adj_entry(qfl, curbuf->b_fnum, curwin->w_cursor.lnum,
+ eap->addr_count > 0 ? eap->line2 : 0, dir);
+
+ if (errornr > 0) {
+ qf_jump(qi, 0, errornr, false);
+ } else {
+ EMSG(_(e_no_more_items));
+ }
+}
+
+
+/// Return the autocmd name for the :cfile Ex commands
+static char_u * cfile_get_auname(cmdidx_T cmdidx)
+{
+ switch (cmdidx) {
+ case CMD_cfile: return (char_u *)"cfile";
+ case CMD_cgetfile: return (char_u *)"cgetfile";
+ case CMD_caddfile: return (char_u *)"caddfile";
+ case CMD_lfile: return (char_u *)"lfile";
+ case CMD_lgetfile: return (char_u *)"lgetfile";
+ case CMD_laddfile: return (char_u *)"laddfile";
+ default: return NULL;
+ }
+}
+
+
+// ":cfile"/":cgetfile"/":caddfile" commands.
+// ":lfile"/":lgetfile"/":laddfile" commands.
void ex_cfile(exarg_T *eap)
{
win_T *wp = NULL;
qf_info_T *qi = &ql_info;
char_u *au_name = NULL;
- switch (eap->cmdidx) {
- case CMD_cfile: au_name = (char_u *)"cfile"; break;
- case CMD_cgetfile: au_name = (char_u *)"cgetfile"; break;
- case CMD_caddfile: au_name = (char_u *)"caddfile"; break;
- case CMD_lfile: au_name = (char_u *)"lfile"; break;
- case CMD_lgetfile: au_name = (char_u *)"lgetfile"; break;
- case CMD_laddfile: au_name = (char_u *)"laddfile"; break;
- default: break;
+ au_name = cfile_get_auname(eap->cmdidx);
+ if (au_name != NULL
+ && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name, NULL, false, curbuf)) {
+ if (aborting()) {
+ return;
+ }
}
- if (au_name != NULL)
- apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name, NULL, FALSE, curbuf);
- if (*eap->arg != NUL)
+ if (*eap->arg != NUL) {
set_string_option_direct((char_u *)"ef", -1, eap->arg, OPT_FREE, 0);
+ }
char_u *enc = (*curbuf->b_p_menc != NUL) ? curbuf->b_p_menc : p_menc;
- if (eap->cmdidx == CMD_lfile
- || eap->cmdidx == CMD_lgetfile
- || eap->cmdidx == CMD_laddfile) {
+ if (is_loclist_cmd(eap->cmdidx)) {
wp = curwin;
}
+ incr_quickfix_busy();
+
// This function is used by the :cfile, :cgetfile and :caddfile
// commands.
// :cfile always creates a new quickfix list and jumps to the
@@ -3943,13 +4693,14 @@ void ex_cfile(exarg_T *eap)
if (wp != NULL) {
qi = GET_LOC_LIST(wp);
if (qi == NULL) {
+ decr_quickfix_busy();
return;
}
}
if (res >= 0) {
- qf_list_changed(qi, qi->qf_curlist);
+ qf_list_changed(qf_get_curlist(qi));
}
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
if (au_name != NULL) {
apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name, NULL, false, curbuf);
}
@@ -3960,6 +4711,8 @@ void ex_cfile(exarg_T *eap)
// display the first error
qf_jump_first(qi, save_qfid, eap->forceit);
}
+
+ decr_quickfix_busy();
}
/// Return the vimgrep autocmd name.
@@ -4080,8 +4833,7 @@ static bool vgr_match_buflines(qf_info_T *qi, char_u *fname, buf_T *buf,
// Pass the buffer number so that it gets used even for a
// dummy buffer, unless duplicate_name is set, then the
// buffer will be wiped out below.
- if (qf_add_entry(qi,
- qi->qf_curlist,
+ if (qf_add_entry(qf_get_curlist(qi),
NULL, // dir
fname,
NULL,
@@ -4094,8 +4846,8 @@ static bool vgr_match_buflines(qf_info_T *qi, char_u *fname, buf_T *buf,
NULL, // search pattern
0, // nr
0, // type
- true // valid
- ) == FAIL) {
+ true) // valid
+ == QF_FAIL) {
got_int = true;
break;
}
@@ -4142,12 +4894,10 @@ static void vgr_jump_to_match(qf_info_T *qi, int forceit, int *redraw_for_dummy,
}
}
-/*
- * ":vimgrep {pattern} file(s)"
- * ":vimgrepadd {pattern} file(s)"
- * ":lvimgrep {pattern} file(s)"
- * ":lvimgrepadd {pattern} file(s)"
- */
+// ":vimgrep {pattern} file(s)"
+// ":vimgrepadd {pattern} file(s)"
+// ":lvimgrep {pattern} file(s)"
+// ":lvimgrepadd {pattern} file(s)"
void ex_vimgrep(exarg_T *eap)
{
regmmatch_T regmatch;
@@ -4157,7 +4907,7 @@ void ex_vimgrep(exarg_T *eap)
char_u *s;
char_u *p;
int fi;
- qf_info_T *qi = &ql_info;
+ qf_list_T *qfl;
win_T *wp = NULL;
buf_T *buf;
int duplicate_name = FALSE;
@@ -4182,20 +4932,14 @@ void ex_vimgrep(exarg_T *eap)
}
}
- if (eap->cmdidx == CMD_lgrep
- || eap->cmdidx == CMD_lvimgrep
- || eap->cmdidx == CMD_lgrepadd
- || eap->cmdidx == CMD_lvimgrepadd) {
- qi = ll_get_or_alloc_list(curwin);
- wp = curwin;
- }
+ qf_info_T *qi = qf_cmd_get_or_alloc_stack(eap, &wp);
if (eap->addr_count > 0)
tomatch = eap->line2;
else
tomatch = MAXLNUM;
- /* Get the search pattern: either white-separated or enclosed in // */
+ // Get the search pattern: either white-separated or enclosed in //
regmatch.regprog = NULL;
char_u *title = vim_strsave(qf_cmdtitle(*eap->cmdlinep));
p = skip_vimgrep_pat(eap->arg, &s, &flags);
@@ -4217,14 +4961,15 @@ void ex_vimgrep(exarg_T *eap)
if ((eap->cmdidx != CMD_grepadd && eap->cmdidx != CMD_lgrepadd
&& eap->cmdidx != CMD_vimgrepadd && eap->cmdidx != CMD_lvimgrepadd)
- || qi->qf_curlist == qi->qf_listcount) {
+ || qf_stack_empty(qi)) {
// make place for a new list
qf_new_list(qi, title);
}
- /* parse the list of arguments */
- if (get_arglist_exp(p, &fcount, &fnames, true) == FAIL)
+ // parse the list of arguments
+ if (get_arglist_exp(p, &fcount, &fnames, true) == FAIL) {
goto theend;
+ }
if (fcount == 0) {
EMSG(_(e_nomatch));
goto theend;
@@ -4233,27 +4978,29 @@ void ex_vimgrep(exarg_T *eap)
dirname_start = xmalloc(MAXPATHL);
dirname_now = xmalloc(MAXPATHL);
- /* Remember the current directory, because a BufRead autocommand that does
- * ":lcd %:p:h" changes the meaning of short path names. */
+ // Remember the current directory, because a BufRead autocommand that does
+ // ":lcd %:p:h" changes the meaning of short path names.
os_dirname(dirname_start, MAXPATHL);
+ incr_quickfix_busy();
+
// Remember the current quickfix list identifier, so that we can check for
// autocommands changing the current quickfix list.
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
seconds = (time_t)0;
for (fi = 0; fi < fcount && !got_int && tomatch > 0; fi++) {
fname = path_try_shorten_fname(fnames[fi]);
if (time(NULL) > seconds) {
- /* Display the file name every second or so, show the user we are
- * working on it. */
+ // Display the file name every second or so, show the user we are
+ // working on it.
seconds = time(NULL);
vgr_display_fname(fname);
}
buf = buflist_findname_exp(fnames[fi]);
if (buf == NULL || buf->b_ml.ml_mfp == NULL) {
- /* Remember that a buffer with this name already exists. */
+ // Remember that a buffer with this name already exists.
duplicate_name = (buf != NULL);
using_dummy = TRUE;
redraw_for_dummy = TRUE;
@@ -4268,9 +5015,10 @@ void ex_vimgrep(exarg_T *eap)
// buffer above, autocommands might have changed the quickfix list.
if (!vgr_qflist_valid(wp, qi, save_qfid, *eap->cmdlinep)) {
FreeWild(fcount, fnames);
+ decr_quickfix_busy();
goto theend;
}
- save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ save_qfid = qf_get_curlist(qi)->qf_id;
if (buf == NULL) {
if (!got_int)
@@ -4285,20 +5033,20 @@ void ex_vimgrep(exarg_T *eap)
if (found_match && first_match_buf == NULL)
first_match_buf = buf;
if (duplicate_name) {
- /* Never keep a dummy buffer if there is another buffer
- * with the same name. */
+ // Never keep a dummy buffer if there is another buffer
+ // with the same name.
wipe_dummy_buffer(buf, dirname_start);
buf = NULL;
} else if (!cmdmod.hide
- || buf->b_p_bh[0] == 'u' /* "unload" */
- || buf->b_p_bh[0] == 'w' /* "wipe" */
- || buf->b_p_bh[0] == 'd') { /* "delete" */
- /* When no match was found we don't need to remember the
- * buffer, wipe it out. If there was a match and it
- * wasn't the first one or we won't jump there: only
- * unload the buffer.
- * Ignore 'hidden' here, because it may lead to having too
- * many swap files. */
+ || buf->b_p_bh[0] == 'u' // "unload"
+ || buf->b_p_bh[0] == 'w' // "wipe"
+ || buf->b_p_bh[0] == 'd') { // "delete"
+ // When no match was found we don't need to remember the
+ // buffer, wipe it out. If there was a match and it
+ // wasn't the first one or we won't jump there: only
+ // unload the buffer.
+ // Ignore 'hidden' here, because it may lead to having too
+ // many swap files.
if (!found_match) {
wipe_dummy_buffer(buf, dirname_start);
buf = NULL;
@@ -4322,10 +5070,10 @@ void ex_vimgrep(exarg_T *eap)
target_dir = vim_strsave(dirname_now);
}
- /* The buffer is still loaded, the Filetype autocommands
- * need to be done now, in that buffer. And the modelines
- * need to be done (again). But not the window-local
- * options! */
+ // The buffer is still loaded, the Filetype autocommands
+ // need to be done now, in that buffer. And the modelines
+ // need to be done (again). But not the window-local
+ // options!
aucmd_prepbuf(&aco, buf);
apply_autocmds(EVENT_FILETYPE, buf->b_p_ft,
buf->b_fname, TRUE, buf);
@@ -4338,10 +5086,11 @@ void ex_vimgrep(exarg_T *eap)
FreeWild(fcount, fnames);
- qi->qf_lists[qi->qf_curlist].qf_nonevalid = FALSE;
- qi->qf_lists[qi->qf_curlist].qf_ptr = qi->qf_lists[qi->qf_curlist].qf_start;
- qi->qf_lists[qi->qf_curlist].qf_index = 1;
- qf_list_changed(qi, qi->qf_curlist);
+ qfl = qf_get_curlist(qi);
+ qfl->qf_nonevalid = false;
+ qfl->qf_ptr = qfl->qf_start;
+ qfl->qf_index = 1;
+ qf_list_changed(qfl);
qf_update_buffer(qi, NULL);
@@ -4351,16 +5100,14 @@ void ex_vimgrep(exarg_T *eap)
// The QuickFixCmdPost autocmd may free the quickfix list. Check the list
// is still valid.
- if (!qflist_valid(wp, save_qfid)) {
- goto theend;
- }
-
- if (qf_restore_list(qi, save_qfid) == FAIL) {
+ if (!qflist_valid(wp, save_qfid)
+ || qf_restore_list(qi, save_qfid) == FAIL) {
+ decr_quickfix_busy();
goto theend;
}
- /* Jump to first match. */
- if (qi->qf_lists[qi->qf_curlist].qf_count > 0) {
+ // Jump to first match.
+ if (!qf_list_empty(qf_get_curlist(qi))) {
if ((flags & VGR_NOJUMP) == 0) {
vgr_jump_to_match(qi, eap->forceit, &redraw_for_dummy, first_match_buf,
target_dir);
@@ -4368,8 +5115,10 @@ void ex_vimgrep(exarg_T *eap)
} else
EMSG2(_(e_nomatch2), s);
- /* If we loaded a dummy buffer into the current window, the autocommands
- * may have messed up things, need to redraw and recompute folds. */
+ decr_quickfix_busy();
+
+ // If we loaded a dummy buffer into the current window, the autocommands
+ // may have messed up things, need to redraw and recompute folds.
if (redraw_for_dummy) {
foldUpdateAll(curwin);
}
@@ -4382,18 +5131,16 @@ theend:
vim_regfree(regmatch.regprog);
}
-/*
- * Restore current working directory to "dirname_start" if they differ, taking
- * into account whether it is set locally or globally.
- */
+// Restore current working directory to "dirname_start" if they differ, taking
+// into account whether it is set locally or globally.
static void restore_start_dir(char_u *dirname_start)
{
char_u *dirname_now = xmalloc(MAXPATHL);
os_dirname(dirname_now, MAXPATHL);
if (STRCMP(dirname_start, dirname_now) != 0) {
- /* If the directory has changed, change it back by building up an
- * appropriate ex command and executing it. */
+ // If the directory has changed, change it back by building up an
+ // appropriate ex command and executing it.
exarg_T ea = {
.arg = dirname_start,
.cmdidx = (curwin->w_localdir == NULL) ? CMD_cd : CMD_lcd,
@@ -4403,23 +5150,21 @@ static void restore_start_dir(char_u *dirname_start)
xfree(dirname_now);
}
-/*
- * Load file "fname" into a dummy buffer and return the buffer pointer,
- * placing the directory resulting from the buffer load into the
- * "resulting_dir" pointer. "resulting_dir" must be allocated by the caller
- * prior to calling this function. Restores directory to "dirname_start" prior
- * to returning, if autocmds or the 'autochdir' option have changed it.
- *
- * If creating the dummy buffer does not fail, must call unload_dummy_buffer()
- * or wipe_dummy_buffer() later!
- *
- * Returns NULL if it fails.
- */
+// Load file "fname" into a dummy buffer and return the buffer pointer,
+// placing the directory resulting from the buffer load into the
+// "resulting_dir" pointer. "resulting_dir" must be allocated by the caller
+// prior to calling this function. Restores directory to "dirname_start" prior
+// to returning, if autocmds or the 'autochdir' option have changed it.
+//
+// If creating the dummy buffer does not fail, must call unload_dummy_buffer()
+// or wipe_dummy_buffer() later!
+//
+// Returns NULL if it fails.
static buf_T *
load_dummy_buffer (
char_u *fname,
- char_u *dirname_start, /* in: old directory */
- char_u *resulting_dir /* out: new directory */
+ char_u *dirname_start, // in: old directory
+ char_u *resulting_dir // out: new directory
)
{
buf_T *newbuf;
@@ -4436,24 +5181,24 @@ load_dummy_buffer (
}
set_bufref(&newbufref, newbuf);
- /* Init the options. */
+ // Init the options.
buf_copy_options(newbuf, BCO_ENTER | BCO_NOHELP);
- /* need to open the memfile before putting the buffer in a window */
+ // need to open the memfile before putting the buffer in a window
if (ml_open(newbuf) == OK) {
// Make sure this buffer isn't wiped out by autocommands.
newbuf->b_locked++;
// set curwin/curbuf to buf and save a few things
aucmd_prepbuf(&aco, newbuf);
- /* Need to set the filename for autocommands. */
- (void)setfname(curbuf, fname, NULL, FALSE);
+ // Need to set the filename for autocommands.
+ (void)setfname(curbuf, fname, NULL, false);
- /* Create swap file now to avoid the ATTENTION message. */
- check_need_swap(TRUE);
+ // Create swap file now to avoid the ATTENTION message.
+ check_need_swap(true);
- /* Remove the "dummy" flag, otherwise autocommands may not
- * work. */
+ // Remove the "dummy" flag, otherwise autocommands may not
+ // work.
curbuf->b_flags &= ~BF_DUMMY;
newbuf_to_wipe.br_buf = NULL;
@@ -4486,11 +5231,9 @@ load_dummy_buffer (
newbuf->b_flags |= BF_DUMMY;
}
- /*
- * When autocommands/'autochdir' option changed directory: go back.
- * Let the caller know what the resulting dir was first, in case it is
- * important.
- */
+ // When autocommands/'autochdir' option changed directory: go back.
+ // Let the caller know what the resulting dir was first, in case it is
+ // important.
os_dirname(resulting_dir, MAXPATHL);
restore_start_dir(dirname_start);
@@ -4504,55 +5247,98 @@ load_dummy_buffer (
return newbuf;
}
-/*
- * Wipe out the dummy buffer that load_dummy_buffer() created. Restores
- * directory to "dirname_start" prior to returning, if autocmds or the
- * 'autochdir' option have changed it.
- */
+// Wipe out the dummy buffer that load_dummy_buffer() created. Restores
+// directory to "dirname_start" prior to returning, if autocmds or the
+// 'autochdir' option have changed it.
static void wipe_dummy_buffer(buf_T *buf, char_u *dirname_start)
{
- if (curbuf != buf) { /* safety check */
+ if (curbuf != buf) { // safety check
cleanup_T cs;
- /* Reset the error/interrupt/exception state here so that aborting()
- * returns FALSE when wiping out the buffer. Otherwise it doesn't
- * work when got_int is set. */
+ // Reset the error/interrupt/exception state here so that aborting()
+ // returns FALSE when wiping out the buffer. Otherwise it doesn't
+ // work when got_int is set.
enter_cleanup(&cs);
wipe_buffer(buf, FALSE);
- /* Restore the error/interrupt/exception state if not discarded by a
- * new aborting error, interrupt, or uncaught exception. */
+ // Restore the error/interrupt/exception state if not discarded by a
+ // new aborting error, interrupt, or uncaught exception.
leave_cleanup(&cs);
- /* When autocommands/'autochdir' option changed directory: go back. */
+ // When autocommands/'autochdir' option changed directory: go back.
restore_start_dir(dirname_start);
}
}
-/*
- * Unload the dummy buffer that load_dummy_buffer() created. Restores
- * directory to "dirname_start" prior to returning, if autocmds or the
- * 'autochdir' option have changed it.
- */
+// Unload the dummy buffer that load_dummy_buffer() created. Restores
+// directory to "dirname_start" prior to returning, if autocmds or the
+// 'autochdir' option have changed it.
static void unload_dummy_buffer(buf_T *buf, char_u *dirname_start)
{
- if (curbuf != buf) { /* safety check */
- close_buffer(NULL, buf, DOBUF_UNLOAD, FALSE);
+ if (curbuf != buf) { // safety check
+ close_buffer(NULL, buf, DOBUF_UNLOAD, false);
- /* When autocommands/'autochdir' option changed directory: go back. */
+ // When autocommands/'autochdir' option changed directory: go back.
restore_start_dir(dirname_start);
}
}
+/// Copy the specified quickfix entry items into a new dict and appened the dict
+/// to 'list'. Returns OK on success.
+static int get_qfline_items(qfline_T *qfp, list_T *list)
+{
+ char_u buf[2];
+ int bufnum;
+
+ // Handle entries with a non-existing buffer number.
+ bufnum = qfp->qf_fnum;
+ if (bufnum != 0 && (buflist_findnr(bufnum) == NULL)) {
+ bufnum = 0;
+ }
+
+ dict_T *const dict = tv_dict_alloc();
+ tv_list_append_dict(list, dict);
+
+ buf[0] = qfp->qf_type;
+ buf[1] = NUL;
+ if (tv_dict_add_nr(dict, S_LEN("bufnr"), (varnumber_T)bufnum) == FAIL
+ || (tv_dict_add_nr(dict, S_LEN("lnum"), (varnumber_T)qfp->qf_lnum)
+ == FAIL)
+ || (tv_dict_add_nr(dict, S_LEN("col"), (varnumber_T)qfp->qf_col) == FAIL)
+ || (tv_dict_add_nr(dict, S_LEN("vcol"), (varnumber_T)qfp->qf_viscol)
+ == FAIL)
+ || (tv_dict_add_nr(dict, S_LEN("nr"), (varnumber_T)qfp->qf_nr) == FAIL)
+ || (tv_dict_add_str(
+ dict, S_LEN("module"),
+ (qfp->qf_module == NULL ? "" : (const char *)qfp->qf_module))
+ == FAIL)
+ || (tv_dict_add_str(
+ dict, S_LEN("pattern"),
+ (qfp->qf_pattern == NULL ? "" : (const char *)qfp->qf_pattern))
+ == FAIL)
+ || (tv_dict_add_str(
+ dict, S_LEN("text"),
+ (qfp->qf_text == NULL ? "" : (const char *)qfp->qf_text))
+ == FAIL)
+ || (tv_dict_add_str(dict, S_LEN("type"), (const char *)buf) == FAIL)
+ || (tv_dict_add_nr(dict, S_LEN("valid"), (varnumber_T)qfp->qf_valid)
+ == FAIL)) {
+ // tv_dict_add* fail only if key already exist, but this is a newly
+ // allocated dictionary which is thus guaranteed to have no existing keys.
+ assert(false);
+ }
+
+ return OK;
+}
+
/// Add each quickfix error to list "list" as a dictionary.
/// If qf_idx is -1, use the current list. Otherwise, use the specified list.
-int get_errorlist(const qf_info_T *qi_arg, win_T *wp, int qf_idx, list_T *list)
+int get_errorlist(qf_info_T *qi_arg, win_T *wp, int qf_idx, list_T *list)
{
- const qf_info_T *qi = qi_arg;
- char_u buf[2];
+ qf_info_T *qi = qi_arg;
+ qf_list_T *qfl;
qfline_T *qfp;
int i;
- int bufnum;
if (qi == NULL) {
qi = &ql_info;
@@ -4568,56 +5354,19 @@ int get_errorlist(const qf_info_T *qi_arg, win_T *wp, int qf_idx, list_T *list)
qf_idx = qi->qf_curlist;
}
- if (qf_idx >= qi->qf_listcount
- || qi->qf_lists[qf_idx].qf_count == 0) {
+ if (qf_idx >= qi->qf_listcount) {
return FAIL;
}
- qfp = qi->qf_lists[qf_idx].qf_start;
- for (i = 1; !got_int && i <= qi->qf_lists[qf_idx].qf_count; i++) {
- // Handle entries with a non-existing buffer number.
- bufnum = qfp->qf_fnum;
- if (bufnum != 0 && (buflist_findnr(bufnum) == NULL))
- bufnum = 0;
-
- dict_T *const dict = tv_dict_alloc();
- tv_list_append_dict(list, dict);
-
- buf[0] = qfp->qf_type;
- buf[1] = NUL;
- if (tv_dict_add_nr(dict, S_LEN("bufnr"), (varnumber_T)bufnum) == FAIL
- || (tv_dict_add_nr(dict, S_LEN("lnum"), (varnumber_T)qfp->qf_lnum)
- == FAIL)
- || (tv_dict_add_nr(dict, S_LEN("col"), (varnumber_T)qfp->qf_col)
- == FAIL)
- || (tv_dict_add_nr(dict, S_LEN("vcol"), (varnumber_T)qfp->qf_viscol)
- == FAIL)
- || (tv_dict_add_nr(dict, S_LEN("nr"), (varnumber_T)qfp->qf_nr) == FAIL)
- || tv_dict_add_str(dict, S_LEN("module"),
- (qfp->qf_module == NULL
- ? ""
- : (const char *)qfp->qf_module)) == FAIL
- || tv_dict_add_str(dict, S_LEN("pattern"),
- (qfp->qf_pattern == NULL
- ? ""
- : (const char *)qfp->qf_pattern)) == FAIL
- || tv_dict_add_str(dict, S_LEN("text"),
- (qfp->qf_text == NULL
- ? ""
- : (const char *)qfp->qf_text)) == FAIL
- || tv_dict_add_str(dict, S_LEN("type"), (const char *)buf) == FAIL
- || (tv_dict_add_nr(dict, S_LEN("valid"), (varnumber_T)qfp->qf_valid)
- == FAIL)) {
- // tv_dict_add* fail only if key already exist, but this is a newly
- // allocated dictionary which is thus guaranteed to have no existing keys.
- assert(false);
- }
+ qfl = qf_get_list(qi, qf_idx);
+ if (qf_list_empty(qfl)) {
+ return FAIL;
+ }
- qfp = qfp->qf_next;
- if (qfp == NULL) {
- break;
- }
+ FOR_ALL_QFL_ITEMS(qfl, qfp, i) {
+ get_qfline_items(qfp, list);
}
+
return OK;
}
@@ -4633,7 +5382,8 @@ enum {
QF_GETLIST_IDX = 0x40,
QF_GETLIST_SIZE = 0x80,
QF_GETLIST_TICK = 0x100,
- QF_GETLIST_ALL = 0x1FF
+ QF_GETLIST_FILEWINID = 0x200,
+ QF_GETLIST_ALL = 0x3FF,
};
/// Parse text from 'di' and return the quickfix list items.
@@ -4657,12 +5407,12 @@ static int qf_get_list_from_lines(dict_T *what, dictitem_T *di, dict_T *retdict)
}
list_T *l = tv_list_alloc(kListLenMayKnow);
- qf_info_T *const qi = ll_new_list();
+ qf_info_T *const qi = qf_alloc_stack(QFLT_INTERNAL);
if (qf_init_ext(qi, 0, NULL, NULL, &di->di_tv, errorformat,
true, (linenr_T)0, (linenr_T)0, NULL, NULL) > 0) {
(void)get_errorlist(qi, NULL, 0, l);
- qf_free(qi, 0);
+ qf_free(&qi->qf_lists[0]);
}
xfree(qi);
@@ -4689,12 +5439,17 @@ static int qf_winid(qf_info_T *qi)
}
/// Convert the keys in 'what' to quickfix list property flags.
-static int qf_getprop_keys2flags(dict_T *what)
+static int qf_getprop_keys2flags(const dict_T *what, bool loclist)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
int flags = QF_GETLIST_NONE;
if (tv_dict_find(what, S_LEN("all")) != NULL) {
flags |= QF_GETLIST_ALL;
+ if (!loclist) {
+ // File window ID is applicable only to location list windows
+ flags &= ~QF_GETLIST_FILEWINID;
+ }
}
if (tv_dict_find(what, S_LEN("title")) != NULL) {
flags |= QF_GETLIST_TITLE;
@@ -4723,6 +5478,9 @@ static int qf_getprop_keys2flags(dict_T *what)
if (tv_dict_find(what, S_LEN("changedtick")) != NULL) {
flags |= QF_GETLIST_TICK;
}
+ if (loclist && tv_dict_find(what, S_LEN("filewinid")) != NULL) {
+ flags |= QF_GETLIST_FILEWINID;
+ }
return flags;
}
@@ -4776,7 +5534,10 @@ static int qf_getprop_qfidx(qf_info_T *qi, dict_T *what)
}
/// Return default values for quickfix list properties in retdict.
-static int qf_getprop_defaults(qf_info_T *qi, int flags, dict_T *retdict)
+static int qf_getprop_defaults(qf_info_T *qi,
+ int flags,
+ int locstack,
+ dict_T *retdict)
{
int status = OK;
@@ -4808,15 +5569,37 @@ static int qf_getprop_defaults(qf_info_T *qi, int flags, dict_T *retdict)
if ((status == OK) && (flags & QF_GETLIST_TICK)) {
status = tv_dict_add_nr(retdict, S_LEN("changedtick"), 0);
}
+ if ((status == OK) && locstack && (flags & QF_GETLIST_FILEWINID)) {
+ status = tv_dict_add_nr(retdict, S_LEN("filewinid"), 0);
+ }
return status;
}
/// Return the quickfix list title as 'title' in retdict
-static int qf_getprop_title(qf_info_T *qi, int qf_idx, dict_T *retdict)
+static int qf_getprop_title(qf_list_T *qfl, dict_T *retdict)
{
return tv_dict_add_str(retdict, S_LEN("title"),
- (const char *)qi->qf_lists[qf_idx].qf_title);
+ (const char *)qfl->qf_title);
+}
+
+// Returns the identifier of the window used to display files from a location
+// list. If there is no associated window, then returns 0. Useful only when
+// called from a location list window.
+static int qf_getprop_filewinid(const win_T *wp, const qf_info_T *qi,
+ dict_T *retdict)
+ FUNC_ATTR_NONNULL_ARG(3)
+{
+ handle_T winid = 0;
+
+ if (wp != NULL && IS_LL_WINDOW(wp)) {
+ win_T *ll_wp = qf_find_win_with_loclist(qi);
+ if (ll_wp != NULL) {
+ winid = ll_wp->handle;
+ }
+ }
+
+ return tv_dict_add_nr(retdict, S_LEN("filewinid"), winid);
}
/// Return the quickfix list items/entries as 'items' in retdict
@@ -4830,13 +5613,13 @@ static int qf_getprop_items(qf_info_T *qi, int qf_idx, dict_T *retdict)
}
/// Return the quickfix list context (if any) as 'context' in retdict.
-static int qf_getprop_ctx(qf_info_T *qi, int qf_idx, dict_T *retdict)
+static int qf_getprop_ctx(qf_list_T *qfl, dict_T *retdict)
{
int status;
- if (qi->qf_lists[qf_idx].qf_ctx != NULL) {
+ if (qfl->qf_ctx != NULL) {
dictitem_T *di = tv_dict_item_alloc_len(S_LEN("context"));
- tv_copy(qi->qf_lists[qf_idx].qf_ctx, &di->di_tv);
+ tv_copy(qfl->qf_ctx, &di->di_tv);
status = tv_dict_add(retdict, di);
if (status == FAIL) {
tv_dict_item_free(di);
@@ -4848,15 +5631,15 @@ static int qf_getprop_ctx(qf_info_T *qi, int qf_idx, dict_T *retdict)
return status;
}
-/// Return the quickfix list index as 'idx' in retdict
-static int qf_getprop_idx(qf_info_T *qi, int qf_idx, dict_T *retdict)
+/// Return the current quickfix list index as 'idx' in retdict
+static int qf_getprop_idx(qf_list_T *qfl, dict_T *retdict)
{
- int idx = qi->qf_lists[qf_idx].qf_index;
- if (qi->qf_lists[qf_idx].qf_count == 0) {
- // For empty lists, qf_index is set to 1
- idx = 0;
+ int curidx = qfl->qf_index;
+ if (qf_list_empty(qfl)) {
+ // For empty lists, current index is set to 0
+ curidx = 0;
}
- return tv_dict_add_nr(retdict, S_LEN("idx"), idx);
+ return tv_dict_add_nr(retdict, S_LEN("idx"), curidx);
}
/// Return quickfix/location list details (title) as a dictionary.
@@ -4865,9 +5648,10 @@ static int qf_getprop_idx(qf_info_T *qi, int qf_idx, dict_T *retdict)
int qf_get_properties(win_T *wp, dict_T *what, dict_T *retdict)
{
qf_info_T *qi = &ql_info;
+ qf_list_T *qfl;
dictitem_T *di = NULL;
int status = OK;
- int qf_idx;
+ int qf_idx = INVALID_QFIDX;
if ((di = tv_dict_find(what, S_LEN("lines"))) != NULL) {
return qf_get_list_from_lines(what, di, retdict);
@@ -4877,19 +5661,21 @@ int qf_get_properties(win_T *wp, dict_T *what, dict_T *retdict)
qi = GET_LOC_LIST(wp);
}
- int flags = qf_getprop_keys2flags(what);
+ const int flags = qf_getprop_keys2flags(what, wp != NULL);
- if (qi != NULL && qi->qf_listcount != 0) {
+ if (!qf_stack_empty(qi)) {
qf_idx = qf_getprop_qfidx(qi, what);
}
// List is not present or is empty
- if (qi == NULL || qi->qf_listcount == 0 || qf_idx == INVALID_QFIDX) {
- return qf_getprop_defaults(qi, flags, retdict);
+ if (qf_stack_empty(qi) || qf_idx == INVALID_QFIDX) {
+ return qf_getprop_defaults(qi, flags, wp != NULL, retdict);
}
+ qfl = qf_get_list(qi, qf_idx);
+
if (flags & QF_GETLIST_TITLE) {
- status = qf_getprop_title(qi, qf_idx, retdict);
+ status = qf_getprop_title(qfl, retdict);
}
if ((status == OK) && (flags & QF_GETLIST_NR)) {
status = tv_dict_add_nr(retdict, S_LEN("nr"), qf_idx + 1);
@@ -4901,21 +5687,101 @@ int qf_get_properties(win_T *wp, dict_T *what, dict_T *retdict)
status = qf_getprop_items(qi, qf_idx, retdict);
}
if ((status == OK) && (flags & QF_GETLIST_CONTEXT)) {
- status = qf_getprop_ctx(qi, qf_idx, retdict);
+ status = qf_getprop_ctx(qfl, retdict);
}
if ((status == OK) && (flags & QF_GETLIST_ID)) {
- status = tv_dict_add_nr(retdict, S_LEN("id"), qi->qf_lists[qf_idx].qf_id);
+ status = tv_dict_add_nr(retdict, S_LEN("id"), qfl->qf_id);
}
if ((status == OK) && (flags & QF_GETLIST_IDX)) {
- status = qf_getprop_idx(qi, qf_idx, retdict);
+ status = qf_getprop_idx(qfl, retdict);
}
if ((status == OK) && (flags & QF_GETLIST_SIZE)) {
status = tv_dict_add_nr(retdict, S_LEN("size"),
- qi->qf_lists[qf_idx].qf_count);
+ qfl->qf_count);
}
if ((status == OK) && (flags & QF_GETLIST_TICK)) {
status = tv_dict_add_nr(retdict, S_LEN("changedtick"),
- qi->qf_lists[qf_idx].qf_changedtick);
+ qfl->qf_changedtick);
+ }
+ if ((status == OK) && (wp != NULL) && (flags & QF_GETLIST_FILEWINID)) {
+ status = qf_getprop_filewinid(wp, qi, retdict);
+ }
+
+ return status;
+}
+
+/// Add a new quickfix entry to list at 'qf_idx' in the stack 'qi' from the
+/// items in the dict 'd'. If it is a valid error entry, then set 'valid_entry'
+/// to true.
+static int qf_add_entry_from_dict(
+ qf_list_T *qfl,
+ const dict_T *d,
+ bool first_entry,
+ bool *valid_entry)
+ FUNC_ATTR_NONNULL_ALL
+{
+ static bool did_bufnr_emsg;
+
+ if (first_entry) {
+ did_bufnr_emsg = false;
+ }
+
+ char *const filename = tv_dict_get_string(d, "filename", true);
+ char *const module = tv_dict_get_string(d, "module", true);
+ int bufnum = (int)tv_dict_get_number(d, "bufnr");
+ const long lnum = (long)tv_dict_get_number(d, "lnum");
+ const int col = (int)tv_dict_get_number(d, "col");
+ const char_u vcol = (char_u)tv_dict_get_number(d, "vcol");
+ const int nr = (int)tv_dict_get_number(d, "nr");
+ const char *const type = tv_dict_get_string(d, "type", false);
+ char *const pattern = tv_dict_get_string(d, "pattern", true);
+ char *text = tv_dict_get_string(d, "text", true);
+ if (text == NULL) {
+ text = xcalloc(1, 1);
+ }
+ bool valid = true;
+ if ((filename == NULL && bufnum == 0)
+ || (lnum == 0 && pattern == NULL)) {
+ valid = false;
+ }
+
+ // Mark entries with non-existing buffer number as not valid. Give the
+ // error message only once.
+ if (bufnum != 0 && (buflist_findnr(bufnum) == NULL)) {
+ if (!did_bufnr_emsg) {
+ did_bufnr_emsg = true;
+ EMSGN(_("E92: Buffer %" PRId64 " not found"), bufnum);
+ }
+ valid = false;
+ bufnum = 0;
+ }
+
+ // If the 'valid' field is present it overrules the detected value.
+ if (tv_dict_find(d, "valid", -1) != NULL) {
+ valid = tv_dict_get_number(d, "valid");
+ }
+
+ const int status = qf_add_entry(qfl,
+ NULL, // dir
+ (char_u *)filename,
+ (char_u *)module,
+ bufnum,
+ (char_u *)text,
+ lnum,
+ col,
+ vcol, // vis_col
+ (char_u *)pattern, // search pattern
+ nr,
+ (char_u)(type == NULL ? NUL : *type),
+ valid);
+
+ xfree(filename);
+ xfree(module);
+ xfree(pattern);
+ xfree(text);
+
+ if (valid) {
+ *valid_entry = true;
}
return status;
@@ -4926,21 +5792,22 @@ int qf_get_properties(win_T *wp, dict_T *what, dict_T *retdict)
static int qf_add_entries(qf_info_T *qi, int qf_idx, list_T *list,
char_u *title, int action)
{
- dict_T *d;
+ qf_list_T *qfl = qf_get_list(qi, qf_idx);
qfline_T *old_last = NULL;
int retval = OK;
- bool did_bufnr_emsg = false;
+ bool valid_entry = false;
if (action == ' ' || qf_idx == qi->qf_listcount) {
// make place for a new list
qf_new_list(qi, title);
qf_idx = qi->qf_curlist;
- } else if (action == 'a' && qi->qf_lists[qf_idx].qf_count > 0) {
+ qfl = qf_get_list(qi, qf_idx);
+ } else if (action == 'a' && !qf_list_empty(qfl)) {
// Adding to existing list, use last entry.
- old_last = qi->qf_lists[qf_idx].qf_last;
+ old_last = qfl->qf_last;
} else if (action == 'r') {
- qf_free_items(qi, qf_idx);
- qf_store_title(qi, qf_idx, title);
+ qf_free_items(qfl);
+ qf_store_title(qfl, title);
}
TV_LIST_ITER_CONST(list, li, {
@@ -4948,83 +5815,35 @@ static int qf_add_entries(qf_info_T *qi, int qf_idx, list_T *list,
continue; // Skip non-dict items.
}
- d = TV_LIST_ITEM_TV(li)->vval.v_dict;
+ const dict_T *const d = TV_LIST_ITEM_TV(li)->vval.v_dict;
if (d == NULL) {
continue;
}
- char *const filename = tv_dict_get_string(d, "filename", true);
- char *const module = tv_dict_get_string(d, "module", true);
- int bufnum = (int)tv_dict_get_number(d, "bufnr");
- long lnum = (long)tv_dict_get_number(d, "lnum");
- int col = (int)tv_dict_get_number(d, "col");
- char_u vcol = (char_u)tv_dict_get_number(d, "vcol");
- int nr = (int)tv_dict_get_number(d, "nr");
- const char *type_str = tv_dict_get_string(d, "type", false);
- const char_u type = (char_u)(uint8_t)(type_str == NULL ? NUL : *type_str);
- char *const pattern = tv_dict_get_string(d, "pattern", true);
- char *text = tv_dict_get_string(d, "text", true);
- if (text == NULL) {
- text = xcalloc(1, 1);
- }
- bool valid = true;
- if ((filename == NULL && bufnum == 0) || (lnum == 0 && pattern == NULL)) {
- valid = false;
- }
-
- /* Mark entries with non-existing buffer number as not valid. Give the
- * error message only once. */
- if (bufnum != 0 && (buflist_findnr(bufnum) == NULL)) {
- if (!did_bufnr_emsg) {
- did_bufnr_emsg = TRUE;
- EMSGN(_("E92: Buffer %" PRId64 " not found"), bufnum);
- }
- valid = false;
- bufnum = 0;
- }
-
- // If the 'valid' field is present it overrules the detected value.
- if (tv_dict_find(d, "valid", -1) != NULL) {
- valid = (int)tv_dict_get_number(d, "valid");
- }
-
- int status = qf_add_entry(qi,
- qf_idx,
- NULL, // dir
- (char_u *)filename,
- (char_u *)module,
- bufnum,
- (char_u *)text,
- lnum,
- col,
- vcol, // vis_col
- (char_u *)pattern, // search pattern
- nr,
- type,
- valid);
-
- xfree(filename);
- xfree(module);
- xfree(pattern);
- xfree(text);
-
- if (status == FAIL) {
- retval = FAIL;
+ retval = qf_add_entry_from_dict(qfl, d, li == tv_list_first(list),
+ &valid_entry);
+ if (retval == QF_FAIL) {
break;
}
});
- if (qi->qf_lists[qf_idx].qf_index == 0) {
- // no valid entry
- qi->qf_lists[qf_idx].qf_nonevalid = true;
- } else {
- qi->qf_lists[qf_idx].qf_nonevalid = false;
+ // Check if any valid error entries are added to the list.
+ if (valid_entry) {
+ qfl->qf_nonevalid = false;
+ } else if (qfl->qf_index == 0) {
+ qfl->qf_nonevalid = true;
}
+
+ // If not appending to the list, set the current error to the first entry
if (action != 'a') {
- qi->qf_lists[qf_idx].qf_ptr = qi->qf_lists[qf_idx].qf_start;
- if (qi->qf_lists[qf_idx].qf_count > 0) {
- qi->qf_lists[qf_idx].qf_index = 1;
- }
+ qfl->qf_ptr = qfl->qf_start;
+ }
+
+ // Update the current error index if not appending to the list or if the
+ // list was empty before and it is not empty now.
+ if ((action != 'a' || qfl->qf_index == 0)
+ && !qf_list_empty(qfl)) {
+ qfl->qf_index = 1;
}
// Don't update the cursor in quickfix window when appending entries
@@ -5033,7 +5852,7 @@ static int qf_add_entries(qf_info_T *qi, int qf_idx, list_T *list,
return retval;
}
-// Get the quickfix list index from 'nr' or 'id'
+/// Get the quickfix list index from 'nr' or 'id'
static int qf_setprop_get_qfidx(
const qf_info_T *qi,
const dict_T *what,
@@ -5057,7 +5876,7 @@ static int qf_setprop_get_qfidx(
// non-available list and add the new list at the end of the
// stack.
*newlist = true;
- qf_idx = qi->qf_listcount > 0 ? qi->qf_listcount - 1 : 0;
+ qf_idx = qf_stack_empty(qi) ? 0 : qi->qf_listcount - 1;
} else if (qf_idx < 0 || qf_idx >= qi->qf_listcount) {
return INVALID_QFIDX;
} else if (action != ' ') {
@@ -5065,7 +5884,7 @@ static int qf_setprop_get_qfidx(
}
} else if (di->di_tv.v_type == VAR_STRING
&& strequal((const char *)di->di_tv.vval.v_string, "$")) {
- if (qi->qf_listcount > 0) {
+ if (!qf_stack_empty(qi)) {
qf_idx = qi->qf_listcount - 1;
} else if (*newlist) {
qf_idx = 0;
@@ -5093,13 +5912,13 @@ static int qf_setprop_title(qf_info_T *qi, int qf_idx, const dict_T *what,
const dictitem_T *di)
FUNC_ATTR_NONNULL_ALL
{
+ qf_list_T *qfl = qf_get_list(qi, qf_idx);
if (di->di_tv.v_type != VAR_STRING) {
return FAIL;
}
- xfree(qi->qf_lists[qf_idx].qf_title);
- qi->qf_lists[qf_idx].qf_title =
- (char_u *)tv_dict_get_string(what, "title", true);
+ xfree(qfl->qf_title);
+ qfl->qf_title = (char_u *)tv_dict_get_string(what, "title", true);
if (qf_idx == qi->qf_curlist) {
qf_update_win_titlevar(qi);
}
@@ -5153,7 +5972,7 @@ static int qf_setprop_items_from_lines(
}
if (action == 'r') {
- qf_free_items(qi, qf_idx);
+ qf_free_items(&qi->qf_lists[qf_idx]);
}
if (qf_init_ext(qi, qf_idx, NULL, NULL, &di->di_tv, errorformat,
false, (linenr_T)0, (linenr_T)0, NULL, NULL) > 0) {
@@ -5164,27 +5983,28 @@ static int qf_setprop_items_from_lines(
}
// Set quickfix list context.
-static int qf_setprop_context(qf_info_T *qi, int qf_idx, dictitem_T *di)
+static int qf_setprop_context(qf_list_T *qfl, dictitem_T *di)
FUNC_ATTR_NONNULL_ALL
{
- tv_free(qi->qf_lists[qf_idx].qf_ctx);
+ tv_free(qfl->qf_ctx);
typval_T *ctx = xcalloc(1, sizeof(typval_T));
tv_copy(&di->di_tv, ctx);
- qi->qf_lists[qf_idx].qf_ctx = ctx;
+ qfl->qf_ctx = ctx;
return OK;
}
-// Set quickfix/location list properties (title, items, context).
-// Also used to add items from parsing a list of lines.
-// Used by the setqflist() and setloclist() VimL functions.
+/// Set quickfix/location list properties (title, items, context).
+/// Also used to add items from parsing a list of lines.
+/// Used by the setqflist() and setloclist() Vim script functions.
static int qf_set_properties(qf_info_T *qi, const dict_T *what, int action,
char_u *title)
FUNC_ATTR_NONNULL_ALL
{
+ qf_list_T *qfl;
dictitem_T *di;
int retval = FAIL;
- bool newlist = action == ' ' || qi->qf_curlist == qi->qf_listcount;
+ bool newlist = action == ' ' || qf_stack_empty(qi);
int qf_idx = qf_setprop_get_qfidx(qi, what, action, &newlist);
if (qf_idx == INVALID_QFIDX) { // List not found
return FAIL;
@@ -5196,6 +6016,7 @@ static int qf_set_properties(qf_info_T *qi, const dict_T *what, int action,
qf_idx = qi->qf_curlist;
}
+ qfl = qf_get_list(qi, qf_idx);
if ((di = tv_dict_find(what, S_LEN("title"))) != NULL) {
retval = qf_setprop_title(qi, qf_idx, what, di);
}
@@ -5206,17 +6027,18 @@ static int qf_set_properties(qf_info_T *qi, const dict_T *what, int action,
retval = qf_setprop_items_from_lines(qi, qf_idx, what, di, action);
}
if ((di = tv_dict_find(what, S_LEN("context"))) != NULL) {
- retval = qf_setprop_context(qi, qf_idx, di);
+ retval = qf_setprop_context(qfl, di);
}
if (retval == OK) {
- qf_list_changed(qi, qf_idx);
+ qf_list_changed(qfl);
}
return retval;
}
-// Find the non-location list window with the specified location list.
+/// Find the non-location list window with the specified location list stack in
+/// the current tabpage.
static win_T * find_win_with_ll(qf_info_T *qi)
{
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
@@ -5237,7 +6059,7 @@ static void qf_free_stack(win_T *wp, qf_info_T *qi)
if (qfwin != NULL) {
// If the quickfix/location list window is open, then clear it
if (qi->qf_curlist < qi->qf_listcount) {
- qf_free(qi, qi->qf_curlist);
+ qf_free(qf_get_curlist(qi));
}
qf_update_buffer(qi, NULL);
}
@@ -5261,15 +6083,14 @@ static void qf_free_stack(win_T *wp, qf_info_T *qi)
} else if (IS_LL_WINDOW(orig_wp)) {
// If the location list window is open, then create a new empty location
// list
- qf_info_T *new_ll = ll_new_list();
+ qf_info_T *new_ll = qf_alloc_stack(QFLT_LOCATION);
// first free the list reference in the location list window
ll_free_all(&orig_wp->w_llist_ref);
orig_wp->w_llist_ref = new_ll;
if (llwin != NULL) {
- llwin->w_llist = new_ll;
- new_ll->qf_refcount++;
+ win_set_loclist(wp, new_ll);
}
}
}
@@ -5290,15 +6111,22 @@ int set_errorlist(win_T *wp, list_T *list, int action, char_u *title,
if (action == 'f') {
// Free the entire quickfix or location list stack
qf_free_stack(wp, qi);
- } else if (what != NULL) {
+ return OK;
+ }
+
+ incr_quickfix_busy();
+
+ if (what != NULL) {
retval = qf_set_properties(qi, what, action, title);
} else {
retval = qf_add_entries(qi, qi->qf_curlist, list, title, action);
if (retval == OK) {
- qf_list_changed(qi, qi->qf_curlist);
+ qf_list_changed(qf_get_curlist(qi));
}
}
+ decr_quickfix_busy();
+
return retval;
}
@@ -5349,45 +6177,79 @@ bool set_ref_in_quickfix(int copyID)
return abort;
}
-/*
- * ":[range]cbuffer [bufnr]" command.
- * ":[range]caddbuffer [bufnr]" command.
- * ":[range]cgetbuffer [bufnr]" command.
- * ":[range]lbuffer [bufnr]" command.
- * ":[range]laddbuffer [bufnr]" command.
- * ":[range]lgetbuffer [bufnr]" command.
- */
-void ex_cbuffer(exarg_T *eap)
+/// Return the autocmd name for the :cbuffer Ex commands
+static char_u * cbuffer_get_auname(cmdidx_T cmdidx)
+{
+ switch (cmdidx) {
+ case CMD_cbuffer: return (char_u *)"cbuffer";
+ case CMD_cgetbuffer: return (char_u *)"cgetbuffer";
+ case CMD_caddbuffer: return (char_u *)"caddbuffer";
+ case CMD_lbuffer: return (char_u *)"lbuffer";
+ case CMD_lgetbuffer: return (char_u *)"lgetbuffer";
+ case CMD_laddbuffer: return (char_u *)"laddbuffer";
+ default: return NULL;
+ }
+}
+
+/// Process and validate the arguments passed to the :cbuffer, :caddbuffer,
+/// :cgetbuffer, :lbuffer, :laddbuffer, :lgetbuffer Ex commands.
+static int cbuffer_process_args(exarg_T *eap,
+ buf_T **bufp,
+ linenr_T *line1,
+ linenr_T *line2)
{
buf_T *buf = NULL;
- qf_info_T *qi = &ql_info;
- const char *au_name = NULL;
- win_T *wp = NULL;
- switch (eap->cmdidx) {
- case CMD_cbuffer:
- au_name = "cbuffer";
- break;
- case CMD_cgetbuffer:
- au_name = "cgetbuffer";
- break;
- case CMD_caddbuffer:
- au_name = "caddbuffer";
- break;
- case CMD_lbuffer:
- au_name = "lbuffer";
- break;
- case CMD_lgetbuffer:
- au_name = "lgetbuffer";
- break;
- case CMD_laddbuffer:
- au_name = "laddbuffer";
- break;
- default:
- break;
+ if (*eap->arg == NUL)
+ buf = curbuf;
+ else if (*skipwhite(skipdigits(eap->arg)) == NUL)
+ buf = buflist_findnr(atoi((char *)eap->arg));
+
+ if (buf == NULL) {
+ EMSG(_(e_invarg));
+ return FAIL;
+ }
+
+ if (buf->b_ml.ml_mfp == NULL) {
+ EMSG(_("E681: Buffer is not loaded"));
+ return FAIL;
+ }
+
+ if (eap->addr_count == 0) {
+ eap->line1 = 1;
+ eap->line2 = buf->b_ml.ml_line_count;
+ }
+
+ if (eap->line1 < 1 || eap->line1 > buf->b_ml.ml_line_count
+ || eap->line2 < 1 || eap->line2 > buf->b_ml.ml_line_count) {
+ EMSG(_(e_invrange));
+ return FAIL;
}
- if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, (char_u *)au_name,
+ *line1 = eap->line1;
+ *line2 = eap->line2;
+ *bufp = buf;
+
+ return OK;
+}
+
+// ":[range]cbuffer [bufnr]" command.
+// ":[range]caddbuffer [bufnr]" command.
+// ":[range]cgetbuffer [bufnr]" command.
+// ":[range]lbuffer [bufnr]" command.
+// ":[range]laddbuffer [bufnr]" command.
+// ":[range]lgetbuffer [bufnr]" command.
+void ex_cbuffer(exarg_T *eap)
+{
+ buf_T *buf = NULL;
+ char_u *au_name = NULL;
+ win_T *wp = NULL;
+ char_u *qf_title;
+ linenr_T line1;
+ linenr_T line2;
+
+ au_name = cbuffer_get_auname(eap->cmdidx);
+ if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name,
curbuf->b_fname, true, curbuf)) {
if (aborting()) {
return;
@@ -5395,134 +6257,112 @@ void ex_cbuffer(exarg_T *eap)
}
// Must come after autocommands.
- if (eap->cmdidx == CMD_lbuffer
- || eap->cmdidx == CMD_lgetbuffer
- || eap->cmdidx == CMD_laddbuffer) {
- qi = ll_get_or_alloc_list(curwin);
- wp = curwin;
+ qf_info_T *qi = qf_cmd_get_or_alloc_stack(eap, &wp);
+
+ if (cbuffer_process_args(eap, &buf, &line1, &line2) == FAIL) {
+ return;
}
- if (*eap->arg == NUL)
- buf = curbuf;
- else if (*skipwhite(skipdigits(eap->arg)) == NUL)
- buf = buflist_findnr(atoi((char *)eap->arg));
- if (buf == NULL)
- EMSG(_(e_invarg));
- else if (buf->b_ml.ml_mfp == NULL)
- EMSG(_("E681: Buffer is not loaded"));
- else {
- if (eap->addr_count == 0) {
- eap->line1 = 1;
- eap->line2 = buf->b_ml.ml_line_count;
- }
- if (eap->line1 < 1 || eap->line1 > buf->b_ml.ml_line_count
- || eap->line2 < 1 || eap->line2 > buf->b_ml.ml_line_count) {
- EMSG(_(e_invrange));
- } else {
- char_u *qf_title = qf_cmdtitle(*eap->cmdlinep);
+ qf_title = qf_cmdtitle(*eap->cmdlinep);
- if (buf->b_sfname) {
- vim_snprintf((char *)IObuff, IOSIZE, "%s (%s)",
- (char *)qf_title, (char *)buf->b_sfname);
- qf_title = IObuff;
- }
+ if (buf->b_sfname) {
+ vim_snprintf((char *)IObuff, IOSIZE, "%s (%s)",
+ (char *)qf_title, (char *)buf->b_sfname);
+ qf_title = IObuff;
+ }
- int res = qf_init_ext(qi, qi->qf_curlist, NULL, buf, NULL, p_efm,
- (eap->cmdidx != CMD_caddbuffer
- && eap->cmdidx != CMD_laddbuffer),
- eap->line1, eap->line2, qf_title, NULL);
- if (res >= 0) {
- qf_list_changed(qi, qi->qf_curlist);
- }
- // Remember the current quickfix list identifier, so that we can
- // check for autocommands changing the current quickfix list.
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
- if (au_name != NULL) {
- const buf_T *const curbuf_old = curbuf;
- apply_autocmds(EVENT_QUICKFIXCMDPOST, (char_u *)au_name,
- curbuf->b_fname, true, curbuf);
- if (curbuf != curbuf_old) {
- // Autocommands changed buffer, don't jump now, "qi" may
- // be invalid.
- res = 0;
- }
- }
- // Jump to the first error for new list and if autocmds didn't
- // free the list.
- if (res > 0 && (eap->cmdidx == CMD_cbuffer || eap->cmdidx == CMD_lbuffer)
- && qflist_valid(wp, save_qfid)) {
- // display the first error
- qf_jump_first(qi, save_qfid, eap->forceit);
- }
+ incr_quickfix_busy();
+
+ int res = qf_init_ext(qi, qi->qf_curlist, NULL, buf, NULL, p_efm,
+ (eap->cmdidx != CMD_caddbuffer
+ && eap->cmdidx != CMD_laddbuffer),
+ eap->line1, eap->line2, qf_title, NULL);
+ if (qf_stack_empty(qi)) {
+ decr_quickfix_busy();
+ return;
+ }
+ if (res >= 0) {
+ qf_list_changed(qf_get_curlist(qi));
+ }
+ // Remember the current quickfix list identifier, so that we can
+ // check for autocommands changing the current quickfix list.
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
+ if (au_name != NULL) {
+ const buf_T *const curbuf_old = curbuf;
+ apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name,
+ curbuf->b_fname, true, curbuf);
+ if (curbuf != curbuf_old) {
+ // Autocommands changed buffer, don't jump now, "qi" may
+ // be invalid.
+ res = 0;
}
}
+ // Jump to the first error for new list and if autocmds didn't
+ // free the list.
+ if (res > 0 && (eap->cmdidx == CMD_cbuffer || eap->cmdidx == CMD_lbuffer)
+ && qflist_valid(wp, save_qfid)) {
+ // display the first error
+ qf_jump_first(qi, save_qfid, eap->forceit);
+ }
+
+ decr_quickfix_busy();
+}
+
+/// Return the autocmd name for the :cexpr Ex commands.
+static char_u * cexpr_get_auname(cmdidx_T cmdidx)
+{
+ switch (cmdidx) {
+ case CMD_cexpr: return (char_u *)"cexpr";
+ case CMD_cgetexpr: return (char_u *)"cgetexpr";
+ case CMD_caddexpr: return (char_u *)"caddexpr";
+ case CMD_lexpr: return (char_u *)"lexpr";
+ case CMD_lgetexpr: return (char_u *)"lgetexpr";
+ case CMD_laddexpr: return (char_u *)"laddexpr";
+ default: return NULL;
+ }
}
-/*
- * ":cexpr {expr}", ":cgetexpr {expr}", ":caddexpr {expr}" command.
- * ":lexpr {expr}", ":lgetexpr {expr}", ":laddexpr {expr}" command.
- */
+/// ":cexpr {expr}", ":cgetexpr {expr}", ":caddexpr {expr}" command.
+/// ":lexpr {expr}", ":lgetexpr {expr}", ":laddexpr {expr}" command.
void ex_cexpr(exarg_T *eap)
{
- qf_info_T *qi = &ql_info;
- const char *au_name = NULL;
+ char_u *au_name = NULL;
win_T *wp = NULL;
- switch (eap->cmdidx) {
- case CMD_cexpr:
- au_name = "cexpr";
- break;
- case CMD_cgetexpr:
- au_name = "cgetexpr";
- break;
- case CMD_caddexpr:
- au_name = "caddexpr";
- break;
- case CMD_lexpr:
- au_name = "lexpr";
- break;
- case CMD_lgetexpr:
- au_name = "lgetexpr";
- break;
- case CMD_laddexpr:
- au_name = "laddexpr";
- break;
- default:
- break;
- }
- if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, (char_u *)au_name,
+ au_name = cexpr_get_auname(eap->cmdidx);
+ if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name,
curbuf->b_fname, true, curbuf)) {
if (aborting()) {
return;
}
}
- if (eap->cmdidx == CMD_lexpr
- || eap->cmdidx == CMD_lgetexpr
- || eap->cmdidx == CMD_laddexpr) {
- qi = ll_get_or_alloc_list(curwin);
- wp = curwin;
- }
+ qf_info_T *qi = qf_cmd_get_or_alloc_stack(eap, &wp);
- /* Evaluate the expression. When the result is a string or a list we can
- * use it to fill the errorlist. */
+ // Evaluate the expression. When the result is a string or a list we can
+ // use it to fill the errorlist.
typval_T tv;
if (eval0(eap->arg, &tv, NULL, true) != FAIL) {
if ((tv.v_type == VAR_STRING && tv.vval.v_string != NULL)
|| tv.v_type == VAR_LIST) {
+ incr_quickfix_busy();
int res = qf_init_ext(qi, qi->qf_curlist, NULL, NULL, &tv, p_efm,
(eap->cmdidx != CMD_caddexpr
&& eap->cmdidx != CMD_laddexpr),
(linenr_T)0, (linenr_T)0,
qf_cmdtitle(*eap->cmdlinep), NULL);
+ if (qf_stack_empty(qi)) {
+ decr_quickfix_busy();
+ goto cleanup;
+ }
if (res >= 0) {
- qf_list_changed(qi, qi->qf_curlist);
+ qf_list_changed(qf_get_curlist(qi));
}
// Remember the current quickfix list identifier, so that we can
// check for autocommands changing the current quickfix list.
- unsigned save_qfid = qi->qf_lists[qi->qf_curlist].qf_id;
+ unsigned save_qfid = qf_get_curlist(qi)->qf_id;
if (au_name != NULL) {
- apply_autocmds(EVENT_QUICKFIXCMDPOST, (char_u *)au_name,
+ apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name,
curbuf->b_fname, true, curbuf);
}
// Jump to the first error for a new list and if autocmds didn't
@@ -5533,33 +6373,155 @@ void ex_cexpr(exarg_T *eap)
// display the first error
qf_jump_first(qi, save_qfid, eap->forceit);
}
+ decr_quickfix_busy();
} else {
EMSG(_("E777: String or List expected"));
}
+cleanup:
tv_clear(&tv);
}
}
-/*
- * ":helpgrep {pattern}"
- */
-void ex_helpgrep(exarg_T *eap)
+// Get the location list for ":lhelpgrep"
+static qf_info_T *hgr_get_ll(bool *new_ll)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET
+{
+ win_T *wp;
+ qf_info_T *qi;
+
+ // If the current window is a help window, then use it
+ if (bt_help(curwin->w_buffer)) {
+ wp = curwin;
+ } else {
+ // Find an existing help window
+ wp = qf_find_help_win();
+ }
+
+ if (wp == NULL) { // Help window not found
+ qi = NULL;
+ } else {
+ qi = wp->w_llist;
+ }
+ if (qi == NULL) {
+ // Allocate a new location list for help text matches
+ qi = qf_alloc_stack(QFLT_LOCATION);
+ *new_ll = true;
+ }
+
+ return qi;
+}
+
+// Search for a pattern in a help file.
+static void hgr_search_file(
+ qf_info_T *qi,
+ char_u *fname,
+ regmatch_T *p_regmatch)
+ FUNC_ATTR_NONNULL_ARG(1, 3)
+{
+ FILE *const fd = os_fopen((char *)fname, "r");
+ if (fd == NULL) {
+ return;
+ }
+
+ long lnum = 1;
+ while (!vim_fgets(IObuff, IOSIZE, fd) && !got_int) {
+ char_u *line = IObuff;
+
+ if (vim_regexec(p_regmatch, line, (colnr_T)0)) {
+ int l = (int)STRLEN(line);
+
+ // remove trailing CR, LF, spaces, etc.
+ while (l > 0 && line[l - 1] <= ' ') {
+ line[--l] = NUL;
+ }
+
+ if (qf_add_entry(qf_get_curlist(qi),
+ NULL, // dir
+ fname,
+ NULL,
+ 0,
+ line,
+ lnum,
+ (int)(p_regmatch->startp[0] - line) + 1, // col
+ false, // vis_col
+ NULL, // search pattern
+ 0, // nr
+ 1, // type
+ true) // valid
+ == QF_FAIL) {
+ got_int = true;
+ if (line != IObuff) {
+ xfree(line);
+ }
+ break;
+ }
+ }
+ if (line != IObuff) {
+ xfree(line);
+ }
+ lnum++;
+ line_breakcheck();
+ }
+ fclose(fd);
+}
+
+// Search for a pattern in all the help files in the doc directory under
+// the given directory.
+static void hgr_search_files_in_dir(
+ qf_info_T *qi,
+ char_u *dirname,
+ regmatch_T *p_regmatch,
+ const char_u *lang)
+ FUNC_ATTR_NONNULL_ARG(1, 2, 3)
{
- regmatch_T regmatch;
- char_u *save_cpo;
- char_u *p;
int fcount;
- char_u **fnames;
- FILE *fd;
- int fi;
- long lnum;
- char_u *lang;
- qf_info_T *qi = &ql_info;
- int new_qi = FALSE;
- char_u *au_name = NULL;
+ char_u **fnames;
+
+ // Find all "*.txt" and "*.??x" files in the "doc" directory.
+ add_pathsep((char *)dirname);
+ STRCAT(dirname, "doc/*.\\(txt\\|??x\\)"); // NOLINT
+ if (gen_expand_wildcards(1, &dirname, &fcount,
+ &fnames, EW_FILE|EW_SILENT) == OK
+ && fcount > 0) {
+ for (int fi = 0; fi < fcount && !got_int; fi++) {
+ // Skip files for a different language.
+ if (lang != NULL
+ && STRNICMP(lang, fnames[fi] + STRLEN(fnames[fi]) - 3, 2) != 0
+ && !(STRNICMP(lang, "en", 2) == 0
+ && STRNICMP("txt", fnames[fi] + STRLEN(fnames[fi]) - 3, 3)
+ == 0)) {
+ continue;
+ }
- /* Check for a specified language */
- lang = check_help_lang(eap->arg);
+ hgr_search_file(qi, fnames[fi], p_regmatch);
+ }
+ FreeWild(fcount, fnames);
+ }
+}
+
+// Search for a pattern in all the help files in the 'runtimepath'
+// and add the matches to a quickfix list.
+// 'lang' is the language specifier. If supplied, then only matches in the
+// specified language are found.
+static void hgr_search_in_rtp(qf_info_T *qi, regmatch_T *p_regmatch,
+ const char_u *lang)
+ FUNC_ATTR_NONNULL_ARG(1, 2)
+{
+ // Go through all directories in 'runtimepath'
+ char_u *p = p_rtp;
+ while (*p != NUL && !got_int) {
+ copy_option_part(&p, NameBuff, MAXPATHL, ",");
+
+ hgr_search_files_in_dir(qi, NameBuff, p_regmatch, lang);
+ }
+}
+
+// ":helpgrep {pattern}"
+void ex_helpgrep(exarg_T *eap)
+{
+ qf_info_T *qi = &ql_info;
+ bool new_qi = false;
+ char_u *au_name = NULL;
switch (eap->cmdidx) {
case CMD_helpgrep: au_name = (char_u *)"helpgrep"; break;
@@ -5573,150 +6535,67 @@ void ex_helpgrep(exarg_T *eap)
}
}
- /* Make 'cpoptions' empty, the 'l' flag should not be used here. */
- save_cpo = p_cpo;
+ // Make 'cpoptions' empty, the 'l' flag should not be used here.
+ char_u *const save_cpo = p_cpo;
p_cpo = empty_option;
- if (eap->cmdidx == CMD_lhelpgrep) {
- win_T *wp = NULL;
-
- // If the current window is a help window, then use it
- if (bt_help(curwin->w_buffer)) {
- wp = curwin;
- } else {
- // Find an existing help window
- FOR_ALL_WINDOWS_IN_TAB(wp2, curtab) {
- if (bt_help(wp2->w_buffer)) {
- wp = wp2;
- break;
- }
- }
- }
-
- if (wp == NULL) { // Help window not found
- qi = NULL;
- } else {
- qi = wp->w_llist;
- }
- if (qi == NULL) {
- /* Allocate a new location list for help text matches */
- qi = ll_new_list();
- new_qi = TRUE;
- }
+ if (is_loclist_cmd(eap->cmdidx)) {
+ qi = hgr_get_ll(&new_qi);
}
- regmatch.regprog = vim_regcomp(eap->arg, RE_MAGIC + RE_STRING);
- regmatch.rm_ic = FALSE;
+ incr_quickfix_busy();
+
+ // Check for a specified language
+ char_u *const lang = check_help_lang(eap->arg);
+ regmatch_T regmatch = {
+ .regprog = vim_regcomp(eap->arg, RE_MAGIC + RE_STRING),
+ .rm_ic = false,
+ };
if (regmatch.regprog != NULL) {
// Create a new quickfix list.
qf_new_list(qi, qf_cmdtitle(*eap->cmdlinep));
- // Go through all the directories in 'runtimepath'
- p = p_rtp;
- while (*p != NUL && !got_int) {
- copy_option_part(&p, NameBuff, MAXPATHL, ",");
-
- /* Find all "*.txt" and "*.??x" files in the "doc" directory. */
- add_pathsep((char *)NameBuff);
- STRCAT(NameBuff, "doc/*.\\(txt\\|??x\\)");
-
- // Note: We cannot just do `&NameBuff` because it is a statically sized array
- // so `NameBuff == &NameBuff` according to C semantics.
- char_u *buff_list[1] = {NameBuff};
- if (gen_expand_wildcards(1, buff_list, &fcount,
- &fnames, EW_FILE|EW_SILENT) == OK
- && fcount > 0) {
- for (fi = 0; fi < fcount && !got_int; fi++) {
- // Skip files for a different language.
- if (lang != NULL
- && STRNICMP(lang, fnames[fi] + STRLEN(fnames[fi]) - 3, 2) != 0
- && !(STRNICMP(lang, "en", 2) == 0
- && STRNICMP("txt", fnames[fi]
- + STRLEN(fnames[fi]) - 3, 3) == 0)) {
- continue;
- }
- fd = os_fopen((char *)fnames[fi], "r");
- if (fd != NULL) {
- lnum = 1;
- while (!vim_fgets(IObuff, IOSIZE, fd) && !got_int) {
- char_u *line = IObuff;
- if (vim_regexec(&regmatch, line, (colnr_T)0)) {
- int l = (int)STRLEN(line);
-
- /* remove trailing CR, LF, spaces, etc. */
- while (l > 0 && line[l - 1] <= ' ')
- line[--l] = NUL;
-
- if (qf_add_entry(qi,
- qi->qf_curlist,
- NULL, // dir
- fnames[fi],
- NULL,
- 0,
- line,
- lnum,
- (int)(regmatch.startp[0] - line)
- + 1, // col
- false, // vis_col
- NULL, // search pattern
- 0, // nr
- 1, // type
- true) // valid
- == FAIL) {
- got_int = true;
- if (line != IObuff) {
- xfree(line);
- }
- break;
- }
- }
- if (line != IObuff)
- xfree(line);
- ++lnum;
- line_breakcheck();
- }
- fclose(fd);
- }
- }
- FreeWild(fcount, fnames);
- }
- }
+ hgr_search_in_rtp(qi, &regmatch, lang);
vim_regfree(regmatch.regprog);
- qi->qf_lists[qi->qf_curlist].qf_nonevalid = FALSE;
- qi->qf_lists[qi->qf_curlist].qf_ptr =
- qi->qf_lists[qi->qf_curlist].qf_start;
- qi->qf_lists[qi->qf_curlist].qf_index = 1;
+ qf_list_T *qfl = qf_get_curlist(qi);
+ qfl->qf_nonevalid = false;
+ qfl->qf_ptr = qfl->qf_start;
+ qfl->qf_index = 1;
+ qf_list_changed(qfl);
+ qf_update_buffer(qi, NULL);
}
- if (p_cpo == empty_option)
+ if (p_cpo == empty_option) {
p_cpo = save_cpo;
- else
- /* Darn, some plugin changed the value. */
+ } else {
+ // Darn, some plugin changed the value.
free_string_option(save_cpo);
-
- qf_list_changed(qi, qi->qf_curlist);
- qf_update_buffer(qi, NULL);
+ }
if (au_name != NULL) {
apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name,
curbuf->b_fname, true, curbuf);
if (!new_qi && IS_LL_STACK(qi) && qf_find_buf(qi) == NULL) {
// autocommands made "qi" invalid
+ decr_quickfix_busy();
return;
}
}
- /* Jump to first match. */
- if (qi->qf_lists[qi->qf_curlist].qf_count > 0)
- qf_jump(qi, 0, 0, FALSE);
- else
+ // Jump to first match.
+ if (!qf_list_empty(qf_get_curlist(qi))) {
+ qf_jump(qi, 0, 0, false);
+ } else {
EMSG2(_(e_nomatch2), eap->arg);
+ }
+
+ decr_quickfix_busy();
if (eap->cmdidx == CMD_lhelpgrep) {
- /* If the help window is not opened or if it already points to the
- * correct location list, then free the new location list. */
+ // If the help window is not opened or if it already points to the
+ // correct location list, then free the new location list.
if (!bt_help(curwin->w_buffer) || curwin->w_llist == qi) {
if (new_qi) {
ll_free_all(&qi);
@@ -5727,3 +6606,4 @@ void ex_helpgrep(exarg_T *eap)
}
}
+
diff --git a/src/nvim/regexp_nfa.c b/src/nvim/regexp_nfa.c
index 63640e723e..f33c61d39f 100644
--- a/src/nvim/regexp_nfa.c
+++ b/src/nvim/regexp_nfa.c
@@ -4943,7 +4943,7 @@ static int skip_to_start(int c, colnr_T *colp)
*/
static long find_match_text(colnr_T startcol, int regstart, char_u *match_text)
{
-#define PTR2LEN(x) enc_utf8 ? utf_ptr2len(x) : MB_PTR2LEN(x)
+#define PTR2LEN(x) utf_ptr2len(x)
colnr_T col = startcol;
int regstart_len = PTR2LEN(regline + startcol);
diff --git a/src/nvim/screen.c b/src/nvim/screen.c
index 5bcd2c808d..1d29ae064e 100644
--- a/src/nvim/screen.c
+++ b/src/nvim/screen.c
@@ -124,7 +124,7 @@
// temporary buffer for rendering a single screenline, so it can be
-// comparared with previous contents to calulate smallest delta.
+// comparared with previous contents to calculate smallest delta.
static size_t linebuf_size = 0;
static schar_T *linebuf_char = NULL;
static sattr_T *linebuf_attr = NULL;
@@ -286,6 +286,11 @@ int update_screen(int type)
return FAIL;
}
+ // May have postponed updating diffs.
+ if (need_diff_redraw) {
+ diff_redraw(true);
+ }
+
if (must_redraw) {
if (type < must_redraw) /* use maximal type */
type = must_redraw;
@@ -577,11 +582,14 @@ void conceal_check_cursor_line(void)
/// Whether cursorline is drawn in a special way
///
/// If true, both old and new cursorline will need
-/// need to be redrawn when moving cursor within windows.
+/// to be redrawn when moving cursor within windows.
+/// TODO(bfredl): VIsual_active shouldn't be needed, but is used to fix a glitch
+/// caused by scrolling.
bool win_cursorline_standout(const win_T *wp)
FUNC_ATTR_NONNULL_ALL
{
- return wp->w_p_cul || (wp->w_p_cole > 0 && !conceal_cursor_line(wp));
+ return wp->w_p_cul
+ || (wp->w_p_cole > 0 && (VIsual_active || !conceal_cursor_line(wp)));
}
/*
@@ -871,7 +879,7 @@ static void win_update(win_T *wp)
if (wp->w_lines[0].wl_lnum != wp->w_topline)
i += diff_check_fill(wp, wp->w_lines[0].wl_lnum)
- wp->w_old_topfill;
- if (i < wp->w_grid.Rows - 2) { // less than a screen off
+ if (i != 0 && i < wp->w_grid.Rows - 2) { // less than a screen off
// Try to insert the correct number of lines.
// If not the last window, delete the lines at the bottom.
// win_ins_lines may fail when the terminal can't do it.
@@ -2403,10 +2411,10 @@ win_line (
filler_todo = filler_lines;
// Cursor line highlighting for 'cursorline' in the current window.
- if (wp->w_p_cul && lnum == wp->w_cursor.lnum) {
+ if (lnum == wp->w_cursor.lnum) {
// Do not show the cursor line when Visual mode is active, because it's
// not clear what is selected then.
- if (!(wp == curwin && VIsual_active)) {
+ if (wp->w_p_cul && !(wp == curwin && VIsual_active)) {
int cul_attr = win_hl_attr(wp, HLF_CUL);
HlAttrs ae = syn_attr2entry(cul_attr);
@@ -2991,7 +2999,7 @@ win_line (
if (shl->startcol != MAXCOL
&& v >= (long)shl->startcol
&& v < (long)shl->endcol) {
- int tmp_col = v + MB_PTR2LEN(ptr);
+ int tmp_col = v + utfc_ptr2len(ptr);
if (shl->endcol < tmp_col) {
shl->endcol = tmp_col;
@@ -3756,14 +3764,12 @@ win_line (
char_attr = hl_combine_attr(char_attr, extra_attr);
}
- /*
- * Handle the case where we are in column 0 but not on the first
- * character of the line and the user wants us to show us a
- * special character (via 'listchars' option "precedes:<char>".
- */
+ // Handle the case where we are in column 0 but not on the first
+ // character of the line and the user wants us to show us a
+ // special character (via 'listchars' option "precedes:<char>".
if (lcs_prec_todo != NUL
&& wp->w_p_list
- && (wp->w_p_wrap ? wp->w_skipcol > 0 : wp->w_leftcol > 0)
+ && (wp->w_p_wrap ? (wp->w_skipcol > 0 && row == 0) : wp->w_leftcol > 0)
&& filler_todo <= 0
&& draw_state > WL_NR
&& c != NUL) {
@@ -4007,7 +4013,7 @@ win_line (
break;
}
- ++vcol;
+ vcol += cells;
}
}
@@ -4015,10 +4021,13 @@ win_line (
if (wp->w_buffer->terminal) {
// terminal buffers may need to highlight beyond the end of the
// logical line
- while (col < grid->Columns) {
+ int n = wp->w_p_rl ? -1 : 1;
+ while (col >= 0 && col < grid->Columns) {
schar_from_ascii(linebuf_char[off], ' ');
- linebuf_attr[off++] = term_attrs[vcol++];
- col++;
+ linebuf_attr[off] = term_attrs[vcol];
+ off += n;
+ vcol += n;
+ col += n;
}
}
grid_put_linebuf(grid, row, 0, col, grid->Columns, wp->w_p_rl, wp,
@@ -4353,7 +4362,7 @@ static int grid_char_needs_redraw(ScreenGrid *grid, int off_from, int off_to,
|| (line_off2cells(linebuf_char, off_from, off_from + cols) > 1
&& schar_cmp(linebuf_char[off_from + 1],
grid->chars[off_to + 1])))
- || p_wd < 0));
+ || rdb_flags & RDB_NODELTA));
}
/// Move one buffered line to the window grid, but only the characters that
@@ -5103,6 +5112,8 @@ win_redr_custom (
win_T *ewp;
int p_crb_save;
+ ScreenGrid *grid = &default_grid;
+
/* There is a tiny chance that this gets called recursively: When
* redrawing a status line triggers redrawing the ruler or tabline.
* Avoid trouble by not allowing recursion. */
@@ -5142,10 +5153,11 @@ win_redr_custom (
}
maxwidth = wp->w_width - col;
if (!wp->w_status_height) {
+ grid = &msg_grid_adj;
row = Rows - 1;
maxwidth--; // writing in last column may cause scrolling
fillchar = ' ';
- attr = 0;
+ attr = HL_ATTR(HLF_MSG);
}
use_sandbox = was_set_insecurely((char_u *)"rulerformat", 0);
@@ -5195,13 +5207,13 @@ win_redr_custom (
/*
* Draw each snippet with the specified highlighting.
*/
- grid_puts_line_start(&default_grid, row);
+ grid_puts_line_start(grid, row);
curattr = attr;
p = buf;
for (n = 0; hltab[n].start != NULL; n++) {
int textlen = (int)(hltab[n].start - p);
- grid_puts_len(&default_grid, p, textlen, row, col, curattr);
+ grid_puts_len(grid, p, textlen, row, col, curattr);
col += vim_strnsize(p, textlen);
p = hltab[n].start;
@@ -5215,7 +5227,7 @@ win_redr_custom (
curattr = highlight_user[hltab[n].userhl - 1];
}
// Make sure to use an empty string instead of p, if p is beyond buf + len.
- grid_puts(&default_grid, p >= buf + len ? (char_u *)"" : p, row, col,
+ grid_puts(grid, p >= buf + len ? (char_u *)"" : p, row, col,
curattr);
grid_puts_line_flush(false);
@@ -7060,7 +7072,7 @@ static void win_redr_ruler(win_T *wp, int always)
} else {
row = Rows - 1;
fillchar = ' ';
- attr = 0;
+ attr = HL_ATTR(HLF_MSG);
width = Columns;
off = 0;
}
diff --git a/src/nvim/search.c b/src/nvim/search.c
index 7d1c19d68c..5e32715e49 100644
--- a/src/nvim/search.c
+++ b/src/nvim/search.c
@@ -516,19 +516,17 @@ void last_pat_prog(regmmatch_T *regmatch)
/// the index of the first matching
/// subpattern plus one; one if there was none.
int searchit(
- win_T *win, /* window to search in, can be NULL for a
- buffer without a window! */
+ win_T *win, // window to search in; can be NULL for a
+ // buffer without a window!
buf_T *buf,
pos_T *pos,
- pos_T *end_pos, // set to end of the match, unless NULL
+ pos_T *end_pos, // set to end of the match, unless NULL
Direction dir,
char_u *pat,
long count,
int options,
- int pat_use, // which pattern to use when "pat" is empty
- linenr_T stop_lnum, // stop after this line number when != 0
- proftime_T *tm, // timeout limit or NULL
- int *timed_out // set when timed out or NULL
+ int pat_use, // which pattern to use when "pat" is empty
+ searchit_arg_T *extra_arg // optional extra arguments, can be NULL
)
{
int found;
@@ -548,7 +546,16 @@ int searchit(
int submatch = 0;
bool first_match = true;
int save_called_emsg = called_emsg;
- int break_loop = FALSE;
+ int break_loop = false;
+ linenr_T stop_lnum = 0; // stop after this line number when != 0
+ proftime_T *tm = NULL; // timeout limit or NULL
+ int *timed_out = NULL; // set when timed out or NULL
+
+ if (extra_arg != NULL) {
+ stop_lnum = extra_arg->sa_stop_lnum;
+ tm = extra_arg->sa_tm;
+ timed_out = &extra_arg->sa_timed_out;
+ }
if (search_regcomp(pat, RE_SEARCH, pat_use,
(options & (SEARCH_HIS + SEARCH_KEEP)), &regmatch) == FAIL) {
@@ -681,7 +688,7 @@ int searchit(
}
if (matchcol == matchpos.col && ptr[matchcol] != NUL) {
- matchcol += MB_PTR2LEN(ptr + matchcol);
+ matchcol += utfc_ptr2len(ptr + matchcol);
}
if (matchcol == 0 && (options & SEARCH_START)) {
@@ -889,6 +896,9 @@ int searchit(
give_warning((char_u *)_(dir == BACKWARD
? top_bot_msg : bot_top_msg), true);
}
+ if (extra_arg != NULL) {
+ extra_arg->sa_wrapped = true;
+ }
}
if (got_int || called_emsg
|| (timed_out != NULL && *timed_out)
@@ -983,8 +993,7 @@ int do_search(
char_u *pat,
long count,
int options,
- proftime_T *tm, // timeout limit or NULL
- int *timed_out // flag set on timeout or NULL
+ searchit_arg_T *sia // optional arguments or NULL
)
{
pos_T pos; /* position of the last match */
@@ -1271,7 +1280,7 @@ int do_search(
& (SEARCH_KEEP + SEARCH_PEEK + SEARCH_HIS + SEARCH_MSG
+ SEARCH_START
+ ((pat != NULL && *pat == ';') ? 0 : SEARCH_NOOF)))),
- RE_LAST, (linenr_T)0, tm, timed_out);
+ RE_LAST, sia);
if (dircp != NULL) {
*dircp = dirc; // restore second '/' or '?' for normal_cmd()
@@ -1755,7 +1764,7 @@ pos_T *findmatchlimit(oparg_T *oap, int initc, int flags, int64_t maxtravel)
find_mps_values(&initc, &findc, &backwards, FALSE);
if (findc)
break;
- pos.col += MB_PTR2LEN(linep + pos.col);
+ pos.col += utfc_ptr2len(linep + pos.col);
}
if (!findc) {
/* no brace in the line, maybe use " #if" then */
@@ -2234,14 +2243,14 @@ showmatch(
for (p = curbuf->b_p_mps; *p != NUL; ++p) {
if (PTR2CHAR(p) == c && (curwin->w_p_rl ^ p_ri))
break;
- p += MB_PTR2LEN(p) + 1;
- if (PTR2CHAR(p) == c
- && !(curwin->w_p_rl ^ p_ri)
- )
+ p += utfc_ptr2len(p) + 1;
+ if (PTR2CHAR(p) == c && !(curwin->w_p_rl ^ p_ri)) {
break;
- p += MB_PTR2LEN(p);
- if (*p == NUL)
+ }
+ p += utfc_ptr2len(p);
+ if (*p == NUL) {
return;
+ }
}
if ((lpos = findmatch(NULL, NUL)) == NULL) { // no match, so beep
@@ -3768,30 +3777,31 @@ find_prev_quote(
return col_start;
}
-/*
- * Find quote under the cursor, cursor at end.
- * Returns TRUE if found, else FALSE.
- */
-int
-current_quote(
+// Find quote under the cursor, cursor at end.
+// Returns true if found, else false.
+bool current_quote(
oparg_T *oap,
long count,
- int include, /* TRUE == include quote char */
- int quotechar /* Quote character */
+ bool include, // true == include quote char
+ int quotechar // Quote character
)
+ FUNC_ATTR_NONNULL_ALL
{
char_u *line = get_cursor_line_ptr();
int col_end;
int col_start = curwin->w_cursor.col;
bool inclusive = false;
- int vis_empty = true; // Visual selection <= 1 char
- int vis_bef_curs = false; // Visual starts before cursor
- int inside_quotes = false; // Looks like "i'" done before
- int selected_quote = false; // Has quote inside selection
+ bool vis_empty = true; // Visual selection <= 1 char
+ bool vis_bef_curs = false; // Visual starts before cursor
+ bool did_exclusive_adj = false; // adjusted pos for 'selection'
+ bool inside_quotes = false; // Looks like "i'" done before
+ bool selected_quote = false; // Has quote inside selection
int i;
- int restore_vis_bef = false; // resotre VIsual on abort
+ bool restore_vis_bef = false; // resotre VIsual on abort
- // Correct cursor when 'selection' is "exclusive".
+ // When 'selection' is "exclusive" move the cursor to where it would be
+ // with 'selection' "inclusive", so that the logic is the same for both.
+ // The cursor then is moved forward after adjusting the area.
if (VIsual_active) {
// this only works within one line
if (VIsual.lnum != curwin->w_cursor.lnum) {
@@ -3799,8 +3809,17 @@ current_quote(
}
vis_bef_curs = lt(VIsual, curwin->w_cursor);
+ vis_empty = equalpos(VIsual, curwin->w_cursor);
if (*p_sel == 'e') {
- if (!vis_bef_curs) {
+ if (vis_bef_curs) {
+ dec_cursor();
+ did_exclusive_adj = true;
+ } else if (!vis_empty) {
+ dec(&VIsual);
+ did_exclusive_adj = true;
+ }
+ vis_empty = equalpos(VIsual, curwin->w_cursor);
+ if (!vis_bef_curs && !vis_empty) {
// VIsual needs to be start of Visual selection.
pos_T t = curwin->w_cursor;
@@ -3809,9 +3828,7 @@ current_quote(
vis_bef_curs = true;
restore_vis_bef = true;
}
- dec_cursor();
}
- vis_empty = equalpos(VIsual, curwin->w_cursor);
}
if (!vis_empty) {
@@ -3836,7 +3853,7 @@ current_quote(
/* Find out if we have a quote in the selection. */
while (i <= col_end)
if (line[i++] == quotechar) {
- selected_quote = TRUE;
+ selected_quote = true;
break;
}
}
@@ -3925,8 +3942,8 @@ current_quote(
}
}
- /* When "include" is TRUE, include spaces after closing quote or before
- * the starting quote. */
+ // When "include" is true, include spaces after closing quote or before
+ // the starting quote.
if (include) {
if (ascii_iswhite(line[col_end + 1]))
while (ascii_iswhite(line[col_end + 1]))
@@ -3972,9 +3989,10 @@ current_quote(
inclusive = true;
if (VIsual_active) {
if (vis_empty || vis_bef_curs) {
- /* decrement cursor when 'selection' is not exclusive */
- if (*p_sel != 'e')
+ // decrement cursor when 'selection' is not exclusive
+ if (*p_sel != 'e') {
dec_cursor();
+ }
} else {
/* Cursor is at start of Visual area. Set the end of the Visual
* area when it was just inside quotes or it didn't end at a
@@ -3998,11 +4016,13 @@ current_quote(
oap->inclusive = inclusive;
}
- return OK;
+ return true;
abort_search:
if (VIsual_active && *p_sel == 'e') {
- inc_cursor();
+ if (did_exclusive_adj) {
+ inc_cursor();
+ }
if (restore_vis_bef) {
pos_T t = curwin->w_cursor;
@@ -4028,9 +4048,6 @@ current_search(
bool old_p_ws = p_ws;
pos_T save_VIsual = VIsual;
- /* wrapping should not occur */
- p_ws = false;
-
/* Correct cursor when 'selection' is exclusive */
if (VIsual_active && *p_sel == 'e' && lt(VIsual, curwin->w_cursor))
dec_cursor();
@@ -4040,25 +4057,21 @@ current_search(
pos_T pos; // position after the pattern
int result; // result of various function calls
+ orig_pos = pos = curwin->w_cursor;
if (VIsual_active) {
- orig_pos = pos = curwin->w_cursor;
-
// Searching further will extend the match.
if (forward) {
incl(&pos);
} else {
decl(&pos);
}
- } else {
- orig_pos = pos = curwin->w_cursor;
}
// Is the pattern is zero-width?, this time, don't care about the direction
- int one_char = is_one_char(spats[last_idx].pat, true, &curwin->w_cursor,
- FORWARD);
- if (one_char == -1) {
- p_ws = old_p_ws;
- return FAIL; /* pattern not found */
+ int zero_width = is_zero_width(spats[last_idx].pat, true, &curwin->w_cursor,
+ FORWARD);
+ if (zero_width == -1) {
+ return FAIL; // pattern not found
}
/*
@@ -4070,15 +4083,22 @@ current_search(
int dir = forward ? i : !i;
int flags = 0;
- if (!dir && !one_char) {
+ if (!dir && !zero_width) {
flags = SEARCH_END;
}
end_pos = pos;
+ // wrapping should not occur in the first round
+ if (i == 0) {
+ p_ws = false;
+ }
+
result = searchit(curwin, curbuf, &pos, &end_pos,
(dir ? FORWARD : BACKWARD),
spats[last_idx].pat, i ? count : 1,
- SEARCH_KEEP | flags, RE_SEARCH, 0, NULL, NULL);
+ SEARCH_KEEP | flags, RE_SEARCH, NULL);
+
+ p_ws = old_p_ws;
// First search may fail, but then start searching from the
// beginning of the file (cursor might be on the search match)
@@ -4088,7 +4108,6 @@ current_search(
curwin->w_cursor = orig_pos;
if (VIsual_active)
VIsual = save_VIsual;
- p_ws = old_p_ws;
return FAIL;
} else if (i == 0 && !result) {
if (forward) { // try again from start of buffer
@@ -4100,26 +4119,24 @@ current_search(
ml_get(curwin->w_buffer->b_ml.ml_line_count));
}
}
- p_ws = old_p_ws;
}
pos_T start_pos = pos;
- p_ws = old_p_ws;
-
if (!VIsual_active) {
VIsual = start_pos;
}
// put cursor on last character of match
curwin->w_cursor = end_pos;
- if (lt(VIsual, end_pos)) {
+ if (lt(VIsual, end_pos) && forward) {
dec_cursor();
+ } else if (VIsual_active && lt(curwin->w_cursor, VIsual)) {
+ curwin->w_cursor = pos; // put the cursor on the start of the match
}
VIsual_active = true;
VIsual_mode = 'v';
- redraw_curbuf_later(INVERTED); // Update the inversion.
if (*p_sel == 'e') {
// Correction for exclusive selection depends on the direction.
if (forward && ltoreq(VIsual, curwin->w_cursor)) {
@@ -4141,13 +4158,13 @@ current_search(
return OK;
}
-/// Check if the pattern is one character long or zero-width.
+/// Check if the pattern is zero-width.
/// If move is true, check from the beginning of the buffer,
/// else from position "cur".
/// "direction" is FORWARD or BACKWARD.
/// Returns TRUE, FALSE or -1 for failure.
-static int is_one_char(char_u *pattern, bool move, pos_T *cur,
- Direction direction)
+static int
+is_zero_width(char_u *pattern, int move, pos_T *cur, Direction direction)
{
regmmatch_T regmatch;
int nmatched = 0;
@@ -4175,7 +4192,7 @@ static int is_one_char(char_u *pattern, bool move, pos_T *cur,
flag = SEARCH_START;
}
if (searchit(curwin, curbuf, &pos, NULL, direction, pattern, 1,
- SEARCH_KEEP + flag, RE_SEARCH, 0, NULL, NULL) != FAIL) {
+ SEARCH_KEEP + flag, RE_SEARCH, NULL) != FAIL) {
// Zero-width pattern should match somewhere, then we can check if
// start and end are in the same position.
called_emsg = false;
@@ -4184,7 +4201,7 @@ static int is_one_char(char_u *pattern, bool move, pos_T *cur,
nmatched = vim_regexec_multi(&regmatch, curwin, curbuf,
pos.lnum, regmatch.startpos[0].col,
NULL, NULL);
- if (!nmatched) {
+ if (nmatched != 0) {
break;
}
} while (direction == FORWARD
@@ -4195,10 +4212,6 @@ static int is_one_char(char_u *pattern, bool move, pos_T *cur,
result = (nmatched != 0
&& regmatch.startpos[0].lnum == regmatch.endpos[0].lnum
&& regmatch.startpos[0].col == regmatch.endpos[0].col);
- // one char width
- if (!result && inc(&pos) >= 0 && pos.col == regmatch.endpos[0].col) {
- result = true;
- }
}
}
@@ -4262,7 +4275,7 @@ static void search_stat(int dirc, pos_T *pos,
start = profile_setlimit(20L);
while (!got_int && searchit(curwin, curbuf, &lastpos, NULL,
FORWARD, NULL, 1, SEARCH_KEEP, RE_LAST,
- (linenr_T)0, NULL, NULL) != FAIL) {
+ NULL) != FAIL) {
// Stop after passing the time limit.
if (profile_passed_limit(start)) {
cnt = OUT_OF_TIME;
@@ -4458,7 +4471,8 @@ find_pattern_in_path(
if (i == max_path_depth) {
break;
}
- if (path_full_compare(new_fname, files[i].name, true) & kEqualFiles) {
+ if (path_full_compare(new_fname, files[i].name,
+ true, true) & kEqualFiles) {
if (type != CHECK_PATH
&& action == ACTION_SHOW_ALL && files[i].matched) {
msg_putchar('\n'); // cursor below last one */
@@ -4844,7 +4858,7 @@ exit_matched:
&& action == ACTION_EXPAND
&& !(compl_cont_status & CONT_SOL)
&& *startp != NUL
- && *(p = startp + MB_PTR2LEN(startp)) != NUL)
+ && *(p = startp + utfc_ptr2len(startp)) != NUL)
goto search_line;
}
line_breakcheck();
diff --git a/src/nvim/search.h b/src/nvim/search.h
index cb094aab8c..0366aee8a1 100644
--- a/src/nvim/search.h
+++ b/src/nvim/search.h
@@ -70,6 +70,15 @@ typedef struct spat {
dict_T *additional_data; ///< Additional data from ShaDa file.
} SearchPattern;
+/// Optional extra arguments for searchit().
+typedef struct {
+ linenr_T sa_stop_lnum; ///< stop after this line number when != 0
+ proftime_T *sa_tm; ///< timeout limit or NULL
+ int sa_timed_out; ///< set when timed out
+ int sa_wrapped; ///< search wrapped around
+} searchit_arg_T;
+
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "search.h.generated.h"
#endif
diff --git a/src/nvim/spell.c b/src/nvim/spell.c
index 724a0332bc..5feb7efda9 100644
--- a/src/nvim/spell.c
+++ b/src/nvim/spell.c
@@ -1910,11 +1910,11 @@ int init_syl_tab(slang_T *slang)
// Count the number of syllables in "word".
// When "word" contains spaces the syllables after the last space are counted.
// Returns zero if syllables are not defines.
-static int count_syllables(slang_T *slang, char_u *word)
+static int count_syllables(slang_T *slang, const char_u *word)
+ FUNC_ATTR_NONNULL_ALL
{
int cnt = 0;
bool skip = false;
- char_u *p;
int len;
syl_item_T *syl;
int c;
@@ -1922,7 +1922,7 @@ static int count_syllables(slang_T *slang, char_u *word)
if (slang->sl_syllable == NULL)
return 0;
- for (p = word; *p != NUL; p += len) {
+ for (const char_u *p = word; *p != NUL; p += len) {
// When running into a space reset counter.
if (*p == ' ') {
len = 1;
@@ -2031,7 +2031,8 @@ char_u *did_set_spelllang(win_T *wp)
// Check if we loaded this language before.
for (slang = first_lang; slang != NULL; slang = slang->sl_next) {
- if (path_full_compare(lang, slang->sl_fname, false) == kEqualFiles) {
+ if (path_full_compare(lang, slang->sl_fname, false, true)
+ == kEqualFiles) {
break;
}
}
@@ -2076,7 +2077,7 @@ char_u *did_set_spelllang(win_T *wp)
// Loop over the languages, there can be several files for "lang".
for (slang = first_lang; slang != NULL; slang = slang->sl_next) {
if (filename
- ? path_full_compare(lang, slang->sl_fname, false) == kEqualFiles
+ ? path_full_compare(lang, slang->sl_fname, false, true) == kEqualFiles
: STRICMP(lang, slang->sl_name) == 0) {
region_mask = REGION_ALL;
if (!filename && region != NULL) {
@@ -2129,7 +2130,7 @@ char_u *did_set_spelllang(win_T *wp)
for (c = 0; c < ga.ga_len; ++c) {
p = LANGP_ENTRY(ga, c)->lp_slang->sl_fname;
if (p != NULL
- && path_full_compare(spf_name, p, false) == kEqualFiles) {
+ && path_full_compare(spf_name, p, false, true) == kEqualFiles) {
break;
}
}
@@ -2139,7 +2140,8 @@ char_u *did_set_spelllang(win_T *wp)
// Check if it was loaded already.
for (slang = first_lang; slang != NULL; slang = slang->sl_next) {
- if (path_full_compare(spf_name, slang->sl_fname, false) == kEqualFiles) {
+ if (path_full_compare(spf_name, slang->sl_fname, false, true)
+ == kEqualFiles) {
break;
}
}
@@ -2568,36 +2570,33 @@ void init_spell_chartab(void)
/// Thus this only works properly when past the first character of the word.
///
/// @param wp Buffer used.
-static bool spell_iswordp(char_u *p, win_T *wp)
+static bool spell_iswordp(const char_u *p, const win_T *wp)
+ FUNC_ATTR_NONNULL_ALL
{
- char_u *s;
- int l;
int c;
- if (has_mbyte) {
- l = MB_PTR2LEN(p);
- s = p;
- if (l == 1) {
- // be quick for ASCII
- if (wp->w_s->b_spell_ismw[*p])
- s = p + 1; // skip a mid-word character
- } else {
- c = utf_ptr2char(p);
- if (c < 256 ? wp->w_s->b_spell_ismw[c]
- : (wp->w_s->b_spell_ismw_mb != NULL
- && vim_strchr(wp->w_s->b_spell_ismw_mb, c) != NULL)) {
- s = p + l;
- }
+ const int l = utfc_ptr2len(p);
+ const char_u *s = p;
+ if (l == 1) {
+ // be quick for ASCII
+ if (wp->w_s->b_spell_ismw[*p]) {
+ s = p + 1; // skip a mid-word character
}
-
- c = utf_ptr2char(s);
- if (c > 255) {
- return spell_mb_isword_class(mb_get_class(s), wp);
+ } else {
+ c = utf_ptr2char(p);
+ if (c < 256
+ ? wp->w_s->b_spell_ismw[c]
+ : (wp->w_s->b_spell_ismw_mb != NULL
+ && vim_strchr(wp->w_s->b_spell_ismw_mb, c) != NULL)) {
+ s = p + l;
}
- return spelltab.st_isw[c];
}
- return spelltab.st_isw[wp->w_s->b_spell_ismw[*p] ? p[1] : p[0]];
+ c = utf_ptr2char(s);
+ if (c > 255) {
+ return spell_mb_isword_class(mb_get_class(s), wp);
+ }
+ return spelltab.st_isw[c];
}
// Returns true if "p" points to a word character.
@@ -2615,7 +2614,8 @@ bool spell_iswordp_nmw(const char_u *p, win_T *wp)
// Only for characters above 255.
// Unicode subscript and superscript are not considered word characters.
// See also utf_class() in mbyte.c.
-static bool spell_mb_isword_class(int cl, win_T *wp)
+static bool spell_mb_isword_class(int cl, const win_T *wp)
+ FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
if (wp->w_s->b_cjk)
// East Asian characters are not considered word characters.
@@ -2625,9 +2625,10 @@ static bool spell_mb_isword_class(int cl, win_T *wp)
// Returns true if "p" points to a word character.
// Wide version of spell_iswordp().
-static bool spell_iswordp_w(int *p, win_T *wp)
+static bool spell_iswordp_w(const int *p, const win_T *wp)
+ FUNC_ATTR_NONNULL_ALL
{
- int *s;
+ const int *s;
if (*p < 256 ? wp->w_s->b_spell_ismw[*p]
: (wp->w_s->b_spell_ismw_mb != NULL
@@ -3031,7 +3032,7 @@ void ex_spellrepall(exarg_T *eap)
sub_nlines = 0;
curwin->w_cursor.lnum = 0;
while (!got_int) {
- if (do_search(NULL, '/', frompat, 1L, SEARCH_KEEP, NULL, NULL) == 0
+ if (do_search(NULL, '/', frompat, 1L, SEARCH_KEEP, NULL) == 0
|| u_save_cursor() == FAIL) {
break;
}
@@ -4116,7 +4117,7 @@ static void suggest_trie_walk(suginfo_T *su, langp_T *lp, char_u *fword, bool so
&& goodword_ends) {
int l;
- l = MB_PTR2LEN(fword + sp->ts_fidx);
+ l = utfc_ptr2len(fword + sp->ts_fidx);
if (fword_ends) {
// Copy the skipped character to preword.
memmove(preword + sp->ts_prewordlen,
@@ -4262,7 +4263,7 @@ static void suggest_trie_walk(suginfo_T *su, langp_T *lp, char_u *fword, bool so
// Correct ts_fidx for the byte length of the
// character (we didn't check that before).
sp->ts_fidx = sp->ts_fcharstart
- + MB_PTR2LEN(fword + sp->ts_fcharstart);
+ + utfc_ptr2len(fword + sp->ts_fcharstart);
// For changing a composing character adjust
// the score from SCORE_SUBST to
@@ -4361,7 +4362,7 @@ static void suggest_trie_walk(suginfo_T *su, langp_T *lp, char_u *fword, bool so
// a bit illogical for soundfold tree but it does give better
// results.
c = utf_ptr2char(fword + sp->ts_fidx);
- stack[depth].ts_fidx += MB_PTR2LEN(fword + sp->ts_fidx);
+ stack[depth].ts_fidx += utfc_ptr2len(fword + sp->ts_fidx);
if (utf_iscomposing(c)) {
stack[depth].ts_score -= SCORE_DEL - SCORE_DELCOMP;
} else if (c == utf_ptr2char(fword + stack[depth].ts_fidx)) {
@@ -4531,9 +4532,9 @@ static void suggest_trie_walk(suginfo_T *su, langp_T *lp, char_u *fword, bool so
case STATE_UNSWAP:
// Undo the STATE_SWAP swap: "21" -> "12".
p = fword + sp->ts_fidx;
- n = MB_PTR2LEN(p);
+ n = utfc_ptr2len(p);
c = utf_ptr2char(p + n);
- memmove(p + MB_PTR2LEN(p + n), p, n);
+ memmove(p + utfc_ptr2len(p + n), p, n);
utf_char2bytes(c, p);
FALLTHROUGH;
@@ -4587,11 +4588,11 @@ static void suggest_trie_walk(suginfo_T *su, langp_T *lp, char_u *fword, bool so
case STATE_UNSWAP3:
// Undo STATE_SWAP3: "321" -> "123"
p = fword + sp->ts_fidx;
- n = MB_PTR2LEN(p);
+ n = utfc_ptr2len(p);
c2 = utf_ptr2char(p + n);
- fl = MB_PTR2LEN(p + n);
+ fl = utfc_ptr2len(p + n);
c = utf_ptr2char(p + n + fl);
- tl = MB_PTR2LEN(p + n + fl);
+ tl = utfc_ptr2len(p + n + fl);
memmove(p + fl + tl, p, n);
utf_char2bytes(c, p);
utf_char2bytes(c2, p + tl);
@@ -4635,10 +4636,10 @@ static void suggest_trie_walk(suginfo_T *su, langp_T *lp, char_u *fword, bool so
case STATE_UNROT3L:
// Undo ROT3L: "231" -> "123"
p = fword + sp->ts_fidx;
- n = MB_PTR2LEN(p);
- n += MB_PTR2LEN(p + n);
+ n = utfc_ptr2len(p);
+ n += utfc_ptr2len(p + n);
c = utf_ptr2char(p + n);
- tl = MB_PTR2LEN(p + n);
+ tl = utfc_ptr2len(p + n);
memmove(p + tl, p, n);
utf_char2bytes(c, p);
@@ -4673,9 +4674,9 @@ static void suggest_trie_walk(suginfo_T *su, langp_T *lp, char_u *fword, bool so
// Undo ROT3R: "312" -> "123"
p = fword + sp->ts_fidx;
c = utf_ptr2char(p);
- tl = MB_PTR2LEN(p);
- n = MB_PTR2LEN(p + tl);
- n += MB_PTR2LEN(p + tl + n);
+ tl = utfc_ptr2len(p);
+ n = utfc_ptr2len(p + tl);
+ n += utfc_ptr2len(p + tl + n);
memmove(p, p + tl, n);
utf_char2bytes(c, p + n);
diff --git a/src/nvim/spellfile.c b/src/nvim/spellfile.c
index 01daafa09e..4fac001bc5 100644
--- a/src/nvim/spellfile.c
+++ b/src/nvim/spellfile.c
@@ -265,6 +265,8 @@
// follow; never used in prefix tree
#define BY_SPECIAL BY_FLAGS2 // highest special byte value
+#define ZERO_FLAG 65009 // used when flag is zero: "0"
+
// Flags used in .spl file for soundsalike flags.
#define SAL_F0LLOWUP 1
#define SAL_COLLAPSE 2
@@ -1787,7 +1789,7 @@ spell_reload_one (
bool didit = false;
for (slang = first_lang; slang != NULL; slang = slang->sl_next) {
- if (path_full_compare(fname, slang->sl_fname, false) == kEqualFiles) {
+ if (path_full_compare(fname, slang->sl_fname, false, true) == kEqualFiles) {
slang_clear(slang);
if (spell_load_file(fname, NULL, slang, false) == NULL)
// reloading failed, clear the language
@@ -2783,6 +2785,7 @@ static unsigned affitem2flag(int flagtype, char_u *item, char_u *fname, int lnum
}
// Get one affix name from "*pp" and advance the pointer.
+// Returns ZERO_FLAG for "0".
// Returns zero for an error, still advances the pointer then.
static unsigned get_affitem(int flagtype, char_u **pp)
{
@@ -2794,6 +2797,9 @@ static unsigned get_affitem(int flagtype, char_u **pp)
return 0;
}
res = getdigits_int(pp, true, 0);
+ if (res == 0) {
+ res = ZERO_FLAG;
+ }
} else {
res = mb_ptr2char_adv((const char_u **)pp);
if (flagtype == AFT_LONG || (flagtype == AFT_CAPLONG
@@ -2915,10 +2921,15 @@ static bool flag_in_afflist(int flagtype, char_u *afflist, unsigned flag)
int digits = getdigits_int(&p, true, 0);
assert(digits >= 0);
n = (unsigned int)digits;
- if (n == flag)
+ if (n == 0) {
+ n = ZERO_FLAG;
+ }
+ if (n == flag) {
return true;
- if (*p != NUL) // skip over comma
- ++p;
+ }
+ if (*p != NUL) { // skip over comma
+ p++;
+ }
}
break;
}
@@ -4719,7 +4730,8 @@ static void spell_make_sugfile(spellinfo_T *spin, char_u *wfname)
// of the code for the soundfolding stuff.
// It might have been done already by spell_reload_one().
for (slang = first_lang; slang != NULL; slang = slang->sl_next) {
- if (path_full_compare(wfname, slang->sl_fname, false) == kEqualFiles) {
+ if (path_full_compare(wfname, slang->sl_fname, false, true)
+ == kEqualFiles) {
break;
}
}
diff --git a/src/nvim/state.c b/src/nvim/state.c
index 81bc078a88..b195c1d96b 100644
--- a/src/nvim/state.c
+++ b/src/nvim/state.c
@@ -156,7 +156,7 @@ char *get_mode(void)
buf[0] = 'n';
if (finish_op) {
buf[1] = 'o';
- // to be able to detect force-linewise/blockwise/characterwise operations
+ // to be able to detect force-linewise/blockwise/charwise operations
buf[2] = (char)motion_force;
} else if (restart_edit == 'I' || restart_edit == 'R'
|| restart_edit == 'V') {
diff --git a/src/nvim/strings.c b/src/nvim/strings.c
index 3ba9354c67..2f5491fda5 100644
--- a/src/nvim/strings.c
+++ b/src/nvim/strings.c
@@ -953,11 +953,17 @@ int vim_vsnprintf_typval(
- mb_string2cells((char_u *)str_arg));
}
if (precision) {
- const char *p1 = str_arg;
- for (size_t i = 0; i < precision && *p1; i++) {
- p1 += mb_ptr2len((const char_u *)p1);
+ char_u *p1;
+ size_t i = 0;
+
+ for (p1 = (char_u *)str_arg; *p1;
+ p1 += mb_ptr2len(p1)) {
+ i += (size_t)utf_ptr2cells(p1);
+ if (i > precision) {
+ break;
+ }
}
- str_arg_l = precision = (size_t)(p1 - str_arg);
+ str_arg_l = precision = (size_t)(p1 - (char_u *)str_arg);
}
}
break;
diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c
index 5a61238d8c..61ee225eba 100644
--- a/src/nvim/syntax.c
+++ b/src/nvim/syntax.c
@@ -116,10 +116,10 @@ static int include_link = 0; /* when 2 include "nvim/link" and "clear" */
/// following names, separated by commas (but no spaces!).
static char *(hl_name_table[]) =
{ "bold", "standout", "underline", "undercurl",
- "italic", "reverse", "inverse", "strikethrough", "NONE" };
+ "italic", "reverse", "inverse", "strikethrough", "nocombine", "NONE" };
static int hl_attr_table[] =
{ HL_BOLD, HL_STANDOUT, HL_UNDERLINE, HL_UNDERCURL, HL_ITALIC, HL_INVERSE,
- HL_INVERSE, HL_STRIKETHROUGH, 0 };
+ HL_INVERSE, HL_STRIKETHROUGH, HL_NOCOMBINE, 0 };
// The patterns that are being searched for are stored in a syn_pattern.
// A match item consists of one pattern.
@@ -2460,11 +2460,8 @@ update_si_end(
int force /* when TRUE overrule a previous end */
)
{
- lpos_T startpos;
- lpos_T endpos;
lpos_T hl_endpos;
lpos_T end_endpos;
- int end_idx;
/* return quickly for a keyword */
if (sip->si_idx < 0)
@@ -2480,9 +2477,12 @@ update_si_end(
* We need to find the end of the region. It may continue in the next
* line.
*/
- end_idx = 0;
- startpos.lnum = current_lnum;
- startpos.col = startcol;
+ int end_idx = 0;
+ lpos_T startpos = {
+ .lnum = current_lnum,
+ .col = startcol,
+ };
+ lpos_T endpos = { 0 };
find_endpos(sip->si_idx, &startpos, &endpos, &hl_endpos,
&(sip->si_flags), &end_endpos, &end_idx, sip->si_extmatch);
@@ -7316,7 +7316,7 @@ static void set_hl_attr(int idx)
sgp->sg_attr = hl_get_syn_attr(idx+1, at_en);
- // a cursor style uses this syn_id, make sure its atribute is updated.
+ // a cursor style uses this syn_id, make sure its attribute is updated.
if (cursor_mode_uses_syn_id(idx+1)) {
ui_mode_info_set();
}
@@ -7569,8 +7569,8 @@ void highlight_changed(void)
{
int id;
char_u userhl[30]; // use 30 to avoid compiler warning
- int id_SNC = -1;
int id_S = -1;
+ int id_SNC = 0;
int hlcnt;
need_highlight_changed = FALSE;
diff --git a/src/nvim/tag.c b/src/nvim/tag.c
index 91f3da1793..3629b37c32 100644
--- a/src/nvim/tag.c
+++ b/src/nvim/tag.c
@@ -51,17 +51,21 @@
* Structure to hold pointers to various items in a tag line.
*/
typedef struct tag_pointers {
- /* filled in by parse_tag_line(): */
- char_u *tagname; /* start of tag name (skip "file:") */
- char_u *tagname_end; /* char after tag name */
- char_u *fname; /* first char of file name */
- char_u *fname_end; /* char after file name */
- char_u *command; /* first char of command */
- /* filled in by parse_match(): */
- char_u *command_end; /* first char after command */
- char_u *tag_fname; /* file name of the tags file */
- char_u *tagkind; /* "kind:" value */
- char_u *tagkind_end; /* end of tagkind */
+ // filled in by parse_tag_line():
+ char_u *tagname; // start of tag name (skip "file:")
+ char_u *tagname_end; // char after tag name
+ char_u *fname; // first char of file name
+ char_u *fname_end; // char after file name
+ char_u *command; // first char of command
+ // filled in by parse_match():
+ char_u *command_end; // first char after command
+ char_u *tag_fname; // file name of the tags file. This is used
+ // when 'tr' is set.
+ char_u *tagkind; // "kind:" value
+ char_u *tagkind_end; // end of tagkind
+ char_u *user_data; // user_data string
+ char_u *user_data_end; // end of user_data
+ linenr_T tagline; // "line:" value
} tagptrs_T;
/*
@@ -102,6 +106,10 @@ static char_u *nofile_fname = NULL; /* fname for NOTAGFILE error */
static char_u *bottommsg = (char_u *)N_("E555: at bottom of tag stack");
static char_u *topmsg = (char_u *)N_("E556: at top of tag stack");
+static char_u *recurmsg
+ = (char_u *)N_("E986: cannot modify the tag stack within tagfunc");
+static char_u *tfu_inv_ret_msg
+ = (char_u *)N_("E987: invalid return value from tagfunc");
static char_u *tagmatchname = NULL; /* name of last used tag */
@@ -109,7 +117,12 @@ static char_u *tagmatchname = NULL; /* name of last used tag */
* Tag for preview window is remembered separately, to avoid messing up the
* normal tagstack.
*/
-static taggy_T ptag_entry = { NULL, { { 0, 0, 0 }, 0, 0, NULL }, 0, 0 };
+static taggy_T ptag_entry = { NULL, { { 0, 0, 0 }, 0, 0, NULL }, 0, 0, NULL };
+
+static int tfu_in_use = false; // disallow recursive call of tagfunc
+
+// Used instead of NUL to separate tag fields in the growarrays.
+#define TAG_SEP 0x02
/*
* Jump to tag; handling of tag commands and tag stack
@@ -131,13 +144,13 @@ static taggy_T ptag_entry = { NULL, { { 0, 0, 0 }, 0, 0, NULL }, 0, 0 };
*
* for cscope, returns TRUE if we jumped to tag or aborted, FALSE otherwise
*/
-int
-do_tag (
- char_u *tag, /* tag (pattern) to jump to */
+int
+do_tag(
+ char_u *tag, // tag (pattern) to jump to
int type,
int count,
- int forceit, /* :ta with ! */
- int verbose /* print "tag not found" message */
+ int forceit, // :ta with !
+ int verbose // print "tag not found" message
)
{
taggy_T *tagstack = curwin->w_tagstack;
@@ -148,28 +161,20 @@ do_tag (
int oldtagstackidx = tagstackidx;
int prevtagstackidx = tagstackidx;
int prev_num_matches;
- int new_tag = FALSE;
- int other_name;
- int i, j, k;
- int idx;
+ int new_tag = false;
+ int i;
int ic;
- char_u *p;
- char_u *name;
- int no_regexp = FALSE;
+ int no_regexp = false;
int error_cur_match = 0;
- char_u *command_end;
- int save_pos = FALSE;
+ int save_pos = false;
fmark_T saved_fmark;
- int taglen;
- int jumped_to_tag = FALSE;
- tagptrs_T tagp, tagp2;
+ int jumped_to_tag = false;
int new_num_matches;
char_u **new_matches;
- int attr;
int use_tagstack;
- int skip_msg = FALSE;
- char_u *buf_ffname = curbuf->b_ffname; /* name to use for
- priority computation */
+ int skip_msg = false;
+ char_u *buf_ffname = curbuf->b_ffname; // name for priority computation
+ int use_tfu = 1;
/* remember the matches for the last used tag */
static int num_matches = 0;
@@ -177,19 +182,25 @@ do_tag (
static char_u **matches = NULL;
static int flags;
+ if (tfu_in_use) {
+ EMSG(_(recurmsg));
+ return false;
+ }
+
#ifdef EXITFREE
if (type == DT_FREE) {
/* remove the list of matches */
FreeWild(num_matches, matches);
cs_free_tags();
num_matches = 0;
- return FALSE;
+ return false;
}
#endif
if (type == DT_HELP) {
type = DT_TAG;
- no_regexp = TRUE;
+ no_regexp = true;
+ use_tfu = 0;
}
prev_num_matches = num_matches;
@@ -205,14 +216,15 @@ do_tag (
use_tagstack = false;
new_tag = true;
if (g_do_tagpreview != 0) {
- xfree(ptag_entry.tagname);
+ tagstack_clear_entry(&ptag_entry);
ptag_entry.tagname = vim_strsave(tag);
}
} else {
- if (g_do_tagpreview != 0)
- use_tagstack = FALSE;
- else
- use_tagstack = TRUE;
+ if (g_do_tagpreview != 0) {
+ use_tagstack = false;
+ } else {
+ use_tagstack = true;
+ }
/* new pattern, add to the tag stack */
if (*tag != NUL
@@ -228,7 +240,7 @@ do_tag (
cur_match = ptag_entry.cur_match;
cur_fnum = ptag_entry.cur_fnum;
} else {
- xfree(ptag_entry.tagname);
+ tagstack_clear_entry(&ptag_entry);
ptag_entry.tagname = vim_strsave(tag);
}
} else {
@@ -236,16 +248,18 @@ do_tag (
* If the last used entry is not at the top, delete all tag
* stack entries above it.
*/
- while (tagstackidx < tagstacklen)
- xfree(tagstack[--tagstacklen].tagname);
+ while (tagstackidx < tagstacklen) {
+ tagstack_clear_entry(&tagstack[--tagstacklen]);
+ }
/* if the tagstack is full: remove oldest entry */
if (++tagstacklen > TAGSTACKSIZE) {
tagstacklen = TAGSTACKSIZE;
- xfree(tagstack[0].tagname);
- for (i = 1; i < tagstacklen; ++i)
+ tagstack_clear_entry(&tagstack[0]);
+ for (i = 1; i < tagstacklen; i++) {
tagstack[i - 1] = tagstack[i];
- --tagstackidx;
+ }
+ tagstackidx--;
}
// put the tag name in the tag stack
@@ -253,15 +267,15 @@ do_tag (
curwin->w_tagstacklen = tagstacklen;
- save_pos = TRUE; /* save the cursor position below */
+ save_pos = true; // save the cursor position below
}
- new_tag = TRUE;
+ new_tag = true;
} else {
if (
- g_do_tagpreview != 0 ? ptag_entry.tagname == NULL :
- tagstacklen == 0) {
- /* empty stack */
+ g_do_tagpreview != 0 ? ptag_entry.tagname == NULL :
+ tagstacklen == 0) {
+ // empty stack
EMSG(_(e_tagstack));
goto end_do_tag;
}
@@ -271,7 +285,7 @@ do_tag (
if ((tagstackidx -= count) < 0) {
EMSG(_(bottommsg));
if (tagstackidx + count == 0) {
- /* We did [num]^T from the bottom of the stack */
+ // We did [num]^T from the bottom of the stack
tagstackidx = 0;
goto end_do_tag;
}
@@ -279,7 +293,7 @@ do_tag (
* way to the bottom now.
*/
tagstackidx = 0;
- } else if (tagstackidx >= tagstacklen) { /* count == 0? */
+ } else if (tagstackidx >= tagstacklen) { // count == 0?
EMSG(_(topmsg));
goto end_do_tag;
}
@@ -293,8 +307,8 @@ do_tag (
* file was changed) keep original position in tag stack.
*/
if (buflist_getfile(saved_fmark.fnum, saved_fmark.mark.lnum,
- GETF_SETMARK, forceit) == FAIL) {
- tagstackidx = oldtagstackidx; /* back to old posn */
+ GETF_SETMARK, forceit) == FAIL) {
+ tagstackidx = oldtagstackidx; // back to old posn
goto end_do_tag;
}
/* A BufReadPost autocommand may jump to the '" mark, but
@@ -305,12 +319,12 @@ do_tag (
curwin->w_cursor.lnum = saved_fmark.mark.lnum;
}
curwin->w_cursor.col = saved_fmark.mark.col;
- curwin->w_set_curswant = TRUE;
+ curwin->w_set_curswant = true;
check_cursor();
if ((fdo_flags & FDO_TAG) && old_KeyTyped)
foldOpenCursor();
- /* remove the old list of matches */
+ // remove the old list of matches
FreeWild(num_matches, matches);
cs_free_tags();
num_matches = 0;
@@ -325,8 +339,8 @@ do_tag (
cur_match = ptag_entry.cur_match;
cur_fnum = ptag_entry.cur_fnum;
} else {
- /* ":tag" (no argument): go to newer pattern */
- save_pos = TRUE; /* save the cursor position below */
+ // ":tag" (no argument): go to newer pattern
+ save_pos = true; // save the cursor position below
if ((tagstackidx += count - 1) >= tagstacklen) {
/*
* Beyond the last one, just give an error message and
@@ -335,8 +349,8 @@ do_tag (
*/
tagstackidx = tagstacklen - 1;
EMSG(_(topmsg));
- save_pos = FALSE;
- } else if (tagstackidx < 0) { /* must have been count == 0 */
+ save_pos = false;
+ } else if (tagstackidx < 0) { // must have been count == 0
EMSG(_(bottommsg));
tagstackidx = 0;
goto end_do_tag;
@@ -344,9 +358,9 @@ do_tag (
cur_match = tagstack[tagstackidx].cur_match;
cur_fnum = tagstack[tagstackidx].cur_fnum;
}
- new_tag = TRUE;
- } else { /* go to other matching tag */
- /* Save index for when selection is cancelled. */
+ new_tag = true;
+ } else { // go to other matching tag
+ // Save index for when selection is cancelled.
prevtagstackidx = tagstackidx;
if (g_do_tagpreview != 0) {
@@ -371,7 +385,7 @@ do_tag (
cur_match = MAXCOL - 1;
else if (cur_match < 0) {
EMSG(_("E425: Cannot go before first matching tag"));
- skip_msg = TRUE;
+ skip_msg = true;
cur_match = 0;
cur_fnum = curbuf->b_fnum;
}
@@ -418,15 +432,17 @@ do_tag (
* Repeat searching for tags, when a file has not been found.
*/
for (;; ) {
- /*
- * When desired match not found yet, try to find it (and others).
- */
- if (use_tagstack)
+ int other_name;
+ char_u *name;
+
+ // When desired match not found yet, try to find it (and others).
+ if (use_tagstack) {
name = tagstack[tagstackidx].tagname;
- else if (g_do_tagpreview != 0)
+ } else if (g_do_tagpreview != 0) {
name = ptag_entry.tagname;
- else
+ } else {
name = tag;
+ }
other_name = (tagmatchname == NULL || STRCMP(tagmatchname, name) != 0);
if (new_tag
|| (cur_match >= num_matches && max_num_matches != MAXCOL)
@@ -446,38 +462,49 @@ do_tag (
max_num_matches = cur_match + 1;
}
- /* when the argument starts with '/', use it as a regexp */
+ // when the argument starts with '/', use it as a regexp
if (!no_regexp && *name == '/') {
flags = TAG_REGEXP;
++name;
} else
flags = TAG_NOIC;
- if (type == DT_CSCOPE)
+ if (type == DT_CSCOPE) {
flags = TAG_CSCOPE;
- if (verbose)
+ }
+ if (verbose) {
flags |= TAG_VERBOSE;
+ }
+ if (!use_tfu) {
+ flags |= TAG_NO_TAGFUNC;
+ }
+
if (find_tags(name, &new_num_matches, &new_matches, flags,
- max_num_matches, buf_ffname) == OK
- && new_num_matches < max_num_matches)
- max_num_matches = MAXCOL; /* If less than max_num_matches
- found: all matches found. */
+ max_num_matches, buf_ffname) == OK
+ && new_num_matches < max_num_matches) {
+ max_num_matches = MAXCOL; // If less than max_num_matches
+ // found: all matches found.
+ }
/* If there already were some matches for the same name, move them
* to the start. Avoids that the order changes when using
* ":tnext" and jumping to another file. */
if (!new_tag && !other_name) {
- /* Find the position of each old match in the new list. Need
- * to use parse_match() to find the tag line. */
- idx = 0;
- for (j = 0; j < num_matches; ++j) {
+ int j, k;
+ int idx = 0;
+ tagptrs_T tagp, tagp2;
+
+ // Find the position of each old match in the new list. Need
+ // to use parse_match() to find the tag line.
+ for (j = 0; j < num_matches; j++) {
parse_match(matches[j], &tagp);
for (i = idx; i < new_num_matches; ++i) {
parse_match(new_matches[i], &tagp2);
if (STRCMP(tagp.tagname, tagp2.tagname) == 0) {
- p = new_matches[i];
- for (k = i; k > idx; --k)
+ char_u *p = new_matches[i];
+ for (k = i; k > idx; k--) {
new_matches[k] = new_matches[k - 1];
+ }
new_matches[idx++] = p;
break;
}
@@ -504,304 +531,27 @@ do_tag (
// jump to count'th matching tag.
cur_match = count > 0 ? count - 1 : 0;
} else if (type == DT_SELECT || (type == DT_JUMP && num_matches > 1)) {
- // List all the matching tags.
- // Assume that the first match indicates how long the tags can
- // be, and align the file names to that.
- parse_match(matches[0], &tagp);
- taglen = (int)(tagp.tagname_end - tagp.tagname + 2);
- if (taglen < 18)
- taglen = 18;
- if (taglen > Columns - 25)
- taglen = MAXCOL;
- if (msg_col == 0)
- msg_didout = FALSE; /* overwrite previous message */
- msg_start();
- MSG_PUTS_ATTR(_(" # pri kind tag"), HL_ATTR(HLF_T));
- msg_clr_eos();
- taglen_advance(taglen);
- MSG_PUTS_ATTR(_("file\n"), HL_ATTR(HLF_T));
-
- for (i = 0; i < num_matches && !got_int; i++) {
- parse_match(matches[i], &tagp);
- if (!new_tag && ((g_do_tagpreview != 0 && i == ptag_entry.cur_match)
- || (use_tagstack
- && i == tagstack[tagstackidx].cur_match))) {
- *IObuff = '>';
- } else {
- *IObuff = ' ';
- }
- vim_snprintf((char *)IObuff + 1, IOSIZE - 1, "%2d %s ", i + 1,
- mt_names[matches[i][0] & MT_MASK]);
- msg_puts((const char *)IObuff);
- if (tagp.tagkind != NULL) {
- msg_outtrans_len(tagp.tagkind,
- (int)(tagp.tagkind_end - tagp.tagkind));
- }
- msg_advance(13);
- msg_outtrans_len_attr(tagp.tagname,
- (int)(tagp.tagname_end - tagp.tagname),
- HL_ATTR(HLF_T));
- msg_putchar(' ');
- taglen_advance(taglen);
-
- /* Find out the actual file name. If it is long, truncate
- * it and put "..." in the middle */
- p = tag_full_fname(&tagp);
- msg_puts_long_attr(p, HL_ATTR(HLF_D));
- xfree(p);
-
- if (msg_col > 0)
- msg_putchar('\n');
- if (got_int)
- break;
- msg_advance(15);
-
- /* print any extra fields */
- command_end = tagp.command_end;
- if (command_end != NULL) {
- p = command_end + 3;
- while (*p && *p != '\r' && *p != '\n') {
- while (*p == TAB)
- ++p;
-
- /* skip "file:" without a value (static tag) */
- if (STRNCMP(p, "file:", 5) == 0
- && ascii_isspace(p[5])) {
- p += 5;
- continue;
- }
- /* skip "kind:<kind>" and "<kind>" */
- if (p == tagp.tagkind
- || (p + 5 == tagp.tagkind
- && STRNCMP(p, "kind:", 5) == 0)) {
- p = tagp.tagkind_end;
- continue;
- }
- // print all other extra fields
- attr = HL_ATTR(HLF_CM);
- while (*p && *p != '\r' && *p != '\n') {
- if (msg_col + ptr2cells(p) >= Columns) {
- msg_putchar('\n');
- if (got_int)
- break;
- msg_advance(15);
- }
- p = msg_outtrans_one(p, attr);
- if (*p == TAB) {
- msg_puts_attr(" ", attr);
- break;
- }
- if (*p == ':')
- attr = 0;
- }
- }
- if (msg_col > 15) {
- msg_putchar('\n');
- if (got_int)
- break;
- msg_advance(15);
- }
- } else {
- for (p = tagp.command;
- *p && *p != '\r' && *p != '\n'; ++p)
- ;
- command_end = p;
- }
-
- /*
- * Put the info (in several lines) at column 15.
- * Don't display "/^" and "?^".
- */
- p = tagp.command;
- if (*p == '/' || *p == '?') {
- ++p;
- if (*p == '^')
- ++p;
- }
- /* Remove leading whitespace from pattern */
- while (p != command_end && ascii_isspace(*p))
- ++p;
-
- while (p != command_end) {
- if (msg_col + (*p == TAB ? 1 : ptr2cells(p)) > Columns)
- msg_putchar('\n');
- if (got_int)
- break;
- msg_advance(15);
-
- // Skip backslash used for escaping a command char or a backslash.
- if (*p == '\\' && (*(p + 1) == *tagp.command
- || *(p + 1) == '\\')) {
- ++p;
- }
-
- if (*p == TAB) {
- msg_putchar(' ');
- ++p;
- } else
- p = msg_outtrans_one(p, 0);
-
- /* don't display the "$/;\"" and "$?;\"" */
- if (p == command_end - 2 && *p == '$'
- && *(p + 1) == *tagp.command)
- break;
- /* don't display matching '/' or '?' */
- if (p == command_end - 1 && *p == *tagp.command
- && (*p == '/' || *p == '?'))
- break;
- }
- if (msg_col)
- msg_putchar('\n');
- os_breakcheck();
- }
- if (got_int) {
- got_int = false; // only stop the listing
- }
+ print_tag_list(new_tag, use_tagstack, num_matches, matches);
ask_for_selection = true;
} else if (type == DT_LTAG) {
- list_T *list;
- char_u tag_name[128 + 1];
- char_u *fname;
- char_u *cmd;
-
- /*
- * Add the matching tags to the location list for the current
- * window.
- */
-
- fname = xmalloc(MAXPATHL + 1);
- cmd = xmalloc(CMDBUFFSIZE + 1);
- list = tv_list_alloc(num_matches);
-
- for (i = 0; i < num_matches; ++i) {
- int len, cmd_len;
- long lnum;
- dict_T *dict;
-
- parse_match(matches[i], &tagp);
-
- /* Save the tag name */
- len = (int)(tagp.tagname_end - tagp.tagname);
- if (len > 128)
- len = 128;
- STRLCPY(tag_name, tagp.tagname, len + 1);
-
- /* Save the tag file name */
- p = tag_full_fname(&tagp);
- STRLCPY(fname, p, MAXPATHL + 1);
- xfree(p);
-
- /*
- * Get the line number or the search pattern used to locate
- * the tag.
- */
- lnum = 0;
- if (isdigit(*tagp.command))
- /* Line number is used to locate the tag */
- lnum = atol((char *)tagp.command);
- else {
- char_u *cmd_start, *cmd_end;
-
- /* Search pattern is used to locate the tag */
-
- /* Locate the end of the command */
- cmd_start = tagp.command;
- cmd_end = tagp.command_end;
- if (cmd_end == NULL) {
- for (p = tagp.command;
- *p && *p != '\r' && *p != '\n'; ++p)
- ;
- cmd_end = p;
- }
-
- /*
- * Now, cmd_end points to the character after the
- * command. Adjust it to point to the last
- * character of the command.
- */
- cmd_end--;
-
- /*
- * Skip the '/' and '?' characters at the
- * beginning and end of the search pattern.
- */
- if (*cmd_start == '/' || *cmd_start == '?')
- cmd_start++;
-
- if (*cmd_end == '/' || *cmd_end == '?')
- cmd_end--;
-
- len = 0;
- cmd[0] = NUL;
-
- /*
- * If "^" is present in the tag search pattern, then
- * copy it first.
- */
- if (*cmd_start == '^') {
- STRCPY(cmd, "^");
- cmd_start++;
- len++;
- }
-
- /*
- * Precede the tag pattern with \V to make it very
- * nomagic.
- */
- STRCAT(cmd, "\\V");
- len += 2;
-
- cmd_len = (int)(cmd_end - cmd_start + 1);
- if (cmd_len > (CMDBUFFSIZE - 5))
- cmd_len = CMDBUFFSIZE - 5;
- STRNCAT(cmd, cmd_start, cmd_len);
- len += cmd_len;
-
- if (cmd[len - 1] == '$') {
- /*
- * Replace '$' at the end of the search pattern
- * with '\$'
- */
- cmd[len - 1] = '\\';
- cmd[len] = '$';
- len++;
- }
-
- cmd[len] = NUL;
- }
-
- dict = tv_dict_alloc();
- tv_list_append_dict(list, dict);
-
- tv_dict_add_str(dict, S_LEN("text"), (const char *)tag_name);
- tv_dict_add_str(dict, S_LEN("filename"), (const char *)fname);
- tv_dict_add_nr(dict, S_LEN("lnum"), lnum);
- if (lnum == 0) {
- tv_dict_add_str(dict, S_LEN("pattern"), (const char *)cmd);
- }
+ if (add_llist_tags(tag, num_matches, matches) == FAIL) {
+ goto end_do_tag;
}
- vim_snprintf((char *)IObuff, IOSIZE, "ltag %s", tag);
- set_errorlist(curwin, list, ' ', IObuff, NULL);
-
- tv_list_free(list);
- xfree(fname);
- xfree(cmd);
-
- cur_match = 0; /* Jump to the first tag */
+ cur_match = 0; // Jump to the first tag
}
if (ask_for_selection) {
// Ask to select a tag from the list.
i = prompt_for_number(NULL);
if (i <= 0 || i > num_matches || got_int) {
- /* no valid choice: don't change anything */
+ // no valid choice: don't change anything
if (use_tagstack) {
tagstack[tagstackidx].fmark = saved_fmark;
tagstackidx = prevtagstackidx;
}
cs_free_tags();
- jumped_to_tag = TRUE;
+ jumped_to_tag = true;
break;
}
cur_match = i - 1;
@@ -817,14 +567,25 @@ do_tag (
EMSG(_("E427: There is only one matching tag"));
else
EMSG(_("E428: Cannot go beyond last matching tag"));
- skip_msg = TRUE;
+ skip_msg = true;
}
cur_match = num_matches - 1;
}
if (use_tagstack) {
+ tagptrs_T tagp2;
+
tagstack[tagstackidx].cur_match = cur_match;
tagstack[tagstackidx].cur_fnum = cur_fnum;
- ++tagstackidx;
+
+ // store user-provided data originating from tagfunc
+ if (use_tfu && parse_match(matches[cur_match], &tagp2) == OK
+ && tagp2.user_data) {
+ XFREE_CLEAR(tagstack[tagstackidx].user_data);
+ tagstack[tagstackidx].user_data = vim_strnsave(
+ tagp2.user_data, tagp2.user_data_end - tagp2.user_data);
+ }
+
+ tagstackidx++;
} else if (g_do_tagpreview != 0) {
ptag_entry.cur_match = cur_match;
ptag_entry.cur_fnum = cur_fnum;
@@ -843,13 +604,14 @@ do_tag (
&& type != DT_CSCOPE
&& (num_matches > 1 || ic)
&& !skip_msg) {
- /* Give an indication of the number of matching tags */
- sprintf((char *)IObuff, _("tag %d of %d%s"),
- cur_match + 1,
- num_matches,
- max_num_matches != MAXCOL ? _(" or more") : "");
- if (ic)
+ // Give an indication of the number of matching tags
+ snprintf((char *)IObuff, sizeof(IObuff), _("tag %d of %d%s"),
+ cur_match + 1,
+ num_matches,
+ max_num_matches != MAXCOL ? _(" or more") : "");
+ if (ic) {
STRCAT(IObuff, _(" Using tag with different case!"));
+ }
if ((num_matches > prev_num_matches || new_tag)
&& num_matches > 1) {
if (ic) {
@@ -867,7 +629,7 @@ do_tag (
}
}
- /* Let the SwapExists event know what tag we are jumping to. */
+ // Let the SwapExists event know what tag we are jumping to.
vim_snprintf((char *)IObuff, IOSIZE, ":ta %s\r", name);
set_vim_var_string(VV_SWAPCOMMAND, (char *) IObuff, -1);
@@ -879,7 +641,7 @@ do_tag (
set_vim_var_string(VV_SWAPCOMMAND, NULL, -1);
if (i == NOTAGFILE) {
- /* File not found: try again with another matching tag */
+ // File not found: try again with another matching tag
if ((type == DT_PREV && cur_match > 0)
|| ((type == DT_TAG || type == DT_NEXT
|| type == DT_FIRST)
@@ -902,22 +664,352 @@ do_tag (
* tagstackidx is still valid. */
if (use_tagstack && tagstackidx > curwin->w_tagstacklen)
tagstackidx = curwin->w_tagstackidx;
- jumped_to_tag = TRUE;
+ jumped_to_tag = true;
}
}
break;
}
end_do_tag:
- /* Only store the new index when using the tagstack and it's valid. */
- if (use_tagstack && tagstackidx <= curwin->w_tagstacklen)
+ // Only store the new index when using the tagstack and it's valid.
+ if (use_tagstack && tagstackidx <= curwin->w_tagstacklen) {
curwin->w_tagstackidx = tagstackidx;
+ }
postponed_split = 0; // don't split next time
g_do_tagpreview = 0; // don't do tag preview next time
return jumped_to_tag;
}
+//
+// List all the matching tags.
+//
+static void
+print_tag_list(
+ int new_tag,
+ int use_tagstack,
+ int num_matches,
+ char_u **matches)
+{
+ taggy_T *tagstack = curwin->w_tagstack;
+ int tagstackidx = curwin->w_tagstackidx;
+ int i;
+ char_u *p;
+ char_u *command_end;
+ tagptrs_T tagp;
+ int taglen;
+ int attr;
+
+ // Assume that the first match indicates how long the tags can
+ // be, and align the file names to that.
+ parse_match(matches[0], &tagp);
+ taglen = (int)(tagp.tagname_end - tagp.tagname + 2);
+ if (taglen < 18) {
+ taglen = 18;
+ }
+ if (taglen > Columns - 25) {
+ taglen = MAXCOL;
+ }
+ if (msg_col == 0) {
+ msg_didout = false; // overwrite previous message
+ }
+ msg_start();
+ msg_puts_attr(_(" # pri kind tag"), HL_ATTR(HLF_T));
+ msg_clr_eos();
+ taglen_advance(taglen);
+ msg_puts_attr(_("file\n"), HL_ATTR(HLF_T));
+
+ for (i = 0; i < num_matches && !got_int; i++) {
+ parse_match(matches[i], &tagp);
+ if (!new_tag && (
+ (g_do_tagpreview != 0
+ && i == ptag_entry.cur_match)
+ || (use_tagstack
+ && i == tagstack[tagstackidx].cur_match))) {
+ *IObuff = '>';
+ } else {
+ *IObuff = ' ';
+ }
+ vim_snprintf((char *)IObuff + 1, IOSIZE - 1,
+ "%2d %s ", i + 1,
+ mt_names[matches[i][0] & MT_MASK]);
+ msg_puts((char *)IObuff);
+ if (tagp.tagkind != NULL) {
+ msg_outtrans_len(tagp.tagkind,
+ (int)(tagp.tagkind_end - tagp.tagkind));
+ }
+ msg_advance(13);
+ msg_outtrans_len_attr(tagp.tagname,
+ (int)(tagp.tagname_end - tagp.tagname),
+ HL_ATTR(HLF_T));
+ msg_putchar(' ');
+ taglen_advance(taglen);
+
+ // Find out the actual file name. If it is long, truncate
+ // it and put "..." in the middle
+ p = tag_full_fname(&tagp);
+ if (p != NULL) {
+ msg_outtrans_attr(p, HL_ATTR(HLF_D));
+ XFREE_CLEAR(p);
+ }
+ if (msg_col > 0) {
+ msg_putchar('\n');
+ }
+ if (got_int) {
+ break;
+ }
+ msg_advance(15);
+
+ // print any extra fields
+ command_end = tagp.command_end;
+ if (command_end != NULL) {
+ p = command_end + 3;
+ while (*p && *p != '\r' && *p != '\n') {
+ while (*p == TAB) {
+ p++;
+ }
+
+ // skip "file:" without a value (static tag)
+ if (STRNCMP(p, "file:", 5) == 0 && ascii_isspace(p[5])) {
+ p += 5;
+ continue;
+ }
+ // skip "kind:<kind>" and "<kind>"
+ if (p == tagp.tagkind
+ || (p + 5 == tagp.tagkind
+ && STRNCMP(p, "kind:", 5) == 0)) {
+ p = tagp.tagkind_end;
+ continue;
+ }
+ // print all other extra fields
+ attr = HL_ATTR(HLF_CM);
+ while (*p && *p != '\r' && *p != '\n') {
+ if (msg_col + ptr2cells(p) >= Columns) {
+ msg_putchar('\n');
+ if (got_int) {
+ break;
+ }
+ msg_advance(15);
+ }
+ p = msg_outtrans_one(p, attr);
+ if (*p == TAB) {
+ msg_puts_attr(" ", attr);
+ break;
+ }
+ if (*p == ':') {
+ attr = 0;
+ }
+ }
+ }
+ if (msg_col > 15) {
+ msg_putchar('\n');
+ if (got_int) {
+ break;
+ }
+ msg_advance(15);
+ }
+ } else {
+ for (p = tagp.command;
+ *p && *p != '\r' && *p != '\n';
+ p++) {
+ }
+ command_end = p;
+ }
+
+ // Put the info (in several lines) at column 15.
+ // Don't display "/^" and "?^".
+ p = tagp.command;
+ if (*p == '/' || *p == '?') {
+ p++;
+ if (*p == '^') {
+ p++;
+ }
+ }
+ // Remove leading whitespace from pattern
+ while (p != command_end && ascii_isspace(*p)) {
+ p++;
+ }
+
+ while (p != command_end) {
+ if (msg_col + (*p == TAB ? 1 : ptr2cells(p)) > Columns) {
+ msg_putchar('\n');
+ }
+ if (got_int) {
+ break;
+ }
+ msg_advance(15);
+
+ // skip backslash used for escaping a command char or
+ // a backslash
+ if (*p == '\\' && (*(p + 1) == *tagp.command
+ || *(p + 1) == '\\')) {
+ p++;
+ }
+
+ if (*p == TAB) {
+ msg_putchar(' ');
+ p++;
+ } else {
+ p = msg_outtrans_one(p, 0);
+ }
+
+ // don't display the "$/;\"" and "$?;\""
+ if (p == command_end - 2 && *p == '$'
+ && *(p + 1) == *tagp.command) {
+ break;
+ }
+ // don't display matching '/' or '?'
+ if (p == command_end - 1 && *p == *tagp.command
+ && (*p == '/' || *p == '?')) {
+ break;
+ }
+ }
+ if (msg_col) {
+ msg_putchar('\n');
+ }
+ os_breakcheck();
+ }
+ if (got_int) {
+ got_int = false; // only stop the listing
+ }
+}
+
+//
+// Add the matching tags to the location list for the current
+// window.
+//
+static int
+add_llist_tags(
+ char_u *tag,
+ int num_matches,
+ char_u **matches)
+{
+ list_T *list;
+ char_u tag_name[128 + 1];
+ char_u *fname;
+ char_u *cmd;
+ int i;
+ char_u *p;
+ tagptrs_T tagp;
+
+ fname = xmalloc(MAXPATHL + 1);
+ cmd = xmalloc(CMDBUFFSIZE + 1);
+ list = tv_list_alloc(0);
+
+ for (i = 0; i < num_matches; i++) {
+ int len, cmd_len;
+ long lnum;
+ dict_T *dict;
+
+ parse_match(matches[i], &tagp);
+
+ // Save the tag name
+ len = (int)(tagp.tagname_end - tagp.tagname);
+ if (len > 128) {
+ len = 128;
+ }
+ xstrlcpy((char *)tag_name, (const char *)tagp.tagname, len);
+ tag_name[len] = NUL;
+
+ // Save the tag file name
+ p = tag_full_fname(&tagp);
+ if (p == NULL) {
+ continue;
+ }
+ xstrlcpy((char *)fname, (const char *)p, MAXPATHL);
+ XFREE_CLEAR(p);
+
+ // Get the line number or the search pattern used to locate
+ // the tag.
+ lnum = 0;
+ if (isdigit(*tagp.command)) {
+ // Line number is used to locate the tag
+ lnum = atol((char *)tagp.command);
+ } else {
+ char_u *cmd_start, *cmd_end;
+
+ // Search pattern is used to locate the tag
+
+ // Locate the end of the command
+ cmd_start = tagp.command;
+ cmd_end = tagp.command_end;
+ if (cmd_end == NULL) {
+ for (p = tagp.command;
+ *p && *p != '\r' && *p != '\n'; p++) {
+ }
+ cmd_end = p;
+ }
+
+ // Now, cmd_end points to the character after the
+ // command. Adjust it to point to the last
+ // character of the command.
+ cmd_end--;
+
+ // Skip the '/' and '?' characters at the
+ // beginning and end of the search pattern.
+ if (*cmd_start == '/' || *cmd_start == '?') {
+ cmd_start++;
+ }
+
+ if (*cmd_end == '/' || *cmd_end == '?') {
+ cmd_end--;
+ }
+
+ len = 0;
+ cmd[0] = NUL;
+
+ // If "^" is present in the tag search pattern, then
+ // copy it first.
+ if (*cmd_start == '^') {
+ STRCPY(cmd, "^");
+ cmd_start++;
+ len++;
+ }
+
+ // Precede the tag pattern with \V to make it very
+ // nomagic.
+ STRCAT(cmd, "\\V");
+ len += 2;
+
+ cmd_len = (int)(cmd_end - cmd_start + 1);
+ if (cmd_len > (CMDBUFFSIZE - 5)) {
+ cmd_len = CMDBUFFSIZE - 5;
+ }
+ xstrlcat((char *)cmd, (char *)cmd_start, cmd_len);
+ len += cmd_len;
+
+ if (cmd[len - 1] == '$') {
+ // Replace '$' at the end of the search pattern
+ // with '\$'
+ cmd[len - 1] = '\\';
+ cmd[len] = '$';
+ len++;
+ }
+
+ cmd[len] = NUL;
+ }
+
+ dict = tv_dict_alloc();
+ tv_list_append_dict(list, dict);
+
+ tv_dict_add_str(dict, S_LEN("text"), (const char *)tag_name);
+ tv_dict_add_str(dict, S_LEN("filename"), (const char *)fname);
+ tv_dict_add_nr(dict, S_LEN("lnum"), lnum);
+ if (lnum == 0) {
+ tv_dict_add_str(dict, S_LEN("pattern"), (const char *)cmd);
+ }
+ }
+
+ vim_snprintf((char *)IObuff, IOSIZE, "ltag %s", tag);
+ set_errorlist(curwin, list, ' ', IObuff, NULL);
+
+ tv_list_free(list);
+ XFREE_CLEAR(fname);
+ XFREE_CLEAR(cmd);
+
+ return OK;
+}
+
/*
* Free cached tags.
*/
@@ -1029,6 +1121,210 @@ static void prepare_pats(pat_T *pats, int has_re)
pats->regmatch.regprog = NULL;
}
+//
+// Call the user-defined function to generate a list of tags used by
+// find_tags().
+//
+// Return OK if at least 1 tag has been successfully found,
+// NOTDONE if the function returns v:null, and FAIL otherwise.
+//
+static int find_tagfunc_tags(
+ char_u *pat, // pattern supplied to the user-defined function
+ garray_T *ga, // the tags will be placed here
+ int *match_count, // here the number of tags found will be placed
+ int flags, // flags from find_tags (TAG_*)
+ char_u *buf_ffname) // name of buffer for priority
+{
+ pos_T save_pos;
+ list_T *taglist;
+ int ntags = 0;
+ int result = FAIL;
+ typval_T args[4];
+ typval_T rettv;
+ char_u flagString[3];
+ dict_T *d;
+ taggy_T *tag = &curwin->w_tagstack[curwin->w_tagstackidx];
+
+ if (*curbuf->b_p_tfu == NUL) {
+ return FAIL;
+ }
+
+ args[0].v_type = VAR_STRING;
+ args[0].vval.v_string = pat;
+ args[1].v_type = VAR_STRING;
+ args[1].vval.v_string = flagString;
+
+ // create 'info' dict argument
+ d = tv_dict_alloc();
+ if (tag->user_data != NULL) {
+ tv_dict_add_str(d, S_LEN("user_data"), (const char *)tag->user_data);
+ }
+ if (buf_ffname != NULL) {
+ tv_dict_add_str(d, S_LEN("buf_ffname"), (const char *)buf_ffname);
+ }
+
+ d->dv_refcount++;
+ args[2].v_type = VAR_DICT;
+ args[2].vval.v_dict = d;
+
+ args[3].v_type = VAR_UNKNOWN;
+
+ vim_snprintf((char *)flagString, sizeof(flagString),
+ "%s%s",
+ g_tag_at_cursor ? "c": "",
+ flags & TAG_INS_COMP ? "i": "");
+
+ save_pos = curwin->w_cursor;
+ result = call_vim_function(curbuf->b_p_tfu, 3, args, &rettv);
+ curwin->w_cursor = save_pos; // restore the cursor position
+ d->dv_refcount--;
+
+ if (result == FAIL) {
+ return FAIL;
+ }
+ if (rettv.v_type == VAR_SPECIAL && rettv.vval.v_number == VV_NULL) {
+ tv_clear(&rettv);
+ return NOTDONE;
+ }
+ if (rettv.v_type != VAR_LIST || !rettv.vval.v_list) {
+ tv_clear(&rettv);
+ EMSG(_(tfu_inv_ret_msg));
+ return FAIL;
+ }
+ taglist = rettv.vval.v_list;
+
+ TV_LIST_ITER_CONST(taglist, li, {
+ char_u *res_name;
+ char_u *res_fname;
+ char_u *res_cmd;
+ char_u *res_kind;
+ int has_extra = 0;
+ int name_only = flags & TAG_NAMES;
+
+ if (TV_LIST_ITEM_TV(li)->v_type != VAR_DICT) {
+ EMSG(_(tfu_inv_ret_msg));
+ break;
+ }
+
+ size_t len = 2;
+ res_name = NULL;
+ res_fname = NULL;
+ res_cmd = NULL;
+ res_kind = NULL;
+
+ TV_DICT_ITER(TV_LIST_ITEM_TV(li)->vval.v_dict, di, {
+ const char_u *dict_key = di->di_key;
+ typval_T *tv = &di->di_tv;
+
+ if (tv->v_type != VAR_STRING || tv->vval.v_string == NULL) {
+ continue;
+ }
+
+ len += STRLEN(tv->vval.v_string) + 1; // Space for "\tVALUE"
+ if (!STRCMP(dict_key, "name")) {
+ res_name = tv->vval.v_string;
+ continue;
+ }
+ if (!STRCMP(dict_key, "filename")) {
+ res_fname = tv->vval.v_string;
+ continue;
+ }
+ if (!STRCMP(dict_key, "cmd")) {
+ res_cmd = tv->vval.v_string;
+ continue;
+ }
+ has_extra = 1;
+ if (!STRCMP(dict_key, "kind")) {
+ res_kind = tv->vval.v_string;
+ continue;
+ }
+ // Other elements will be stored as "\tKEY:VALUE"
+ // Allocate space for the key and the colon
+ len += STRLEN(dict_key) + 1;
+ });
+
+ if (has_extra) {
+ len += 2; // need space for ;"
+ }
+
+ if (!res_name || !res_fname || !res_cmd) {
+ EMSG(_(tfu_inv_ret_msg));
+ break;
+ }
+
+ char_u *const mfp = name_only ? vim_strsave(res_name) : xmalloc(len + 2);
+
+ if (!name_only) {
+ char_u *p = mfp;
+
+ *p++ = MT_GL_OTH + 1; // mtt
+ *p++ = TAG_SEP; // no tag file name
+
+ STRCPY(p, res_name);
+ p += STRLEN(p);
+
+ *p++ = TAB;
+ STRCPY(p, res_fname);
+ p += STRLEN(p);
+
+ *p++ = TAB;
+ STRCPY(p, res_cmd);
+ p += STRLEN(p);
+
+ if (has_extra) {
+ STRCPY(p, ";\"");
+ p += STRLEN(p);
+
+ if (res_kind) {
+ *p++ = TAB;
+ STRCPY(p, res_kind);
+ p += STRLEN(p);
+ }
+
+ TV_DICT_ITER(TV_LIST_ITEM_TV(li)->vval.v_dict, di, {
+ const char_u *dict_key = di->di_key;
+ typval_T *tv = &di->di_tv;
+ if (tv->v_type != VAR_STRING || tv->vval.v_string == NULL) {
+ continue;
+ }
+
+ if (!STRCMP(dict_key, "name")) {
+ continue;
+ }
+ if (!STRCMP(dict_key, "filename")) {
+ continue;
+ }
+ if (!STRCMP(dict_key, "cmd")) {
+ continue;
+ }
+ if (!STRCMP(dict_key, "kind")) {
+ continue;
+ }
+
+ *p++ = TAB;
+ STRCPY(p, dict_key);
+ p += STRLEN(p);
+ STRCPY(p, ":");
+ p += STRLEN(p);
+ STRCPY(p, tv->vval.v_string);
+ p += STRLEN(p);
+ });
+ }
+ }
+
+ // Add all matches because tagfunc should do filtering.
+ ga_grow(ga, 1);
+ ((char_u **)(ga->ga_data))[ga->ga_len++] = mfp;
+ ntags++;
+ result = OK;
+ });
+
+ tv_clear(&rettv);
+
+ *match_count = ntags;
+ return result;
+}
+
/*
* find_tags() - search for tags in tags files
*
@@ -1054,12 +1350,13 @@ static void prepare_pats(pat_T *pats, int has_re)
* TAG_NOIC don't always ignore case
* TAG_KEEP_LANG keep language
* TAG_CSCOPE use cscope results for tags
+ * TAG_NO_TAGFUNC do not call the 'tagfunc' function
*/
-int
-find_tags (
- char_u *pat, /* pattern to search for */
- int *num_matches, /* return: number of matches found */
- char_u ***matchesp, /* return: array of matches found */
+int
+find_tags(
+ char_u *pat, // pattern to search for
+ int *num_matches, // return: number of matches found
+ char_u ***matchesp, // return: array of matches found
int flags,
int mincount, /* MAXCOL: find all matches
other: minimal number of matches */
@@ -1094,7 +1391,6 @@ find_tags (
int low_char; // first char at low_offset
int high_char; // first char at high_offset
} search_info;
- off_T filesize;
int tagcmp;
off_T offset;
int round;
@@ -1144,6 +1440,7 @@ find_tags (
int get_it_again = FALSE;
int use_cscope = (flags & TAG_CSCOPE);
int verbose = (flags & TAG_VERBOSE);
+ int use_tfu = ((flags & TAG_NO_TAGFUNC) == 0);
int save_p_ic = p_ic;
// Change the value of 'ignorecase' according to 'tagcase' for the
@@ -1221,6 +1518,16 @@ find_tags (
// uninitialised.
memset(&search_info, 0, 1); // -V512
+ if (*curbuf->b_p_tfu != NUL && use_tfu && !tfu_in_use) {
+ tfu_in_use = true;
+ retval = find_tagfunc_tags(pat, &ga_match[0], &match_count,
+ flags, buf_ffname);
+ tfu_in_use = false;
+ if (retval != NOTDONE) {
+ goto findtag_end;
+ }
+ }
+
/*
* When finding a specified number of matches, first try with matching
* case, so binary search can be used, and try ignore-case matches in a
@@ -1352,12 +1659,9 @@ find_tags (
break; /* End the binary search without a match. */
else
search_info.curr_offset = offset;
- }
- /*
- * Skipping back (after a match during binary search).
- */
- else if (state == TS_SKIP_BACK) {
- search_info.curr_offset -= LSIZE * 2;
+ } else if (state == TS_SKIP_BACK) {
+ // Skipping back (after a match during binary search).
+ search_info.curr_offset -= lbuf_size * 2;
if (search_info.curr_offset < 0) {
search_info.curr_offset = 0;
rewind(fp);
@@ -1373,7 +1677,7 @@ find_tags (
/* Adjust the search file offset to the correct position */
search_info.curr_offset_used = search_info.curr_offset;
vim_fseek(fp, search_info.curr_offset, SEEK_SET);
- eof = vim_fgets(lbuf, LSIZE, fp);
+ eof = vim_fgets(lbuf, lbuf_size, fp);
if (!eof && search_info.curr_offset != 0) {
/* The explicit cast is to work around a bug in gcc 3.4.2
* (repeated below). */
@@ -1383,12 +1687,12 @@ find_tags (
vim_fseek(fp, search_info.low_offset, SEEK_SET);
search_info.curr_offset = search_info.low_offset;
}
- eof = vim_fgets(lbuf, LSIZE, fp);
+ eof = vim_fgets(lbuf, lbuf_size, fp);
}
/* skip empty and blank lines */
while (!eof && vim_isblankline(lbuf)) {
search_info.curr_offset = vim_ftell(fp);
- eof = vim_fgets(lbuf, LSIZE, fp);
+ eof = vim_fgets(lbuf, lbuf_size, fp);
}
if (eof) {
/* Hit end of file. Skip backwards. */
@@ -1404,10 +1708,9 @@ find_tags (
else {
/* skip empty and blank lines */
do {
- if (use_cscope)
- eof = cs_fgets(lbuf, LSIZE);
- else
- eof = vim_fgets(lbuf, LSIZE, fp);
+ eof = use_cscope
+ ? cs_fgets(lbuf, lbuf_size)
+ : vim_fgets(lbuf, lbuf_size, fp);
} while (!eof && vim_isblankline(lbuf));
if (eof) {
@@ -1503,19 +1806,21 @@ line_read_in:
state = TS_LINEAR;
}
- /*
- * When starting a binary search, get the size of the file and
- * compute the first offset.
- */
+ // When starting a binary search, get the size of the file and
+ // compute the first offset.
if (state == TS_BINARY) {
- // Get the tag file size.
- if ((filesize = vim_lseek(fileno(fp), (off_T)0L, SEEK_END)) <= 0) {
+ if (vim_fseek(fp, 0, SEEK_END) != 0) {
+ // can't seek, don't use binary search
state = TS_LINEAR;
} else {
- vim_lseek(fileno(fp), (off_T)0L, SEEK_SET);
-
- /* Calculate the first read offset in the file. Start
- * the search in the middle of the file. */
+ // Get the tag file size.
+ // Don't use lseek(), it doesn't work
+ // properly on MacOS Catalina.
+ const off_T filesize = vim_ftell(fp);
+ vim_fseek(fp, 0, SEEK_SET);
+
+ // Calculate the first read offset in the file. Start
+ // the search in the middle of the file.
search_info.low_offset = 0;
search_info.low_char = 0;
search_info.high_offset = filesize;
@@ -1530,19 +1835,14 @@ parse_line:
// When the line is too long the NUL will not be in the
// last-but-one byte (see vim_fgets()).
// Has been reported for Mozilla JS with extremely long names.
- // In that case we can't parse it and we ignore the line.
- if (lbuf[LSIZE - 2] != NUL && !use_cscope) {
- if (p_verbose >= 5) {
- verbose_enter();
- MSG(_("Ignoring long line in tags file"));
- verbose_leave();
- }
- if (state != TS_LINEAR) {
- // Avoid getting stuck.
- linear = true;
- state = TS_LINEAR;
- vim_fseek(fp, search_info.low_offset, SEEK_SET);
- }
+ // In that case we need to increase lbuf_size.
+ if (lbuf[lbuf_size - 2] != NUL && !use_cscope) {
+ lbuf_size *= 2;
+ xfree(lbuf);
+ lbuf = xmalloc(lbuf_size);
+ // this will try the same thing again, make sure the offset is
+ // different
+ search_info.curr_offset = 0;
continue;
}
@@ -1802,7 +2102,6 @@ parse_line:
}
}
} else {
-#define TAG_SEP 0x02
size_t tag_fname_len = STRLEN(tag_fname);
// Save the tag in a buffer.
// Use 0x02 to separate fields (Can't use NUL, because the
@@ -1986,9 +2285,7 @@ void free_tag_stuff(void)
do_tag(NULL, DT_FREE, 0, 0, 0);
tag_freematch();
- if (ptag_entry.tagname) {
- XFREE_CLEAR(ptag_entry.tagname);
- }
+ tagstack_clear_entry(&ptag_entry);
}
#endif
@@ -1999,11 +2296,11 @@ void free_tag_stuff(void)
*
* Return FAIL if no more tag file names, OK otherwise.
*/
-int
-get_tagfname (
- tagname_T *tnp, /* holds status info */
- int first, /* TRUE when first file name is wanted */
- char_u *buf /* pointer to buffer of MAXPATHL chars */
+int
+get_tagfname(
+ tagname_T *tnp, // holds status info
+ int first, // TRUE when first file name is wanted
+ char_u *buf // pointer to buffer of MAXPATHL chars
)
{
char_u *fname = NULL;
@@ -2128,9 +2425,9 @@ void tagname_free(tagname_T *tnp)
*
* Return FAIL if there is a format error in this line, OK otherwise.
*/
-static int
-parse_tag_line (
- char_u *lbuf, /* line to be parsed */
+static int
+parse_tag_line(
+ char_u *lbuf, // line to be parsed
tagptrs_T *tagp
)
{
@@ -2211,10 +2508,10 @@ static size_t matching_line_len(const char_u *const lbuf)
*
* Return OK or FAIL.
*/
-static int
-parse_match (
- char_u *lbuf, /* input: matching line */
- tagptrs_T *tagp /* output: pointers into the line */
+static int
+parse_match(
+ char_u *lbuf, // input: matching line
+ tagptrs_T *tagp // output: pointers into the line
)
{
int retval;
@@ -2229,6 +2526,8 @@ parse_match (
tagp);
tagp->tagkind = NULL;
+ tagp->user_data = NULL;
+ tagp->tagline = 0;
tagp->command_end = NULL;
if (retval == OK) {
@@ -2246,13 +2545,19 @@ parse_match (
while (ASCII_ISALPHA(*p)) {
if (STRNCMP(p, "kind:", 5) == 0) {
tagp->tagkind = p + 5;
+ } else if (STRNCMP(p, "user_data:", 10) == 0) {
+ tagp->user_data = p + 10;
+ } else if (STRNCMP(p, "line:", 5) == 0) {
+ tagp->tagline = atoi((char *)p + 5);
+ }
+ if (tagp->tagkind != NULL && tagp->user_data != NULL) {
break;
}
+
pc = vim_strchr(p, ':');
pt = vim_strchr(p, '\t');
if (pc == NULL || (pt != NULL && pc > pt)) {
tagp->tagkind = p;
- break;
}
if (pt == NULL)
break;
@@ -2266,6 +2571,12 @@ parse_match (
;
tagp->tagkind_end = p;
}
+ if (tagp->user_data != NULL) {
+ for (p = tagp->user_data;
+ *p && *p != '\t' && *p != '\r' && *p != '\n'; p++) {
+ }
+ tagp->user_data_end = p;
+ }
}
return retval;
}
@@ -2334,6 +2645,9 @@ static int jumpto_tag(
str = tagp.command;
for (pbuf_end = pbuf; *str && *str != '\n' && *str != '\r'; ) {
*pbuf_end++ = *str++;
+ if (pbuf_end - pbuf + 1 >= LSIZE) {
+ break;
+ }
}
*pbuf_end = NUL;
@@ -2485,9 +2799,15 @@ static int jumpto_tag(
p_ic = FALSE; /* don't ignore case now */
p_scs = FALSE;
save_lnum = curwin->w_cursor.lnum;
- curwin->w_cursor.lnum = 0; /* start search before first line */
+ if (tagp.tagline > 0) {
+ // start search before line from "line:" field
+ curwin->w_cursor.lnum = tagp.tagline - 1;
+ } else {
+ // start search before first line
+ curwin->w_cursor.lnum = 0;
+ }
if (do_search(NULL, pbuf[0], pbuf + 1, (long)1,
- search_options, NULL, NULL)) {
+ search_options, NULL)) {
retval = OK;
} else {
int found = 1;
@@ -2498,20 +2818,18 @@ static int jumpto_tag(
*/
p_ic = TRUE;
if (!do_search(NULL, pbuf[0], pbuf + 1, (long)1,
- search_options, NULL, NULL)) {
+ search_options, NULL)) {
// Failed to find pattern, take a guess: "^func ("
found = 2;
(void)test_for_static(&tagp);
cc = *tagp.tagname_end;
*tagp.tagname_end = NUL;
snprintf((char *)pbuf, LSIZE, "^%s\\s\\*(", tagp.tagname);
- if (!do_search(NULL, '/', pbuf, (long)1,
- search_options, NULL, NULL)) {
+ if (!do_search(NULL, '/', pbuf, (long)1, search_options, NULL)) {
// Guess again: "^char * \<func ("
snprintf((char *)pbuf, LSIZE, "^\\[#a-zA-Z_]\\.\\*\\<%s\\s\\*(",
tagp.tagname);
- if (!do_search(NULL, '/', pbuf, (long)1,
- search_options, NULL, NULL)) {
+ if (!do_search(NULL, '/', pbuf, (long)1, search_options, NULL)) {
found = 0;
}
}
@@ -2666,7 +2984,8 @@ static int test_for_current(char_u *fname, char_u *fname_end, char_u *tag_fname,
*fname_end = NUL;
}
fullname = expand_tag_fname(fname, tag_fname, true);
- retval = (path_full_compare(fullname, buf_ffname, true) & kEqualFiles);
+ retval = (path_full_compare(fullname, buf_ffname, true, true)
+ & kEqualFiles);
xfree(fullname);
*fname_end = c;
}
@@ -2715,6 +3034,15 @@ static int find_extra(char_u **pp)
return FAIL;
}
+//
+// Free a single entry in a tag stack
+//
+static void tagstack_clear_entry(taggy_T *item)
+{
+ XFREE_CLEAR(item->tagname);
+ XFREE_CLEAR(item->user_data);
+}
+
int
expand_tags (
int tagnames, /* expand tag names */
@@ -2734,14 +3062,16 @@ expand_tags (
tagnmflag = TAG_NAMES;
else
tagnmflag = 0;
- if (pat[0] == '/')
+ if (pat[0] == '/') {
ret = find_tags(pat + 1, num_file, file,
- TAG_REGEXP | tagnmflag | TAG_VERBOSE,
- TAG_MANY, curbuf->b_ffname);
- else
+ TAG_REGEXP | tagnmflag | TAG_VERBOSE | TAG_NO_TAGFUNC,
+ TAG_MANY, curbuf->b_ffname);
+ } else {
ret = find_tags(pat, num_file, file,
- TAG_REGEXP | tagnmflag | TAG_VERBOSE | TAG_NOIC,
- TAG_MANY, curbuf->b_ffname);
+ TAG_REGEXP | tagnmflag | TAG_VERBOSE
+ | TAG_NO_TAGFUNC | TAG_NOIC,
+ TAG_MANY, curbuf->b_ffname);
+ }
if (ret == OK && !tagnames) {
/* Reorganize the tags for display and matching as strings of:
* "<tagname>\0<kind>\0<filename>\0"
@@ -2767,8 +3097,8 @@ expand_tags (
* Add a tag field to the dictionary "dict".
* Return OK or FAIL.
*/
-static int
-add_tag_field (
+static int
+add_tag_field(
dict_T *dict,
const char *field_name,
const char_u *start, // start of the value
@@ -2903,6 +3233,9 @@ static void get_tag_details(taggy_T *tag, dict_T *retdict)
tv_dict_add_str(retdict, S_LEN("tagname"), (const char *)tag->tagname);
tv_dict_add_nr(retdict, S_LEN("matchnr"), tag->cur_match + 1);
tv_dict_add_nr(retdict, S_LEN("bufnr"), tag->cur_fnum);
+ if (tag->user_data) {
+ tv_dict_add_str(retdict, S_LEN("user_data"), (const char *)tag->user_data);
+ }
pos = tv_list_alloc(4);
tv_dict_add_list(retdict, S_LEN("from"), pos);
@@ -2941,7 +3274,7 @@ static void tagstack_clear(win_T *wp)
{
// Free the current tag stack
for (int i = 0; i < wp->w_tagstacklen; i++) {
- xfree(wp->w_tagstack[i].tagname);
+ tagstack_clear_entry(&wp->w_tagstack[i]);
}
wp->w_tagstacklen = 0;
wp->w_tagstackidx = 0;
@@ -2952,7 +3285,7 @@ static void tagstack_clear(win_T *wp)
static void tagstack_shift(win_T *wp)
{
taggy_T *tagstack = wp->w_tagstack;
- xfree(tagstack[0].tagname);
+ tagstack_clear_entry(&tagstack[0]);
for (int i = 1; i < wp->w_tagstacklen; i++) {
tagstack[i - 1] = tagstack[i];
}
@@ -2966,7 +3299,8 @@ static void tagstack_push_item(
int cur_fnum,
int cur_match,
pos_T mark,
- int fnum)
+ int fnum,
+ char_u *user_data)
{
taggy_T *tagstack = wp->w_tagstack;
int idx = wp->w_tagstacklen; // top of the stack
@@ -2986,6 +3320,7 @@ static void tagstack_push_item(
}
tagstack[idx].fmark.mark = mark;
tagstack[idx].fmark.fnum = fnum;
+ tagstack[idx].user_data = user_data;
}
// Add a list of items to the tag stack in the specified window
@@ -3021,10 +3356,13 @@ static void tagstack_push_items(win_T *wp, list_T *l)
if (mark.col > 0) {
mark.col--;
}
- tagstack_push_item(wp, tagname,
- (int)tv_dict_get_number(itemdict, "bufnr"),
- (int)tv_dict_get_number(itemdict, "matchnr") - 1,
- mark, fnum);
+ tagstack_push_item(
+ wp,
+ tagname,
+ (int)tv_dict_get_number(itemdict, "bufnr"),
+ (int)tv_dict_get_number(itemdict, "matchnr") - 1,
+ mark, fnum,
+ (char_u *)tv_dict_get_string(itemdict, "user_data", true));
}
}
@@ -3048,6 +3386,12 @@ int set_tagstack(win_T *wp, dict_T *d, int action)
dictitem_T *di;
list_T *l;
+ // not allowed to alter the tag stack entries from inside tagfunc
+ if (tfu_in_use) {
+ EMSG(_(recurmsg));
+ return FAIL;
+ }
+
if ((di = tv_dict_find(d, "items", -1)) != NULL) {
if (di->di_tv.v_type != VAR_LIST) {
return FAIL;
diff --git a/src/nvim/tag.h b/src/nvim/tag.h
index a8fddd05da..9f671043b3 100644
--- a/src/nvim/tag.h
+++ b/src/nvim/tag.h
@@ -20,20 +20,21 @@
#define DT_LTAG 11 /* tag using location list */
#define DT_FREE 99 /* free cached matches */
-/*
- * flags for find_tags().
- */
-#define TAG_HELP 1 /* only search for help tags */
-#define TAG_NAMES 2 /* only return name of tag */
-#define TAG_REGEXP 4 /* use tag pattern as regexp */
-#define TAG_NOIC 8 /* don't always ignore case */
-#define TAG_CSCOPE 16 /* cscope tag */
-#define TAG_VERBOSE 32 /* message verbosity */
-#define TAG_INS_COMP 64 /* Currently doing insert completion */
-#define TAG_KEEP_LANG 128 /* keep current language */
+//
+// flags for find_tags().
+//
+#define TAG_HELP 1 // only search for help tags
+#define TAG_NAMES 2 // only return name of tag
+#define TAG_REGEXP 4 // use tag pattern as regexp
+#define TAG_NOIC 8 // don't always ignore case
+#define TAG_CSCOPE 16 // cscope tag
+#define TAG_VERBOSE 32 // message verbosity
+#define TAG_INS_COMP 64 // Currently doing insert completion
+#define TAG_KEEP_LANG 128 // keep current language
+#define TAG_NO_TAGFUNC 256 // do not use 'tagfunc'
-#define TAG_MANY 300 /* When finding many tags (for completion),
- find up to this many tags */
+#define TAG_MANY 300 // When finding many tags (for completion),
+ // find up to this many tags
/*
* Structure used for get_tagfname().
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
index 8fcc8bf0a5..c5e756905a 100644
--- a/src/nvim/terminal.c
+++ b/src/nvim/terminal.c
@@ -138,6 +138,8 @@ struct terminal {
int pressed_button; // which mouse button is pressed
bool pending_resize; // pending width/height
+ bool color_set[16];
+
size_t refcount; // reference count
};
@@ -220,8 +222,6 @@ Terminal *terminal_open(TerminalOptions opts)
rv->sb_size = (size_t)curbuf->b_p_scbk;
rv->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * rv->sb_size);
- vterm_state_set_bold_highbright(state, true);
-
// Configure the color palette. Try to get the color from:
//
// - b:terminal_color_{NUM}
@@ -243,6 +243,7 @@ Terminal *terminal_open(TerminalOptions opts)
(uint8_t)((color_val >> 8) & 0xFF),
(uint8_t)((color_val >> 0) & 0xFF));
vterm_state_set_palette_color(state, i, &color);
+ rv->color_set[i] = true;
}
}
}
@@ -600,16 +601,22 @@ void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr,
int vt_fg = fg_default ? -1 : get_rgb(state, cell.fg);
int vt_bg = bg_default ? -1 : get_rgb(state, cell.bg);
- int vt_fg_idx = ((!fg_default && VTERM_COLOR_IS_INDEXED(&cell.fg))
- ? cell.fg.indexed.idx + 1 : 0);
- int vt_bg_idx = ((!bg_default && VTERM_COLOR_IS_INDEXED(&cell.bg))
- ? cell.bg.indexed.idx + 1 : 0);
+ bool fg_indexed = VTERM_COLOR_IS_INDEXED(&cell.fg);
+ bool bg_indexed = VTERM_COLOR_IS_INDEXED(&cell.bg);
+
+ int vt_fg_idx = ((!fg_default && fg_indexed) ? cell.fg.indexed.idx + 1 : 0);
+ int vt_bg_idx = ((!bg_default && bg_indexed) ? cell.bg.indexed.idx + 1 : 0);
+
+ bool fg_set = vt_fg_idx && vt_fg_idx <= 16 && term->color_set[vt_fg_idx-1];
+ bool bg_set = vt_bg_idx && vt_bg_idx <= 16 && term->color_set[vt_bg_idx-1];
int hl_attrs = (cell.attrs.bold ? HL_BOLD : 0)
| (cell.attrs.italic ? HL_ITALIC : 0)
| (cell.attrs.reverse ? HL_INVERSE : 0)
| (cell.attrs.underline ? HL_UNDERLINE : 0)
- | (cell.attrs.strike ? HL_STRIKETHROUGH: 0);
+ | (cell.attrs.strike ? HL_STRIKETHROUGH: 0)
+ | ((fg_indexed && !fg_set) ? HL_FG_INDEXED : 0)
+ | ((bg_indexed && !bg_set) ? HL_BG_INDEXED : 0);
int attr_id = 0;
diff --git a/src/nvim/testdir/Makefile b/src/nvim/testdir/Makefile
index d1a449c7cc..b470dbf8f6 100644
--- a/src/nvim/testdir/Makefile
+++ b/src/nvim/testdir/Makefile
@@ -11,11 +11,10 @@ ROOT := ../../..
export SHELL := sh
export NVIM_PRG := $(NVIM_PRG)
-export TMPDIR := $(abspath ../../../Xtest-tmpdir)
+export TMPDIR := $(abspath Xtest-tmpdir)
SCRIPTS_DEFAULT = \
test42.out \
- test48.out \
test64.out \
ifneq ($(OS),Windows_NT)
@@ -34,21 +33,23 @@ SCRIPTS ?= $(SCRIPTS_DEFAULT)
# Tests using runtest.vim.
NEW_TESTS_ALOT := test_alot_utf8 test_alot
-NEW_TESTS_IN_ALOT := $(shell sed '/^source/ s/^source //;s/\.vim$$//' test_alot*.vim)
+NEW_TESTS_IN_ALOT := $(shell sed -n '/^source/ s/^source //; s/\.vim$$//p' $(addsuffix .vim,$(NEW_TESTS_ALOT)))
+NEW_TESTS_IN_ALOT_LATIN := $(shell sed -n '/^source/ s/^source //; s/\.vim$$//p' test_alot_latin.vim)
# Ignored tests.
# test_alot_latin: Nvim does not allow setting encoding.
# test_autochdir: ported to Lua, but kept for easier merging.
# test_eval_func: used as include in old-style test (test_eval.in).
# test_listlbr: Nvim does not allow setting encoding.
# test_largefile: uses too much resources to run on CI.
-NEW_TESTS_IGNORE := $(NEW_TESTS_IN_ALOT) $(NEW_TESTS_ALOT) \
- test_alot_latin \
+NEW_TESTS_IGNORE := \
+ test_alot_latin $(NEW_TESTS_IN_ALOT_LATIN) \
test_autochdir \
test_eval_func \
test_listlbr \
test_largefile \
-NEW_TESTS ?= $(addsuffix .res,$(sort $(filter-out $(NEW_TESTS_IGNORE),$(basename $(notdir $(wildcard test_*.vim))))) $(NEW_TESTS_ALOT))
+NEW_TESTS := $(sort $(basename $(notdir $(wildcard test_*.vim))))
+NEW_TESTS_RES := $(addsuffix .res,$(filter-out $(NEW_TESTS_ALOT) $(NEW_TESTS_IN_ALOT) $(NEW_TESTS_IGNORE),$(NEW_TESTS)) $(NEW_TESTS_ALOT))
ifdef VALGRIND_GDB
@@ -112,6 +113,16 @@ fixff:
-$(NVIM_PRG) $(NO_INITS) -u unix.vim "+argdo set ff=dos|upd" +q \
dotest.in
+# Execute an individual new style test, e.g.:
+# make test_largefile
+$(NEW_TESTS):
+ rm -f $@.res test.log messages
+ @MAKEFLAGS=--no-print-directory $(MAKE) -f Makefile $@.res
+ @cat messages
+ @if test -f test.log; then \
+ exit 1; \
+ fi
+
RM_ON_RUN := test.out X* viminfo
RM_ON_START := test.ok
RUN_VIM := $(TOOL) $(NVIM_PRG) -u unix.vim -U NONE -i viminfo --headless --noplugin -s dotest.in
@@ -172,7 +183,7 @@ newtests: newtestssilent
cat messages && cat test.log; \
fi"
-newtestssilent: $(NEW_TESTS)
+newtestssilent: $(NEW_TESTS_RES)
%.res: %.vim .gdbinit
@echo "[OLDTEST] Running" $*
diff --git a/src/nvim/testdir/runnvim.sh b/src/nvim/testdir/runnvim.sh
index 2dcd9150be..72f9254635 100755
--- a/src/nvim/testdir/runnvim.sh
+++ b/src/nvim/testdir/runnvim.sh
@@ -82,6 +82,11 @@ main() {(
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.
+ if ! test -f message; then
+ cp -a test.log messages
+ fi
fi
)}
diff --git a/src/nvim/testdir/runtest.vim b/src/nvim/testdir/runtest.vim
index 593ce6fcdc..2d4134a644 100644
--- a/src/nvim/testdir/runtest.vim
+++ b/src/nvim/testdir/runtest.vim
@@ -282,18 +282,23 @@ endif
" Names of flaky tests.
let s:flaky_tests = [
+ \ 'Test_autocmd_SafeState()',
\ 'Test_cursorhold_insert()',
\ 'Test_exit_callback_interval()',
\ 'Test_map_timeout_with_timer_interrupt()',
\ 'Test_oneshot()',
\ 'Test_out_cb()',
\ 'Test_paused()',
+ \ 'Test_popup_and_window_resize()',
\ 'Test_quoteplus()',
\ 'Test_quotestar()',
\ 'Test_reltime()',
\ 'Test_repeat_many()',
\ 'Test_repeat_three()',
+ \ 'Test_state()',
\ 'Test_stop_all_in_callback()',
+ \ 'Test_term_mouse_double_click_to_create_tab',
+ \ 'Test_term_mouse_multiple_clicks_to_visually_select()',
\ 'Test_terminal_composing_unicode()',
\ 'Test_terminal_redir_file()',
\ 'Test_terminal_tmap()',
diff --git a/src/nvim/testdir/setup.vim b/src/nvim/testdir/setup.vim
index ea28f328ae..d032c9a739 100644
--- a/src/nvim/testdir/setup.vim
+++ b/src/nvim/testdir/setup.vim
@@ -19,6 +19,7 @@ set sidescroll=0
set tags=./tags,tags
set undodir^=.
set wildoptions=
+set startofline
" Prevent Nvim log from writing to stderr.
let $NVIM_LOG_FILE = exists($NVIM_LOG_FILE) ? $NVIM_LOG_FILE : 'Xnvim.log'
diff --git a/src/nvim/testdir/shared.vim b/src/nvim/testdir/shared.vim
index df512e2e3f..3875ffc056 100644
--- a/src/nvim/testdir/shared.vim
+++ b/src/nvim/testdir/shared.vim
@@ -69,7 +69,8 @@ endfunc
" Read the port number from the Xportnr file.
func GetPort()
let l = []
- for i in range(200)
+ " with 200 it sometimes failed
+ for i in range(400)
try
let l = readfile("Xportnr")
catch
@@ -135,39 +136,80 @@ endfunc
" Wait for up to five seconds for "expr" to become true. "expr" can be a
" stringified expression to evaluate, or a funcref without arguments.
+" Using a lambda works best. Example:
+" call WaitFor({-> status == "ok"})
"
" A second argument can be used to specify a different timeout in msec.
"
-" Return time slept in milliseconds. With the +reltime feature this can be
-" more than the actual waiting time. Without +reltime it can also be less.
+" When successful the time slept is returned.
+" When running into the timeout an exception is thrown, thus the function does
+" not return.
func WaitFor(expr, ...)
let timeout = get(a:000, 0, 5000)
+ let slept = s:WaitForCommon(a:expr, v:null, timeout)
+ if slept < 0
+ throw 'WaitFor() timed out after ' . timeout . ' msec'
+ endif
+ return slept
+endfunc
+
+" Wait for up to five seconds for "assert" to return zero. "assert" must be a
+" (lambda) function containing one assert function. Example:
+" call WaitForAssert({-> assert_equal("dead", job_status(job)})
+"
+" A second argument can be used to specify a different timeout in msec.
+"
+" Return zero for success, one for failure (like the assert function).
+func WaitForAssert(assert, ...)
+ let timeout = get(a:000, 0, 5000)
+ if s:WaitForCommon(v:null, a:assert, timeout) < 0
+ return 1
+ endif
+ return 0
+endfunc
+
+" Common implementation of WaitFor() and WaitForAssert().
+" Either "expr" or "assert" is not v:null
+" Return the waiting time for success, -1 for failure.
+func s:WaitForCommon(expr, assert, timeout)
" using reltime() is more accurate, but not always available
+ let slept = 0
if has('reltime')
let start = reltime()
- else
- let slept = 0
endif
- if type(a:expr) == v:t_func
- let Test = a:expr
- else
- let Test = {-> eval(a:expr) }
- endif
- for i in range(timeout / 10)
- if Test()
- if has('reltime')
- return float2nr(reltimefloat(reltime(start)) * 1000)
- endif
+
+ while 1
+ if type(a:expr) == v:t_func
+ let success = a:expr()
+ elseif type(a:assert) == v:t_func
+ let success = a:assert() == 0
+ else
+ let success = eval(a:expr)
+ endif
+ if success
return slept
endif
- if !has('reltime')
- let slept += 10
+
+ if slept >= a:timeout
+ break
+ endif
+ if type(a:assert) == v:t_func
+ " Remove the error added by the assert function.
+ call remove(v:errors, -1)
endif
+
sleep 10m
- endfor
- throw 'WaitFor() timed out after ' . timeout . ' msec'
+ if has('reltime')
+ let slept = float2nr(reltimefloat(reltime(start)) * 1000)
+ else
+ let slept += 10
+ endif
+ endwhile
+
+ return -1 " timed out
endfunc
+
" Wait for up to a given milliseconds.
" With the +timers feature this waits for key-input by getchar(), Resume()
" feeds key-input and resumes process. Return time waited in milliseconds.
@@ -211,6 +253,8 @@ func GetVimProg()
endif
endfunc
+let g:valgrind_cnt = 1
+
" Get the command to run Vim, with -u NONE and --headless arguments.
" If there is an argument use it instead of "NONE".
func GetVimCommand(...)
@@ -226,14 +270,25 @@ func GetVimCommand(...)
endif
let cmd .= ' --headless -i NONE'
let cmd = substitute(cmd, 'VIMRUNTIME=.*VIMRUNTIME;', '', '')
+
+ " If using valgrind, make sure every run uses a different log file.
+ if cmd =~ 'valgrind.*--log-file='
+ let cmd = substitute(cmd, '--log-file=\(^\s*\)', '--log-file=\1.' . g:valgrind_cnt, '')
+ let g:valgrind_cnt += 1
+ endif
+
return cmd
endfunc
-" Get the command to run Vim, with --clean.
+" Get the command to run Vim, with --clean instead of "-u NONE".
func GetVimCommandClean()
let cmd = GetVimCommand()
let cmd = substitute(cmd, '-u NONE', '--clean', '')
let cmd = substitute(cmd, '--headless', '', '')
+
+ " Optionally run Vim under valgrind
+ " let cmd = 'valgrind --tool=memcheck --leak-check=yes --num-callers=25 --log-file=valgrind ' . cmd
+
return cmd
endfunc
@@ -249,9 +304,6 @@ endfunc
func RunVimPiped(before, after, arguments, pipecmd)
let $NVIM_LOG_FILE = exists($NVIM_LOG_FILE) ? $NVIM_LOG_FILE : 'Xnvim.log'
let cmd = GetVimCommand()
- if cmd == ''
- return 0
- endif
let args = ''
if len(a:before) > 0
call writefile(a:before, 'Xbefore.vim')
@@ -262,6 +314,9 @@ func RunVimPiped(before, after, arguments, pipecmd)
let args .= ' -S Xafter.vim'
endif
+ " Optionally run Vim under valgrind
+ " let cmd = 'valgrind --tool=memcheck --leak-check=yes --num-callers=25 --log-file=valgrind ' . cmd
+
exe "silent !" . a:pipecmd . cmd . args . ' ' . a:arguments
if len(a:before) > 0
diff --git a/src/nvim/testdir/test42.in b/src/nvim/testdir/test42.in
index baa6e67d26..d9057e72fb 100644
--- a/src/nvim/testdir/test42.in
+++ b/src/nvim/testdir/test42.in
Binary files differ
diff --git a/src/nvim/testdir/test48.in b/src/nvim/testdir/test48.in
deleted file mode 100644
index 1df5a3c46a..0000000000
--- a/src/nvim/testdir/test48.in
+++ /dev/null
@@ -1,82 +0,0 @@
-This is a test of 'virtualedit'.
-
-STARTTEST
-:set noswf
-:set ve=all
-j-dgg
-:"
-:" Insert "keyword keyw", ESC, C CTRL-N, shows "keyword ykeyword".
-:" Repeating CTRL-N fixes it. (Mary Ellen Foster)
-2/w
-C
-:"
-:" Using "C" then then <CR> moves the last remaining character to the next
-:" line. (Mary Ellen Foster)
-j^/are
-C are belong to vim
-:"
-:" When past the end of a line that ends in a single character "b" skips
-:" that word.
-^$15lbC7
-:"
-:" Make sure 'i' works
-$4li<-- should be 3 ' '
-:"
-:" Make sure 'C' works
-$4lC<-- should be 3 ' '
-:"
-:" Make sure 'a' works
-$4la<-- should be 4 ' '
-:"
-:" Make sure 'A' works
-$4lA<-- should be 0 ' '
-:"
-:" Make sure 'D' works
-$4lDi<-- 'D' should be intact
-:"
-:" Test for yank bug reported by Mark Waggoner.
-:set ve=block
-^2w3jyGp
-:"
-:" Test "r" beyond the end of the line
-:set ve=all
-/^"r"
-$5lrxa<-- should be 'x'
-:"
-:" Test "r" on a tab
-:" Note that for this test, 'ts' must be 8 (the default).
-^5lrxA<-- should be ' x '
-:"
-:" Test to make sure 'x' can delete control characters
-:set display=uhex
-^xxxxxxi[This line should contain only the text between the brackets.]
-:set display=
-:"
-:" Test for ^Y/^E due to bad w_virtcol value, reported by
-:" Roy <royl@netropolis.net>.
-^O3li4li4li <-- should show the name of a noted text editor
-^o4li4li4li <-- and its version number-dd
-:"
-:" Test for yanking and pasting using the small delete register
-gg/^foo
-dewve"-p
-:wq! test.out
-ENDTEST
-foo, bar
-keyword keyw
-all your base are belong to us
-1 2 3 4 5 6
-'i'
-'C'
-'a'
-'A'
-'D'
-this is a test
-this is a test
-this is a test
-"r"
-"r"
-ab sd
-abcv6efi.him0kl
-
-
diff --git a/src/nvim/testdir/test48.ok b/src/nvim/testdir/test48.ok
deleted file mode 100644
index 14cd9b12ec..0000000000
--- a/src/nvim/testdir/test48.ok
+++ /dev/null
@@ -1,23 +0,0 @@
-, foo
-keyword keyword
-all your base
-are belong to vim
-1 2 3 4 5 7
-'i' <-- should be 3 ' '
-'C' <-- should be 3 ' '
-'a' <-- should be 4 ' '
-'A'<-- should be 0 ' '
-'D' <-- 'D' should be intact
-this is a test
-this is a test
-this is a test
-"r" x<-- should be 'x'
-"r" x <-- should be ' x '
-[This line should contain only the text between the brackets.]
- v i m <-- should show the name of a noted text editor
- 6 . 0 <-- and its version number
-
-a
-a
-a
-
diff --git a/src/nvim/testdir/test49.vim b/src/nvim/testdir/test49.vim
index 837e55ebca..fc79f57d2e 100644
--- a/src/nvim/testdir/test49.vim
+++ b/src/nvim/testdir/test49.vim
@@ -178,7 +178,7 @@ endif
" next Xpath value. No new Xnext value is prepared. The argument
" should be 2^(n-1) for the nth Xloop command inside the loop.
" If the loop has only one Xloop command, the argument can be
-" ommitted (default: 1).
+" omitted (default: 1).
"
" - Use XloopNEXT before ":continue" and ":endwhile". This computes a new
" Xnext value for the next execution of the loop by multiplying the old
diff --git a/src/nvim/testdir/test_alot.vim b/src/nvim/testdir/test_alot.vim
index 6bf2e8329c..5668f45dea 100644
--- a/src/nvim/testdir/test_alot.vim
+++ b/src/nvim/testdir/test_alot.vim
@@ -2,6 +2,7 @@
" This makes testing go faster, since Vim doesn't need to restart.
source test_assign.vim
+source test_backup.vim
source test_behave.vim
source test_cd.vim
source test_changedtick.vim
@@ -27,7 +28,6 @@ source test_jumps.vim
source test_fileformat.vim
source test_filetype.vim
source test_lambda.vim
-source test_mapping.vim
source test_menu.vim
source test_messages.vim
source test_modeline.vim
@@ -45,6 +45,7 @@ source test_syn_attr.vim
source test_tabline.vim
source test_tabpage.vim
source test_tagcase.vim
+source test_tagfunc.vim
source test_tagjump.vim
source test_taglist.vim
source test_true_false.vim
diff --git a/src/nvim/testdir/test_assert.vim b/src/nvim/testdir/test_assert.vim
index a4c8ce7e43..4cc90eca7a 100644
--- a/src/nvim/testdir/test_assert.vim
+++ b/src/nvim/testdir/test_assert.vim
@@ -1,5 +1,40 @@
" Test that the methods used for testing work.
+func Test_assert_equalfile()
+ call assert_equal(1, assert_equalfile('abcabc', 'xyzxyz'))
+ call assert_match("E485: Can't read file abcabc", v:errors[0])
+ call remove(v:errors, 0)
+
+ let goodtext = ["one", "two", "three"]
+ call writefile(goodtext, 'Xone')
+ call assert_equal(1, assert_equalfile('Xone', 'xyzxyz'))
+ call assert_match("E485: Can't read file xyzxyz", v:errors[0])
+ call remove(v:errors, 0)
+
+ call writefile(goodtext, 'Xtwo')
+ call assert_equal(0, assert_equalfile('Xone', 'Xtwo'))
+
+ call writefile([goodtext[0]], 'Xone')
+ call assert_equal(1, assert_equalfile('Xone', 'Xtwo'))
+ call assert_match("first file is shorter", v:errors[0])
+ call remove(v:errors, 0)
+
+ call writefile(goodtext, 'Xone')
+ call writefile([goodtext[0]], 'Xtwo')
+ call assert_equal(1, assert_equalfile('Xone', 'Xtwo'))
+ call assert_match("second file is shorter", v:errors[0])
+ call remove(v:errors, 0)
+
+ call writefile(['1234X89'], 'Xone')
+ call writefile(['1234Y89'], 'Xtwo')
+ call assert_equal(1, assert_equalfile('Xone', 'Xtwo'))
+ call assert_match("difference at byte 4", v:errors[0])
+ call remove(v:errors, 0)
+
+ call delete('Xone')
+ call delete('Xtwo')
+endfunc
+
func Test_assert_fails_in_try_block()
try
call assert_equal(0, assert_fails('throw "error"'))
diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim
index 5848940a2b..275b053bc9 100644
--- a/src/nvim/testdir/test_autocmd.vim
+++ b/src/nvim/testdir/test_autocmd.vim
@@ -425,18 +425,20 @@ func Test_autocmd_bufwipe_in_SessLoadPost()
set noswapfile
mksession!
- let content = ['set nocp noswapfile',
- \ 'let v:swapchoice="e"',
- \ 'augroup test_autocmd_sessionload',
- \ 'autocmd!',
- \ 'autocmd SessionLoadPost * exe bufnr("Xsomething") . "bw!"',
- \ 'augroup END',
- \ '',
- \ 'func WriteErrors()',
- \ ' call writefile([execute("messages")], "Xerrors")',
- \ 'endfunc',
- \ 'au VimLeave * call WriteErrors()',
- \ ]
+ let content =<< trim [CODE]
+ set nocp noswapfile
+ let v:swapchoice="e"
+ augroup test_autocmd_sessionload
+ autocmd!
+ autocmd SessionLoadPost * exe bufnr("Xsomething") . "bw!"
+ augroup END
+
+ func WriteErrors()
+ call writefile([execute("messages")], "Xerrors")
+ endfunc
+ au VimLeave * call WriteErrors()
+ [CODE]
+
call writefile(content, 'Xvimrc')
call system(v:progpath. ' --headless -i NONE -u Xvimrc --noplugins -S Session.vim -c cq')
let errors = join(readfile('Xerrors'))
@@ -454,27 +456,29 @@ func Test_autocmd_bufwipe_in_SessLoadPost2()
set noswapfile
mksession!
- let content = ['set nocp noswapfile',
- \ 'function! DeleteInactiveBufs()',
- \ ' tabfirst',
- \ ' let tabblist = []',
- \ ' for i in range(1, tabpagenr(''$''))',
- \ ' call extend(tabblist, tabpagebuflist(i))',
- \ ' endfor',
- \ ' for b in range(1, bufnr(''$''))',
- \ ' if bufexists(b) && buflisted(b) && (index(tabblist, b) == -1 || bufname(b) =~# ''^$'')',
- \ ' exec ''bwipeout '' . b',
- \ ' endif',
- \ ' endfor',
- \ ' echomsg "SessionLoadPost DONE"',
- \ 'endfunction',
- \ 'au SessionLoadPost * call DeleteInactiveBufs()',
- \ '',
- \ 'func WriteErrors()',
- \ ' call writefile([execute("messages")], "Xerrors")',
- \ 'endfunc',
- \ 'au VimLeave * call WriteErrors()',
- \ ]
+ let content =<< trim [CODE]
+ set nocp noswapfile
+ function! DeleteInactiveBufs()
+ tabfirst
+ let tabblist = []
+ for i in range(1, tabpagenr(''$''))
+ call extend(tabblist, tabpagebuflist(i))
+ endfor
+ for b in range(1, bufnr(''$''))
+ if bufexists(b) && buflisted(b) && (index(tabblist, b) == -1 || bufname(b) =~# ''^$'')
+ exec ''bwipeout '' . b
+ endif
+ endfor
+ echomsg "SessionLoadPost DONE"
+ endfunction
+ au SessionLoadPost * call DeleteInactiveBufs()
+
+ func WriteErrors()
+ call writefile([execute("messages")], "Xerrors")
+ endfunc
+ au VimLeave * call WriteErrors()
+ [CODE]
+
call writefile(content, 'Xvimrc')
call system(v:progpath. ' --headless -i NONE -u Xvimrc --noplugins -S Session.vim -c cq')
let errors = join(readfile('Xerrors'))
@@ -936,21 +940,23 @@ func Test_bufunload_all()
call writefile(['Test file Xxx1'], 'Xxx1')"
call writefile(['Test file Xxx2'], 'Xxx2')"
- let 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')",
- \ "edit Xxx1",
- \ "split Xxx2",
- \ "q"]
+ let content =<< trim [CODE]
+ 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')
+ edit Xxx1
+ split Xxx2
+ q
+ [CODE]
+
call writefile(content, 'Xtest')
call delete('Xout')
@@ -1344,11 +1350,11 @@ func Test_Changed_FirstTime()
let buf = term_start([GetVimProg(), '--clean', '-c', 'set noswapfile'], {'term_rows': 3})
call assert_equal('running', term_getstatus(buf))
" Wait for the ruler (in the status line) to be shown.
- call WaitFor({-> term_getline(buf, 3) =~# '\<All$'})
+ call WaitForAssert({-> assert_match('\<All$', term_getline(buf, 3))})
" It's only adding autocmd, so that no event occurs.
call term_sendkeys(buf, ":au! TextChanged <buffer> call writefile(['No'], 'Xchanged.txt')\<cr>")
call term_sendkeys(buf, "\<C-\\>\<C-N>:qa!\<cr>")
- call WaitFor({-> term_getstatus(buf) == 'finished'})
+ call WaitForAssert({-> assert_equal('finished', term_getstatus(buf))})
call assert_equal([''], readfile('Xchanged.txt'))
" clean up
diff --git a/src/nvim/testdir/test_backup.vim b/src/nvim/testdir/test_backup.vim
new file mode 100644
index 0000000000..fa10430613
--- /dev/null
+++ b/src/nvim/testdir/test_backup.vim
@@ -0,0 +1,58 @@
+" Tests for the backup function
+
+func Test_backup()
+ set backup backupdir=.
+ new
+ call setline(1, ['line1', 'line2'])
+ :f Xbackup.txt
+ :w! Xbackup.txt
+ " backup file is only created after
+ " writing a second time (before overwriting)
+ :w! Xbackup.txt
+ let l = readfile('Xbackup.txt~')
+ call assert_equal(['line1', 'line2'], l)
+ bw!
+ set backup&vim backupdir&vim
+ call delete('Xbackup.txt')
+ call delete('Xbackup.txt~')
+endfunc
+
+func Test_backup2()
+ set backup backupdir=.//
+ new
+ call setline(1, ['line1', 'line2', 'line3'])
+ :f Xbackup.txt
+ :w! Xbackup.txt
+ " backup file is only created after
+ " writing a second time (before overwriting)
+ :w! Xbackup.txt
+ sp *Xbackup.txt~
+ call assert_equal(['line1', 'line2', 'line3'], getline(1,'$'))
+ let f=expand('%')
+ call assert_match('src%nvim%testdir%Xbackup.txt\~', f)
+ bw!
+ bw!
+ call delete('Xbackup.txt')
+ call delete(f)
+ set backup&vim backupdir&vim
+endfunc
+
+func Test_backup2_backupcopy()
+ set backup backupdir=.// backupcopy=yes
+ new
+ call setline(1, ['line1', 'line2', 'line3'])
+ :f Xbackup.txt
+ :w! Xbackup.txt
+ " backup file is only created after
+ " writing a second time (before overwriting)
+ :w! Xbackup.txt
+ sp *Xbackup.txt~
+ call assert_equal(['line1', 'line2', 'line3'], getline(1,'$'))
+ let f=expand('%')
+ call assert_match('src%nvim%testdir%Xbackup.txt\~', f)
+ bw!
+ bw!
+ call delete('Xbackup.txt')
+ call delete(f)
+ set backup&vim backupdir&vim backupcopy&vim
+endfunc
diff --git a/src/nvim/testdir/test_cindent.vim b/src/nvim/testdir/test_cindent.vim
index 7c2c5e341c..d9795d9335 100644
--- a/src/nvim/testdir/test_cindent.vim
+++ b/src/nvim/testdir/test_cindent.vim
@@ -18,25 +18,25 @@ endfunc
func Test_cino_extern_c()
" Test for cino-E
- let without_ind = [
- \ '#ifdef __cplusplus',
- \ 'extern "C" {',
- \ '#endif',
- \ 'int func_a(void);',
- \ '#ifdef __cplusplus',
- \ '}',
- \ '#endif'
- \ ]
+ let without_ind =<< trim [CODE]
+ #ifdef __cplusplus
+ extern "C" {
+ #endif
+ int func_a(void);
+ #ifdef __cplusplus
+ }
+ #endif
+ [CODE]
- let with_ind = [
- \ '#ifdef __cplusplus',
- \ 'extern "C" {',
- \ '#endif',
- \ "\tint func_a(void);",
- \ '#ifdef __cplusplus',
- \ '}',
- \ '#endif'
- \ ]
+ let with_ind =<< trim [CODE]
+ #ifdef __cplusplus
+ extern "C" {
+ #endif
+ int func_a(void);
+ #ifdef __cplusplus
+ }
+ #endif
+ [CODE]
new
setlocal cindent cinoptions=E0
call setline(1, without_ind)
@@ -89,16 +89,32 @@ func Test_cindent_expr()
return v:lnum == 1 ? shiftwidth() : 0
endfunc
setl expandtab sw=8 indentkeys+=; indentexpr=MyIndentFunction()
- call setline(1, ['var_a = something()', 'b = something()'])
+ let testinput =<< trim [CODE]
+ var_a = something()
+ b = something()
+ [CODE]
+ call setline(1, testinput)
call cursor(1, 1)
call feedkeys("^\<c-v>j$A;\<esc>", 'tnix')
- call assert_equal([' var_a = something();', 'b = something();'], getline(1, '$'))
+ let expected =<< [CODE]
+ var_a = something();
+b = something();
+[CODE]
+ call assert_equal(expected, getline(1, '$'))
%d
- call setline(1, [' var_a = something()', ' b = something()'])
+ let testinput =<< [CODE]
+ var_a = something()
+ b = something()
+[CODE]
+ call setline(1, testinput)
call cursor(1, 1)
call feedkeys("^\<c-v>j$A;\<esc>", 'tnix')
- call assert_equal([' var_a = something();', ' b = something()'], getline(1, '$'))
+ let expected =<< [CODE]
+ var_a = something();
+ b = something()
+[CODE]
+ call assert_equal(expected, getline(1, '$'))
bw!
endfunc
diff --git a/src/nvim/testdir/test_clientserver.vim b/src/nvim/testdir/test_clientserver.vim
index 813cb338a5..3377f86126 100644
--- a/src/nvim/testdir/test_clientserver.vim
+++ b/src/nvim/testdir/test_clientserver.vim
@@ -28,12 +28,11 @@ func Test_client_server()
let name = 'XVIMTEST'
let cmd .= ' --servername ' . name
let job = job_start(cmd, {'stoponexit': 'kill', 'out_io': 'null'})
- call WaitFor({-> job_status(job) == "run"})
+ call WaitForAssert({-> assert_equal("run", job_status(job))})
" Takes a short while for the server to be active.
" When using valgrind it takes much longer.
- call WaitFor('serverlist() =~ "' . name . '"')
- call assert_match(name, serverlist())
+ call WaitForAssert({-> assert_match(name, serverlist())})
call remote_foreground(name)
@@ -54,12 +53,10 @@ func Test_client_server()
endif
" Wait for the server to be up and answering requests.
sleep 100m
- call WaitFor('remote_expr("' . name . '", "v:version", "", 1) != ""')
- call assert_true(remote_expr(name, "v:version", "", 1) != "")
+ call WaitForAssert({-> assert_true(remote_expr(name, "v:version", "", 1) != "")})
call remote_send(name, ":let testvar = 'maybe'\<CR>")
- call WaitFor('remote_expr("' . name . '", "testvar", "", 1) == "maybe"')
- call assert_equal('maybe', remote_expr(name, "testvar", "", 2))
+ call WaitForAssert({-> assert_equal('maybe', remote_expr(name, "testvar", "", 2))})
endif
call assert_fails('call remote_send("XXX", ":let testvar = ''yes''\<CR>")', 'E241')
@@ -94,7 +91,7 @@ func Test_client_server()
call remote_send(name, ":qa!\<CR>")
try
- call WaitFor({-> job_status(job) == "dead"})
+ call WaitForAssert({-> assert_equal("dead", job_status(job))})
finally
if job_status(job) != 'dead'
call assert_report('Server did not exit')
diff --git a/src/nvim/testdir/test_cmdline.vim b/src/nvim/testdir/test_cmdline.vim
index e6aafd964b..9c3c33a943 100644
--- a/src/nvim/testdir/test_cmdline.vim
+++ b/src/nvim/testdir/test_cmdline.vim
@@ -1,6 +1,5 @@
" Tests for editing the command line.
-
func Test_complete_tab()
call writefile(['testfile'], 'Xtestfile')
call feedkeys(":e Xtestf\t\r", "tx")
@@ -79,26 +78,45 @@ func Test_map_completion()
call feedkeys(":map <silent> <sp\<Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"map <silent> <special>', getreg(':'))
+ map <Middle>x middle
+
map ,f commaf
map ,g commaf
+ map <Left> left
+ map <A-Left>x shiftleft
call feedkeys(":map ,\<Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"map ,f', getreg(':'))
call feedkeys(":map ,\<Tab>\<Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"map ,g', getreg(':'))
+ call feedkeys(":map <L\<Tab>\<Home>\"\<CR>", 'xt')
+ call assert_equal('"map <Left>', getreg(':'))
+ call feedkeys(":map <A-Left>\<Tab>\<Home>\"\<CR>", 'xt')
+ call assert_equal("\"map <A-Left>\<Tab>", getreg(':'))
unmap ,f
unmap ,g
+ unmap <Left>
+ unmap <A-Left>x
set cpo-=< cpo-=B cpo-=k
map <Left> left
call feedkeys(":map <L\<Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"map <Left>', getreg(':'))
+ call feedkeys(":map <M\<Tab>\<Home>\"\<CR>", 'xt')
+ " call assert_equal("\"map <M\<Tab>", getreg(':'))
unmap <Left>
" set cpo+=<
map <Left> left
+ exe "set t_k6=\<Esc>[17~"
+ call feedkeys(":map \<Esc>[17~x f6x\<CR>", 'xt')
call feedkeys(":map <L\<Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"map <Left>', getreg(':'))
+ if !has('gui_running')
+ call feedkeys(":map \<Esc>[17~\<Tab>\<Home>\"\<CR>", 'xt')
+ " call assert_equal("\"map <F6>x", getreg(':'))
+ endif
unmap <Left>
+ call feedkeys(":unmap \<Esc>[17~x\<CR>", 'xt')
set cpo-=<
set cpo+=B
@@ -114,6 +132,9 @@ func Test_map_completion()
call assert_equal('"map <Left>', getreg(':'))
unmap <Left>
" set cpo-=k
+
+ unmap <Middle>x
+ set cpo&vim
endfunc
func Test_match_completion()
@@ -160,6 +181,7 @@ func Test_expr_completion()
endif
for cmd in [
\ 'let a = ',
+ \ 'const a = ',
\ 'if',
\ 'elseif',
\ 'while',
@@ -302,7 +324,7 @@ func Test_getcompletion()
call assert_equal([], l)
let l = getcompletion('.', 'shellcmd')
- call assert_equal(['./', '../'], l[0:1])
+ call assert_equal(['./', '../'], filter(l, 'v:val =~ "\\./"'))
call assert_equal(-1, match(l[2:], '^\.\.\?/$'))
let root = has('win32') ? 'C:\\' : '/'
let l = getcompletion(root, 'shellcmd')
@@ -376,6 +398,29 @@ func Test_getcompletion()
call assert_fails('call getcompletion("", "burp")', 'E475:')
endfunc
+func Test_shellcmd_completion()
+ let save_path = $PATH
+
+ call mkdir('Xpathdir/Xpathsubdir', 'p')
+ call writefile([''], 'Xpathdir/Xfile.exe')
+ call setfperm('Xpathdir/Xfile.exe', 'rwx------')
+
+ " Set PATH to example directory without trailing slash.
+ let $PATH = getcwd() . '/Xpathdir'
+
+ " Test for the ":!<TAB>" case. Previously, this would include subdirs of
+ " dirs in the PATH, even though they won't be executed. We check that only
+ " subdirs of the PWD and executables from the PATH are included in the
+ " suggestions.
+ let actual = getcompletion('X', 'shellcmd')
+ let expected = map(filter(glob('*', 0, 1), 'isdirectory(v:val) && v:val[0] == "X"'), 'v:val . "/"')
+ call insert(expected, 'Xfile.exe')
+ call assert_equal(expected, actual)
+
+ call delete('Xpathdir', 'rf')
+ let $PATH = save_path
+endfunc
+
func Test_expand_star_star()
call mkdir('a/b', 'p')
call writefile(['asdfasdf'], 'a/b/fileXname')
@@ -477,6 +522,51 @@ func Test_cmdline_complete_user_cmd()
delcommand Foo
endfunc
+func Test_cmdline_complete_user_names()
+ if has('unix') && executable('whoami')
+ let whoami = systemlist('whoami')[0]
+ let first_letter = whoami[0]
+ if len(first_letter) > 0
+ " Trying completion of :e ~x where x is the first letter of
+ " the user name should complete to at least the user name.
+ call feedkeys(':e ~' . first_letter . "\<c-a>\<c-B>\"\<cr>", 'tx')
+ call assert_match('^"e \~.*\<' . whoami . '\>', @:)
+ endif
+ endif
+ if has('win32')
+ " Just in case: check that the system has an Administrator account.
+ let names = system('net user')
+ if names =~ 'Administrator'
+ " Trying completion of :e ~A should complete to Administrator.
+ " There could be other names starting with "A" before Administrator.
+ call feedkeys(':e ~A' . "\<c-a>\<c-B>\"\<cr>", 'tx')
+ call assert_match('^"e \~.*Administrator', @:)
+ endif
+ endif
+endfunc
+
+funct Test_cmdline_complete_languages()
+ let lang = substitute(execute('language messages'), '.*"\(.*\)"$', '\1', '')
+
+ call feedkeys(":language \<c-a>\<c-b>\"\<cr>", 'tx')
+ call assert_match('^"language .*\<ctype\>.*\<messages\>.*\<time\>', @:)
+
+ if has('unix')
+ " TODO: these tests don't work on Windows. lang appears to be 'C'
+ " but C does not appear in the completion. Why?
+ call assert_match('^"language .*\<' . lang . '\>', @:)
+
+ call feedkeys(":language messages \<c-a>\<c-b>\"\<cr>", 'tx')
+ call assert_match('^"language .*\<' . lang . '\>', @:)
+
+ call feedkeys(":language ctype \<c-a>\<c-b>\"\<cr>", 'tx')
+ call assert_match('^"language .*\<' . lang . '\>', @:)
+
+ call feedkeys(":language time \<c-a>\<c-b>\"\<cr>", 'tx')
+ call assert_match('^"language .*\<' . lang . '\>', @:)
+ endif
+endfunc
+
func Test_cmdline_write_alternatefile()
new
call setline('.', ['one', 'two'])
@@ -529,6 +619,8 @@ func Check_cmdline(cmdtype)
return ''
endfunc
+set cpo&
+
func Test_getcmdtype()
call feedkeys(":MyCmd a\<C-R>=Check_cmdline(':')\<CR>\<Esc>", "xt")
@@ -569,6 +661,37 @@ func Test_getcmdwintype()
call assert_equal('', getcmdwintype())
endfunc
+func Test_getcmdwin_autocmd()
+ let s:seq = []
+ augroup CmdWin
+ au WinEnter * call add(s:seq, 'WinEnter ' .. win_getid())
+ au WinLeave * call add(s:seq, 'WinLeave ' .. win_getid())
+ au BufEnter * call add(s:seq, 'BufEnter ' .. bufnr())
+ au BufLeave * call add(s:seq, 'BufLeave ' .. bufnr())
+ au CmdWinEnter * call add(s:seq, 'CmdWinEnter ' .. win_getid())
+ au CmdWinLeave * call add(s:seq, 'CmdWinLeave ' .. win_getid())
+
+ let org_winid = win_getid()
+ let org_bufnr = bufnr()
+ call feedkeys("q::let a = getcmdwintype()\<CR>:let s:cmd_winid = win_getid()\<CR>:let s:cmd_bufnr = bufnr()\<CR>:q\<CR>", 'x!')
+ call assert_equal(':', a)
+ call assert_equal([
+ \ 'WinLeave ' .. org_winid,
+ \ 'WinEnter ' .. s:cmd_winid,
+ \ 'BufLeave ' .. org_bufnr,
+ \ 'BufEnter ' .. s:cmd_bufnr,
+ \ 'CmdWinEnter ' .. s:cmd_winid,
+ \ 'CmdWinLeave ' .. s:cmd_winid,
+ \ 'BufLeave ' .. s:cmd_bufnr,
+ \ 'WinLeave ' .. s:cmd_winid,
+ \ 'WinEnter ' .. org_winid,
+ \ 'BufEnter ' .. org_bufnr,
+ \ ], s:seq)
+
+ au!
+ augroup END
+endfunc
+
func Test_verbosefile()
set verbosefile=Xlog
echomsg 'foo'
@@ -628,4 +751,7 @@ func Test_cmdline_overstrike()
let &encoding = encoding_save
endfunc
-set cpo&
+func Test_cmdwin_feedkeys()
+ " This should not generate E488
+ call feedkeys("q:\<CR>", 'x')
+endfunc
diff --git a/src/nvim/testdir/test_compiler.vim b/src/nvim/testdir/test_compiler.vim
index 46c14d8bc3..40d3cdbdae 100644
--- a/src/nvim/testdir/test_compiler.vim
+++ b/src/nvim/testdir/test_compiler.vim
@@ -10,6 +10,10 @@ func Test_compiler()
unlet $LANG
endif
+ " %:S does not work properly with 'shellslash' set
+ let save_shellslash = &shellslash
+ set noshellslash
+
e Xfoo.pl
compiler perl
call assert_equal('perl', b:current_compiler)
@@ -24,9 +28,11 @@ func Test_compiler()
w!
call feedkeys(":make\<CR>\<CR>", 'tx')
let a=execute('clist')
- call assert_match("\n 1 Xfoo.pl:3: Global symbol \"\$foo\" "
- \ . "requires explicit package name", a)
+ call assert_match('\n \d\+ Xfoo.pl:3: Global symbol "$foo" '
+ \ . 'requires explicit package name', a)
+
+ let &shellslash = save_shellslash
call delete('Xfoo.pl')
bw!
endfunc
diff --git a/src/nvim/testdir/test_const.vim b/src/nvim/testdir/test_const.vim
index 06062c5e58..eaf200e9bb 100644
--- a/src/nvim/testdir/test_const.vim
+++ b/src/nvim/testdir/test_const.vim
@@ -176,6 +176,26 @@ func Test_cannot_modify_existing_variable()
call assert_fails('const [i2, f2, s2] = [1, 1.1, "vim"]', 'E995:')
endfunc
+func Test_const_with_condition()
+ const x = 0
+ if 0 | const x = 1 | endif
+ call assert_equal(0, x)
+endfunc
+
+func Test_lockvar()
+ let x = 'hello'
+ lockvar x
+ call assert_fails('let x = "there"', 'E741')
+ if 0 | unlockvar x | endif
+ call assert_fails('let x = "there"', 'E741')
+ unlockvar x
+ let x = 'there'
+
+ if 0 | lockvar x | endif
+ let x = 'again'
+endfunc
+
+
func Test_const_with_index_access()
let l = [1, 2, 3]
call assert_fails('const l[0] = 4', 'E996:')
diff --git a/src/nvim/testdir/test_debugger.vim b/src/nvim/testdir/test_debugger.vim
index 3ef460b4fe..130bcf8910 100644
--- a/src/nvim/testdir/test_debugger.vim
+++ b/src/nvim/testdir/test_debugger.vim
@@ -26,27 +26,29 @@ func Test_Debugger()
endif
" Create a Vim script with some functions
- call writefile([
- \ 'func Foo()',
- \ ' let var1 = 1',
- \ ' let var2 = Bar(var1) + 9',
- \ ' return var2',
- \ 'endfunc',
- \ 'func Bar(var)',
- \ ' let var1 = 2 + a:var',
- \ ' let var2 = Bazz(var1) + 4',
- \ ' return var2',
- \ 'endfunc',
- \ 'func Bazz(var)',
- \ ' try',
- \ ' let var1 = 3 + a:var',
- \ ' let var3 = "another var"',
- \ ' let var3 = "value2"',
- \ ' catch',
- \ ' let var4 = "exception"',
- \ ' endtry',
- \ ' return var1',
- \ 'endfunc'], 'Xtest.vim')
+ let lines =<< trim END
+ func Foo()
+ let var1 = 1
+ let var2 = Bar(var1) + 9
+ return var2
+ endfunc
+ func Bar(var)
+ let var1 = 2 + a:var
+ let var2 = Bazz(var1) + 4
+ return var2
+ endfunc
+ func Bazz(var)
+ try
+ let var1 = 3 + a:var
+ let var3 = "another var"
+ let var3 = "value2"
+ catch
+ let var4 = "exception"
+ endtry
+ return var1
+ endfunc
+ END
+ call writefile(lines, 'Xtest.vim')
" Start Vim in a terminal
let buf = RunVimInTerminal('-S Xtest.vim', {})
@@ -294,11 +296,13 @@ func Test_Debugger()
" Tests for :breakadd file and :breakadd here
" Breakpoints should be set before sourcing the file
- call writefile([
- \ 'let var1 = 10',
- \ 'let var2 = 20',
- \ 'let var3 = 30',
- \ 'let var4 = 40'], 'Xtest.vim')
+ let lines =<< trim END
+ let var1 = 10
+ let var2 = 20
+ let var3 = 30
+ let var4 = 40
+ END
+ call writefile(lines, 'Xtest.vim')
" Start Vim in a terminal
let buf = RunVimInTerminal('Xtest.vim', {})
diff --git a/src/nvim/testdir/test_diffmode.vim b/src/nvim/testdir/test_diffmode.vim
index 57b19aa817..21e0271bda 100644
--- a/src/nvim/testdir/test_diffmode.vim
+++ b/src/nvim/testdir/test_diffmode.vim
@@ -773,3 +773,28 @@ func Test_diff_of_diff()
call StopVimInTerminal(buf)
call delete('Xtest_diff_diff')
endfunc
+
+func CloseoffSetup()
+ enew
+ call setline(1, ['one', 'two', 'three'])
+ diffthis
+ new
+ call setline(1, ['one', 'tow', 'three'])
+ diffthis
+ call assert_equal(1, &diff)
+ only!
+endfunc
+
+func Test_diff_closeoff()
+ " "closeoff" included by default: last diff win gets 'diff' reset'
+ call CloseoffSetup()
+ call assert_equal(0, &diff)
+ enew!
+
+ " "closeoff" excluded: last diff win keeps 'diff' set'
+ set diffopt-=closeoff
+ call CloseoffSetup()
+ call assert_equal(1, &diff)
+ diffoff!
+ enew!
+endfunc
diff --git a/src/nvim/testdir/test_digraph.vim b/src/nvim/testdir/test_digraph.vim
index 62a5da33df..5da05e85b5 100644
--- a/src/nvim/testdir/test_digraph.vim
+++ b/src/nvim/testdir/test_digraph.vim
@@ -465,4 +465,17 @@ func Test_show_digraph()
bwipe!
endfunc
+func Test_show_digraph_cp1251()
+ throw 'skipped: Nvim supports ''utf8'' encoding only'
+ if !has('multi_byte')
+ return
+ endif
+ new
+ set encoding=cp1251
+ call Put_Dig("='")
+ call assert_equal("\n<\xfa> <|z> <M-z> 250, Hex fa, Oct 372, Digr ='", execute('ascii'))
+ set encoding=utf-8
+ bwipe!
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/test_display.vim b/src/nvim/testdir/test_display.vim
index 5feb59eef1..1c2f5a05ff 100644
--- a/src/nvim/testdir/test_display.vim
+++ b/src/nvim/testdir/test_display.vim
@@ -69,3 +69,59 @@ func! Test_display_foldtext_mbyte()
set foldtext& fillchars& foldmethod& fdc&
bw!
endfunc
+
+func Test_display_listchars_precedes()
+ set fillchars+=vert:\|
+ call NewWindow(10, 10)
+ " Need a physical line that wraps over the complete
+ " window size
+ call append(0, repeat('aaa aaa aa ', 10))
+ call append(1, repeat(['bbb bbb bbb bbb'], 2))
+ " remove blank trailing line
+ $d
+ set list nowrap
+ call cursor(1, 1)
+ " move to end of line and scroll 2 characters back
+ norm! $2zh
+ let lines=ScreenLines([1,4], winwidth(0)+1)
+ let expect = [
+ \ " aaa aa $ |",
+ \ "$ |",
+ \ "$ |",
+ \ "~ |",
+ \ ]
+ call assert_equal(expect, lines)
+ set list listchars+=precedes:< nowrap
+ call cursor(1, 1)
+ " move to end of line and scroll 2 characters back
+ norm! $2zh
+ let lines = ScreenLines([1,4], winwidth(0)+1)
+ let expect = [
+ \ "<aaa aa $ |",
+ \ "< |",
+ \ "< |",
+ \ "~ |",
+ \ ]
+ call assert_equal(expect, lines)
+ set wrap
+ call cursor(1, 1)
+ " the complete line should be displayed in the window
+ norm! $
+
+ let lines = ScreenLines([1,10], winwidth(0)+1)
+ let expect = [
+ \ "<aaa aaa a|",
+ \ "a aaa aaa |",
+ \ "aa aaa aaa|",
+ \ " aa aaa aa|",
+ \ "a aa aaa a|",
+ \ "aa aa aaa |",
+ \ "aaa aa aaa|",
+ \ " aaa aa aa|",
+ \ "a aaa aa a|",
+ \ "aa aaa aa |",
+ \ ]
+ call assert_equal(expect, lines)
+ set list& listchars& wrap&
+ bw!
+endfunc
diff --git a/src/nvim/testdir/test_exit.vim b/src/nvim/testdir/test_exit.vim
index 8f02fd29e3..99a401d4a4 100644
--- a/src/nvim/testdir/test_exit.vim
+++ b/src/nvim/testdir/test_exit.vim
@@ -3,52 +3,78 @@
source shared.vim
func Test_exiting()
- let after = [
- \ 'au QuitPre * call writefile(["QuitPre"], "Xtestout")',
- \ 'au ExitPre * call writefile(["ExitPre"], "Xtestout", "a")',
- \ 'quit',
- \ ]
+ let after =<< trim [CODE]
+ au QuitPre * call writefile(["QuitPre"], "Xtestout")
+ au ExitPre * call writefile(["ExitPre"], "Xtestout", "a")
+ quit
+ [CODE]
+
if RunVim([], after, '')
call assert_equal(['QuitPre', 'ExitPre'], readfile('Xtestout'))
endif
call delete('Xtestout')
- let after = [
- \ 'au QuitPre * call writefile(["QuitPre"], "Xtestout")',
- \ 'au ExitPre * call writefile(["ExitPre"], "Xtestout", "a")',
- \ 'help',
- \ 'wincmd w',
- \ 'quit',
- \ ]
+ let after =<< trim [CODE]
+ au QuitPre * call writefile(["QuitPre"], "Xtestout")
+ au ExitPre * call writefile(["ExitPre"], "Xtestout", "a")
+ help
+ wincmd w
+ quit
+ [CODE]
+
if RunVim([], after, '')
call assert_equal(['QuitPre', 'ExitPre'], readfile('Xtestout'))
endif
call delete('Xtestout')
- let after = [
- \ 'au QuitPre * call writefile(["QuitPre"], "Xtestout")',
- \ 'au ExitPre * call writefile(["ExitPre"], "Xtestout", "a")',
- \ 'split',
- \ 'new',
- \ 'qall',
- \ ]
+ let after =<< trim [CODE]
+ au QuitPre * call writefile(["QuitPre"], "Xtestout")
+ au ExitPre * call writefile(["ExitPre"], "Xtestout", "a")
+ split
+ new
+ qall
+ [CODE]
+
if RunVim([], after, '')
call assert_equal(['QuitPre', 'ExitPre'], readfile('Xtestout'))
endif
call delete('Xtestout')
- let after = [
- \ 'au QuitPre * call writefile(["QuitPre"], "Xtestout", "a")',
- \ 'au ExitPre * call writefile(["ExitPre"], "Xtestout", "a")',
- \ 'augroup nasty',
- \ ' au ExitPre * split',
- \ 'augroup END',
- \ 'quit',
- \ 'augroup nasty',
- \ ' au! ExitPre',
- \ 'augroup END',
- \ 'quit',
- \ ]
+ " ExitPre autocommand splits the window, so that it's no longer the last one.
+ let after =<< trim [CODE]
+ au QuitPre * call writefile(["QuitPre"], "Xtestout", "a")
+ au ExitPre * call writefile(["ExitPre"], "Xtestout", "a")
+ augroup nasty
+ au ExitPre * split
+ augroup END
+ quit
+ augroup nasty
+ au! ExitPre
+ augroup END
+ quit
+ [CODE]
+
+ if RunVim([], after, '')
+ call assert_equal(['QuitPre', 'ExitPre', 'QuitPre', 'ExitPre'],
+ \ readfile('Xtestout'))
+ endif
+ call delete('Xtestout')
+
+ " ExitPre autocommand splits and closes the window, so that there is still
+ " one window but it's a different one.
+ let after =<< trim [CODE]
+ au QuitPre * call writefile(["QuitPre"], "Xtestout", "a")
+ au ExitPre * call writefile(["ExitPre"], "Xtestout", "a")
+ augroup nasty
+ au ExitPre * split | only
+ augroup END
+ quit
+ augroup nasty
+ au! ExitPre
+ augroup END
+ quit
+ [CODE]
+
if RunVim([], after, '')
call assert_equal(['QuitPre', 'ExitPre', 'QuitPre', 'ExitPre'],
\ readfile('Xtestout'))
diff --git a/src/nvim/testdir/test_expr.vim b/src/nvim/testdir/test_expr.vim
index 4f99625e73..dd546dbf71 100644
--- a/src/nvim/testdir/test_expr.vim
+++ b/src/nvim/testdir/test_expr.vim
@@ -279,6 +279,9 @@ function Test_printf_misc()
call assert_equal('abc ', printf('%-4s', 'abc'))
call assert_equal('abc ', printf('%-4S', 'abc'))
+ call assert_equal('ðŸ', printf('%.2S', 'ðŸðŸ'))
+ call assert_equal('', printf('%.1S', 'ðŸðŸ'))
+
call assert_equal('1%', printf('%d%%', 1))
endfunc
diff --git a/src/nvim/testdir/test_filetype.vim b/src/nvim/testdir/test_filetype.vim
index 7512d599b8..e085f58e56 100644
--- a/src/nvim/testdir/test_filetype.vim
+++ b/src/nvim/testdir/test_filetype.vim
@@ -122,6 +122,7 @@ let s:filename_checks = {
\ 'cvs': ['cvs123'],
\ 'cvsrc': ['.cvsrc'],
\ 'cynpp': ['file.cyn'],
+ \ 'dart': ['file.dart', 'file.drt'],
\ 'datascript': ['file.ds'],
\ 'dcd': ['file.dcd'],
\ 'debcontrol': ['/debian/control'],
@@ -201,6 +202,7 @@ let s:filename_checks = {
\ 'hex': ['file.hex', 'file.h32'],
\ 'hgcommit': ['hg-editor-file.txt'],
\ 'hog': ['file.hog', 'snort.conf', 'vision.conf'],
+ \ 'hollywood': ['file.hws'],
\ 'hostconf': ['/etc/host.conf'],
\ 'hostsaccess': ['/etc/hosts.allow', '/etc/hosts.deny'],
\ 'template': ['file.tmpl'],
@@ -221,7 +223,7 @@ let s:filename_checks = {
\ 'jam': ['file.jpl', 'file.jpr'],
\ 'java': ['file.java', 'file.jav'],
\ 'javacc': ['file.jj', 'file.jjt'],
- \ 'javascript': ['file.js', 'file.javascript', 'file.es', 'file.mjs'],
+ \ 'javascript': ['file.js', 'file.javascript', 'file.es', 'file.mjs', 'file.cjs'],
\ 'javascriptreact': ['file.jsx'],
\ 'jess': ['file.clp'],
\ 'jgraph': ['file.jgr'],
@@ -273,6 +275,7 @@ let s:filename_checks = {
\ 'mason': ['file.mason', 'file.mhtml', 'file.comp'],
\ 'master': ['file.mas', 'file.master'],
\ 'mel': ['file.mel'],
+ \ 'meson': ['meson.build', 'meson_options.txt'],
\ 'messages': ['/log/auth', '/log/cron', '/log/daemon', '/log/debug', '/log/kern', '/log/lpr', '/log/mail', '/log/messages', '/log/news/news', '/log/syslog', '/log/user',
\ '/log/auth.log', '/log/cron.log', '/log/daemon.log', '/log/debug.log', '/log/kern.log', '/log/lpr.log', '/log/mail.log', '/log/messages.log', '/log/news/news.log', '/log/syslog.log', '/log/user.log',
\ '/log/auth.err', '/log/cron.err', '/log/daemon.err', '/log/debug.err', '/log/kern.err', '/log/lpr.err', '/log/mail.err', '/log/messages.err', '/log/news/news.err', '/log/syslog.err', '/log/user.err',
diff --git a/src/nvim/testdir/test_fnamemodify.vim b/src/nvim/testdir/test_fnamemodify.vim
new file mode 100644
index 0000000000..116d23ba88
--- /dev/null
+++ b/src/nvim/testdir/test_fnamemodify.vim
@@ -0,0 +1,75 @@
+" Test filename modifiers.
+
+func Test_fnamemodify()
+ let save_home = $HOME
+ let save_shell = &shell
+ let $HOME = fnamemodify('.', ':p:h:h')
+ set shell=sh
+
+ call assert_equal('/', fnamemodify('.', ':p')[-1:])
+ call assert_equal('r', fnamemodify('.', ':p:h')[-1:])
+ call assert_equal('t', fnamemodify('test.out', ':p')[-1:])
+ call assert_equal('test.out', fnamemodify('test.out', ':.'))
+ call assert_equal('a', fnamemodify('../testdir/a', ':.'))
+ call assert_equal('~/testdir/test.out', fnamemodify('test.out', ':~'))
+ call assert_equal('~/testdir/a', fnamemodify('../testdir/a', ':~'))
+ call assert_equal('a', fnamemodify('../testdir/a', ':t'))
+ call assert_equal('', fnamemodify('.', ':p:t'))
+ call assert_equal('test.out', fnamemodify('test.out', ':p:t'))
+ call assert_equal('out', fnamemodify('test.out', ':p:e'))
+ call assert_equal('out', fnamemodify('test.out', ':p:t:e'))
+ call assert_equal('abc.fb2.tar', fnamemodify('abc.fb2.tar.gz', ':r'))
+ call assert_equal('abc.fb2', fnamemodify('abc.fb2.tar.gz', ':r:r'))
+ call assert_equal('abc', fnamemodify('abc.fb2.tar.gz', ':r:r:r'))
+ call assert_equal('testdir/abc.fb2', substitute(fnamemodify('abc.fb2.tar.gz', ':p:r:r'), '.*\(testdir/.*\)', '\1', ''))
+ call assert_equal('gz', fnamemodify('abc.fb2.tar.gz', ':e'))
+ call assert_equal('tar.gz', fnamemodify('abc.fb2.tar.gz', ':e:e'))
+ call assert_equal('fb2.tar.gz', fnamemodify('abc.fb2.tar.gz', ':e:e:e'))
+ call assert_equal('fb2.tar.gz', fnamemodify('abc.fb2.tar.gz', ':e:e:e:e'))
+ call assert_equal('tar', fnamemodify('abc.fb2.tar.gz', ':e:e:r'))
+
+ call assert_equal('''abc def''', fnamemodify('abc def', ':S'))
+ call assert_equal('''abc" "def''', fnamemodify('abc" "def', ':S'))
+ call assert_equal('''abc"%"def''', fnamemodify('abc"%"def', ':S'))
+ call assert_equal('''abc''\'''' ''\''''def''', fnamemodify('abc'' ''def', ':S'))
+ call assert_equal('''abc''\''''%''\''''def''', fnamemodify('abc''%''def', ':S'))
+ sp test_alot.vim
+ call assert_equal(expand('%:r:S'), shellescape(expand('%:r')))
+ call assert_equal('test_alot,''test_alot'',test_alot.vim', join([expand('%:r'), expand('%:r:S'), expand('%')], ','))
+ quit
+
+ call assert_equal("'abc\ndef'", fnamemodify("abc\ndef", ':S'))
+ set shell=tcsh
+ call assert_equal("'abc\\\ndef'", fnamemodify("abc\ndef", ':S'))
+
+ let $HOME = save_home
+ let &shell = save_shell
+endfunc
+
+func Test_fnamemodify_er()
+ call assert_equal("with", fnamemodify("path/to/file.with.extensions", ':e:e:r:r'))
+
+ call assert_equal('c', fnamemodify('a.c', ':e'))
+ call assert_equal('c', fnamemodify('a.c', ':e:e'))
+ call assert_equal('c', fnamemodify('a.c', ':e:e:r'))
+ call assert_equal('c', fnamemodify('a.c', ':e:e:r:r'))
+
+ call assert_equal('rb', fnamemodify('a.spec.rb', ':e:r'))
+ call assert_equal('rb', fnamemodify('a.spec.rb', ':e:r'))
+ call assert_equal('spec.rb', fnamemodify('a.spec.rb', ':e:e'))
+ call assert_equal('spec', fnamemodify('a.spec.rb', ':e:e:r'))
+ call assert_equal('spec', fnamemodify('a.spec.rb', ':e:e:r:r'))
+ call assert_equal('spec', fnamemodify('a.b.spec.rb', ':e:e:r'))
+ call assert_equal('b.spec', fnamemodify('a.b.spec.rb', ':e:e:e:r'))
+ call assert_equal('b', fnamemodify('a.b.spec.rb', ':e:e:e:r:r'))
+
+ call assert_equal('spec', fnamemodify('a.b.spec.rb', ':r:e'))
+ call assert_equal('b', fnamemodify('a.b.spec.rb', ':r:r:e'))
+
+ call assert_equal('c', fnamemodify('a.b.c.d.e', ':r:r:e'))
+ call assert_equal('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e'))
+
+ " :e never includes the whole filename, so "a.b":e:e:e --> "b"
+ call assert_equal('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e'))
+ call assert_equal('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e:e'))
+endfunc
diff --git a/src/nvim/testdir/test_fold.vim b/src/nvim/testdir/test_fold.vim
index 3cb42579be..324f3f8cf2 100644
--- a/src/nvim/testdir/test_fold.vim
+++ b/src/nvim/testdir/test_fold.vim
@@ -520,17 +520,18 @@ func Test_fold_create_marker_in_C()
set fdm=marker fdl=9
set filetype=c
- let content = [
- \ '/*',
- \ ' * comment',
- \ ' * ',
- \ ' *',
- \ ' */',
- \ 'int f(int* p) {',
- \ ' *p = 3;',
- \ ' return 0;',
- \ '}'
- \]
+ let content =<< trim [CODE]
+ /*
+ * comment
+ *
+ *
+ */
+ int f(int* p) {
+ *p = 3;
+ return 0;
+ }
+ [CODE]
+
for c in range(len(content) - 1)
bw!
call append(0, content)
@@ -756,3 +757,15 @@ func Test_fold_delete_with_marker()
bwipe!
bwipe!
endfunc
+
+func Test_fold_delete_with_marker_and_whichwrap()
+ new
+ let content1 = ['']
+ let content2 = ['folded line 1 "{{{1', ' test', ' test2', ' test3', '', 'folded line 2 "{{{1', ' test', ' test2', ' test3']
+ call setline(1, content1 + content2)
+ set fdm=marker ww+=l
+ normal! x
+ call assert_equal(content2, getline(1, '$'))
+ set fdm& ww&
+ bwipe!
+endfunc
diff --git a/src/nvim/testdir/test_functions.vim b/src/nvim/testdir/test_functions.vim
index a36c51f56f..7822507f86 100644
--- a/src/nvim/testdir/test_functions.vim
+++ b/src/nvim/testdir/test_functions.vim
@@ -1132,6 +1132,13 @@ func Test_reg_executing_and_recording()
" :normal command saves and restores reg_executing
let s:reg_stat = ''
+ let @q = ":call TestFunc()\<CR>:call s:save_reg_stat()\<CR>"
+ func TestFunc() abort
+ normal! ia
+ endfunc
+ call feedkeys("@q", 'xt')
+ call assert_equal(':q', s:reg_stat)
+ delfunc TestFunc
" getchar() command saves and restores reg_executing
map W :call TestFunc()<CR>
diff --git a/src/nvim/testdir/test_gf.vim b/src/nvim/testdir/test_gf.vim
index accd21e9a3..d301874891 100644
--- a/src/nvim/testdir/test_gf.vim
+++ b/src/nvim/testdir/test_gf.vim
@@ -99,3 +99,28 @@ func Test_gf()
call delete('Xtest1')
call delete('Xtestgf')
endfunc
+
+func Test_gf_visual()
+ call writefile([], "Xtest_gf_visual")
+ new
+ call setline(1, 'XXXtest_gf_visualXXX')
+ set hidden
+
+ " Visually select Xtest_gf_visual and use gf to go to that file
+ norm! ttvtXgf
+ call assert_equal('Xtest_gf_visual', bufname('%'))
+
+ bwipe!
+ call delete('Xtest_gf_visual')
+ set hidden&
+endfunc
+
+func Test_gf_error()
+ new
+ call assert_fails('normal gf', 'E446:')
+ call assert_fails('normal gF', 'E446:')
+ call setline(1, '/doesnotexist')
+ call assert_fails('normal gf', 'E447:')
+ call assert_fails('normal gF', 'E447:')
+ bwipe!
+endfunc
diff --git a/src/nvim/testdir/test_gn.vim b/src/nvim/testdir/test_gn.vim
index 5e74289b00..d41675be0c 100644
--- a/src/nvim/testdir/test_gn.vim
+++ b/src/nvim/testdir/test_gn.vim
@@ -129,6 +129,33 @@ func Test_gn_command()
call assert_equal([' nnoremap', '', 'match'], getline(1,'$'))
sil! %d_
+ " make sure it works correctly for one-char wide search items
+ call setline('.', ['abcdefghi'])
+ let @/ = 'a'
+ exe "norm! 0fhvhhgNgU"
+ call assert_equal(['ABCDEFGHi'], getline(1,'$'))
+ call setline('.', ['abcdefghi'])
+ let @/ = 'b'
+ " this gn wraps around the end of the file
+ exe "norm! 0fhvhhgngU"
+ call assert_equal(['aBCDEFGHi'], getline(1,'$'))
+ sil! %d _
+ call setline('.', ['abcdefghi'])
+ let @/ = 'f'
+ exe "norm! 0vllgngU"
+ call assert_equal(['ABCDEFghi'], getline(1,'$'))
+ sil! %d _
+ call setline('.', ['12345678'])
+ let @/ = '5'
+ norm! gg0f7vhhhhgnd
+ call assert_equal(['12348'], getline(1,'$'))
+ sil! %d _
+ call setline('.', ['12345678'])
+ let @/ = '5'
+ norm! gg0f2vf7gNd
+ call assert_equal(['1678'], getline(1,'$'))
+ sil! %d _
+
set wrapscan&vim
set belloff&vim
endfu
diff --git a/src/nvim/testdir/test_goto.vim b/src/nvim/testdir/test_goto.vim
index c0235b1707..19513b315a 100644
--- a/src/nvim/testdir/test_goto.vim
+++ b/src/nvim/testdir/test_goto.vim
@@ -15,262 +15,283 @@ func XTest_goto_decl(cmd, lines, line, col)
endfunc
func Test_gD()
- let lines = [
- \ 'int x;',
- \ '',
- \ 'int func(void)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int x;
+
+ int func(void)
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gD', lines, 1, 5)
endfunc
func Test_gD_too()
- let lines = [
- \ 'Filename x;',
- \ '',
- \ 'int Filename',
- \ 'int func() {',
- \ ' Filename x;',
- \ ' return x;',
- \ ]
+ let lines =<< trim [CODE]
+ Filename x;
+
+ int Filename
+ int func() {
+ Filename x;
+ return x;
+ [CODE]
+
call XTest_goto_decl('gD', lines, 1, 10)
endfunc
func Test_gD_comment()
- let lines = [
- \ '/* int x; */',
- \ 'int x;',
- \ '',
- \ 'int func(void)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ /* int x; */
+ int x;
+
+ int func(void)
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gD', lines, 2, 5)
endfunc
func Test_gD_inline_comment()
- let lines = [
- \ 'int y /* , x */;',
- \ 'int x;',
- \ '',
- \ 'int func(void)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int y /* , x */;
+ int x;
+
+ int func(void)
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gD', lines, 2, 5)
endfunc
func Test_gD_string()
- let lines = [
- \ 'char *s[] = "x";',
- \ 'int x = 1;',
- \ '',
- \ 'int func(void)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ char *s[] = "x";
+ int x = 1;
+
+ int func(void)
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gD', lines, 2, 5)
endfunc
func Test_gD_string_same_line()
- let lines = [
- \ 'char *s[] = "x", int x = 1;',
- \ '',
- \ 'int func(void)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ char *s[] = "x", int x = 1;
+
+ int func(void)
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gD', lines, 1, 22)
endfunc
func Test_gD_char()
- let lines = [
- \ "char c = 'x';",
- \ 'int x = 1;',
- \ '',
- \ 'int func(void)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ char c = 'x';
+ int x = 1;
+
+ int func(void)
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gD', lines, 2, 5)
endfunc
func Test_gd()
- let lines = [
- \ 'int x;',
- \ '',
- \ 'int func(int x)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int x;
+
+ int func(int x)
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 3, 14)
endfunc
func Test_gd_not_local()
- let lines = [
- \ 'int func1(void)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ '',
- \ 'int func2(int x)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int func1(void)
+ {
+ return x;
+ }
+
+ int func2(int x)
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 3, 10)
endfunc
func Test_gd_kr_style()
- let lines = [
- \ 'int func(x)',
- \ ' int x;',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int func(x)
+ int x;
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 2, 7)
endfunc
func Test_gd_missing_braces()
- let lines = [
- \ 'def func1(a)',
- \ ' a + 1',
- \ 'end',
- \ '',
- \ 'a = 1',
- \ '',
- \ 'def func2()',
- \ ' return a',
- \ 'end',
- \ ]
+ let lines =<< trim [CODE]
+ def func1(a)
+ a + 1
+ end
+
+ a = 1
+
+ def func2()
+ return a
+ end
+ [CODE]
+
call XTest_goto_decl('gd', lines, 1, 11)
endfunc
func Test_gd_comment()
- let lines = [
- \ 'int func(void)',
- \ '{',
- \ ' /* int x; */',
- \ ' int x;',
- \ ' return x;',
- \ '}',
- \]
+ let lines =<< trim [CODE]
+ int func(void)
+ {
+ /* int x; */
+ int x;
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 4, 7)
endfunc
func Test_gd_comment_in_string()
- let lines = [
- \ 'int func(void)',
- \ '{',
- \ ' char *s ="//"; int x;',
- \ ' int x;',
- \ ' return x;',
- \ '}',
- \]
+ let lines =<< trim [CODE]
+ int func(void)
+ {
+ char *s ="//"; int x;
+ int x;
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 3, 22)
endfunc
func Test_gd_string_in_comment()
set comments=
- let lines = [
- \ 'int func(void)',
- \ '{',
- \ ' /* " */ int x;',
- \ ' int x;',
- \ ' return x;',
- \ '}',
- \]
+ let lines =<< trim [CODE]
+ int func(void)
+ {
+ /* " */ int x;
+ int x;
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 3, 15)
set comments&
endfunc
func Test_gd_inline_comment()
- let lines = [
- \ 'int func(/* x is an int */ int x)',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int func(/* x is an int */ int x)
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 1, 32)
endfunc
func Test_gd_inline_comment_only()
- let lines = [
- \ 'int func(void) /* one lonely x */',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int func(void) /* one lonely x */
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 3, 10)
endfunc
func Test_gd_inline_comment_body()
- let lines = [
- \ 'int func(void)',
- \ '{',
- \ ' int y /* , x */;',
- \ '',
- \ ' for (/* int x = 0 */; y < 2; y++);',
- \ '',
- \ ' int x = 0;',
- \ '',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int func(void)
+ {
+ int y /* , x */;
+
+ for (/* int x = 0 */; y < 2; y++);
+
+ int x = 0;
+
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 7, 7)
endfunc
func Test_gd_trailing_multiline_comment()
- let lines = [
- \ 'int func(int x) /* x is an int */',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int func(int x) /* x is an int */
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 1, 14)
endfunc
func Test_gd_trailing_comment()
- let lines = [
- \ 'int func(int x) // x is an int',
- \ '{',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int func(int x) // x is an int
+ {
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 1, 14)
endfunc
func Test_gd_string()
- let lines = [
- \ 'int func(void)',
- \ '{',
- \ ' char *s = "x";',
- \ ' int x = 1;',
- \ '',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int func(void)
+ {
+ char *s = "x";
+ int x = 1;
+
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 4, 7)
endfunc
func Test_gd_string_only()
- let lines = [
- \ 'int func(void)',
- \ '{',
- \ ' char *s = "x";',
- \ '',
- \ ' return x;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int func(void)
+ {
+ char *s = "x";
+
+ return x;
+ }
+ [CODE]
+
call XTest_goto_decl('gd', lines, 5, 10)
endfunc
@@ -289,24 +310,25 @@ func Test_cursorline_keep_col()
endfunc
func Test_gd_local_block()
- let lines = [
- \ ' int main()',
- \ '{',
- \ ' char *a = "NOT NULL";',
- \ ' if(a)',
- \ ' {',
- \ ' char *b = a;',
- \ ' printf("%s\n", b);',
- \ ' }',
- \ ' else',
- \ ' {',
- \ ' char *b = "NULL";',
- \ ' return b;',
- \ ' }',
- \ '',
- \ ' return 0;',
- \ '}',
- \ ]
+ let lines =<< trim [CODE]
+ int main()
+ {
+ char *a = "NOT NULL";
+ if(a)
+ {
+ char *b = a;
+ printf("%s\n", b);
+ }
+ else
+ {
+ char *b = "NULL";
+ return b;
+ }
+
+ return 0;
+ }
+ [CODE]
+
call XTest_goto_decl('1gd', lines, 11, 11)
endfunc
diff --git a/src/nvim/testdir/test_help.vim b/src/nvim/testdir/test_help.vim
index ed3181564c..01fb9917e9 100644
--- a/src/nvim/testdir/test_help.vim
+++ b/src/nvim/testdir/test_help.vim
@@ -21,6 +21,12 @@ func Test_help_errors()
bwipe!
endfunc
+func Test_help_expr()
+ help expr-!~?
+ call assert_equal('eval.txt', expand('%:t'))
+ close
+endfunc
+
func Test_help_keyword()
new
set keywordprg=:help
diff --git a/src/nvim/testdir/test_ins_complete.vim b/src/nvim/testdir/test_ins_complete.vim
index 7f52481ba8..52ec281d82 100644
--- a/src/nvim/testdir/test_ins_complete.vim
+++ b/src/nvim/testdir/test_ins_complete.vim
@@ -285,3 +285,21 @@ func Test_compl_feedkeys()
bwipe!
set completeopt&
endfunc
+
+func Test_compl_in_cmdwin()
+ set wildmenu wildchar=<Tab>
+ com! -nargs=1 -complete=command GetInput let input = <q-args>
+ com! -buffer TestCommand echo 'TestCommand'
+
+ let input = ''
+ call feedkeys("q:iGetInput T\<C-x>\<C-v>\<CR>", 'tx!')
+ call assert_equal('TestCommand', input)
+
+ let input = ''
+ call feedkeys("q::GetInput T\<Tab>\<CR>:q\<CR>", 'tx!')
+ call assert_equal('T', input)
+
+ delcom TestCommand
+ delcom GetInput
+ set wildmenu& wildchar&
+endfunc
diff --git a/src/nvim/testdir/test_join.vim b/src/nvim/testdir/test_join.vim
index 1c97414164..ecb55c9af6 100644
--- a/src/nvim/testdir/test_join.vim
+++ b/src/nvim/testdir/test_join.vim
@@ -9,6 +9,27 @@ func Test_join_with_count()
call setline(1, ['one', 'two', 'three', 'four'])
normal 10J
call assert_equal('one two three four', getline(1))
+
+ call setline(1, ['one', '', 'two'])
+ normal J
+ call assert_equal('one', getline(1))
+
+ call setline(1, ['one', ' ', 'two'])
+ normal J
+ call assert_equal('one', getline(1))
+
+ call setline(1, ['one', '', '', 'two'])
+ normal JJ
+ call assert_equal('one', getline(1))
+
+ call setline(1, ['one', ' ', ' ', 'two'])
+ normal JJ
+ call assert_equal('one', getline(1))
+
+ call setline(1, ['one', '', '', 'two'])
+ normal 2J
+ call assert_equal('one', getline(1))
+
quit!
endfunc
diff --git a/src/nvim/testdir/test_let.vim b/src/nvim/testdir/test_let.vim
index 8a6f1bc320..0b9331ee38 100644
--- a/src/nvim/testdir/test_let.vim
+++ b/src/nvim/testdir/test_let.vim
@@ -24,6 +24,10 @@ func Test_let()
let out = execute('let a {0 == 1 ? "a" : "b"}')
let s = "\na #1\nb #2"
call assert_equal(s, out)
+
+ let x = 0
+ if 0 | let x = 1 | endif
+ call assert_equal(0, x)
endfunc
func s:set_arg1(a) abort
@@ -140,3 +144,161 @@ func Test_let_varg_fail()
call assert_fails('call s:set_varg7(1)', 'E742:')
call s:set_varg8([0])
endfunction
+
+func Test_let_utf8_environment()
+ let $a = 'ĀĒĪŌŪã‚ã„ã†ãˆãŠ'
+ call assert_equal('ĀĒĪŌŪã‚ã„ã†ãˆãŠ', $a)
+endfunc
+
+func Test_let_heredoc_fails()
+ call assert_fails('let v =<< marker', 'E991:')
+
+ let text =<< trim END
+ func WrongSyntax()
+ let v =<< that there
+ endfunc
+ END
+ call writefile(text, 'XheredocFail')
+ call assert_fails('source XheredocFail', 'E126:')
+ call delete('XheredocFail')
+
+ let text =<< trim CodeEnd
+ func MissingEnd()
+ let v =<< END
+ endfunc
+ CodeEnd
+ call writefile(text, 'XheredocWrong')
+ call assert_fails('source XheredocWrong', 'E126:')
+ call delete('XheredocWrong')
+
+ let text =<< trim TEXTend
+ let v =<< " comment
+ TEXTend
+ call writefile(text, 'XheredocNoMarker')
+ call assert_fails('source XheredocNoMarker', 'E172:')
+ call delete('XheredocNoMarker')
+
+ let text =<< trim TEXTend
+ let v =<< text
+ TEXTend
+ call writefile(text, 'XheredocBadMarker')
+ call assert_fails('source XheredocBadMarker', 'E221:')
+ call delete('XheredocBadMarker')
+endfunc
+
+func Test_let_heredoc_trim_no_indent_marker()
+ let text =<< trim END
+ Text
+ with
+ indent
+END
+ call assert_equal(['Text', 'with', 'indent'], text)
+endfunc
+
+" Test for the setting a variable using the heredoc syntax
+func Test_let_heredoc()
+ let var1 =<< END
+Some sample text
+ Text with indent
+ !@#$%^&*()-+_={}|[]\~`:";'<>?,./
+END
+
+ call assert_equal(["Some sample text", "\tText with indent", " !@#$%^&*()-+_={}|[]\\~`:\";'<>?,./"], var1)
+
+ let var2 =<< XXX
+Editor
+XXX
+ call assert_equal(['Editor'], var2)
+
+ let var3 =<<END
+END
+ call assert_equal([], var3)
+
+ let var3 =<<END
+vim
+
+end
+ END
+END
+END
+ call assert_equal(['vim', '', 'end', ' END', 'END '], var3)
+
+ let var1 =<< trim END
+ Line1
+ Line2
+ Line3
+ END
+ END
+ call assert_equal(['Line1', ' Line2', "\tLine3", ' END'], var1)
+
+ let var1 =<< trim !!!
+ Line1
+ line2
+ Line3
+ !!!
+ !!!
+ call assert_equal(['Line1', ' line2', "\tLine3", '!!!',], var1)
+
+ let var1 =<< trim XX
+ Line1
+ XX
+ call assert_equal(['Line1'], var1)
+
+ " ignore "endfunc"
+ let var1 =<< END
+something
+endfunc
+END
+ call assert_equal(['something', 'endfunc'], var1)
+
+ " ignore "endfunc" with trim
+ let var1 =<< trim END
+ something
+ endfunc
+ END
+ call assert_equal(['something', 'endfunc'], var1)
+
+ " ignore "python << xx"
+ let var1 =<<END
+something
+python << xx
+END
+ call assert_equal(['something', 'python << xx'], var1)
+
+ " ignore "python << xx" with trim
+ let var1 =<< trim END
+ something
+ python << xx
+ END
+ call assert_equal(['something', 'python << xx'], var1)
+
+ " ignore "append"
+ let var1 =<< E
+something
+app
+E
+ call assert_equal(['something', 'app'], var1)
+
+ " ignore "append" with trim
+ let var1 =<< trim END
+ something
+ app
+ END
+ call assert_equal(['something', 'app'], var1)
+
+ let check = []
+ if 0
+ let check =<< trim END
+ from heredoc
+ END
+ endif
+ call assert_equal([], check)
+
+ " unpack assignment
+ let [a, b, c] =<< END
+ x
+ \y
+ z
+END
+ call assert_equal([' x', ' \y', ' z'], [a, b, c])
+endfunc
diff --git a/src/nvim/testdir/test_mksession_utf8.vim b/src/nvim/testdir/test_mksession_utf8.vim
index 67af3a9ca2..722fd28beb 100644
--- a/src/nvim/testdir/test_mksession_utf8.vim
+++ b/src/nvim/testdir/test_mksession_utf8.vim
@@ -65,34 +65,35 @@ func Test_mksession_utf8()
call wincol()
mksession! test_mks.out
let li = filter(readfile('test_mks.out'), 'v:val =~# "\\(^ *normal! 0\\|^ *exe ''normal!\\)"')
- let expected = [
- \ 'normal! 016|',
- \ 'normal! 016|',
- \ 'normal! 016|',
- \ 'normal! 08|',
- \ 'normal! 08|',
- \ 'normal! 016|',
- \ 'normal! 016|',
- \ 'normal! 016|',
- \ " exe 'normal! ' . s:c . '|zs' . 16 . '|'",
- \ " normal! 016|",
- \ " exe 'normal! ' . s:c . '|zs' . 16 . '|'",
- \ " normal! 016|",
- \ " exe 'normal! ' . s:c . '|zs' . 16 . '|'",
- \ " normal! 016|",
- \ " exe 'normal! ' . s:c . '|zs' . 8 . '|'",
- \ " normal! 08|",
- \ " exe 'normal! ' . s:c . '|zs' . 8 . '|'",
- \ " normal! 08|",
- \ " exe 'normal! ' . s:c . '|zs' . 16 . '|'",
- \ " normal! 016|",
- \ " exe 'normal! ' . s:c . '|zs' . 16 . '|'",
- \ " normal! 016|",
- \ " exe 'normal! ' . s:c . '|zs' . 16 . '|'",
- \ " normal! 016|",
- \ " exe 'normal! ' . s:c . '|zs' . 16 . '|'",
- \ " normal! 016|"
- \ ]
+ let expected =<< trim [DATA]
+ normal! 016|
+ normal! 016|
+ normal! 016|
+ normal! 08|
+ normal! 08|
+ normal! 016|
+ normal! 016|
+ normal! 016|
+ exe 'normal! ' . s:c . '|zs' . 16 . '|'
+ normal! 016|
+ exe 'normal! ' . s:c . '|zs' . 16 . '|'
+ normal! 016|
+ exe 'normal! ' . s:c . '|zs' . 16 . '|'
+ normal! 016|
+ exe 'normal! ' . s:c . '|zs' . 8 . '|'
+ normal! 08|
+ exe 'normal! ' . s:c . '|zs' . 8 . '|'
+ normal! 08|
+ exe 'normal! ' . s:c . '|zs' . 16 . '|'
+ normal! 016|
+ exe 'normal! ' . s:c . '|zs' . 16 . '|'
+ normal! 016|
+ exe 'normal! ' . s:c . '|zs' . 16 . '|'
+ normal! 016|
+ exe 'normal! ' . s:c . '|zs' . 16 . '|'
+ normal! 016|
+ [DATA]
+
call assert_equal(expected, li)
tabclose!
diff --git a/src/nvim/testdir/test_normal.vim b/src/nvim/testdir/test_normal.vim
index 532beb9c39..ad6d325510 100644
--- a/src/nvim/testdir/test_normal.vim
+++ b/src/nvim/testdir/test_normal.vim
@@ -2,12 +2,12 @@
source shared.vim
-func! Setup_NewWindow()
+func Setup_NewWindow()
10new
call setline(1, range(1,100))
endfunc
-func! MyFormatExpr()
+func MyFormatExpr()
" Adds '->$' at lines having numbers followed by trailing whitespace
for ln in range(v:lnum, v:lnum+v:count-1)
let line = getline(ln)
@@ -17,7 +17,7 @@ func! MyFormatExpr()
endfor
endfunc
-func! CountSpaces(type, ...)
+func CountSpaces(type, ...)
" for testing operatorfunc
" will count the number of spaces
" and return the result in g:a
@@ -37,7 +37,7 @@ func! CountSpaces(type, ...)
let @@ = reg_save
endfunc
-func! OpfuncDummy(type, ...)
+func OpfuncDummy(type, ...)
" for testing operatorfunc
let g:opt=&linebreak
@@ -81,7 +81,7 @@ fun! Test_normal00_optrans()
bw!
endfunc
-func! Test_normal01_keymodel()
+func Test_normal01_keymodel()
call Setup_NewWindow()
" Test 1: depending on 'keymodel' <s-down> does something different
50
@@ -115,7 +115,7 @@ func! Test_normal01_keymodel()
bw!
endfunc
-func! Test_normal02_selectmode()
+func Test_normal02_selectmode()
" some basic select mode tests
call Setup_NewWindow()
50
@@ -129,7 +129,7 @@ func! Test_normal02_selectmode()
bw!
endfunc
-func! Test_normal02_selectmode2()
+func Test_normal02_selectmode2()
" some basic select mode tests
call Setup_NewWindow()
50
@@ -139,7 +139,7 @@ func! Test_normal02_selectmode2()
bw!
endfunc
-func! Test_normal03_join()
+func Test_normal03_join()
" basic join test
call Setup_NewWindow()
50
@@ -159,7 +159,7 @@ func! Test_normal03_join()
bw!
endfunc
-func! Test_normal04_filter()
+func Test_normal04_filter()
" basic filter test
" only test on non windows platform
if has('win32')
@@ -185,7 +185,7 @@ func! Test_normal04_filter()
bw!
endfunc
-func! Test_normal05_formatexpr()
+func Test_normal05_formatexpr()
" basic formatexpr test
call Setup_NewWindow()
%d_
@@ -222,7 +222,7 @@ func Test_normal05_formatexpr_setopt()
set formatexpr=
endfunc
-func! Test_normal06_formatprg()
+func Test_normal06_formatprg()
" basic test for formatprg
" only test on non windows platform
if has('win32')
@@ -256,7 +256,7 @@ func! Test_normal06_formatprg()
call delete('Xsed_format.sh')
endfunc
-func! Test_normal07_internalfmt()
+func Test_normal07_internalfmt()
" basic test for internal formmatter to textwidth of 12
let list=range(1,11)
call map(list, 'v:val." "')
@@ -270,7 +270,7 @@ func! Test_normal07_internalfmt()
bw!
endfunc
-func! Test_normal08_fold()
+func Test_normal08_fold()
" basic tests for foldopen/folddelete
if !has("folding")
return
@@ -329,7 +329,7 @@ func! Test_normal08_fold()
bw!
endfunc
-func! Test_normal09_operatorfunc()
+func Test_normal09_operatorfunc()
" Test operatorfunc
call Setup_NewWindow()
" Add some spaces for counting
@@ -359,7 +359,7 @@ func! Test_normal09_operatorfunc()
bw!
endfunc
-func! Test_normal09a_operatorfunc()
+func Test_normal09a_operatorfunc()
" Test operatorfunc
call Setup_NewWindow()
" Add some spaces for counting
@@ -385,7 +385,7 @@ func! Test_normal09a_operatorfunc()
unlet! g:opt
endfunc
-func! Test_normal10_expand()
+func Test_normal10_expand()
" Test for expand()
10new
call setline(1, ['1', 'ifooar,,cbar'])
@@ -420,7 +420,7 @@ func! Test_normal10_expand()
bw!
endfunc
-func! Test_normal11_showcmd()
+func Test_normal11_showcmd()
" test for 'showcmd'
10new
exe "norm! ofoobar\<esc>"
@@ -435,7 +435,7 @@ func! Test_normal11_showcmd()
bw!
endfunc
-func! Test_normal12_nv_error()
+func Test_normal12_nv_error()
" Test for nv_error
10new
call setline(1, range(1,5))
@@ -445,7 +445,7 @@ func! Test_normal12_nv_error()
bw!
endfunc
-func! Test_normal13_help()
+func Test_normal13_help()
" Test for F1
call assert_equal(1, winnr())
call feedkeys("\<f1>", 'txi')
@@ -454,7 +454,7 @@ func! Test_normal13_help()
bw!
endfunc
-func! Test_normal14_page()
+func Test_normal14_page()
" basic test for Ctrl-F and Ctrl-B
call Setup_NewWindow()
exe "norm! \<c-f>"
@@ -488,7 +488,7 @@ func! Test_normal14_page()
bw!
endfunc
-func! Test_normal14_page_eol()
+func Test_normal14_page_eol()
10new
norm oxxxxxxx
exe "norm 2\<c-f>"
@@ -497,7 +497,7 @@ func! Test_normal14_page_eol()
bw!
endfunc
-func! Test_normal15_z_scroll_vert()
+func Test_normal15_z_scroll_vert()
" basic test for z commands that scroll the window
call Setup_NewWindow()
100
@@ -586,7 +586,7 @@ func! Test_normal15_z_scroll_vert()
bw!
endfunc
-func! Test_normal16_z_scroll_hor()
+func Test_normal16_z_scroll_hor()
" basic test for z commands that scroll the window
10new
15vsp
@@ -652,7 +652,7 @@ func! Test_normal16_z_scroll_hor()
bw!
endfunc
-func! Test_normal17_z_scroll_hor2()
+func Test_normal17_z_scroll_hor2()
" basic test for z commands that scroll the window
" using 'sidescrolloff' setting
10new
@@ -719,7 +719,7 @@ func! Test_normal17_z_scroll_hor2()
bw!
endfunc
-func! Test_normal18_z_fold()
+func Test_normal18_z_fold()
" basic tests for foldopen/folddelete
if !has("folding")
return
@@ -1090,7 +1090,7 @@ func! Test_normal18_z_fold()
bw!
endfunc
-func! Test_normal19_z_spell()
+func Test_normal19_z_spell()
if !has("spell") || !has('syntax')
return
endif
@@ -1245,7 +1245,7 @@ func! Test_normal19_z_spell()
bw!
endfunc
-func! Test_normal20_exmode()
+func Test_normal20_exmode()
if !has("unix")
" Reading from redirected file doesn't work on MS-Windows
return
@@ -1263,24 +1263,38 @@ func! Test_normal20_exmode()
bw!
endfunc
-func! Test_normal21_nv_hat()
- set hidden
- new
- " to many buffers opened already, will not work
- "call assert_fails(":b#", 'E23')
- "call assert_equal('', @#)
- e Xfoobar
- e Xfile2
- call feedkeys("\<c-^>", 't')
- call assert_equal("Xfile2", fnamemodify(bufname('%'), ':t'))
- call feedkeys("f\<c-^>", 't')
- call assert_equal("Xfile2", fnamemodify(bufname('%'), ':t'))
- " clean up
- set nohidden
- bw!
+func Test_normal21_nv_hat()
+
+ " Edit a fresh file and wipe the buffer list so that there is no alternate
+ " file present. Next, check for the expected command failures.
+ edit Xfoo | %bw
+ call assert_fails(':buffer #', 'E86')
+ call assert_fails(':execute "normal! \<C-^>"', 'E23')
+
+ " Test for the expected behavior when switching between two named buffers.
+ edit Xfoo | edit Xbar
+ call feedkeys("\<C-^>", 'tx')
+ call assert_equal('Xfoo', fnamemodify(bufname('%'), ':t'))
+ call feedkeys("\<C-^>", 'tx')
+ call assert_equal('Xbar', fnamemodify(bufname('%'), ':t'))
+
+ " Test for the expected behavior when only one buffer is named.
+ enew | let l:nr = bufnr('%')
+ call feedkeys("\<C-^>", 'tx')
+ call assert_equal('Xbar', fnamemodify(bufname('%'), ':t'))
+ call feedkeys("\<C-^>", 'tx')
+ call assert_equal('', bufname('%'))
+ call assert_equal(l:nr, bufnr('%'))
+
+ " Test that no action is taken by "<C-^>" when an operator is pending.
+ edit Xfoo
+ call feedkeys("ci\<C-^>", 'tx')
+ call assert_equal('Xfoo', fnamemodify(bufname('%'), ':t'))
+
+ %bw!
endfunc
-func! Test_normal22_zet()
+func Test_normal22_zet()
" Test for ZZ
" let shell = &shell
" let &shell = 'sh'
@@ -1308,7 +1322,7 @@ func! Test_normal22_zet()
" let &shell = shell
endfunc
-func! Test_normal23_K()
+func Test_normal23_K()
" Test for K command
new
call append(0, ['helphelp.txt', 'man', 'aa%bb', 'cc|dd'])
@@ -1353,8 +1367,9 @@ func! Test_normal23_K()
return
endif
- if has('mac')
- " In MacOS, the option for specifying a pager is different
+ let not_gnu_man = has('mac') || has('bsd')
+ if not_gnu_man
+ " In MacOS and BSD, the option for specifying a pager is different
set keywordprg=man\ -P\ cat
else
set keywordprg=man\ --pager=cat
@@ -1362,7 +1377,7 @@ func! Test_normal23_K()
" Test for using man
2
let a = execute('unsilent norm! K')
- if has('mac')
+ if not_gnu_man
call assert_match("man -P cat 'man'", a)
else
call assert_match("man --pager=cat 'man'", a)
@@ -1373,7 +1388,7 @@ func! Test_normal23_K()
bw!
endfunc
-func! Test_normal24_rot13()
+func Test_normal24_rot13()
" Testing for g?? g?g?
new
call append(0, 'abcdefghijklmnopqrstuvwxyzäüö')
@@ -1387,7 +1402,7 @@ func! Test_normal24_rot13()
bw!
endfunc
-func! Test_normal25_tag()
+func Test_normal25_tag()
" Testing for CTRL-] g CTRL-] g]
" CTRL-W g] CTRL-W CTRL-] CTRL-W g CTRL-]
h
@@ -1454,7 +1469,7 @@ func! Test_normal25_tag()
helpclose
endfunc
-func! Test_normal26_put()
+func Test_normal26_put()
" Test for ]p ]P [p and [P
new
call append(0, ['while read LINE', 'do', ' ((count++))', ' if [ $? -ne 0 ]; then', " echo 'Error writing file'", ' fi', 'done'])
@@ -1473,7 +1488,7 @@ func! Test_normal26_put()
bw!
endfunc
-func! Test_normal27_bracket()
+func Test_normal27_bracket()
" Test for [' [` ]' ]`
call Setup_NewWindow()
1,21s/.\+/ & b/
@@ -1524,7 +1539,7 @@ func! Test_normal27_bracket()
bw!
endfunc
-func! Test_normal28_parenthesis()
+func Test_normal28_parenthesis()
" basic testing for ( and )
new
call append(0, ['This is a test. With some sentences!', '', 'Even with a question? And one more. And no sentence here'])
@@ -1549,52 +1564,94 @@ endfunc
fun! Test_normal29_brace()
" basic test for { and } movements
- let text= ['A paragraph begins after each empty line, and also at each of a set of',
- \ 'paragraph macros, specified by the pairs of characters in the ''paragraphs''',
- \ 'option. The default is "IPLPPPQPP TPHPLIPpLpItpplpipbp", which corresponds to',
- \ 'the macros ".IP", ".LP", etc. (These are nroff macros, so the dot must be in',
- \ 'the first column). A section boundary is also a paragraph boundary.',
- \ 'Note that a blank line (only containing white space) is NOT a paragraph',
- \ 'boundary.',
- \ '',
- \ '',
- \ 'Also note that this does not include a ''{'' or ''}'' in the first column. When',
- \ 'the ''{'' flag is in ''cpoptions'' then ''{'' in the first column is used as a',
- \ 'paragraph boundary |posix|.',
- \ '{',
- \ 'This is no paragraph',
- \ 'unless the ''{'' is set',
- \ 'in ''cpoptions''',
- \ '}',
- \ '.IP',
- \ 'The nroff macros IP separates a paragraph',
- \ 'That means, it must be a ''.''',
- \ 'followed by IP',
- \ '.LPIt does not matter, if afterwards some',
- \ 'more characters follow.',
- \ '.SHAlso section boundaries from the nroff',
- \ 'macros terminate a paragraph. That means',
- \ 'a character like this:',
- \ '.NH',
- \ 'End of text here']
+ let text =<< trim [DATA]
+ A paragraph begins after each empty line, and also at each of a set of
+ paragraph macros, specified by the pairs of characters in the 'paragraphs'
+ option. The default is "IPLPPPQPP TPHPLIPpLpItpplpipbp", which corresponds to
+ the macros ".IP", ".LP", etc. (These are nroff macros, so the dot must be in
+ the first column). A section boundary is also a paragraph boundary.
+ Note that a blank line (only containing white space) is NOT a paragraph
+ boundary.
+
+
+ Also note that this does not include a '{' or '}' in the first column. When
+ the '{' flag is in 'cpoptions' then '{' in the first column is used as a
+ paragraph boundary |posix|.
+ {
+ This is no paragraph
+ unless the '{' is set
+ in 'cpoptions'
+ }
+ .IP
+ The nroff macros IP separates a paragraph
+ That means, it must be a '.'
+ followed by IP
+ .LPIt does not matter, if afterwards some
+ more characters follow.
+ .SHAlso section boundaries from the nroff
+ macros terminate a paragraph. That means
+ a character like this:
+ .NH
+ End of text here
+ [DATA]
+
new
call append(0, text)
1
norm! 0d2}
- call assert_equal(['.IP',
- \ 'The nroff macros IP separates a paragraph', 'That means, it must be a ''.''', 'followed by IP',
- \ '.LPIt does not matter, if afterwards some', 'more characters follow.', '.SHAlso section boundaries from the nroff',
- \ 'macros terminate a paragraph. That means', 'a character like this:', '.NH', 'End of text here', ''], getline(1,'$'))
+
+ let expected =<< trim [DATA]
+ .IP
+ The nroff macros IP separates a paragraph
+ That means, it must be a '.'
+ followed by IP
+ .LPIt does not matter, if afterwards some
+ more characters follow.
+ .SHAlso section boundaries from the nroff
+ macros terminate a paragraph. That means
+ a character like this:
+ .NH
+ End of text here
+
+ [DATA]
+ call assert_equal(expected, getline(1, '$'))
+
norm! 0d}
- call assert_equal(['.LPIt does not matter, if afterwards some', 'more characters follow.',
- \ '.SHAlso section boundaries from the nroff', 'macros terminate a paragraph. That means',
- \ 'a character like this:', '.NH', 'End of text here', ''], getline(1, '$'))
+
+ let expected =<< trim [DATA]
+ .LPIt does not matter, if afterwards some
+ more characters follow.
+ .SHAlso section boundaries from the nroff
+ macros terminate a paragraph. That means
+ a character like this:
+ .NH
+ End of text here
+
+ [DATA]
+ call assert_equal(expected, getline(1, '$'))
+
$
norm! d{
- call assert_equal(['.LPIt does not matter, if afterwards some', 'more characters follow.',
- \ '.SHAlso section boundaries from the nroff', 'macros terminate a paragraph. That means', 'a character like this:', ''], getline(1, '$'))
+
+ let expected =<< trim [DATA]
+ .LPIt does not matter, if afterwards some
+ more characters follow.
+ .SHAlso section boundaries from the nroff
+ macros terminate a paragraph. That means
+ a character like this:
+
+ [DATA]
+ call assert_equal(expected, getline(1, '$'))
+
norm! d{
- call assert_equal(['.LPIt does not matter, if afterwards some', 'more characters follow.', ''], getline(1,'$'))
+
+ let expected =<< trim [DATA]
+ .LPIt does not matter, if afterwards some
+ more characters follow.
+
+ [DATA]
+ call assert_equal(expected, getline(1, '$'))
+
" Test with { in cpooptions
%d
call append(0, text)
@@ -1602,21 +1659,62 @@ fun! Test_normal29_brace()
" set cpo+={
" 1
" norm! 0d2}
- " call assert_equal(['{', 'This is no paragraph', 'unless the ''{'' is set', 'in ''cpoptions''', '}',
- " \ '.IP', 'The nroff macros IP separates a paragraph', 'That means, it must be a ''.''',
- " \ 'followed by IP', '.LPIt does not matter, if afterwards some', 'more characters follow.',
- " \ '.SHAlso section boundaries from the nroff', 'macros terminate a paragraph. That means',
- " \ 'a character like this:', '.NH', 'End of text here', ''], getline(1,'$'))
+ " let expected =<< trim [DATA]
+ " {
+ " This is no paragraph
+ " unless the '{' is set
+ " in 'cpoptions'
+ " }
+ " .IP
+ " The nroff macros IP separates a paragraph
+ " That means, it must be a '.'
+ " followed by IP
+ " .LPIt does not matter, if afterwards some
+ " more characters follow.
+ " .SHAlso section boundaries from the nroff
+ " macros terminate a paragraph. That means
+ " a character like this:
+ " .NH
+ " End of text here
+ "
+ " [DATA]
+ " call assert_equal(expected, getline(1, '$'))
+ "
" $
" norm! d}
- " call assert_equal(['{', 'This is no paragraph', 'unless the ''{'' is set', 'in ''cpoptions''', '}',
- " \ '.IP', 'The nroff macros IP separates a paragraph', 'That means, it must be a ''.''',
- " \ 'followed by IP', '.LPIt does not matter, if afterwards some', 'more characters follow.',
- " \ '.SHAlso section boundaries from the nroff', 'macros terminate a paragraph. That means',
- " \ 'a character like this:', '.NH', 'End of text here', ''], getline(1,'$'))
+ " let expected =<< trim [DATA]
+ " {
+ " This is no paragraph
+ " unless the '{' is set
+ " in 'cpoptions'
+ " }
+ " .IP
+ " The nroff macros IP separates a paragraph
+ " That means, it must be a '.'
+ " followed by IP
+ " .LPIt does not matter, if afterwards some
+ " more characters follow.
+ " .SHAlso section boundaries from the nroff
+ " macros terminate a paragraph. That means
+ " a character like this:
+ " .NH
+ " End of text here
+ "
+ " [DATA]
+ " call assert_equal(expected, getline(1, '$'))
+ "
" norm! gg}
" norm! d5}
- " call assert_equal(['{', 'This is no paragraph', 'unless the ''{'' is set', 'in ''cpoptions''', '}', ''], getline(1,'$'))
+ "
+ " let expected =<< trim [DATA]
+ " {
+ " This is no paragraph
+ " unless the '{' is set
+ " in 'cpoptions'
+ " }
+
+ " [DATA]
+ " call assert_equal(expected, getline(1, '$'))
" clean up
set cpo-={
@@ -1718,7 +1816,7 @@ fun! Test_normal31_r_cmd()
bw!
endfunc
-func! Test_normal32_g_cmd1()
+func Test_normal32_g_cmd1()
" Test for g*, g#
new
call append(0, ['abc.x_foo', 'x_foobar.abc'])
@@ -1798,6 +1896,7 @@ fun! Test_normal33_g_cmd2()
set wrap listchars= sbr=
let lineA='abcdefghijklmnopqrstuvwxyz'
let lineB='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ let lineC='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
$put =lineA
$put =lineB
@@ -1831,9 +1930,30 @@ fun! Test_normal33_g_cmd2()
call assert_equal(15, col('.'))
call assert_equal('l', getreg(0))
+ norm! 2ggdd
+ $put =lineC
+
+ " Test for gM
+ norm! gMyl
+ call assert_equal(73, col('.'))
+ call assert_equal('0', getreg(0))
+ " Test for 20gM
+ norm! 20gMyl
+ call assert_equal(29, col('.'))
+ call assert_equal('S', getreg(0))
+ " Test for 60gM
+ norm! 60gMyl
+ call assert_equal(87, col('.'))
+ call assert_equal('E', getreg(0))
+
+ " Test for g Ctrl-G
+ set ff=unix
+ let a=execute(":norm! g\<c-g>")
+ call assert_match('Col 87 of 144; Line 2 of 2; Word 1 of 1; Byte 88 of 146', a)
+
" Test for gI
norm! gIfoo
- call assert_equal(['', 'fooabcdefghijk lmno0123456789AMNOPQRSTUVWXYZ'], getline(1,'$'))
+ call assert_equal(['', 'foo0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'], getline(1,'$'))
" Test for gi
wincmd c
@@ -1849,7 +1969,7 @@ fun! Test_normal33_g_cmd2()
bw!
endfunc
-func! Test_g_ctrl_g()
+func Test_g_ctrl_g()
new
let a = execute(":norm! g\<c-g>")
@@ -2139,7 +2259,7 @@ fun! Test_normal41_insert_reg()
bw!
endfunc
-func! Test_normal42_halfpage()
+func Test_normal42_halfpage()
" basic test for Ctrl-D and Ctrl-U
call Setup_NewWindow()
call assert_equal(5, &scroll)
@@ -2207,7 +2327,7 @@ fun! Test_normal43_textobject1()
bw!
endfunc
-func! Test_normal44_textobjects2()
+func Test_normal44_textobjects2()
" basic testing for is and as text objects
new
call append(0, ['This is a test. With some sentences!', '', 'Even with a question? And one more. And no sentence here'])
@@ -2262,7 +2382,7 @@ func! Test_normal44_textobjects2()
bw!
endfunc
-func! Test_normal45_drop()
+func Test_normal45_drop()
if !has('dnd')
" The ~ register does not exist
call assert_beeps('norm! "~')
@@ -2280,7 +2400,7 @@ func! Test_normal45_drop()
bw!
endfunc
-func! Test_normal46_ignore()
+func Test_normal46_ignore()
new
" How to test this?
" let's just for now test, that the buffer
@@ -2299,7 +2419,7 @@ func! Test_normal46_ignore()
bw!
endfunc
-func! Test_normal47_visual_buf_wipe()
+func Test_normal47_visual_buf_wipe()
" This was causing a crash or ml_get error.
enew!
call setline(1,'xxx')
@@ -2313,7 +2433,7 @@ func! Test_normal47_visual_buf_wipe()
set nomodified
endfunc
-func! Test_normal47_autocmd()
+func Test_normal47_autocmd()
" disabled, does not seem to be possible currently
throw "Skipped: not possible to test cursorhold autocmd while waiting for input in normal_cmd"
new
@@ -2331,14 +2451,14 @@ func! Test_normal47_autocmd()
bw!
endfunc
-func! Test_normal48_wincmd()
+func Test_normal48_wincmd()
new
exe "norm! \<c-w>c"
call assert_equal(1, winnr('$'))
call assert_fails(":norm! \<c-w>c", "E444")
endfunc
-func! Test_normal49_counts()
+func Test_normal49_counts()
new
call setline(1, 'one two three four five six seven eight nine ten')
1
@@ -2347,7 +2467,7 @@ func! Test_normal49_counts()
bw!
endfunc
-func! Test_normal50_commandline()
+func Test_normal50_commandline()
if !has("timers") || !has("cmdline_hist") || !has("vertsplit")
return
endif
@@ -2378,7 +2498,7 @@ func! Test_normal50_commandline()
bw!
endfunc
-func! Test_normal51_FileChangedRO()
+func Test_normal51_FileChangedRO()
if !has("autocmd")
return
endif
@@ -2395,7 +2515,7 @@ func! Test_normal51_FileChangedRO()
call delete("Xreadonly.log")
endfunc
-func! Test_normal52_rl()
+func Test_normal52_rl()
if !has("rightleft")
return
endif
@@ -2428,7 +2548,7 @@ func! Test_normal52_rl()
bw!
endfunc
-func! Test_normal53_digraph()
+func Test_normal53_digraph()
if !has('digraphs')
return
endif
@@ -2474,6 +2594,15 @@ func Test_normal_large_count()
bwipe!
endfunc
+func Test_delete_until_paragraph()
+ new
+ normal grádv}
+ call assert_equal('á', getline(1))
+ normal grád}
+ call assert_equal('', getline(1))
+ bwipe!
+endfunc
+
" Test for the gr (virtual replace) command
" Test for the bug fixed by 7.4.387
func Test_gr_command()
@@ -2516,13 +2645,27 @@ func Test_changelist()
let &ul = save_ul
endfunc
-func Test_delete_until_paragraph()
- new
- normal grádv}
- call assert_equal('á', getline(1))
- normal grád}
- call assert_equal('', getline(1))
- bwipe!
+func Test_nv_hat_count()
+ %bwipeout!
+ let l:nr = bufnr('%') + 1
+ call assert_fails(':execute "normal! ' . l:nr . '\<C-^>"', 'E92')
+
+ edit Xfoo
+ let l:foo_nr = bufnr('Xfoo')
+
+ edit Xbar
+ let l:bar_nr = bufnr('Xbar')
+
+ " Make sure we are not just using the alternate file.
+ edit Xbaz
+
+ call feedkeys(l:foo_nr . "\<C-^>", 'tx')
+ call assert_equal('Xfoo', fnamemodify(bufname('%'), ':t'))
+
+ call feedkeys(l:bar_nr . "\<C-^>", 'tx')
+ call assert_equal('Xbar', fnamemodify(bufname('%'), ':t'))
+
+ %bwipeout!
endfunc
func Test_message_when_using_ctrl_c()
@@ -2617,3 +2760,105 @@ Piece of Java
close!
endfunc
+
+fun! Test_normal_gdollar_cmd()
+ if !has("jumplist")
+ return
+ endif
+ " Tests for g cmds
+ call Setup_NewWindow()
+ " Make long lines that will wrap
+ %s/$/\=repeat(' foobar', 10)/
+ 20vsp
+ set wrap
+ " Test for g$ with count
+ norm! gg
+ norm! 0vg$y
+ call assert_equal(20, col("'>"))
+ call assert_equal('1 foobar foobar foob', getreg(0))
+ norm! gg
+ norm! 0v4g$y
+ call assert_equal(72, col("'>"))
+ call assert_equal('1 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.."\n", getreg(0))
+ norm! gg
+ norm! 0v6g$y
+ call assert_equal(40, col("'>"))
+ call assert_equal('1 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.. "\n"..
+ \ '2 foobar foobar foobar foobar foobar foo', getreg(0))
+ set nowrap
+ " clean up
+ norm! gg
+ norm! 0vg$y
+ call assert_equal(20, col("'>"))
+ call assert_equal('1 foobar foobar foob', getreg(0))
+ norm! gg
+ norm! 0v4g$y
+ call assert_equal(20, col("'>"))
+ call assert_equal('1 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.. "\n"..
+ \ '2 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.. "\n"..
+ \ '3 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.. "\n"..
+ \ '4 foobar foobar foob', getreg(0))
+ norm! gg
+ norm! 0v6g$y
+ call assert_equal(20, col("'>"))
+ call assert_equal('1 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.. "\n"..
+ \ '2 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.. "\n"..
+ \ '3 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.. "\n"..
+ \ '4 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.. "\n"..
+ \ '5 foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar'.. "\n"..
+ \ '6 foobar foobar foob', getreg(0))
+ " Move to last line, also down movement is not possible, should still move
+ " the cursor to the last visible char
+ norm! G
+ norm! 0v6g$y
+ call assert_equal(20, col("'>"))
+ call assert_equal('100 foobar foobar fo', getreg(0))
+ bw!
+endfunc
+
+func Test_normal_gk()
+ " needs 80 column new window
+ new
+ vert 80new
+ put =[repeat('x',90)..' {{{1', 'x {{{1']
+ norm! gk
+ " In a 80 column wide terminal the window will be only 78 char
+ " (because Vim will leave space for the other window),
+ " but if the terminal is larger, it will be 80 chars, so verify the
+ " cursor column correctly.
+ call assert_equal(winwidth(0)+1, col('.'))
+ call assert_equal(winwidth(0)+1, virtcol('.'))
+ norm! j
+ call assert_equal(6, col('.'))
+ call assert_equal(6, virtcol('.'))
+ norm! gk
+ call assert_equal(95, col('.'))
+ call assert_equal(95, virtcol('.'))
+ bw!
+ bw!
+
+ " needs 80 column new window
+ new
+ vert 80new
+ set number
+ set numberwidth=10
+ set cpoptions+=n
+ put =[repeat('0',90), repeat('1',90)]
+ norm! 075l
+ call assert_equal(76, col('.'))
+ norm! gk
+ call assert_equal(1, col('.'))
+ norm! gk
+ call assert_equal(76, col('.'))
+ norm! gk
+ call assert_equal(1, col('.'))
+ norm! gj
+ call assert_equal(76, col('.'))
+ norm! gj
+ call assert_equal(1, col('.'))
+ norm! gj
+ call assert_equal(76, col('.'))
+ bw!
+ bw!
+ set cpoptions& number& numberwidth&
+endfunc
diff --git a/src/nvim/testdir/test_options.vim b/src/nvim/testdir/test_options.vim
index f4f5cbca61..6fcc372591 100644
--- a/src/nvim/testdir/test_options.vim
+++ b/src/nvim/testdir/test_options.vim
@@ -476,13 +476,19 @@ func Test_shortmess_F2()
call assert_match('file2', execute('bn', ''))
set shortmess+=F
call assert_true(empty(execute('bn', '')))
+ " call assert_false(test_getvalue('need_fileinfo'))
call assert_true(empty(execute('bn', '')))
+ " call assert_false(test_getvalue('need_fileinfo'))
set hidden
call assert_true(empty(execute('bn', '')))
+ " call assert_false(test_getvalue('need_fileinfo'))
call assert_true(empty(execute('bn', '')))
+ " call assert_false(test_getvalue('need_fileinfo'))
set nohidden
call assert_true(empty(execute('bn', '')))
+ " call assert_false(test_getvalue('need_fileinfo'))
call assert_true(empty(execute('bn', '')))
+ " call assert_false(test_getvalue('need_fileinfo'))
" Accommodate Nvim default.
set shortmess-=F
call assert_match('file1', execute('bn', ''))
diff --git a/src/nvim/testdir/test_popup.vim b/src/nvim/testdir/test_popup.vim
index 53df30bb19..8083672808 100644
--- a/src/nvim/testdir/test_popup.vim
+++ b/src/nvim/testdir/test_popup.vim
@@ -1,6 +1,7 @@
" Test for completion menu
source shared.vim
+source screendump.vim
let g:months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
let g:setting = ''
@@ -680,18 +681,15 @@ func Test_popup_and_window_resize()
call term_sendkeys(buf, "\<c-v>")
call term_wait(buf, 100)
" popup first entry "!" must be at the top
- call WaitFor({-> term_getline(buf, 1) =~ "^!"})
- call assert_match('^!\s*$', term_getline(buf, 1))
+ call WaitForAssert({-> assert_match('^!\s*$', term_getline(buf, 1))})
exe 'resize +' . (h - 1)
call term_wait(buf, 100)
redraw!
" popup shifted down, first line is now empty
- call WaitFor({-> term_getline(buf, 1) == ""})
- call assert_equal('', term_getline(buf, 1))
+ call WaitForAssert({-> assert_equal('', term_getline(buf, 1))})
sleep 100m
" popup is below cursor line and shows first match "!"
- call WaitFor({-> term_getline(buf, term_getcursor(buf)[0] + 1) =~ "^!"})
- call assert_match('^!\s*$', term_getline(buf, term_getcursor(buf)[0] + 1))
+ call WaitForAssert({-> assert_match('^!\s*$', term_getline(buf, term_getcursor(buf)[0] + 1))})
" cursor line also shows !
call assert_match('^!\s*$', term_getline(buf, term_getcursor(buf)[0]))
bwipe!
@@ -735,6 +733,39 @@ func Test_popup_and_preview_autocommand()
bw!
endfunc
+func Test_popup_position()
+ if !CanRunVimInTerminal()
+ return
+ endif
+ let lines =<< trim END
+ 123456789_123456789_123456789_a
+ 123456789_123456789_123456789_b
+ 123
+ END
+ call writefile(lines, 'Xtest')
+ let buf = RunVimInTerminal('Xtest', {})
+ call term_sendkeys(buf, ":vsplit\<CR>")
+
+ " default pumwidth in left window: overlap in right window
+ call term_sendkeys(buf, "GA\<C-N>")
+ call VerifyScreenDump(buf, 'Test_popup_position_01', {'rows': 8})
+ call term_sendkeys(buf, "\<Esc>u")
+
+ " default pumwidth: fill until right of window
+ call term_sendkeys(buf, "\<C-W>l")
+ call term_sendkeys(buf, "GA\<C-N>")
+ call VerifyScreenDump(buf, 'Test_popup_position_02', {'rows': 8})
+
+ " larger pumwidth: used as minimum width
+ call term_sendkeys(buf, "\<Esc>u")
+ call term_sendkeys(buf, ":set pumwidth=30\<CR>")
+ call term_sendkeys(buf, "GA\<C-N>")
+ call VerifyScreenDump(buf, 'Test_popup_position_03', {'rows': 8})
+
+ call term_sendkeys(buf, "\<Esc>u")
+ call StopVimInTerminal(buf)
+ call delete('Xtest')
+endfunc
func Test_popup_complete_backwards()
new
@@ -746,6 +777,16 @@ func Test_popup_complete_backwards()
bwipe!
endfunc
+func Test_popup_complete_backwards_ctrl_p()
+ new
+ call setline(1, ['Post', 'Port', 'Po'])
+ let expected=['Post', 'Port', 'Port']
+ call cursor(3,2)
+ call feedkeys("A\<C-P>\<C-N>rt\<cr>", 'tx')
+ call assert_equal(expected, getline(1,'$'))
+ bwipe!
+endfunc
+
fun! Test_complete_o_tab()
throw 'skipped: Nvim does not support test_override()'
let s:o_char_pressed = 0
diff --git a/src/nvim/testdir/test_profile.vim b/src/nvim/testdir/test_profile.vim
index 7e853eeac3..f3eb88abf0 100644
--- a/src/nvim/testdir/test_profile.vim
+++ b/src/nvim/testdir/test_profile.vim
@@ -6,34 +6,34 @@ endif
source screendump.vim
func Test_profile_func()
- let lines = [
- \ 'profile start Xprofile_func.log',
- \ 'profile func Foo*"',
- \ "func! Foo1()",
- \ "endfunc",
- \ "func! Foo2()",
- \ " let l:count = 100",
- \ " while l:count > 0",
- \ " let l:count = l:count - 1",
- \ " endwhile",
- \ "endfunc",
- \ "func! Foo3()",
- \ "endfunc",
- \ "func! Bar()",
- \ "endfunc",
- \ "call Foo1()",
- \ "call Foo1()",
- \ "profile pause",
- \ "call Foo1()",
- \ "profile continue",
- \ "call Foo2()",
- \ "call Foo3()",
- \ "call Bar()",
- \ "if !v:profiling",
- \ " delfunc Foo2",
- \ "endif",
- \ "delfunc Foo3",
- \ ]
+ let lines =<< trim [CODE]
+ profile start Xprofile_func.log
+ profile func Foo*
+ func! Foo1()
+ endfunc
+ func! Foo2()
+ let l:count = 100
+ while l:count > 0
+ let l:count = l:count - 1
+ endwhile
+ endfunc
+ func! Foo3()
+ endfunc
+ func! Bar()
+ endfunc
+ call Foo1()
+ call Foo1()
+ profile pause
+ call Foo1()
+ profile continue
+ call Foo2()
+ call Foo3()
+ call Bar()
+ if !v:profiling
+ delfunc Foo2
+ endif
+ delfunc Foo3
+ [CODE]
call writefile(lines, 'Xprofile_func.vim')
call system(v:progpath
@@ -54,7 +54,7 @@ func Test_profile_func()
call assert_equal(30, len(lines))
call assert_equal('FUNCTION Foo1()', lines[0])
- call assert_match('Defined:.*Xprofile_func.vim', lines[1])
+ call assert_match('Defined:.*Xprofile_func.vim:3', lines[1])
call assert_equal('Called 2 times', lines[2])
call assert_match('^Total time:\s\+\d\+\.\d\+$', lines[3])
call assert_match('^ Self time:\s\+\d\+\.\d\+$', lines[4])
@@ -88,38 +88,38 @@ func Test_profile_func()
endfunc
func Test_profile_func_with_ifelse()
- let lines = [
- \ "func! Foo1()",
- \ " if 1",
- \ " let x = 0",
- \ " elseif 1",
- \ " let x = 1",
- \ " else",
- \ " let x = 2",
- \ " endif",
- \ "endfunc",
- \ "func! Foo2()",
- \ " if 0",
- \ " let x = 0",
- \ " elseif 1",
- \ " let x = 1",
- \ " else",
- \ " let x = 2",
- \ " endif",
- \ "endfunc",
- \ "func! Foo3()",
- \ " if 0",
- \ " let x = 0",
- \ " elseif 0",
- \ " let x = 1",
- \ " else",
- \ " let x = 2",
- \ " endif",
- \ "endfunc",
- \ "call Foo1()",
- \ "call Foo2()",
- \ "call Foo3()",
- \ ]
+ let lines =<< trim [CODE]
+ func! Foo1()
+ if 1
+ let x = 0
+ elseif 1
+ let x = 1
+ else
+ let x = 2
+ endif
+ endfunc
+ func! Foo2()
+ if 0
+ let x = 0
+ elseif 1
+ let x = 1
+ else
+ let x = 2
+ endif
+ endfunc
+ func! Foo3()
+ if 0
+ let x = 0
+ elseif 0
+ let x = 1
+ else
+ let x = 2
+ endif
+ endfunc
+ call Foo1()
+ call Foo2()
+ call Foo3()
+ [CODE]
call writefile(lines, 'Xprofile_func.vim')
call system(v:progpath
@@ -198,41 +198,41 @@ func Test_profile_func_with_ifelse()
endfunc
func Test_profile_func_with_trycatch()
- let lines = [
- \ "func! Foo1()",
- \ " try",
- \ " let x = 0",
- \ " catch",
- \ " let x = 1",
- \ " finally",
- \ " let x = 2",
- \ " endtry",
- \ "endfunc",
- \ "func! Foo2()",
- \ " try",
- \ " throw 0",
- \ " catch",
- \ " let x = 1",
- \ " finally",
- \ " let x = 2",
- \ " endtry",
- \ "endfunc",
- \ "func! Foo3()",
- \ " try",
- \ " throw 0",
- \ " catch",
- \ " throw 1",
- \ " finally",
- \ " let x = 2",
- \ " endtry",
- \ "endfunc",
- \ "call Foo1()",
- \ "call Foo2()",
- \ "try",
- \ " call Foo3()",
- \ "catch",
- \ "endtry",
- \ ]
+ let lines =<< trim [CODE]
+ func! Foo1()
+ try
+ let x = 0
+ catch
+ let x = 1
+ finally
+ let x = 2
+ endtry
+ endfunc
+ func! Foo2()
+ try
+ throw 0
+ catch
+ let x = 1
+ finally
+ let x = 2
+ endtry
+ endfunc
+ func! Foo3()
+ try
+ throw 0
+ catch
+ throw 1
+ finally
+ let x = 2
+ endtry
+ endfunc
+ call Foo1()
+ call Foo2()
+ try
+ call Foo3()
+ catch
+ endtry
+ [CODE]
call writefile(lines, 'Xprofile_func.vim')
call system(v:progpath
@@ -311,15 +311,15 @@ func Test_profile_func_with_trycatch()
endfunc
func Test_profile_file()
- let lines = [
- \ 'func! Foo()',
- \ 'endfunc',
- \ 'for i in range(10)',
- \ ' " a comment',
- \ ' call Foo()',
- \ 'endfor',
- \ 'call Foo()',
- \ ]
+ let lines =<< trim [CODE]
+ func! Foo()
+ endfunc
+ for i in range(10)
+ " a comment
+ call Foo()
+ endfor
+ call Foo()
+ [CODE]
call writefile(lines, 'Xprofile_file.vim')
call system(v:progpath
@@ -450,26 +450,27 @@ func Test_profile_truncate_mbyte()
endfunc
func Test_profdel_func()
- let lines = [
- \ 'profile start Xprofile_file.log',
- \ 'func! Foo1()',
- \ 'endfunc',
- \ 'func! Foo2()',
- \ 'endfunc',
- \ 'func! Foo3()',
- \ 'endfunc',
- \ '',
- \ 'profile func Foo1',
- \ 'profile func Foo2',
- \ 'call Foo1()',
- \ 'call Foo2()',
- \ '',
- \ 'profile func Foo3',
- \ 'profdel func Foo2',
- \ 'profdel func Foo3',
- \ 'call Foo1()',
- \ 'call Foo2()',
- \ 'call Foo3()' ]
+ let lines =<< trim [CODE]
+ profile start Xprofile_file.log
+ func! Foo1()
+ endfunc
+ func! Foo2()
+ endfunc
+ func! Foo3()
+ endfunc
+
+ profile func Foo1
+ profile func Foo2
+ call Foo1()
+ call Foo2()
+
+ profile func Foo3
+ profdel func Foo2
+ profdel func Foo3
+ call Foo1()
+ call Foo2()
+ call Foo3()
+ [CODE]
call writefile(lines, 'Xprofile_file.vim')
call system(v:progpath . ' -es --clean -c "so Xprofile_file.vim" -c q')
call assert_equal(0, v:shell_error)
@@ -496,14 +497,15 @@ endfunc
func Test_profdel_star()
" Foo() is invoked once before and once after 'profdel *'.
" So profiling should report it only once.
- let lines = [
- \ 'profile start Xprofile_file.log',
- \ 'func! Foo()',
- \ 'endfunc',
- \ 'profile func Foo',
- \ 'call Foo()',
- \ 'profdel *',
- \ 'call Foo()' ]
+ let lines =<< trim [CODE]
+ profile start Xprofile_file.log
+ func! Foo()
+ endfunc
+ profile func Foo
+ call Foo()
+ profdel *
+ call Foo()
+ [CODE]
call writefile(lines, 'Xprofile_file.vim')
call system(v:progpath . ' -es --clean -c "so Xprofile_file.vim" -c q')
call assert_equal(0, v:shell_error)
diff --git a/src/nvim/testdir/test_quickfix.vim b/src/nvim/testdir/test_quickfix.vim
index b7f45aeeb1..d7b387c2c9 100644
--- a/src/nvim/testdir/test_quickfix.vim
+++ b/src/nvim/testdir/test_quickfix.vim
@@ -37,6 +37,8 @@ func s:setup_commands(cchar)
command! -nargs=* Xgrepadd <mods> grepadd <args>
command! -nargs=* Xhelpgrep helpgrep <args>
command! -nargs=0 -count Xcc <count>cc
+ command! -count=1 -nargs=0 Xbelow <mods><count>cbelow
+ command! -count=1 -nargs=0 Xabove <mods><count>cabove
let g:Xgetlist = function('getqflist')
let g:Xsetlist = function('setqflist')
call setqflist([], 'f')
@@ -70,6 +72,8 @@ func s:setup_commands(cchar)
command! -nargs=* Xgrepadd <mods> lgrepadd <args>
command! -nargs=* Xhelpgrep lhelpgrep <args>
command! -nargs=0 -count Xcc <count>ll
+ command! -count=1 -nargs=0 Xbelow <mods><count>lbelow
+ command! -count=1 -nargs=0 Xabove <mods><count>labove
let g:Xgetlist = function('getloclist', [0])
let g:Xsetlist = function('setloclist', [0])
call setloclist(0, [], 'f')
@@ -163,6 +167,12 @@ endfunc
func XageTests(cchar)
call s:setup_commands(a:cchar)
+ if a:cchar == 'l'
+ " No location list for the current window
+ call assert_fails('lolder', 'E776:')
+ call assert_fails('lnewer', 'E776:')
+ endif
+
let list = [{'bufnr': bufnr('%'), 'lnum': 1}]
call g:Xsetlist(list)
@@ -273,6 +283,27 @@ func Test_cwindow()
call XwindowTests('l')
endfunc
+func Test_copenHeight()
+ copen
+ wincmd H
+ let height = winheight(0)
+ copen 10
+ call assert_equal(height, winheight(0))
+ quit
+endfunc
+
+func Test_copenHeight_tabline()
+ set tabline=foo showtabline=2
+ copen
+ wincmd H
+ let height = winheight(0)
+ copen 10
+ call assert_equal(height, winheight(0))
+ quit
+ set tabline& showtabline&
+endfunc
+
+
" Tests for the :cfile, :lfile, :caddfile, :laddfile, :cgetfile and :lgetfile
" commands.
func XfileTests(cchar)
@@ -540,6 +571,8 @@ func s:test_xhelpgrep(cchar)
" Search for non existing help string
call assert_fails('Xhelpgrep a1b2c3', 'E480:')
+ " Invalid regular expression
+ call assert_fails('Xhelpgrep \@<!', 'E480:')
endfunc
func Test_helpgrep()
@@ -775,68 +808,68 @@ func Test_efm1()
return
endif
- let l = [
- \ '"Xtestfile", line 4.12: 1506-045 (S) Undeclared identifier fd_set.',
- \ '"Xtestfile", line 6 col 19; this is an error',
- \ 'gcc -c -DHAVE_CONFIsing-prototypes -I/usr/X11R6/include version.c',
- \ 'Xtestfile:9: parse error before `asd''',
- \ 'make: *** [vim] Error 1',
- \ 'in file "Xtestfile" linenr 10: there is an error',
- \ '',
- \ '2 returned',
- \ '"Xtestfile", line 11 col 1; this is an error',
- \ '"Xtestfile", line 12 col 2; this is another error',
- \ '"Xtestfile", line 14:10; this is an error in column 10',
- \ '=Xtestfile=, line 15:10; this is another error, but in vcol 10 this time',
- \ '"Xtestfile", linenr 16: yet another problem',
- \ 'Error in "Xtestfile" at line 17:',
- \ 'x should be a dot',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 17',
- \ ' ^',
- \ 'Error in "Xtestfile" at line 18:',
- \ 'x should be a dot',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 18',
- \ '.............^',
- \ 'Error in "Xtestfile" at line 19:',
- \ 'x should be a dot',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 19',
- \ '--------------^',
- \ 'Error in "Xtestfile" at line 20:',
- \ 'x should be a dot',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 20',
- \ ' ^',
- \ '',
- \ 'Does anyone know what is the problem and how to correction it?',
- \ '"Xtestfile", line 21 col 9: What is the title of the quickfix window?',
- \ '"Xtestfile", line 22 col 9: What is the title of the quickfix window?'
- \ ]
+ let l =<< trim [DATA]
+ "Xtestfile", line 4.12: 1506-045 (S) Undeclared identifier fd_set.
+ "Xtestfile", line 6 col 19; this is an error
+ gcc -c -DHAVE_CONFIsing-prototypes -I/usr/X11R6/include version.c
+ Xtestfile:9: parse error before `asd'
+ make: *** [vim] Error 1
+ in file "Xtestfile" linenr 10: there is an error
+
+ 2 returned
+ "Xtestfile", line 11 col 1; this is an error
+ "Xtestfile", line 12 col 2; this is another error
+ "Xtestfile", line 14:10; this is an error in column 10
+ =Xtestfile=, line 15:10; this is another error, but in vcol 10 this time
+ "Xtestfile", linenr 16: yet another problem
+ Error in "Xtestfile" at line 17:
+ x should be a dot
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 17
+ ^
+ Error in "Xtestfile" at line 18:
+ x should be a dot
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 18
+ .............^
+ Error in "Xtestfile" at line 19:
+ x should be a dot
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 19
+ --------------^
+ Error in "Xtestfile" at line 20:
+ x should be a dot
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 20
+ ^
+
+ Does anyone know what is the problem and how to correction it?
+ "Xtestfile", line 21 col 9: What is the title of the quickfix window?
+ "Xtestfile", line 22 col 9: What is the title of the quickfix window?
+ [DATA]
call writefile(l, 'Xerrorfile1')
call writefile(l[:-2], 'Xerrorfile2')
- let m = [
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 2',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 3',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 4',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 5',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 6',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 7',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 8',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 9',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 10',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 11',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 12',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 13',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 14',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 15',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 16',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 17',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 18',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 19',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 20',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 21',
- \ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 22'
- \ ]
+ let m =<< [DATA]
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 2
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 3
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 4
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 5
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 6
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 7
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 8
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 9
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 10
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 11
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 12
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 13
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 14
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 15
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 16
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 17
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 18
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 19
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 20
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 21
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx line 22
+[DATA]
call writefile(m, 'Xtestfile')
let save_efm = &efm
@@ -895,20 +928,21 @@ func s:dir_stack_tests(cchar)
let save_efm=&efm
set efm=%DEntering\ dir\ '%f',%f:%l:%m,%XLeaving\ dir\ '%f'
- let lines = ["Entering dir 'dir1/a'",
- \ 'habits2.txt:1:Nine Healthy Habits',
- \ "Entering dir 'b'",
- \ 'habits3.txt:2:0 Hours of television',
- \ 'habits2.txt:7:5 Small meals',
- \ "Entering dir 'dir1/c'",
- \ 'habits4.txt:3:1 Hour of exercise',
- \ "Leaving dir 'dir1/c'",
- \ "Leaving dir 'dir1/a'",
- \ 'habits1.txt:4:2 Liters of water',
- \ "Entering dir 'dir2'",
- \ 'habits5.txt:5:3 Cups of hot green tea',
- \ "Leaving dir 'dir2'"
- \]
+ let lines =<< trim [DATA]
+ Entering dir 'dir1/a'
+ habits2.txt:1:Nine Healthy Habits
+ Entering dir 'b'
+ habits3.txt:2:0 Hours of television
+ habits2.txt:7:5 Small meals
+ Entering dir 'dir1/c'
+ habits4.txt:3:1 Hour of exercise
+ Leaving dir 'dir1/c'
+ Leaving dir 'dir1/a'
+ habits1.txt:4:2 Liters of water
+ Entering dir 'dir2'
+ habits5.txt:5:3 Cups of hot green tea
+ Leaving dir 'dir2
+ [DATA]
Xexpr ""
for l in lines
@@ -942,18 +976,20 @@ func Test_efm_dirstack()
call mkdir('dir1/c')
call mkdir('dir2')
- let lines = ["Nine Healthy Habits",
- \ "0 Hours of television",
- \ "1 Hour of exercise",
- \ "2 Liters of water",
- \ "3 Cups of hot green tea",
- \ "4 Short mental breaks",
- \ "5 Small meals",
- \ "6 AM wake up time",
- \ "7 Minutes of laughter",
- \ "8 Hours of sleep (at least)",
- \ "9 PM end of the day and off to bed"
- \ ]
+ let lines =<< trim [DATA]
+ Nine Healthy Habits,
+ 0 Hours of television,
+ 1 Hour of exercise,
+ 2 Liters of water,
+ 3 Cups of hot green tea,
+ 4 Short mental breaks,
+ 5 Small meals,
+ 6 AM wake up time,
+ 7 Minutes of laughter,
+ 8 Hours of sleep (at least),
+ 9 PM end of the day and off to bed
+ [DATA]
+
call writefile(lines, 'habits1.txt')
call writefile(lines, 'dir1/a/habits2.txt')
call writefile(lines, 'dir1/a/b/habits3.txt')
@@ -1042,28 +1078,29 @@ func Test_efm2()
set efm=%f:%s
cexpr 'Xtestfile:Line search text'
let l = getqflist()
- call assert_equal(l[0].pattern, '^\VLine search text\$')
- call assert_equal(l[0].lnum, 0)
+ call assert_equal('^\VLine search text\$', l[0].pattern)
+ call assert_equal(0, l[0].lnum)
let l = split(execute('clist', ''), "\n")
call assert_equal([' 1 Xtestfile:^\VLine search text\$: '], l)
" Test for %P, %Q and %t format specifiers
- let lines=["[Xtestfile1]",
- \ "(1,17) error: ';' missing",
- \ "(21,2) warning: variable 'z' not defined",
- \ "(67,3) error: end of file found before string ended",
- \ "--",
- \ "",
- \ "[Xtestfile2]",
- \ "--",
- \ "",
- \ "[Xtestfile3]",
- \ "NEW compiler v1.1",
- \ "(2,2) warning: variable 'x' not defined",
- \ "(67,3) warning: 's' already defined",
- \ "--"
- \]
+ let lines =<< trim [DATA]
+ [Xtestfile1]
+ (1,17) error: ';' missing
+ (21,2) warning: variable 'z' not defined
+ (67,3) error: end of file found before string ended
+ --
+
+ [Xtestfile2]
+ --
+
+ [Xtestfile3]
+ NEW compiler v1.1
+ (2,2) warning: variable 'x' not defined
+ (67,3) warning: 's' already defined
+ --
+ [DATA]
set efm=%+P[%f]%r,(%l\\,%c)%*[\ ]%t%*[^:]:\ %m,%+Q--%r
" To exercise the push/pop file functionality in quickfix, the test files
" need to be created.
@@ -1085,11 +1122,13 @@ func Test_efm2()
call delete('Xtestfile3')
" Tests for %E, %C and %Z format specifiers
- let lines = ["Error 275",
- \ "line 42",
- \ "column 3",
- \ "' ' expected after '--'"
- \]
+ let lines =<< trim [DATA]
+ Error 275
+ line 42
+ column 3
+ ' ' expected after '--'
+ [DATA]
+
set efm=%EError\ %n,%Cline\ %l,%Ccolumn\ %c,%Z%m
cgetexpr lines
let l = getqflist()
@@ -1100,9 +1139,11 @@ func Test_efm2()
call assert_equal("\n' ' expected after '--'", l[0].text)
" Test for %>
- let lines = ["Error in line 147 of foo.c:",
- \"unknown variable 'i'"
- \]
+ let lines =<< trim [DATA]
+ Error in line 147 of foo.c:
+ unknown variable 'i'
+ [DATA]
+
set efm=unknown\ variable\ %m,%E%>Error\ in\ line\ %l\ of\ %f:,%Z%m
cgetexpr lines
let l = getqflist()
@@ -1111,21 +1152,21 @@ func Test_efm2()
call assert_equal("\nunknown variable 'i'", l[0].text)
" Test for %A, %C and other formats
- let lines = [
- \"==============================================================",
- \"FAIL: testGetTypeIdCachesResult (dbfacadeTest.DjsDBFacadeTest)",
- \"--------------------------------------------------------------",
- \"Traceback (most recent call last):",
- \' File "unittests/dbfacadeTest.py", line 89, in testFoo',
- \" self.assertEquals(34, dtid)",
- \' File "/usr/lib/python2.2/unittest.py", line 286, in',
- \" failUnlessEqual",
- \" raise self.failureException, \\",
- \"AssertionError: 34 != 33",
- \"",
- \"--------------------------------------------------------------",
- \"Ran 27 tests in 0.063s"
- \]
+ let lines =<< trim [DATA]
+ ==============================================================
+ FAIL: testGetTypeIdCachesResult (dbfacadeTest.DjsDBFacadeTest)
+ --------------------------------------------------------------
+ Traceback (most recent call last):
+ File "unittests/dbfacadeTest.py", line 89, in testFoo
+ self.assertEquals(34, dtid)
+ File "/usr/lib/python2.2/unittest.py", line 286, in
+ failUnlessEqual
+ raise self.failureException, \\
+ AssertionError: 34 != 33
+
+ --------------------------------------------------------------
+ Ran 27 tests in 0.063s
+ [DATA]
set efm=%C\ %.%#,%A\ \ File\ \"%f\"\\,\ line\ %l%.%#,%Z%[%^\ ]%\\@=%m
cgetexpr lines
let l = getqflist()
@@ -1279,6 +1320,28 @@ func SetXlistTests(cchar, bnum)
let l = g:Xgetlist()
call g:Xsetlist(l)
call assert_equal(0, g:Xgetlist()[0].valid)
+ " Adding a non-valid entry should not mark the list as having valid entries
+ call g:Xsetlist([{'bufnr':a:bnum, 'lnum':5, 'valid':0}], 'a')
+ Xwindow
+ call assert_equal(1, winnr('$'))
+
+ " :cnext/:cprev should still work even with invalid entries in the list
+ let l = [{'bufnr' : a:bnum, 'lnum' : 1, 'text' : '1', 'valid' : 0},
+ \ {'bufnr' : a:bnum, 'lnum' : 2, 'text' : '2', 'valid' : 0}]
+ call g:Xsetlist(l)
+ Xnext
+ call assert_equal(2, g:Xgetlist({'idx' : 0}).idx)
+ Xprev
+ call assert_equal(1, g:Xgetlist({'idx' : 0}).idx)
+ " :cnext/:cprev should still work after appending invalid entries to an
+ " empty list
+ call g:Xsetlist([])
+ call g:Xsetlist(l, 'a')
+ Xnext
+ call assert_equal(2, g:Xgetlist({'idx' : 0}).idx)
+ Xprev
+ call assert_equal(1, g:Xgetlist({'idx' : 0}).idx)
+
call g:Xsetlist([{'text':'Text1', 'valid':1}])
Xwindow
call assert_equal(2, winnr('$'))
@@ -1579,6 +1642,14 @@ func Test_switchbuf()
call assert_equal(3, tabpagenr('$'))
tabfirst | enew | tabonly | only
+ set switchbuf=uselast
+ split
+ let last_winid = win_getid()
+ copen
+ exe "normal 1G\<CR>"
+ call assert_equal(last_winid, win_getid())
+ enew | only
+
set switchbuf=
edit Xqftestfile1
let file1_winid = win_getid()
@@ -1955,6 +2026,18 @@ func Xproperty_tests(cchar)
call g:Xsetlist([], 'r', {'items' : [{'filename' : 'F1', 'lnum' : 10, 'text' : 'L10'}]})
call assert_equal('TestTitle', g:Xgetlist({'title' : 1}).title)
+ " Test for getting id of window associated with a location list window
+ if a:cchar == 'l'
+ only
+ call assert_equal(0, g:Xgetlist({'all' : 1}).filewinid)
+ let wid = win_getid()
+ Xopen
+ call assert_equal(wid, g:Xgetlist({'filewinid' : 1}).filewinid)
+ wincmd w
+ call assert_equal(0, g:Xgetlist({'filewinid' : 1}).filewinid)
+ only
+ endif
+
" The following used to crash Vim with address sanitizer
call g:Xsetlist([], 'f')
call g:Xsetlist([], 'a', {'items' : [{'filename':'F1', 'lnum':10}]})
@@ -2317,6 +2400,12 @@ func XvimgrepTests(cchar)
call assert_equal('Xtestfile2', bufname(''))
call assert_equal('Editor:Emacs EmAcS', l[0].text)
+ " Test for unloading a buffer after vimgrep searched the buffer
+ %bwipe
+ Xvimgrep /Editor/j Xtestfile*
+ call assert_equal(0, getbufinfo('Xtestfile1')[0].loaded)
+ call assert_equal([], getbufinfo('Xtestfile2'))
+
call delete('Xtestfile1')
call delete('Xtestfile2')
endfunc
@@ -2489,7 +2578,7 @@ func Test_file_from_copen()
cclose
augroup! QF_Test
-endfunction
+endfunc
func Test_resize_from_copen()
augroup QF_Test
@@ -2864,185 +2953,101 @@ func Test_qf_id()
call Xqfid_tests('l')
endfunc
-func Test_getqflist_invalid_nr()
- " The following commands used to crash Vim
- cexpr ""
- call getqflist({'nr' : $XXX_DOES_NOT_EXIST_XXX})
-
- " Cleanup
- call setqflist([], 'r')
-endfunc
-
-" Test for shortening/simplifying the file name when opening the
-" quickfix window or when displaying the quickfix list
-func Test_shorten_fname()
- if !has('unix')
- return
- endif
- %bwipe
- " Create a quickfix list with a absolute path filename
- let fname = getcwd() . '/test_quickfix.vim'
- call setqflist([], ' ', {'lines':[fname . ":20:Line20"], 'efm':'%f:%l:%m'})
- call assert_equal(fname, bufname('test_quickfix.vim'))
- " Opening the quickfix window should simplify the file path
- cwindow
- call assert_equal('test_quickfix.vim', bufname('test_quickfix.vim'))
- cclose
- %bwipe
- " Create a quickfix list with a absolute path filename
- call setqflist([], ' ', {'lines':[fname . ":20:Line20"], 'efm':'%f:%l:%m'})
- call assert_equal(fname, bufname('test_quickfix.vim'))
- " Displaying the quickfix list should simplify the file path
- silent! clist
- call assert_equal('test_quickfix.vim', bufname('test_quickfix.vim'))
-endfunc
-
-" Quickfix title tests
-" In the below tests, 'exe "cmd"' is used to invoke the quickfix commands.
-" Otherwise due to indentation, the title is set with spaces at the beginning
-" of the command.
-func Test_qftitle()
- call writefile(["F1:1:Line1"], 'Xerr')
-
- " :cexpr
- exe "cexpr readfile('Xerr')"
- call assert_equal(":cexpr readfile('Xerr')", getqflist({'title' : 1}).title)
-
- " :cgetexpr
- exe "cgetexpr readfile('Xerr')"
- call assert_equal(":cgetexpr readfile('Xerr')",
- \ getqflist({'title' : 1}).title)
-
- " :caddexpr
- call setqflist([], 'f')
- exe "caddexpr readfile('Xerr')"
- call assert_equal(":caddexpr readfile('Xerr')",
- \ getqflist({'title' : 1}).title)
-
- " :cbuffer
- new Xerr
- exe "cbuffer"
- call assert_equal(':cbuffer (Xerr)', getqflist({'title' : 1}).title)
-
- " :cgetbuffer
- edit Xerr
- exe "cgetbuffer"
- call assert_equal(':cgetbuffer (Xerr)', getqflist({'title' : 1}).title)
-
- " :caddbuffer
- call setqflist([], 'f')
- edit Xerr
- exe "caddbuffer"
- call assert_equal(':caddbuffer (Xerr)', getqflist({'title' : 1}).title)
-
- " :cfile
- exe "cfile Xerr"
- call assert_equal(':cfile Xerr', getqflist({'title' : 1}).title)
-
- " :cgetfile
- exe "cgetfile Xerr"
- call assert_equal(':cgetfile Xerr', getqflist({'title' : 1}).title)
-
- " :caddfile
- call setqflist([], 'f')
- exe "caddfile Xerr"
- call assert_equal(':caddfile Xerr', getqflist({'title' : 1}).title)
-
- " :grep
- set grepprg=internal
- exe "grep F1 Xerr"
- call assert_equal(':grep F1 Xerr', getqflist({'title' : 1}).title)
-
- " :grepadd
- call setqflist([], 'f')
- exe "grepadd F1 Xerr"
- call assert_equal(':grepadd F1 Xerr', getqflist({'title' : 1}).title)
- set grepprg&vim
-
- " :vimgrep
- exe "vimgrep F1 Xerr"
- call assert_equal(':vimgrep F1 Xerr', getqflist({'title' : 1}).title)
+func Xqfjump_tests(cchar)
+ call s:setup_commands(a:cchar)
- " :vimgrepadd
- call setqflist([], 'f')
- exe "vimgrepadd F1 Xerr"
- call assert_equal(':vimgrepadd F1 Xerr', getqflist({'title' : 1}).title)
+ call writefile(["Line1\tFoo", "Line2"], 'F1')
+ call writefile(["Line1\tBar", "Line2"], 'F2')
+ call writefile(["Line1\tBaz", "Line2"], 'F3')
- call setqflist(['F1:10:L10'], ' ')
- call assert_equal(':setqflist()', getqflist({'title' : 1}).title)
+ call g:Xsetlist([], 'f')
- call setqflist([], 'f')
- call setqflist(['F1:10:L10'], 'a')
- call assert_equal(':setqflist()', getqflist({'title' : 1}).title)
+ " Tests for
+ " Jumping to a line using a pattern
+ " Jumping to a column greater than the last column in a line
+ " Jumping to a line greater than the last line in the file
+ let l = []
+ for i in range(1, 7)
+ call add(l, {})
+ endfor
+ let l[0].filename='F1'
+ let l[0].pattern='Line1'
+ let l[1].filename='F2'
+ let l[1].pattern='Line1'
+ let l[2].filename='F3'
+ let l[2].pattern='Line1'
+ let l[3].filename='F3'
+ let l[3].lnum=1
+ let l[3].col=9
+ let l[3].vcol=1
+ let l[4].filename='F3'
+ let l[4].lnum=99
+ let l[5].filename='F3'
+ let l[5].lnum=1
+ let l[5].col=99
+ let l[5].vcol=1
+ let l[6].filename='F3'
+ let l[6].pattern='abcxyz'
- call setqflist([], 'f')
- call setqflist(['F1:10:L10'], 'r')
- call assert_equal(':setqflist()', getqflist({'title' : 1}).title)
+ call g:Xsetlist([], ' ', {'items' : l})
+ Xopen | only
+ 2Xnext
+ call assert_equal(3, g:Xgetlist({'idx' : 0}).idx)
+ call assert_equal('F3', bufname('%'))
+ Xnext
+ call assert_equal(7, col('.'))
+ Xnext
+ call assert_equal(2, line('.'))
+ Xnext
+ call assert_equal(9, col('.'))
+ 2
+ Xnext
+ call assert_equal(2, line('.'))
- close
- call delete('Xerr')
+ if a:cchar == 'l'
+ " When jumping to a location list entry in the location list window and
+ " no usable windows are available, then a new window should be opened.
+ enew! | new | only
+ call g:Xsetlist([], 'f')
+ setlocal buftype=nofile
+ new
+ call g:Xsetlist([], ' ', {'lines' : ['F1:1:1:Line1', 'F1:2:2:Line2', 'F2:1:1:Line1', 'F2:2:2:Line2', 'F3:1:1:Line1', 'F3:2:2:Line2']})
+ Xopen
+ let winid = win_getid()
+ wincmd p
+ close
+ call win_gotoid(winid)
+ Xnext
+ call assert_equal(3, winnr('$'))
+ call assert_equal(1, winnr())
+ call assert_equal(2, line('.'))
- call setqflist([], ' ', {'title' : 'Errors'})
- copen
- call assert_equal('Errors', w:quickfix_title)
- call setqflist([], 'r', {'items' : [{'filename' : 'a.c', 'lnum' : 10}]})
- call assert_equal('Errors', w:quickfix_title)
- cclose
-endfunc
+ " When jumping to an entry in the location list window and the window
+ " associated with the location list is not present and a window containing
+ " the file is already present, then that window should be used.
+ close
+ belowright new
+ call g:Xsetlist([], 'f')
+ edit F3
+ call win_gotoid(winid)
+ Xlast
+ call assert_equal(3, winnr())
+ call assert_equal(6, g:Xgetlist({'size' : 1}).size)
+ call assert_equal(winid, g:Xgetlist({'winid' : 1}).winid)
+ endif
-" Test for the position of the quickfix and location list window
-func Test_qfwin_pos()
- " Open two windows
+ " Cleanup
+ enew!
new | only
- new
- cexpr ['F1:10:L10']
- copen
- " Quickfix window should be the bottom most window
- call assert_equal(3, winnr())
- close
- " Open at the very top
- wincmd t
- topleft copen
- call assert_equal(1, winnr())
- close
- " open left of the current window
- wincmd t
- below new
- leftabove copen
- call assert_equal(2, winnr())
- close
- " open right of the current window
- rightbelow copen
- call assert_equal(3, winnr())
- close
-endfunc
-" The following test used to crash Vim
-func Test_lhelpgrep_autocmd()
- lhelpgrep quickfix
- autocmd QuickFixCmdPost * call setloclist(0, [], 'f')
- lhelpgrep buffer
- call assert_equal('help', &filetype)
- call assert_equal(0, getloclist(0, {'nr' : '$'}).nr)
- lhelpgrep tabpage
- call assert_equal('help', &filetype)
- call assert_equal(1, getloclist(0, {'nr' : '$'}).nr)
- au! QuickFixCmdPost
- new | only
+ call delete('F1')
+ call delete('F2')
+ call delete('F3')
endfunc
-" Test to make sure that an empty quickfix buffer is not reused for loading
-" a normal buffer.
-func Test_empty_qfbuf()
- enew | only
- call writefile(["Test"], 'Xfile1')
- call setqflist([], 'f')
- copen | only
- let qfbuf = bufnr('')
- edit Xfile1
- call assert_notequal(qfbuf, bufnr(''))
- enew
- call delete('Xfile1')
+func Test_qfjump()
+ call Xqfjump_tests('c')
+ call Xqfjump_tests('l')
endfunc
" Tests for the getqflist() and getloclist() functions when the list is not
@@ -3061,7 +3066,17 @@ func Xgetlist_empty_tests(cchar)
call assert_equal('', g:Xgetlist({'title' : 0}).title)
call assert_equal(0, g:Xgetlist({'winid' : 0}).winid)
call assert_equal(0, g:Xgetlist({'changedtick' : 0}).changedtick)
- call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [], 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0, 'changedtick': 0}, g:Xgetlist({'all' : 0}))
+ if a:cchar == 'c'
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0,
+ \ 'items' : [], 'nr' : 0, 'size' : 0,
+ \ 'title' : '', 'winid' : 0, 'changedtick': 0},
+ \ g:Xgetlist({'all' : 0}))
+ else
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0,
+ \ 'items' : [], 'nr' : 0, 'size' : 0, 'title' : '',
+ \ 'winid' : 0, 'changedtick': 0, 'filewinid' : 0},
+ \ g:Xgetlist({'all' : 0}))
+ endif
" Quickfix window with empty stack
silent! Xopen
@@ -3094,7 +3109,16 @@ func Xgetlist_empty_tests(cchar)
call assert_equal('', g:Xgetlist({'id' : qfid, 'title' : 0}).title)
call assert_equal(0, g:Xgetlist({'id' : qfid, 'winid' : 0}).winid)
call assert_equal(0, g:Xgetlist({'id' : qfid, 'changedtick' : 0}).changedtick)
- call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [], 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0, 'changedtick' : 0}, g:Xgetlist({'id' : qfid, 'all' : 0}))
+ if a:cchar == 'c'
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [],
+ \ 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0,
+ \ 'changedtick' : 0}, g:Xgetlist({'id' : qfid, 'all' : 0}))
+ else
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [],
+ \ 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0,
+ \ 'changedtick' : 0, 'filewinid' : 0},
+ \ g:Xgetlist({'id' : qfid, 'all' : 0}))
+ endif
" Non-existing quickfix list number
call assert_equal('', g:Xgetlist({'nr' : 5, 'context' : 0}).context)
@@ -3106,7 +3130,16 @@ func Xgetlist_empty_tests(cchar)
call assert_equal('', g:Xgetlist({'nr' : 5, 'title' : 0}).title)
call assert_equal(0, g:Xgetlist({'nr' : 5, 'winid' : 0}).winid)
call assert_equal(0, g:Xgetlist({'nr' : 5, 'changedtick' : 0}).changedtick)
- call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [], 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0, 'changedtick' : 0}, g:Xgetlist({'nr' : 5, 'all' : 0}))
+ if a:cchar == 'c'
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [],
+ \ 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0,
+ \ 'changedtick' : 0}, g:Xgetlist({'nr' : 5, 'all' : 0}))
+ else
+ call assert_equal({'context' : '', 'id' : 0, 'idx' : 0, 'items' : [],
+ \ 'nr' : 0, 'size' : 0, 'title' : '', 'winid' : 0,
+ \ 'changedtick' : 0, 'filewinid' : 0},
+ \ g:Xgetlist({'nr' : 5, 'all' : 0}))
+ endif
endfunc
func Test_getqflist()
@@ -3114,6 +3147,16 @@ func Test_getqflist()
call Xgetlist_empty_tests('l')
endfunc
+
+func Test_getqflist_invalid_nr()
+ " The following commands used to crash Vim
+ cexpr ""
+ call getqflist({'nr' : $XXX_DOES_NOT_EXIST_XXX})
+
+ " Cleanup
+ call setqflist([], 'r')
+endfunc
+
" Tests for the quickfix/location list changedtick
func Xqftick_tests(cchar)
call s:setup_commands(a:cchar)
@@ -3172,6 +3215,41 @@ func Test_qf_tick()
call Xqftick_tests('l')
endfunc
+" Test helpgrep with lang specifier
+func Xtest_helpgrep_with_lang_specifier(cchar)
+ call s:setup_commands(a:cchar)
+ Xhelpgrep Vim@en
+ call assert_equal('help', &filetype)
+ call assert_notequal(0, g:Xgetlist({'nr' : '$'}).nr)
+ new | only
+endfunc
+
+func Test_helpgrep_with_lang_specifier()
+ call Xtest_helpgrep_with_lang_specifier('c')
+ call Xtest_helpgrep_with_lang_specifier('l')
+endfunc
+
+" The following test used to crash Vim.
+" Open the location list window and close the regular window associated with
+" the location list. When the garbage collection runs now, it incorrectly
+" marks the location list context as not in use and frees the context.
+func Test_ll_window_ctx()
+ call setloclist(0, [], 'f')
+ call setloclist(0, [], 'a', {'context' : []})
+ lopen | only
+ call test_garbagecollect_now()
+ echo getloclist(0, {'context' : 1}).context
+ enew | only
+endfunc
+
+" The following test used to crash vim
+func Test_lfile_crash()
+ sp Xtest
+ au QuickFixCmdPre * bw
+ call assert_fails('lfile', 'E40')
+ au! QuickFixCmdPre
+endfunc
+
" The following test used to crash vim
func Test_lbuffer_crash()
sv Xtest
@@ -3195,7 +3273,28 @@ func Test_lexpr_crash()
augroup QF_Test
au!
augroup END
+
+ enew | only
+ augroup QF_Test
+ au!
+ au BufNew * call setloclist(0, [], 'f')
+ augroup END
+ lexpr 'x:1:x'
+ augroup QF_Test
+ au!
+ augroup END
+
enew | only
+ lexpr ''
+ lopen
+ augroup QF_Test
+ au!
+ au FileType * call setloclist(0, [], 'f')
+ augroup END
+ lexpr ''
+ augroup QF_Test
+ au!
+ augroup END
endfunc
" The following test used to crash Vim
@@ -3212,122 +3311,31 @@ func Test_lvimgrep_crash()
enew | only
endfunc
-func Xqfjump_tests(cchar)
- call s:setup_commands(a:cchar)
-
- call writefile(["Line1\tFoo", "Line2"], 'F1')
- call writefile(["Line1\tBar", "Line2"], 'F2')
- call writefile(["Line1\tBaz", "Line2"], 'F3')
-
- call g:Xsetlist([], 'f')
-
- " Tests for
- " Jumping to a line using a pattern
- " Jumping to a column greater than the last column in a line
- " Jumping to a line greater than the last line in the file
- let l = []
- for i in range(1, 7)
- call add(l, {})
- endfor
- let l[0].filename='F1'
- let l[0].pattern='Line1'
- let l[1].filename='F2'
- let l[1].pattern='Line1'
- let l[2].filename='F3'
- let l[2].pattern='Line1'
- let l[3].filename='F3'
- let l[3].lnum=1
- let l[3].col=9
- let l[3].vcol=1
- let l[4].filename='F3'
- let l[4].lnum=99
- let l[5].filename='F3'
- let l[5].lnum=1
- let l[5].col=99
- let l[5].vcol=1
- let l[6].filename='F3'
- let l[6].pattern='abcxyz'
-
- call g:Xsetlist([], ' ', {'items' : l})
- Xopen | only
- 2Xnext
- call assert_equal(3, g:Xgetlist({'idx' : 0}).idx)
- call assert_equal('F3', bufname('%'))
- Xnext
- call assert_equal(7, col('.'))
- Xnext
- call assert_equal(2, line('.'))
- Xnext
- call assert_equal(9, col('.'))
- 2
- Xnext
- call assert_equal(2, line('.'))
-
- if a:cchar == 'l'
- " When jumping to a location list entry in the location list window and
- " no usable windows are available, then a new window should be opened.
- enew! | new | only
- call g:Xsetlist([], 'f')
- setlocal buftype=nofile
- new
- call g:Xsetlist([], ' ', {'lines' : ['F1:1:1:Line1', 'F1:2:2:Line2', 'F2:1:1:Line1', 'F2:2:2:Line2', 'F3:1:1:Line1', 'F3:2:2:Line2']})
- Xopen
- let winid = win_getid()
- wincmd p
- close
- call win_gotoid(winid)
- Xnext
- call assert_equal(3, winnr('$'))
- call assert_equal(1, winnr())
- call assert_equal(2, line('.'))
-
- " When jumping to an entry in the location list window and the window
- " associated with the location list is not present and a window containing
- " the file is already present, then that window should be used.
- close
- belowright new
- call g:Xsetlist([], 'f')
- edit F3
- call win_gotoid(winid)
- Xlast
- call assert_equal(3, winnr())
- call assert_equal(6, g:Xgetlist({'size' : 1}).size)
- call assert_equal(winid, g:Xgetlist({'winid' : 1}).winid)
- endif
-
- " Cleanup
- enew!
+" Test for the position of the quickfix and location list window
+func Test_qfwin_pos()
+ " Open two windows
new | only
-
- call delete('F1')
- call delete('F2')
- call delete('F3')
-endfunc
-
-func Test_qfjump()
- call Xqfjump_tests('c')
- call Xqfjump_tests('l')
-endfunc
-
-" The following test used to crash Vim.
-" Open the location list window and close the regular window associated with
-" the location list. When the garbage collection runs now, it incorrectly
-" marks the location list context as not in use and frees the context.
-func Test_ll_window_ctx()
- call setloclist(0, [], 'f')
- call setloclist(0, [], 'a', {'context' : []})
- lopen | only
- call test_garbagecollect_now()
- echo getloclist(0, {'context' : 1}).context
- enew | only
-endfunc
-
-" The following test used to crash vim
-func Test_lfile_crash()
- sp Xtest
- au QuickFixCmdPre * bw
- call assert_fails('lfile', 'E40')
- au! QuickFixCmdPre
+ new
+ cexpr ['F1:10:L10']
+ copen
+ " Quickfix window should be the bottom most window
+ call assert_equal(3, winnr())
+ close
+ " Open at the very top
+ wincmd t
+ topleft copen
+ call assert_equal(1, winnr())
+ close
+ " open left of the current window
+ wincmd t
+ below new
+ leftabove copen
+ call assert_equal(2, winnr())
+ close
+ " open right of the current window
+ rightbelow copen
+ call assert_equal(3, winnr())
+ close
endfunc
" Tests for quickfix/location lists changed by autocommands when
@@ -3371,6 +3379,137 @@ func Test_vimgrep_autocmd()
call setqflist([], 'f')
endfunc
+" The following test used to crash Vim
+func Test_lhelpgrep_autocmd()
+ lhelpgrep quickfix
+ autocmd QuickFixCmdPost * call setloclist(0, [], 'f')
+ lhelpgrep buffer
+ call assert_equal('help', &filetype)
+ call assert_equal(0, getloclist(0, {'nr' : '$'}).nr)
+ lhelpgrep tabpage
+ call assert_equal('help', &filetype)
+ call assert_equal(1, getloclist(0, {'nr' : '$'}).nr)
+ au! QuickFixCmdPost
+ new | only
+endfunc
+
+" Test for shortening/simplifying the file name when opening the
+" quickfix window or when displaying the quickfix list
+func Test_shorten_fname()
+ if !has('unix')
+ return
+ endif
+ %bwipe
+ " Create a quickfix list with a absolute path filename
+ let fname = getcwd() . '/test_quickfix.vim'
+ call setqflist([], ' ', {'lines':[fname . ":20:Line20"], 'efm':'%f:%l:%m'})
+ call assert_equal(fname, bufname('test_quickfix.vim'))
+ " Opening the quickfix window should simplify the file path
+ cwindow
+ call assert_equal('test_quickfix.vim', bufname('test_quickfix.vim'))
+ cclose
+ %bwipe
+ " Create a quickfix list with a absolute path filename
+ call setqflist([], ' ', {'lines':[fname . ":20:Line20"], 'efm':'%f:%l:%m'})
+ call assert_equal(fname, bufname('test_quickfix.vim'))
+ " Displaying the quickfix list should simplify the file path
+ silent! clist
+ call assert_equal('test_quickfix.vim', bufname('test_quickfix.vim'))
+endfunc
+
+" Quickfix title tests
+" In the below tests, 'exe "cmd"' is used to invoke the quickfix commands.
+" Otherwise due to indentation, the title is set with spaces at the beginning
+" of the command.
+func Test_qftitle()
+ call writefile(["F1:1:Line1"], 'Xerr')
+
+ " :cexpr
+ exe "cexpr readfile('Xerr')"
+ call assert_equal(":cexpr readfile('Xerr')", getqflist({'title' : 1}).title)
+
+ " :cgetexpr
+ exe "cgetexpr readfile('Xerr')"
+ call assert_equal(":cgetexpr readfile('Xerr')",
+ \ getqflist({'title' : 1}).title)
+
+ " :caddexpr
+ call setqflist([], 'f')
+ exe "caddexpr readfile('Xerr')"
+ call assert_equal(":caddexpr readfile('Xerr')",
+ \ getqflist({'title' : 1}).title)
+
+ " :cbuffer
+ new Xerr
+ exe "cbuffer"
+ call assert_equal(':cbuffer (Xerr)', getqflist({'title' : 1}).title)
+
+ " :cgetbuffer
+ edit Xerr
+ exe "cgetbuffer"
+ call assert_equal(':cgetbuffer (Xerr)', getqflist({'title' : 1}).title)
+
+ " :caddbuffer
+ call setqflist([], 'f')
+ edit Xerr
+ exe "caddbuffer"
+ call assert_equal(':caddbuffer (Xerr)', getqflist({'title' : 1}).title)
+
+ " :cfile
+ exe "cfile Xerr"
+ call assert_equal(':cfile Xerr', getqflist({'title' : 1}).title)
+
+ " :cgetfile
+ exe "cgetfile Xerr"
+ call assert_equal(':cgetfile Xerr', getqflist({'title' : 1}).title)
+
+ " :caddfile
+ call setqflist([], 'f')
+ exe "caddfile Xerr"
+ call assert_equal(':caddfile Xerr', getqflist({'title' : 1}).title)
+
+ " :grep
+ set grepprg=internal
+ exe "grep F1 Xerr"
+ call assert_equal(':grep F1 Xerr', getqflist({'title' : 1}).title)
+
+ " :grepadd
+ call setqflist([], 'f')
+ exe "grepadd F1 Xerr"
+ call assert_equal(':grepadd F1 Xerr', getqflist({'title' : 1}).title)
+ set grepprg&vim
+
+ " :vimgrep
+ exe "vimgrep F1 Xerr"
+ call assert_equal(':vimgrep F1 Xerr', getqflist({'title' : 1}).title)
+
+ " :vimgrepadd
+ call setqflist([], 'f')
+ exe "vimgrepadd F1 Xerr"
+ call assert_equal(':vimgrepadd F1 Xerr', getqflist({'title' : 1}).title)
+
+ call setqflist(['F1:10:L10'], ' ')
+ call assert_equal(':setqflist()', getqflist({'title' : 1}).title)
+
+ call setqflist([], 'f')
+ call setqflist(['F1:10:L10'], 'a')
+ call assert_equal(':setqflist()', getqflist({'title' : 1}).title)
+
+ call setqflist([], 'f')
+ call setqflist(['F1:10:L10'], 'r')
+ call assert_equal(':setqflist()', getqflist({'title' : 1}).title)
+
+ close
+ call delete('Xerr')
+
+ call setqflist([], ' ', {'title' : 'Errors'})
+ copen
+ call assert_equal('Errors', w:quickfix_title)
+ call setqflist([], 'r', {'items' : [{'filename' : 'a.c', 'lnum' : 10}]})
+ call assert_equal('Errors', w:quickfix_title)
+ cclose
+endfunc
+
func Test_lbuffer_with_bwipe()
new
new
@@ -3383,23 +3522,6 @@ func Test_lbuffer_with_bwipe()
augroup END
endfunc
-" Tests for the ':filter /pat/ clist' command
-func Test_filter_clist()
- cexpr ['Xfile1:10:10:Line 10', 'Xfile2:15:15:Line 15']
- call assert_equal([' 2 Xfile2:15 col 15: Line 15'],
- \ split(execute('filter /Line 15/ clist'), "\n"))
- call assert_equal([' 1 Xfile1:10 col 10: Line 10'],
- \ split(execute('filter /Xfile1/ clist'), "\n"))
- call assert_equal([], split(execute('filter /abc/ clist'), "\n"))
-
- call setqflist([{'module' : 'abc', 'pattern' : 'pat1'},
- \ {'module' : 'pqr', 'pattern' : 'pat2'}], ' ')
- call assert_equal([' 2 pqr:pat2: '],
- \ split(execute('filter /pqr/ clist'), "\n"))
- call assert_equal([' 1 abc:pat1: '],
- \ split(execute('filter /pat1/ clist'), "\n"))
-endfunc
-
" Test for an autocmd freeing the quickfix/location list when cexpr/lexpr is
" running
func Xexpr_acmd_freelist(cchar)
@@ -3549,6 +3671,23 @@ func Test_autocmd_changelist()
call Xautocmd_changelist('l')
endfunc
+" Tests for the ':filter /pat/ clist' command
+func Test_filter_clist()
+ cexpr ['Xfile1:10:10:Line 10', 'Xfile2:15:15:Line 15']
+ call assert_equal([' 2 Xfile2:15 col 15: Line 15'],
+ \ split(execute('filter /Line 15/ clist'), "\n"))
+ call assert_equal([' 1 Xfile1:10 col 10: Line 10'],
+ \ split(execute('filter /Xfile1/ clist'), "\n"))
+ call assert_equal([], split(execute('filter /abc/ clist'), "\n"))
+
+ call setqflist([{'module' : 'abc', 'pattern' : 'pat1'},
+ \ {'module' : 'pqr', 'pattern' : 'pat2'}], ' ')
+ call assert_equal([' 2 pqr:pat2: '],
+ \ split(execute('filter /pqr/ clist'), "\n"))
+ call assert_equal([' 1 abc:pat1: '],
+ \ split(execute('filter /pat1/ clist'), "\n"))
+endfunc
+
" Tests for the "CTRL-W <CR>" command.
func Xview_result_split_tests(cchar)
call s:setup_commands(a:cchar)
@@ -3575,3 +3714,269 @@ func Test_curswant()
call assert_equal(getcurpos()[4], virtcol('.'))
cclose | helpclose
endfunc
+
+" Test for parsing entries using visual screen column
+func Test_viscol()
+ enew
+ call writefile(["Col1\tCol2\tCol3"], 'Xfile1')
+ edit Xfile1
+
+ " Use byte offset for column number
+ set efm&
+ cexpr "Xfile1:1:5:XX\nXfile1:1:9:YY\nXfile1:1:20:ZZ"
+ call assert_equal([5, 8], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([9, 12], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([14, 20], [col('.'), virtcol('.')])
+
+ " Use screen column offset for column number
+ set efm=%f:%l:%v:%m
+ cexpr "Xfile1:1:8:XX\nXfile1:1:12:YY\nXfile1:1:20:ZZ"
+ call assert_equal([5, 8], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([9, 12], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([14, 20], [col('.'), virtcol('.')])
+ cexpr "Xfile1:1:6:XX\nXfile1:1:15:YY\nXfile1:1:24:ZZ"
+ call assert_equal([5, 8], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([10, 16], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([14, 20], [col('.'), virtcol('.')])
+
+ enew
+ call writefile(["Col1\täü\töß\tCol4"], 'Xfile1')
+
+ " Use byte offset for column number
+ set efm&
+ cexpr "Xfile1:1:8:XX\nXfile1:1:11:YY\nXfile1:1:16:ZZ"
+ call assert_equal([8, 10], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([11, 17], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([16, 25], [col('.'), virtcol('.')])
+
+ " Use screen column offset for column number
+ set efm=%f:%l:%v:%m
+ cexpr "Xfile1:1:10:XX\nXfile1:1:17:YY\nXfile1:1:25:ZZ"
+ call assert_equal([8, 10], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([11, 17], [col('.'), virtcol('.')])
+ cnext
+ call assert_equal([16, 25], [col('.'), virtcol('.')])
+
+ enew | only
+ set efm&
+ call delete('Xfile1')
+endfunc
+
+" Test to make sure that an empty quickfix buffer is not reused for loading
+" a normal buffer.
+func Test_empty_qfbuf()
+ enew | only
+ call writefile(["Test"], 'Xfile1')
+ call setqflist([], 'f')
+ copen | only
+ let qfbuf = bufnr('')
+ edit Xfile1
+ call assert_notequal(qfbuf, bufnr(''))
+ enew
+ call delete('Xfile1')
+endfunc
+
+" Test for the :cbelow, :cabove, :lbelow and :labove commands.
+func Xtest_below(cchar)
+ call s:setup_commands(a:cchar)
+
+ " No quickfix/location list
+ call assert_fails('Xbelow', 'E42:')
+ call assert_fails('Xabove', 'E42:')
+
+ " Empty quickfix/location list
+ call g:Xsetlist([])
+ call assert_fails('Xbelow', 'E42:')
+ call assert_fails('Xabove', 'E42:')
+
+ call s:create_test_file('X1')
+ call s:create_test_file('X2')
+ call s:create_test_file('X3')
+ call s:create_test_file('X4')
+
+ " Invalid entries
+ edit X1
+ call g:Xsetlist(["E1", "E2"])
+ call assert_fails('Xbelow', 'E42:')
+ call assert_fails('Xabove', 'E42:')
+ call assert_fails('3Xbelow', 'E42:')
+ call assert_fails('4Xabove', 'E42:')
+
+ " Test the commands with various arguments
+ Xexpr ["X1:5:L5", "X2:5:L5", "X2:10:L10", "X2:15:L15", "X3:3:L3"]
+ edit +7 X2
+ Xabove
+ call assert_equal(['X2', 5], [bufname(''), line('.')])
+ call assert_fails('Xabove', 'E553:')
+ normal 2j
+ Xbelow
+ call assert_equal(['X2', 10], [bufname(''), line('.')])
+ " Last error in this file
+ Xbelow 99
+ call assert_equal(['X2', 15], [bufname(''), line('.')])
+ call assert_fails('Xbelow', 'E553:')
+ " First error in this file
+ Xabove 99
+ call assert_equal(['X2', 5], [bufname(''), line('.')])
+ call assert_fails('Xabove', 'E553:')
+ normal gg
+ Xbelow 2
+ call assert_equal(['X2', 10], [bufname(''), line('.')])
+ normal G
+ Xabove 2
+ call assert_equal(['X2', 10], [bufname(''), line('.')])
+ edit X4
+ call assert_fails('Xabove', 'E42:')
+ call assert_fails('Xbelow', 'E42:')
+ if a:cchar == 'l'
+ " If a buffer has location list entries from some other window but not
+ " from the current window, then the commands should fail.
+ edit X1 | split | call setloclist(0, [], 'f')
+ call assert_fails('Xabove', 'E776:')
+ call assert_fails('Xbelow', 'E776:')
+ close
+ endif
+
+ " Test for lines with multiple quickfix entries
+ Xexpr ["X1:5:L5", "X2:5:1:L5_1", "X2:5:2:L5_2", "X2:5:3:L5_3",
+ \ "X2:10:1:L10_1", "X2:10:2:L10_2", "X2:10:3:L10_3",
+ \ "X2:15:1:L15_1", "X2:15:2:L15_2", "X2:15:3:L15_3", "X3:3:L3"]
+ edit +1 X2
+ Xbelow 2
+ call assert_equal(['X2', 10, 1], [bufname(''), line('.'), col('.')])
+ normal gg
+ Xbelow 99
+ call assert_equal(['X2', 15, 1], [bufname(''), line('.'), col('.')])
+ normal G
+ Xabove 2
+ call assert_equal(['X2', 10, 1], [bufname(''), line('.'), col('.')])
+ normal G
+ Xabove 99
+ call assert_equal(['X2', 5, 1], [bufname(''), line('.'), col('.')])
+ normal 10G
+ Xabove
+ call assert_equal(['X2', 5, 1], [bufname(''), line('.'), col('.')])
+ normal 10G
+ Xbelow
+ call assert_equal(['X2', 15, 1], [bufname(''), line('.'), col('.')])
+
+ " Invalid range
+ if a:cchar == 'c'
+ call assert_fails('-2cbelow', 'E553:')
+ " TODO: should go to first error in the current line?
+ 0cabove
+ else
+ call assert_fails('-2lbelow', 'E553:')
+ " TODO: should go to first error in the current line?
+ 0labove
+ endif
+
+ call delete('X1')
+ call delete('X2')
+ call delete('X3')
+ call delete('X4')
+endfunc
+
+func Test_cbelow()
+ call Xtest_below('c')
+ call Xtest_below('l')
+endfunc
+
+" Test for aborting quickfix commands using QuickFixCmdPre
+func Xtest_qfcmd_abort(cchar)
+ call s:setup_commands(a:cchar)
+
+ call g:Xsetlist([], 'f')
+
+ " cexpr/lexpr
+ let e = ''
+ try
+ Xexpr ["F1:10:Line10", "F2:20:Line20"]
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+
+ " cfile/lfile
+ call writefile(["F1:10:Line10", "F2:20:Line20"], 'Xfile1')
+ let e = ''
+ try
+ Xfile Xfile1
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+ call delete('Xfile1')
+
+ " cgetbuffer/lgetbuffer
+ enew!
+ call append(0, ["F1:10:Line10", "F2:20:Line20"])
+ let e = ''
+ try
+ Xgetbuffer
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+ enew!
+
+ " vimgrep/lvimgrep
+ let e = ''
+ try
+ Xvimgrep /func/ test_quickfix.vim
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+
+ " helpgrep/lhelpgrep
+ let e = ''
+ try
+ Xhelpgrep quickfix
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+
+ " grep/lgrep
+ if has('unix')
+ let e = ''
+ try
+ silent Xgrep func test_quickfix.vim
+ catch /.*/
+ let e = v:exception
+ endtry
+ call assert_equal('AbortCmd', e)
+ call assert_equal(0, g:Xgetlist({'nr' : '$'}).nr)
+ endif
+endfunc
+
+func Test_qfcmd_abort()
+ augroup QF_Test
+ au!
+ autocmd QuickFixCmdPre * throw "AbortCmd"
+ augroup END
+
+ call Xtest_qfcmd_abort('c')
+ call Xtest_qfcmd_abort('l')
+
+ augroup QF_Test
+ au!
+ augroup END
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/test_quotestar.vim b/src/nvim/testdir/test_quotestar.vim
index b83fbe40e8..ce5a9ee827 100644
--- a/src/nvim/testdir/test_quotestar.vim
+++ b/src/nvim/testdir/test_quotestar.vim
@@ -54,34 +54,33 @@ func Do_test_quotestar_for_x11()
" Make sure a previous server has exited
try
call remote_send(name, ":qa!\<CR>")
- call WaitFor('serverlist() !~ "' . name . '"')
catch /E241:/
endtry
- call assert_notmatch(name, serverlist())
+ call WaitForAssert({-> assert_notmatch(name, serverlist())})
let cmd .= ' --servername ' . name
let job = job_start(cmd, {'stoponexit': 'kill', 'out_io': 'null'})
- call WaitFor({-> job_status(job) == "run"})
+ call WaitForAssert({-> assert_equal("run", job_status(job))})
" Takes a short while for the server to be active.
- call WaitFor('serverlist() =~ "' . name . '"')
+ call WaitForAssert({-> assert_match(name, serverlist())})
" Wait for the server to be up and answering requests. One second is not
" always sufficient.
- call WaitFor('remote_expr("' . name . '", "v:version", "", 2) != ""')
+ call WaitForAssert({-> assert_notequal('', remote_expr(name, "v:version", "", 2))})
" Clear the *-register of this vim instance and wait for it to be picked up
" by the server.
let @* = 'no'
call remote_foreground(name)
- call WaitFor('remote_expr("' . name . '", "@*", "", 1) == "no"')
+ call WaitForAssert({-> assert_equal("no", remote_expr(name, "@*", "", 1))})
" Set the * register on the server.
call remote_send(name, ":let @* = 'yes'\<CR>")
- call WaitFor('remote_expr("' . name . '", "@*", "", 1) == "yes"')
+ call WaitForAssert({-> assert_equal("yes", remote_expr(name, "@*", "", 1))})
" Check that the *-register of this vim instance is changed as expected.
- call WaitFor('@* == "yes"')
+ call WaitForAssert({-> assert_equal("yes", @*)})
" Handle the large selection over 262040 byte.
let length = 262044
@@ -109,18 +108,17 @@ func Do_test_quotestar_for_x11()
call remote_send(name, ":gui -f\<CR>")
endif
" Wait for the server in the GUI to be up and answering requests.
- call WaitFor('remote_expr("' . name . '", "has(\"gui_running\")", "", 1) =~ "1"')
+ call WaitForAssert({-> assert_match("1", remote_expr(name, "has('gui_running')", "", 1))})
call remote_send(name, ":let @* = 'maybe'\<CR>")
- call WaitFor('remote_expr("' . name . '", "@*", "", 1) == "maybe"')
- call assert_equal('maybe', remote_expr(name, "@*", "", 2))
+ call WaitForAssert({-> assert_equal("maybe", remote_expr(name, "@*", "", 2))})
call assert_equal('maybe', @*)
endif
call remote_send(name, ":qa!\<CR>")
try
- call WaitFor({-> job_status(job) == "dead"})
+ call WaitForAssert({-> assert_equal("dead", job_status(job))})
finally
if job_status(job) != 'dead'
call assert_report('Server did not exit')
diff --git a/src/nvim/testdir/test_recover.vim b/src/nvim/testdir/test_recover.vim
index 09c8d1cda6..fc073cacd2 100644
--- a/src/nvim/testdir/test_recover.vim
+++ b/src/nvim/testdir/test_recover.vim
@@ -14,6 +14,12 @@ func Test_recover_root_dir()
set dir=/notexist/
endif
call assert_fails('split Xtest', 'E303:')
+
+ " No error with empty 'directory' setting.
+ set directory=
+ split XtestOK
+ close!
+
set dir&
endfunc
diff --git a/src/nvim/testdir/test_registers.vim b/src/nvim/testdir/test_registers.vim
index 298268a994..d4f58af10a 100644
--- a/src/nvim/testdir/test_registers.vim
+++ b/src/nvim/testdir/test_registers.vim
@@ -1,3 +1,16 @@
+"
+" Tests for register operations
+"
+
+" This test must be executed first to check for empty and unset registers.
+func Test_aaa_empty_reg_test()
+ call assert_fails('normal @@', 'E748:')
+ call assert_fails('normal @%', 'E354:')
+ call assert_fails('normal @#', 'E354:')
+ call assert_fails('normal @!', 'E354:')
+ call assert_fails('normal @:', 'E30:')
+ call assert_fails('normal @.', 'E29:')
+endfunc
func Test_yank_shows_register()
enew
@@ -82,3 +95,76 @@ func Test_recording_esc_sequence()
let &t_F2 = save_F2
endif
endfunc
+
+" Test for executing the last used register (@)
+func Test_last_used_exec_reg()
+ " Test for the @: command
+ let a = ''
+ call feedkeys(":let a ..= 'Vim'\<CR>", 'xt')
+ normal @:
+ call assert_equal('VimVim', a)
+
+ " Test for the @= command
+ let x = ''
+ let a = ":let x ..= 'Vim'\<CR>"
+ exe "normal @=a\<CR>"
+ normal @@
+ call assert_equal('VimVim', x)
+
+ " Test for the @. command
+ let a = ''
+ call feedkeys("i:let a ..= 'Edit'\<CR>", 'xt')
+ normal @.
+ normal @@
+ call assert_equal('EditEdit', a)
+
+ enew!
+endfunc
+
+func Test_get_register()
+ enew
+ edit Xfile1
+ edit Xfile2
+ call assert_equal('Xfile2', getreg('%'))
+ call assert_equal('Xfile1', getreg('#'))
+
+ call feedkeys("iTwo\<Esc>", 'xt')
+ call assert_equal('Two', getreg('.'))
+ call assert_equal('', getreg('_'))
+ call assert_beeps('normal ":yy')
+ call assert_beeps('normal "%yy')
+ call assert_beeps('normal ".yy')
+
+ call assert_equal('', getreg("\<C-F>"))
+ call assert_equal('', getreg("\<C-W>"))
+ call assert_equal('', getreg("\<C-L>"))
+
+ call assert_equal('', getregtype('!'))
+
+ enew!
+endfunc
+
+func Test_set_register()
+ call assert_fails("call setreg('#', 200)", 'E86:')
+
+ edit Xfile_alt_1
+ let b1 = bufnr('')
+ edit Xfile_alt_2
+ let b2 = bufnr('')
+ edit Xfile_alt_3
+ let b3 = bufnr('')
+ call setreg('#', 'alt_1')
+ call assert_equal('Xfile_alt_1', getreg('#'))
+ call setreg('#', b2)
+ call assert_equal('Xfile_alt_2', getreg('#'))
+
+ let ab = 'regwrite'
+ call setreg('=', '')
+ call setreg('=', 'a', 'a')
+ call setreg('=', 'b', 'a')
+ call assert_equal('regwrite', getreg('='))
+
+ enew!
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/test_search.vim b/src/nvim/testdir/test_search.vim
index 5d4c2a015f..68eb311e3c 100644
--- a/src/nvim/testdir/test_search.vim
+++ b/src/nvim/testdir/test_search.vim
@@ -57,7 +57,7 @@ func Test_search_cmdline()
call feedkeys("/the".repeat("\<C-G>", 6)."\<cr>", 'tx')
call assert_equal(' 8 them', getline('.'))
:1
- " eigth match
+ " eighth match
call feedkeys("/the".repeat("\<C-G>", 7)."\<cr>", 'tx')
call assert_equal(' 9 these', getline('.'))
:1
@@ -99,7 +99,7 @@ func Test_search_cmdline()
call feedkeys("/the".repeat("\<C-G>", 6)."\<cr>", 'tx')
call assert_equal(' 8 them', getline('.'))
:1
- " eigth match
+ " eighth match
call feedkeys("/the".repeat("\<C-G>", 7)."\<cr>", 'tx')
call assert_equal(' 9 these', getline('.'))
:1
diff --git a/src/nvim/testdir/test_spell.vim b/src/nvim/testdir/test_spell.vim
index e49b5542fa..e2016d7927 100644
--- a/src/nvim/testdir/test_spell.vim
+++ b/src/nvim/testdir/test_spell.vim
@@ -283,9 +283,9 @@ func Test_zz_affix()
\ ])
call LoadAffAndDic(g:test_data_aff7, g:test_data_dic7)
- call RunGoodBad("meea1 meea\xE9 bar prebar barmeat prebarmeat leadprebar lead tail leadtail leadmiddletail",
+ call RunGoodBad("meea1 meezero meea\xE9 bar prebar barmeat prebarmeat leadprebar lead tail leadtail leadmiddletail",
\ "bad: mee meea2 prabar probarmaat middle leadmiddle middletail taillead leadprobar",
- \ ["bar", "barmeat", "lead", "meea1", "meea\xE9", "prebar", "prebarmeat", "tail"],
+ \ ["bar", "barmeat", "lead", "meea1", "meea\xE9", "meezero", "prebar", "prebarmeat", "tail"],
\ [
\ ["bad", ["bar", "lead", "tail"]],
\ ["mee", ["meea1", "meea\xE9", "bar"]],
@@ -320,6 +320,19 @@ func Test_zz_Numbers()
\ ])
endfunc
+" Affix flags
+func Test_zz_affix_flags()
+ call LoadAffAndDic(g:test_data_aff10, g:test_data_dic10)
+ call RunGoodBad("drink drinkable drinkables drinktable drinkabletable",
+ \ "bad: drinks drinkstable drinkablestable",
+ \ ["drink", "drinkable", "drinkables", "table"],
+ \ [['bad', []],
+ \ ['drinks', ['drink']],
+ \ ['drinkstable', ['drinktable', 'drinkable', 'drink table']],
+ \ ['drinkablestable', ['drinkabletable', 'drinkables table', 'drinkable table']],
+ \ ])
+endfunc
+
function FirstSpellWord()
call feedkeys("/^start:\n", 'tx')
normal ]smm
@@ -713,6 +726,9 @@ let g:test_data_aff7 = [
\"SFX 61003 Y 1",
\"SFX 61003 0 meat .",
\"",
+ \"SFX 0 Y 1",
+ \"SFX 0 0 zero .",
+ \"",
\"SFX 391 Y 1",
\"SFX 391 0 a1 .",
\"",
@@ -724,7 +740,7 @@ let g:test_data_aff7 = [
\ ]
let g:test_data_dic7 = [
\"1234",
- \"mee/391,111,9999",
+ \"mee/0,391,111,9999",
\"bar/17,61003,123",
\"lead/2",
\"tail/123",
@@ -748,6 +764,21 @@ let g:test_data_dic9 = [
\"foo",
\"bar",
\ ]
+let g:test_data_aff10 = [
+ \"COMPOUNDRULE se",
+ \"COMPOUNDPERMITFLAG p",
+ \"",
+ \"SFX A Y 1",
+ \"SFX A 0 able/Mp .",
+ \"",
+ \"SFX M Y 1",
+ \"SFX M 0 s .",
+ \ ]
+let g:test_data_dic10 = [
+ \"1234",
+ \"drink/As",
+ \"table/e",
+ \ ]
let g:test_data_aff_sal = [
\"SET ISO8859-1",
\"TRY esianrtolcdugmphbyfvkwjkqxz-\xEB\xE9\xE8\xEA\xEF\xEE\xE4\xE0\xE2\xF6\xFC\xFB'ESIANRTOLCDUGMPHBYFVKWJKQXZ",
diff --git a/src/nvim/testdir/test_startup.vim b/src/nvim/testdir/test_startup.vim
index 1e70f28a00..a38625cca8 100644
--- a/src/nvim/testdir/test_startup.vim
+++ b/src/nvim/testdir/test_startup.vim
@@ -20,25 +20,27 @@ func Test_after_comes_later()
if !has('packages')
return
endif
- let before = [
- \ 'set nocp viminfo+=nviminfo',
- \ 'set guioptions+=M',
- \ 'let $HOME = "/does/not/exist"',
- \ 'set loadplugins',
- \ 'set rtp=Xhere,Xafter,Xanother',
- \ 'set packpath=Xhere,Xafter',
- \ 'set nomore',
- \ 'let g:sequence = ""',
- \ ]
- let after = [
- \ 'redir! > Xtestout',
- \ 'scriptnames',
- \ 'redir END',
- \ 'redir! > Xsequence',
- \ 'echo g:sequence',
- \ 'redir END',
- \ 'quit',
- \ ]
+ let before =<< trim [CODE]
+ set nocp viminfo+=nviminfo
+ set guioptions+=M
+ let $HOME = "/does/not/exist"
+ set loadplugins
+ set rtp=Xhere,Xafter,Xanother
+ set packpath=Xhere,Xafter
+ set nomore
+ let g:sequence = ""
+ [CODE]
+
+ let after =<< trim [CODE]
+ redir! > Xtestout
+ scriptnames
+ redir END
+ redir! > Xsequence
+ echo g:sequence
+ redir END
+ quit
+ [CODE]
+
call mkdir('Xhere/plugin', 'p')
call writefile(['let g:sequence .= "here "'], 'Xhere/plugin/here.vim')
call mkdir('Xanother/plugin', 'p')
@@ -77,15 +79,16 @@ func Test_pack_in_rtp_when_plugins_run()
if !has('packages')
return
endif
- let before = [
- \ 'set nocp viminfo+=nviminfo',
- \ 'set guioptions+=M',
- \ 'let $HOME = "/does/not/exist"',
- \ 'set loadplugins',
- \ 'set rtp=Xhere',
- \ 'set packpath=Xhere',
- \ 'set nomore',
- \ ]
+ let before =<< trim [CODE]
+ set nocp viminfo+=nviminfo
+ set guioptions+=M
+ let $HOME = "/does/not/exist"
+ set loadplugins
+ set rtp=Xhere
+ set packpath=Xhere
+ set nomore
+ [CODE]
+
let after = [
\ 'quit',
\ ]
@@ -133,11 +136,12 @@ endfunc
func Test_compatible_args()
throw "skipped: Nvim is always 'nocompatible'"
- let after = [
- \ 'call writefile([string(&compatible)], "Xtestout")',
- \ 'set viminfo+=nviminfo',
- \ 'quit',
- \ ]
+ let after =<< trim [CODE]
+ call writefile([string(&compatible)], "Xtestout")
+ set viminfo+=nviminfo
+ quit
+ [CODE]
+
if RunVim([], after, '-C')
let lines = readfile('Xtestout')
call assert_equal('1', lines[0])
@@ -154,14 +158,15 @@ endfunc
" Test the -o[N] and -O[N] arguments to open N windows split
" horizontally or vertically.
func Test_o_arg()
- let after = [
- \ 'call writefile([winnr("$"),
- \ winheight(1), winheight(2), &lines,
- \ winwidth(1), winwidth(2), &columns,
- \ bufname(winbufnr(1)), bufname(winbufnr(2))],
- \ "Xtestout")',
- \ 'qall',
- \ ]
+ let after =<< trim [CODE]
+ call writefile([winnr("$"),
+ \ winheight(1), winheight(2), &lines,
+ \ winwidth(1), winwidth(2), &columns,
+ \ bufname(winbufnr(1)), bufname(winbufnr(2))],
+ \ "Xtestout")
+ qall
+ [CODE]
+
if RunVim([], after, '-o2')
" Open 2 windows split horizontally. Expect:
" - 2 windows
@@ -230,10 +235,11 @@ endfunc
" Test the -p[N] argument to open N tabpages.
func Test_p_arg()
- let after = [
- \ 'call writefile(split(execute("tabs"), "\n"), "Xtestout")',
- \ 'qall',
- \ ]
+ let after =<< trim [CODE]
+ call writefile(split(execute("tabs"), "\n"), "Xtestout")
+ qall
+ [CODE]
+
if RunVim([], after, '-p2')
let lines = readfile('Xtestout')
call assert_equal(4, len(lines))
@@ -290,10 +296,11 @@ endfunc
" -M resets 'modifiable' and 'write'
" -R sets 'readonly'
func Test_m_M_R()
- let after = [
- \ 'call writefile([&write, &modifiable, &readonly, &updatecount], "Xtestout")',
- \ 'qall',
- \ ]
+ let after =<< trim [CODE]
+ call writefile([&write, &modifiable, &readonly, &updatecount], "Xtestout")
+ qall
+ [CODE]
+
if RunVim([], after, '')
let lines = readfile('Xtestout')
call assert_equal(['1', '1', '0', '200'], lines)
@@ -316,10 +323,11 @@ endfunc
" Test the -A, -F and -H arguments (Arabic, Farsi and Hebrew modes).
func Test_A_F_H_arg()
- let after = [
- \ 'call writefile([&rightleft, &arabic, 0, &hkmap], "Xtestout")',
- \ 'qall',
- \ ]
+ let after =<< trim [CODE]
+ call writefile([&rightleft, &arabic, 0, &hkmap], "Xtestout")
+ qall
+ [CODE]
+
" Use silent Ex mode to avoid the hit-Enter prompt for the warning that
" 'encoding' is not utf-8.
if has('arabic') && &encoding == 'utf-8' && RunVim([], after, '-e -s -A')
@@ -423,10 +431,11 @@ func Test_invalid_args()
endfunc
func Test_file_args()
- let after = [
- \ 'call writefile(argv(), "Xtestout")',
- \ 'qall',
- \ ]
+ let after =<< trim [CODE]
+ call writefile(argv(), "Xtestout")
+ qall
+ [CODE]
+
if RunVim([], after, '')
let lines = readfile('Xtestout')
call assert_equal(0, len(lines))
@@ -487,10 +496,11 @@ func Test_startuptime()
endfunc
func Test_read_stdin()
- let after = [
- \ 'write Xtestout',
- \ 'quit!',
- \ ]
+ let after =<< trim [CODE]
+ write Xtestout
+ quit!
+ [CODE]
+
if RunVimPiped([], after, '-', 'echo something | ')
let lines = readfile('Xtestout')
" MS-Windows adds a space after the word
@@ -540,20 +550,22 @@ endfunc
func Test_zzz_startinsert()
" Test :startinsert
call writefile(['123456'], 'Xtestout')
- let after = [
- \ ':startinsert',
- \ 'call feedkeys("foobar\<c-o>:wq\<cr>","t")'
- \ ]
+ let after =<< trim [CODE]
+ :startinsert
+ call feedkeys("foobar\<c-o>:wq\<cr>","t")
+ [CODE]
+
if RunVim([], after, 'Xtestout')
let lines = readfile('Xtestout')
call assert_equal(['foobar123456'], lines)
endif
" Test :startinsert!
call writefile(['123456'], 'Xtestout')
- let after = [
- \ ':startinsert!',
- \ 'call feedkeys("foobar\<c-o>:wq\<cr>","t")'
- \ ]
+ let after =<< trim [CODE]
+ :startinsert!
+ call feedkeys("foobar\<c-o>:wq\<cr>","t")
+ [CODE]
+
if RunVim([], after, 'Xtestout')
let lines = readfile('Xtestout')
call assert_equal(['123456foobar'], lines)
diff --git a/src/nvim/testdir/test_statusline.vim b/src/nvim/testdir/test_statusline.vim
index b86340a23a..48ec777ffd 100644
--- a/src/nvim/testdir/test_statusline.vim
+++ b/src/nvim/testdir/test_statusline.vim
@@ -347,3 +347,25 @@ func Test_statusline()
set laststatus&
set splitbelow&
endfunc
+
+func Test_statusline_visual()
+ func CallWordcount()
+ call wordcount()
+ endfunc
+ new x1
+ setl statusline=count=%{CallWordcount()}
+ " buffer must not be empty
+ call setline(1, 'hello')
+
+ " window with more lines than x1
+ new x2
+ call setline(1, range(10))
+ $
+ " Visual mode in line below liast line in x1 should not give ml_get error
+ call feedkeys("\<C-V>", "xt")
+ redraw
+
+ delfunc CallWordcount
+ bwipe! x1
+ bwipe! x2
+endfunc
diff --git a/src/nvim/testdir/test_substitute.vim b/src/nvim/testdir/test_substitute.vim
index b29b678129..e94bd22cea 100644
--- a/src/nvim/testdir/test_substitute.vim
+++ b/src/nvim/testdir/test_substitute.vim
@@ -149,6 +149,7 @@ func Run_SubCmd_Tests(tests)
for t in a:tests
let start = line('.') + 1
let end = start + len(t[2]) - 1
+ " TODO: why is there a one second delay the first time we get here?
exe "normal o" . t[0]
call cursor(start, 1)
exe t[1]
@@ -240,7 +241,7 @@ func Test_sub_cmd_3()
call Run_SubCmd_Tests(tests)
endfunc
-" Test for submatch() on :substitue.
+" Test for submatch() on :substitute.
func Test_sub_cmd_4()
set magic&
set cpo&
@@ -717,3 +718,12 @@ one two
close!
endfunc
+
+func Test_sub_beyond_end()
+ new
+ call setline(1, '#')
+ let @/ = '^#\n\zs'
+ s///e
+ call assert_equal('#', getline(1))
+ bwipe!
+endfunc
diff --git a/src/nvim/testdir/test_swap.vim b/src/nvim/testdir/test_swap.vim
index 11eb324488..e072e9ed7f 100644
--- a/src/nvim/testdir/test_swap.vim
+++ b/src/nvim/testdir/test_swap.vim
@@ -221,3 +221,87 @@ func Test_swapfile_delete()
augroup END
augroup! test_swapfile_delete
endfunc
+
+func Test_swap_recover()
+ autocmd! SwapExists
+ augroup test_swap_recover
+ autocmd!
+ autocmd SwapExists * let v:swapchoice = 'r'
+ augroup END
+
+
+ call mkdir('Xswap')
+ let $Xswap = 'foo' " Check for issue #4369.
+ set dir=Xswap//
+ " Create a valid swapfile by editing a file.
+ split Xswap/text
+ call setline(1, ['one', 'two', 'three'])
+ write " file is written, not modified
+ " read the swapfile as a Blob
+ let swapfile_name = swapname('%')
+ let swapfile_bytes = readfile(swapfile_name, 'B')
+
+ " Close the file and recreate the swap file.
+ quit
+ call writefile(swapfile_bytes, swapfile_name)
+ " Edit the file again. This triggers recovery.
+ try
+ split Xswap/text
+ catch
+ " E308 should be caught, not E305.
+ call assert_exception('E308:') " Original file may have been changed
+ endtry
+ " The file should be recovered.
+ call assert_equal(['one', 'two', 'three'], getline(1, 3))
+ quit!
+
+ call delete('Xswap/text')
+ call delete(swapfile_name)
+ call delete('Xswap', 'd')
+ unlet $Xswap
+ set dir&
+ augroup test_swap_recover
+ autocmd!
+ augroup END
+ augroup! test_swap_recover
+endfunc
+
+func Test_swap_recover_ext()
+ autocmd! SwapExists
+ augroup test_swap_recover_ext
+ autocmd!
+ autocmd SwapExists * let v:swapchoice = 'r'
+ augroup END
+
+
+ " Create a valid swapfile by editing a file with a special extension.
+ split Xtest.scr
+ call setline(1, ['one', 'two', 'three'])
+ write " file is written, not modified
+ write " write again to make sure the swapfile is created
+ " read the swapfile as a Blob
+ let swapfile_name = swapname('%')
+ let swapfile_bytes = readfile(swapfile_name, 'B')
+
+ " Close and delete the file and recreate the swap file.
+ quit
+ call delete('Xtest.scr')
+ call writefile(swapfile_bytes, swapfile_name)
+ " Edit the file again. This triggers recovery.
+ try
+ split Xtest.scr
+ catch
+ " E308 should be caught, not E306.
+ call assert_exception('E308:') " Original file may have been changed
+ endtry
+ " The file should be recovered.
+ call assert_equal(['one', 'two', 'three'], getline(1, 3))
+ quit!
+
+ call delete('Xtest.scr')
+ call delete(swapfile_name)
+ augroup test_swap_recover_ext
+ autocmd!
+ augroup END
+ augroup! test_swap_recover_ext
+endfunc
diff --git a/src/nvim/testdir/test_syntax.vim b/src/nvim/testdir/test_syntax.vim
index 598a00476c..6cada1503f 100644
--- a/src/nvim/testdir/test_syntax.vim
+++ b/src/nvim/testdir/test_syntax.vim
@@ -1,6 +1,7 @@
" Test for syntax and syntax iskeyword option
source view_util.vim
+source screendump.vim
func GetSyntaxItem(pat)
let c = ''
@@ -501,9 +502,7 @@ func Test_syntax_c()
endif
call writefile([
\ '/* comment line at the top */',
- \ ' int',
- \ 'main(int argc, char **argv)// another comment',
- \ '{',
+ \ 'int main(int argc, char **argv) { // another comment',
\ '#if 0',
\ ' int not_used;',
\ '#else',
@@ -518,6 +517,7 @@ func Test_syntax_c()
\ ' for (int i = 0; i < count; ++i) {',
\ ' break;',
\ ' }',
+ \ " Note: asdf",
\ '}',
\ ], 'Xtest.c')
@@ -526,7 +526,8 @@ func Test_syntax_c()
let $COLORFGBG = '15;0'
let buf = RunVimInTerminal('Xtest.c', {})
- call VerifyScreenDump(buf, 'Test_syntax_c_01')
+ call term_sendkeys(buf, ":syn keyword Search Note\r")
+ call VerifyScreenDump(buf, 'Test_syntax_c_01', {})
call StopVimInTerminal(buf)
let $COLORFGBG = ''
diff --git a/src/nvim/testdir/test_tagfunc.vim b/src/nvim/testdir/test_tagfunc.vim
new file mode 100644
index 0000000000..242aa3a235
--- /dev/null
+++ b/src/nvim/testdir/test_tagfunc.vim
@@ -0,0 +1,84 @@
+" Test 'tagfunc'
+
+func TagFunc(pat, flag, info)
+ let g:tagfunc_args = [a:pat, a:flag, a:info]
+ let tags = []
+ for num in range(1,10)
+ let tags += [{
+ \ 'cmd': '2', 'name': 'nothing'.num, 'kind': 'm',
+ \ 'filename': 'Xfile1', 'user_data': 'somedata'.num,
+ \}]
+ endfor
+ return tags
+endfunc
+
+func Test_tagfunc()
+ set tagfunc=TagFunc
+ new Xfile1
+ call setline(1, ['empty', 'one()', 'empty'])
+ write
+
+ call assert_equal({'cmd': '2', 'static': 0,
+ \ 'name': 'nothing2', 'user_data': 'somedata2',
+ \ 'kind': 'm', 'filename': 'Xfile1'}, taglist('.')[1])
+
+ call settagstack(win_getid(), {'items': []})
+
+ tag arbitrary
+ call assert_equal('arbitrary', g:tagfunc_args[0])
+ call assert_equal('', g:tagfunc_args[1])
+ call assert_equal('somedata1', gettagstack().items[0].user_data)
+ 5tag arbitrary
+ call assert_equal('arbitrary', g:tagfunc_args[0])
+ call assert_equal('', g:tagfunc_args[1])
+ call assert_equal('somedata5', gettagstack().items[1].user_data)
+ pop
+ tag
+ call assert_equal('arbitrary', g:tagfunc_args[0])
+ call assert_equal('', g:tagfunc_args[1])
+ call assert_equal('somedata5', gettagstack().items[1].user_data)
+
+ let g:tagfunc_args=[]
+ execute "normal! \<c-]>"
+ call assert_equal('one', g:tagfunc_args[0])
+ call assert_equal('c', g:tagfunc_args[1])
+
+ set cpt=t
+ let g:tagfunc_args=[]
+ execute "normal! i\<c-n>\<c-y>"
+ call assert_equal('ci', g:tagfunc_args[1])
+ call assert_equal('nothing1', getline('.')[0:7])
+
+ func BadTagFunc1(...)
+ return 0
+ endfunc
+ func BadTagFunc2(...)
+ return [1]
+ endfunc
+ func BadTagFunc3(...)
+ return [{'name': 'foo'}]
+ endfunc
+
+ for &tagfunc in ['BadTagFunc1', 'BadTagFunc2', 'BadTagFunc3']
+ try
+ tag nothing
+ call assert_false(1, 'tag command should have failed')
+ catch
+ call assert_exception('E987:')
+ endtry
+ exe 'delf' &tagfunc
+ endfor
+
+ func NullTagFunc(...)
+ return v:null
+ endfunc
+ set tags= tfu=NullTagFunc
+ call assert_fails('tag nothing', 'E426')
+ delf NullTagFunc
+
+ bwipe!
+ set tags& tfu& cpt&
+ call delete('Xfile1')
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/test_tagjump.vim b/src/nvim/testdir/test_tagjump.vim
index ce527a5e1d..fe98ef1ae2 100644
--- a/src/nvim/testdir/test_tagjump.vim
+++ b/src/nvim/testdir/test_tagjump.vim
@@ -449,7 +449,8 @@ func Test_tag_line_toolong()
call assert_report(v:exception)
catch /.*/
endtry
- call assert_equal('Ignoring long line in tags file', split(execute('messages'), '\n')[-1])
+ call assert_equal('Searching tags file Xtags', split(execute('messages'), '\n')[-1])
+
call writefile([
\ '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567 django/contrib/admin/templates/admin/edit_inline/stacked.html 16;" j line:16 language:HTML'
\ ], 'Xtags')
@@ -460,10 +461,52 @@ func Test_tag_line_toolong()
call assert_report(v:exception)
catch /.*/
endtry
- call assert_equal('Ignoring long line in tags file', split(execute('messages'), '\n')[-1])
+ call assert_equal('Searching tags file Xtags', split(execute('messages'), '\n')[-1])
+
+ " binary search works in file with long line
+ call writefile([
+ \ 'asdfasfd nowhere 16',
+ \ 'foobar Xsomewhere 3; " 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567',
+ \ 'zasdfasfd nowhere 16',
+ \ ], 'Xtags')
+ call writefile([
+ \ 'one',
+ \ 'two',
+ \ 'trhee',
+ \ 'four',
+ \ ], 'Xsomewhere')
+ tag foobar
+ call assert_equal('Xsomewhere', expand('%'))
+ call assert_equal(3, getcurpos()[1])
+
call delete('Xtags')
+ call delete('Xsomewhere')
set tags&
let &verbose = old_vbs
endfunc
+func Test_tagline()
+ call writefile([
+ \ 'provision Xtest.py /^ def provision(self, **kwargs):$/;" m line:1 language:Python class:Foo',
+ \ 'provision Xtest.py /^ def provision(self, **kwargs):$/;" m line:3 language:Python class:Bar',
+ \], 'Xtags')
+ call writefile([
+ \ ' def provision(self, **kwargs):',
+ \ ' pass',
+ \ ' def provision(self, **kwargs):',
+ \ ' pass',
+ \], 'Xtest.py')
+
+ set tags=Xtags
+
+ 1tag provision
+ call assert_equal(line('.'), 1)
+ 2tag provision
+ call assert_equal(line('.'), 3)
+
+ call delete('Xtags')
+ call delete('Xtest.py')
+ set tags&
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/test_textobjects.vim b/src/nvim/testdir/test_textobjects.vim
index 9194e0014d..b20c4df311 100644
--- a/src/nvim/testdir/test_textobjects.vim
+++ b/src/nvim/testdir/test_textobjects.vim
@@ -46,8 +46,18 @@ func Test_quote_selection_selection_exclusive()
new
call setline(1, "a 'bcde' f")
set selection=exclusive
+
exe "norm! fdvhi'y"
call assert_equal('bcde', @")
+
+ let @"='dummy'
+ exe "norm! $gevi'y"
+ call assert_equal('bcde', @")
+
+ let @"='dummy'
+ exe "norm! 0fbhvi'y"
+ call assert_equal('bcde', @")
+
set selection&vim
bw!
endfunc
diff --git a/src/nvim/testdir/test_timers.vim b/src/nvim/testdir/test_timers.vim
index 24c735865c..bd63d94729 100644
--- a/src/nvim/testdir/test_timers.vim
+++ b/src/nvim/testdir/test_timers.vim
@@ -141,7 +141,7 @@ endfunc
func Test_delete_myself()
let g:called = 0
let t = timer_start(10, 'StopMyself', {'repeat': -1})
- call WaitFor('g:called == 2')
+ call WaitForAssert({-> assert_equal(2, g:called)})
call assert_equal(2, g:called)
call assert_equal([], timer_info(t))
endfunc
@@ -208,7 +208,7 @@ func Test_timer_errors()
let g:call_count = 0
let timer = timer_start(10, 'FuncWithError', {'repeat': -1})
" Timer will be stopped after failing 3 out of 3 times.
- call WaitFor('g:call_count == 3')
+ call WaitForAssert({-> assert_equal(3, g:call_count)})
sleep 50m
call assert_equal(3, g:call_count)
endfunc
@@ -226,7 +226,7 @@ func Test_timer_catch_error()
let g:call_count = 0
let timer = timer_start(10, 'FuncWithCaughtError', {'repeat': 4})
" Timer will not be stopped.
- call WaitFor('g:call_count == 4')
+ call WaitForAssert({-> assert_equal(4, g:call_count)})
sleep 50m
call assert_equal(4, g:call_count)
endfunc
@@ -252,4 +252,15 @@ func Test_peek_and_get_char()
call timer_stop(intr)
endfunc
+func Test_ex_mode()
+ " Function with an empty line.
+ func Foo(...)
+
+ endfunc
+ let timer = timer_start(40, function('g:Foo'), {'repeat':-1})
+ " This used to throw error E749.
+ exe "normal Qsleep 100m\rvi\r"
+ call timer_stop(timer)
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/test_vimscript.vim b/src/nvim/testdir/test_vimscript.vim
index f39e53d6dd..d2f13ff072 100644
--- a/src/nvim/testdir/test_vimscript.vim
+++ b/src/nvim/testdir/test_vimscript.vim
@@ -1284,7 +1284,7 @@ func s:DoNothing()
endfunc
func Test_script_local_func()
- set nocp viminfo+=nviminfo
+ set nocp nomore viminfo+=nviminfo
new
nnoremap <buffer> _x :call <SID>DoNothing()<bar>call <SID>DoLast()<bar>delfunc <SID>DoNothing<bar>delfunc <SID>DoLast<cr>
@@ -1409,6 +1409,76 @@ func Test_compound_assignment_operators()
let @/ = ''
endfunc
+func Test_function_defined_line()
+ if has('gui_running')
+ " Can't catch the output of gvim.
+ return
+ endif
+
+ let lines =<< trim [CODE]
+ " F1
+ func F1()
+ " F2
+ func F2()
+ "
+ "
+ "
+ return
+ endfunc
+ " F3
+ execute "func F3()\n\n\n\nreturn\nendfunc"
+ " F4
+ execute "func F4()\n
+ \\n
+ \\n
+ \\n
+ \return\n
+ \endfunc"
+ endfunc
+ " F5
+ execute "func F5()\n\n\n\nreturn\nendfunc"
+ " F6
+ execute "func F6()\n
+ \\n
+ \\n
+ \\n
+ \return\n
+ \endfunc"
+ call F1()
+ verbose func F1
+ verbose func F2
+ verbose func F3
+ verbose func F4
+ verbose func F5
+ verbose func F6
+ qall!
+ [CODE]
+
+ call writefile(lines, 'Xtest.vim')
+ let res = system(v:progpath .. ' --clean -es -X -S Xtest.vim')
+ call assert_equal(0, v:shell_error)
+
+ let m = matchstr(res, 'function F1()[^[:print:]]*[[:print:]]*')
+ call assert_match(' line 2$', m)
+
+ let m = matchstr(res, 'function F2()[^[:print:]]*[[:print:]]*')
+ call assert_match(' line 4$', m)
+
+ let m = matchstr(res, 'function F3()[^[:print:]]*[[:print:]]*')
+ call assert_match(' line 11$', m)
+
+ let m = matchstr(res, 'function F4()[^[:print:]]*[[:print:]]*')
+ call assert_match(' line 13$', m)
+
+ let m = matchstr(res, 'function F5()[^[:print:]]*[[:print:]]*')
+ call assert_match(' line 21$', m)
+
+ let m = matchstr(res, 'function F6()[^[:print:]]*[[:print:]]*')
+ call assert_match(' line 23$', m)
+
+ call delete('Xtest.vim')
+endfunc
+
"-------------------------------------------------------------------------------
" Modelines {{{1
" vim: ts=8 sw=4 tw=80 fdm=marker
diff --git a/src/nvim/testdir/test_virtualedit.vim b/src/nvim/testdir/test_virtualedit.vim
index 67adede8d7..8f992f7501 100644
--- a/src/nvim/testdir/test_virtualedit.vim
+++ b/src/nvim/testdir/test_virtualedit.vim
@@ -73,3 +73,147 @@ func Test_edit_CTRL_G()
bwipe!
set virtualedit=
endfunc
+
+func Test_edit_change()
+ new
+ set virtualedit=all
+ call setline(1, "\t⒌")
+ normal Cx
+ call assert_equal('x', getline(1))
+ bwipe!
+endfunc
+
+" Insert "keyword keyw", ESC, C CTRL-N, shows "keyword ykeyword".
+" Repeating CTRL-N fixes it. (Mary Ellen Foster)
+func Test_ve_completion()
+ new
+ set completeopt&vim
+ set virtualedit=all
+ exe "normal ikeyword keyw\<Esc>C\<C-N>"
+ call assert_equal('keyword keyword', getline(1))
+ bwipe!
+ set virtualedit=
+endfunc
+
+" Using "C" then then <CR> moves the last remaining character to the next
+" line. (Mary Ellen Foster)
+func Test_ve_del_to_eol()
+ new
+ set virtualedit=all
+ call append(0, 'all your base are belong to us')
+ call search('are', 'w')
+ exe "normal C\<CR>are belong to vim"
+ call assert_equal(['all your base ', 'are belong to vim'], getline(1, 2))
+ bwipe!
+ set virtualedit=
+endfunc
+
+" When past the end of a line that ends in a single character "b" skips
+" that word.
+func Test_ve_b_past_eol()
+ new
+ set virtualedit=all
+ call append(0, '1 2 3 4 5 6')
+ normal gg^$15lbC7
+ call assert_equal('1 2 3 4 5 7', getline(1))
+ bwipe!
+ set virtualedit=
+endfunc
+
+" Make sure 'i', 'C', 'a', 'A' and 'D' works
+func Test_ve_ins_del()
+ new
+ set virtualedit=all
+ call append(0, ["'i'", "'C'", "'a'", "'A'", "'D'"])
+ call cursor(1, 1)
+ normal $4lix
+ call assert_equal("'i' x", getline(1))
+ call cursor(2, 1)
+ normal $4lCx
+ call assert_equal("'C' x", getline(2))
+ call cursor(3, 1)
+ normal $4lax
+ call assert_equal("'a' x", getline(3))
+ call cursor(4, 1)
+ normal $4lAx
+ call assert_equal("'A'x", getline(4))
+ call cursor(5, 1)
+ normal $4lDix
+ call assert_equal("'D' x", getline(5))
+ bwipe!
+ set virtualedit=
+endfunc
+
+" Test for yank bug reported by Mark Waggoner.
+func Test_yank_block()
+ new
+ set virtualedit=block
+ call append(0, repeat(['this is a test'], 3))
+ exe "normal gg^2w\<C-V>3jy"
+ call assert_equal("a\na\na\n ", @")
+ bwipe!
+ set virtualedit=
+endfunc
+
+" Test "r" beyond the end of the line
+func Test_replace_after_eol()
+ new
+ set virtualedit=all
+ call append(0, '"r"')
+ normal gg$5lrxa
+ call assert_equal('"r" x', getline(1))
+ bwipe!
+ set virtualedit=
+endfunc
+
+" Test "r" on a tab
+" Note that for this test, 'ts' must be 8 (the default).
+func Test_replace_on_tab()
+ new
+ set virtualedit=all
+ call append(0, "'r'\t")
+ normal gg^5lrxAy
+ call assert_equal("'r' x y", getline(1))
+ bwipe!
+ set virtualedit=
+endfunc
+
+" Test to make sure 'x' can delete control characters
+func Test_ve_del_ctrl_chars()
+ new
+ set virtualedit=all
+ call append(0, "a\<C-V>b\<CR>sd")
+ set display=uhex
+ normal gg^xxxxxxi[text]
+ set display=
+ call assert_equal('[text]', getline(1))
+ bwipe!
+ set virtualedit=
+endfunc
+
+" Test for ^Y/^E due to bad w_virtcol value, reported by
+" Roy <royl@netropolis.net>.
+func Test_ins_copy_char()
+ new
+ set virtualedit=all
+ call append(0, 'abcv8efi.him2kl')
+ exe "normal gg^O\<Esc>3li\<C-E>\<Esc>4li\<C-E>\<Esc>4li\<C-E> <--"
+ exe "normal j^o\<Esc>4li\<C-Y>\<Esc>4li\<C-Y>\<Esc>4li\<C-Y> <--"
+ call assert_equal(' v i m <--', getline(1))
+ call assert_equal(' 8 . 2 <--', getline(3))
+ bwipe!
+ set virtualedit=
+endfunc
+
+" Test for yanking and pasting using the small delete register
+func Test_yank_paste_small_del_reg()
+ new
+ set virtualedit=all
+ call append(0, "foo, bar")
+ normal ggdewve"-p
+ call assert_equal(', foo', getline(1))
+ bwipe!
+ set virtualedit=
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/test_window_cmd.vim b/src/nvim/testdir/test_window_cmd.vim
index c87c0a0af4..aaa291f87d 100644
--- a/src/nvim/testdir/test_window_cmd.vim
+++ b/src/nvim/testdir/test_window_cmd.vim
@@ -69,18 +69,6 @@ function Test_window_cmd_wincmd_gf()
augroup! test_window_cmd_wincmd_gf
endfunc
-func Test_next_split_all()
- " This was causing an illegal memory access.
- n x
- norm axxx
- split
- split
- s/x
- s/x
- all
- bwipe!
-endfunc
-
func Test_window_quit()
e Xa
split Xb
@@ -117,15 +105,71 @@ func Test_window_vertical_split()
bw
endfunc
+" Test the ":wincmd ^" and "<C-W>^" commands.
func Test_window_split_edit_alternate()
- e Xa
- e Xb
+ " Test for failure when the alternate buffer/file no longer exists.
+ edit Xfoo | %bw
+ call assert_fails(':wincmd ^', 'E23')
+
+ " Test for the expected behavior when we have two named buffers.
+ edit Xfoo | edit Xbar
wincmd ^
- call assert_equal('Xa', bufname(winbufnr(1)))
- call assert_equal('Xb', bufname(winbufnr(2)))
+ call assert_equal('Xfoo', bufname(winbufnr(1)))
+ call assert_equal('Xbar', bufname(winbufnr(2)))
+ only
- bw Xa Xb
+ " Test for the expected behavior when the alternate buffer is not named.
+ enew | let l:nr1 = bufnr('%')
+ edit Xfoo | let l:nr2 = bufnr('%')
+ wincmd ^
+ call assert_equal(l:nr1, winbufnr(1))
+ call assert_equal(l:nr2, winbufnr(2))
+ only
+
+ " FIXME: this currently fails on AppVeyor, but passes locally
+ if !has('win32')
+ " Test the Normal mode command.
+ call feedkeys("\<C-W>\<C-^>", 'tx')
+ call assert_equal(l:nr2, winbufnr(1))
+ call assert_equal(l:nr1, winbufnr(2))
+ endif
+
+ %bw!
+endfunc
+
+" Test the ":[count]wincmd ^" and "[count]<C-W>^" commands.
+func Test_window_split_edit_bufnr()
+
+ %bwipeout
+ let l:nr = bufnr('%') + 1
+ call assert_fails(':execute "normal! ' . l:nr . '\<C-W>\<C-^>"', 'E92')
+ call assert_fails(':' . l:nr . 'wincmd ^', 'E16')
+ call assert_fails(':0wincmd ^', 'E16')
+
+ edit Xfoo | edit Xbar | edit Xbaz
+ let l:foo_nr = bufnr('Xfoo')
+ let l:bar_nr = bufnr('Xbar')
+ let l:baz_nr = bufnr('Xbaz')
+
+ " FIXME: this currently fails on AppVeyor, but passes locally
+ if !has('win32')
+ call feedkeys(l:foo_nr . "\<C-W>\<C-^>", 'tx')
+ call assert_equal('Xfoo', bufname(winbufnr(1)))
+ call assert_equal('Xbaz', bufname(winbufnr(2)))
+ only
+
+ call feedkeys(l:bar_nr . "\<C-W>\<C-^>", 'tx')
+ call assert_equal('Xbar', bufname(winbufnr(1)))
+ call assert_equal('Xfoo', bufname(winbufnr(2)))
+ only
+
+ execute l:baz_nr . 'wincmd ^'
+ call assert_equal('Xbaz', bufname(winbufnr(1)))
+ call assert_equal('Xbar', bufname(winbufnr(2)))
+ endif
+
+ %bw!
endfunc
func Test_window_preview()
@@ -446,6 +490,17 @@ func Test_window_newtab()
%bw!
endfunc
+func Test_next_split_all()
+ " This was causing an illegal memory access.
+ n x
+ norm axxx
+ split
+ split
+ s/x
+ s/x
+ all
+ bwipe!
+endfunc
" Tests for adjusting window and contents
func GetScreenStr(row)
@@ -485,6 +540,11 @@ func Test_window_contents()
call test_garbagecollect_now()
endfunc
+func Test_window_colon_command()
+ " This was reading invalid memory.
+ exe "norm! v\<C-W>:\<C-U>echo v:version"
+endfunc
+
func Test_access_freed_mem()
" This was accessing freed memory
au * 0 vs xxx
@@ -700,6 +760,42 @@ func Test_relative_cursor_second_line_after_resize()
let &so = so_save
endfunc
+func Test_split_noscroll()
+ let so_save = &so
+ enew
+ call setline(1, range(1, 8))
+ normal 100%
+ split
+
+ 1wincmd w
+ let winid1 = win_getid()
+ let info1 = getwininfo(winid1)[0]
+
+ 2wincmd w
+ let winid2 = win_getid()
+ let info2 = getwininfo(winid2)[0]
+
+ call assert_equal(1, info1.topline)
+ call assert_equal(1, info2.topline)
+
+ " window that fits all lines by itself, but not when split: closing other
+ " window should restore fraction.
+ only!
+ call setline(1, range(1, &lines - 10))
+ exe &lines / 4
+ let winid1 = win_getid()
+ let info1 = getwininfo(winid1)[0]
+ call assert_equal(1, info1.topline)
+ new
+ redraw
+ close
+ let info1 = getwininfo(winid1)[0]
+ call assert_equal(1, info1.topline)
+
+ bwipe!
+ let &so = so_save
+endfunc
+
" Tests for the winnr() function
func Test_winnr()
only | tabonly
@@ -745,9 +841,4 @@ func Test_winnr()
only | tabonly
endfunc
-func Test_window_colon_command()
- " This was reading invalid memory.
- exe "norm! v\<C-W>:\<C-U>echo v:version"
-endfunc
-
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/nvim/testdir/view_util.vim b/src/nvim/testdir/view_util.vim
index 29ea073f97..520f65c1e7 100644
--- a/src/nvim/testdir/view_util.vim
+++ b/src/nvim/testdir/view_util.vim
@@ -42,6 +42,7 @@ endfunction
function! NewWindow(height, width) abort
exe a:height . 'new'
exe a:width . 'vsp'
+ set winfixwidth winfixheight
redraw!
endfunction
diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c
index ed9b410a19..c71378463f 100644
--- a/src/nvim/tui/input.c
+++ b/src/nvim/tui/input.c
@@ -26,7 +26,8 @@ void tinput_init(TermInput *input, Loop *loop)
{
input->loop = loop;
input->paste = 0;
- input->in_fd = 0;
+ input->in_fd = STDIN_FILENO;
+ input->waiting_for_bg_response = 0;
input->key_buffer = rbuffer_new(KEY_BUFFER_SIZE);
uv_mutex_init(&input->key_buffer_mutex);
uv_cond_init(&input->key_buffer_cond);
@@ -35,7 +36,7 @@ void tinput_init(TermInput *input, Loop *loop)
// echo q | nvim -es
// ls *.md | xargs nvim
#ifdef WIN32
- if (!os_isatty(0)) {
+ if (!os_isatty(input->in_fd)) {
const HANDLE conin_handle = CreateFile("CONIN$",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
@@ -45,8 +46,8 @@ void tinput_init(TermInput *input, Loop *loop)
assert(input->in_fd != -1);
}
#else
- if (!os_isatty(0) && os_isatty(2)) {
- input->in_fd = 2;
+ if (!os_isatty(input->in_fd) && os_isatty(STDERR_FILENO)) {
+ input->in_fd = STDERR_FILENO;
}
#endif
input_global_fd_init(input->in_fd);
@@ -443,6 +444,9 @@ static void set_bg_deferred(void **argv)
// [1] https://en.wikipedia.org/wiki/Luma_%28video%29
static bool handle_background_color(TermInput *input)
{
+ if (input->waiting_for_bg_response <= 0) {
+ return false;
+ }
size_t count = 0;
size_t component = 0;
size_t header_size = 0;
@@ -461,8 +465,13 @@ static bool handle_background_color(TermInput *input)
header_size = 10;
num_components = 4;
} else {
+ input->waiting_for_bg_response--;
+ if (input->waiting_for_bg_response == 0) {
+ DLOG("did not get a response for terminal background query");
+ }
return false;
}
+ input->waiting_for_bg_response = 0;
rbuffer_consumed(input->read_stream.buffer, header_size);
RBUFFER_EACH(input->read_stream.buffer, c, i) {
count = i + 1;
@@ -503,6 +512,12 @@ static bool handle_background_color(TermInput *input)
}
return true;
}
+#ifdef UNIT_TESTING
+bool ut_handle_background_color(TermInput *input)
+{
+ return handle_background_color(input);
+}
+#endif
static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_,
void *data, bool eof)
diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h
index a4071fab40..77bd6fa132 100644
--- a/src/nvim/tui/input.h
+++ b/src/nvim/tui/input.h
@@ -12,6 +12,7 @@ typedef struct term_input {
// Phases: -1=all 0=disabled 1=first-chunk 2=continue 3=last-chunk
int8_t paste;
bool waiting;
+ int8_t waiting_for_bg_response;
TermKey *tk;
#if TERMKEY_VERSION_MAJOR > 0 || TERMKEY_VERSION_MINOR > 18
TermKey_Terminfo_Getstr_Hook *tk_ti_hook_fn; ///< libtermkey terminfo hook
@@ -28,4 +29,8 @@ typedef struct term_input {
# include "tui/input.h.generated.h"
#endif
+#ifdef UNIT_TESTING
+bool ut_handle_background_color(TermInput *input);
+#endif
+
#endif // NVIM_TUI_INPUT_H
diff --git a/src/nvim/tui/terminfo.c b/src/nvim/tui/terminfo.c
index 14023ce2cb..03173afe07 100644
--- a/src/nvim/tui/terminfo.c
+++ b/src/nvim/tui/terminfo.c
@@ -47,9 +47,8 @@ bool terminfo_is_bsd_console(const char *term)
// like cursor-shaping. Assume that TERM=xterm is degraded. #8644
return strequal(term, "xterm") && !!os_getenv("XTERM_VERSION");
# endif
-#else
- return false;
#endif
+ return false;
}
/// Loads a built-in terminfo db when we (unibilium) failed to load a terminfo
diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c
index 791756e5c5..60e1353000 100644
--- a/src/nvim/tui/tui.c
+++ b/src/nvim/tui/tui.c
@@ -93,7 +93,7 @@ typedef struct {
int out_fd;
bool scroll_region_is_full_screen;
bool can_change_scroll_region;
- bool can_set_lr_margin;
+ bool can_set_lr_margin; // smglr
bool can_set_left_right_margin;
bool can_scroll;
bool can_erase_chars;
@@ -220,7 +220,7 @@ static void terminfo_start(UI *ui)
data->unibi_ext.reset_cursor_style = -1;
data->unibi_ext.get_bg = -1;
data->unibi_ext.set_underline_color = -1;
- data->out_fd = 1;
+ data->out_fd = STDOUT_FILENO;
data->out_isatty = os_isatty(data->out_fd);
const char *term = os_getenv("TERM");
@@ -234,7 +234,9 @@ static void terminfo_start(UI *ui)
// Set up unibilium/terminfo.
char *termname = NULL;
if (term) {
+ os_env_var_lock();
data->ut = unibi_from_term(term);
+ os_env_var_unlock();
if (data->ut) {
termname = xstrdup(term);
}
@@ -296,6 +298,7 @@ static void terminfo_start(UI *ui)
unibi_out(ui, unibi_keypad_xmit);
unibi_out(ui, unibi_clear_screen);
// Ask the terminal to send us the background color.
+ data->input.waiting_for_bg_response = 5;
unibi_out_ext(ui, data->unibi_ext.get_bg);
// Enable bracketed paste
unibi_out_ext(ui, data->unibi_ext.enable_bracketed_paste);
@@ -312,6 +315,7 @@ static void terminfo_start(UI *ui)
uv_pipe_init(&data->write_loop, &data->output_handle.pipe, 0);
uv_pipe_open(&data->output_handle.pipe, data->out_fd);
}
+ flush_buf(ui);
}
static void terminfo_stop(UI *ui)
@@ -363,6 +367,7 @@ static void tui_terminal_after_startup(UI *ui)
// Emit this after Nvim startup, not during. This works around a tmux
// 2.3 bug(?) which caused slow drawing during startup. #7649
unibi_out_ext(ui, data->unibi_ext.enable_focus_reporting);
+ flush_buf(ui);
}
static void tui_terminal_stop(UI *ui)
@@ -430,9 +435,6 @@ static void tui_main(UIBridgeData *bridge, UI *ui)
}
if (!tui_is_stopped(ui)) {
tui_terminal_after_startup(ui);
- // Tickle `main_loop` with a dummy event, else the initial "focus-gained"
- // terminal response may not get processed until user hits a key.
- loop_schedule_deferred(&main_loop, event_create(loop_dummy_event, 0));
}
// "Passive" (I/O-driven) loop: TUI thread "main loop".
while (!tui_is_stopped(ui)) {
@@ -513,20 +515,8 @@ static void update_attrs(UI *ui, int attr_id)
}
data->print_attr_id = attr_id;
HlAttrs attrs = kv_A(data->attrs, (size_t)attr_id);
-
- int fg = ui->rgb ? attrs.rgb_fg_color : (attrs.cterm_fg_color - 1);
- if (fg == -1) {
- fg = ui->rgb ? data->clear_attrs.rgb_fg_color
- : (data->clear_attrs.cterm_fg_color - 1);
- }
-
- int bg = ui->rgb ? attrs.rgb_bg_color : (attrs.cterm_bg_color - 1);
- if (bg == -1) {
- bg = ui->rgb ? data->clear_attrs.rgb_bg_color
- : (data->clear_attrs.cterm_bg_color - 1);
- }
-
int attr = ui->rgb ? attrs.rgb_ae_attr : attrs.cterm_ae_attr;
+
bool bold = attr & HL_BOLD;
bool italic = attr & HL_ITALIC;
bool reverse = attr & HL_INVERSE;
@@ -594,14 +584,29 @@ static void update_attrs(UI *ui, int attr_id)
unibi_out_ext(ui, data->unibi_ext.set_underline_color);
}
}
- if (ui->rgb) {
+
+ int fg, bg;
+ if (ui->rgb && !(attr & HL_FG_INDEXED)) {
+ fg = ((attrs.rgb_fg_color != -1)
+ ? attrs.rgb_fg_color : data->clear_attrs.rgb_fg_color);
if (fg != -1) {
UNIBI_SET_NUM_VAR(data->params[0], (fg >> 16) & 0xff); // red
UNIBI_SET_NUM_VAR(data->params[1], (fg >> 8) & 0xff); // green
UNIBI_SET_NUM_VAR(data->params[2], fg & 0xff); // blue
unibi_out_ext(ui, data->unibi_ext.set_rgb_foreground);
}
+ } else {
+ fg = (attrs.cterm_fg_color
+ ? attrs.cterm_fg_color - 1 : (data->clear_attrs.cterm_fg_color - 1));
+ if (fg != -1) {
+ UNIBI_SET_NUM_VAR(data->params[0], fg);
+ unibi_out(ui, unibi_set_a_foreground);
+ }
+ }
+ if (ui->rgb && !(attr & HL_BG_INDEXED)) {
+ bg = ((attrs.rgb_bg_color != -1)
+ ? attrs.rgb_bg_color : data->clear_attrs.rgb_bg_color);
if (bg != -1) {
UNIBI_SET_NUM_VAR(data->params[0], (bg >> 16) & 0xff); // red
UNIBI_SET_NUM_VAR(data->params[1], (bg >> 8) & 0xff); // green
@@ -609,17 +614,15 @@ static void update_attrs(UI *ui, int attr_id)
unibi_out_ext(ui, data->unibi_ext.set_rgb_background);
}
} else {
- if (fg != -1) {
- UNIBI_SET_NUM_VAR(data->params[0], fg);
- unibi_out(ui, unibi_set_a_foreground);
- }
-
+ bg = (attrs.cterm_bg_color
+ ? attrs.cterm_bg_color - 1 : (data->clear_attrs.cterm_bg_color - 1));
if (bg != -1) {
UNIBI_SET_NUM_VAR(data->params[0], bg);
unibi_out(ui, unibi_set_a_background);
}
}
+
data->default_attr = fg == -1 && bg == -1
&& !bold && !italic && !underline && !undercurl && !reverse && !standout
&& !strikethrough;
@@ -1598,6 +1601,12 @@ static void patch_terminfo_bugs(TUIData *data, const char *term,
unibi_set_if_empty(ut, unibi_set_lr_margin, "\x1b[%i%p1%d;%p2%ds");
unibi_set_if_empty(ut, unibi_set_left_margin_parm, "\x1b[%i%p1%ds");
unibi_set_if_empty(ut, unibi_set_right_margin_parm, "\x1b[%i;%p2%ds");
+ } else {
+ // Fix things advertised via TERM=xterm, for non-xterm.
+ if (unibi_get_str(ut, unibi_set_lr_margin)) {
+ ILOG("Disabling smglr with TERM=xterm for non-xterm.");
+ unibi_set_str(ut, unibi_set_lr_margin, NULL);
+ }
}
#ifdef WIN32
diff --git a/src/nvim/ui_compositor.c b/src/nvim/ui_compositor.c
index 20ffc1b88e..e582d8f859 100644
--- a/src/nvim/ui_compositor.c
+++ b/src/nvim/ui_compositor.c
@@ -425,6 +425,15 @@ static void compose_line(Integer row, Integer startcol, Integer endcol,
flags = flags & ~kLineFlagWrap;
}
+ for (int i = skipstart; i < (endcol-skipend)-startcol; i++) {
+ if (attrbuf[i] < 0) {
+ if (rdb_flags & RDB_INVALID) {
+ abort();
+ } else {
+ attrbuf[i] = 0;
+ }
+ }
+ }
ui_composed_call_raw_line(1, row, startcol+skipstart,
endcol-skipend, endcol-skipend, 0, flags,
(const schar_T *)linebuf+skipstart,
@@ -535,6 +544,11 @@ static void ui_comp_raw_line(UI *ui, Integer grid, Integer row,
} else {
compose_debug(row, row+1, startcol, endcol, dbghl_normal, false);
compose_debug(row, row+1, endcol, clearcol, dbghl_clear, true);
+#ifndef NDEBUG
+ for (int i = 0; i < endcol-startcol; i++) {
+ assert(attrs[i] >= 0);
+ }
+#endif
ui_composed_call_raw_line(1, row, startcol, endcol, clearcol, clearattr,
flags, chunk, attrs);
}
@@ -612,7 +626,7 @@ static void ui_comp_grid_scroll(UI *ui, Integer grid, Integer top,
if (covered || curgrid->blending) {
// TODO(bfredl):
// 1. check if rectangles actually overlap
- // 2. calulate subareas that can scroll.
+ // 2. calculate subareas that can scroll.
compose_debug(top, bot, left, right, dbghl_recompose, true);
for (int r = (int)(top + MAX(-rows, 0)); r < bot - MAX(rows, 0); r++) {
// TODO(bfredl): workaround for win_update() performing two scrolls in a
diff --git a/src/nvim/undo.c b/src/nvim/undo.c
index 035613c7fd..539d42765d 100644
--- a/src/nvim/undo.c
+++ b/src/nvim/undo.c
@@ -91,7 +91,9 @@
#include "nvim/fileio.h"
#include "nvim/fold.h"
#include "nvim/buffer_updates.h"
+#include "nvim/pos.h" // MAXLNUM
#include "nvim/mark.h"
+#include "nvim/mark_extended.h"
#include "nvim/memline.h"
#include "nvim/message.h"
#include "nvim/misc1.h"
@@ -106,6 +108,7 @@
#include "nvim/types.h"
#include "nvim/os/os.h"
#include "nvim/os/time.h"
+#include "nvim/lib/kvec.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "undo.c.generated.h"
@@ -222,9 +225,6 @@ int u_save_cursor(void)
*/
int u_save(linenr_T top, linenr_T bot)
{
- if (undo_off)
- return OK;
-
if (top >= bot || bot > (curbuf->b_ml.ml_line_count + 1)) {
return FAIL; /* rely on caller to do error messages */
}
@@ -243,10 +243,7 @@ int u_save(linenr_T top, linenr_T bot)
*/
int u_savesub(linenr_T lnum)
{
- if (undo_off)
- return OK;
-
- return u_savecommon(lnum - 1, lnum + 1, lnum + 1, FALSE);
+ return u_savecommon(lnum - 1, lnum + 1, lnum + 1, false);
}
/*
@@ -257,10 +254,7 @@ int u_savesub(linenr_T lnum)
*/
int u_inssub(linenr_T lnum)
{
- if (undo_off)
- return OK;
-
- return u_savecommon(lnum - 1, lnum, lnum + 1, FALSE);
+ return u_savecommon(lnum - 1, lnum, lnum + 1, false);
}
/*
@@ -272,9 +266,6 @@ int u_inssub(linenr_T lnum)
*/
int u_savedel(linenr_T lnum, long nlines)
{
- if (undo_off)
- return OK;
-
return u_savecommon(lnum - 1, lnum + nlines,
nlines == curbuf->b_ml.ml_line_count ? 2 : lnum, FALSE);
}
@@ -384,6 +375,7 @@ int u_savecommon(linenr_T top, linenr_T bot, linenr_T newbot, int reload)
* up the undo info when out of memory.
*/
uhp = xmalloc(sizeof(u_header_T));
+ kv_init(uhp->uh_extmark);
#ifdef U_DEBUG
uhp->uh_magic = UH_MAGIC;
#endif
@@ -2249,10 +2241,10 @@ static void u_undoredo(int undo, bool do_buf_event)
xfree((char_u *)uep->ue_array);
}
- /* adjust marks */
+ // Adjust marks
if (oldsize != newsize) {
mark_adjust(top + 1, top + oldsize, (long)MAXLNUM,
- (long)newsize - (long)oldsize, false);
+ (long)newsize - (long)oldsize, false, kExtmarkNOOP);
if (curbuf->b_op_start.lnum > top + oldsize) {
curbuf->b_op_start.lnum += newsize - oldsize;
}
@@ -2285,6 +2277,23 @@ static void u_undoredo(int undo, bool do_buf_event)
newlist = uep;
}
+ // Adjust Extmarks
+ ExtmarkUndoObject undo_info;
+ if (undo) {
+ for (i = (int)kv_size(curhead->uh_extmark) - 1; i > -1; i--) {
+ undo_info = kv_A(curhead->uh_extmark, i);
+ extmark_apply_undo(undo_info, undo);
+ }
+ // redo
+ } else {
+ for (i = 0; i < (int)kv_size(curhead->uh_extmark); i++) {
+ undo_info = kv_A(curhead->uh_extmark, i);
+ extmark_apply_undo(undo_info, undo);
+ }
+ }
+ // finish Adjusting extmarks
+
+
curhead->uh_entry = newlist;
curhead->uh_flags = new_flags;
if ((old_flags & UH_EMPTYBUF) && BUFEMPTY()) {
@@ -2828,6 +2837,8 @@ u_freeentries(
u_freeentry(uep, uep->ue_size);
}
+ kv_destroy(uhp->uh_extmark);
+
#ifdef U_DEBUG
uhp->uh_magic = 0;
#endif
@@ -2902,9 +2913,6 @@ void u_undoline(void)
colnr_T t;
char_u *oldp;
- if (undo_off)
- return;
-
if (curbuf->b_u_line_ptr == NULL
|| curbuf->b_u_line_lnum > curbuf->b_ml.ml_line_count) {
beep_flush();
@@ -3022,3 +3030,34 @@ list_T *u_eval_tree(const u_header_T *const first_uhp)
return list;
}
+
+// Given the buffer, Return the undo header. If none is set, set one first.
+// NULL will be returned if e.g undolevels = -1 (undo disabled)
+u_header_T *u_force_get_undo_header(buf_T *buf)
+{
+ u_header_T *uhp = NULL;
+ if (buf->b_u_curhead != NULL) {
+ uhp = buf->b_u_curhead;
+ } else if (buf->b_u_newhead) {
+ uhp = buf->b_u_newhead;
+ }
+ // Create the first undo header for the buffer
+ if (!uhp) {
+ // Undo is normally invoked in change code, which already has swapped
+ // curbuf.
+ buf_T *save_curbuf = curbuf;
+ curbuf = buf;
+ // Args are tricky: this means replace empty range by empty range..
+ u_savecommon(0, 1, 1, true);
+ curbuf = save_curbuf;
+
+ uhp = buf->b_u_curhead;
+ if (!uhp) {
+ uhp = buf->b_u_newhead;
+ if (get_undolevel() > 0 && !uhp) {
+ abort();
+ }
+ }
+ }
+ return uhp;
+}
diff --git a/src/nvim/undo_defs.h b/src/nvim/undo_defs.h
index 6c7e2bba41..0fa3b415ec 100644
--- a/src/nvim/undo_defs.h
+++ b/src/nvim/undo_defs.h
@@ -4,6 +4,7 @@
#include <time.h> // for time_t
#include "nvim/pos.h"
+#include "nvim/mark_extended_defs.h"
#include "nvim/mark_defs.h"
typedef struct u_header u_header_T;
@@ -56,14 +57,15 @@ struct u_header {
u_entry_T *uh_getbot_entry; /* pointer to where ue_bot must be set */
pos_T uh_cursor; /* cursor position before saving */
long uh_cursor_vcol;
- int uh_flags; /* see below */
- fmark_T uh_namedm[NMARKS]; /* marks before undo/after redo */
- visualinfo_T uh_visual; /* Visual areas before undo/after redo */
- time_t uh_time; /* timestamp when the change was made */
- long uh_save_nr; /* set when the file was saved after the
- changes in this block */
+ int uh_flags; // see below
+ fmark_T uh_namedm[NMARKS]; // marks before undo/after redo
+ extmark_undo_vec_t uh_extmark; // info to move extmarks
+ visualinfo_T uh_visual; // Visual areas before undo/after redo
+ time_t uh_time; // timestamp when the change was made
+ long uh_save_nr; // set when the file was saved after the
+ // changes in this block
#ifdef U_DEBUG
- int uh_magic; /* magic number to check allocation */
+ int uh_magic; // magic number to check allocation
#endif
};
diff --git a/src/nvim/version.c b/src/nvim/version.c
index b0619d6273..4cadc9fd6c 100644
--- a/src/nvim/version.c
+++ b/src/nvim/version.c
@@ -83,7 +83,7 @@ static const int included_patches[] = {
1838,
1837,
1836,
- // 1835,
+ 1835,
1834,
1833,
1832,
@@ -106,7 +106,7 @@ static const int included_patches[] = {
1815,
1814,
1813,
- // 1812,
+ 1812,
1811,
1810,
1809,
@@ -120,12 +120,12 @@ static const int included_patches[] = {
1801,
1800,
1799,
- // 1798,
+ 1798,
1797,
1796,
1795,
// 1794,
- // 1793,
+ 1793,
1792,
1791,
1790,
@@ -142,13 +142,13 @@ static const int included_patches[] = {
1779,
1778,
1777,
- // 1776,
+ 1776,
1775,
// 1774,
1773,
- // 1772,
- // 1771,
- // 1770,
+ 1772,
+ 1771,
+ 1770,
// 1769,
1768,
// 1767,
@@ -164,7 +164,7 @@ static const int included_patches[] = {
1757,
1756,
1755,
- // 1754,
+ 1754,
1753,
1752,
1751,
@@ -176,7 +176,7 @@ static const int included_patches[] = {
// 1745,
// 1744,
// 1743,
- // 1742,
+ 1742,
1741,
1740,
1739,
@@ -185,7 +185,7 @@ static const int included_patches[] = {
1736,
1735,
1734,
- // 1733,
+ 1733,
// 1732,
1731,
1730,
@@ -193,10 +193,10 @@ static const int included_patches[] = {
1728,
1727,
1726,
- // 1725,
+ 1725,
1724,
1723,
- // 1722,
+ 1722,
1721,
1720,
1719,
@@ -207,7 +207,7 @@ static const int included_patches[] = {
1714,
1713,
// 1712,
- // 1711,
+ 1711,
1710,
1709,
1708,
@@ -233,7 +233,7 @@ static const int included_patches[] = {
1688,
1687,
1686,
- // 1685,
+ 1685,
1684,
1683,
1682,
@@ -252,8 +252,8 @@ static const int included_patches[] = {
1669,
// 1668,
1667,
- // 1666,
- // 1665,
+ 1666,
+ 1665,
1664,
1663,
1662,
@@ -294,12 +294,12 @@ static const int included_patches[] = {
1627,
1626,
1625,
- // 1624,
+ 1624,
1623,
1622,
- // 1621,
+ 1621,
1620,
- // 1619,
+ 1619,
1618,
// 1617,
// 1616,
@@ -336,7 +336,7 @@ static const int included_patches[] = {
1585,
1584,
1583,
- // 1582,
+ 1582,
1581,
1580,
1579,
@@ -379,7 +379,7 @@ static const int included_patches[] = {
1542,
1541,
// 1540,
- // 1539,
+ 1539,
// 1538,
1537,
1536,
@@ -389,13 +389,13 @@ static const int included_patches[] = {
1532,
// 1531,
1530,
- // 1529,
+ 1529,
1528,
1527,
1526,
// 1525,
1524,
- // 1523,
+ 1523,
// 1522,
1521,
// 1520,
@@ -426,7 +426,7 @@ static const int included_patches[] = {
// 1495,
1494,
1493,
- // 1492,
+ 1492,
// 1491,
1490,
1489,
@@ -467,10 +467,10 @@ static const int included_patches[] = {
// 1454,
1453,
1452,
- // 1451,
+ 1451,
1450,
- // 1449,
- // 1448,
+ 1449,
+ 1448,
1447,
1446,
1445,
@@ -513,7 +513,7 @@ static const int included_patches[] = {
1408,
1407,
1406,
- // 1405,
+ 1405,
1404,
1403,
1402,
@@ -552,7 +552,7 @@ static const int included_patches[] = {
1369,
1368,
// 1367,
- // 1366,
+ 1366,
1365,
1364,
1363,
@@ -742,7 +742,7 @@ static const int included_patches[] = {
1179,
1178,
1177,
- // 1176,
+ 1176,
1175,
1174,
1173,
@@ -784,7 +784,7 @@ static const int included_patches[] = {
1137,
1136,
1135,
- // 1134,
+ 1134,
1133,
1132,
1131,
@@ -809,7 +809,7 @@ static const int included_patches[] = {
1112,
1111,
1110,
- // 1109,
+ 1109,
1108,
1107,
1106,
@@ -880,7 +880,7 @@ static const int included_patches[] = {
1041,
1040,
1039,
- // 1038,
+ 1038,
1037,
1036,
1035,
@@ -948,7 +948,7 @@ static const int included_patches[] = {
973,
972,
971,
- // 970,
+ 970,
969,
968,
967,
@@ -977,14 +977,14 @@ static const int included_patches[] = {
944,
943,
942,
- // 941,
+ 941,
940,
939,
938,
- // 937,
+ 937,
936,
935,
- // 934,
+ 934,
933,
932,
931,
@@ -1004,7 +1004,7 @@ static const int included_patches[] = {
917,
916,
915,
- // 914,
+ 914,
913,
912,
911,
@@ -1093,7 +1093,7 @@ static const int included_patches[] = {
828,
827,
826,
- // 825,
+ 825,
824,
823,
822,
diff --git a/src/nvim/window.c b/src/nvim/window.c
index 1e6de73549..76fc36607c 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -70,8 +70,8 @@ static char *m_onlyone = N_("Already only one window");
/*
* all CTRL-W window commands are handled here, called from normal_cmd().
*/
-void
-do_window (
+void
+do_window(
int nchar,
long Prenum,
int xchar /* extra char from ":wincmd gx" or NUL */
@@ -131,8 +131,20 @@ do_window (
case '^':
CHECK_CMDWIN;
reset_VIsual_and_resel(); // stop Visual mode
- cmd_with_count("split #", (char_u *)cbuf, sizeof(cbuf), Prenum);
- do_cmdline_cmd(cbuf);
+
+ if (buflist_findnr(Prenum == 0 ? curwin->w_alt_fnum : Prenum) == NULL) {
+ if (Prenum == 0) {
+ EMSG(_(e_noalt));
+ } else {
+ EMSGN(_("E92: Buffer %" PRId64 " not found"), Prenum);
+ }
+ break;
+ }
+
+ if (!curbuf_locked() && win_split(0, 0) == OK) {
+ (void)buflist_getfile(Prenum == 0 ? curwin->w_alt_fnum : Prenum,
+ (linenr_T)0, GETF_ALT, false);
+ }
break;
/* open new window */
@@ -621,6 +633,12 @@ void win_set_minimal_style(win_T *wp)
xfree(wp->w_p_scl);
wp->w_p_scl = (char_u *)xstrdup("auto");
}
+
+ // colorcolumn: cleared
+ if (wp->w_p_cc != NULL && *wp->w_p_cc != NUL) {
+ xfree(wp->w_p_cc);
+ wp->w_p_cc = (char_u *)xstrdup("");
+ }
}
void win_config_float(win_T *wp, FloatConfig fconfig)
@@ -1518,17 +1536,22 @@ static void win_init(win_T *newp, win_T *oldp, int flags)
/* Don't copy the location list. */
newp->w_llist = NULL;
newp->w_llist_ref = NULL;
- } else
- copy_loclist(oldp, newp);
+ } else {
+ copy_loclist_stack(oldp, newp);
+ }
newp->w_localdir = (oldp->w_localdir == NULL)
? NULL : vim_strsave(oldp->w_localdir);
/* copy tagstack and folds */
for (i = 0; i < oldp->w_tagstacklen; i++) {
- newp->w_tagstack[i] = oldp->w_tagstack[i];
- if (newp->w_tagstack[i].tagname != NULL)
- newp->w_tagstack[i].tagname =
- vim_strsave(newp->w_tagstack[i].tagname);
+ taggy_T *tag = &newp->w_tagstack[i];
+ *tag = oldp->w_tagstack[i];
+ if (tag->tagname != NULL) {
+ tag->tagname = vim_strsave(tag->tagname);
+ }
+ if (tag->user_data != NULL) {
+ tag->user_data = vim_strsave(tag->user_data);
+ }
}
newp->w_tagstackidx = oldp->w_tagstackidx;
newp->w_tagstacklen = oldp->w_tagstacklen;
@@ -1558,7 +1581,7 @@ static void win_init_some(win_T *newp, win_T *oldp)
/// Check if "win" is a pointer to an existing window in the current tabpage.
///
/// @param win window to check
-bool win_valid(win_T *win) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
+bool win_valid(const win_T *win) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
if (win == NULL) {
return false;
@@ -2401,6 +2424,7 @@ int win_close(win_T *win, bool free_buf)
bool help_window = false;
tabpage_T *prev_curtab = curtab;
frame_T *win_frame = win->w_floating ? NULL : win->w_frame->fr_parent;
+ const bool had_diffmode = win->w_p_diff;
if (last_window() && !win->w_floating) {
EMSG(_("E444: Cannot close last window"));
@@ -2625,6 +2649,22 @@ int win_close(win_T *win, bool free_buf)
if (help_window)
restore_snapshot(SNAP_HELP_IDX, close_curwin);
+ // If the window had 'diff' set and now there is only one window left in
+ // the tab page with 'diff' set, and "closeoff" is in 'diffopt', then
+ // execute ":diffoff!".
+ if (diffopt_closeoff() && had_diffmode && curtab == prev_curtab) {
+ int diffcount = 0;
+
+ FOR_ALL_WINDOWS_IN_TAB(dwin, curtab) {
+ if (dwin->w_p_diff) {
+ diffcount++;
+ }
+ }
+ if (diffcount == 1) {
+ do_cmdline_cmd("diffoff!");
+ }
+ }
+
curwin->w_pos_changed = true;
redraw_all_later(NOT_VALID);
return OK;
@@ -4332,9 +4372,10 @@ static void win_goto_hor(bool left, long count)
}
}
-/*
- * Make window "wp" the current window.
- */
+/// Make window `wp` the current window.
+///
+/// @warning Autocmds may close the window immediately, so caller must check
+/// win_valid(wp).
void win_enter(win_T *wp, bool undo_sync)
{
win_enter_ext(wp, undo_sync, false, false, true, true);
@@ -4626,8 +4667,10 @@ win_free (
xfree(wp->w_lines);
- for (i = 0; i < wp->w_tagstacklen; ++i)
+ for (i = 0; i < wp->w_tagstacklen; i++) {
xfree(wp->w_tagstack[i].tagname);
+ xfree(wp->w_tagstack[i].user_data);
+ }
xfree(wp->w_localdir);
@@ -5598,10 +5641,14 @@ void scroll_to_fraction(win_T *wp, int prev_height)
int sline, line_size;
int height = wp->w_height_inner;
- // Don't change w_topline when height is zero. Don't set w_topline when
- // 'scrollbind' is set and this isn't the current window.
+ // Don't change w_topline in any of these cases:
+ // - window height is 0
+ // - 'scrollbind' is set and this isn't the current window
+ // - window height is sufficient to display the whole buffer and first line
+ // is visible.
if (height > 0
&& (!wp->w_p_scb || wp == curwin)
+ && (height < wp->w_buffer->b_ml.ml_line_count || wp->w_topline > 1)
) {
/*
* Find a value for w_topline that shows the cursor at the same
diff --git a/src/tree_sitter/LICENSE b/src/tree_sitter/LICENSE
new file mode 100644
index 0000000000..971b81f9a8
--- /dev/null
+++ b/src/tree_sitter/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 Max Brunsfeld
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/tree_sitter/alloc.h b/src/tree_sitter/alloc.h
new file mode 100644
index 0000000000..2229995bd1
--- /dev/null
+++ b/src/tree_sitter/alloc.h
@@ -0,0 +1,94 @@
+#ifndef TREE_SITTER_ALLOC_H_
+#define TREE_SITTER_ALLOC_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "nvim/memory.h"
+
+#if 1
+
+static inline bool ts_toggle_allocation_recording(bool value) {
+ return false;
+}
+
+#define ts_malloc xmalloc
+#define ts_calloc xcalloc
+#define ts_realloc xrealloc
+#define ts_free xfree
+
+#elif defined(TREE_SITTER_TEST)
+
+void *ts_record_malloc(size_t);
+void *ts_record_calloc(size_t, size_t);
+void *ts_record_realloc(void *, size_t);
+void ts_record_free(void *);
+bool ts_toggle_allocation_recording(bool);
+
+static inline void *ts_malloc(size_t size) {
+ return ts_record_malloc(size);
+}
+
+static inline void *ts_calloc(size_t count, size_t size) {
+ return ts_record_calloc(count, size);
+}
+
+static inline void *ts_realloc(void *buffer, size_t size) {
+ return ts_record_realloc(buffer, size);
+}
+
+static inline void ts_free(void *buffer) {
+ ts_record_free(buffer);
+}
+
+#else
+
+#include <stdlib.h>
+
+static inline bool ts_toggle_allocation_recording(bool value) {
+ return false;
+}
+
+static inline void *ts_malloc(size_t size) {
+ void *result = malloc(size);
+ if (size > 0 && !result) {
+ fprintf(stderr, "tree-sitter failed to allocate %lu bytes", size);
+ exit(1);
+ }
+ return result;
+}
+
+static inline void *ts_calloc(size_t count, size_t size) {
+ void *result = calloc(count, size);
+ if (count > 0 && !result) {
+ fprintf(stderr, "tree-sitter failed to allocate %lu bytes", count * size);
+ exit(1);
+ }
+ return result;
+}
+
+static inline void *ts_realloc(void *buffer, size_t size) {
+ void *result = realloc(buffer, size);
+ if (size > 0 && !result) {
+ fprintf(stderr, "tree-sitter failed to reallocate %lu bytes", size);
+ exit(1);
+ }
+ return result;
+}
+
+static inline void ts_free(void *buffer) {
+ free(buffer);
+}
+
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_ALLOC_H_
diff --git a/src/tree_sitter/api.h b/src/tree_sitter/api.h
new file mode 100644
index 0000000000..d39d0521ee
--- /dev/null
+++ b/src/tree_sitter/api.h
@@ -0,0 +1,660 @@
+#ifndef TREE_SITTER_API_H_
+#define TREE_SITTER_API_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+/****************************/
+/* Section - ABI Versioning */
+/****************************/
+
+#define TREE_SITTER_LANGUAGE_VERSION 11
+#define TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION 9
+
+/*******************/
+/* Section - Types */
+/*******************/
+
+typedef uint16_t TSSymbol;
+typedef uint16_t TSFieldId;
+typedef struct TSLanguage TSLanguage;
+typedef struct TSParser TSParser;
+typedef struct TSTree TSTree;
+
+typedef enum {
+ TSInputEncodingUTF8,
+ TSInputEncodingUTF16,
+} TSInputEncoding;
+
+typedef enum {
+ TSSymbolTypeRegular,
+ TSSymbolTypeAnonymous,
+ TSSymbolTypeAuxiliary,
+} TSSymbolType;
+
+typedef struct {
+ uint32_t row;
+ uint32_t column;
+} TSPoint;
+
+typedef struct {
+ TSPoint start_point;
+ TSPoint end_point;
+ uint32_t start_byte;
+ uint32_t end_byte;
+} TSRange;
+
+typedef struct {
+ void *payload;
+ const char *(*read)(void *payload, uint32_t byte_index, TSPoint position, uint32_t *bytes_read);
+ TSInputEncoding encoding;
+} TSInput;
+
+typedef enum {
+ TSLogTypeParse,
+ TSLogTypeLex,
+} TSLogType;
+
+typedef struct {
+ void *payload;
+ void (*log)(void *payload, TSLogType, const char *);
+} TSLogger;
+
+typedef struct {
+ uint32_t start_byte;
+ uint32_t old_end_byte;
+ uint32_t new_end_byte;
+ TSPoint start_point;
+ TSPoint old_end_point;
+ TSPoint new_end_point;
+} TSInputEdit;
+
+typedef struct {
+ uint32_t context[4];
+ const void *id;
+ const TSTree *tree;
+} TSNode;
+
+typedef struct {
+ const void *tree;
+ const void *id;
+ uint32_t context[2];
+} TSTreeCursor;
+
+/********************/
+/* Section - Parser */
+/********************/
+
+/**
+ * Create a new parser.
+ */
+TSParser *ts_parser_new(void);
+
+/**
+ * Delete the parser, freeing all of the memory that it used.
+ */
+void ts_parser_delete(TSParser *parser);
+
+/**
+ * Set the language that the parser should use for parsing.
+ *
+ * Returns a boolean indicating whether or not the language was successfully
+ * assigned. True means assignment succeeded. False means there was a version
+ * mismatch: the language was generated with an incompatible version of the
+ * Tree-sitter CLI. Check the language's version using `ts_language_version`
+ * and compare it to this library's `TREE_SITTER_LANGUAGE_VERSION` and
+ * `TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION` constants.
+ */
+bool ts_parser_set_language(TSParser *self, const TSLanguage *language);
+
+/**
+ * Get the parser's current language.
+ */
+const TSLanguage *ts_parser_language(const TSParser *self);
+
+/**
+ * Set the spans of text that the parser should include when parsing.
+ *
+ * By default, the parser will always include entire documents. This function
+ * allows you to parse only a *portion* of a document but still return a syntax
+ * tree whose ranges match up with the document as a whole. You can also pass
+ * multiple disjoint ranges.
+ *
+ * The second and third parameters specify the location and length of an array
+ * of ranges. The parser does *not* take ownership of these ranges; it copies
+ * the data, so it doesn't matter how these ranges are allocated.
+ */
+void ts_parser_set_included_ranges(
+ TSParser *self,
+ const TSRange *ranges,
+ uint32_t length
+);
+
+/**
+ * Get the ranges of text that the parser will include when parsing.
+ *
+ * The returned pointer is owned by the parser. The caller should not free it
+ * or write to it. The length of the array will be written to the given
+ * `length` pointer.
+ */
+const TSRange *ts_parser_included_ranges(
+ const TSParser *self,
+ uint32_t *length
+);
+
+/**
+ * Use the parser to parse some source code and create a syntax tree.
+ *
+ * If you are parsing this document for the first time, pass `NULL` for the
+ * `old_tree` parameter. Otherwise, if you have already parsed an earlier
+ * version of this document and the document has since been edited, pass the
+ * previous syntax tree so that the unchanged parts of it can be reused.
+ * This will save time and memory. For this to work correctly, you must have
+ * already edited the old syntax tree using the `ts_tree_edit` function in a
+ * way that exactly matches the source code changes.
+ *
+ * The `TSInput` parameter lets you specify how to read the text. It has the
+ * following three fields:
+ * 1. `read`: A function to retrieve a chunk of text at a given byte offset
+ * and (row, column) position. The function should return a pointer to the
+ * text and write its length to the the `bytes_read` pointer. The parser
+ * does not take ownership of this buffer; it just borrows it until it has
+ * finished reading it. The function should write a zero value to the
+ * `bytes_read` pointer to indicate the end of the document.
+ * 2. `payload`: An arbitrary pointer that will be passed to each invocation
+ * of the `read` function.
+ * 3. `encoding`: An indication of how the text is encoded. Either
+ * `TSInputEncodingUTF8` or `TSInputEncodingUTF16`.
+ *
+ * This function returns a syntax tree on success, and `NULL` on failure. There
+ * are three possible reasons for failure:
+ * 1. The parser does not have a language assigned. Check for this using the
+ `ts_parser_language` function.
+ * 2. Parsing was cancelled due to a timeout that was set by an earlier call to
+ * the `ts_parser_set_timeout_micros` function. You can resume parsing from
+ * where the parser left out by calling `ts_parser_parse` again with the
+ * same arguments. Or you can start parsing from scratch by first calling
+ * `ts_parser_reset`.
+ * 3. Parsing was cancelled using a cancellation flag that was set by an
+ * earlier call to `ts_parser_set_cancellation_flag`. You can resume parsing
+ * from where the parser left out by calling `ts_parser_parse` again with
+ * the same arguments.
+ */
+TSTree *ts_parser_parse(
+ TSParser *self,
+ const TSTree *old_tree,
+ TSInput input
+);
+
+/**
+ * Use the parser to parse some source code stored in one contiguous buffer.
+ * The first two parameters are the same as in the `ts_parser_parse` function
+ * above. The second two parameters indicate the location of the buffer and its
+ * length in bytes.
+ */
+TSTree *ts_parser_parse_string(
+ TSParser *self,
+ const TSTree *old_tree,
+ const char *string,
+ uint32_t length
+);
+
+/**
+ * Use the parser to parse some source code stored in one contiguous buffer with
+ * a given encoding. The first four parameters work the same as in the
+ * `ts_parser_parse_string` method above. The final parameter indicates whether
+ * the text is encoded as UTF8 or UTF16.
+ */
+TSTree *ts_parser_parse_string_encoding(
+ TSParser *self,
+ const TSTree *old_tree,
+ const char *string,
+ uint32_t length,
+ TSInputEncoding encoding
+);
+
+/**
+ * Instruct the parser to start the next parse from the beginning.
+ *
+ * If the parser previously failed because of a timeout or a cancellation, then
+ * by default, it will resume where it left off on the next call to
+ * `ts_parser_parse` or other parsing functions. If you don't want to resume,
+ * and instead intend to use this parser to parse some other document, you must
+ * call this `ts_parser_reset` first.
+ */
+void ts_parser_reset(TSParser *self);
+
+/**
+ * Set the maximum duration in microseconds that parsing should be allowed to
+ * take before halting. If parsing takes longer than this, it will halt early,
+ * returning NULL. See `ts_parser_parse` for more information.
+ */
+void ts_parser_set_timeout_micros(TSParser *self, uint64_t timeout);
+
+/**
+ * Get the duration in microseconds that parsing is allowed to take.
+ */
+uint64_t ts_parser_timeout_micros(const TSParser *self);
+
+/**
+ * Set the parser's current cancellation flag pointer. If a non-null pointer is
+ * assigned, then the parser will periodically read from this pointer during
+ * parsing. If it reads a non-zero value, it will halt early, returning NULL.
+ * See `ts_parser_parse` for more information.
+ */
+void ts_parser_set_cancellation_flag(TSParser *self, const size_t *flag);
+
+/**
+ * Get the parser's current cancellation flag pointer.
+ */
+const size_t *ts_parser_cancellation_flag(const TSParser *self);
+
+/**
+ * Set the logger that a parser should use during parsing.
+ *
+ * The parser does not take ownership over the logger payload. If a logger was
+ * previously assigned, the caller is responsible for releasing any memory
+ * owned by the previous logger.
+ */
+void ts_parser_set_logger(TSParser *self, TSLogger logger);
+
+/**
+ * Get the parser's current logger.
+ */
+TSLogger ts_parser_logger(const TSParser *self);
+
+/**
+ * Set the file descriptor to which the parser should write debugging graphs
+ * during parsing. The graphs are formatted in the DOT language. You may want
+ * to pipe these graphs directly to a `dot(1)` process in order to generate
+ * SVG output. You can turn off this logging by passing a negative number.
+ */
+void ts_parser_print_dot_graphs(TSParser *self, int file);
+
+/**
+ * Set whether or not the parser should halt immediately upon detecting an
+ * error. This will generally result in a syntax tree with an error at the
+ * root, and one or more partial syntax trees within the error. This behavior
+ * may not be supported long-term.
+ */
+void ts_parser_halt_on_error(TSParser *self, bool halt);
+
+/******************/
+/* Section - Tree */
+/******************/
+
+/**
+ * Create a shallow copy of the syntax tree. This is very fast.
+ *
+ * You need to copy a syntax tree in order to use it on more than one thread at
+ * a time, as syntax trees are not thread safe.
+ */
+TSTree *ts_tree_copy(const TSTree *self);
+
+/**
+ * Delete the syntax tree, freeing all of the memory that it used.
+ */
+void ts_tree_delete(TSTree *self);
+
+/**
+ * Get the root node of the syntax tree.
+ */
+TSNode ts_tree_root_node(const TSTree *self);
+
+/**
+ * Get the language that was used to parse the syntax tree.
+ */
+const TSLanguage *ts_tree_language(const TSTree *);
+
+/**
+ * Edit the syntax tree to keep it in sync with source code that has been
+ * edited.
+ *
+ * You must describe the edit both in terms of byte offsets and in terms of
+ * (row, column) coordinates.
+ */
+void ts_tree_edit(TSTree *self, const TSInputEdit *edit);
+
+/**
+ * Compare a new syntax tree to a previous syntax tree representing the same
+ * document, returning an array of ranges whose syntactic structure has changed.
+ *
+ * For this to work correctly, the old syntax tree must have been edited such
+ * that its ranges match up to the new tree. Generally, you'll want to call
+ * this function right after calling one of the `ts_parser_parse` functions,
+ * passing in the new tree that was returned from `ts_parser_parse` and the old
+ * tree that was passed as a parameter.
+ *
+ * The returned array is allocated using `malloc` and the caller is responsible
+ * for freeing it using `free`. The length of the array will be written to the
+ * given `length` pointer.
+ */
+TSRange *ts_tree_get_changed_ranges(
+ const TSTree *self,
+ const TSTree *old_tree,
+ uint32_t *length
+);
+
+/**
+ * Write a DOT graph describing the syntax tree to the given file.
+ */
+void ts_tree_print_dot_graph(const TSTree *, FILE *);
+
+/******************/
+/* Section - Node */
+/******************/
+
+/**
+ * Get the node's type as a null-terminated string.
+ */
+const char *ts_node_type(TSNode);
+
+/**
+ * Get the node's type as a numerical id.
+ */
+TSSymbol ts_node_symbol(TSNode);
+
+/**
+ * Get the node's start byte.
+ */
+uint32_t ts_node_start_byte(TSNode);
+
+/**
+ * Get the node's start position in terms of rows and columns.
+ */
+TSPoint ts_node_start_point(TSNode);
+
+/**
+ * Get the node's end byte.
+ */
+uint32_t ts_node_end_byte(TSNode);
+
+/**
+ * Get the node's end position in terms of rows and columns.
+ */
+TSPoint ts_node_end_point(TSNode);
+
+/**
+ * Get an S-expression representing the node as a string.
+ *
+ * This string is allocated with `malloc` and the caller is responsible for
+ * freeing it using `free`.
+ */
+char *ts_node_string(TSNode);
+
+/**
+ * Check if the node is null. Functions like `ts_node_child` and
+ * `ts_node_next_sibling` will return a null node to indicate that no such node
+ * was found.
+ */
+bool ts_node_is_null(TSNode);
+
+/**
+ * Check if the node is *named*. Named nodes correspond to named rules in the
+ * grammar, whereas *anonymous* nodes correspond to string literals in the
+ * grammar.
+ */
+bool ts_node_is_named(TSNode);
+
+/**
+ * Check if the node is *missing*. Missing nodes are inserted by the parser in
+ * order to recover from certain kinds of syntax errors.
+ */
+bool ts_node_is_missing(TSNode);
+
+/**
+ * Check if the node is *missing*. Missing nodes are inserted by the parser in
+ * order to recover from certain kinds of syntax errors.
+ */
+bool ts_node_is_extra(TSNode);
+
+/**
+ * Check if a syntax node has been edited.
+ */
+bool ts_node_has_changes(TSNode);
+
+/**
+ * Check if the node is a syntax error or contains any syntax errors.
+ */
+bool ts_node_has_error(TSNode);
+
+/**
+ * Get the node's immediate parent.
+ */
+TSNode ts_node_parent(TSNode);
+
+/**
+ * Get the node's child at the given index, where zero represents the first
+ * child.
+ */
+TSNode ts_node_child(TSNode, uint32_t);
+
+/**
+ * Get the node's number of children.
+ */
+uint32_t ts_node_child_count(TSNode);
+
+/**
+ * Get the node's *named* child at the given index.
+ *
+ * See also `ts_node_is_named`.
+ */
+TSNode ts_node_named_child(TSNode, uint32_t);
+
+/**
+ * Get the node's number of *named* children.
+ *
+ * See also `ts_node_is_named`.
+ */
+uint32_t ts_node_named_child_count(TSNode);
+
+/**
+ * Get the node's child with the given field name.
+ */
+TSNode ts_node_child_by_field_name(
+ TSNode self,
+ const char *field_name,
+ uint32_t field_name_length
+);
+
+/**
+ * Get the node's child with the given numerical field id.
+ *
+ * You can convert a field name to an id using the
+ * `ts_language_field_id_for_name` function.
+ */
+TSNode ts_node_child_by_field_id(TSNode, TSFieldId);
+
+/**
+ * Get the node's next / previous sibling.
+ */
+TSNode ts_node_next_sibling(TSNode);
+TSNode ts_node_prev_sibling(TSNode);
+
+/**
+ * Get the node's next / previous *named* sibling.
+ */
+TSNode ts_node_next_named_sibling(TSNode);
+TSNode ts_node_prev_named_sibling(TSNode);
+
+/**
+ * Get the node's first child that extends beyond the given byte offset.
+ */
+TSNode ts_node_first_child_for_byte(TSNode, uint32_t);
+
+/**
+ * Get the node's first named child that extends beyond the given byte offset.
+ */
+TSNode ts_node_first_named_child_for_byte(TSNode, uint32_t);
+
+/**
+ * Get the smallest node within this node that spans the given range of bytes
+ * or (row, column) positions.
+ */
+TSNode ts_node_descendant_for_byte_range(TSNode, uint32_t, uint32_t);
+TSNode ts_node_descendant_for_point_range(TSNode, TSPoint, TSPoint);
+
+/**
+ * Get the smallest named node within this node that spans the given range of
+ * bytes or (row, column) positions.
+ */
+TSNode ts_node_named_descendant_for_byte_range(TSNode, uint32_t, uint32_t);
+TSNode ts_node_named_descendant_for_point_range(TSNode, TSPoint, TSPoint);
+
+/**
+ * Edit the node to keep it in-sync with source code that has been edited.
+ *
+ * This function is only rarely needed. When you edit a syntax tree with the
+ * `ts_tree_edit` function, all of the nodes that you retrieve from the tree
+ * afterward will already reflect the edit. You only need to use `ts_node_edit`
+ * when you have a `TSNode` instance that you want to keep and continue to use
+ * after an edit.
+ */
+void ts_node_edit(TSNode *, const TSInputEdit *);
+
+/**
+ * Check if two nodes are identical.
+ */
+bool ts_node_eq(TSNode, TSNode);
+
+/************************/
+/* Section - TreeCursor */
+/************************/
+
+/**
+ * Create a new tree cursor starting from the given node.
+ *
+ * A tree cursor allows you to walk a syntax tree more efficiently than is
+ * possible using the `TSNode` functions. It is a mutable object that is always
+ * on a certain syntax node, and can be moved imperatively to different nodes.
+ */
+TSTreeCursor ts_tree_cursor_new(TSNode);
+
+/**
+ * Delete a tree cursor, freeing all of the memory that it used.
+ */
+void ts_tree_cursor_delete(TSTreeCursor *);
+
+/**
+ * Re-initialize a tree cursor to start at a different ndoe.
+ */
+void ts_tree_cursor_reset(TSTreeCursor *, TSNode);
+
+/**
+ * Get the tree cursor's current node.
+ */
+TSNode ts_tree_cursor_current_node(const TSTreeCursor *);
+
+/**
+ * Get the field name of the tree cursor's current node.
+ *
+ * This returns `NULL` if the current node doesn't have a field.
+ * See also `ts_node_child_by_field_name`.
+ */
+const char *ts_tree_cursor_current_field_name(const TSTreeCursor *);
+
+/**
+ * Get the field name of the tree cursor's current node.
+ *
+ * This returns zero if the current node doesn't have a field.
+ * See also `ts_node_child_by_field_id`, `ts_language_field_id_for_name`.
+ */
+TSFieldId ts_tree_cursor_current_field_id(const TSTreeCursor *);
+
+/**
+ * Move the cursor to the parent of its current node.
+ *
+ * This returns `true` if the cursor successfully moved, and returns `false`
+ * if there was no parent node (the cursor was already on the root node).
+ */
+bool ts_tree_cursor_goto_parent(TSTreeCursor *);
+
+/**
+ * Move the cursor to the next sibling of its current node.
+ *
+ * This returns `true` if the cursor successfully moved, and returns `false`
+ * if there was no next sibling node.
+ */
+bool ts_tree_cursor_goto_next_sibling(TSTreeCursor *);
+
+/**
+ * Move the cursor to the first schild of its current node.
+ *
+ * This returns `true` if the cursor successfully moved, and returns `false`
+ * if there were no children.
+ */
+bool ts_tree_cursor_goto_first_child(TSTreeCursor *);
+
+/**
+ * Move the cursor to the first schild of its current node that extends beyond
+ * the given byte offset.
+ *
+ * This returns the index of the child node if one was found, and returns -1
+ * if no such child was found.
+ */
+int64_t ts_tree_cursor_goto_first_child_for_byte(TSTreeCursor *, uint32_t);
+
+TSTreeCursor ts_tree_cursor_copy(const TSTreeCursor *);
+
+/**********************/
+/* Section - Language */
+/**********************/
+
+/**
+ * Get the number of distinct node types in the language.
+ */
+uint32_t ts_language_symbol_count(const TSLanguage *);
+
+/**
+ * Get a node type string for the given numerical id.
+ */
+const char *ts_language_symbol_name(const TSLanguage *, TSSymbol);
+
+/**
+ * Get the numerical id for the given node type string.
+ */
+TSSymbol ts_language_symbol_for_name(const TSLanguage *, const char *);
+
+/**
+ * Get the number of distinct field names in the language.
+ */
+uint32_t ts_language_field_count(const TSLanguage *);
+
+/**
+ * Get the field name string for the given numerical id.
+ */
+const char *ts_language_field_name_for_id(const TSLanguage *, TSFieldId);
+
+/**
+ * Get the numerical id for the given field name string.
+ */
+TSFieldId ts_language_field_id_for_name(const TSLanguage *, const char *, uint32_t);
+
+/**
+ * Check whether the given node type id belongs to named nodes, anonymous nodes,
+ * or a hidden nodes.
+ *
+ * See also `ts_node_is_named`. Hidden nodes are never returned from the API.
+ */
+TSSymbolType ts_language_symbol_type(const TSLanguage *, TSSymbol);
+
+/**
+ * Get the ABI version number for this language. This version number is used
+ * to ensure that languages were generated by a compatible version of
+ * Tree-sitter.
+ *
+ * See also `ts_parser_set_language`.
+ */
+uint32_t ts_language_version(const TSLanguage *);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_API_H_
diff --git a/src/tree_sitter/array.h b/src/tree_sitter/array.h
new file mode 100644
index 0000000000..bc77e687bf
--- /dev/null
+++ b/src/tree_sitter/array.h
@@ -0,0 +1,142 @@
+#ifndef TREE_SITTER_ARRAY_H_
+#define TREE_SITTER_ARRAY_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <assert.h>
+#include <stdbool.h>
+#include "./alloc.h"
+
+#define Array(T) \
+ struct { \
+ T *contents; \
+ uint32_t size; \
+ uint32_t capacity; \
+ }
+
+#define array_init(self) \
+ ((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL)
+
+#define array_new() \
+ { NULL, 0, 0 }
+
+#define array_get(self, index) \
+ (assert((uint32_t)index < (self)->size), &(self)->contents[index])
+
+#define array_front(self) array_get(self, 0)
+
+#define array_back(self) array_get(self, (self)->size - 1)
+
+#define array_clear(self) ((self)->size = 0)
+
+#define array_reserve(self, new_capacity) \
+ array__reserve((VoidArray *)(self), array__elem_size(self), new_capacity)
+
+#define array_erase(self, index) \
+ array__erase((VoidArray *)(self), array__elem_size(self), index)
+
+#define array_delete(self) array__delete((VoidArray *)self)
+
+#define array_push(self, element) \
+ (array__grow((VoidArray *)(self), 1, array__elem_size(self)), \
+ (self)->contents[(self)->size++] = (element))
+
+#define array_grow_by(self, count) \
+ (array__grow((VoidArray *)(self), count, array__elem_size(self)), \
+ memset((self)->contents + (self)->size, 0, (count) * array__elem_size(self)), \
+ (self)->size += (count))
+
+#define array_push_all(self, other) \
+ array_splice((self), (self)->size, 0, (other)->size, (other)->contents)
+
+#define array_splice(self, index, old_count, new_count, new_contents) \
+ array__splice((VoidArray *)(self), array__elem_size(self), index, old_count, \
+ new_count, new_contents)
+
+#define array_insert(self, index, element) \
+ array__splice((VoidArray *)(self), array__elem_size(self), index, 0, 1, &element)
+
+#define array_pop(self) ((self)->contents[--(self)->size])
+
+#define array_assign(self, other) \
+ array__assign((VoidArray *)(self), (const VoidArray *)(other), array__elem_size(self))
+
+// Private
+
+typedef Array(void) VoidArray;
+
+#define array__elem_size(self) sizeof(*(self)->contents)
+
+static inline void array__delete(VoidArray *self) {
+ ts_free(self->contents);
+ self->contents = NULL;
+ self->size = 0;
+ self->capacity = 0;
+}
+
+static inline void array__erase(VoidArray *self, size_t element_size,
+ uint32_t index) {
+ assert(index < self->size);
+ char *contents = (char *)self->contents;
+ memmove(contents + index * element_size, contents + (index + 1) * element_size,
+ (self->size - index - 1) * element_size);
+ self->size--;
+}
+
+static inline void array__reserve(VoidArray *self, size_t element_size, uint32_t new_capacity) {
+ if (new_capacity > self->capacity) {
+ if (self->contents) {
+ self->contents = ts_realloc(self->contents, new_capacity * element_size);
+ } else {
+ self->contents = ts_calloc(new_capacity, element_size);
+ }
+ self->capacity = new_capacity;
+ }
+}
+
+static inline void array__assign(VoidArray *self, const VoidArray *other, size_t element_size) {
+ array__reserve(self, element_size, other->size);
+ self->size = other->size;
+ memcpy(self->contents, other->contents, self->size * element_size);
+}
+
+static inline void array__grow(VoidArray *self, size_t count, size_t element_size) {
+ size_t new_size = self->size + count;
+ if (new_size > self->capacity) {
+ size_t new_capacity = self->capacity * 2;
+ if (new_capacity < 8) new_capacity = 8;
+ if (new_capacity < new_size) new_capacity = new_size;
+ array__reserve(self, element_size, new_capacity);
+ }
+}
+
+static inline void array__splice(VoidArray *self, size_t element_size,
+ uint32_t index, uint32_t old_count,
+ uint32_t new_count, const void *elements) {
+ uint32_t new_size = self->size + new_count - old_count;
+ uint32_t old_end = index + old_count;
+ uint32_t new_end = index + new_count;
+ assert(old_end <= self->size);
+
+ array__reserve(self, element_size, new_size);
+
+ char *contents = (char *)self->contents;
+ if (self->size > old_end)
+ memmove(contents + new_end * element_size, contents + old_end * element_size,
+ (self->size - old_end) * element_size);
+ if (new_count > 0)
+ memcpy((contents + index * element_size), elements,
+ new_count * element_size);
+ self->size += new_count - old_count;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_ARRAY_H_
diff --git a/src/tree_sitter/atomic.h b/src/tree_sitter/atomic.h
new file mode 100644
index 0000000000..301ee36700
--- /dev/null
+++ b/src/tree_sitter/atomic.h
@@ -0,0 +1,42 @@
+#ifndef TREE_SITTER_ATOMIC_H_
+#define TREE_SITTER_ATOMIC_H_
+
+#include <stdint.h>
+
+#ifdef _WIN32
+
+#include <windows.h>
+
+static inline size_t atomic_load(const volatile size_t *p) {
+ return *p;
+}
+
+static inline uint32_t atomic_inc(volatile uint32_t *p) {
+ return InterlockedIncrement(p);
+}
+
+static inline uint32_t atomic_dec(volatile uint32_t *p) {
+ return InterlockedDecrement(p);
+}
+
+#else
+
+static inline size_t atomic_load(const volatile size_t *p) {
+#ifdef __ATOMIC_RELAXED
+ return __atomic_load_n(p, __ATOMIC_RELAXED);
+#else
+ return __sync_fetch_and_add((volatile size_t *)p, 0);
+#endif
+}
+
+static inline uint32_t atomic_inc(volatile uint32_t *p) {
+ return __sync_add_and_fetch(p, 1u);
+}
+
+static inline uint32_t atomic_dec(volatile uint32_t *p) {
+ return __sync_sub_and_fetch(p, 1u);
+}
+
+#endif
+
+#endif // TREE_SITTER_ATOMIC_H_
diff --git a/src/tree_sitter/clock.h b/src/tree_sitter/clock.h
new file mode 100644
index 0000000000..94545f3566
--- /dev/null
+++ b/src/tree_sitter/clock.h
@@ -0,0 +1,141 @@
+#ifndef TREE_SITTER_CLOCK_H_
+#define TREE_SITTER_CLOCK_H_
+
+#include <stdint.h>
+
+typedef uint64_t TSDuration;
+
+#ifdef _WIN32
+
+// Windows:
+// * Represent a time as a performance counter value.
+// * Represent a duration as a number of performance counter ticks.
+
+#include <windows.h>
+typedef uint64_t TSClock;
+
+static inline TSDuration duration_from_micros(uint64_t micros) {
+ LARGE_INTEGER frequency;
+ QueryPerformanceFrequency(&frequency);
+ return micros * (uint64_t)frequency.QuadPart / 1000000;
+}
+
+static inline uint64_t duration_to_micros(TSDuration self) {
+ LARGE_INTEGER frequency;
+ QueryPerformanceFrequency(&frequency);
+ return self * 1000000 / (uint64_t)frequency.QuadPart;
+}
+
+static inline TSClock clock_null(void) {
+ return 0;
+}
+
+static inline TSClock clock_now(void) {
+ LARGE_INTEGER result;
+ QueryPerformanceCounter(&result);
+ return (uint64_t)result.QuadPart;
+}
+
+static inline TSClock clock_after(TSClock base, TSDuration duration) {
+ return base + duration;
+}
+
+static inline bool clock_is_null(TSClock self) {
+ return !self;
+}
+
+static inline bool clock_is_gt(TSClock self, TSClock other) {
+ return self > other;
+}
+
+#elif defined(CLOCK_MONOTONIC) && !defined(__APPLE__)
+
+// POSIX with monotonic clock support (Linux)
+// * Represent a time as a monotonic (seconds, nanoseconds) pair.
+// * Represent a duration as a number of microseconds.
+//
+// On these platforms, parse timeouts will correspond accurately to
+// real time, regardless of what other processes are running.
+
+#include <time.h>
+typedef struct timespec TSClock;
+
+static inline TSDuration duration_from_micros(uint64_t micros) {
+ return micros;
+}
+
+static inline uint64_t duration_to_micros(TSDuration self) {
+ return self;
+}
+
+static inline TSClock clock_now(void) {
+ TSClock result;
+ clock_gettime(CLOCK_MONOTONIC, &result);
+ return result;
+}
+
+static inline TSClock clock_null(void) {
+ return (TSClock) {0, 0};
+}
+
+static inline TSClock clock_after(TSClock base, TSDuration duration) {
+ TSClock result = base;
+ result.tv_sec += duration / 1000000;
+ result.tv_nsec += (duration % 1000000) * 1000;
+ return result;
+}
+
+static inline bool clock_is_null(TSClock self) {
+ return !self.tv_sec;
+}
+
+static inline bool clock_is_gt(TSClock self, TSClock other) {
+ if (self.tv_sec > other.tv_sec) return true;
+ if (self.tv_sec < other.tv_sec) return false;
+ return self.tv_nsec > other.tv_nsec;
+}
+
+#else
+
+// macOS or POSIX without monotonic clock support
+// * Represent a time as a process clock value.
+// * Represent a duration as a number of process clock ticks.
+//
+// On these platforms, parse timeouts may be affected by other processes,
+// which is not ideal, but is better than using a non-monotonic time API
+// like `gettimeofday`.
+
+#include <time.h>
+typedef uint64_t TSClock;
+
+static inline TSDuration duration_from_micros(uint64_t micros) {
+ return micros * (uint64_t)CLOCKS_PER_SEC / 1000000;
+}
+
+static inline uint64_t duration_to_micros(TSDuration self) {
+ return self * 1000000 / (uint64_t)CLOCKS_PER_SEC;
+}
+
+static inline TSClock clock_null(void) {
+ return 0;
+}
+
+static inline TSClock clock_now(void) {
+ return (uint64_t)clock();
+}
+
+static inline TSClock clock_after(TSClock base, TSDuration duration) {
+ return base + duration;
+}
+
+static inline bool clock_is_null(TSClock self) {
+ return !self;
+}
+
+static inline bool clock_is_gt(TSClock self, TSClock other) {
+ return self > other;
+}
+
+#endif
+
+#endif // TREE_SITTER_CLOCK_H_
diff --git a/src/tree_sitter/error_costs.h b/src/tree_sitter/error_costs.h
new file mode 100644
index 0000000000..32d3666a66
--- /dev/null
+++ b/src/tree_sitter/error_costs.h
@@ -0,0 +1,11 @@
+#ifndef TREE_SITTER_ERROR_COSTS_H_
+#define TREE_SITTER_ERROR_COSTS_H_
+
+#define ERROR_STATE 0
+#define ERROR_COST_PER_RECOVERY 500
+#define ERROR_COST_PER_MISSING_TREE 110
+#define ERROR_COST_PER_SKIPPED_TREE 100
+#define ERROR_COST_PER_SKIPPED_LINE 30
+#define ERROR_COST_PER_SKIPPED_CHAR 1
+
+#endif
diff --git a/src/tree_sitter/get_changed_ranges.c b/src/tree_sitter/get_changed_ranges.c
new file mode 100644
index 0000000000..5bd1d814bd
--- /dev/null
+++ b/src/tree_sitter/get_changed_ranges.c
@@ -0,0 +1,482 @@
+#include "./get_changed_ranges.h"
+#include "./subtree.h"
+#include "./language.h"
+#include "./error_costs.h"
+#include "./tree_cursor.h"
+#include <assert.h>
+
+// #define DEBUG_GET_CHANGED_RANGES
+
+static void ts_range_array_add(TSRangeArray *self, Length start, Length end) {
+ if (self->size > 0) {
+ TSRange *last_range = array_back(self);
+ if (start.bytes <= last_range->end_byte) {
+ last_range->end_byte = end.bytes;
+ last_range->end_point = end.extent;
+ return;
+ }
+ }
+
+ if (start.bytes < end.bytes) {
+ TSRange range = { start.extent, end.extent, start.bytes, end.bytes };
+ array_push(self, range);
+ }
+}
+
+bool ts_range_array_intersects(const TSRangeArray *self, unsigned start_index,
+ uint32_t start_byte, uint32_t end_byte) {
+ for (unsigned i = start_index; i < self->size; i++) {
+ TSRange *range = &self->contents[i];
+ if (range->end_byte > start_byte) {
+ if (range->start_byte >= end_byte) break;
+ return true;
+ }
+ }
+ return false;
+}
+
+void ts_range_array_get_changed_ranges(
+ const TSRange *old_ranges, unsigned old_range_count,
+ const TSRange *new_ranges, unsigned new_range_count,
+ TSRangeArray *differences
+) {
+ unsigned new_index = 0;
+ unsigned old_index = 0;
+ Length current_position = length_zero();
+ bool in_old_range = false;
+ bool in_new_range = false;
+
+ while (old_index < old_range_count || new_index < new_range_count) {
+ const TSRange *old_range = &old_ranges[old_index];
+ const TSRange *new_range = &new_ranges[new_index];
+
+ Length next_old_position;
+ if (in_old_range) {
+ next_old_position = (Length) {old_range->end_byte, old_range->end_point};
+ } else if (old_index < old_range_count) {
+ next_old_position = (Length) {old_range->start_byte, old_range->start_point};
+ } else {
+ next_old_position = LENGTH_MAX;
+ }
+
+ Length next_new_position;
+ if (in_new_range) {
+ next_new_position = (Length) {new_range->end_byte, new_range->end_point};
+ } else if (new_index < new_range_count) {
+ next_new_position = (Length) {new_range->start_byte, new_range->start_point};
+ } else {
+ next_new_position = LENGTH_MAX;
+ }
+
+ if (next_old_position.bytes < next_new_position.bytes) {
+ if (in_old_range != in_new_range) {
+ ts_range_array_add(differences, current_position, next_old_position);
+ }
+ if (in_old_range) old_index++;
+ current_position = next_old_position;
+ in_old_range = !in_old_range;
+ } else if (next_new_position.bytes < next_old_position.bytes) {
+ if (in_old_range != in_new_range) {
+ ts_range_array_add(differences, current_position, next_new_position);
+ }
+ if (in_new_range) new_index++;
+ current_position = next_new_position;
+ in_new_range = !in_new_range;
+ } else {
+ if (in_old_range != in_new_range) {
+ ts_range_array_add(differences, current_position, next_new_position);
+ }
+ if (in_old_range) old_index++;
+ if (in_new_range) new_index++;
+ in_old_range = !in_old_range;
+ in_new_range = !in_new_range;
+ current_position = next_new_position;
+ }
+ }
+}
+
+typedef struct {
+ TreeCursor cursor;
+ const TSLanguage *language;
+ unsigned visible_depth;
+ bool in_padding;
+} Iterator;
+
+static Iterator iterator_new(TreeCursor *cursor, const Subtree *tree, const TSLanguage *language) {
+ array_clear(&cursor->stack);
+ array_push(&cursor->stack, ((TreeCursorEntry){
+ .subtree = tree,
+ .position = length_zero(),
+ .child_index = 0,
+ .structural_child_index = 0,
+ }));
+ return (Iterator) {
+ .cursor = *cursor,
+ .language = language,
+ .visible_depth = 1,
+ .in_padding = false,
+ };
+}
+
+static bool iterator_done(Iterator *self) {
+ return self->cursor.stack.size == 0;
+}
+
+static Length iterator_start_position(Iterator *self) {
+ TreeCursorEntry entry = *array_back(&self->cursor.stack);
+ if (self->in_padding) {
+ return entry.position;
+ } else {
+ return length_add(entry.position, ts_subtree_padding(*entry.subtree));
+ }
+}
+
+static Length iterator_end_position(Iterator *self) {
+ TreeCursorEntry entry = *array_back(&self->cursor.stack);
+ Length result = length_add(entry.position, ts_subtree_padding(*entry.subtree));
+ if (self->in_padding) {
+ return result;
+ } else {
+ return length_add(result, ts_subtree_size(*entry.subtree));
+ }
+}
+
+static bool iterator_tree_is_visible(const Iterator *self) {
+ TreeCursorEntry entry = *array_back(&self->cursor.stack);
+ if (ts_subtree_visible(*entry.subtree)) return true;
+ if (self->cursor.stack.size > 1) {
+ Subtree parent = *self->cursor.stack.contents[self->cursor.stack.size - 2].subtree;
+ const TSSymbol *alias_sequence = ts_language_alias_sequence(
+ self->language,
+ parent.ptr->production_id
+ );
+ return alias_sequence && alias_sequence[entry.structural_child_index] != 0;
+ }
+ return false;
+}
+
+static void iterator_get_visible_state(const Iterator *self, Subtree *tree,
+ TSSymbol *alias_symbol, uint32_t *start_byte) {
+ uint32_t i = self->cursor.stack.size - 1;
+
+ if (self->in_padding) {
+ if (i == 0) return;
+ i--;
+ }
+
+ for (; i + 1 > 0; i--) {
+ TreeCursorEntry entry = self->cursor.stack.contents[i];
+
+ if (i > 0) {
+ const Subtree *parent = self->cursor.stack.contents[i - 1].subtree;
+ const TSSymbol *alias_sequence = ts_language_alias_sequence(
+ self->language,
+ parent->ptr->production_id
+ );
+ if (alias_sequence) {
+ *alias_symbol = alias_sequence[entry.structural_child_index];
+ }
+ }
+
+ if (ts_subtree_visible(*entry.subtree) || *alias_symbol) {
+ *tree = *entry.subtree;
+ *start_byte = entry.position.bytes;
+ break;
+ }
+ }
+}
+
+static void iterator_ascend(Iterator *self) {
+ if (iterator_done(self)) return;
+ if (iterator_tree_is_visible(self) && !self->in_padding) self->visible_depth--;
+ if (array_back(&self->cursor.stack)->child_index > 0) self->in_padding = false;
+ self->cursor.stack.size--;
+}
+
+static bool iterator_descend(Iterator *self, uint32_t goal_position) {
+ if (self->in_padding) return false;
+
+ bool did_descend;
+ do {
+ did_descend = false;
+ TreeCursorEntry entry = *array_back(&self->cursor.stack);
+ Length position = entry.position;
+ uint32_t structural_child_index = 0;
+ for (uint32_t i = 0, n = ts_subtree_child_count(*entry.subtree); i < n; i++) {
+ const Subtree *child = &entry.subtree->ptr->children[i];
+ Length child_left = length_add(position, ts_subtree_padding(*child));
+ Length child_right = length_add(child_left, ts_subtree_size(*child));
+
+ if (child_right.bytes > goal_position) {
+ array_push(&self->cursor.stack, ((TreeCursorEntry){
+ .subtree = child,
+ .position = position,
+ .child_index = i,
+ .structural_child_index = structural_child_index,
+ }));
+
+ if (iterator_tree_is_visible(self)) {
+ if (child_left.bytes > goal_position) {
+ self->in_padding = true;
+ } else {
+ self->visible_depth++;
+ }
+ return true;
+ }
+
+ did_descend = true;
+ break;
+ }
+
+ position = child_right;
+ if (!ts_subtree_extra(*child)) structural_child_index++;
+ }
+ } while (did_descend);
+
+ return false;
+}
+
+static void iterator_advance(Iterator *self) {
+ if (self->in_padding) {
+ self->in_padding = false;
+ if (iterator_tree_is_visible(self)) {
+ self->visible_depth++;
+ } else {
+ iterator_descend(self, 0);
+ }
+ return;
+ }
+
+ for (;;) {
+ if (iterator_tree_is_visible(self)) self->visible_depth--;
+ TreeCursorEntry entry = array_pop(&self->cursor.stack);
+ if (iterator_done(self)) return;
+
+ const Subtree *parent = array_back(&self->cursor.stack)->subtree;
+ uint32_t child_index = entry.child_index + 1;
+ if (ts_subtree_child_count(*parent) > child_index) {
+ Length position = length_add(entry.position, ts_subtree_total_size(*entry.subtree));
+ uint32_t structural_child_index = entry.structural_child_index;
+ if (!ts_subtree_extra(*entry.subtree)) structural_child_index++;
+ const Subtree *next_child = &parent->ptr->children[child_index];
+
+ array_push(&self->cursor.stack, ((TreeCursorEntry){
+ .subtree = next_child,
+ .position = position,
+ .child_index = child_index,
+ .structural_child_index = structural_child_index,
+ }));
+
+ if (iterator_tree_is_visible(self)) {
+ if (ts_subtree_padding(*next_child).bytes > 0) {
+ self->in_padding = true;
+ } else {
+ self->visible_depth++;
+ }
+ } else {
+ iterator_descend(self, 0);
+ }
+ break;
+ }
+ }
+}
+
+typedef enum {
+ IteratorDiffers,
+ IteratorMayDiffer,
+ IteratorMatches,
+} IteratorComparison;
+
+static IteratorComparison iterator_compare(const Iterator *old_iter, const Iterator *new_iter) {
+ Subtree old_tree = NULL_SUBTREE;
+ Subtree new_tree = NULL_SUBTREE;
+ uint32_t old_start = 0;
+ uint32_t new_start = 0;
+ TSSymbol old_alias_symbol = 0;
+ TSSymbol new_alias_symbol = 0;
+ iterator_get_visible_state(old_iter, &old_tree, &old_alias_symbol, &old_start);
+ iterator_get_visible_state(new_iter, &new_tree, &new_alias_symbol, &new_start);
+
+ if (!old_tree.ptr && !new_tree.ptr) return IteratorMatches;
+ if (!old_tree.ptr || !new_tree.ptr) return IteratorDiffers;
+
+ if (
+ old_alias_symbol == new_alias_symbol &&
+ ts_subtree_symbol(old_tree) == ts_subtree_symbol(new_tree)
+ ) {
+ if (old_start == new_start &&
+ !ts_subtree_has_changes(old_tree) &&
+ ts_subtree_symbol(old_tree) != ts_builtin_sym_error &&
+ ts_subtree_size(old_tree).bytes == ts_subtree_size(new_tree).bytes &&
+ ts_subtree_parse_state(old_tree) != TS_TREE_STATE_NONE &&
+ ts_subtree_parse_state(new_tree) != TS_TREE_STATE_NONE &&
+ (ts_subtree_parse_state(old_tree) == ERROR_STATE) ==
+ (ts_subtree_parse_state(new_tree) == ERROR_STATE)) {
+ return IteratorMatches;
+ } else {
+ return IteratorMayDiffer;
+ }
+ }
+
+ return IteratorDiffers;
+}
+
+#ifdef DEBUG_GET_CHANGED_RANGES
+static inline void iterator_print_state(Iterator *self) {
+ TreeCursorEntry entry = *array_back(&self->cursor.stack);
+ TSPoint start = iterator_start_position(self).extent;
+ TSPoint end = iterator_end_position(self).extent;
+ const char *name = ts_language_symbol_name(self->language, ts_subtree_symbol(*entry.subtree));
+ printf(
+ "(%-25s %s\t depth:%u [%u, %u] - [%u, %u])",
+ name, self->in_padding ? "(p)" : " ",
+ self->visible_depth,
+ start.row + 1, start.column,
+ end.row + 1, end.column
+ );
+}
+#endif
+
+unsigned ts_subtree_get_changed_ranges(const Subtree *old_tree, const Subtree *new_tree,
+ TreeCursor *cursor1, TreeCursor *cursor2,
+ const TSLanguage *language,
+ const TSRangeArray *included_range_differences,
+ TSRange **ranges) {
+ TSRangeArray results = array_new();
+
+ Iterator old_iter = iterator_new(cursor1, old_tree, language);
+ Iterator new_iter = iterator_new(cursor2, new_tree, language);
+
+ unsigned included_range_difference_index = 0;
+
+ Length position = iterator_start_position(&old_iter);
+ Length next_position = iterator_start_position(&new_iter);
+ if (position.bytes < next_position.bytes) {
+ ts_range_array_add(&results, position, next_position);
+ position = next_position;
+ } else if (position.bytes > next_position.bytes) {
+ ts_range_array_add(&results, next_position, position);
+ next_position = position;
+ }
+
+ do {
+ #ifdef DEBUG_GET_CHANGED_RANGES
+ printf("At [%-2u, %-2u] Compare ", position.extent.row + 1, position.extent.column);
+ iterator_print_state(&old_iter);
+ printf("\tvs\t");
+ iterator_print_state(&new_iter);
+ puts("");
+ #endif
+
+ // Compare the old and new subtrees.
+ IteratorComparison comparison = iterator_compare(&old_iter, &new_iter);
+
+ // Even if the two subtrees appear to be identical, they could differ
+ // internally if they contain a range of text that was previously
+ // excluded from the parse, and is now included, or vice-versa.
+ if (comparison == IteratorMatches && ts_range_array_intersects(
+ included_range_differences,
+ included_range_difference_index,
+ position.bytes,
+ iterator_end_position(&old_iter).bytes
+ )) {
+ comparison = IteratorMayDiffer;
+ }
+
+ bool is_changed = false;
+ switch (comparison) {
+ // If the subtrees are definitely identical, move to the end
+ // of both subtrees.
+ case IteratorMatches:
+ next_position = iterator_end_position(&old_iter);
+ break;
+
+ // If the subtrees might differ internally, descend into both
+ // subtrees, finding the first child that spans the current position.
+ case IteratorMayDiffer:
+ if (iterator_descend(&old_iter, position.bytes)) {
+ if (!iterator_descend(&new_iter, position.bytes)) {
+ is_changed = true;
+ next_position = iterator_end_position(&old_iter);
+ }
+ } else if (iterator_descend(&new_iter, position.bytes)) {
+ is_changed = true;
+ next_position = iterator_end_position(&new_iter);
+ } else {
+ next_position = length_min(
+ iterator_end_position(&old_iter),
+ iterator_end_position(&new_iter)
+ );
+ }
+ break;
+
+ // If the subtrees are different, record a change and then move
+ // to the end of both subtrees.
+ case IteratorDiffers:
+ is_changed = true;
+ next_position = length_min(
+ iterator_end_position(&old_iter),
+ iterator_end_position(&new_iter)
+ );
+ break;
+ }
+
+ // Ensure that both iterators are caught up to the current position.
+ while (
+ !iterator_done(&old_iter) &&
+ iterator_end_position(&old_iter).bytes <= next_position.bytes
+ ) iterator_advance(&old_iter);
+ while (
+ !iterator_done(&new_iter) &&
+ iterator_end_position(&new_iter).bytes <= next_position.bytes
+ ) iterator_advance(&new_iter);
+
+ // Ensure that both iterators are at the same depth in the tree.
+ while (old_iter.visible_depth > new_iter.visible_depth) {
+ iterator_ascend(&old_iter);
+ }
+ while (new_iter.visible_depth > old_iter.visible_depth) {
+ iterator_ascend(&new_iter);
+ }
+
+ if (is_changed) {
+ #ifdef DEBUG_GET_CHANGED_RANGES
+ printf(
+ " change: [[%u, %u] - [%u, %u]]\n",
+ position.extent.row + 1, position.extent.column,
+ next_position.extent.row + 1, next_position.extent.column
+ );
+ #endif
+
+ ts_range_array_add(&results, position, next_position);
+ }
+
+ position = next_position;
+
+ // Keep track of the current position in the included range differences
+ // array in order to avoid scanning the entire array on each iteration.
+ while (included_range_difference_index < included_range_differences->size) {
+ const TSRange *range = &included_range_differences->contents[
+ included_range_difference_index
+ ];
+ if (range->end_byte <= position.bytes) {
+ included_range_difference_index++;
+ } else {
+ break;
+ }
+ }
+ } while (!iterator_done(&old_iter) && !iterator_done(&new_iter));
+
+ Length old_size = ts_subtree_total_size(*old_tree);
+ Length new_size = ts_subtree_total_size(*new_tree);
+ if (old_size.bytes < new_size.bytes) {
+ ts_range_array_add(&results, old_size, new_size);
+ } else if (new_size.bytes < old_size.bytes) {
+ ts_range_array_add(&results, new_size, old_size);
+ }
+
+ *cursor1 = old_iter.cursor;
+ *cursor2 = new_iter.cursor;
+ *ranges = results.contents;
+ return results.size;
+}
diff --git a/src/tree_sitter/get_changed_ranges.h b/src/tree_sitter/get_changed_ranges.h
new file mode 100644
index 0000000000..a1f1dbb430
--- /dev/null
+++ b/src/tree_sitter/get_changed_ranges.h
@@ -0,0 +1,36 @@
+#ifndef TREE_SITTER_GET_CHANGED_RANGES_H_
+#define TREE_SITTER_GET_CHANGED_RANGES_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "./tree_cursor.h"
+#include "./subtree.h"
+
+typedef Array(TSRange) TSRangeArray;
+
+void ts_range_array_get_changed_ranges(
+ const TSRange *old_ranges, unsigned old_range_count,
+ const TSRange *new_ranges, unsigned new_range_count,
+ TSRangeArray *differences
+);
+
+bool ts_range_array_intersects(
+ const TSRangeArray *self, unsigned start_index,
+ uint32_t start_byte, uint32_t end_byte
+);
+
+unsigned ts_subtree_get_changed_ranges(
+ const Subtree *old_tree, const Subtree *new_tree,
+ TreeCursor *cursor1, TreeCursor *cursor2,
+ const TSLanguage *language,
+ const TSRangeArray *included_range_differences,
+ TSRange **ranges
+);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_GET_CHANGED_RANGES_H_
diff --git a/src/tree_sitter/language.c b/src/tree_sitter/language.c
new file mode 100644
index 0000000000..1bfb1a8d03
--- /dev/null
+++ b/src/tree_sitter/language.c
@@ -0,0 +1,107 @@
+#include "./language.h"
+#include "./subtree.h"
+#include "./error_costs.h"
+#include <string.h>
+
+void ts_language_table_entry(const TSLanguage *self, TSStateId state,
+ TSSymbol symbol, TableEntry *result) {
+ if (symbol == ts_builtin_sym_error || symbol == ts_builtin_sym_error_repeat) {
+ result->action_count = 0;
+ result->is_reusable = false;
+ result->actions = NULL;
+ } else {
+ assert(symbol < self->token_count);
+ uint32_t action_index = ts_language_lookup(self, state, symbol);
+ const TSParseActionEntry *entry = &self->parse_actions[action_index];
+ result->action_count = entry->count;
+ result->is_reusable = entry->reusable;
+ result->actions = (const TSParseAction *)(entry + 1);
+ }
+}
+
+uint32_t ts_language_symbol_count(const TSLanguage *language) {
+ return language->symbol_count + language->alias_count;
+}
+
+uint32_t ts_language_version(const TSLanguage *language) {
+ return language->version;
+}
+
+TSSymbolMetadata ts_language_symbol_metadata(const TSLanguage *language, TSSymbol symbol) {
+ if (symbol == ts_builtin_sym_error) {
+ return (TSSymbolMetadata){.visible = true, .named = true};
+ } else if (symbol == ts_builtin_sym_error_repeat) {
+ return (TSSymbolMetadata){.visible = false, .named = false};
+ } else {
+ return language->symbol_metadata[symbol];
+ }
+}
+
+const char *ts_language_symbol_name(const TSLanguage *language, TSSymbol symbol) {
+ if (symbol == ts_builtin_sym_error) {
+ return "ERROR";
+ } else if (symbol == ts_builtin_sym_error_repeat) {
+ return "_ERROR";
+ } else {
+ return language->symbol_names[symbol];
+ }
+}
+
+TSSymbol ts_language_symbol_for_name(const TSLanguage *self, const char *name) {
+ if (!strcmp(name, "ERROR")) return ts_builtin_sym_error;
+
+ uint32_t count = ts_language_symbol_count(self);
+ for (TSSymbol i = 0; i < count; i++) {
+ if (!strcmp(self->symbol_names[i], name)) {
+ return i;
+ }
+ }
+ return 0;
+}
+
+TSSymbolType ts_language_symbol_type(const TSLanguage *language, TSSymbol symbol) {
+ TSSymbolMetadata metadata = ts_language_symbol_metadata(language, symbol);
+ if (metadata.named) {
+ return TSSymbolTypeRegular;
+ } else if (metadata.visible) {
+ return TSSymbolTypeAnonymous;
+ } else {
+ return TSSymbolTypeAuxiliary;
+ }
+}
+
+uint32_t ts_language_field_count(const TSLanguage *self) {
+ if (self->version >= TREE_SITTER_LANGUAGE_VERSION_WITH_FIELDS) {
+ return self->field_count;
+ } else {
+ return 0;
+ }
+}
+
+const char *ts_language_field_name_for_id(const TSLanguage *self, TSFieldId id) {
+ uint32_t count = ts_language_field_count(self);
+ if (count) {
+ return self->field_names[id];
+ } else {
+ return NULL;
+ }
+}
+
+TSFieldId ts_language_field_id_for_name(
+ const TSLanguage *self,
+ const char *name,
+ uint32_t name_length
+) {
+ uint32_t count = ts_language_field_count(self);
+ for (TSSymbol i = 1; i < count + 1; i++) {
+ switch (strncmp(name, self->field_names[i], name_length)) {
+ case 0:
+ return i;
+ case -1:
+ return 0;
+ default:
+ break;
+ }
+ }
+ return 0;
+}
diff --git a/src/tree_sitter/language.h b/src/tree_sitter/language.h
new file mode 100644
index 0000000000..0741486a1b
--- /dev/null
+++ b/src/tree_sitter/language.h
@@ -0,0 +1,138 @@
+#ifndef TREE_SITTER_LANGUAGE_H_
+#define TREE_SITTER_LANGUAGE_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "./subtree.h"
+#include "tree_sitter/parser.h"
+
+#define ts_builtin_sym_error_repeat (ts_builtin_sym_error - 1)
+#define TREE_SITTER_LANGUAGE_VERSION_WITH_FIELDS 10
+#define TREE_SITTER_LANGUAGE_VERSION_WITH_SMALL_STATES 11
+
+typedef struct {
+ const TSParseAction *actions;
+ uint32_t action_count;
+ bool is_reusable;
+} TableEntry;
+
+void ts_language_table_entry(const TSLanguage *, TSStateId, TSSymbol, TableEntry *);
+
+TSSymbolMetadata ts_language_symbol_metadata(const TSLanguage *, TSSymbol);
+
+static inline bool ts_language_is_symbol_external(const TSLanguage *self, TSSymbol symbol) {
+ return 0 < symbol && symbol < self->external_token_count + 1;
+}
+
+static inline const TSParseAction *ts_language_actions(const TSLanguage *self,
+ TSStateId state,
+ TSSymbol symbol,
+ uint32_t *count) {
+ TableEntry entry;
+ ts_language_table_entry(self, state, symbol, &entry);
+ *count = entry.action_count;
+ return entry.actions;
+}
+
+static inline bool ts_language_has_actions(const TSLanguage *self,
+ TSStateId state,
+ TSSymbol symbol) {
+ TableEntry entry;
+ ts_language_table_entry(self, state, symbol, &entry);
+ return entry.action_count > 0;
+}
+
+static inline bool ts_language_has_reduce_action(const TSLanguage *self,
+ TSStateId state,
+ TSSymbol symbol) {
+ TableEntry entry;
+ ts_language_table_entry(self, state, symbol, &entry);
+ return entry.action_count > 0 && entry.actions[0].type == TSParseActionTypeReduce;
+}
+
+static inline uint16_t ts_language_lookup(
+ const TSLanguage *self,
+ TSStateId state,
+ TSSymbol symbol
+) {
+ if (
+ self->version >= TREE_SITTER_LANGUAGE_VERSION_WITH_SMALL_STATES &&
+ state >= self->large_state_count
+ ) {
+ uint32_t index = self->small_parse_table_map[state - self->large_state_count];
+ const uint16_t *data = &self->small_parse_table[index];
+ uint16_t section_count = *(data++);
+ for (unsigned i = 0; i < section_count; i++) {
+ uint16_t section_value = *(data++);
+ uint16_t symbol_count = *(data++);
+ for (unsigned i = 0; i < symbol_count; i++) {
+ if (*(data++) == symbol) return section_value;
+ }
+ }
+ return 0;
+ } else {
+ return self->parse_table[state * self->symbol_count + symbol];
+ }
+}
+
+static inline TSStateId ts_language_next_state(const TSLanguage *self,
+ TSStateId state,
+ TSSymbol symbol) {
+ if (symbol == ts_builtin_sym_error || symbol == ts_builtin_sym_error_repeat) {
+ return 0;
+ } else if (symbol < self->token_count) {
+ uint32_t count;
+ const TSParseAction *actions = ts_language_actions(self, state, symbol, &count);
+ if (count > 0) {
+ TSParseAction action = actions[count - 1];
+ if (action.type == TSParseActionTypeShift || action.type == TSParseActionTypeRecover) {
+ return action.params.state;
+ }
+ }
+ return 0;
+ } else {
+ return ts_language_lookup(self, state, symbol);
+ }
+}
+
+static inline const bool *
+ts_language_enabled_external_tokens(const TSLanguage *self,
+ unsigned external_scanner_state) {
+ if (external_scanner_state == 0) {
+ return NULL;
+ } else {
+ return self->external_scanner.states + self->external_token_count * external_scanner_state;
+ }
+}
+
+static inline const TSSymbol *
+ts_language_alias_sequence(const TSLanguage *self, uint32_t production_id) {
+ return production_id > 0 ?
+ self->alias_sequences + production_id * self->max_alias_sequence_length :
+ NULL;
+}
+
+static inline void ts_language_field_map(
+ const TSLanguage *self,
+ uint32_t production_id,
+ const TSFieldMapEntry **start,
+ const TSFieldMapEntry **end
+) {
+ if (self->version < TREE_SITTER_LANGUAGE_VERSION_WITH_FIELDS || self->field_count == 0) {
+ *start = NULL;
+ *end = NULL;
+ return;
+ }
+
+ TSFieldMapSlice slice = self->field_map_slices[production_id];
+ *start = &self->field_map_entries[slice.index];
+ *end = &self->field_map_entries[slice.index] + slice.length;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_LANGUAGE_H_
diff --git a/src/tree_sitter/length.h b/src/tree_sitter/length.h
new file mode 100644
index 0000000000..61de9fc1d5
--- /dev/null
+++ b/src/tree_sitter/length.h
@@ -0,0 +1,44 @@
+#ifndef TREE_SITTER_LENGTH_H_
+#define TREE_SITTER_LENGTH_H_
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include "./point.h"
+#include "tree_sitter/api.h"
+
+typedef struct {
+ uint32_t bytes;
+ TSPoint extent;
+} Length;
+
+static const Length LENGTH_UNDEFINED = {0, {0, 1}};
+static const Length LENGTH_MAX = {UINT32_MAX, {UINT32_MAX, UINT32_MAX}};
+
+static inline bool length_is_undefined(Length length) {
+ return length.bytes == 0 && length.extent.column != 0;
+}
+
+static inline Length length_min(Length len1, Length len2) {
+ return (len1.bytes < len2.bytes) ? len1 : len2;
+}
+
+static inline Length length_add(Length len1, Length len2) {
+ Length result;
+ result.bytes = len1.bytes + len2.bytes;
+ result.extent = point_add(len1.extent, len2.extent);
+ return result;
+}
+
+static inline Length length_sub(Length len1, Length len2) {
+ Length result;
+ result.bytes = len1.bytes - len2.bytes;
+ result.extent = point_sub(len1.extent, len2.extent);
+ return result;
+}
+
+static inline Length length_zero(void) {
+ Length result = {0, {0, 0}};
+ return result;
+}
+
+#endif
diff --git a/src/tree_sitter/lexer.c b/src/tree_sitter/lexer.c
new file mode 100644
index 0000000000..fdc127466f
--- /dev/null
+++ b/src/tree_sitter/lexer.c
@@ -0,0 +1,322 @@
+#include <stdio.h>
+#include "./lexer.h"
+#include "./subtree.h"
+#include "./length.h"
+#include "./utf16.h"
+#include "utf8proc.h"
+
+#define LOG(...) \
+ if (self->logger.log) { \
+ snprintf(self->debug_buffer, TREE_SITTER_SERIALIZATION_BUFFER_SIZE, __VA_ARGS__); \
+ self->logger.log(self->logger.payload, TSLogTypeLex, self->debug_buffer); \
+ }
+
+#define LOG_CHARACTER(message, character) \
+ LOG( \
+ 32 <= character && character < 127 ? \
+ message " character:'%c'" : \
+ message " character:%d", character \
+ )
+
+static const char empty_chunk[3] = { 0, 0 };
+
+static const int32_t BYTE_ORDER_MARK = 0xFEFF;
+
+static void ts_lexer__get_chunk(Lexer *self) {
+ self->chunk_start = self->current_position.bytes;
+ self->chunk = self->input.read(
+ self->input.payload,
+ self->current_position.bytes,
+ self->current_position.extent,
+ &self->chunk_size
+ );
+ if (!self->chunk_size) self->chunk = empty_chunk;
+}
+
+typedef utf8proc_ssize_t (*DecodeFunction)(
+ const utf8proc_uint8_t *,
+ utf8proc_ssize_t,
+ utf8proc_int32_t *
+);
+
+static void ts_lexer__get_lookahead(Lexer *self) {
+ uint32_t position_in_chunk = self->current_position.bytes - self->chunk_start;
+ const uint8_t *chunk = (const uint8_t *)self->chunk + position_in_chunk;
+ uint32_t size = self->chunk_size - position_in_chunk;
+
+ if (size == 0) {
+ self->lookahead_size = 1;
+ self->data.lookahead = '\0';
+ return;
+ }
+
+ DecodeFunction decode =
+ self->input.encoding == TSInputEncodingUTF8 ? utf8proc_iterate : utf16_iterate;
+
+ self->lookahead_size = decode(chunk, size, &self->data.lookahead);
+
+ // If this chunk ended in the middle of a multi-byte character,
+ // try again with a fresh chunk.
+ if (self->data.lookahead == -1 && size < 4) {
+ ts_lexer__get_chunk(self);
+ chunk = (const uint8_t *)self->chunk;
+ size = self->chunk_size;
+ self->lookahead_size = decode(chunk, size, &self->data.lookahead);
+ }
+
+ if (self->data.lookahead == -1) {
+ self->lookahead_size = 1;
+ }
+}
+
+static void ts_lexer__advance(TSLexer *payload, bool skip) {
+ Lexer *self = (Lexer *)payload;
+ if (self->chunk == empty_chunk)
+ return;
+
+ if (self->lookahead_size) {
+ self->current_position.bytes += self->lookahead_size;
+ if (self->data.lookahead == '\n') {
+ self->current_position.extent.row++;
+ self->current_position.extent.column = 0;
+ } else {
+ self->current_position.extent.column += self->lookahead_size;
+ }
+ }
+
+ TSRange *current_range = &self->included_ranges[self->current_included_range_index];
+ if (self->current_position.bytes == current_range->end_byte) {
+ self->current_included_range_index++;
+ if (self->current_included_range_index == self->included_range_count) {
+ self->data.lookahead = '\0';
+ self->lookahead_size = 1;
+ return;
+ } else {
+ current_range++;
+ self->current_position = (Length) {
+ current_range->start_byte,
+ current_range->start_point,
+ };
+ }
+ }
+
+ if (skip) {
+ LOG_CHARACTER("skip", self->data.lookahead);
+ self->token_start_position = self->current_position;
+ } else {
+ LOG_CHARACTER("consume", self->data.lookahead);
+ }
+
+ if (self->current_position.bytes >= self->chunk_start + self->chunk_size) {
+ ts_lexer__get_chunk(self);
+ }
+
+ ts_lexer__get_lookahead(self);
+}
+
+static void ts_lexer__mark_end(TSLexer *payload) {
+ Lexer *self = (Lexer *)payload;
+ TSRange *current_included_range = &self->included_ranges[self->current_included_range_index];
+ if (self->current_included_range_index > 0 &&
+ self->current_position.bytes == current_included_range->start_byte) {
+ TSRange *previous_included_range = current_included_range - 1;
+ self->token_end_position = (Length) {
+ previous_included_range->end_byte,
+ previous_included_range->end_point,
+ };
+ } else {
+ self->token_end_position = self->current_position;
+ }
+}
+
+static uint32_t ts_lexer__get_column(TSLexer *payload) {
+ Lexer *self = (Lexer *)payload;
+ uint32_t goal_byte = self->current_position.bytes;
+
+ self->current_position.bytes -= self->current_position.extent.column;
+ self->current_position.extent.column = 0;
+
+ if (self->current_position.bytes < self->chunk_start) {
+ ts_lexer__get_chunk(self);
+ }
+
+ uint32_t result = 0;
+ while (self->current_position.bytes < goal_byte) {
+ ts_lexer__advance(payload, false);
+ result++;
+ }
+
+ return result;
+}
+
+static bool ts_lexer__is_at_included_range_start(TSLexer *payload) {
+ const Lexer *self = (const Lexer *)payload;
+ TSRange *current_range = &self->included_ranges[self->current_included_range_index];
+ return self->current_position.bytes == current_range->start_byte;
+}
+
+// The lexer's methods are stored as a struct field so that generated
+// parsers can call them without needing to be linked against this library.
+
+void ts_lexer_init(Lexer *self) {
+ *self = (Lexer) {
+ .data = {
+ .advance = ts_lexer__advance,
+ .mark_end = ts_lexer__mark_end,
+ .get_column = ts_lexer__get_column,
+ .is_at_included_range_start = ts_lexer__is_at_included_range_start,
+ .lookahead = 0,
+ .result_symbol = 0,
+ },
+ .chunk = NULL,
+ .chunk_start = 0,
+ .current_position = {UINT32_MAX, {0, 0}},
+ .logger = {
+ .payload = NULL,
+ .log = NULL
+ },
+ .current_included_range_index = 0,
+ };
+
+ self->included_ranges = NULL;
+ ts_lexer_set_included_ranges(self, NULL, 0);
+ ts_lexer_reset(self, length_zero());
+}
+
+void ts_lexer_delete(Lexer *self) {
+ ts_free(self->included_ranges);
+}
+
+void ts_lexer_set_input(Lexer *self, TSInput input) {
+ self->input = input;
+ self->data.lookahead = 0;
+ self->lookahead_size = 0;
+ self->chunk = 0;
+ self->chunk_start = 0;
+ self->chunk_size = 0;
+}
+
+static void ts_lexer_goto(Lexer *self, Length position) {
+ bool found_included_range = false;
+ for (unsigned i = 0; i < self->included_range_count; i++) {
+ TSRange *included_range = &self->included_ranges[i];
+ if (included_range->end_byte > position.bytes) {
+ if (included_range->start_byte > position.bytes) {
+ position = (Length) {
+ .bytes = included_range->start_byte,
+ .extent = included_range->start_point,
+ };
+ }
+
+ self->current_included_range_index = i;
+ found_included_range = true;
+ break;
+ }
+ }
+
+ if (!found_included_range) {
+ TSRange *last_included_range = &self->included_ranges[self->included_range_count - 1];
+ position = (Length) {
+ .bytes = last_included_range->end_byte,
+ .extent = last_included_range->end_point,
+ };
+ self->chunk = empty_chunk;
+ self->chunk_start = position.bytes;
+ self->chunk_size = 2;
+ }
+
+ self->token_start_position = position;
+ self->token_end_position = LENGTH_UNDEFINED;
+ self->current_position = position;
+
+ if (self->chunk && (position.bytes < self->chunk_start ||
+ position.bytes >= self->chunk_start + self->chunk_size)) {
+ self->chunk = 0;
+ self->chunk_start = 0;
+ self->chunk_size = 0;
+ }
+
+ self->lookahead_size = 0;
+ self->data.lookahead = 0;
+}
+
+void ts_lexer_reset(Lexer *self, Length position) {
+ if (position.bytes != self->current_position.bytes) ts_lexer_goto(self, position);
+}
+
+void ts_lexer_start(Lexer *self) {
+ self->token_start_position = self->current_position;
+ self->token_end_position = LENGTH_UNDEFINED;
+ self->data.result_symbol = 0;
+ if (!self->chunk) ts_lexer__get_chunk(self);
+ if (!self->lookahead_size) ts_lexer__get_lookahead(self);
+ if (
+ self->current_position.bytes == 0 &&
+ self->data.lookahead == BYTE_ORDER_MARK
+ ) ts_lexer__advance((TSLexer *)self, true);
+}
+
+void ts_lexer_finish(Lexer *self, uint32_t *lookahead_end_byte) {
+ if (length_is_undefined(self->token_end_position)) {
+ ts_lexer__mark_end(&self->data);
+ }
+
+ uint32_t current_lookahead_end_byte = self->current_position.bytes + 1;
+
+ // In order to determine that a byte sequence is invalid UTF8 or UTF16,
+ // the character decoding algorithm may have looked at the following byte.
+ // Therefore, the next byte *after* the current (invalid) character
+ // affects the interpretation of the current character.
+ if (self->data.lookahead == -1) {
+ current_lookahead_end_byte++;
+ }
+
+ if (current_lookahead_end_byte > *lookahead_end_byte) {
+ *lookahead_end_byte = current_lookahead_end_byte;
+ }
+}
+
+void ts_lexer_advance_to_end(Lexer *self) {
+ while (self->data.lookahead != 0) {
+ ts_lexer__advance((TSLexer *)self, false);
+ }
+}
+
+void ts_lexer_mark_end(Lexer *self) {
+ ts_lexer__mark_end(&self->data);
+}
+
+static const TSRange DEFAULT_RANGES[] = {
+ {
+ .start_point = {
+ .row = 0,
+ .column = 0,
+ },
+ .end_point = {
+ .row = UINT32_MAX,
+ .column = UINT32_MAX,
+ },
+ .start_byte = 0,
+ .end_byte = UINT32_MAX
+ }
+};
+
+void ts_lexer_set_included_ranges(Lexer *self, const TSRange *ranges, uint32_t count) {
+ if (!ranges) {
+ ranges = DEFAULT_RANGES;
+ count = 1;
+ }
+
+ size_t sz = count * sizeof(TSRange);
+ self->included_ranges = ts_realloc(self->included_ranges, sz);
+ memcpy(self->included_ranges, ranges, sz);
+ self->included_range_count = count;
+ ts_lexer_goto(self, self->current_position);
+}
+
+TSRange *ts_lexer_included_ranges(const Lexer *self, uint32_t *count) {
+ *count = self->included_range_count;
+ return self->included_ranges;
+}
+
+#undef LOG
diff --git a/src/tree_sitter/lexer.h b/src/tree_sitter/lexer.h
new file mode 100644
index 0000000000..f523d88f65
--- /dev/null
+++ b/src/tree_sitter/lexer.h
@@ -0,0 +1,48 @@
+#ifndef TREE_SITTER_LEXER_H_
+#define TREE_SITTER_LEXER_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "./length.h"
+#include "./subtree.h"
+#include "tree_sitter/api.h"
+#include "tree_sitter/parser.h"
+
+typedef struct {
+ TSLexer data;
+ Length current_position;
+ Length token_start_position;
+ Length token_end_position;
+
+ TSRange * included_ranges;
+ size_t included_range_count;
+ size_t current_included_range_index;
+
+ const char *chunk;
+ uint32_t chunk_start;
+ uint32_t chunk_size;
+ uint32_t lookahead_size;
+
+ TSInput input;
+ TSLogger logger;
+ char debug_buffer[TREE_SITTER_SERIALIZATION_BUFFER_SIZE];
+} Lexer;
+
+void ts_lexer_init(Lexer *);
+void ts_lexer_delete(Lexer *);
+void ts_lexer_set_input(Lexer *, TSInput);
+void ts_lexer_reset(Lexer *, Length);
+void ts_lexer_start(Lexer *);
+void ts_lexer_finish(Lexer *, uint32_t *);
+void ts_lexer_advance_to_end(Lexer *);
+void ts_lexer_mark_end(Lexer *);
+void ts_lexer_set_included_ranges(Lexer *self, const TSRange *ranges, uint32_t count);
+TSRange *ts_lexer_included_ranges(const Lexer *self, uint32_t *count);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_LEXER_H_
diff --git a/src/tree_sitter/lib.c b/src/tree_sitter/lib.c
new file mode 100644
index 0000000000..fc5fbc9210
--- /dev/null
+++ b/src/tree_sitter/lib.c
@@ -0,0 +1,20 @@
+// The Tree-sitter library can be built by compiling this one source file.
+//
+// The following directories must be added to the include path:
+// - include
+// - utf8proc
+
+#define _POSIX_C_SOURCE 200112L
+#define UTF8PROC_STATIC
+
+#include "./get_changed_ranges.c"
+#include "./language.c"
+#include "./lexer.c"
+#include "./node.c"
+#include "./parser.c"
+#include "./stack.c"
+#include "./subtree.c"
+#include "./tree_cursor.c"
+#include "./tree.c"
+#include "./utf16.c"
+#include "utf8proc.c"
diff --git a/src/tree_sitter/node.c b/src/tree_sitter/node.c
new file mode 100644
index 0000000000..6b2be36ee5
--- /dev/null
+++ b/src/tree_sitter/node.c
@@ -0,0 +1,673 @@
+#include <stdbool.h>
+#include "./subtree.h"
+#include "./tree.h"
+#include "./language.h"
+
+typedef struct {
+ Subtree parent;
+ const TSTree *tree;
+ Length position;
+ uint32_t child_index;
+ uint32_t structural_child_index;
+ const TSSymbol *alias_sequence;
+} NodeChildIterator;
+
+// TSNode - constructors
+
+TSNode ts_node_new(
+ const TSTree *tree,
+ const Subtree *subtree,
+ Length position,
+ TSSymbol alias
+) {
+ return (TSNode) {
+ {position.bytes, position.extent.row, position.extent.column, alias},
+ subtree,
+ tree,
+ };
+}
+
+static inline TSNode ts_node__null(void) {
+ return ts_node_new(NULL, NULL, length_zero(), 0);
+}
+
+// TSNode - accessors
+
+uint32_t ts_node_start_byte(TSNode self) {
+ return self.context[0];
+}
+
+TSPoint ts_node_start_point(TSNode self) {
+ return (TSPoint) {self.context[1], self.context[2]};
+}
+
+static inline uint32_t ts_node__alias(const TSNode *self) {
+ return self->context[3];
+}
+
+static inline Subtree ts_node__subtree(TSNode self) {
+ return *(const Subtree *)self.id;
+}
+
+// NodeChildIterator
+
+static inline NodeChildIterator ts_node_iterate_children(const TSNode *node) {
+ Subtree subtree = ts_node__subtree(*node);
+ if (ts_subtree_child_count(subtree) == 0) {
+ return (NodeChildIterator) {NULL_SUBTREE, node->tree, length_zero(), 0, 0, NULL};
+ }
+ const TSSymbol *alias_sequence = ts_language_alias_sequence(
+ node->tree->language,
+ subtree.ptr->production_id
+ );
+ return (NodeChildIterator) {
+ .tree = node->tree,
+ .parent = subtree,
+ .position = {ts_node_start_byte(*node), ts_node_start_point(*node)},
+ .child_index = 0,
+ .structural_child_index = 0,
+ .alias_sequence = alias_sequence,
+ };
+}
+
+static inline bool ts_node_child_iterator_done(NodeChildIterator *self) {
+ return self->child_index == self->parent.ptr->child_count;
+}
+
+static inline bool ts_node_child_iterator_next(
+ NodeChildIterator *self,
+ TSNode *result
+) {
+ if (!self->parent.ptr || ts_node_child_iterator_done(self)) return false;
+ const Subtree *child = &self->parent.ptr->children[self->child_index];
+ TSSymbol alias_symbol = 0;
+ if (!ts_subtree_extra(*child)) {
+ if (self->alias_sequence) {
+ alias_symbol = self->alias_sequence[self->structural_child_index];
+ }
+ self->structural_child_index++;
+ }
+ if (self->child_index > 0) {
+ self->position = length_add(self->position, ts_subtree_padding(*child));
+ }
+ *result = ts_node_new(
+ self->tree,
+ child,
+ self->position,
+ alias_symbol
+ );
+ self->position = length_add(self->position, ts_subtree_size(*child));
+ self->child_index++;
+ return true;
+}
+
+// TSNode - private
+
+static inline bool ts_node__is_relevant(TSNode self, bool include_anonymous) {
+ Subtree tree = ts_node__subtree(self);
+ if (include_anonymous) {
+ return ts_subtree_visible(tree) || ts_node__alias(&self);
+ } else {
+ TSSymbol alias = ts_node__alias(&self);
+ if (alias) {
+ return ts_language_symbol_metadata(self.tree->language, alias).named;
+ } else {
+ return ts_subtree_visible(tree) && ts_subtree_named(tree);
+ }
+ }
+}
+
+static inline uint32_t ts_node__relevant_child_count(
+ TSNode self,
+ bool include_anonymous
+) {
+ Subtree tree = ts_node__subtree(self);
+ if (ts_subtree_child_count(tree) > 0) {
+ if (include_anonymous) {
+ return tree.ptr->visible_child_count;
+ } else {
+ return tree.ptr->named_child_count;
+ }
+ } else {
+ return 0;
+ }
+}
+
+static inline TSNode ts_node__child(
+ TSNode self,
+ uint32_t child_index,
+ bool include_anonymous
+) {
+ TSNode result = self;
+ bool did_descend = true;
+
+ while (did_descend) {
+ did_descend = false;
+
+ TSNode child;
+ uint32_t index = 0;
+ NodeChildIterator iterator = ts_node_iterate_children(&result);
+ while (ts_node_child_iterator_next(&iterator, &child)) {
+ if (ts_node__is_relevant(child, include_anonymous)) {
+ if (index == child_index) {
+ ts_tree_set_cached_parent(self.tree, &child, &self);
+ return child;
+ }
+ index++;
+ } else {
+ uint32_t grandchild_index = child_index - index;
+ uint32_t grandchild_count = ts_node__relevant_child_count(child, include_anonymous);
+ if (grandchild_index < grandchild_count) {
+ did_descend = true;
+ result = child;
+ child_index = grandchild_index;
+ break;
+ }
+ index += grandchild_count;
+ }
+ }
+ }
+
+ return ts_node__null();
+}
+
+static bool ts_subtree_has_trailing_empty_descendant(
+ Subtree self,
+ Subtree other
+) {
+ for (unsigned i = ts_subtree_child_count(self) - 1; i + 1 > 0; i--) {
+ Subtree child = self.ptr->children[i];
+ if (ts_subtree_total_bytes(child) > 0) break;
+ if (child.ptr == other.ptr || ts_subtree_has_trailing_empty_descendant(child, other)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static inline TSNode ts_node__prev_sibling(TSNode self, bool include_anonymous) {
+ Subtree self_subtree = ts_node__subtree(self);
+ bool self_is_empty = ts_subtree_total_bytes(self_subtree) == 0;
+ uint32_t target_end_byte = ts_node_end_byte(self);
+
+ TSNode node = ts_node_parent(self);
+ TSNode earlier_node = ts_node__null();
+ bool earlier_node_is_relevant = false;
+
+ while (!ts_node_is_null(node)) {
+ TSNode earlier_child = ts_node__null();
+ bool earlier_child_is_relevant = false;
+ bool found_child_containing_target = false;
+
+ TSNode child;
+ NodeChildIterator iterator = ts_node_iterate_children(&node);
+ while (ts_node_child_iterator_next(&iterator, &child)) {
+ if (child.id == self.id) break;
+ if (iterator.position.bytes > target_end_byte) {
+ found_child_containing_target = true;
+ break;
+ }
+
+ if (iterator.position.bytes == target_end_byte &&
+ (!self_is_empty ||
+ ts_subtree_has_trailing_empty_descendant(ts_node__subtree(child), self_subtree))) {
+ found_child_containing_target = true;
+ break;
+ }
+
+ if (ts_node__is_relevant(child, include_anonymous)) {
+ earlier_child = child;
+ earlier_child_is_relevant = true;
+ } else if (ts_node__relevant_child_count(child, include_anonymous) > 0) {
+ earlier_child = child;
+ earlier_child_is_relevant = false;
+ }
+ }
+
+ if (found_child_containing_target) {
+ if (!ts_node_is_null(earlier_child)) {
+ earlier_node = earlier_child;
+ earlier_node_is_relevant = earlier_child_is_relevant;
+ }
+ node = child;
+ } else if (earlier_child_is_relevant) {
+ return earlier_child;
+ } else if (!ts_node_is_null(earlier_child)) {
+ node = earlier_child;
+ } else if (earlier_node_is_relevant) {
+ return earlier_node;
+ } else {
+ node = earlier_node;
+ }
+ }
+
+ return ts_node__null();
+}
+
+static inline TSNode ts_node__next_sibling(TSNode self, bool include_anonymous) {
+ uint32_t target_end_byte = ts_node_end_byte(self);
+
+ TSNode node = ts_node_parent(self);
+ TSNode later_node = ts_node__null();
+ bool later_node_is_relevant = false;
+
+ while (!ts_node_is_null(node)) {
+ TSNode later_child = ts_node__null();
+ bool later_child_is_relevant = false;
+ TSNode child_containing_target = ts_node__null();
+
+ TSNode child;
+ NodeChildIterator iterator = ts_node_iterate_children(&node);
+ while (ts_node_child_iterator_next(&iterator, &child)) {
+ if (iterator.position.bytes < target_end_byte) continue;
+ if (ts_node_start_byte(child) <= ts_node_start_byte(self)) {
+ if (ts_node__subtree(child).ptr != ts_node__subtree(self).ptr) {
+ child_containing_target = child;
+ }
+ } else if (ts_node__is_relevant(child, include_anonymous)) {
+ later_child = child;
+ later_child_is_relevant = true;
+ break;
+ } else if (ts_node__relevant_child_count(child, include_anonymous) > 0) {
+ later_child = child;
+ later_child_is_relevant = false;
+ break;
+ }
+ }
+
+ if (!ts_node_is_null(child_containing_target)) {
+ if (!ts_node_is_null(later_child)) {
+ later_node = later_child;
+ later_node_is_relevant = later_child_is_relevant;
+ }
+ node = child_containing_target;
+ } else if (later_child_is_relevant) {
+ return later_child;
+ } else if (!ts_node_is_null(later_child)) {
+ node = later_child;
+ } else if (later_node_is_relevant) {
+ return later_node;
+ } else {
+ node = later_node;
+ }
+ }
+
+ return ts_node__null();
+}
+
+static inline TSNode ts_node__first_child_for_byte(
+ TSNode self,
+ uint32_t goal,
+ bool include_anonymous
+) {
+ TSNode node = self;
+ bool did_descend = true;
+
+ while (did_descend) {
+ did_descend = false;
+
+ TSNode child;
+ NodeChildIterator iterator = ts_node_iterate_children(&node);
+ while (ts_node_child_iterator_next(&iterator, &child)) {
+ if (ts_node_end_byte(child) > goal) {
+ if (ts_node__is_relevant(child, include_anonymous)) {
+ return child;
+ } else if (ts_node_child_count(child) > 0) {
+ did_descend = true;
+ node = child;
+ break;
+ }
+ }
+ }
+ }
+
+ return ts_node__null();
+}
+
+static inline TSNode ts_node__descendant_for_byte_range(
+ TSNode self,
+ uint32_t range_start,
+ uint32_t range_end,
+ bool include_anonymous
+) {
+ TSNode node = self;
+ TSNode last_visible_node = self;
+
+ bool did_descend = true;
+ while (did_descend) {
+ did_descend = false;
+
+ TSNode child;
+ NodeChildIterator iterator = ts_node_iterate_children(&node);
+ while (ts_node_child_iterator_next(&iterator, &child)) {
+ uint32_t node_end = iterator.position.bytes;
+
+ // The end of this node must extend far enough forward to touch
+ // the end of the range and exceed the start of the range.
+ if (node_end < range_end) continue;
+ if (node_end <= range_start) continue;
+
+ // The start of this node must extend far enough backward to
+ // touch the start of the range.
+ if (range_start < ts_node_start_byte(child)) break;
+
+ node = child;
+ if (ts_node__is_relevant(node, include_anonymous)) {
+ ts_tree_set_cached_parent(self.tree, &child, &last_visible_node);
+ last_visible_node = node;
+ }
+ did_descend = true;
+ break;
+ }
+ }
+
+ return last_visible_node;
+}
+
+static inline TSNode ts_node__descendant_for_point_range(
+ TSNode self,
+ TSPoint range_start,
+ TSPoint range_end,
+ bool include_anonymous
+) {
+ TSNode node = self;
+ TSNode last_visible_node = self;
+
+ bool did_descend = true;
+ while (did_descend) {
+ did_descend = false;
+
+ TSNode child;
+ NodeChildIterator iterator = ts_node_iterate_children(&node);
+ while (ts_node_child_iterator_next(&iterator, &child)) {
+ TSPoint node_end = iterator.position.extent;
+
+ // The end of this node must extend far enough forward to touch
+ // the end of the range and exceed the start of the range.
+ if (point_lt(node_end, range_end)) continue;
+ if (point_lte(node_end, range_start)) continue;
+
+ // The start of this node must extend far enough backward to
+ // touch the start of the range.
+ if (point_lt(range_start, ts_node_start_point(child))) break;
+
+ node = child;
+ if (ts_node__is_relevant(node, include_anonymous)) {
+ ts_tree_set_cached_parent(self.tree, &child, &last_visible_node);
+ last_visible_node = node;
+ }
+ did_descend = true;
+ break;
+ }
+ }
+
+ return last_visible_node;
+}
+
+// TSNode - public
+
+uint32_t ts_node_end_byte(TSNode self) {
+ return ts_node_start_byte(self) + ts_subtree_size(ts_node__subtree(self)).bytes;
+}
+
+TSPoint ts_node_end_point(TSNode self) {
+ return point_add(ts_node_start_point(self), ts_subtree_size(ts_node__subtree(self)).extent);
+}
+
+TSSymbol ts_node_symbol(TSNode self) {
+ return ts_node__alias(&self)
+ ? ts_node__alias(&self)
+ : ts_subtree_symbol(ts_node__subtree(self));
+}
+
+const char *ts_node_type(TSNode self) {
+ return ts_language_symbol_name(self.tree->language, ts_node_symbol(self));
+}
+
+char *ts_node_string(TSNode self) {
+ return ts_subtree_string(ts_node__subtree(self), self.tree->language, false);
+}
+
+bool ts_node_eq(TSNode self, TSNode other) {
+ return self.tree == other.tree && self.id == other.id;
+}
+
+bool ts_node_is_null(TSNode self) {
+ return self.id == 0;
+}
+
+bool ts_node_is_extra(TSNode self) {
+ return ts_subtree_extra(ts_node__subtree(self));
+}
+
+bool ts_node_is_named(TSNode self) {
+ TSSymbol alias = ts_node__alias(&self);
+ return alias
+ ? ts_language_symbol_metadata(self.tree->language, alias).named
+ : ts_subtree_named(ts_node__subtree(self));
+}
+
+bool ts_node_is_missing(TSNode self) {
+ return ts_subtree_missing(ts_node__subtree(self));
+}
+
+bool ts_node_has_changes(TSNode self) {
+ return ts_subtree_has_changes(ts_node__subtree(self));
+}
+
+bool ts_node_has_error(TSNode self) {
+ return ts_subtree_error_cost(ts_node__subtree(self)) > 0;
+}
+
+TSNode ts_node_parent(TSNode self) {
+ TSNode node = ts_tree_get_cached_parent(self.tree, &self);
+ if (node.id) return node;
+
+ node = ts_tree_root_node(self.tree);
+ uint32_t end_byte = ts_node_end_byte(self);
+ if (node.id == self.id) return ts_node__null();
+
+ TSNode last_visible_node = node;
+ bool did_descend = true;
+ while (did_descend) {
+ did_descend = false;
+
+ TSNode child;
+ NodeChildIterator iterator = ts_node_iterate_children(&node);
+ while (ts_node_child_iterator_next(&iterator, &child)) {
+ if (
+ ts_node_start_byte(child) > ts_node_start_byte(self) ||
+ child.id == self.id
+ ) break;
+ if (iterator.position.bytes >= end_byte) {
+ node = child;
+ if (ts_node__is_relevant(child, true)) {
+ ts_tree_set_cached_parent(self.tree, &node, &last_visible_node);
+ last_visible_node = node;
+ }
+ did_descend = true;
+ break;
+ }
+ }
+ }
+
+ return last_visible_node;
+}
+
+TSNode ts_node_child(TSNode self, uint32_t child_index) {
+ return ts_node__child(self, child_index, true);
+}
+
+TSNode ts_node_named_child(TSNode self, uint32_t child_index) {
+ return ts_node__child(self, child_index, false);
+}
+
+TSNode ts_node_child_by_field_id(TSNode self, TSFieldId field_id) {
+recur:
+ if (!field_id || ts_node_child_count(self) == 0) return ts_node__null();
+
+ const TSFieldMapEntry *field_map, *field_map_end;
+ ts_language_field_map(
+ self.tree->language,
+ ts_node__subtree(self).ptr->production_id,
+ &field_map,
+ &field_map_end
+ );
+ if (field_map == field_map_end) return ts_node__null();
+
+ // The field mappings are sorted by their field id. Scan all
+ // the mappings to find the ones for the given field id.
+ while (field_map->field_id < field_id) {
+ field_map++;
+ if (field_map == field_map_end) return ts_node__null();
+ }
+ while (field_map_end[-1].field_id > field_id) {
+ field_map_end--;
+ if (field_map == field_map_end) return ts_node__null();
+ }
+
+ TSNode child;
+ NodeChildIterator iterator = ts_node_iterate_children(&self);
+ while (ts_node_child_iterator_next(&iterator, &child)) {
+ if (!ts_subtree_extra(ts_node__subtree(child))) {
+ uint32_t index = iterator.structural_child_index - 1;
+ if (index < field_map->child_index) continue;
+
+ // Hidden nodes' fields are "inherited" by their visible parent.
+ if (field_map->inherited) {
+
+ // If this is the *last* possible child node for this field,
+ // then perform a tail call to avoid recursion.
+ if (field_map + 1 == field_map_end) {
+ self = child;
+ goto recur;
+ }
+
+ // Otherwise, descend into this child, but if it doesn't contain
+ // the field, continue searching subsequent children.
+ else {
+ TSNode result = ts_node_child_by_field_id(child, field_id);
+ if (result.id) return result;
+ field_map++;
+ if (field_map == field_map_end) return ts_node__null();
+ }
+ }
+
+ else if (ts_node__is_relevant(child, true)) {
+ return child;
+ }
+
+ // If the field refers to a hidden node, return its first visible
+ // child.
+ else {
+ return ts_node_child(child, 0);
+ }
+ }
+ }
+
+ return ts_node__null();
+}
+
+TSNode ts_node_child_by_field_name(
+ TSNode self,
+ const char *name,
+ uint32_t name_length
+) {
+ TSFieldId field_id = ts_language_field_id_for_name(
+ self.tree->language,
+ name,
+ name_length
+ );
+ return ts_node_child_by_field_id(self, field_id);
+}
+
+uint32_t ts_node_child_count(TSNode self) {
+ Subtree tree = ts_node__subtree(self);
+ if (ts_subtree_child_count(tree) > 0) {
+ return tree.ptr->visible_child_count;
+ } else {
+ return 0;
+ }
+}
+
+uint32_t ts_node_named_child_count(TSNode self) {
+ Subtree tree = ts_node__subtree(self);
+ if (ts_subtree_child_count(tree) > 0) {
+ return tree.ptr->named_child_count;
+ } else {
+ return 0;
+ }
+}
+
+TSNode ts_node_next_sibling(TSNode self) {
+ return ts_node__next_sibling(self, true);
+}
+
+TSNode ts_node_next_named_sibling(TSNode self) {
+ return ts_node__next_sibling(self, false);
+}
+
+TSNode ts_node_prev_sibling(TSNode self) {
+ return ts_node__prev_sibling(self, true);
+}
+
+TSNode ts_node_prev_named_sibling(TSNode self) {
+ return ts_node__prev_sibling(self, false);
+}
+
+TSNode ts_node_first_child_for_byte(TSNode self, uint32_t byte) {
+ return ts_node__first_child_for_byte(self, byte, true);
+}
+
+TSNode ts_node_first_named_child_for_byte(TSNode self, uint32_t byte) {
+ return ts_node__first_child_for_byte(self, byte, false);
+}
+
+TSNode ts_node_descendant_for_byte_range(
+ TSNode self,
+ uint32_t start,
+ uint32_t end
+) {
+ return ts_node__descendant_for_byte_range(self, start, end, true);
+}
+
+TSNode ts_node_named_descendant_for_byte_range(
+ TSNode self,
+ uint32_t start,
+ uint32_t end
+) {
+ return ts_node__descendant_for_byte_range(self, start, end, false);
+}
+
+TSNode ts_node_descendant_for_point_range(
+ TSNode self,
+ TSPoint start,
+ TSPoint end
+) {
+ return ts_node__descendant_for_point_range(self, start, end, true);
+}
+
+TSNode ts_node_named_descendant_for_point_range(
+ TSNode self,
+ TSPoint start,
+ TSPoint end
+) {
+ return ts_node__descendant_for_point_range(self, start, end, false);
+}
+
+void ts_node_edit(TSNode *self, const TSInputEdit *edit) {
+ uint32_t start_byte = ts_node_start_byte(*self);
+ TSPoint start_point = ts_node_start_point(*self);
+
+ if (start_byte >= edit->old_end_byte) {
+ start_byte = edit->new_end_byte + (start_byte - edit->old_end_byte);
+ start_point = point_add(edit->new_end_point, point_sub(start_point, edit->old_end_point));
+ } else if (start_byte > edit->start_byte) {
+ start_byte = edit->new_end_byte;
+ start_point = edit->new_end_point;
+ }
+
+ self->context[0] = start_byte;
+ self->context[1] = start_point.row;
+ self->context[2] = start_point.column;
+}
diff --git a/src/tree_sitter/parser.c b/src/tree_sitter/parser.c
new file mode 100644
index 0000000000..88b20845fd
--- /dev/null
+++ b/src/tree_sitter/parser.c
@@ -0,0 +1,1887 @@
+#include <time.h>
+#include <assert.h>
+#include <stdio.h>
+#include <limits.h>
+#include <stdbool.h>
+#include "tree_sitter/api.h"
+#include "./alloc.h"
+#include "./array.h"
+#include "./atomic.h"
+#include "./clock.h"
+#include "./error_costs.h"
+#include "./get_changed_ranges.h"
+#include "./language.h"
+#include "./length.h"
+#include "./lexer.h"
+#include "./reduce_action.h"
+#include "./reusable_node.h"
+#include "./stack.h"
+#include "./subtree.h"
+#include "./tree.h"
+
+#define LOG(...) \
+ if (self->lexer.logger.log || self->dot_graph_file) { \
+ snprintf(self->lexer.debug_buffer, TREE_SITTER_SERIALIZATION_BUFFER_SIZE, __VA_ARGS__); \
+ ts_parser__log(self); \
+ }
+
+#define LOG_STACK() \
+ if (self->dot_graph_file) { \
+ ts_stack_print_dot_graph(self->stack, self->language, self->dot_graph_file); \
+ fputs("\n\n", self->dot_graph_file); \
+ }
+
+#define LOG_TREE(tree) \
+ if (self->dot_graph_file) { \
+ ts_subtree_print_dot_graph(tree, self->language, self->dot_graph_file); \
+ fputs("\n", self->dot_graph_file); \
+ }
+
+#define SYM_NAME(symbol) ts_language_symbol_name(self->language, symbol)
+
+#define TREE_NAME(tree) SYM_NAME(ts_subtree_symbol(tree))
+
+static const unsigned MAX_VERSION_COUNT = 6;
+static const unsigned MAX_VERSION_COUNT_OVERFLOW = 4;
+static const unsigned MAX_SUMMARY_DEPTH = 16;
+static const unsigned MAX_COST_DIFFERENCE = 16 * ERROR_COST_PER_SKIPPED_TREE;
+static const unsigned OP_COUNT_PER_TIMEOUT_CHECK = 100;
+
+typedef struct {
+ Subtree token;
+ Subtree last_external_token;
+ uint32_t byte_index;
+} TokenCache;
+
+struct TSParser {
+ Lexer lexer;
+ Stack *stack;
+ SubtreePool tree_pool;
+ const TSLanguage *language;
+ ReduceActionSet reduce_actions;
+ Subtree finished_tree;
+ SubtreeHeapData scratch_tree_data;
+ MutableSubtree scratch_tree;
+ TokenCache token_cache;
+ ReusableNode reusable_node;
+ void *external_scanner_payload;
+ FILE *dot_graph_file;
+ TSClock end_clock;
+ TSDuration timeout_duration;
+ unsigned accept_count;
+ unsigned operation_count;
+ const volatile size_t *cancellation_flag;
+ bool halt_on_error;
+ Subtree old_tree;
+ TSRangeArray included_range_differences;
+ unsigned included_range_difference_index;
+};
+
+typedef struct {
+ unsigned cost;
+ unsigned node_count;
+ int dynamic_precedence;
+ bool is_in_error;
+} ErrorStatus;
+
+typedef enum {
+ ErrorComparisonTakeLeft,
+ ErrorComparisonPreferLeft,
+ ErrorComparisonNone,
+ ErrorComparisonPreferRight,
+ ErrorComparisonTakeRight,
+} ErrorComparison;
+
+typedef struct {
+ const char *string;
+ uint32_t length;
+} TSStringInput;
+
+// StringInput
+
+static const char *ts_string_input_read(
+ void *_self,
+ uint32_t byte,
+ TSPoint _,
+ uint32_t *length
+) {
+ TSStringInput *self = (TSStringInput *)_self;
+ if (byte >= self->length) {
+ *length = 0;
+ return "";
+ } else {
+ *length = self->length - byte;
+ return self->string + byte;
+ }
+}
+
+// Parser - Private
+
+static void ts_parser__log(TSParser *self) {
+ if (self->lexer.logger.log) {
+ self->lexer.logger.log(
+ self->lexer.logger.payload,
+ TSLogTypeParse,
+ self->lexer.debug_buffer
+ );
+ }
+
+ if (self->dot_graph_file) {
+ fprintf(self->dot_graph_file, "graph {\nlabel=\"");
+ for (char *c = &self->lexer.debug_buffer[0]; *c != 0; c++) {
+ if (*c == '"') fputc('\\', self->dot_graph_file);
+ fputc(*c, self->dot_graph_file);
+ }
+ fprintf(self->dot_graph_file, "\"\n}\n\n");
+ }
+}
+
+static bool ts_parser__breakdown_top_of_stack(
+ TSParser *self,
+ StackVersion version
+) {
+ bool did_break_down = false;
+ bool pending = false;
+
+ do {
+ StackSliceArray pop = ts_stack_pop_pending(self->stack, version);
+ if (!pop.size) break;
+
+ did_break_down = true;
+ pending = false;
+ for (uint32_t i = 0; i < pop.size; i++) {
+ StackSlice slice = pop.contents[i];
+ TSStateId state = ts_stack_state(self->stack, slice.version);
+ Subtree parent = *array_front(&slice.subtrees);
+
+ for (uint32_t j = 0, n = ts_subtree_child_count(parent); j < n; j++) {
+ Subtree child = parent.ptr->children[j];
+ pending = ts_subtree_child_count(child) > 0;
+
+ if (ts_subtree_is_error(child)) {
+ state = ERROR_STATE;
+ } else if (!ts_subtree_extra(child)) {
+ state = ts_language_next_state(self->language, state, ts_subtree_symbol(child));
+ }
+
+ ts_subtree_retain(child);
+ ts_stack_push(self->stack, slice.version, child, pending, state);
+ }
+
+ for (uint32_t j = 1; j < slice.subtrees.size; j++) {
+ Subtree tree = slice.subtrees.contents[j];
+ ts_stack_push(self->stack, slice.version, tree, false, state);
+ }
+
+ ts_subtree_release(&self->tree_pool, parent);
+ array_delete(&slice.subtrees);
+
+ LOG("breakdown_top_of_stack tree:%s", TREE_NAME(parent));
+ LOG_STACK();
+ }
+ } while (pending);
+
+ return did_break_down;
+}
+
+static void ts_parser__breakdown_lookahead(
+ TSParser *self,
+ Subtree *lookahead,
+ TSStateId state,
+ ReusableNode *reusable_node
+) {
+ bool did_descend = false;
+ Subtree tree = reusable_node_tree(reusable_node);
+ while (ts_subtree_child_count(tree) > 0 && ts_subtree_parse_state(tree) != state) {
+ LOG("state_mismatch sym:%s", TREE_NAME(tree));
+ reusable_node_descend(reusable_node);
+ tree = reusable_node_tree(reusable_node);
+ did_descend = true;
+ }
+
+ if (did_descend) {
+ ts_subtree_release(&self->tree_pool, *lookahead);
+ *lookahead = tree;
+ ts_subtree_retain(*lookahead);
+ }
+}
+
+static ErrorComparison ts_parser__compare_versions(
+ TSParser *self,
+ ErrorStatus a,
+ ErrorStatus b
+) {
+ if (!a.is_in_error && b.is_in_error) {
+ if (a.cost < b.cost) {
+ return ErrorComparisonTakeLeft;
+ } else {
+ return ErrorComparisonPreferLeft;
+ }
+ }
+
+ if (a.is_in_error && !b.is_in_error) {
+ if (b.cost < a.cost) {
+ return ErrorComparisonTakeRight;
+ } else {
+ return ErrorComparisonPreferRight;
+ }
+ }
+
+ if (a.cost < b.cost) {
+ if ((b.cost - a.cost) * (1 + a.node_count) > MAX_COST_DIFFERENCE) {
+ return ErrorComparisonTakeLeft;
+ } else {
+ return ErrorComparisonPreferLeft;
+ }
+ }
+
+ if (b.cost < a.cost) {
+ if ((a.cost - b.cost) * (1 + b.node_count) > MAX_COST_DIFFERENCE) {
+ return ErrorComparisonTakeRight;
+ } else {
+ return ErrorComparisonPreferRight;
+ }
+ }
+
+ if (a.dynamic_precedence > b.dynamic_precedence) return ErrorComparisonPreferLeft;
+ if (b.dynamic_precedence > a.dynamic_precedence) return ErrorComparisonPreferRight;
+ return ErrorComparisonNone;
+}
+
+static ErrorStatus ts_parser__version_status(
+ TSParser *self,
+ StackVersion version
+) {
+ unsigned cost = ts_stack_error_cost(self->stack, version);
+ bool is_paused = ts_stack_is_paused(self->stack, version);
+ if (is_paused) cost += ERROR_COST_PER_SKIPPED_TREE;
+ return (ErrorStatus) {
+ .cost = cost,
+ .node_count = ts_stack_node_count_since_error(self->stack, version),
+ .dynamic_precedence = ts_stack_dynamic_precedence(self->stack, version),
+ .is_in_error = is_paused || ts_stack_state(self->stack, version) == ERROR_STATE
+ };
+}
+
+static bool ts_parser__better_version_exists(
+ TSParser *self,
+ StackVersion version,
+ bool is_in_error,
+ unsigned cost
+) {
+ if (self->finished_tree.ptr && ts_subtree_error_cost(self->finished_tree) <= cost) {
+ return true;
+ }
+
+ Length position = ts_stack_position(self->stack, version);
+ ErrorStatus status = {
+ .cost = cost,
+ .is_in_error = is_in_error,
+ .dynamic_precedence = ts_stack_dynamic_precedence(self->stack, version),
+ .node_count = ts_stack_node_count_since_error(self->stack, version),
+ };
+
+ for (StackVersion i = 0, n = ts_stack_version_count(self->stack); i < n; i++) {
+ if (i == version ||
+ !ts_stack_is_active(self->stack, i) ||
+ ts_stack_position(self->stack, i).bytes < position.bytes) continue;
+ ErrorStatus status_i = ts_parser__version_status(self, i);
+ switch (ts_parser__compare_versions(self, status, status_i)) {
+ case ErrorComparisonTakeRight:
+ return true;
+ case ErrorComparisonPreferRight:
+ if (ts_stack_can_merge(self->stack, i, version)) return true;
+ default:
+ break;
+ }
+ }
+
+ return false;
+}
+
+static void ts_parser__restore_external_scanner(
+ TSParser *self,
+ Subtree external_token
+) {
+ if (external_token.ptr) {
+ self->language->external_scanner.deserialize(
+ self->external_scanner_payload,
+ ts_external_scanner_state_data(&external_token.ptr->external_scanner_state),
+ external_token.ptr->external_scanner_state.length
+ );
+ } else {
+ self->language->external_scanner.deserialize(self->external_scanner_payload, NULL, 0);
+ }
+}
+
+static bool ts_parser__can_reuse_first_leaf(
+ TSParser *self,
+ TSStateId state,
+ Subtree tree,
+ TableEntry *table_entry
+) {
+ TSLexMode current_lex_mode = self->language->lex_modes[state];
+ TSSymbol leaf_symbol = ts_subtree_leaf_symbol(tree);
+ TSStateId leaf_state = ts_subtree_leaf_parse_state(tree);
+ TSLexMode leaf_lex_mode = self->language->lex_modes[leaf_state];
+
+ // If the token was created in a state with the same set of lookaheads, it is reusable.
+ if (
+ table_entry->action_count > 0 &&
+ memcmp(&leaf_lex_mode, &current_lex_mode, sizeof(TSLexMode)) == 0 &&
+ (
+ leaf_symbol != self->language->keyword_capture_token ||
+ (!ts_subtree_is_keyword(tree) && ts_subtree_parse_state(tree) == state)
+ )
+ ) return true;
+
+ // Empty tokens are not reusable in states with different lookaheads.
+ if (ts_subtree_size(tree).bytes == 0 && leaf_symbol != ts_builtin_sym_end) return false;
+
+ // If the current state allows external tokens or other tokens that conflict with this
+ // token, this token is not reusable.
+ return current_lex_mode.external_lex_state == 0 && table_entry->is_reusable;
+}
+
+static Subtree ts_parser__lex(
+ TSParser *self,
+ StackVersion version,
+ TSStateId parse_state
+) {
+ Length start_position = ts_stack_position(self->stack, version);
+ Subtree external_token = ts_stack_last_external_token(self->stack, version);
+ TSLexMode lex_mode = self->language->lex_modes[parse_state];
+ const bool *valid_external_tokens = ts_language_enabled_external_tokens(
+ self->language,
+ lex_mode.external_lex_state
+ );
+
+ bool found_external_token = false;
+ bool error_mode = parse_state == ERROR_STATE;
+ bool skipped_error = false;
+ int32_t first_error_character = 0;
+ Length error_start_position = length_zero();
+ Length error_end_position = length_zero();
+ uint32_t lookahead_end_byte = 0;
+ ts_lexer_reset(&self->lexer, start_position);
+
+ for (;;) {
+ Length current_position = self->lexer.current_position;
+
+ if (valid_external_tokens) {
+ LOG(
+ "lex_external state:%d, row:%u, column:%u",
+ lex_mode.external_lex_state,
+ current_position.extent.row + 1,
+ current_position.extent.column
+ );
+ ts_lexer_start(&self->lexer);
+ ts_parser__restore_external_scanner(self, external_token);
+ bool found_token = self->language->external_scanner.scan(
+ self->external_scanner_payload,
+ &self->lexer.data,
+ valid_external_tokens
+ );
+ ts_lexer_finish(&self->lexer, &lookahead_end_byte);
+
+ // Zero-length external tokens are generally allowed, but they're not
+ // allowed right after a syntax error. This is for two reasons:
+ // 1. After a syntax error, the lexer is looking for any possible token,
+ // as opposed to the specific set of tokens that are valid in some
+ // parse state. In this situation, it's very easy for an external
+ // scanner to produce unwanted zero-length tokens.
+ // 2. The parser sometimes inserts *missing* tokens to recover from
+ // errors. These tokens are also zero-length. If we allow more
+ // zero-length tokens to be created after missing tokens, it
+ // can lead to infinite loops. Forbidding zero-length tokens
+ // right at the point of error recovery is a conservative strategy
+ // for preventing this kind of infinite loop.
+ if (found_token && (
+ self->lexer.token_end_position.bytes > current_position.bytes ||
+ (!error_mode && ts_stack_has_advanced_since_error(self->stack, version))
+ )) {
+ found_external_token = true;
+ break;
+ }
+
+ ts_lexer_reset(&self->lexer, current_position);
+ }
+
+ LOG(
+ "lex_internal state:%d, row:%u, column:%u",
+ lex_mode.lex_state,
+ current_position.extent.row + 1,
+ current_position.extent.column
+ );
+ ts_lexer_start(&self->lexer);
+ bool found_token = self->language->lex_fn(&self->lexer.data, lex_mode.lex_state);
+ ts_lexer_finish(&self->lexer, &lookahead_end_byte);
+ if (found_token) break;
+
+ if (!error_mode) {
+ error_mode = true;
+ lex_mode = self->language->lex_modes[ERROR_STATE];
+ valid_external_tokens = ts_language_enabled_external_tokens(
+ self->language,
+ lex_mode.external_lex_state
+ );
+ ts_lexer_reset(&self->lexer, start_position);
+ continue;
+ }
+
+ if (!skipped_error) {
+ LOG("skip_unrecognized_character");
+ skipped_error = true;
+ error_start_position = self->lexer.token_start_position;
+ error_end_position = self->lexer.token_start_position;
+ first_error_character = self->lexer.data.lookahead;
+ }
+
+ if (self->lexer.current_position.bytes == error_end_position.bytes) {
+ if (self->lexer.data.lookahead == 0) {
+ self->lexer.data.result_symbol = ts_builtin_sym_error;
+ break;
+ }
+ self->lexer.data.advance(&self->lexer.data, false);
+ }
+
+ error_end_position = self->lexer.current_position;
+ }
+
+ Subtree result;
+ if (skipped_error) {
+ Length padding = length_sub(error_start_position, start_position);
+ Length size = length_sub(error_end_position, error_start_position);
+ uint32_t lookahead_bytes = lookahead_end_byte - error_end_position.bytes;
+ result = ts_subtree_new_error(
+ &self->tree_pool,
+ first_error_character,
+ padding,
+ size,
+ lookahead_bytes,
+ parse_state,
+ self->language
+ );
+
+ LOG(
+ "lexed_lookahead sym:%s, size:%u, character:'%c'",
+ SYM_NAME(ts_subtree_symbol(result)),
+ ts_subtree_total_size(result).bytes,
+ first_error_character
+ );
+ } else {
+ if (self->lexer.token_end_position.bytes < self->lexer.token_start_position.bytes) {
+ self->lexer.token_start_position = self->lexer.token_end_position;
+ }
+
+ bool is_keyword = false;
+ TSSymbol symbol = self->lexer.data.result_symbol;
+ Length padding = length_sub(self->lexer.token_start_position, start_position);
+ Length size = length_sub(self->lexer.token_end_position, self->lexer.token_start_position);
+ uint32_t lookahead_bytes = lookahead_end_byte - self->lexer.token_end_position.bytes;
+
+ if (found_external_token) {
+ symbol = self->language->external_scanner.symbol_map[symbol];
+ } else if (symbol == self->language->keyword_capture_token && symbol != 0) {
+ uint32_t end_byte = self->lexer.token_end_position.bytes;
+ ts_lexer_reset(&self->lexer, self->lexer.token_start_position);
+ ts_lexer_start(&self->lexer);
+ if (
+ self->language->keyword_lex_fn(&self->lexer.data, 0) &&
+ self->lexer.token_end_position.bytes == end_byte &&
+ ts_language_has_actions(self->language, parse_state, self->lexer.data.result_symbol)
+ ) {
+ is_keyword = true;
+ symbol = self->lexer.data.result_symbol;
+ }
+ }
+
+ result = ts_subtree_new_leaf(
+ &self->tree_pool,
+ symbol,
+ padding,
+ size,
+ lookahead_bytes,
+ parse_state,
+ found_external_token,
+ is_keyword,
+ self->language
+ );
+
+ if (found_external_token) {
+ unsigned length = self->language->external_scanner.serialize(
+ self->external_scanner_payload,
+ self->lexer.debug_buffer
+ );
+ ts_external_scanner_state_init(
+ &((SubtreeHeapData *)result.ptr)->external_scanner_state,
+ self->lexer.debug_buffer,
+ length
+ );
+ }
+
+ LOG(
+ "lexed_lookahead sym:%s, size:%u",
+ SYM_NAME(ts_subtree_symbol(result)),
+ ts_subtree_total_size(result).bytes
+ );
+ }
+
+ return result;
+}
+
+static Subtree ts_parser__get_cached_token(
+ TSParser *self,
+ TSStateId state,
+ size_t position,
+ Subtree last_external_token,
+ TableEntry *table_entry
+) {
+ TokenCache *cache = &self->token_cache;
+ if (
+ cache->token.ptr && cache->byte_index == position &&
+ ts_subtree_external_scanner_state_eq(cache->last_external_token, last_external_token)
+ ) {
+ ts_language_table_entry(self->language, state, ts_subtree_symbol(cache->token), table_entry);
+ if (ts_parser__can_reuse_first_leaf(self, state, cache->token, table_entry)) {
+ ts_subtree_retain(cache->token);
+ return cache->token;
+ }
+ }
+ return NULL_SUBTREE;
+}
+
+static void ts_parser__set_cached_token(
+ TSParser *self,
+ size_t byte_index,
+ Subtree last_external_token,
+ Subtree token
+) {
+ TokenCache *cache = &self->token_cache;
+ if (token.ptr) ts_subtree_retain(token);
+ if (last_external_token.ptr) ts_subtree_retain(last_external_token);
+ if (cache->token.ptr) ts_subtree_release(&self->tree_pool, cache->token);
+ if (cache->last_external_token.ptr) ts_subtree_release(&self->tree_pool, cache->last_external_token);
+ cache->token = token;
+ cache->byte_index = byte_index;
+ cache->last_external_token = last_external_token;
+}
+
+static bool ts_parser__has_included_range_difference(
+ const TSParser *self,
+ uint32_t start_position,
+ uint32_t end_position
+) {
+ return ts_range_array_intersects(
+ &self->included_range_differences,
+ self->included_range_difference_index,
+ start_position,
+ end_position
+ );
+}
+
+static Subtree ts_parser__reuse_node(
+ TSParser *self,
+ StackVersion version,
+ TSStateId *state,
+ uint32_t position,
+ Subtree last_external_token,
+ TableEntry *table_entry
+) {
+ Subtree result;
+ while ((result = reusable_node_tree(&self->reusable_node)).ptr) {
+ uint32_t byte_offset = reusable_node_byte_offset(&self->reusable_node);
+ uint32_t end_byte_offset = byte_offset + ts_subtree_total_bytes(result);
+
+ if (byte_offset > position) {
+ LOG("before_reusable_node symbol:%s", TREE_NAME(result));
+ break;
+ }
+
+ if (byte_offset < position) {
+ LOG("past_reusable_node symbol:%s", TREE_NAME(result));
+ if (end_byte_offset <= position || !reusable_node_descend(&self->reusable_node)) {
+ reusable_node_advance(&self->reusable_node);
+ }
+ continue;
+ }
+
+ if (!ts_subtree_external_scanner_state_eq(self->reusable_node.last_external_token, last_external_token)) {
+ LOG("reusable_node_has_different_external_scanner_state symbol:%s", TREE_NAME(result));
+ reusable_node_advance(&self->reusable_node);
+ continue;
+ }
+
+ const char *reason = NULL;
+ if (ts_subtree_has_changes(result)) {
+ reason = "has_changes";
+ } else if (ts_subtree_is_error(result)) {
+ reason = "is_error";
+ } else if (ts_subtree_missing(result)) {
+ reason = "is_missing";
+ } else if (ts_subtree_is_fragile(result)) {
+ reason = "is_fragile";
+ } else if (ts_parser__has_included_range_difference(self, byte_offset, end_byte_offset)) {
+ reason = "contains_different_included_range";
+ }
+
+ if (reason) {
+ LOG("cant_reuse_node_%s tree:%s", reason, TREE_NAME(result));
+ if (!reusable_node_descend(&self->reusable_node)) {
+ reusable_node_advance(&self->reusable_node);
+ ts_parser__breakdown_top_of_stack(self, version);
+ *state = ts_stack_state(self->stack, version);
+ }
+ continue;
+ }
+
+ TSSymbol leaf_symbol = ts_subtree_leaf_symbol(result);
+ ts_language_table_entry(self->language, *state, leaf_symbol, table_entry);
+ if (!ts_parser__can_reuse_first_leaf(self, *state, result, table_entry)) {
+ LOG(
+ "cant_reuse_node symbol:%s, first_leaf_symbol:%s",
+ TREE_NAME(result),
+ SYM_NAME(leaf_symbol)
+ );
+ reusable_node_advance_past_leaf(&self->reusable_node);
+ break;
+ }
+
+ LOG("reuse_node symbol:%s", TREE_NAME(result));
+ ts_subtree_retain(result);
+ return result;
+ }
+
+ return NULL_SUBTREE;
+}
+
+static bool ts_parser__select_tree(TSParser *self, Subtree left, Subtree right) {
+ if (!left.ptr) return true;
+ if (!right.ptr) return false;
+
+ if (ts_subtree_error_cost(right) < ts_subtree_error_cost(left)) {
+ LOG("select_smaller_error symbol:%s, over_symbol:%s", TREE_NAME(right), TREE_NAME(left));
+ return true;
+ }
+
+ if (ts_subtree_error_cost(left) < ts_subtree_error_cost(right)) {
+ LOG("select_smaller_error symbol:%s, over_symbol:%s", TREE_NAME(left), TREE_NAME(right));
+ return false;
+ }
+
+ if (ts_subtree_dynamic_precedence(right) > ts_subtree_dynamic_precedence(left)) {
+ LOG("select_higher_precedence symbol:%s, prec:%u, over_symbol:%s, other_prec:%u",
+ TREE_NAME(right), ts_subtree_dynamic_precedence(right), TREE_NAME(left),
+ ts_subtree_dynamic_precedence(left));
+ return true;
+ }
+
+ if (ts_subtree_dynamic_precedence(left) > ts_subtree_dynamic_precedence(right)) {
+ LOG("select_higher_precedence symbol:%s, prec:%u, over_symbol:%s, other_prec:%u",
+ TREE_NAME(left), ts_subtree_dynamic_precedence(left), TREE_NAME(right),
+ ts_subtree_dynamic_precedence(right));
+ return false;
+ }
+
+ if (ts_subtree_error_cost(left) > 0) return true;
+
+ int comparison = ts_subtree_compare(left, right);
+ switch (comparison) {
+ case -1:
+ LOG("select_earlier symbol:%s, over_symbol:%s", TREE_NAME(left), TREE_NAME(right));
+ return false;
+ break;
+ case 1:
+ LOG("select_earlier symbol:%s, over_symbol:%s", TREE_NAME(right), TREE_NAME(left));
+ return true;
+ default:
+ LOG("select_existing symbol:%s, over_symbol:%s", TREE_NAME(left), TREE_NAME(right));
+ return false;
+ }
+}
+
+static void ts_parser__shift(
+ TSParser *self,
+ StackVersion version,
+ TSStateId state,
+ Subtree lookahead,
+ bool extra
+) {
+ Subtree subtree_to_push;
+ if (extra != ts_subtree_extra(lookahead)) {
+ MutableSubtree result = ts_subtree_make_mut(&self->tree_pool, lookahead);
+ ts_subtree_set_extra(&result);
+ subtree_to_push = ts_subtree_from_mut(result);
+ } else {
+ subtree_to_push = lookahead;
+ }
+
+ bool is_pending = ts_subtree_child_count(subtree_to_push) > 0;
+ ts_stack_push(self->stack, version, subtree_to_push, is_pending, state);
+ if (ts_subtree_has_external_tokens(subtree_to_push)) {
+ ts_stack_set_last_external_token(
+ self->stack, version, ts_subtree_last_external_token(subtree_to_push)
+ );
+ }
+}
+
+static bool ts_parser__replace_children(
+ TSParser *self,
+ MutableSubtree *tree,
+ SubtreeArray *children
+) {
+ *self->scratch_tree.ptr = *tree->ptr;
+ self->scratch_tree.ptr->child_count = 0;
+ ts_subtree_set_children(self->scratch_tree, children->contents, children->size, self->language);
+ if (ts_parser__select_tree(self, ts_subtree_from_mut(*tree), ts_subtree_from_mut(self->scratch_tree))) {
+ *tree->ptr = *self->scratch_tree.ptr;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+static StackVersion ts_parser__reduce(
+ TSParser *self,
+ StackVersion version,
+ TSSymbol symbol,
+ uint32_t count,
+ int dynamic_precedence,
+ uint16_t production_id,
+ bool fragile
+) {
+ uint32_t initial_version_count = ts_stack_version_count(self->stack);
+ uint32_t removed_version_count = 0;
+ StackSliceArray pop = ts_stack_pop_count(self->stack, version, count);
+
+ for (uint32_t i = 0; i < pop.size; i++) {
+ StackSlice slice = pop.contents[i];
+ StackVersion slice_version = slice.version - removed_version_count;
+
+ // Error recovery can sometimes cause lots of stack versions to merge,
+ // such that a single pop operation can produce a lots of slices.
+ // Avoid creating too many stack versions in that situation.
+ if (i > 0 && slice_version > MAX_VERSION_COUNT + MAX_VERSION_COUNT_OVERFLOW) {
+ ts_stack_remove_version(self->stack, slice_version);
+ ts_subtree_array_delete(&self->tree_pool, &slice.subtrees);
+ removed_version_count++;
+ while (i + 1 < pop.size) {
+ StackSlice next_slice = pop.contents[i + 1];
+ if (next_slice.version != slice.version) break;
+ ts_subtree_array_delete(&self->tree_pool, &next_slice.subtrees);
+ i++;
+ }
+ continue;
+ }
+
+ // Extra tokens on top of the stack should not be included in this new parent
+ // node. They will be re-pushed onto the stack after the parent node is
+ // created and pushed.
+ SubtreeArray children = slice.subtrees;
+ while (children.size > 0 && ts_subtree_extra(children.contents[children.size - 1])) {
+ children.size--;
+ }
+
+ MutableSubtree parent = ts_subtree_new_node(&self->tree_pool,
+ symbol, &children, production_id, self->language
+ );
+
+ // This pop operation may have caused multiple stack versions to collapse
+ // into one, because they all diverged from a common state. In that case,
+ // choose one of the arrays of trees to be the parent node's children, and
+ // delete the rest of the tree arrays.
+ while (i + 1 < pop.size) {
+ StackSlice next_slice = pop.contents[i + 1];
+ if (next_slice.version != slice.version) break;
+ i++;
+
+ SubtreeArray children = next_slice.subtrees;
+ while (children.size > 0 && ts_subtree_extra(children.contents[children.size - 1])) {
+ children.size--;
+ }
+
+ if (ts_parser__replace_children(self, &parent, &children)) {
+ ts_subtree_array_delete(&self->tree_pool, &slice.subtrees);
+ slice = next_slice;
+ } else {
+ ts_subtree_array_delete(&self->tree_pool, &next_slice.subtrees);
+ }
+ }
+
+ parent.ptr->dynamic_precedence += dynamic_precedence;
+ parent.ptr->production_id = production_id;
+
+ TSStateId state = ts_stack_state(self->stack, slice_version);
+ TSStateId next_state = ts_language_next_state(self->language, state, symbol);
+ if (fragile || pop.size > 1 || initial_version_count > 1) {
+ parent.ptr->fragile_left = true;
+ parent.ptr->fragile_right = true;
+ parent.ptr->parse_state = TS_TREE_STATE_NONE;
+ } else {
+ parent.ptr->parse_state = state;
+ }
+
+ // Push the parent node onto the stack, along with any extra tokens that
+ // were previously on top of the stack.
+ ts_stack_push(self->stack, slice_version, ts_subtree_from_mut(parent), false, next_state);
+ for (uint32_t j = parent.ptr->child_count; j < slice.subtrees.size; j++) {
+ ts_stack_push(self->stack, slice_version, slice.subtrees.contents[j], false, next_state);
+ }
+
+ for (StackVersion j = 0; j < slice_version; j++) {
+ if (j == version) continue;
+ if (ts_stack_merge(self->stack, j, slice_version)) {
+ removed_version_count++;
+ break;
+ }
+ }
+ }
+
+ // Return the first new stack version that was created.
+ return ts_stack_version_count(self->stack) > initial_version_count
+ ? initial_version_count
+ : STACK_VERSION_NONE;
+}
+
+static void ts_parser__accept(
+ TSParser *self,
+ StackVersion version,
+ Subtree lookahead
+) {
+ assert(ts_subtree_is_eof(lookahead));
+ ts_stack_push(self->stack, version, lookahead, false, 1);
+
+ StackSliceArray pop = ts_stack_pop_all(self->stack, version);
+ for (uint32_t i = 0; i < pop.size; i++) {
+ SubtreeArray trees = pop.contents[i].subtrees;
+
+ Subtree root = NULL_SUBTREE;
+ for (uint32_t j = trees.size - 1; j + 1 > 0; j--) {
+ Subtree child = trees.contents[j];
+ if (!ts_subtree_extra(child)) {
+ assert(!child.data.is_inline);
+ uint32_t child_count = ts_subtree_child_count(child);
+ for (uint32_t k = 0; k < child_count; k++) {
+ ts_subtree_retain(child.ptr->children[k]);
+ }
+ array_splice(&trees, j, 1, child_count, child.ptr->children);
+ root = ts_subtree_from_mut(ts_subtree_new_node(
+ &self->tree_pool,
+ ts_subtree_symbol(child),
+ &trees,
+ child.ptr->production_id,
+ self->language
+ ));
+ ts_subtree_release(&self->tree_pool, child);
+ break;
+ }
+ }
+
+ assert(root.ptr);
+ self->accept_count++;
+
+ if (self->finished_tree.ptr) {
+ if (ts_parser__select_tree(self, self->finished_tree, root)) {
+ ts_subtree_release(&self->tree_pool, self->finished_tree);
+ self->finished_tree = root;
+ } else {
+ ts_subtree_release(&self->tree_pool, root);
+ }
+ } else {
+ self->finished_tree = root;
+ }
+ }
+
+ ts_stack_remove_version(self->stack, pop.contents[0].version);
+ ts_stack_halt(self->stack, version);
+}
+
+static bool ts_parser__do_all_potential_reductions(
+ TSParser *self,
+ StackVersion starting_version,
+ TSSymbol lookahead_symbol
+) {
+ uint32_t initial_version_count = ts_stack_version_count(self->stack);
+
+ bool can_shift_lookahead_symbol = false;
+ StackVersion version = starting_version;
+ for (unsigned i = 0; true; i++) {
+ uint32_t version_count = ts_stack_version_count(self->stack);
+ if (version >= version_count) break;
+
+ bool merged = false;
+ for (StackVersion i = initial_version_count; i < version; i++) {
+ if (ts_stack_merge(self->stack, i, version)) {
+ merged = true;
+ break;
+ }
+ }
+ if (merged) continue;
+
+ TSStateId state = ts_stack_state(self->stack, version);
+ bool has_shift_action = false;
+ array_clear(&self->reduce_actions);
+
+ TSSymbol first_symbol, end_symbol;
+ if (lookahead_symbol != 0) {
+ first_symbol = lookahead_symbol;
+ end_symbol = lookahead_symbol + 1;
+ } else {
+ first_symbol = 1;
+ end_symbol = self->language->token_count;
+ }
+
+ for (TSSymbol symbol = first_symbol; symbol < end_symbol; symbol++) {
+ TableEntry entry;
+ ts_language_table_entry(self->language, state, symbol, &entry);
+ for (uint32_t i = 0; i < entry.action_count; i++) {
+ TSParseAction action = entry.actions[i];
+ switch (action.type) {
+ case TSParseActionTypeShift:
+ case TSParseActionTypeRecover:
+ if (!action.params.extra && !action.params.repetition) has_shift_action = true;
+ break;
+ case TSParseActionTypeReduce:
+ if (action.params.child_count > 0)
+ ts_reduce_action_set_add(&self->reduce_actions, (ReduceAction){
+ .symbol = action.params.symbol,
+ .count = action.params.child_count,
+ .dynamic_precedence = action.params.dynamic_precedence,
+ .production_id = action.params.production_id,
+ });
+ default:
+ break;
+ }
+ }
+ }
+
+ StackVersion reduction_version = STACK_VERSION_NONE;
+ for (uint32_t i = 0; i < self->reduce_actions.size; i++) {
+ ReduceAction action = self->reduce_actions.contents[i];
+
+ reduction_version = ts_parser__reduce(
+ self, version, action.symbol, action.count,
+ action.dynamic_precedence, action.production_id,
+ true
+ );
+ }
+
+ if (has_shift_action) {
+ can_shift_lookahead_symbol = true;
+ } else if (reduction_version != STACK_VERSION_NONE && i < MAX_VERSION_COUNT) {
+ ts_stack_renumber_version(self->stack, reduction_version, version);
+ continue;
+ } else if (lookahead_symbol != 0) {
+ ts_stack_remove_version(self->stack, version);
+ }
+
+ if (version == starting_version) {
+ version = version_count;
+ } else {
+ version++;
+ }
+ }
+
+ return can_shift_lookahead_symbol;
+}
+
+static void ts_parser__handle_error(
+ TSParser *self,
+ StackVersion version,
+ TSSymbol lookahead_symbol
+) {
+ uint32_t previous_version_count = ts_stack_version_count(self->stack);
+
+ // Perform any reductions that can happen in this state, regardless of the lookahead. After
+ // skipping one or more invalid tokens, the parser might find a token that would have allowed
+ // a reduction to take place.
+ ts_parser__do_all_potential_reductions(self, version, 0);
+ uint32_t version_count = ts_stack_version_count(self->stack);
+ Length position = ts_stack_position(self->stack, version);
+
+ // Push a discontinuity onto the stack. Merge all of the stack versions that
+ // were created in the previous step.
+ bool did_insert_missing_token = false;
+ for (StackVersion v = version; v < version_count;) {
+ if (!did_insert_missing_token) {
+ TSStateId state = ts_stack_state(self->stack, v);
+ for (TSSymbol missing_symbol = 1;
+ missing_symbol < self->language->token_count;
+ missing_symbol++) {
+ TSStateId state_after_missing_symbol = ts_language_next_state(
+ self->language, state, missing_symbol
+ );
+ if (state_after_missing_symbol == 0) continue;
+
+ if (ts_language_has_reduce_action(
+ self->language,
+ state_after_missing_symbol,
+ lookahead_symbol
+ )) {
+ // In case the parser is currently outside of any included range, the lexer will
+ // snap to the beginning of the next included range. The missing token's padding
+ // must be assigned to position it within the next included range.
+ ts_lexer_reset(&self->lexer, position);
+ ts_lexer_mark_end(&self->lexer);
+ Length padding = length_sub(self->lexer.token_end_position, position);
+
+ StackVersion version_with_missing_tree = ts_stack_copy_version(self->stack, v);
+ Subtree missing_tree = ts_subtree_new_missing_leaf(
+ &self->tree_pool, missing_symbol, padding, self->language
+ );
+ ts_stack_push(
+ self->stack, version_with_missing_tree,
+ missing_tree, false,
+ state_after_missing_symbol
+ );
+
+ if (ts_parser__do_all_potential_reductions(
+ self, version_with_missing_tree,
+ lookahead_symbol
+ )) {
+ LOG(
+ "recover_with_missing symbol:%s, state:%u",
+ SYM_NAME(missing_symbol),
+ ts_stack_state(self->stack, version_with_missing_tree)
+ );
+ did_insert_missing_token = true;
+ break;
+ }
+ }
+ }
+ }
+
+ ts_stack_push(self->stack, v, NULL_SUBTREE, false, ERROR_STATE);
+ v = (v == version) ? previous_version_count : v + 1;
+ }
+
+ for (unsigned i = previous_version_count; i < version_count; i++) {
+ bool did_merge = ts_stack_merge(self->stack, version, previous_version_count);
+ assert(did_merge);
+ }
+
+ ts_stack_record_summary(self->stack, version, MAX_SUMMARY_DEPTH);
+ LOG_STACK();
+}
+
+static void ts_parser__halt_parse(TSParser *self) {
+ LOG("halting_parse");
+ LOG_STACK();
+
+ ts_lexer_advance_to_end(&self->lexer);
+ Length remaining_length = length_sub(
+ self->lexer.current_position,
+ ts_stack_position(self->stack, 0)
+ );
+
+ Subtree filler_node = ts_subtree_new_error(
+ &self->tree_pool,
+ 0,
+ length_zero(),
+ remaining_length,
+ remaining_length.bytes,
+ 0,
+ self->language
+ );
+ ts_subtree_to_mut_unsafe(filler_node).ptr->visible = false;
+ ts_stack_push(self->stack, 0, filler_node, false, 0);
+
+ SubtreeArray children = array_new();
+ Subtree root_error = ts_subtree_new_error_node(&self->tree_pool, &children, false, self->language);
+ ts_stack_push(self->stack, 0, root_error, false, 0);
+
+ Subtree eof = ts_subtree_new_leaf(
+ &self->tree_pool,
+ ts_builtin_sym_end,
+ length_zero(),
+ length_zero(),
+ 0,
+ 0,
+ false,
+ false,
+ self->language
+ );
+ ts_parser__accept(self, 0, eof);
+}
+
+static bool ts_parser__recover_to_state(
+ TSParser *self,
+ StackVersion version,
+ unsigned depth,
+ TSStateId goal_state
+) {
+ StackSliceArray pop = ts_stack_pop_count(self->stack, version, depth);
+ StackVersion previous_version = STACK_VERSION_NONE;
+
+ for (unsigned i = 0; i < pop.size; i++) {
+ StackSlice slice = pop.contents[i];
+
+ if (slice.version == previous_version) {
+ ts_subtree_array_delete(&self->tree_pool, &slice.subtrees);
+ array_erase(&pop, i--);
+ continue;
+ }
+
+ if (ts_stack_state(self->stack, slice.version) != goal_state) {
+ ts_stack_halt(self->stack, slice.version);
+ ts_subtree_array_delete(&self->tree_pool, &slice.subtrees);
+ array_erase(&pop, i--);
+ continue;
+ }
+
+ SubtreeArray error_trees = ts_stack_pop_error(self->stack, slice.version);
+ if (error_trees.size > 0) {
+ assert(error_trees.size == 1);
+ Subtree error_tree = error_trees.contents[0];
+ uint32_t error_child_count = ts_subtree_child_count(error_tree);
+ if (error_child_count > 0) {
+ array_splice(&slice.subtrees, 0, 0, error_child_count, error_tree.ptr->children);
+ for (unsigned j = 0; j < error_child_count; j++) {
+ ts_subtree_retain(slice.subtrees.contents[j]);
+ }
+ }
+ ts_subtree_array_delete(&self->tree_pool, &error_trees);
+ }
+
+ SubtreeArray trailing_extras = ts_subtree_array_remove_trailing_extras(&slice.subtrees);
+
+ if (slice.subtrees.size > 0) {
+ Subtree error = ts_subtree_new_error_node(&self->tree_pool, &slice.subtrees, true, self->language);
+ ts_stack_push(self->stack, slice.version, error, false, goal_state);
+ } else {
+ array_delete(&slice.subtrees);
+ }
+
+ for (unsigned j = 0; j < trailing_extras.size; j++) {
+ Subtree tree = trailing_extras.contents[j];
+ ts_stack_push(self->stack, slice.version, tree, false, goal_state);
+ }
+
+ previous_version = slice.version;
+ array_delete(&trailing_extras);
+ }
+
+ return previous_version != STACK_VERSION_NONE;
+}
+
+static void ts_parser__recover(
+ TSParser *self,
+ StackVersion version,
+ Subtree lookahead
+) {
+ bool did_recover = false;
+ unsigned previous_version_count = ts_stack_version_count(self->stack);
+ Length position = ts_stack_position(self->stack, version);
+ StackSummary *summary = ts_stack_get_summary(self->stack, version);
+ unsigned node_count_since_error = ts_stack_node_count_since_error(self->stack, version);
+ unsigned current_error_cost = ts_stack_error_cost(self->stack, version);
+
+ // When the parser is in the error state, there are two strategies for recovering with a
+ // given lookahead token:
+ // 1. Find a previous state on the stack in which that lookahead token would be valid. Then,
+ // create a new stack version that is in that state again. This entails popping all of the
+ // subtrees that have been pushed onto the stack since that previous state, and wrapping
+ // them in an ERROR node.
+ // 2. Wrap the lookahead token in an ERROR node, push that ERROR node onto the stack, and
+ // move on to the next lookahead token, remaining in the error state.
+ //
+ // First, try the strategy 1. Upon entering the error state, the parser recorded a summary
+ // of the previous parse states and their depths. Look at each state in the summary, to see
+ // if the current lookahead token would be valid in that state.
+ if (summary && !ts_subtree_is_error(lookahead)) {
+ for (unsigned i = 0; i < summary->size; i++) {
+ StackSummaryEntry entry = summary->contents[i];
+
+ if (entry.state == ERROR_STATE) continue;
+ if (entry.position.bytes == position.bytes) continue;
+ unsigned depth = entry.depth;
+ if (node_count_since_error > 0) depth++;
+
+ // Do not recover in ways that create redundant stack versions.
+ bool would_merge = false;
+ for (unsigned j = 0; j < previous_version_count; j++) {
+ if (
+ ts_stack_state(self->stack, j) == entry.state &&
+ ts_stack_position(self->stack, j).bytes == position.bytes
+ ) {
+ would_merge = true;
+ break;
+ }
+ }
+ if (would_merge) continue;
+
+ // Do not recover if the result would clearly be worse than some existing stack version.
+ unsigned new_cost =
+ current_error_cost +
+ entry.depth * ERROR_COST_PER_SKIPPED_TREE +
+ (position.bytes - entry.position.bytes) * ERROR_COST_PER_SKIPPED_CHAR +
+ (position.extent.row - entry.position.extent.row) * ERROR_COST_PER_SKIPPED_LINE;
+ if (ts_parser__better_version_exists(self, version, false, new_cost)) break;
+
+ // If the current lookahead token is valid in some previous state, recover to that state.
+ // Then stop looking for further recoveries.
+ if (ts_language_has_actions(self->language, entry.state, ts_subtree_symbol(lookahead))) {
+ if (ts_parser__recover_to_state(self, version, depth, entry.state)) {
+ did_recover = true;
+ LOG("recover_to_previous state:%u, depth:%u", entry.state, depth);
+ LOG_STACK();
+ break;
+ }
+ }
+ }
+ }
+
+ // In the process of attemping to recover, some stack versions may have been created
+ // and subsequently halted. Remove those versions.
+ for (unsigned i = previous_version_count; i < ts_stack_version_count(self->stack); i++) {
+ if (!ts_stack_is_active(self->stack, i)) {
+ ts_stack_remove_version(self->stack, i--);
+ }
+ }
+
+ // If strategy 1 succeeded, a new stack version will have been created which is able to handle
+ // the current lookahead token. Now, in addition, try strategy 2 described above: skip the
+ // current lookahead token by wrapping it in an ERROR node.
+
+ // Don't pursue this additional strategy if there are already too many stack versions.
+ if (did_recover && ts_stack_version_count(self->stack) > MAX_VERSION_COUNT) {
+ ts_stack_halt(self->stack, version);
+ ts_subtree_release(&self->tree_pool, lookahead);
+ return;
+ }
+
+ // If the parser is still in the error state at the end of the file, just wrap everything
+ // in an ERROR node and terminate.
+ if (ts_subtree_is_eof(lookahead)) {
+ LOG("recover_eof");
+ SubtreeArray children = array_new();
+ Subtree parent = ts_subtree_new_error_node(&self->tree_pool, &children, false, self->language);
+ ts_stack_push(self->stack, version, parent, false, 1);
+ ts_parser__accept(self, version, lookahead);
+ return;
+ }
+
+ // Do not recover if the result would clearly be worse than some existing stack version.
+ unsigned new_cost =
+ current_error_cost + ERROR_COST_PER_SKIPPED_TREE +
+ ts_subtree_total_bytes(lookahead) * ERROR_COST_PER_SKIPPED_CHAR +
+ ts_subtree_total_size(lookahead).extent.row * ERROR_COST_PER_SKIPPED_LINE;
+ if (ts_parser__better_version_exists(self, version, false, new_cost)) {
+ ts_stack_halt(self->stack, version);
+ ts_subtree_release(&self->tree_pool, lookahead);
+ return;
+ }
+
+ // If the current lookahead token is an extra token, mark it as extra. This means it won't
+ // be counted in error cost calculations.
+ unsigned n;
+ const TSParseAction *actions = ts_language_actions(self->language, 1, ts_subtree_symbol(lookahead), &n);
+ if (n > 0 && actions[n - 1].type == TSParseActionTypeShift && actions[n - 1].params.extra) {
+ MutableSubtree mutable_lookahead = ts_subtree_make_mut(&self->tree_pool, lookahead);
+ ts_subtree_set_extra(&mutable_lookahead);
+ lookahead = ts_subtree_from_mut(mutable_lookahead);
+ }
+
+ // Wrap the lookahead token in an ERROR.
+ LOG("skip_token symbol:%s", TREE_NAME(lookahead));
+ SubtreeArray children = array_new();
+ array_reserve(&children, 1);
+ array_push(&children, lookahead);
+ MutableSubtree error_repeat = ts_subtree_new_node(
+ &self->tree_pool,
+ ts_builtin_sym_error_repeat,
+ &children,
+ 0,
+ self->language
+ );
+
+ // If other tokens have already been skipped, so there is already an ERROR at the top of the
+ // stack, then pop that ERROR off the stack and wrap the two ERRORs together into one larger
+ // ERROR.
+ if (node_count_since_error > 0) {
+ StackSliceArray pop = ts_stack_pop_count(self->stack, version, 1);
+
+ // TODO: Figure out how to make this condition occur.
+ // See https://github.com/atom/atom/issues/18450#issuecomment-439579778
+ // If multiple stack versions have merged at this point, just pick one of the errors
+ // arbitrarily and discard the rest.
+ if (pop.size > 1) {
+ for (unsigned i = 1; i < pop.size; i++) {
+ ts_subtree_array_delete(&self->tree_pool, &pop.contents[i].subtrees);
+ }
+ while (ts_stack_version_count(self->stack) > pop.contents[0].version + 1) {
+ ts_stack_remove_version(self->stack, pop.contents[0].version + 1);
+ }
+ }
+
+ ts_stack_renumber_version(self->stack, pop.contents[0].version, version);
+ array_push(&pop.contents[0].subtrees, ts_subtree_from_mut(error_repeat));
+ error_repeat = ts_subtree_new_node(
+ &self->tree_pool,
+ ts_builtin_sym_error_repeat,
+ &pop.contents[0].subtrees,
+ 0,
+ self->language
+ );
+ }
+
+ // Push the new ERROR onto the stack.
+ ts_stack_push(self->stack, version, ts_subtree_from_mut(error_repeat), false, ERROR_STATE);
+ if (ts_subtree_has_external_tokens(lookahead)) {
+ ts_stack_set_last_external_token(
+ self->stack, version, ts_subtree_last_external_token(lookahead)
+ );
+ }
+}
+
+static bool ts_parser__advance(
+ TSParser *self,
+ StackVersion version,
+ bool allow_node_reuse
+) {
+ TSStateId state = ts_stack_state(self->stack, version);
+ uint32_t position = ts_stack_position(self->stack, version).bytes;
+ Subtree last_external_token = ts_stack_last_external_token(self->stack, version);
+
+ bool did_reuse = true;
+ Subtree lookahead = NULL_SUBTREE;
+ TableEntry table_entry = {.action_count = 0};
+
+ // If possible, reuse a node from the previous syntax tree.
+ if (allow_node_reuse) {
+ lookahead = ts_parser__reuse_node(
+ self, version, &state, position, last_external_token, &table_entry
+ );
+ }
+
+ // If no node from the previous syntax tree could be reused, then try to
+ // reuse the token previously returned by the lexer.
+ if (!lookahead.ptr) {
+ did_reuse = false;
+ lookahead = ts_parser__get_cached_token(
+ self, state, position, last_external_token, &table_entry
+ );
+ }
+
+ // Otherwise, re-run the lexer.
+ if (!lookahead.ptr) {
+ lookahead = ts_parser__lex(self, version, state);
+ ts_parser__set_cached_token(self, position, last_external_token, lookahead);
+ ts_language_table_entry(self->language, state, ts_subtree_symbol(lookahead), &table_entry);
+ }
+
+ for (;;) {
+ // If a cancellation flag or a timeout was provided, then check every
+ // time a fixed number of parse actions has been processed.
+ if (++self->operation_count == OP_COUNT_PER_TIMEOUT_CHECK) {
+ self->operation_count = 0;
+ }
+ if (
+ self->operation_count == 0 &&
+ ((self->cancellation_flag && atomic_load(self->cancellation_flag)) ||
+ (!clock_is_null(self->end_clock) && clock_is_gt(clock_now(), self->end_clock)))
+ ) {
+ ts_subtree_release(&self->tree_pool, lookahead);
+ return false;
+ }
+
+ // Process each parse action for the current lookahead token in
+ // the current state. If there are multiple actions, then this is
+ // an ambiguous state. REDUCE actions always create a new stack
+ // version, whereas SHIFT actions update the existing stack version
+ // and terminate this loop.
+ StackVersion last_reduction_version = STACK_VERSION_NONE;
+ for (uint32_t i = 0; i < table_entry.action_count; i++) {
+ TSParseAction action = table_entry.actions[i];
+
+ switch (action.type) {
+ case TSParseActionTypeShift: {
+ if (action.params.repetition) break;
+ TSStateId next_state;
+ if (action.params.extra) {
+
+ // TODO: remove when TREE_SITTER_LANGUAGE_VERSION 9 is out.
+ if (state == ERROR_STATE) continue;
+
+ next_state = state;
+ LOG("shift_extra");
+ } else {
+ next_state = action.params.state;
+ LOG("shift state:%u", next_state);
+ }
+
+ if (ts_subtree_child_count(lookahead) > 0) {
+ ts_parser__breakdown_lookahead(self, &lookahead, state, &self->reusable_node);
+ next_state = ts_language_next_state(self->language, state, ts_subtree_symbol(lookahead));
+ }
+
+ ts_parser__shift(self, version, next_state, lookahead, action.params.extra);
+ if (did_reuse) reusable_node_advance(&self->reusable_node);
+ return true;
+ }
+
+ case TSParseActionTypeReduce: {
+ bool is_fragile = table_entry.action_count > 1;
+ LOG("reduce sym:%s, child_count:%u", SYM_NAME(action.params.symbol), action.params.child_count);
+ StackVersion reduction_version = ts_parser__reduce(
+ self, version, action.params.symbol, action.params.child_count,
+ action.params.dynamic_precedence, action.params.production_id,
+ is_fragile
+ );
+ if (reduction_version != STACK_VERSION_NONE) {
+ last_reduction_version = reduction_version;
+ }
+ break;
+ }
+
+ case TSParseActionTypeAccept: {
+ LOG("accept");
+ ts_parser__accept(self, version, lookahead);
+ return true;
+ }
+
+ case TSParseActionTypeRecover: {
+ if (ts_subtree_child_count(lookahead) > 0) {
+ ts_parser__breakdown_lookahead(self, &lookahead, ERROR_STATE, &self->reusable_node);
+ }
+
+ ts_parser__recover(self, version, lookahead);
+ if (did_reuse) reusable_node_advance(&self->reusable_node);
+ return true;
+ }
+ }
+ }
+
+ // If a reduction was performed, then replace the current stack version
+ // with one of the stack versions created by a reduction, and continue
+ // processing this version of the stack with the same lookahead symbol.
+ if (last_reduction_version != STACK_VERSION_NONE) {
+ ts_stack_renumber_version(self->stack, last_reduction_version, version);
+ LOG_STACK();
+ state = ts_stack_state(self->stack, version);
+ ts_language_table_entry(
+ self->language,
+ state,
+ ts_subtree_leaf_symbol(lookahead),
+ &table_entry
+ );
+ continue;
+ }
+
+ // If there were no parse actions for the current lookahead token, then
+ // it is not valid in this state. If the current lookahead token is a
+ // keyword, then switch to treating it as the normal word token if that
+ // token is valid in this state.
+ if (
+ ts_subtree_is_keyword(lookahead) &&
+ ts_subtree_symbol(lookahead) != self->language->keyword_capture_token
+ ) {
+ ts_language_table_entry(self->language, state, self->language->keyword_capture_token, &table_entry);
+ if (table_entry.action_count > 0) {
+ LOG(
+ "switch from_keyword:%s, to_word_token:%s",
+ TREE_NAME(lookahead),
+ SYM_NAME(self->language->keyword_capture_token)
+ );
+
+ MutableSubtree mutable_lookahead = ts_subtree_make_mut(&self->tree_pool, lookahead);
+ ts_subtree_set_symbol(&mutable_lookahead, self->language->keyword_capture_token, self->language);
+ lookahead = ts_subtree_from_mut(mutable_lookahead);
+ continue;
+ }
+ }
+
+ // If the current lookahead token is not valid and the parser is
+ // already in the error state, restart the error recovery process.
+ // TODO - can this be unified with the other `RECOVER` case above?
+ if (state == ERROR_STATE) {
+ ts_parser__recover(self, version, lookahead);
+ return true;
+ }
+
+ // If the current lookahead token is not valid and the previous
+ // subtree on the stack was reused from an old tree, it isn't actually
+ // valid to reuse it. Remove it from the stack, and in its place,
+ // push each of its children. Then try again to process the current
+ // lookahead.
+ if (ts_parser__breakdown_top_of_stack(self, version)) {
+ continue;
+ }
+
+ // At this point, the current lookahead token is definitely not valid
+ // for this parse stack version. Mark this version as paused and continue
+ // processing any other stack versions that might exist. If some other
+ // version advances successfully, then this version can simply be removed.
+ // But if all versions end up paused, then error recovery is needed.
+ LOG("detect_error");
+ ts_stack_pause(self->stack, version, ts_subtree_leaf_symbol(lookahead));
+ ts_subtree_release(&self->tree_pool, lookahead);
+ return true;
+ }
+}
+
+static unsigned ts_parser__condense_stack(TSParser *self) {
+ bool made_changes = false;
+ unsigned min_error_cost = UINT_MAX;
+ for (StackVersion i = 0; i < ts_stack_version_count(self->stack); i++) {
+ // Prune any versions that have been marked for removal.
+ if (ts_stack_is_halted(self->stack, i)) {
+ ts_stack_remove_version(self->stack, i);
+ i--;
+ continue;
+ }
+
+ // Keep track of the minimum error cost of any stack version so
+ // that it can be returned.
+ ErrorStatus status_i = ts_parser__version_status(self, i);
+ if (!status_i.is_in_error && status_i.cost < min_error_cost) {
+ min_error_cost = status_i.cost;
+ }
+
+ // Examine each pair of stack versions, removing any versions that
+ // are clearly worse than another version. Ensure that the versions
+ // are ordered from most promising to least promising.
+ for (StackVersion j = 0; j < i; j++) {
+ ErrorStatus status_j = ts_parser__version_status(self, j);
+
+ switch (ts_parser__compare_versions(self, status_j, status_i)) {
+ case ErrorComparisonTakeLeft:
+ made_changes = true;
+ ts_stack_remove_version(self->stack, i);
+ i--;
+ j = i;
+ break;
+
+ case ErrorComparisonPreferLeft:
+ case ErrorComparisonNone:
+ if (ts_stack_merge(self->stack, j, i)) {
+ made_changes = true;
+ i--;
+ j = i;
+ }
+ break;
+
+ case ErrorComparisonPreferRight:
+ made_changes = true;
+ if (ts_stack_merge(self->stack, j, i)) {
+ i--;
+ j = i;
+ } else {
+ ts_stack_swap_versions(self->stack, i, j);
+ }
+ break;
+
+ case ErrorComparisonTakeRight:
+ made_changes = true;
+ ts_stack_remove_version(self->stack, j);
+ i--;
+ j--;
+ break;
+ }
+ }
+ }
+
+ // Enfore a hard upper bound on the number of stack versions by
+ // discarding the least promising versions.
+ while (ts_stack_version_count(self->stack) > MAX_VERSION_COUNT) {
+ ts_stack_remove_version(self->stack, MAX_VERSION_COUNT);
+ made_changes = true;
+ }
+
+ // If the best-performing stack version is currently paused, or all
+ // versions are paused, then resume the best paused version and begin
+ // the error recovery process. Otherwise, remove the paused versions.
+ if (ts_stack_version_count(self->stack) > 0) {
+ bool has_unpaused_version = false;
+ for (StackVersion i = 0, n = ts_stack_version_count(self->stack); i < n; i++) {
+ if (ts_stack_is_paused(self->stack, i)) {
+ if (!has_unpaused_version && self->accept_count < MAX_VERSION_COUNT) {
+ LOG("resume version:%u", i);
+ min_error_cost = ts_stack_error_cost(self->stack, i);
+ TSSymbol lookahead_symbol = ts_stack_resume(self->stack, i);
+ ts_parser__handle_error(self, i, lookahead_symbol);
+ has_unpaused_version = true;
+ } else {
+ ts_stack_remove_version(self->stack, i);
+ i--;
+ n--;
+ }
+ } else {
+ has_unpaused_version = true;
+ }
+ }
+ }
+
+ if (made_changes) {
+ LOG("condense");
+ LOG_STACK();
+ }
+
+ return min_error_cost;
+}
+
+static bool ts_parser_has_outstanding_parse(TSParser *self) {
+ return (
+ self->lexer.current_position.bytes > 0 ||
+ ts_stack_state(self->stack, 0) != 1
+ );
+}
+
+// Parser - Public
+
+TSParser *ts_parser_new(void) {
+ TSParser *self = ts_calloc(1, sizeof(TSParser));
+ ts_lexer_init(&self->lexer);
+ array_init(&self->reduce_actions);
+ array_reserve(&self->reduce_actions, 4);
+ self->tree_pool = ts_subtree_pool_new(32);
+ self->stack = ts_stack_new(&self->tree_pool);
+ self->finished_tree = NULL_SUBTREE;
+ self->reusable_node = reusable_node_new();
+ self->dot_graph_file = NULL;
+ self->halt_on_error = false;
+ self->cancellation_flag = NULL;
+ self->timeout_duration = 0;
+ self->end_clock = clock_null();
+ self->operation_count = 0;
+ self->old_tree = NULL_SUBTREE;
+ self->scratch_tree.ptr = &self->scratch_tree_data;
+ self->included_range_differences = (TSRangeArray) array_new();
+ self->included_range_difference_index = 0;
+ ts_parser__set_cached_token(self, 0, NULL_SUBTREE, NULL_SUBTREE);
+ return self;
+}
+
+void ts_parser_delete(TSParser *self) {
+ if (!self) return;
+
+ ts_stack_delete(self->stack);
+ if (self->reduce_actions.contents) {
+ array_delete(&self->reduce_actions);
+ }
+ if (self->included_range_differences.contents) {
+ array_delete(&self->included_range_differences);
+ }
+ if (self->old_tree.ptr) {
+ ts_subtree_release(&self->tree_pool, self->old_tree);
+ self->old_tree = NULL_SUBTREE;
+ }
+ ts_lexer_delete(&self->lexer);
+ ts_parser__set_cached_token(self, 0, NULL_SUBTREE, NULL_SUBTREE);
+ ts_subtree_pool_delete(&self->tree_pool);
+ reusable_node_delete(&self->reusable_node);
+ ts_parser_set_language(self, NULL);
+ ts_free(self);
+}
+
+const TSLanguage *ts_parser_language(const TSParser *self) {
+ return self->language;
+}
+
+bool ts_parser_set_language(TSParser *self, const TSLanguage *language) {
+ if (language) {
+ if (language->version > TREE_SITTER_LANGUAGE_VERSION) return false;
+ if (language->version < TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION) return false;
+ }
+
+ if (self->external_scanner_payload && self->language->external_scanner.destroy) {
+ self->language->external_scanner.destroy(self->external_scanner_payload);
+ }
+
+ if (language && language->external_scanner.create) {
+ self->external_scanner_payload = language->external_scanner.create();
+ } else {
+ self->external_scanner_payload = NULL;
+ }
+
+ self->language = language;
+ return true;
+}
+
+TSLogger ts_parser_logger(const TSParser *self) {
+ return self->lexer.logger;
+}
+
+void ts_parser_set_logger(TSParser *self, TSLogger logger) {
+ self->lexer.logger = logger;
+}
+
+void ts_parser_print_dot_graphs(TSParser *self, int fd) {
+ if (self->dot_graph_file) {
+ fclose(self->dot_graph_file);
+ }
+
+ if (fd >= 0) {
+ self->dot_graph_file = fdopen(fd, "a");
+ } else {
+ self->dot_graph_file = NULL;
+ }
+}
+
+void ts_parser_halt_on_error(TSParser *self, bool should_halt_on_error) {
+ self->halt_on_error = should_halt_on_error;
+}
+
+const size_t *ts_parser_cancellation_flag(const TSParser *self) {
+ return (const size_t *)self->cancellation_flag;
+}
+
+void ts_parser_set_cancellation_flag(TSParser *self, const size_t *flag) {
+ self->cancellation_flag = (const volatile size_t *)flag;
+}
+
+uint64_t ts_parser_timeout_micros(const TSParser *self) {
+ return duration_to_micros(self->timeout_duration);
+}
+
+void ts_parser_set_timeout_micros(TSParser *self, uint64_t timeout_micros) {
+ self->timeout_duration = duration_from_micros(timeout_micros);
+}
+
+void ts_parser_set_included_ranges(TSParser *self, const TSRange *ranges, uint32_t count) {
+ ts_lexer_set_included_ranges(&self->lexer, ranges, count);
+}
+
+const TSRange *ts_parser_included_ranges(const TSParser *self, uint32_t *count) {
+ return ts_lexer_included_ranges(&self->lexer, count);
+}
+
+void ts_parser_reset(TSParser *self) {
+ if (self->language->external_scanner.deserialize) {
+ self->language->external_scanner.deserialize(self->external_scanner_payload, NULL, 0);
+ }
+
+ if (self->old_tree.ptr) {
+ ts_subtree_release(&self->tree_pool, self->old_tree);
+ self->old_tree = NULL_SUBTREE;
+ }
+
+ reusable_node_clear(&self->reusable_node);
+ ts_lexer_reset(&self->lexer, length_zero());
+ ts_stack_clear(self->stack);
+ ts_parser__set_cached_token(self, 0, NULL_SUBTREE, NULL_SUBTREE);
+ if (self->finished_tree.ptr) {
+ ts_subtree_release(&self->tree_pool, self->finished_tree);
+ self->finished_tree = NULL_SUBTREE;
+ }
+ self->accept_count = 0;
+}
+
+TSTree *ts_parser_parse(
+ TSParser *self,
+ const TSTree *old_tree,
+ TSInput input
+) {
+ if (!self->language || !input.read) return NULL;
+
+ ts_lexer_set_input(&self->lexer, input);
+
+ array_clear(&self->included_range_differences);
+ self->included_range_difference_index = 0;
+
+ if (ts_parser_has_outstanding_parse(self)) {
+ LOG("resume_parsing");
+ } else if (old_tree) {
+ ts_subtree_retain(old_tree->root);
+ self->old_tree = old_tree->root;
+ ts_range_array_get_changed_ranges(
+ old_tree->included_ranges, old_tree->included_range_count,
+ self->lexer.included_ranges, self->lexer.included_range_count,
+ &self->included_range_differences
+ );
+ reusable_node_reset(&self->reusable_node, old_tree->root);
+ LOG("parse_after_edit");
+ LOG_TREE(self->old_tree);
+ for (unsigned i = 0; i < self->included_range_differences.size; i++) {
+ TSRange *range = &self->included_range_differences.contents[i];
+ LOG("different_included_range %u - %u", range->start_byte, range->end_byte);
+ }
+ } else {
+ reusable_node_clear(&self->reusable_node);
+ LOG("new_parse");
+ }
+
+ uint32_t position = 0, last_position = 0, version_count = 0;
+ self->operation_count = 0;
+ if (self->timeout_duration) {
+ self->end_clock = clock_after(clock_now(), self->timeout_duration);
+ } else {
+ self->end_clock = clock_null();
+ }
+
+ do {
+ for (StackVersion version = 0;
+ version_count = ts_stack_version_count(self->stack), version < version_count;
+ version++) {
+ bool allow_node_reuse = version_count == 1;
+ while (ts_stack_is_active(self->stack, version)) {
+ LOG("process version:%d, version_count:%u, state:%d, row:%u, col:%u",
+ version, ts_stack_version_count(self->stack),
+ ts_stack_state(self->stack, version),
+ ts_stack_position(self->stack, version).extent.row + 1,
+ ts_stack_position(self->stack, version).extent.column);
+
+ if (!ts_parser__advance(self, version, allow_node_reuse)) return NULL;
+ LOG_STACK();
+
+ position = ts_stack_position(self->stack, version).bytes;
+ if (position > last_position || (version > 0 && position == last_position)) {
+ last_position = position;
+ break;
+ }
+ }
+ }
+
+ unsigned min_error_cost = ts_parser__condense_stack(self);
+ if (self->finished_tree.ptr && ts_subtree_error_cost(self->finished_tree) < min_error_cost) {
+ break;
+ } else if (self->halt_on_error && min_error_cost > 0) {
+ ts_parser__halt_parse(self);
+ break;
+ }
+
+ while (self->included_range_difference_index < self->included_range_differences.size) {
+ TSRange *range = &self->included_range_differences.contents[self->included_range_difference_index];
+ if (range->end_byte <= position) {
+ self->included_range_difference_index++;
+ } else {
+ break;
+ }
+ }
+ } while (version_count != 0);
+
+ ts_subtree_balance(self->finished_tree, &self->tree_pool, self->language);
+ LOG("done");
+ LOG_TREE(self->finished_tree);
+
+ TSTree *result = ts_tree_new(
+ self->finished_tree,
+ self->language,
+ self->lexer.included_ranges,
+ self->lexer.included_range_count
+ );
+ self->finished_tree = NULL_SUBTREE;
+ ts_parser_reset(self);
+ return result;
+}
+
+TSTree *ts_parser_parse_string(
+ TSParser *self,
+ const TSTree *old_tree,
+ const char *string,
+ uint32_t length
+) {
+ return ts_parser_parse_string_encoding(self, old_tree, string, length, TSInputEncodingUTF8);
+}
+
+TSTree *ts_parser_parse_string_encoding(TSParser *self, const TSTree *old_tree,
+ const char *string, uint32_t length, TSInputEncoding encoding) {
+ TSStringInput input = {string, length};
+ return ts_parser_parse(self, old_tree, (TSInput) {
+ &input,
+ ts_string_input_read,
+ encoding,
+ });
+}
+
+#undef LOG
diff --git a/src/tree_sitter/parser.h b/src/tree_sitter/parser.h
new file mode 100644
index 0000000000..974a7ca52f
--- /dev/null
+++ b/src/tree_sitter/parser.h
@@ -0,0 +1,220 @@
+#ifndef TREE_SITTER_PARSER_H_
+#define TREE_SITTER_PARSER_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#define ts_builtin_sym_error ((TSSymbol)-1)
+#define ts_builtin_sym_end 0
+#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
+
+#ifndef TREE_SITTER_API_H_
+typedef uint16_t TSSymbol;
+typedef uint16_t TSFieldId;
+typedef struct TSLanguage TSLanguage;
+#endif
+
+typedef struct {
+ TSFieldId field_id;
+ uint8_t child_index;
+ bool inherited;
+} TSFieldMapEntry;
+
+typedef struct {
+ uint16_t index;
+ uint16_t length;
+} TSFieldMapSlice;
+
+typedef uint16_t TSStateId;
+
+typedef struct {
+ bool visible : 1;
+ bool named : 1;
+} TSSymbolMetadata;
+
+typedef struct TSLexer TSLexer;
+
+struct TSLexer {
+ int32_t lookahead;
+ TSSymbol result_symbol;
+ void (*advance)(TSLexer *, bool);
+ void (*mark_end)(TSLexer *);
+ uint32_t (*get_column)(TSLexer *);
+ bool (*is_at_included_range_start)(TSLexer *);
+};
+
+typedef enum {
+ TSParseActionTypeShift,
+ TSParseActionTypeReduce,
+ TSParseActionTypeAccept,
+ TSParseActionTypeRecover,
+} TSParseActionType;
+
+typedef struct {
+ union {
+ struct {
+ TSStateId state;
+ bool extra : 1;
+ bool repetition : 1;
+ };
+ struct {
+ TSSymbol symbol;
+ int16_t dynamic_precedence;
+ uint8_t child_count;
+ uint8_t production_id;
+ };
+ } params;
+ TSParseActionType type : 4;
+} TSParseAction;
+
+typedef struct {
+ uint16_t lex_state;
+ uint16_t external_lex_state;
+} TSLexMode;
+
+typedef union {
+ TSParseAction action;
+ struct {
+ uint8_t count;
+ bool reusable : 1;
+ };
+} TSParseActionEntry;
+
+struct TSLanguage {
+ uint32_t version;
+ uint32_t symbol_count;
+ uint32_t alias_count;
+ uint32_t token_count;
+ uint32_t external_token_count;
+ const char **symbol_names;
+ const TSSymbolMetadata *symbol_metadata;
+ const uint16_t *parse_table;
+ const TSParseActionEntry *parse_actions;
+ const TSLexMode *lex_modes;
+ const TSSymbol *alias_sequences;
+ uint16_t max_alias_sequence_length;
+ bool (*lex_fn)(TSLexer *, TSStateId);
+ bool (*keyword_lex_fn)(TSLexer *, TSStateId);
+ TSSymbol keyword_capture_token;
+ struct {
+ const bool *states;
+ const TSSymbol *symbol_map;
+ void *(*create)(void);
+ void (*destroy)(void *);
+ bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
+ unsigned (*serialize)(void *, char *);
+ void (*deserialize)(void *, const char *, unsigned);
+ } external_scanner;
+ uint32_t field_count;
+ const TSFieldMapSlice *field_map_slices;
+ const TSFieldMapEntry *field_map_entries;
+ const char **field_names;
+ uint32_t large_state_count;
+ const uint16_t *small_parse_table;
+ const uint32_t *small_parse_table_map;
+};
+
+/*
+ * Lexer Macros
+ */
+
+#define START_LEXER() \
+ bool result = false; \
+ bool skip = false; \
+ int32_t lookahead; \
+ goto start; \
+ next_state: \
+ lexer->advance(lexer, skip); \
+ start: \
+ skip = false; \
+ lookahead = lexer->lookahead;
+
+#define ADVANCE(state_value) \
+ { \
+ state = state_value; \
+ goto next_state; \
+ }
+
+#define SKIP(state_value) \
+ { \
+ skip = true; \
+ state = state_value; \
+ goto next_state; \
+ }
+
+#define ACCEPT_TOKEN(symbol_value) \
+ result = true; \
+ lexer->result_symbol = symbol_value; \
+ lexer->mark_end(lexer);
+
+#define END_STATE() return result;
+
+/*
+ * Parse Table Macros
+ */
+
+#define SMALL_STATE(id) id - LARGE_STATE_COUNT
+
+#define STATE(id) id
+
+#define ACTIONS(id) id
+
+#define SHIFT(state_value) \
+ { \
+ { \
+ .type = TSParseActionTypeShift, \
+ .params = {.state = state_value}, \
+ } \
+ }
+
+#define SHIFT_REPEAT(state_value) \
+ { \
+ { \
+ .type = TSParseActionTypeShift, \
+ .params = { \
+ .state = state_value, \
+ .repetition = true \
+ }, \
+ } \
+ }
+
+#define RECOVER() \
+ { \
+ { .type = TSParseActionTypeRecover } \
+ }
+
+#define SHIFT_EXTRA() \
+ { \
+ { \
+ .type = TSParseActionTypeShift, \
+ .params = {.extra = true} \
+ } \
+ }
+
+#define REDUCE(symbol_val, child_count_val, ...) \
+ { \
+ { \
+ .type = TSParseActionTypeReduce, \
+ .params = { \
+ .symbol = symbol_val, \
+ .child_count = child_count_val, \
+ __VA_ARGS__ \
+ } \
+ } \
+ }
+
+#define ACCEPT_INPUT() \
+ { \
+ { .type = TSParseActionTypeAccept } \
+ }
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_PARSER_H_
diff --git a/src/tree_sitter/point.h b/src/tree_sitter/point.h
new file mode 100644
index 0000000000..4d0aed18ef
--- /dev/null
+++ b/src/tree_sitter/point.h
@@ -0,0 +1,53 @@
+#ifndef TREE_SITTER_POINT_H_
+#define TREE_SITTER_POINT_H_
+
+#include "tree_sitter/api.h"
+
+#define POINT_MAX ((TSPoint) {UINT32_MAX, UINT32_MAX})
+
+static inline TSPoint point__new(unsigned row, unsigned column) {
+ TSPoint result = {row, column};
+ return result;
+}
+
+static inline TSPoint point_add(TSPoint a, TSPoint b) {
+ if (b.row > 0)
+ return point__new(a.row + b.row, b.column);
+ else
+ return point__new(a.row, a.column + b.column);
+}
+
+static inline TSPoint point_sub(TSPoint a, TSPoint b) {
+ if (a.row > b.row)
+ return point__new(a.row - b.row, a.column);
+ else
+ return point__new(0, a.column - b.column);
+}
+
+static inline bool point_lte(TSPoint a, TSPoint b) {
+ return (a.row < b.row) || (a.row == b.row && a.column <= b.column);
+}
+
+static inline bool point_lt(TSPoint a, TSPoint b) {
+ return (a.row < b.row) || (a.row == b.row && a.column < b.column);
+}
+
+static inline bool point_eq(TSPoint a, TSPoint b) {
+ return a.row == b.row && a.column == b.column;
+}
+
+static inline TSPoint point_min(TSPoint a, TSPoint b) {
+ if (a.row < b.row || (a.row == b.row && a.column < b.column))
+ return a;
+ else
+ return b;
+}
+
+static inline TSPoint point_max(TSPoint a, TSPoint b) {
+ if (a.row > b.row || (a.row == b.row && a.column > b.column))
+ return a;
+ else
+ return b;
+}
+
+#endif
diff --git a/src/tree_sitter/reduce_action.h b/src/tree_sitter/reduce_action.h
new file mode 100644
index 0000000000..72aff08d73
--- /dev/null
+++ b/src/tree_sitter/reduce_action.h
@@ -0,0 +1,34 @@
+#ifndef TREE_SITTER_REDUCE_ACTION_H_
+#define TREE_SITTER_REDUCE_ACTION_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "./array.h"
+#include "tree_sitter/api.h"
+
+typedef struct {
+ uint32_t count;
+ TSSymbol symbol;
+ int dynamic_precedence;
+ unsigned short production_id;
+} ReduceAction;
+
+typedef Array(ReduceAction) ReduceActionSet;
+
+static inline void ts_reduce_action_set_add(ReduceActionSet *self,
+ ReduceAction new_action) {
+ for (uint32_t i = 0; i < self->size; i++) {
+ ReduceAction action = self->contents[i];
+ if (action.symbol == new_action.symbol && action.count == new_action.count)
+ return;
+ }
+ array_push(self, new_action);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_REDUCE_ACTION_H_
diff --git a/src/tree_sitter/reusable_node.h b/src/tree_sitter/reusable_node.h
new file mode 100644
index 0000000000..9cba951909
--- /dev/null
+++ b/src/tree_sitter/reusable_node.h
@@ -0,0 +1,88 @@
+#include "./subtree.h"
+
+typedef struct {
+ Subtree tree;
+ uint32_t child_index;
+ uint32_t byte_offset;
+} StackEntry;
+
+typedef struct {
+ Array(StackEntry) stack;
+ Subtree last_external_token;
+} ReusableNode;
+
+static inline ReusableNode reusable_node_new(void) {
+ return (ReusableNode) {array_new(), NULL_SUBTREE};
+}
+
+static inline void reusable_node_clear(ReusableNode *self) {
+ array_clear(&self->stack);
+ self->last_external_token = NULL_SUBTREE;
+}
+
+static inline void reusable_node_reset(ReusableNode *self, Subtree tree) {
+ reusable_node_clear(self);
+ array_push(&self->stack, ((StackEntry) {
+ .tree = tree,
+ .child_index = 0,
+ .byte_offset = 0,
+ }));
+}
+
+static inline Subtree reusable_node_tree(ReusableNode *self) {
+ return self->stack.size > 0
+ ? self->stack.contents[self->stack.size - 1].tree
+ : NULL_SUBTREE;
+}
+
+static inline uint32_t reusable_node_byte_offset(ReusableNode *self) {
+ return self->stack.size > 0
+ ? self->stack.contents[self->stack.size - 1].byte_offset
+ : UINT32_MAX;
+}
+
+static inline void reusable_node_delete(ReusableNode *self) {
+ array_delete(&self->stack);
+}
+
+static inline void reusable_node_advance(ReusableNode *self) {
+ StackEntry last_entry = *array_back(&self->stack);
+ uint32_t byte_offset = last_entry.byte_offset + ts_subtree_total_bytes(last_entry.tree);
+ if (ts_subtree_has_external_tokens(last_entry.tree)) {
+ self->last_external_token = ts_subtree_last_external_token(last_entry.tree);
+ }
+
+ Subtree tree;
+ uint32_t next_index;
+ do {
+ StackEntry popped_entry = array_pop(&self->stack);
+ next_index = popped_entry.child_index + 1;
+ if (self->stack.size == 0) return;
+ tree = array_back(&self->stack)->tree;
+ } while (ts_subtree_child_count(tree) <= next_index);
+
+ array_push(&self->stack, ((StackEntry) {
+ .tree = tree.ptr->children[next_index],
+ .child_index = next_index,
+ .byte_offset = byte_offset,
+ }));
+}
+
+static inline bool reusable_node_descend(ReusableNode *self) {
+ StackEntry last_entry = *array_back(&self->stack);
+ if (ts_subtree_child_count(last_entry.tree) > 0) {
+ array_push(&self->stack, ((StackEntry) {
+ .tree = last_entry.tree.ptr->children[0],
+ .child_index = 0,
+ .byte_offset = last_entry.byte_offset,
+ }));
+ return true;
+ } else {
+ return false;
+ }
+}
+
+static inline void reusable_node_advance_past_leaf(ReusableNode *self) {
+ while (reusable_node_descend(self)) {}
+ reusable_node_advance(self);
+}
diff --git a/src/tree_sitter/stack.c b/src/tree_sitter/stack.c
new file mode 100644
index 0000000000..3e842c99c3
--- /dev/null
+++ b/src/tree_sitter/stack.c
@@ -0,0 +1,846 @@
+#include "./alloc.h"
+#include "./language.h"
+#include "./subtree.h"
+#include "./array.h"
+#include "./stack.h"
+#include "./length.h"
+#include <assert.h>
+#include <stdio.h>
+
+#define MAX_LINK_COUNT 8
+#define MAX_NODE_POOL_SIZE 50
+#define MAX_ITERATOR_COUNT 64
+
+#ifdef _WIN32
+#define inline __forceinline
+#else
+#define inline static inline __attribute__((always_inline))
+#endif
+
+typedef struct StackNode StackNode;
+
+typedef struct {
+ StackNode *node;
+ Subtree subtree;
+ bool is_pending;
+} StackLink;
+
+struct StackNode {
+ TSStateId state;
+ Length position;
+ StackLink links[MAX_LINK_COUNT];
+ short unsigned int link_count;
+ uint32_t ref_count;
+ unsigned error_cost;
+ unsigned node_count;
+ int dynamic_precedence;
+};
+
+typedef struct {
+ StackNode *node;
+ SubtreeArray subtrees;
+ uint32_t subtree_count;
+ bool is_pending;
+} StackIterator;
+
+typedef struct {
+ void *payload;
+ StackIterateCallback callback;
+} StackIterateSession;
+
+typedef Array(StackNode *) StackNodeArray;
+
+typedef enum {
+ StackStatusActive,
+ StackStatusPaused,
+ StackStatusHalted,
+} StackStatus;
+
+typedef struct {
+ StackNode *node;
+ Subtree last_external_token;
+ StackSummary *summary;
+ unsigned node_count_at_last_error;
+ TSSymbol lookahead_when_paused;
+ StackStatus status;
+} StackHead;
+
+struct Stack {
+ Array(StackHead) heads;
+ StackSliceArray slices;
+ Array(StackIterator) iterators;
+ StackNodeArray node_pool;
+ StackNode *base_node;
+ SubtreePool *subtree_pool;
+};
+
+typedef unsigned StackAction;
+enum {
+ StackActionNone,
+ StackActionStop = 1,
+ StackActionPop = 2,
+};
+
+typedef StackAction (*StackCallback)(void *, const StackIterator *);
+
+static void stack_node_retain(StackNode *self) {
+ if (!self)
+ return;
+ assert(self->ref_count > 0);
+ self->ref_count++;
+ assert(self->ref_count != 0);
+}
+
+static void stack_node_release(StackNode *self, StackNodeArray *pool, SubtreePool *subtree_pool) {
+recur:
+ assert(self->ref_count != 0);
+ self->ref_count--;
+ if (self->ref_count > 0) return;
+
+ StackNode *first_predecessor = NULL;
+ if (self->link_count > 0) {
+ for (unsigned i = self->link_count - 1; i > 0; i--) {
+ StackLink link = self->links[i];
+ if (link.subtree.ptr) ts_subtree_release(subtree_pool, link.subtree);
+ stack_node_release(link.node, pool, subtree_pool);
+ }
+ StackLink link = self->links[0];
+ if (link.subtree.ptr) ts_subtree_release(subtree_pool, link.subtree);
+ first_predecessor = self->links[0].node;
+ }
+
+ if (pool->size < MAX_NODE_POOL_SIZE) {
+ array_push(pool, self);
+ } else {
+ ts_free(self);
+ }
+
+ if (first_predecessor) {
+ self = first_predecessor;
+ goto recur;
+ }
+}
+
+static StackNode *stack_node_new(StackNode *previous_node, Subtree subtree,
+ bool is_pending, TSStateId state, StackNodeArray *pool) {
+ StackNode *node = pool->size > 0 ?
+ array_pop(pool) :
+ ts_malloc(sizeof(StackNode));
+ *node = (StackNode){.ref_count = 1, .link_count = 0, .state = state};
+
+ if (previous_node) {
+ node->link_count = 1;
+ node->links[0] = (StackLink){
+ .node = previous_node,
+ .subtree = subtree,
+ .is_pending = is_pending,
+ };
+
+ node->position = previous_node->position;
+ node->error_cost = previous_node->error_cost;
+ node->dynamic_precedence = previous_node->dynamic_precedence;
+ node->node_count = previous_node->node_count;
+
+ if (subtree.ptr) {
+ node->error_cost += ts_subtree_error_cost(subtree);
+ node->position = length_add(node->position, ts_subtree_total_size(subtree));
+ node->node_count += ts_subtree_node_count(subtree);
+ node->dynamic_precedence += ts_subtree_dynamic_precedence(subtree);
+ }
+ } else {
+ node->position = length_zero();
+ node->error_cost = 0;
+ }
+
+ return node;
+}
+
+static bool stack__subtree_is_equivalent(Subtree left, Subtree right) {
+ return
+ left.ptr == right.ptr ||
+ (left.ptr && right.ptr &&
+ ts_subtree_symbol(left) == ts_subtree_symbol(right) &&
+ ((ts_subtree_error_cost(left) > 0 && ts_subtree_error_cost(right) > 0) ||
+ (ts_subtree_padding(left).bytes == ts_subtree_padding(right).bytes &&
+ ts_subtree_size(left).bytes == ts_subtree_size(right).bytes &&
+ ts_subtree_child_count(left) == ts_subtree_child_count(right) &&
+ ts_subtree_extra(left) == ts_subtree_extra(right) &&
+ ts_subtree_external_scanner_state_eq(left, right))));
+}
+
+static void stack_node_add_link(StackNode *self, StackLink link, SubtreePool *subtree_pool) {
+ if (link.node == self) return;
+
+ for (int i = 0; i < self->link_count; i++) {
+ StackLink *existing_link = &self->links[i];
+ if (stack__subtree_is_equivalent(existing_link->subtree, link.subtree)) {
+ // In general, we preserve ambiguities until they are removed from the stack
+ // during a pop operation where multiple paths lead to the same node. But in
+ // the special case where two links directly connect the same pair of nodes,
+ // we can safely remove the ambiguity ahead of time without changing behavior.
+ if (existing_link->node == link.node) {
+ if (
+ ts_subtree_dynamic_precedence(link.subtree) >
+ ts_subtree_dynamic_precedence(existing_link->subtree)
+ ) {
+ ts_subtree_retain(link.subtree);
+ ts_subtree_release(subtree_pool, existing_link->subtree);
+ existing_link->subtree = link.subtree;
+ self->dynamic_precedence =
+ link.node->dynamic_precedence + ts_subtree_dynamic_precedence(link.subtree);
+ }
+ return;
+ }
+
+ // If the previous nodes are mergeable, merge them recursively.
+ if (existing_link->node->state == link.node->state &&
+ existing_link->node->position.bytes == link.node->position.bytes) {
+ for (int j = 0; j < link.node->link_count; j++) {
+ stack_node_add_link(existing_link->node, link.node->links[j], subtree_pool);
+ }
+ int32_t dynamic_precedence = link.node->dynamic_precedence;
+ if (link.subtree.ptr) {
+ dynamic_precedence += ts_subtree_dynamic_precedence(link.subtree);
+ }
+ if (dynamic_precedence > self->dynamic_precedence) {
+ self->dynamic_precedence = dynamic_precedence;
+ }
+ return;
+ }
+ }
+ }
+
+ if (self->link_count == MAX_LINK_COUNT) return;
+
+ stack_node_retain(link.node);
+ unsigned node_count = link.node->node_count;
+ int dynamic_precedence = link.node->dynamic_precedence;
+ self->links[self->link_count++] = link;
+
+ if (link.subtree.ptr) {
+ ts_subtree_retain(link.subtree);
+ node_count += ts_subtree_node_count(link.subtree);
+ dynamic_precedence += ts_subtree_dynamic_precedence(link.subtree);
+ }
+
+ if (node_count > self->node_count) self->node_count = node_count;
+ if (dynamic_precedence > self->dynamic_precedence) self->dynamic_precedence = dynamic_precedence;
+}
+
+static void stack_head_delete(StackHead *self, StackNodeArray *pool, SubtreePool *subtree_pool) {
+ if (self->node) {
+ if (self->last_external_token.ptr) {
+ ts_subtree_release(subtree_pool, self->last_external_token);
+ }
+ if (self->summary) {
+ array_delete(self->summary);
+ ts_free(self->summary);
+ }
+ stack_node_release(self->node, pool, subtree_pool);
+ }
+}
+
+static StackVersion ts_stack__add_version(Stack *self, StackVersion original_version,
+ StackNode *node) {
+ StackHead head = {
+ .node = node,
+ .node_count_at_last_error = self->heads.contents[original_version].node_count_at_last_error,
+ .last_external_token = self->heads.contents[original_version].last_external_token,
+ .status = StackStatusActive,
+ .lookahead_when_paused = 0,
+ };
+ array_push(&self->heads, head);
+ stack_node_retain(node);
+ if (head.last_external_token.ptr) ts_subtree_retain(head.last_external_token);
+ return (StackVersion)(self->heads.size - 1);
+}
+
+static void ts_stack__add_slice(Stack *self, StackVersion original_version,
+ StackNode *node, SubtreeArray *subtrees) {
+ for (uint32_t i = self->slices.size - 1; i + 1 > 0; i--) {
+ StackVersion version = self->slices.contents[i].version;
+ if (self->heads.contents[version].node == node) {
+ StackSlice slice = {*subtrees, version};
+ array_insert(&self->slices, i + 1, slice);
+ return;
+ }
+ }
+
+ StackVersion version = ts_stack__add_version(self, original_version, node);
+ StackSlice slice = { *subtrees, version };
+ array_push(&self->slices, slice);
+}
+
+inline StackSliceArray stack__iter(Stack *self, StackVersion version,
+ StackCallback callback, void *payload,
+ int goal_subtree_count) {
+ array_clear(&self->slices);
+ array_clear(&self->iterators);
+
+ StackHead *head = array_get(&self->heads, version);
+ StackIterator iterator = {
+ .node = head->node,
+ .subtrees = array_new(),
+ .subtree_count = 0,
+ .is_pending = true,
+ };
+
+ bool include_subtrees = false;
+ if (goal_subtree_count >= 0) {
+ include_subtrees = true;
+ array_reserve(&iterator.subtrees, goal_subtree_count);
+ }
+
+ array_push(&self->iterators, iterator);
+
+ while (self->iterators.size > 0) {
+ for (uint32_t i = 0, size = self->iterators.size; i < size; i++) {
+ StackIterator *iterator = &self->iterators.contents[i];
+ StackNode *node = iterator->node;
+
+ StackAction action = callback(payload, iterator);
+ bool should_pop = action & StackActionPop;
+ bool should_stop = action & StackActionStop || node->link_count == 0;
+
+ if (should_pop) {
+ SubtreeArray subtrees = iterator->subtrees;
+ if (!should_stop)
+ ts_subtree_array_copy(subtrees, &subtrees);
+ ts_subtree_array_reverse(&subtrees);
+ ts_stack__add_slice(
+ self,
+ version,
+ node,
+ &subtrees
+ );
+ }
+
+ if (should_stop) {
+ if (!should_pop)
+ ts_subtree_array_delete(self->subtree_pool, &iterator->subtrees);
+ array_erase(&self->iterators, i);
+ i--, size--;
+ continue;
+ }
+
+ for (uint32_t j = 1; j <= node->link_count; j++) {
+ StackIterator *next_iterator;
+ StackLink link;
+ if (j == node->link_count) {
+ link = node->links[0];
+ next_iterator = &self->iterators.contents[i];
+ } else {
+ if (self->iterators.size >= MAX_ITERATOR_COUNT) continue;
+ link = node->links[j];
+ StackIterator current_iterator = self->iterators.contents[i];
+ array_push(&self->iterators, current_iterator);
+ next_iterator = array_back(&self->iterators);
+ ts_subtree_array_copy(next_iterator->subtrees, &next_iterator->subtrees);
+ }
+
+ next_iterator->node = link.node;
+ if (link.subtree.ptr) {
+ if (include_subtrees) {
+ array_push(&next_iterator->subtrees, link.subtree);
+ ts_subtree_retain(link.subtree);
+ }
+
+ if (!ts_subtree_extra(link.subtree)) {
+ next_iterator->subtree_count++;
+ if (!link.is_pending) {
+ next_iterator->is_pending = false;
+ }
+ }
+ } else {
+ next_iterator->subtree_count++;
+ next_iterator->is_pending = false;
+ }
+ }
+ }
+ }
+
+ return self->slices;
+}
+
+Stack *ts_stack_new(SubtreePool *subtree_pool) {
+ Stack *self = ts_calloc(1, sizeof(Stack));
+
+ array_init(&self->heads);
+ array_init(&self->slices);
+ array_init(&self->iterators);
+ array_init(&self->node_pool);
+ array_reserve(&self->heads, 4);
+ array_reserve(&self->slices, 4);
+ array_reserve(&self->iterators, 4);
+ array_reserve(&self->node_pool, MAX_NODE_POOL_SIZE);
+
+ self->subtree_pool = subtree_pool;
+ self->base_node = stack_node_new(NULL, NULL_SUBTREE, false, 1, &self->node_pool);
+ ts_stack_clear(self);
+
+ return self;
+}
+
+void ts_stack_delete(Stack *self) {
+ if (self->slices.contents)
+ array_delete(&self->slices);
+ if (self->iterators.contents)
+ array_delete(&self->iterators);
+ stack_node_release(self->base_node, &self->node_pool, self->subtree_pool);
+ for (uint32_t i = 0; i < self->heads.size; i++) {
+ stack_head_delete(&self->heads.contents[i], &self->node_pool, self->subtree_pool);
+ }
+ array_clear(&self->heads);
+ if (self->node_pool.contents) {
+ for (uint32_t i = 0; i < self->node_pool.size; i++)
+ ts_free(self->node_pool.contents[i]);
+ array_delete(&self->node_pool);
+ }
+ array_delete(&self->heads);
+ ts_free(self);
+}
+
+uint32_t ts_stack_version_count(const Stack *self) {
+ return self->heads.size;
+}
+
+TSStateId ts_stack_state(const Stack *self, StackVersion version) {
+ return array_get(&self->heads, version)->node->state;
+}
+
+Length ts_stack_position(const Stack *self, StackVersion version) {
+ return array_get(&self->heads, version)->node->position;
+}
+
+Subtree ts_stack_last_external_token(const Stack *self, StackVersion version) {
+ return array_get(&self->heads, version)->last_external_token;
+}
+
+void ts_stack_set_last_external_token(Stack *self, StackVersion version, Subtree token) {
+ StackHead *head = array_get(&self->heads, version);
+ if (token.ptr) ts_subtree_retain(token);
+ if (head->last_external_token.ptr) ts_subtree_release(self->subtree_pool, head->last_external_token);
+ head->last_external_token = token;
+}
+
+unsigned ts_stack_error_cost(const Stack *self, StackVersion version) {
+ StackHead *head = array_get(&self->heads, version);
+ unsigned result = head->node->error_cost;
+ if (
+ head->status == StackStatusPaused ||
+ (head->node->state == ERROR_STATE && !head->node->links[0].subtree.ptr)) {
+ result += ERROR_COST_PER_RECOVERY;
+ }
+ return result;
+}
+
+unsigned ts_stack_node_count_since_error(const Stack *self, StackVersion version) {
+ StackHead *head = array_get(&self->heads, version);
+ if (head->node->node_count < head->node_count_at_last_error) {
+ head->node_count_at_last_error = head->node->node_count;
+ }
+ return head->node->node_count - head->node_count_at_last_error;
+}
+
+void ts_stack_push(Stack *self, StackVersion version, Subtree subtree,
+ bool pending, TSStateId state) {
+ StackHead *head = array_get(&self->heads, version);
+ StackNode *new_node = stack_node_new(head->node, subtree, pending, state, &self->node_pool);
+ if (!subtree.ptr) head->node_count_at_last_error = new_node->node_count;
+ head->node = new_node;
+}
+
+inline StackAction iterate_callback(void *payload, const StackIterator *iterator) {
+ StackIterateSession *session = payload;
+ session->callback(
+ session->payload,
+ iterator->node->state,
+ iterator->subtree_count
+ );
+ return StackActionNone;
+}
+
+void ts_stack_iterate(Stack *self, StackVersion version,
+ StackIterateCallback callback, void *payload) {
+ StackIterateSession session = {payload, callback};
+ stack__iter(self, version, iterate_callback, &session, -1);
+}
+
+inline StackAction pop_count_callback(void *payload, const StackIterator *iterator) {
+ unsigned *goal_subtree_count = payload;
+ if (iterator->subtree_count == *goal_subtree_count) {
+ return StackActionPop | StackActionStop;
+ } else {
+ return StackActionNone;
+ }
+}
+
+StackSliceArray ts_stack_pop_count(Stack *self, StackVersion version, uint32_t count) {
+ return stack__iter(self, version, pop_count_callback, &count, count);
+}
+
+inline StackAction pop_pending_callback(void *payload, const StackIterator *iterator) {
+ if (iterator->subtree_count >= 1) {
+ if (iterator->is_pending) {
+ return StackActionPop | StackActionStop;
+ } else {
+ return StackActionStop;
+ }
+ } else {
+ return StackActionNone;
+ }
+}
+
+StackSliceArray ts_stack_pop_pending(Stack *self, StackVersion version) {
+ StackSliceArray pop = stack__iter(self, version, pop_pending_callback, NULL, 0);
+ if (pop.size > 0) {
+ ts_stack_renumber_version(self, pop.contents[0].version, version);
+ pop.contents[0].version = version;
+ }
+ return pop;
+}
+
+inline StackAction pop_error_callback(void *payload, const StackIterator *iterator) {
+ if (iterator->subtrees.size > 0) {
+ bool *found_error = payload;
+ if (!*found_error && ts_subtree_is_error(iterator->subtrees.contents[0])) {
+ *found_error = true;
+ return StackActionPop | StackActionStop;
+ } else {
+ return StackActionStop;
+ }
+ } else {
+ return StackActionNone;
+ }
+}
+
+SubtreeArray ts_stack_pop_error(Stack *self, StackVersion version) {
+ StackNode *node = array_get(&self->heads, version)->node;
+ for (unsigned i = 0; i < node->link_count; i++) {
+ if (node->links[i].subtree.ptr && ts_subtree_is_error(node->links[i].subtree)) {
+ bool found_error = false;
+ StackSliceArray pop = stack__iter(self, version, pop_error_callback, &found_error, 1);
+ if (pop.size > 0) {
+ assert(pop.size == 1);
+ ts_stack_renumber_version(self, pop.contents[0].version, version);
+ return pop.contents[0].subtrees;
+ }
+ break;
+ }
+ }
+ return (SubtreeArray){.size = 0};
+}
+
+inline StackAction pop_all_callback(void *payload, const StackIterator *iterator) {
+ return iterator->node->link_count == 0 ? StackActionPop : StackActionNone;
+}
+
+StackSliceArray ts_stack_pop_all(Stack *self, StackVersion version) {
+ return stack__iter(self, version, pop_all_callback, NULL, 0);
+}
+
+typedef struct {
+ StackSummary *summary;
+ unsigned max_depth;
+} SummarizeStackSession;
+
+inline StackAction summarize_stack_callback(void *payload, const StackIterator *iterator) {
+ SummarizeStackSession *session = payload;
+ TSStateId state = iterator->node->state;
+ unsigned depth = iterator->subtree_count;
+ if (depth > session->max_depth) return StackActionStop;
+ for (unsigned i = session->summary->size - 1; i + 1 > 0; i--) {
+ StackSummaryEntry entry = session->summary->contents[i];
+ if (entry.depth < depth) break;
+ if (entry.depth == depth && entry.state == state) return StackActionNone;
+ }
+ array_push(session->summary, ((StackSummaryEntry){
+ .position = iterator->node->position,
+ .depth = depth,
+ .state = state,
+ }));
+ return StackActionNone;
+}
+
+void ts_stack_record_summary(Stack *self, StackVersion version, unsigned max_depth) {
+ SummarizeStackSession session = {
+ .summary = ts_malloc(sizeof(StackSummary)),
+ .max_depth = max_depth
+ };
+ array_init(session.summary);
+ stack__iter(self, version, summarize_stack_callback, &session, -1);
+ self->heads.contents[version].summary = session.summary;
+}
+
+StackSummary *ts_stack_get_summary(Stack *self, StackVersion version) {
+ return array_get(&self->heads, version)->summary;
+}
+
+int ts_stack_dynamic_precedence(Stack *self, StackVersion version) {
+ return array_get(&self->heads, version)->node->dynamic_precedence;
+}
+
+bool ts_stack_has_advanced_since_error(const Stack *self, StackVersion version) {
+ const StackHead *head = array_get(&self->heads, version);
+ const StackNode *node = head->node;
+ if (node->error_cost == 0) return true;
+ while (node) {
+ if (node->link_count > 0) {
+ Subtree subtree = node->links[0].subtree;
+ if (subtree.ptr) {
+ if (ts_subtree_total_bytes(subtree) > 0) {
+ return true;
+ } else if (
+ node->node_count > head->node_count_at_last_error &&
+ ts_subtree_error_cost(subtree) == 0
+ ) {
+ node = node->links[0].node;
+ continue;
+ }
+ }
+ }
+ break;
+ }
+ return false;
+}
+
+void ts_stack_remove_version(Stack *self, StackVersion version) {
+ stack_head_delete(array_get(&self->heads, version), &self->node_pool, self->subtree_pool);
+ array_erase(&self->heads, version);
+}
+
+void ts_stack_renumber_version(Stack *self, StackVersion v1, StackVersion v2) {
+ if (v1 == v2) return;
+ assert(v2 < v1);
+ assert((uint32_t)v1 < self->heads.size);
+ StackHead *source_head = &self->heads.contents[v1];
+ StackHead *target_head = &self->heads.contents[v2];
+ if (target_head->summary && !source_head->summary) {
+ source_head->summary = target_head->summary;
+ target_head->summary = NULL;
+ }
+ stack_head_delete(target_head, &self->node_pool, self->subtree_pool);
+ *target_head = *source_head;
+ array_erase(&self->heads, v1);
+}
+
+void ts_stack_swap_versions(Stack *self, StackVersion v1, StackVersion v2) {
+ StackHead temporary_head = self->heads.contents[v1];
+ self->heads.contents[v1] = self->heads.contents[v2];
+ self->heads.contents[v2] = temporary_head;
+}
+
+StackVersion ts_stack_copy_version(Stack *self, StackVersion version) {
+ assert(version < self->heads.size);
+ array_push(&self->heads, self->heads.contents[version]);
+ StackHead *head = array_back(&self->heads);
+ stack_node_retain(head->node);
+ if (head->last_external_token.ptr) ts_subtree_retain(head->last_external_token);
+ head->summary = NULL;
+ return self->heads.size - 1;
+}
+
+bool ts_stack_merge(Stack *self, StackVersion version1, StackVersion version2) {
+ if (!ts_stack_can_merge(self, version1, version2)) return false;
+ StackHead *head1 = &self->heads.contents[version1];
+ StackHead *head2 = &self->heads.contents[version2];
+ for (uint32_t i = 0; i < head2->node->link_count; i++) {
+ stack_node_add_link(head1->node, head2->node->links[i], self->subtree_pool);
+ }
+ if (head1->node->state == ERROR_STATE) {
+ head1->node_count_at_last_error = head1->node->node_count;
+ }
+ ts_stack_remove_version(self, version2);
+ return true;
+}
+
+bool ts_stack_can_merge(Stack *self, StackVersion version1, StackVersion version2) {
+ StackHead *head1 = &self->heads.contents[version1];
+ StackHead *head2 = &self->heads.contents[version2];
+ return
+ head1->status == StackStatusActive &&
+ head2->status == StackStatusActive &&
+ head1->node->state == head2->node->state &&
+ head1->node->position.bytes == head2->node->position.bytes &&
+ head1->node->error_cost == head2->node->error_cost &&
+ ts_subtree_external_scanner_state_eq(head1->last_external_token, head2->last_external_token);
+}
+
+void ts_stack_halt(Stack *self, StackVersion version) {
+ array_get(&self->heads, version)->status = StackStatusHalted;
+}
+
+void ts_stack_pause(Stack *self, StackVersion version, TSSymbol lookahead) {
+ StackHead *head = array_get(&self->heads, version);
+ head->status = StackStatusPaused;
+ head->lookahead_when_paused = lookahead;
+ head->node_count_at_last_error = head->node->node_count;
+}
+
+bool ts_stack_is_active(const Stack *self, StackVersion version) {
+ return array_get(&self->heads, version)->status == StackStatusActive;
+}
+
+bool ts_stack_is_halted(const Stack *self, StackVersion version) {
+ return array_get(&self->heads, version)->status == StackStatusHalted;
+}
+
+bool ts_stack_is_paused(const Stack *self, StackVersion version) {
+ return array_get(&self->heads, version)->status == StackStatusPaused;
+}
+
+TSSymbol ts_stack_resume(Stack *self, StackVersion version) {
+ StackHead *head = array_get(&self->heads, version);
+ assert(head->status == StackStatusPaused);
+ TSSymbol result = head->lookahead_when_paused;
+ head->status = StackStatusActive;
+ head->lookahead_when_paused = 0;
+ return result;
+}
+
+void ts_stack_clear(Stack *self) {
+ stack_node_retain(self->base_node);
+ for (uint32_t i = 0; i < self->heads.size; i++) {
+ stack_head_delete(&self->heads.contents[i], &self->node_pool, self->subtree_pool);
+ }
+ array_clear(&self->heads);
+ array_push(&self->heads, ((StackHead){
+ .node = self->base_node,
+ .last_external_token = NULL_SUBTREE,
+ .status = StackStatusActive,
+ .lookahead_when_paused = 0,
+ }));
+}
+
+bool ts_stack_print_dot_graph(Stack *self, const TSLanguage *language, FILE *f) {
+ array_reserve(&self->iterators, 32);
+ bool was_recording_allocations = ts_toggle_allocation_recording(false);
+ if (!f) f = stderr;
+
+ fprintf(f, "digraph stack {\n");
+ fprintf(f, "rankdir=\"RL\";\n");
+ fprintf(f, "edge [arrowhead=none]\n");
+
+ Array(StackNode *) visited_nodes = array_new();
+
+ array_clear(&self->iterators);
+ for (uint32_t i = 0; i < self->heads.size; i++) {
+ StackHead *head = &self->heads.contents[i];
+ if (head->status == StackStatusHalted) continue;
+
+ fprintf(f, "node_head_%u [shape=none, label=\"\"]\n", i);
+ fprintf(f, "node_head_%u -> node_%p [", i, head->node);
+
+ if (head->status == StackStatusPaused) {
+ fprintf(f, "color=red ");
+ }
+ fprintf(f,
+ "label=%u, fontcolor=blue, weight=10000, labeltooltip=\"node_count: %u\nerror_cost: %u",
+ i,
+ ts_stack_node_count_since_error(self, i),
+ ts_stack_error_cost(self, i)
+ );
+
+ if (head->last_external_token.ptr) {
+ const ExternalScannerState *state = &head->last_external_token.ptr->external_scanner_state;
+ const char *data = ts_external_scanner_state_data(state);
+ fprintf(f, "\nexternal_scanner_state:");
+ for (uint32_t j = 0; j < state->length; j++) fprintf(f, " %2X", data[j]);
+ }
+
+ fprintf(f, "\"]\n");
+ array_push(&self->iterators, ((StackIterator){.node = head->node }));
+ }
+
+ bool all_iterators_done = false;
+ while (!all_iterators_done) {
+ all_iterators_done = true;
+
+ for (uint32_t i = 0; i < self->iterators.size; i++) {
+ StackIterator iterator = self->iterators.contents[i];
+ StackNode *node = iterator.node;
+
+ for (uint32_t j = 0; j < visited_nodes.size; j++) {
+ if (visited_nodes.contents[j] == node) {
+ node = NULL;
+ break;
+ }
+ }
+
+ if (!node) continue;
+ all_iterators_done = false;
+
+ fprintf(f, "node_%p [", node);
+ if (node->state == ERROR_STATE) {
+ fprintf(f, "label=\"?\"");
+ } else if (
+ node->link_count == 1 &&
+ node->links[0].subtree.ptr &&
+ ts_subtree_extra(node->links[0].subtree)
+ ) {
+ fprintf(f, "shape=point margin=0 label=\"\"");
+ } else {
+ fprintf(f, "label=\"%d\"", node->state);
+ }
+
+ fprintf(
+ f,
+ " tooltip=\"position: %u,%u\nnode_count:%u\nerror_cost: %u\ndynamic_precedence: %d\"];\n",
+ node->position.extent.row + 1,
+ node->position.extent.column,
+ node->node_count,
+ node->error_cost,
+ node->dynamic_precedence
+ );
+
+ for (int j = 0; j < node->link_count; j++) {
+ StackLink link = node->links[j];
+ fprintf(f, "node_%p -> node_%p [", node, link.node);
+ if (link.is_pending) fprintf(f, "style=dashed ");
+ if (link.subtree.ptr && ts_subtree_extra(link.subtree)) fprintf(f, "fontcolor=gray ");
+
+ if (!link.subtree.ptr) {
+ fprintf(f, "color=red");
+ } else {
+ fprintf(f, "label=\"");
+ bool quoted = ts_subtree_visible(link.subtree) && !ts_subtree_named(link.subtree);
+ if (quoted) fprintf(f, "'");
+ const char *name = ts_language_symbol_name(language, ts_subtree_symbol(link.subtree));
+ for (const char *c = name; *c; c++) {
+ if (*c == '\"' || *c == '\\') fprintf(f, "\\");
+ fprintf(f, "%c", *c);
+ }
+ if (quoted) fprintf(f, "'");
+ fprintf(f, "\"");
+ fprintf(
+ f,
+ "labeltooltip=\"error_cost: %u\ndynamic_precedence: %u\"",
+ ts_subtree_error_cost(link.subtree),
+ ts_subtree_dynamic_precedence(link.subtree)
+ );
+ }
+
+ fprintf(f, "];\n");
+
+ StackIterator *next_iterator;
+ if (j == 0) {
+ next_iterator = &self->iterators.contents[i];
+ } else {
+ array_push(&self->iterators, iterator);
+ next_iterator = array_back(&self->iterators);
+ }
+ next_iterator->node = link.node;
+ }
+
+ array_push(&visited_nodes, node);
+ }
+ }
+
+ fprintf(f, "}\n");
+
+ array_delete(&visited_nodes);
+ ts_toggle_allocation_recording(was_recording_allocations);
+ return true;
+}
+
+#undef inline
diff --git a/src/tree_sitter/stack.h b/src/tree_sitter/stack.h
new file mode 100644
index 0000000000..ec7a69d2b4
--- /dev/null
+++ b/src/tree_sitter/stack.h
@@ -0,0 +1,135 @@
+#ifndef TREE_SITTER_PARSE_STACK_H_
+#define TREE_SITTER_PARSE_STACK_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "./array.h"
+#include "./subtree.h"
+#include "./error_costs.h"
+#include <stdio.h>
+
+typedef struct Stack Stack;
+
+typedef unsigned StackVersion;
+#define STACK_VERSION_NONE ((StackVersion)-1)
+
+typedef struct {
+ SubtreeArray subtrees;
+ StackVersion version;
+} StackSlice;
+typedef Array(StackSlice) StackSliceArray;
+
+typedef struct {
+ Length position;
+ unsigned depth;
+ TSStateId state;
+} StackSummaryEntry;
+typedef Array(StackSummaryEntry) StackSummary;
+
+// Create a stack.
+Stack *ts_stack_new(SubtreePool *);
+
+// Release the memory reserved for a given stack.
+void ts_stack_delete(Stack *);
+
+// Get the stack's current number of versions.
+uint32_t ts_stack_version_count(const Stack *);
+
+// Get the state at the top of the given version of the stack. If the stack is
+// empty, this returns the initial state, 0.
+TSStateId ts_stack_state(const Stack *, StackVersion);
+
+// Get the last external token associated with a given version of the stack.
+Subtree ts_stack_last_external_token(const Stack *, StackVersion);
+
+// Set the last external token associated with a given version of the stack.
+void ts_stack_set_last_external_token(Stack *, StackVersion, Subtree );
+
+// Get the position of the given version of the stack within the document.
+Length ts_stack_position(const Stack *, StackVersion);
+
+// Push a tree and state onto the given version of the stack.
+//
+// This transfers ownership of the tree to the Stack. Callers that
+// need to retain ownership of the tree for their own purposes should
+// first retain the tree.
+void ts_stack_push(Stack *, StackVersion, Subtree , bool, TSStateId);
+
+// Pop the given number of entries from the given version of the stack. This
+// operation can increase the number of stack versions by revealing multiple
+// versions which had previously been merged. It returns an array that
+// specifies the index of each revealed version and the trees that were
+// removed from that version.
+StackSliceArray ts_stack_pop_count(Stack *, StackVersion, uint32_t count);
+
+// Remove an error at the top of the given version of the stack.
+SubtreeArray ts_stack_pop_error(Stack *, StackVersion);
+
+// Remove any pending trees from the top of the given version of the stack.
+StackSliceArray ts_stack_pop_pending(Stack *, StackVersion);
+
+// Remove any all trees from the given version of the stack.
+StackSliceArray ts_stack_pop_all(Stack *, StackVersion);
+
+// Get the maximum number of tree nodes reachable from this version of the stack
+// since the last error was detected.
+unsigned ts_stack_node_count_since_error(const Stack *, StackVersion);
+
+int ts_stack_dynamic_precedence(Stack *, StackVersion);
+
+bool ts_stack_has_advanced_since_error(const Stack *, StackVersion);
+
+// Compute a summary of all the parse states near the top of the given
+// version of the stack and store the summary for later retrieval.
+void ts_stack_record_summary(Stack *, StackVersion, unsigned max_depth);
+
+// Retrieve a summary of all the parse states near the top of the
+// given version of the stack.
+StackSummary *ts_stack_get_summary(Stack *, StackVersion);
+
+// Get the total cost of all errors on the given version of the stack.
+unsigned ts_stack_error_cost(const Stack *, StackVersion version);
+
+// Merge the given two stack versions if possible, returning true
+// if they were successfully merged and false otherwise.
+bool ts_stack_merge(Stack *, StackVersion, StackVersion);
+
+// Determine whether the given two stack versions can be merged.
+bool ts_stack_can_merge(Stack *, StackVersion, StackVersion);
+
+TSSymbol ts_stack_resume(Stack *, StackVersion);
+
+void ts_stack_pause(Stack *, StackVersion, TSSymbol);
+
+void ts_stack_halt(Stack *, StackVersion);
+
+bool ts_stack_is_active(const Stack *, StackVersion);
+
+bool ts_stack_is_paused(const Stack *, StackVersion);
+
+bool ts_stack_is_halted(const Stack *, StackVersion);
+
+void ts_stack_renumber_version(Stack *, StackVersion, StackVersion);
+
+void ts_stack_swap_versions(Stack *, StackVersion, StackVersion);
+
+StackVersion ts_stack_copy_version(Stack *, StackVersion);
+
+// Remove the given version from the stack.
+void ts_stack_remove_version(Stack *, StackVersion);
+
+void ts_stack_clear(Stack *);
+
+bool ts_stack_print_dot_graph(Stack *, const TSLanguage *, FILE *);
+
+typedef void (*StackIterateCallback)(void *, TSStateId, uint32_t);
+
+void ts_stack_iterate(Stack *, StackVersion, StackIterateCallback, void *);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_PARSE_STACK_H_
diff --git a/src/tree_sitter/subtree.c b/src/tree_sitter/subtree.c
new file mode 100644
index 0000000000..e95733eb46
--- /dev/null
+++ b/src/tree_sitter/subtree.c
@@ -0,0 +1,996 @@
+#include <assert.h>
+#include <ctype.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdio.h>
+#include "./alloc.h"
+#include "./atomic.h"
+#include "./subtree.h"
+#include "./length.h"
+#include "./language.h"
+#include "./error_costs.h"
+#include <stddef.h>
+
+typedef struct {
+ Length start;
+ Length old_end;
+ Length new_end;
+} Edit;
+
+#ifdef TREE_SITTER_TEST
+
+#define TS_MAX_INLINE_TREE_LENGTH 2
+#define TS_MAX_TREE_POOL_SIZE 0
+
+#else
+
+#define TS_MAX_INLINE_TREE_LENGTH UINT8_MAX
+#define TS_MAX_TREE_POOL_SIZE 32
+
+#endif
+
+static const ExternalScannerState empty_state = {.length = 0, .short_data = {0}};
+
+// ExternalScannerState
+
+void ts_external_scanner_state_init(ExternalScannerState *self, const char *data, unsigned length) {
+ self->length = length;
+ if (length > sizeof(self->short_data)) {
+ self->long_data = ts_malloc(length);
+ memcpy(self->long_data, data, length);
+ } else {
+ memcpy(self->short_data, data, length);
+ }
+}
+
+ExternalScannerState ts_external_scanner_state_copy(const ExternalScannerState *self) {
+ ExternalScannerState result = *self;
+ if (self->length > sizeof(self->short_data)) {
+ result.long_data = ts_malloc(self->length);
+ memcpy(result.long_data, self->long_data, self->length);
+ }
+ return result;
+}
+
+void ts_external_scanner_state_delete(ExternalScannerState *self) {
+ if (self->length > sizeof(self->short_data)) {
+ ts_free(self->long_data);
+ }
+}
+
+const char *ts_external_scanner_state_data(const ExternalScannerState *self) {
+ if (self->length > sizeof(self->short_data)) {
+ return self->long_data;
+ } else {
+ return self->short_data;
+ }
+}
+
+bool ts_external_scanner_state_eq(const ExternalScannerState *a, const ExternalScannerState *b) {
+ return a == b || (
+ a->length == b->length &&
+ !memcmp(ts_external_scanner_state_data(a), ts_external_scanner_state_data(b), a->length)
+ );
+}
+
+// SubtreeArray
+
+void ts_subtree_array_copy(SubtreeArray self, SubtreeArray *dest) {
+ dest->size = self.size;
+ dest->capacity = self.capacity;
+ dest->contents = self.contents;
+ if (self.capacity > 0) {
+ dest->contents = ts_calloc(self.capacity, sizeof(Subtree));
+ memcpy(dest->contents, self.contents, self.size * sizeof(Subtree));
+ for (uint32_t i = 0; i < self.size; i++) {
+ ts_subtree_retain(dest->contents[i]);
+ }
+ }
+}
+
+void ts_subtree_array_delete(SubtreePool *pool, SubtreeArray *self) {
+ for (uint32_t i = 0; i < self->size; i++) {
+ ts_subtree_release(pool, self->contents[i]);
+ }
+ array_delete(self);
+}
+
+SubtreeArray ts_subtree_array_remove_trailing_extras(SubtreeArray *self) {
+ SubtreeArray result = array_new();
+
+ uint32_t i = self->size - 1;
+ for (; i + 1 > 0; i--) {
+ Subtree child = self->contents[i];
+ if (!ts_subtree_extra(child)) break;
+ array_push(&result, child);
+ }
+
+ self->size = i + 1;
+ ts_subtree_array_reverse(&result);
+ return result;
+}
+
+void ts_subtree_array_reverse(SubtreeArray *self) {
+ for (uint32_t i = 0, limit = self->size / 2; i < limit; i++) {
+ size_t reverse_index = self->size - 1 - i;
+ Subtree swap = self->contents[i];
+ self->contents[i] = self->contents[reverse_index];
+ self->contents[reverse_index] = swap;
+ }
+}
+
+// SubtreePool
+
+SubtreePool ts_subtree_pool_new(uint32_t capacity) {
+ SubtreePool self = {array_new(), array_new()};
+ array_reserve(&self.free_trees, capacity);
+ return self;
+}
+
+void ts_subtree_pool_delete(SubtreePool *self) {
+ if (self->free_trees.contents) {
+ for (unsigned i = 0; i < self->free_trees.size; i++) {
+ ts_free(self->free_trees.contents[i].ptr);
+ }
+ array_delete(&self->free_trees);
+ }
+ if (self->tree_stack.contents) array_delete(&self->tree_stack);
+}
+
+static SubtreeHeapData *ts_subtree_pool_allocate(SubtreePool *self) {
+ if (self->free_trees.size > 0) {
+ return array_pop(&self->free_trees).ptr;
+ } else {
+ return ts_malloc(sizeof(SubtreeHeapData));
+ }
+}
+
+static void ts_subtree_pool_free(SubtreePool *self, SubtreeHeapData *tree) {
+ if (self->free_trees.capacity > 0 && self->free_trees.size + 1 <= TS_MAX_TREE_POOL_SIZE) {
+ array_push(&self->free_trees, (MutableSubtree) {.ptr = tree});
+ } else {
+ ts_free(tree);
+ }
+}
+
+// Subtree
+
+static inline bool ts_subtree_can_inline(Length padding, Length size, uint32_t lookahead_bytes) {
+ return
+ padding.bytes < TS_MAX_INLINE_TREE_LENGTH &&
+ padding.extent.row < 16 &&
+ padding.extent.column < TS_MAX_INLINE_TREE_LENGTH &&
+ size.extent.row == 0 &&
+ size.extent.column < TS_MAX_INLINE_TREE_LENGTH &&
+ lookahead_bytes < 16;
+}
+
+Subtree ts_subtree_new_leaf(
+ SubtreePool *pool, TSSymbol symbol, Length padding, Length size,
+ uint32_t lookahead_bytes, TSStateId parse_state, bool has_external_tokens,
+ bool is_keyword, const TSLanguage *language
+) {
+ TSSymbolMetadata metadata = ts_language_symbol_metadata(language, symbol);
+ bool extra = symbol == ts_builtin_sym_end;
+
+ bool is_inline = (
+ symbol <= UINT8_MAX &&
+ !has_external_tokens &&
+ ts_subtree_can_inline(padding, size, lookahead_bytes)
+ );
+
+ if (is_inline) {
+ return (Subtree) {{
+ .parse_state = parse_state,
+ .symbol = symbol,
+ .padding_bytes = padding.bytes,
+ .padding_rows = padding.extent.row,
+ .padding_columns = padding.extent.column,
+ .size_bytes = size.bytes,
+ .lookahead_bytes = lookahead_bytes,
+ .visible = metadata.visible,
+ .named = metadata.named,
+ .extra = extra,
+ .has_changes = false,
+ .is_missing = false,
+ .is_keyword = is_keyword,
+ .is_inline = true,
+ }};
+ } else {
+ SubtreeHeapData *data = ts_subtree_pool_allocate(pool);
+ *data = (SubtreeHeapData) {
+ .ref_count = 1,
+ .padding = padding,
+ .size = size,
+ .lookahead_bytes = lookahead_bytes,
+ .error_cost = 0,
+ .child_count = 0,
+ .symbol = symbol,
+ .parse_state = parse_state,
+ .visible = metadata.visible,
+ .named = metadata.named,
+ .extra = extra,
+ .fragile_left = false,
+ .fragile_right = false,
+ .has_changes = false,
+ .has_external_tokens = has_external_tokens,
+ .is_missing = false,
+ .is_keyword = is_keyword,
+ .first_leaf = {.symbol = 0, .parse_state = 0},
+ };
+ return (Subtree) {.ptr = data};
+ }
+}
+
+void ts_subtree_set_symbol(
+ MutableSubtree *self,
+ TSSymbol symbol,
+ const TSLanguage *language
+) {
+ TSSymbolMetadata metadata = ts_language_symbol_metadata(language, symbol);
+ if (self->data.is_inline) {
+ assert(symbol < UINT8_MAX);
+ self->data.symbol = symbol;
+ self->data.named = metadata.named;
+ self->data.visible = metadata.visible;
+ } else {
+ self->ptr->symbol = symbol;
+ self->ptr->named = metadata.named;
+ self->ptr->visible = metadata.visible;
+ }
+}
+
+Subtree ts_subtree_new_error(
+ SubtreePool *pool, int32_t lookahead_char, Length padding, Length size,
+ uint32_t bytes_scanned, TSStateId parse_state, const TSLanguage *language
+) {
+ Subtree result = ts_subtree_new_leaf(
+ pool, ts_builtin_sym_error, padding, size, bytes_scanned,
+ parse_state, false, false, language
+ );
+ SubtreeHeapData *data = (SubtreeHeapData *)result.ptr;
+ data->fragile_left = true;
+ data->fragile_right = true;
+ data->lookahead_char = lookahead_char;
+ return result;
+}
+
+MutableSubtree ts_subtree_make_mut(SubtreePool *pool, Subtree self) {
+ if (self.data.is_inline) return (MutableSubtree) {self.data};
+ if (self.ptr->ref_count == 1) return ts_subtree_to_mut_unsafe(self);
+
+ SubtreeHeapData *result = ts_subtree_pool_allocate(pool);
+ memcpy(result, self.ptr, sizeof(SubtreeHeapData));
+ if (result->child_count > 0) {
+ result->children = ts_calloc(self.ptr->child_count, sizeof(Subtree));
+ memcpy(result->children, self.ptr->children, result->child_count * sizeof(Subtree));
+ for (uint32_t i = 0; i < result->child_count; i++) {
+ ts_subtree_retain(result->children[i]);
+ }
+ } else if (result->has_external_tokens) {
+ result->external_scanner_state = ts_external_scanner_state_copy(&self.ptr->external_scanner_state);
+ }
+ result->ref_count = 1;
+ ts_subtree_release(pool, self);
+ return (MutableSubtree) {.ptr = result};
+}
+
+static void ts_subtree__compress(MutableSubtree self, unsigned count, const TSLanguage *language,
+ MutableSubtreeArray *stack) {
+ unsigned initial_stack_size = stack->size;
+
+ MutableSubtree tree = self;
+ TSSymbol symbol = tree.ptr->symbol;
+ for (unsigned i = 0; i < count; i++) {
+ if (tree.ptr->ref_count > 1 || tree.ptr->child_count < 2) break;
+
+ MutableSubtree child = ts_subtree_to_mut_unsafe(tree.ptr->children[0]);
+ if (
+ child.data.is_inline ||
+ child.ptr->child_count < 2 ||
+ child.ptr->ref_count > 1 ||
+ child.ptr->symbol != symbol
+ ) break;
+
+ MutableSubtree grandchild = ts_subtree_to_mut_unsafe(child.ptr->children[0]);
+ if (
+ grandchild.data.is_inline ||
+ grandchild.ptr->child_count < 2 ||
+ grandchild.ptr->ref_count > 1 ||
+ grandchild.ptr->symbol != symbol
+ ) break;
+
+ tree.ptr->children[0] = ts_subtree_from_mut(grandchild);
+ child.ptr->children[0] = grandchild.ptr->children[grandchild.ptr->child_count - 1];
+ grandchild.ptr->children[grandchild.ptr->child_count - 1] = ts_subtree_from_mut(child);
+ array_push(stack, tree);
+ tree = grandchild;
+ }
+
+ while (stack->size > initial_stack_size) {
+ tree = array_pop(stack);
+ MutableSubtree child = ts_subtree_to_mut_unsafe(tree.ptr->children[0]);
+ MutableSubtree grandchild = ts_subtree_to_mut_unsafe(child.ptr->children[child.ptr->child_count - 1]);
+ ts_subtree_set_children(grandchild, grandchild.ptr->children, grandchild.ptr->child_count, language);
+ ts_subtree_set_children(child, child.ptr->children, child.ptr->child_count, language);
+ ts_subtree_set_children(tree, tree.ptr->children, tree.ptr->child_count, language);
+ }
+}
+
+void ts_subtree_balance(Subtree self, SubtreePool *pool, const TSLanguage *language) {
+ array_clear(&pool->tree_stack);
+
+ if (ts_subtree_child_count(self) > 0 && self.ptr->ref_count == 1) {
+ array_push(&pool->tree_stack, ts_subtree_to_mut_unsafe(self));
+ }
+
+ while (pool->tree_stack.size > 0) {
+ MutableSubtree tree = array_pop(&pool->tree_stack);
+
+ if (tree.ptr->repeat_depth > 0) {
+ Subtree child1 = tree.ptr->children[0];
+ Subtree child2 = tree.ptr->children[tree.ptr->child_count - 1];
+ if (
+ ts_subtree_child_count(child1) > 0 &&
+ ts_subtree_child_count(child2) > 0 &&
+ child1.ptr->repeat_depth > child2.ptr->repeat_depth
+ ) {
+ unsigned n = child1.ptr->repeat_depth - child2.ptr->repeat_depth;
+ for (unsigned i = n / 2; i > 0; i /= 2) {
+ ts_subtree__compress(tree, i, language, &pool->tree_stack);
+ n -= i;
+ }
+ }
+ }
+
+ for (uint32_t i = 0; i < tree.ptr->child_count; i++) {
+ Subtree child = tree.ptr->children[i];
+ if (ts_subtree_child_count(child) > 0 && child.ptr->ref_count == 1) {
+ array_push(&pool->tree_stack, ts_subtree_to_mut_unsafe(child));
+ }
+ }
+ }
+}
+
+static inline uint32_t ts_subtree_repeat_depth(Subtree self) {
+ return ts_subtree_child_count(self) ? self.ptr->repeat_depth : 0;
+}
+
+void ts_subtree_set_children(
+ MutableSubtree self, Subtree *children, uint32_t child_count, const TSLanguage *language
+) {
+ assert(!self.data.is_inline);
+
+ if (self.ptr->child_count > 0 && children != self.ptr->children) {
+ ts_free(self.ptr->children);
+ }
+
+ self.ptr->child_count = child_count;
+ self.ptr->children = children;
+ self.ptr->named_child_count = 0;
+ self.ptr->visible_child_count = 0;
+ self.ptr->error_cost = 0;
+ self.ptr->repeat_depth = 0;
+ self.ptr->node_count = 1;
+ self.ptr->has_external_tokens = false;
+ self.ptr->dynamic_precedence = 0;
+
+ uint32_t non_extra_index = 0;
+ const TSSymbol *alias_sequence = ts_language_alias_sequence(language, self.ptr->production_id);
+ uint32_t lookahead_end_byte = 0;
+
+ for (uint32_t i = 0; i < self.ptr->child_count; i++) {
+ Subtree child = self.ptr->children[i];
+
+ if (i == 0) {
+ self.ptr->padding = ts_subtree_padding(child);
+ self.ptr->size = ts_subtree_size(child);
+ } else {
+ self.ptr->size = length_add(self.ptr->size, ts_subtree_total_size(child));
+ }
+
+ uint32_t child_lookahead_end_byte =
+ self.ptr->padding.bytes +
+ self.ptr->size.bytes +
+ ts_subtree_lookahead_bytes(child);
+ if (child_lookahead_end_byte > lookahead_end_byte) lookahead_end_byte = child_lookahead_end_byte;
+
+ if (ts_subtree_symbol(child) != ts_builtin_sym_error_repeat) {
+ self.ptr->error_cost += ts_subtree_error_cost(child);
+ }
+
+ self.ptr->dynamic_precedence += ts_subtree_dynamic_precedence(child);
+ self.ptr->node_count += ts_subtree_node_count(child);
+
+ if (alias_sequence && alias_sequence[non_extra_index] != 0 && !ts_subtree_extra(child)) {
+ self.ptr->visible_child_count++;
+ if (ts_language_symbol_metadata(language, alias_sequence[non_extra_index]).named) {
+ self.ptr->named_child_count++;
+ }
+ } else if (ts_subtree_visible(child)) {
+ self.ptr->visible_child_count++;
+ if (ts_subtree_named(child)) self.ptr->named_child_count++;
+ } else if (ts_subtree_child_count(child) > 0) {
+ self.ptr->visible_child_count += child.ptr->visible_child_count;
+ self.ptr->named_child_count += child.ptr->named_child_count;
+ }
+
+ if (ts_subtree_has_external_tokens(child)) self.ptr->has_external_tokens = true;
+
+ if (ts_subtree_is_error(child)) {
+ self.ptr->fragile_left = self.ptr->fragile_right = true;
+ self.ptr->parse_state = TS_TREE_STATE_NONE;
+ }
+
+ if (!ts_subtree_extra(child)) non_extra_index++;
+ }
+
+ self.ptr->lookahead_bytes = lookahead_end_byte - self.ptr->size.bytes - self.ptr->padding.bytes;
+
+ if (self.ptr->symbol == ts_builtin_sym_error || self.ptr->symbol == ts_builtin_sym_error_repeat) {
+ self.ptr->error_cost +=
+ ERROR_COST_PER_RECOVERY +
+ ERROR_COST_PER_SKIPPED_CHAR * self.ptr->size.bytes +
+ ERROR_COST_PER_SKIPPED_LINE * self.ptr->size.extent.row;
+ for (uint32_t i = 0; i < self.ptr->child_count; i++) {
+ Subtree child = self.ptr->children[i];
+ uint32_t grandchild_count = ts_subtree_child_count(child);
+ if (ts_subtree_extra(child)) continue;
+ if (ts_subtree_is_error(child) && grandchild_count == 0) continue;
+ if (ts_subtree_visible(child)) {
+ self.ptr->error_cost += ERROR_COST_PER_SKIPPED_TREE;
+ } else if (grandchild_count > 0) {
+ self.ptr->error_cost += ERROR_COST_PER_SKIPPED_TREE * child.ptr->visible_child_count;
+ }
+ }
+ }
+
+ if (self.ptr->child_count > 0) {
+ Subtree first_child = self.ptr->children[0];
+ Subtree last_child = self.ptr->children[self.ptr->child_count - 1];
+
+ self.ptr->first_leaf.symbol = ts_subtree_leaf_symbol(first_child);
+ self.ptr->first_leaf.parse_state = ts_subtree_leaf_parse_state(first_child);
+
+ if (ts_subtree_fragile_left(first_child)) self.ptr->fragile_left = true;
+ if (ts_subtree_fragile_right(last_child)) self.ptr->fragile_right = true;
+
+ if (
+ self.ptr->child_count >= 2 &&
+ !self.ptr->visible &&
+ !self.ptr->named &&
+ ts_subtree_symbol(first_child) == self.ptr->symbol
+ ) {
+ if (ts_subtree_repeat_depth(first_child) > ts_subtree_repeat_depth(last_child)) {
+ self.ptr->repeat_depth = ts_subtree_repeat_depth(first_child) + 1;
+ } else {
+ self.ptr->repeat_depth = ts_subtree_repeat_depth(last_child) + 1;
+ }
+ }
+ }
+}
+
+MutableSubtree ts_subtree_new_node(SubtreePool *pool, TSSymbol symbol,
+ SubtreeArray *children, unsigned production_id,
+ const TSLanguage *language) {
+ TSSymbolMetadata metadata = ts_language_symbol_metadata(language, symbol);
+ bool fragile = symbol == ts_builtin_sym_error || symbol == ts_builtin_sym_error_repeat;
+ SubtreeHeapData *data = ts_subtree_pool_allocate(pool);
+ *data = (SubtreeHeapData) {
+ .ref_count = 1,
+ .symbol = symbol,
+ .production_id = production_id,
+ .visible = metadata.visible,
+ .named = metadata.named,
+ .has_changes = false,
+ .fragile_left = fragile,
+ .fragile_right = fragile,
+ .is_keyword = false,
+ .node_count = 0,
+ .first_leaf = {.symbol = 0, .parse_state = 0},
+ };
+ MutableSubtree result = {.ptr = data};
+ ts_subtree_set_children(result, children->contents, children->size, language);
+ return result;
+}
+
+Subtree ts_subtree_new_error_node(SubtreePool *pool, SubtreeArray *children,
+ bool extra, const TSLanguage *language) {
+ MutableSubtree result = ts_subtree_new_node(
+ pool, ts_builtin_sym_error, children, 0, language
+ );
+ result.ptr->extra = extra;
+ return ts_subtree_from_mut(result);
+}
+
+Subtree ts_subtree_new_missing_leaf(SubtreePool *pool, TSSymbol symbol, Length padding,
+ const TSLanguage *language) {
+ Subtree result = ts_subtree_new_leaf(
+ pool, symbol, padding, length_zero(), 0,
+ 0, false, false, language
+ );
+
+ if (result.data.is_inline) {
+ result.data.is_missing = true;
+ } else {
+ ((SubtreeHeapData *)result.ptr)->is_missing = true;
+ }
+
+ return result;
+}
+
+void ts_subtree_retain(Subtree self) {
+ if (self.data.is_inline) return;
+ assert(self.ptr->ref_count > 0);
+ atomic_inc((volatile uint32_t *)&self.ptr->ref_count);
+ assert(self.ptr->ref_count != 0);
+}
+
+void ts_subtree_release(SubtreePool *pool, Subtree self) {
+ if (self.data.is_inline) return;
+ array_clear(&pool->tree_stack);
+
+ assert(self.ptr->ref_count > 0);
+ if (atomic_dec((volatile uint32_t *)&self.ptr->ref_count) == 0) {
+ array_push(&pool->tree_stack, ts_subtree_to_mut_unsafe(self));
+ }
+
+ while (pool->tree_stack.size > 0) {
+ MutableSubtree tree = array_pop(&pool->tree_stack);
+ if (tree.ptr->child_count > 0) {
+ for (uint32_t i = 0; i < tree.ptr->child_count; i++) {
+ Subtree child = tree.ptr->children[i];
+ if (child.data.is_inline) continue;
+ assert(child.ptr->ref_count > 0);
+ if (atomic_dec((volatile uint32_t *)&child.ptr->ref_count) == 0) {
+ array_push(&pool->tree_stack, ts_subtree_to_mut_unsafe(child));
+ }
+ }
+ ts_free(tree.ptr->children);
+ } else if (tree.ptr->has_external_tokens) {
+ ts_external_scanner_state_delete(&tree.ptr->external_scanner_state);
+ }
+ ts_subtree_pool_free(pool, tree.ptr);
+ }
+}
+
+bool ts_subtree_eq(Subtree self, Subtree other) {
+ if (self.data.is_inline || other.data.is_inline) {
+ return memcmp(&self, &other, sizeof(SubtreeInlineData)) == 0;
+ }
+
+ if (self.ptr) {
+ if (!other.ptr) return false;
+ } else {
+ return !other.ptr;
+ }
+
+ if (self.ptr->symbol != other.ptr->symbol) return false;
+ if (self.ptr->visible != other.ptr->visible) return false;
+ if (self.ptr->named != other.ptr->named) return false;
+ if (self.ptr->padding.bytes != other.ptr->padding.bytes) return false;
+ if (self.ptr->size.bytes != other.ptr->size.bytes) return false;
+ if (self.ptr->symbol == ts_builtin_sym_error) return self.ptr->lookahead_char == other.ptr->lookahead_char;
+ if (self.ptr->child_count != other.ptr->child_count) return false;
+ if (self.ptr->child_count > 0) {
+ if (self.ptr->visible_child_count != other.ptr->visible_child_count) return false;
+ if (self.ptr->named_child_count != other.ptr->named_child_count) return false;
+
+ for (uint32_t i = 0; i < self.ptr->child_count; i++) {
+ if (!ts_subtree_eq(self.ptr->children[i], other.ptr->children[i])) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+int ts_subtree_compare(Subtree left, Subtree right) {
+ if (ts_subtree_symbol(left) < ts_subtree_symbol(right)) return -1;
+ if (ts_subtree_symbol(right) < ts_subtree_symbol(left)) return 1;
+ if (ts_subtree_child_count(left) < ts_subtree_child_count(right)) return -1;
+ if (ts_subtree_child_count(right) < ts_subtree_child_count(left)) return 1;
+ for (uint32_t i = 0, n = ts_subtree_child_count(left); i < n; i++) {
+ Subtree left_child = left.ptr->children[i];
+ Subtree right_child = right.ptr->children[i];
+ switch (ts_subtree_compare(left_child, right_child)) {
+ case -1: return -1;
+ case 1: return 1;
+ default: break;
+ }
+ }
+ return 0;
+}
+
+static inline void ts_subtree_set_has_changes(MutableSubtree *self) {
+ if (self->data.is_inline) {
+ self->data.has_changes = true;
+ } else {
+ self->ptr->has_changes = true;
+ }
+}
+
+Subtree ts_subtree_edit(Subtree self, const TSInputEdit *edit, SubtreePool *pool) {
+ typedef struct {
+ Subtree *tree;
+ Edit edit;
+ } StackEntry;
+
+ Array(StackEntry) stack = array_new();
+ array_push(&stack, ((StackEntry) {
+ .tree = &self,
+ .edit = (Edit) {
+ .start = {edit->start_byte, edit->start_point},
+ .old_end = {edit->old_end_byte, edit->old_end_point},
+ .new_end = {edit->new_end_byte, edit->new_end_point},
+ },
+ }));
+
+ while (stack.size) {
+ StackEntry entry = array_pop(&stack);
+ Edit edit = entry.edit;
+ bool is_noop = edit.old_end.bytes == edit.start.bytes && edit.new_end.bytes == edit.start.bytes;
+ bool is_pure_insertion = edit.old_end.bytes == edit.start.bytes;
+
+ Length size = ts_subtree_size(*entry.tree);
+ Length padding = ts_subtree_padding(*entry.tree);
+ uint32_t lookahead_bytes = ts_subtree_lookahead_bytes(*entry.tree);
+ uint32_t end_byte = padding.bytes + size.bytes + lookahead_bytes;
+ if (edit.start.bytes > end_byte || (is_noop && edit.start.bytes == end_byte)) continue;
+
+ // If the edit is entirely within the space before this subtree, then shift this
+ // subtree over according to the edit without changing its size.
+ if (edit.old_end.bytes <= padding.bytes) {
+ padding = length_add(edit.new_end, length_sub(padding, edit.old_end));
+ }
+
+ // If the edit starts in the space before this subtree and extends into this subtree,
+ // shrink the subtree's content to compensate for the change in the space before it.
+ else if (edit.start.bytes < padding.bytes) {
+ size = length_sub(size, length_sub(edit.old_end, padding));
+ padding = edit.new_end;
+ }
+
+ // If the edit is a pure insertion right at the start of the subtree,
+ // shift the subtree over according to the insertion.
+ else if (edit.start.bytes == padding.bytes && is_pure_insertion) {
+ padding = edit.new_end;
+ }
+
+ // If the edit is within this subtree, resize the subtree to reflect the edit.
+ else {
+ uint32_t total_bytes = padding.bytes + size.bytes;
+ if (edit.start.bytes < total_bytes ||
+ (edit.start.bytes == total_bytes && is_pure_insertion)) {
+ size = length_add(
+ length_sub(edit.new_end, padding),
+ length_sub(size, length_sub(edit.old_end, padding))
+ );
+ }
+ }
+
+ MutableSubtree result = ts_subtree_make_mut(pool, *entry.tree);
+
+ if (result.data.is_inline) {
+ if (ts_subtree_can_inline(padding, size, lookahead_bytes)) {
+ result.data.padding_bytes = padding.bytes;
+ result.data.padding_rows = padding.extent.row;
+ result.data.padding_columns = padding.extent.column;
+ result.data.size_bytes = size.bytes;
+ } else {
+ SubtreeHeapData *data = ts_subtree_pool_allocate(pool);
+ data->ref_count = 1;
+ data->padding = padding;
+ data->size = size;
+ data->lookahead_bytes = lookahead_bytes;
+ data->error_cost = 0;
+ data->child_count = 0;
+ data->symbol = result.data.symbol;
+ data->parse_state = result.data.parse_state;
+ data->visible = result.data.visible;
+ data->named = result.data.named;
+ data->extra = result.data.extra;
+ data->fragile_left = false;
+ data->fragile_right = false;
+ data->has_changes = false;
+ data->has_external_tokens = false;
+ data->is_missing = result.data.is_missing;
+ data->is_keyword = result.data.is_keyword;
+ result.ptr = data;
+ }
+ } else {
+ result.ptr->padding = padding;
+ result.ptr->size = size;
+ }
+
+ ts_subtree_set_has_changes(&result);
+ *entry.tree = ts_subtree_from_mut(result);
+
+ Length child_left, child_right = length_zero();
+ for (uint32_t i = 0, n = ts_subtree_child_count(*entry.tree); i < n; i++) {
+ Subtree *child = &result.ptr->children[i];
+ Length child_size = ts_subtree_total_size(*child);
+ child_left = child_right;
+ child_right = length_add(child_left, child_size);
+
+ // If this child ends before the edit, it is not affected.
+ if (child_right.bytes + ts_subtree_lookahead_bytes(*child) < edit.start.bytes) continue;
+
+ // If this child starts after the edit, then we're done processing children.
+ if (child_left.bytes > edit.old_end.bytes ||
+ (child_left.bytes == edit.old_end.bytes && child_size.bytes > 0 && i > 0)) break;
+
+ // Transform edit into the child's coordinate space.
+ Edit child_edit = {
+ .start = length_sub(edit.start, child_left),
+ .old_end = length_sub(edit.old_end, child_left),
+ .new_end = length_sub(edit.new_end, child_left),
+ };
+
+ // Clamp child_edit to the child's bounds.
+ if (edit.start.bytes < child_left.bytes) child_edit.start = length_zero();
+ if (edit.old_end.bytes < child_left.bytes) child_edit.old_end = length_zero();
+ if (edit.new_end.bytes < child_left.bytes) child_edit.new_end = length_zero();
+ if (edit.old_end.bytes > child_right.bytes) child_edit.old_end = child_size;
+
+ // Interpret all inserted text as applying to the *first* child that touches the edit.
+ // Subsequent children are only never have any text inserted into them; they are only
+ // shrunk to compensate for the edit.
+ if (child_right.bytes > edit.start.bytes ||
+ (child_right.bytes == edit.start.bytes && is_pure_insertion)) {
+ edit.new_end = edit.start;
+ }
+
+ // Children that occur before the edit are not reshaped by the edit.
+ else {
+ child_edit.old_end = child_edit.start;
+ child_edit.new_end = child_edit.start;
+ }
+
+ // Queue processing of this child's subtree.
+ array_push(&stack, ((StackEntry) {
+ .tree = child,
+ .edit = child_edit,
+ }));
+ }
+ }
+
+ array_delete(&stack);
+ return self;
+}
+
+Subtree ts_subtree_last_external_token(Subtree tree) {
+ if (!ts_subtree_has_external_tokens(tree)) return NULL_SUBTREE;
+ while (tree.ptr->child_count > 0) {
+ for (uint32_t i = tree.ptr->child_count - 1; i + 1 > 0; i--) {
+ Subtree child = tree.ptr->children[i];
+ if (ts_subtree_has_external_tokens(child)) {
+ tree = child;
+ break;
+ }
+ }
+ }
+ return tree;
+}
+
+static size_t ts_subtree__write_char_to_string(char *s, size_t n, int32_t c) {
+ if (c == 0)
+ return snprintf(s, n, "EOF");
+ if (c == -1)
+ return snprintf(s, n, "INVALID");
+ else if (c == '\n')
+ return snprintf(s, n, "'\\n'");
+ else if (c == '\t')
+ return snprintf(s, n, "'\\t'");
+ else if (c == '\r')
+ return snprintf(s, n, "'\\r'");
+ else if (0 < c && c < 128 && isprint(c))
+ return snprintf(s, n, "'%c'", c);
+ else
+ return snprintf(s, n, "%d", c);
+}
+
+static void ts_subtree__write_dot_string(FILE *f, const char *string) {
+ for (const char *c = string; *c; c++) {
+ if (*c == '"') {
+ fputs("\\\"", f);
+ } else if (*c == '\n') {
+ fputs("\\n", f);
+ } else {
+ fputc(*c, f);
+ }
+ }
+}
+
+static const char *ROOT_FIELD = "__ROOT__";
+
+static size_t ts_subtree__write_to_string(
+ Subtree self, char *string, size_t limit,
+ const TSLanguage *language, bool include_all,
+ TSSymbol alias_symbol, bool alias_is_named, const char *field_name
+) {
+ if (!self.ptr) return snprintf(string, limit, "(NULL)");
+
+ char *cursor = string;
+ char **writer = (limit > 0) ? &cursor : &string;
+ bool is_root = field_name == ROOT_FIELD;
+ bool is_visible =
+ include_all ||
+ ts_subtree_missing(self) ||
+ (
+ alias_symbol
+ ? alias_is_named
+ : ts_subtree_visible(self) && ts_subtree_named(self)
+ );
+
+ if (is_visible) {
+ if (!is_root) {
+ cursor += snprintf(*writer, limit, " ");
+ if (field_name) {
+ cursor += snprintf(*writer, limit, "%s: ", field_name);
+ }
+ }
+
+ if (ts_subtree_is_error(self) && ts_subtree_child_count(self) == 0 && self.ptr->size.bytes > 0) {
+ cursor += snprintf(*writer, limit, "(UNEXPECTED ");
+ cursor += ts_subtree__write_char_to_string(*writer, limit, self.ptr->lookahead_char);
+ } else {
+ TSSymbol symbol = alias_symbol ? alias_symbol : ts_subtree_symbol(self);
+ const char *symbol_name = ts_language_symbol_name(language, symbol);
+ if (ts_subtree_missing(self)) {
+ cursor += snprintf(*writer, limit, "(MISSING ");
+ if (alias_is_named || ts_subtree_named(self)) {
+ cursor += snprintf(*writer, limit, "%s", symbol_name);
+ } else {
+ cursor += snprintf(*writer, limit, "\"%s\"", symbol_name);
+ }
+ } else {
+ cursor += snprintf(*writer, limit, "(%s", symbol_name);
+ }
+ }
+ } else if (is_root) {
+ TSSymbol symbol = ts_subtree_symbol(self);
+ const char *symbol_name = ts_language_symbol_name(language, symbol);
+ cursor += snprintf(*writer, limit, "(\"%s\")", symbol_name);
+ }
+
+ if (ts_subtree_child_count(self)) {
+ const TSSymbol *alias_sequence = ts_language_alias_sequence(language, self.ptr->production_id);
+ const TSFieldMapEntry *field_map, *field_map_end;
+ ts_language_field_map(
+ language,
+ self.ptr->production_id,
+ &field_map,
+ &field_map_end
+ );
+
+ uint32_t structural_child_index = 0;
+ for (uint32_t i = 0; i < self.ptr->child_count; i++) {
+ Subtree child = self.ptr->children[i];
+ if (ts_subtree_extra(child)) {
+ cursor += ts_subtree__write_to_string(
+ child, *writer, limit,
+ language, include_all,
+ 0, false, NULL
+ );
+ } else {
+ TSSymbol alias_symbol = alias_sequence
+ ? alias_sequence[structural_child_index]
+ : 0;
+ bool alias_is_named = alias_symbol
+ ? ts_language_symbol_metadata(language, alias_symbol).named
+ : false;
+
+ const char *child_field_name = is_visible ? NULL : field_name;
+ for (const TSFieldMapEntry *i = field_map; i < field_map_end; i++) {
+ if (!i->inherited && i->child_index == structural_child_index) {
+ child_field_name = language->field_names[i->field_id];
+ break;
+ }
+ }
+
+ cursor += ts_subtree__write_to_string(
+ child, *writer, limit,
+ language, include_all,
+ alias_symbol, alias_is_named, child_field_name
+ );
+ structural_child_index++;
+ }
+ }
+ }
+
+ if (is_visible) cursor += snprintf(*writer, limit, ")");
+
+ return cursor - string;
+}
+
+char *ts_subtree_string(
+ Subtree self,
+ const TSLanguage *language,
+ bool include_all
+) {
+ char scratch_string[1];
+ size_t size = ts_subtree__write_to_string(
+ self, scratch_string, 0,
+ language, include_all,
+ 0, false, ROOT_FIELD
+ ) + 1;
+ char *result = malloc(size * sizeof(char));
+ ts_subtree__write_to_string(
+ self, result, size,
+ language, include_all,
+ 0, false, ROOT_FIELD
+ );
+ return result;
+}
+
+void ts_subtree__print_dot_graph(const Subtree *self, uint32_t start_offset,
+ const TSLanguage *language, TSSymbol alias_symbol,
+ FILE *f) {
+ TSSymbol subtree_symbol = ts_subtree_symbol(*self);
+ TSSymbol symbol = alias_symbol ? alias_symbol : subtree_symbol;
+ uint32_t end_offset = start_offset + ts_subtree_total_bytes(*self);
+ fprintf(f, "tree_%p [label=\"", self);
+ ts_subtree__write_dot_string(f, ts_language_symbol_name(language, symbol));
+ fprintf(f, "\"");
+
+ if (ts_subtree_child_count(*self) == 0) fprintf(f, ", shape=plaintext");
+ if (ts_subtree_extra(*self)) fprintf(f, ", fontcolor=gray");
+
+ fprintf(f, ", tooltip=\""
+ "range: %u - %u\n"
+ "state: %d\n"
+ "error-cost: %u\n"
+ "has-changes: %u\n"
+ "repeat-depth: %u\n"
+ "lookahead-bytes: %u",
+ start_offset, end_offset,
+ ts_subtree_parse_state(*self),
+ ts_subtree_error_cost(*self),
+ ts_subtree_has_changes(*self),
+ ts_subtree_repeat_depth(*self),
+ ts_subtree_lookahead_bytes(*self)
+ );
+
+ if (ts_subtree_is_error(*self) && ts_subtree_child_count(*self) == 0) {
+ fprintf(f, "\ncharacter: '%c'", self->ptr->lookahead_char);
+ }
+
+ fprintf(f, "\"]\n");
+
+ uint32_t child_start_offset = start_offset;
+ uint32_t child_info_offset =
+ language->max_alias_sequence_length *
+ ts_subtree_production_id(*self);
+ for (uint32_t i = 0, n = ts_subtree_child_count(*self); i < n; i++) {
+ const Subtree *child = &self->ptr->children[i];
+ TSSymbol alias_symbol = 0;
+ if (!ts_subtree_extra(*child) && child_info_offset) {
+ alias_symbol = language->alias_sequences[child_info_offset];
+ child_info_offset++;
+ }
+ ts_subtree__print_dot_graph(child, child_start_offset, language, alias_symbol, f);
+ fprintf(f, "tree_%p -> tree_%p [tooltip=%u]\n", self, child, i);
+ child_start_offset += ts_subtree_total_bytes(*child);
+ }
+}
+
+void ts_subtree_print_dot_graph(Subtree self, const TSLanguage *language, FILE *f) {
+ fprintf(f, "digraph tree {\n");
+ fprintf(f, "edge [arrowhead=none]\n");
+ ts_subtree__print_dot_graph(&self, 0, language, 0, f);
+ fprintf(f, "}\n");
+}
+
+bool ts_subtree_external_scanner_state_eq(Subtree self, Subtree other) {
+ const ExternalScannerState *state1 = &empty_state;
+ const ExternalScannerState *state2 = &empty_state;
+ if (self.ptr && ts_subtree_has_external_tokens(self) && !self.ptr->child_count) {
+ state1 = &self.ptr->external_scanner_state;
+ }
+ if (other.ptr && ts_subtree_has_external_tokens(other) && !other.ptr->child_count) {
+ state2 = &other.ptr->external_scanner_state;
+ }
+ return ts_external_scanner_state_eq(state1, state2);
+}
diff --git a/src/tree_sitter/subtree.h b/src/tree_sitter/subtree.h
new file mode 100644
index 0000000000..79ccd92390
--- /dev/null
+++ b/src/tree_sitter/subtree.h
@@ -0,0 +1,281 @@
+#ifndef TREE_SITTER_SUBTREE_H_
+#define TREE_SITTER_SUBTREE_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include "./length.h"
+#include "./array.h"
+#include "./error_costs.h"
+#include "tree_sitter/api.h"
+#include "tree_sitter/parser.h"
+
+static const TSStateId TS_TREE_STATE_NONE = USHRT_MAX;
+#define NULL_SUBTREE ((Subtree) {.ptr = NULL})
+
+typedef union Subtree Subtree;
+typedef union MutableSubtree MutableSubtree;
+
+typedef struct {
+ union {
+ char *long_data;
+ char short_data[24];
+ };
+ uint32_t length;
+} ExternalScannerState;
+
+typedef struct {
+ bool is_inline : 1;
+ bool visible : 1;
+ bool named : 1;
+ bool extra : 1;
+ bool has_changes : 1;
+ bool is_missing : 1;
+ bool is_keyword : 1;
+ uint8_t symbol;
+ uint8_t padding_bytes;
+ uint8_t size_bytes;
+ uint8_t padding_columns;
+ uint8_t padding_rows : 4;
+ uint8_t lookahead_bytes : 4;
+ uint16_t parse_state;
+} SubtreeInlineData;
+
+typedef struct {
+ volatile uint32_t ref_count;
+ Length padding;
+ Length size;
+ uint32_t lookahead_bytes;
+ uint32_t error_cost;
+ uint32_t child_count;
+ TSSymbol symbol;
+ TSStateId parse_state;
+
+ bool visible : 1;
+ bool named : 1;
+ bool extra : 1;
+ bool fragile_left : 1;
+ bool fragile_right : 1;
+ bool has_changes : 1;
+ bool has_external_tokens : 1;
+ bool is_missing : 1;
+ bool is_keyword : 1;
+
+ union {
+ // Non-terminal subtrees (`child_count > 0`)
+ struct {
+ Subtree *children;
+ uint32_t visible_child_count;
+ uint32_t named_child_count;
+ uint32_t node_count;
+ uint32_t repeat_depth;
+ int32_t dynamic_precedence;
+ uint16_t production_id;
+ struct {
+ TSSymbol symbol;
+ TSStateId parse_state;
+ } first_leaf;
+ };
+
+ // External terminal subtrees (`child_count == 0 && has_external_tokens`)
+ ExternalScannerState external_scanner_state;
+
+ // Error terminal subtrees (`child_count == 0 && symbol == ts_builtin_sym_error`)
+ int32_t lookahead_char;
+ };
+} SubtreeHeapData;
+
+union Subtree {
+ SubtreeInlineData data;
+ const SubtreeHeapData *ptr;
+};
+
+union MutableSubtree {
+ SubtreeInlineData data;
+ SubtreeHeapData *ptr;
+};
+
+typedef Array(Subtree) SubtreeArray;
+typedef Array(MutableSubtree) MutableSubtreeArray;
+
+typedef struct {
+ MutableSubtreeArray free_trees;
+ MutableSubtreeArray tree_stack;
+} SubtreePool;
+
+void ts_external_scanner_state_init(ExternalScannerState *, const char *, unsigned);
+const char *ts_external_scanner_state_data(const ExternalScannerState *);
+
+void ts_subtree_array_copy(SubtreeArray, SubtreeArray *);
+void ts_subtree_array_delete(SubtreePool *, SubtreeArray *);
+SubtreeArray ts_subtree_array_remove_trailing_extras(SubtreeArray *);
+void ts_subtree_array_reverse(SubtreeArray *);
+
+SubtreePool ts_subtree_pool_new(uint32_t capacity);
+void ts_subtree_pool_delete(SubtreePool *);
+
+Subtree ts_subtree_new_leaf(
+ SubtreePool *, TSSymbol, Length, Length, uint32_t,
+ TSStateId, bool, bool, const TSLanguage *
+);
+Subtree ts_subtree_new_error(
+ SubtreePool *, int32_t, Length, Length, uint32_t, TSStateId, const TSLanguage *
+);
+MutableSubtree ts_subtree_new_node(SubtreePool *, TSSymbol, SubtreeArray *, unsigned, const TSLanguage *);
+Subtree ts_subtree_new_error_node(SubtreePool *, SubtreeArray *, bool, const TSLanguage *);
+Subtree ts_subtree_new_missing_leaf(SubtreePool *, TSSymbol, Length, const TSLanguage *);
+MutableSubtree ts_subtree_make_mut(SubtreePool *, Subtree);
+void ts_subtree_retain(Subtree);
+void ts_subtree_release(SubtreePool *, Subtree);
+bool ts_subtree_eq(Subtree, Subtree);
+int ts_subtree_compare(Subtree, Subtree);
+void ts_subtree_set_symbol(MutableSubtree *, TSSymbol, const TSLanguage *);
+void ts_subtree_set_children(MutableSubtree, Subtree *, uint32_t, const TSLanguage *);
+void ts_subtree_balance(Subtree, SubtreePool *, const TSLanguage *);
+Subtree ts_subtree_edit(Subtree, const TSInputEdit *edit, SubtreePool *);
+char *ts_subtree_string(Subtree, const TSLanguage *, bool include_all);
+void ts_subtree_print_dot_graph(Subtree, const TSLanguage *, FILE *);
+Subtree ts_subtree_last_external_token(Subtree);
+bool ts_subtree_external_scanner_state_eq(Subtree, Subtree);
+
+#define SUBTREE_GET(self, name) (self.data.is_inline ? self.data.name : self.ptr->name)
+
+static inline TSSymbol ts_subtree_symbol(Subtree self) { return SUBTREE_GET(self, symbol); }
+static inline bool ts_subtree_visible(Subtree self) { return SUBTREE_GET(self, visible); }
+static inline bool ts_subtree_named(Subtree self) { return SUBTREE_GET(self, named); }
+static inline bool ts_subtree_extra(Subtree self) { return SUBTREE_GET(self, extra); }
+static inline bool ts_subtree_has_changes(Subtree self) { return SUBTREE_GET(self, has_changes); }
+static inline bool ts_subtree_missing(Subtree self) { return SUBTREE_GET(self, is_missing); }
+static inline bool ts_subtree_is_keyword(Subtree self) { return SUBTREE_GET(self, is_keyword); }
+static inline TSStateId ts_subtree_parse_state(Subtree self) { return SUBTREE_GET(self, parse_state); }
+static inline uint32_t ts_subtree_lookahead_bytes(Subtree self) { return SUBTREE_GET(self, lookahead_bytes); }
+
+#undef SUBTREE_GET
+
+static inline void ts_subtree_set_extra(MutableSubtree *self) {
+ if (self->data.is_inline) {
+ self->data.extra = true;
+ } else {
+ self->ptr->extra = true;
+ }
+}
+
+static inline TSSymbol ts_subtree_leaf_symbol(Subtree self) {
+ if (self.data.is_inline) return self.data.symbol;
+ if (self.ptr->child_count == 0) return self.ptr->symbol;
+ return self.ptr->first_leaf.symbol;
+}
+
+static inline TSStateId ts_subtree_leaf_parse_state(Subtree self) {
+ if (self.data.is_inline) return self.data.parse_state;
+ if (self.ptr->child_count == 0) return self.ptr->parse_state;
+ return self.ptr->first_leaf.parse_state;
+}
+
+static inline Length ts_subtree_padding(Subtree self) {
+ if (self.data.is_inline) {
+ Length result = {self.data.padding_bytes, {self.data.padding_rows, self.data.padding_columns}};
+ return result;
+ } else {
+ return self.ptr->padding;
+ }
+}
+
+static inline Length ts_subtree_size(Subtree self) {
+ if (self.data.is_inline) {
+ Length result = {self.data.size_bytes, {0, self.data.size_bytes}};
+ return result;
+ } else {
+ return self.ptr->size;
+ }
+}
+
+static inline Length ts_subtree_total_size(Subtree self) {
+ return length_add(ts_subtree_padding(self), ts_subtree_size(self));
+}
+
+static inline uint32_t ts_subtree_total_bytes(Subtree self) {
+ return ts_subtree_total_size(self).bytes;
+}
+
+static inline uint32_t ts_subtree_child_count(Subtree self) {
+ return self.data.is_inline ? 0 : self.ptr->child_count;
+}
+
+static inline uint32_t ts_subtree_node_count(Subtree self) {
+ return (self.data.is_inline || self.ptr->child_count == 0) ? 1 : self.ptr->node_count;
+}
+
+static inline uint32_t ts_subtree_visible_child_count(Subtree self) {
+ if (ts_subtree_child_count(self) > 0) {
+ return self.ptr->visible_child_count;
+ } else {
+ return 0;
+ }
+}
+
+static inline uint32_t ts_subtree_error_cost(Subtree self) {
+ if (ts_subtree_missing(self)) {
+ return ERROR_COST_PER_MISSING_TREE + ERROR_COST_PER_RECOVERY;
+ } else {
+ return self.data.is_inline ? 0 : self.ptr->error_cost;
+ }
+}
+
+static inline int32_t ts_subtree_dynamic_precedence(Subtree self) {
+ return (self.data.is_inline || self.ptr->child_count == 0) ? 0 : self.ptr->dynamic_precedence;
+}
+
+static inline uint16_t ts_subtree_production_id(Subtree self) {
+ if (ts_subtree_child_count(self) > 0) {
+ return self.ptr->production_id;
+ } else {
+ return 0;
+ }
+}
+
+static inline bool ts_subtree_fragile_left(Subtree self) {
+ return self.data.is_inline ? false : self.ptr->fragile_left;
+}
+
+static inline bool ts_subtree_fragile_right(Subtree self) {
+ return self.data.is_inline ? false : self.ptr->fragile_right;
+}
+
+static inline bool ts_subtree_has_external_tokens(Subtree self) {
+ return self.data.is_inline ? false : self.ptr->has_external_tokens;
+}
+
+static inline bool ts_subtree_is_fragile(Subtree self) {
+ return self.data.is_inline ? false : (self.ptr->fragile_left || self.ptr->fragile_right);
+}
+
+static inline bool ts_subtree_is_error(Subtree self) {
+ return ts_subtree_symbol(self) == ts_builtin_sym_error;
+}
+
+static inline bool ts_subtree_is_eof(Subtree self) {
+ return ts_subtree_symbol(self) == ts_builtin_sym_end;
+}
+
+static inline Subtree ts_subtree_from_mut(MutableSubtree self) {
+ Subtree result;
+ result.data = self.data;
+ return result;
+}
+
+static inline MutableSubtree ts_subtree_to_mut_unsafe(Subtree self) {
+ MutableSubtree result;
+ result.data = self.data;
+ return result;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_SUBTREE_H_
diff --git a/src/tree_sitter/tree.c b/src/tree_sitter/tree.c
new file mode 100644
index 0000000000..04cb1d242f
--- /dev/null
+++ b/src/tree_sitter/tree.c
@@ -0,0 +1,149 @@
+#include "tree_sitter/api.h"
+#include "./array.h"
+#include "./get_changed_ranges.h"
+#include "./subtree.h"
+#include "./tree_cursor.h"
+#include "./tree.h"
+
+static const unsigned PARENT_CACHE_CAPACITY = 32;
+
+TSTree *ts_tree_new(
+ Subtree root, const TSLanguage *language,
+ const TSRange *included_ranges, unsigned included_range_count
+) {
+ TSTree *result = ts_malloc(sizeof(TSTree));
+ result->root = root;
+ result->language = language;
+ result->parent_cache = NULL;
+ result->parent_cache_start = 0;
+ result->parent_cache_size = 0;
+ result->included_ranges = ts_calloc(included_range_count, sizeof(TSRange));
+ memcpy(result->included_ranges, included_ranges, included_range_count * sizeof(TSRange));
+ result->included_range_count = included_range_count;
+ return result;
+}
+
+TSTree *ts_tree_copy(const TSTree *self) {
+ ts_subtree_retain(self->root);
+ return ts_tree_new(self->root, self->language, self->included_ranges, self->included_range_count);
+}
+
+void ts_tree_delete(TSTree *self) {
+ if (!self) return;
+
+ SubtreePool pool = ts_subtree_pool_new(0);
+ ts_subtree_release(&pool, self->root);
+ ts_subtree_pool_delete(&pool);
+ ts_free(self->included_ranges);
+ if (self->parent_cache) ts_free(self->parent_cache);
+ ts_free(self);
+}
+
+TSNode ts_tree_root_node(const TSTree *self) {
+ return ts_node_new(self, &self->root, ts_subtree_padding(self->root), 0);
+}
+
+const TSLanguage *ts_tree_language(const TSTree *self) {
+ return self->language;
+}
+
+void ts_tree_edit(TSTree *self, const TSInputEdit *edit) {
+ for (unsigned i = 0; i < self->included_range_count; i++) {
+ TSRange *range = &self->included_ranges[i];
+ if (range->end_byte >= edit->old_end_byte) {
+ if (range->end_byte != UINT32_MAX) {
+ range->end_byte = edit->new_end_byte + (range->end_byte - edit->old_end_byte);
+ range->end_point = point_add(
+ edit->new_end_point,
+ point_sub(range->end_point, edit->old_end_point)
+ );
+ if (range->end_byte < edit->new_end_byte) {
+ range->end_byte = UINT32_MAX;
+ range->end_point = POINT_MAX;
+ }
+ }
+ if (range->start_byte >= edit->old_end_byte) {
+ range->start_byte = edit->new_end_byte + (range->start_byte - edit->old_end_byte);
+ range->start_point = point_add(
+ edit->new_end_point,
+ point_sub(range->start_point, edit->old_end_point)
+ );
+ if (range->start_byte < edit->new_end_byte) {
+ range->start_byte = UINT32_MAX;
+ range->start_point = POINT_MAX;
+ }
+ }
+ }
+ }
+
+ SubtreePool pool = ts_subtree_pool_new(0);
+ self->root = ts_subtree_edit(self->root, edit, &pool);
+ self->parent_cache_start = 0;
+ self->parent_cache_size = 0;
+ ts_subtree_pool_delete(&pool);
+}
+
+TSRange *ts_tree_get_changed_ranges(const TSTree *self, const TSTree *other, uint32_t *count) {
+ TSRange *result;
+ TreeCursor cursor1 = {NULL, array_new()};
+ TreeCursor cursor2 = {NULL, array_new()};
+ TSNode root = ts_tree_root_node(self);
+ ts_tree_cursor_init(&cursor1, root);
+ ts_tree_cursor_init(&cursor2, root);
+
+ TSRangeArray included_range_differences = array_new();
+ ts_range_array_get_changed_ranges(
+ self->included_ranges, self->included_range_count,
+ other->included_ranges, other->included_range_count,
+ &included_range_differences
+ );
+
+ *count = ts_subtree_get_changed_ranges(
+ &self->root, &other->root, &cursor1, &cursor2,
+ self->language, &included_range_differences, &result
+ );
+
+ array_delete(&included_range_differences);
+ array_delete(&cursor1.stack);
+ array_delete(&cursor2.stack);
+ return result;
+}
+
+void ts_tree_print_dot_graph(const TSTree *self, FILE *file) {
+ ts_subtree_print_dot_graph(self->root, self->language, file);
+}
+
+TSNode ts_tree_get_cached_parent(const TSTree *self, const TSNode *node) {
+ for (uint32_t i = 0; i < self->parent_cache_size; i++) {
+ uint32_t index = (self->parent_cache_start + i) % PARENT_CACHE_CAPACITY;
+ ParentCacheEntry *entry = &self->parent_cache[index];
+ if (entry->child == node->id) {
+ return ts_node_new(self, entry->parent, entry->position, entry->alias_symbol);
+ }
+ }
+ return ts_node_new(NULL, NULL, length_zero(), 0);
+}
+
+void ts_tree_set_cached_parent(const TSTree *_self, const TSNode *node, const TSNode *parent) {
+ TSTree *self = (TSTree *)_self;
+ if (!self->parent_cache) {
+ self->parent_cache = ts_calloc(PARENT_CACHE_CAPACITY, sizeof(ParentCacheEntry));
+ }
+
+ uint32_t index = (self->parent_cache_start + self->parent_cache_size) % PARENT_CACHE_CAPACITY;
+ self->parent_cache[index] = (ParentCacheEntry) {
+ .child = node->id,
+ .parent = (const Subtree *)parent->id,
+ .position = {
+ parent->context[0],
+ {parent->context[1], parent->context[2]}
+ },
+ .alias_symbol = parent->context[3],
+ };
+
+ if (self->parent_cache_size == PARENT_CACHE_CAPACITY) {
+ self->parent_cache_start++;
+ } else {
+ self->parent_cache_size++;
+ }
+}
diff --git a/src/tree_sitter/tree.h b/src/tree_sitter/tree.h
new file mode 100644
index 0000000000..92a7e64179
--- /dev/null
+++ b/src/tree_sitter/tree.h
@@ -0,0 +1,34 @@
+#ifndef TREE_SITTER_TREE_H_
+#define TREE_SITTER_TREE_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+ const Subtree *child;
+ const Subtree *parent;
+ Length position;
+ TSSymbol alias_symbol;
+} ParentCacheEntry;
+
+struct TSTree {
+ Subtree root;
+ const TSLanguage *language;
+ ParentCacheEntry *parent_cache;
+ uint32_t parent_cache_start;
+ uint32_t parent_cache_size;
+ TSRange *included_ranges;
+ unsigned included_range_count;
+};
+
+TSTree *ts_tree_new(Subtree root, const TSLanguage *language, const TSRange *, unsigned);
+TSNode ts_node_new(const TSTree *, const Subtree *, Length, TSSymbol);
+TSNode ts_tree_get_cached_parent(const TSTree *, const TSNode *);
+void ts_tree_set_cached_parent(const TSTree *, const TSNode *, const TSNode *);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_TREE_H_
diff --git a/src/tree_sitter/tree_cursor.c b/src/tree_sitter/tree_cursor.c
new file mode 100644
index 0000000000..7103fc411d
--- /dev/null
+++ b/src/tree_sitter/tree_cursor.c
@@ -0,0 +1,302 @@
+#include "tree_sitter/api.h"
+#include "./alloc.h"
+#include "./tree_cursor.h"
+#include "./language.h"
+#include "./tree.h"
+
+typedef struct {
+ Subtree parent;
+ const TSTree *tree;
+ Length position;
+ uint32_t child_index;
+ uint32_t structural_child_index;
+ const TSSymbol *alias_sequence;
+} CursorChildIterator;
+
+// CursorChildIterator
+
+static inline CursorChildIterator ts_tree_cursor_iterate_children(const TreeCursor *self) {
+ TreeCursorEntry *last_entry = array_back(&self->stack);
+ if (ts_subtree_child_count(*last_entry->subtree) == 0) {
+ return (CursorChildIterator) {NULL_SUBTREE, self->tree, length_zero(), 0, 0, NULL};
+ }
+ const TSSymbol *alias_sequence = ts_language_alias_sequence(
+ self->tree->language,
+ last_entry->subtree->ptr->production_id
+ );
+ return (CursorChildIterator) {
+ .tree = self->tree,
+ .parent = *last_entry->subtree,
+ .position = last_entry->position,
+ .child_index = 0,
+ .structural_child_index = 0,
+ .alias_sequence = alias_sequence,
+ };
+}
+
+static inline bool ts_tree_cursor_child_iterator_next(CursorChildIterator *self,
+ TreeCursorEntry *result,
+ bool *visible) {
+ if (!self->parent.ptr || self->child_index == self->parent.ptr->child_count) return false;
+ const Subtree *child = &self->parent.ptr->children[self->child_index];
+ *result = (TreeCursorEntry) {
+ .subtree = child,
+ .position = self->position,
+ .child_index = self->child_index,
+ .structural_child_index = self->structural_child_index,
+ };
+ *visible = ts_subtree_visible(*child);
+ bool extra = ts_subtree_extra(*child);
+ if (!extra && self->alias_sequence) {
+ *visible |= self->alias_sequence[self->structural_child_index];
+ self->structural_child_index++;
+ }
+
+ self->position = length_add(self->position, ts_subtree_size(*child));
+ self->child_index++;
+
+ if (self->child_index < self->parent.ptr->child_count) {
+ Subtree next_child = self->parent.ptr->children[self->child_index];
+ self->position = length_add(self->position, ts_subtree_padding(next_child));
+ }
+
+ return true;
+}
+
+// TSTreeCursor - lifecycle
+
+TSTreeCursor ts_tree_cursor_new(TSNode node) {
+ TSTreeCursor self = {NULL, NULL, {0, 0}};
+ ts_tree_cursor_init((TreeCursor *)&self, node);
+ return self;
+}
+
+void ts_tree_cursor_reset(TSTreeCursor *_self, TSNode node) {
+ ts_tree_cursor_init((TreeCursor *)_self, node);
+}
+
+void ts_tree_cursor_init(TreeCursor *self, TSNode node) {
+ self->tree = node.tree;
+ array_clear(&self->stack);
+ array_push(&self->stack, ((TreeCursorEntry) {
+ .subtree = (const Subtree *)node.id,
+ .position = {
+ ts_node_start_byte(node),
+ ts_node_start_point(node)
+ },
+ .child_index = 0,
+ .structural_child_index = 0,
+ }));
+}
+
+void ts_tree_cursor_delete(TSTreeCursor *_self) {
+ TreeCursor *self = (TreeCursor *)_self;
+ array_delete(&self->stack);
+}
+
+// TSTreeCursor - walking the tree
+
+bool ts_tree_cursor_goto_first_child(TSTreeCursor *_self) {
+ TreeCursor *self = (TreeCursor *)_self;
+
+ bool did_descend;
+ do {
+ did_descend = false;
+
+ bool visible;
+ TreeCursorEntry entry;
+ CursorChildIterator iterator = ts_tree_cursor_iterate_children(self);
+ while (ts_tree_cursor_child_iterator_next(&iterator, &entry, &visible)) {
+ if (visible) {
+ array_push(&self->stack, entry);
+ return true;
+ }
+
+ if (ts_subtree_visible_child_count(*entry.subtree) > 0) {
+ array_push(&self->stack, entry);
+ did_descend = true;
+ break;
+ }
+ }
+ } while (did_descend);
+
+ return false;
+}
+
+int64_t ts_tree_cursor_goto_first_child_for_byte(TSTreeCursor *_self, uint32_t goal_byte) {
+ TreeCursor *self = (TreeCursor *)_self;
+ uint32_t initial_size = self->stack.size;
+ uint32_t visible_child_index = 0;
+
+ bool did_descend;
+ do {
+ did_descend = false;
+
+ bool visible;
+ TreeCursorEntry entry;
+ CursorChildIterator iterator = ts_tree_cursor_iterate_children(self);
+ while (ts_tree_cursor_child_iterator_next(&iterator, &entry, &visible)) {
+ uint32_t end_byte = entry.position.bytes + ts_subtree_size(*entry.subtree).bytes;
+ bool at_goal = end_byte > goal_byte;
+ uint32_t visible_child_count = ts_subtree_visible_child_count(*entry.subtree);
+
+ if (at_goal) {
+ if (visible) {
+ array_push(&self->stack, entry);
+ return visible_child_index;
+ }
+
+ if (visible_child_count > 0) {
+ array_push(&self->stack, entry);
+ did_descend = true;
+ break;
+ }
+ } else if (visible) {
+ visible_child_index++;
+ } else {
+ visible_child_index += visible_child_count;
+ }
+ }
+ } while (did_descend);
+
+ if (self->stack.size > initial_size &&
+ ts_tree_cursor_goto_next_sibling((TSTreeCursor *)self)) {
+ return visible_child_index;
+ }
+
+ self->stack.size = initial_size;
+ return -1;
+}
+
+bool ts_tree_cursor_goto_next_sibling(TSTreeCursor *_self) {
+ TreeCursor *self = (TreeCursor *)_self;
+ uint32_t initial_size = self->stack.size;
+
+ while (self->stack.size > 1) {
+ TreeCursorEntry entry = array_pop(&self->stack);
+ CursorChildIterator iterator = ts_tree_cursor_iterate_children(self);
+ iterator.child_index = entry.child_index;
+ iterator.structural_child_index = entry.structural_child_index;
+ iterator.position = entry.position;
+
+ bool visible = false;
+ ts_tree_cursor_child_iterator_next(&iterator, &entry, &visible);
+ if (visible && self->stack.size + 1 < initial_size) break;
+
+ while (ts_tree_cursor_child_iterator_next(&iterator, &entry, &visible)) {
+ if (visible) {
+ array_push(&self->stack, entry);
+ return true;
+ }
+
+ if (ts_subtree_visible_child_count(*entry.subtree)) {
+ array_push(&self->stack, entry);
+ ts_tree_cursor_goto_first_child(_self);
+ return true;
+ }
+ }
+ }
+
+ self->stack.size = initial_size;
+ return false;
+}
+
+bool ts_tree_cursor_goto_parent(TSTreeCursor *_self) {
+ TreeCursor *self = (TreeCursor *)_self;
+ for (unsigned i = self->stack.size - 2; i + 1 > 0; i--) {
+ TreeCursorEntry *entry = &self->stack.contents[i];
+ bool is_aliased = false;
+ if (i > 0) {
+ TreeCursorEntry *parent_entry = &self->stack.contents[i - 1];
+ const TSSymbol *alias_sequence = ts_language_alias_sequence(
+ self->tree->language,
+ parent_entry->subtree->ptr->production_id
+ );
+ is_aliased = alias_sequence && alias_sequence[entry->structural_child_index];
+ }
+ if (ts_subtree_visible(*entry->subtree) || is_aliased) {
+ self->stack.size = i + 1;
+ return true;
+ }
+ }
+ return false;
+}
+
+TSNode ts_tree_cursor_current_node(const TSTreeCursor *_self) {
+ const TreeCursor *self = (const TreeCursor *)_self;
+ TreeCursorEntry *last_entry = array_back(&self->stack);
+ TSSymbol alias_symbol = 0;
+ if (self->stack.size > 1) {
+ TreeCursorEntry *parent_entry = &self->stack.contents[self->stack.size - 2];
+ const TSSymbol *alias_sequence = ts_language_alias_sequence(
+ self->tree->language,
+ parent_entry->subtree->ptr->production_id
+ );
+ if (alias_sequence && !ts_subtree_extra(*last_entry->subtree)) {
+ alias_symbol = alias_sequence[last_entry->structural_child_index];
+ }
+ }
+ return ts_node_new(
+ self->tree,
+ last_entry->subtree,
+ last_entry->position,
+ alias_symbol
+ );
+}
+
+TSFieldId ts_tree_cursor_current_field_id(const TSTreeCursor *_self) {
+ const TreeCursor *self = (const TreeCursor *)_self;
+
+ // Walk up the tree, visiting the current node and its invisible ancestors.
+ for (unsigned i = self->stack.size - 1; i > 0; i--) {
+ TreeCursorEntry *entry = &self->stack.contents[i];
+ TreeCursorEntry *parent_entry = &self->stack.contents[i - 1];
+
+ // Stop walking up when another visible node is found.
+ if (i != self->stack.size - 1) {
+ if (ts_subtree_visible(*entry->subtree)) break;
+ const TSSymbol *alias_sequence = ts_language_alias_sequence(
+ self->tree->language,
+ parent_entry->subtree->ptr->production_id
+ );
+ if (alias_sequence && alias_sequence[entry->structural_child_index]) {
+ break;
+ }
+ }
+
+ const TSFieldMapEntry *field_map, *field_map_end;
+ ts_language_field_map(
+ self->tree->language,
+ parent_entry->subtree->ptr->production_id,
+ &field_map, &field_map_end
+ );
+
+ while (field_map < field_map_end) {
+ if (
+ !field_map->inherited &&
+ field_map->child_index == entry->structural_child_index
+ ) return field_map->field_id;
+ field_map++;
+ }
+ }
+ return 0;
+}
+
+const char *ts_tree_cursor_current_field_name(const TSTreeCursor *_self) {
+ TSFieldId id = ts_tree_cursor_current_field_id(_self);
+ if (id) {
+ const TreeCursor *self = (const TreeCursor *)_self;
+ return self->tree->language->field_names[id];
+ } else {
+ return NULL;
+ }
+}
+
+TSTreeCursor ts_tree_cursor_copy(const TSTreeCursor *_cursor) {
+ const TreeCursor *cursor = (const TreeCursor *)_cursor;
+ TSTreeCursor res = {NULL, NULL, {0, 0}};
+ TreeCursor *copy = (TreeCursor *)&res;
+ copy->tree = cursor->tree;
+ array_push_all(&copy->stack, &cursor->stack);
+ return res;
+}
diff --git a/src/tree_sitter/tree_cursor.h b/src/tree_sitter/tree_cursor.h
new file mode 100644
index 0000000000..55bdad86da
--- /dev/null
+++ b/src/tree_sitter/tree_cursor.h
@@ -0,0 +1,20 @@
+#ifndef TREE_SITTER_TREE_CURSOR_H_
+#define TREE_SITTER_TREE_CURSOR_H_
+
+#include "./subtree.h"
+
+typedef struct {
+ const Subtree *subtree;
+ Length position;
+ uint32_t child_index;
+ uint32_t structural_child_index;
+} TreeCursorEntry;
+
+typedef struct {
+ const TSTree *tree;
+ Array(TreeCursorEntry) stack;
+} TreeCursor;
+
+void ts_tree_cursor_init(TreeCursor *, TSNode);
+
+#endif // TREE_SITTER_TREE_CURSOR_H_
diff --git a/src/tree_sitter/utf16.c b/src/tree_sitter/utf16.c
new file mode 100644
index 0000000000..3956c01cb9
--- /dev/null
+++ b/src/tree_sitter/utf16.c
@@ -0,0 +1,33 @@
+#include "./utf16.h"
+
+utf8proc_ssize_t utf16_iterate(
+ const utf8proc_uint8_t *string,
+ utf8proc_ssize_t length,
+ utf8proc_int32_t *code_point
+) {
+ if (length < 2) {
+ *code_point = -1;
+ return 0;
+ }
+
+ uint16_t *units = (uint16_t *)string;
+ uint16_t unit = units[0];
+
+ if (unit < 0xd800 || unit >= 0xe000) {
+ *code_point = unit;
+ return 2;
+ }
+
+ if (unit < 0xdc00) {
+ if (length >= 4) {
+ uint16_t next_unit = units[1];
+ if (next_unit >= 0xdc00 && next_unit < 0xe000) {
+ *code_point = 0x10000 + ((unit - 0xd800) << 10) + (next_unit - 0xdc00);
+ return 4;
+ }
+ }
+ }
+
+ *code_point = -1;
+ return 2;
+}
diff --git a/src/tree_sitter/utf16.h b/src/tree_sitter/utf16.h
new file mode 100644
index 0000000000..32fd05e6db
--- /dev/null
+++ b/src/tree_sitter/utf16.h
@@ -0,0 +1,21 @@
+#ifndef TREE_SITTER_UTF16_H_
+#define TREE_SITTER_UTF16_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdint.h>
+#include <stdlib.h>
+#include "utf8proc.h"
+
+// Analogous to utf8proc's utf8proc_iterate function. Reads one code point from
+// the given UTF16 string and stores it in the location pointed to by `code_point`.
+// Returns the number of bytes in `string` that were read.
+utf8proc_ssize_t utf16_iterate(const utf8proc_uint8_t *, utf8proc_ssize_t, utf8proc_int32_t *);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_UTF16_H_
diff --git a/test/README.md b/test/README.md
index 64892b5576..a6e9080a40 100644
--- a/test/README.md
+++ b/test/README.md
@@ -77,11 +77,14 @@ To run all legacy Vim tests:
make oldtest
-To run a *single* legacy test set `TEST_FILE`, for example:
+To run a *single* legacy test file you can use either:
- TEST_FILE=test_syntax.res make oldtest
+ make oldtest TEST_FILE=test_syntax.vim
+
+or:
+
+ make src/nvim/testdir/test_syntax.vim
-- The `.res` extension (instead of `.vim`) is required.
- Specify only the test file name, not the full path.
@@ -126,7 +129,7 @@ end)
To run only test with filter name:
- TEST_TAG='foo.*api' make functionaltest
+ TEST_FILTER='foo.*api' make functionaltest
### Filter by file
@@ -315,11 +318,12 @@ Number; !must be defined to function properly):
- `NVIM_TEST_RUN_TESTTEST` (U) (1): allows running
`test/unit/testtest_spec.lua` used to check how testing infrastructure works.
-- `NVIM_TEST_TRACE_LEVEL` (U) (N): specifies unit tests tracing level: `0`
- disables tracing (the fastest, but you get no data if tests crash and there
- was no core dump generated), `1` or empty/undefined leaves only C function
- cals and returns in the trace (faster then recording everything), `2` records
- all function calls, returns and lua source lines exuecuted.
+- `NVIM_TEST_TRACE_LEVEL` (U) (N): specifies unit tests tracing level:
+ - `0` disables tracing (the fastest, but you get no data if tests crash and
+ there no core dump was generated),
+ - `1` leaves only C function calls and returns in the trace (faster than
+ recording everything),
+ - `2` records all function calls, returns and executed Lua source lines.
- `NVIM_TEST_TRACE_ON_ERROR` (U) (1): makes unit tests yield trace on error in
addition to regular error message.
diff --git a/test/busted/outputHandlers/nvim.lua b/test/busted/outputHandlers/nvim.lua
index 1b500fc999..8f3aad776e 100644
--- a/test/busted/outputHandlers/nvim.lua
+++ b/test/busted/outputHandlers/nvim.lua
@@ -227,8 +227,12 @@ return function(options)
return nil, true
end
+ local function write_status(element, string)
+ io.write(timeString:format(getElapsedTime(element)) .. ' ' .. string)
+ io.flush()
+ end
+
handler.testEnd = function(element, _parent, status, _debug)
- local elapsedTime_ms = getElapsedTime(element)
local string
fileTestCount = fileTestCount + 1
@@ -241,45 +245,21 @@ return function(options)
string = skippedString
elseif status == 'failure' then
failureCount = failureCount + 1
- string = nil
+ string = failureString .. failureDescription(handler.failures[#handler.failures])
elseif status == 'error' then
errorCount = errorCount + 1
- string = nil
- end
-
- if string ~= nil then
- if elapsedTime_ms == elapsedTime_ms then
- string = timeString:format(elapsedTime_ms) .. ' ' .. string
- end
- io.write(string)
- io.flush()
+ string = errorString .. failureDescription(handler.errors[#handler.errors])
+ else
+ string = "unexpected test status! ("..status..")"
end
+ write_status(element, string)
return nil, true
end
- handler.testFailure = function(_element, _parent, _message, _debug)
- io.write(failureString)
- io.flush()
-
- io.write(failureDescription(handler.failures[#handler.failures]))
- io.flush()
- return nil, true
- end
-
- handler.testError = function(_element, _parent, _message, _debug)
- io.write(errorString)
- io.flush()
-
- io.write(failureDescription(handler.errors[#handler.errors]))
- io.flush()
- return nil, true
- end
-
handler.error = function(element, _parent, _message, _debug)
if element.descriptor ~= 'it' then
- io.write(failureDescription(handler.errors[#handler.errors]))
- io.flush()
+ write_status(element, failureDescription(handler.errors[#handler.errors]))
errorCount = errorCount + 1
end
@@ -293,8 +273,6 @@ return function(options)
busted.subscribe({ 'file', 'end' }, handler.fileEnd)
busted.subscribe({ 'test', 'start' }, handler.testStart, { predicate = handler.cancelOnPending })
busted.subscribe({ 'test', 'end' }, handler.testEnd, { predicate = handler.cancelOnPending })
- busted.subscribe({ 'failure', 'it' }, handler.testFailure)
- busted.subscribe({ 'error', 'it' }, handler.testError)
busted.subscribe({ 'failure' }, handler.error)
busted.subscribe({ 'error' }, handler.error)
diff --git a/test/functional/api/keymap_spec.lua b/test/functional/api/keymap_spec.lua
index 773207d360..210394c83f 100644
--- a/test/functional/api/keymap_spec.lua
+++ b/test/functional/api/keymap_spec.lua
@@ -585,13 +585,13 @@ describe('nvim_set_keymap, nvim_del_keymap', function()
end)
it('can set <expr> mappings whose RHS change dynamically', function()
- meths.command_output([[
+ meths.exec([[
function! FlipFlop() abort
if !exists('g:flip') | let g:flip = 0 | endif
let g:flip = !g:flip
return g:flip
endfunction
- ]])
+ ]], true)
eq(1, meths.call_function('FlipFlop', {}))
eq(0, meths.call_function('FlipFlop', {}))
eq(1, meths.call_function('FlipFlop', {}))
diff --git a/test/functional/api/mark_extended_spec.lua b/test/functional/api/mark_extended_spec.lua
new file mode 100644
index 0000000000..bf910568b1
--- /dev/null
+++ b/test/functional/api/mark_extended_spec.lua
@@ -0,0 +1,1389 @@
+local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
+
+local request = helpers.request
+local eq = helpers.eq
+local ok = helpers.ok
+local curbufmeths = helpers.curbufmeths
+local bufmeths = helpers.bufmeths
+local pcall_err = helpers.pcall_err
+local insert = helpers.insert
+local feed = helpers.feed
+local clear = helpers.clear
+local command = helpers.command
+
+local function check_undo_redo(ns, mark, sr, sc, er, ec) --s = start, e = end
+ local rv = curbufmeths.get_extmark_by_id(ns, mark)
+ eq({er, ec}, rv)
+ feed("u")
+ rv = curbufmeths.get_extmark_by_id(ns, mark)
+ eq({sr, sc}, rv)
+ feed("<c-r>")
+ rv = curbufmeths.get_extmark_by_id(ns, mark)
+ eq({er, ec}, rv)
+end
+
+local function set_extmark(ns_id, id, line, col, opts)
+ if opts == nil then
+ opts = {}
+ end
+ return curbufmeths.set_extmark(ns_id, id, line, col, opts)
+end
+
+local function get_extmarks(ns_id, start, end_, opts)
+ if opts == nil then
+ opts = {}
+ end
+ return curbufmeths.get_extmarks(ns_id, start, end_, opts)
+end
+
+describe('API/extmarks', function()
+ local screen
+ local marks, positions, ns_string2, ns_string, init_text, row, col
+ local ns, ns2
+
+ before_each(function()
+ -- Initialize some namespaces and insert 12345 into a buffer
+ marks = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
+ positions = {{0, 0,}, {0, 2}, {0, 3}}
+
+ ns_string = "my-fancy-plugin"
+ ns_string2 = "my-fancy-plugin2"
+ init_text = "12345"
+ row = 0
+ col = 2
+
+ clear()
+ screen = Screen.new(15, 10)
+ screen:attach()
+
+ insert(init_text)
+ ns = request('nvim_create_namespace', ns_string)
+ ns2 = request('nvim_create_namespace', ns_string2)
+ end)
+
+ it('adds, updates and deletes marks #extmarks', function()
+ local rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ eq(marks[1], rv)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({positions[1][1], positions[1][2]}, rv)
+ -- Test adding a second mark on same row works
+ rv = set_extmark(ns, marks[2], positions[2][1], positions[2][2])
+ eq(marks[2], rv)
+
+ -- Test an update, (same pos)
+ rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ eq(marks[1], rv)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[2])
+ eq({positions[2][1], positions[2][2]}, rv)
+ -- Test an update, (new pos)
+ row = positions[1][1]
+ col = positions[1][2] + 1
+ rv = set_extmark(ns, marks[1], row, col)
+ eq(marks[1], rv)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({row, col}, rv)
+
+ -- remove the test marks
+ eq(true, curbufmeths.del_extmark(ns, marks[1]))
+ eq(false, curbufmeths.del_extmark(ns, marks[1]))
+ eq(true, curbufmeths.del_extmark(ns, marks[2]))
+ eq(false, curbufmeths.del_extmark(ns, marks[3]))
+ eq(false, curbufmeths.del_extmark(ns, 1000))
+ end)
+
+ it('can clear a specific namespace range #extmarks', function()
+ set_extmark(ns, 1, 0, 1)
+ set_extmark(ns2, 1, 0, 1)
+ -- force a new undo buffer
+ feed('o<esc>')
+ curbufmeths.clear_namespace(ns2, 0, -1)
+ eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ feed('u')
+ eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({{1, 0, 1}}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ feed('<c-r>')
+ eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ end)
+
+ it('can clear a namespace range using 0,-1 #extmarks', function()
+ set_extmark(ns, 1, 0, 1)
+ set_extmark(ns2, 1, 0, 1)
+ -- force a new undo buffer
+ feed('o<esc>')
+ curbufmeths.clear_namespace(-1, 0, -1)
+ eq({}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ feed('u')
+ eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({{1, 0, 1}}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ feed('<c-r>')
+ eq({}, get_extmarks(ns, {0, 0}, {-1, -1}))
+ eq({}, get_extmarks(ns2, {0, 0}, {-1, -1}))
+ end)
+
+ it('querying for information and ranges #extmarks', function()
+ -- add some more marks
+ for i, m in ipairs(marks) do
+ if positions[i] ~= nil then
+ local rv = set_extmark(ns, m, positions[i][1], positions[i][2])
+ eq(m, rv)
+ end
+ end
+
+ -- {0, 0} and {-1, -1} work as extreme values
+ eq({{1, 0, 0}}, get_extmarks(ns, {0, 0}, {0, 0}))
+ eq({}, get_extmarks(ns, {-1, -1}, {-1, -1}))
+ local rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ for i, m in ipairs(marks) do
+ if positions[i] ~= nil then
+ eq({m, positions[i][1], positions[i][2]}, rv[i])
+ end
+ end
+
+ -- 0 and -1 works as short hand extreme values
+ eq({{1, 0, 0}}, get_extmarks(ns, 0, 0))
+ eq({}, get_extmarks(ns, -1, -1))
+ rv = get_extmarks(ns, 0, -1)
+ for i, m in ipairs(marks) do
+ if positions[i] ~= nil then
+ eq({m, positions[i][1], positions[i][2]}, rv[i])
+ end
+ end
+
+ -- next with mark id
+ rv = get_extmarks(ns, marks[1], {-1, -1}, {limit=1})
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+ rv = get_extmarks(ns, marks[2], {-1, -1}, {limit=1})
+ eq({{marks[2], positions[2][1], positions[2][2]}}, rv)
+ -- next with positional when mark exists at position
+ rv = get_extmarks(ns, positions[1], {-1, -1}, {limit=1})
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+ -- next with positional index (no mark at position)
+ rv = get_extmarks(ns, {positions[1][1], positions[1][2] +1}, {-1, -1}, {limit=1})
+ eq({{marks[2], positions[2][1], positions[2][2]}}, rv)
+ -- next with Extremity index
+ rv = get_extmarks(ns, {0,0}, {-1, -1}, {limit=1})
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+
+ -- nextrange with mark id
+ rv = get_extmarks(ns, marks[1], marks[3])
+ eq({marks[1], positions[1][1], positions[1][2]}, rv[1])
+ eq({marks[2], positions[2][1], positions[2][2]}, rv[2])
+ -- nextrange with `limit`
+ rv = get_extmarks(ns, marks[1], marks[3], {limit=2})
+ eq(2, table.getn(rv))
+ -- nextrange with positional when mark exists at position
+ rv = get_extmarks(ns, positions[1], positions[3])
+ eq({marks[1], positions[1][1], positions[1][2]}, rv[1])
+ eq({marks[2], positions[2][1], positions[2][2]}, rv[2])
+ rv = get_extmarks(ns, positions[2], positions[3])
+ eq(2, table.getn(rv))
+ -- nextrange with positional index (no mark at position)
+ local lower = {positions[1][1], positions[2][2] -1}
+ local upper = {positions[2][1], positions[3][2] - 1}
+ rv = get_extmarks(ns, lower, upper)
+ eq({{marks[2], positions[2][1], positions[2][2]}}, rv)
+ lower = {positions[3][1], positions[3][2] + 1}
+ upper = {positions[3][1], positions[3][2] + 2}
+ rv = get_extmarks(ns, lower, upper)
+ eq({}, rv)
+ -- nextrange with extremity index
+ lower = {positions[2][1], positions[2][2]+1}
+ upper = {-1, -1}
+ rv = get_extmarks(ns, lower, upper)
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+
+ -- prev with mark id
+ rv = get_extmarks(ns, marks[3], {0, 0}, {limit=1})
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+ rv = get_extmarks(ns, marks[2], {0, 0}, {limit=1})
+ eq({{marks[2], positions[2][1], positions[2][2]}}, rv)
+ -- prev with positional when mark exists at position
+ rv = get_extmarks(ns, positions[3], {0, 0}, {limit=1})
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+ -- prev with positional index (no mark at position)
+ rv = get_extmarks(ns, {positions[1][1], positions[1][2] +1}, {0, 0}, {limit=1})
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+ -- prev with Extremity index
+ rv = get_extmarks(ns, {-1,-1}, {0,0}, {limit=1})
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+
+ -- prevrange with mark id
+ rv = get_extmarks(ns, marks[3], marks[1])
+ eq({marks[3], positions[3][1], positions[3][2]}, rv[1])
+ eq({marks[2], positions[2][1], positions[2][2]}, rv[2])
+ eq({marks[1], positions[1][1], positions[1][2]}, rv[3])
+ -- prevrange with limit
+ rv = get_extmarks(ns, marks[3], marks[1], {limit=2})
+ eq(2, table.getn(rv))
+ -- prevrange with positional when mark exists at position
+ rv = get_extmarks(ns, positions[3], positions[1])
+ eq({{marks[3], positions[3][1], positions[3][2]},
+ {marks[2], positions[2][1], positions[2][2]},
+ {marks[1], positions[1][1], positions[1][2]}}, rv)
+ rv = get_extmarks(ns, positions[2], positions[1])
+ eq(2, table.getn(rv))
+ -- prevrange with positional index (no mark at position)
+ lower = {positions[2][1], positions[2][2] + 1}
+ upper = {positions[3][1], positions[3][2] + 1}
+ rv = get_extmarks(ns, upper, lower)
+ eq({{marks[3], positions[3][1], positions[3][2]}}, rv)
+ lower = {positions[3][1], positions[3][2] + 1}
+ upper = {positions[3][1], positions[3][2] + 2}
+ rv = get_extmarks(ns, upper, lower)
+ eq({}, rv)
+ -- prevrange with extremity index
+ lower = {0,0}
+ upper = {positions[2][1], positions[2][2] - 1}
+ rv = get_extmarks(ns, upper, lower)
+ eq({{marks[1], positions[1][1], positions[1][2]}}, rv)
+ end)
+
+ it('querying for information with limit #extmarks', function()
+ -- add some more marks
+ for i, m in ipairs(marks) do
+ if positions[i] ~= nil then
+ local rv = set_extmark(ns, m, positions[i][1], positions[i][2])
+ eq(m, rv)
+ end
+ end
+
+ local rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=2})
+ eq(2, table.getn(rv))
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=3})
+ eq(3, table.getn(rv))
+
+ -- now in reverse
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=2})
+ eq(2, table.getn(rv))
+ rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=3})
+ eq(3, table.getn(rv))
+ end)
+
+ it('get_marks works when mark col > upper col #extmarks', function()
+ feed('A<cr>12345<esc>')
+ feed('A<cr>12345<esc>')
+ set_extmark(ns, 10, 0, 2) -- this shouldn't be found
+ set_extmark(ns, 11, 2, 1) -- this shouldn't be found
+ set_extmark(ns, marks[1], 0, 4) -- check col > our upper bound
+ set_extmark(ns, marks[2], 1, 1) -- check col < lower bound
+ set_extmark(ns, marks[3], 2, 0) -- check is inclusive
+ eq({{marks[1], 0, 4},
+ {marks[2], 1, 1},
+ {marks[3], 2, 0}},
+ get_extmarks(ns, {0, 3}, {2, 0}))
+ end)
+
+ it('get_marks works in reverse when mark col < lower col #extmarks', function()
+ feed('A<cr>12345<esc>')
+ feed('A<cr>12345<esc>')
+ set_extmark(ns, 10, 0, 1) -- this shouldn't be found
+ set_extmark(ns, 11, 2, 4) -- this shouldn't be found
+ set_extmark(ns, marks[1], 2, 1) -- check col < our lower bound
+ set_extmark(ns, marks[2], 1, 4) -- check col > upper bound
+ set_extmark(ns, marks[3], 0, 2) -- check is inclusive
+ local rv = get_extmarks(ns, {2, 3}, {0, 2})
+ eq({{marks[1], 2, 1},
+ {marks[2], 1, 4},
+ {marks[3], 0, 2}},
+ rv)
+ end)
+
+ it('get_marks limit=0 returns nothing #extmarks', function()
+ set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ local rv = get_extmarks(ns, {-1, -1}, {-1, -1}, {limit=0})
+ eq({}, rv)
+ end)
+
+
+ it('marks move with line insertations #extmarks', function()
+ set_extmark(ns, marks[1], 0, 0)
+ feed("yyP")
+ check_undo_redo(ns, marks[1], 0, 0, 1, 0)
+ end)
+
+ it('marks move with multiline insertations #extmarks', function()
+ feed("a<cr>22<cr>33<esc>")
+ set_extmark(ns, marks[1], 1, 1)
+ feed('ggVGyP')
+ check_undo_redo(ns, marks[1], 1, 1, 4, 1)
+ end)
+
+ it('marks move with line join #extmarks', function()
+ -- do_join in ops.c
+ feed("a<cr>222<esc>")
+ set_extmark(ns, marks[1], 1, 0)
+ feed('ggJ')
+ check_undo_redo(ns, marks[1], 1, 0, 0, 6)
+ end)
+
+ it('join works when no marks are present #extmarks', function()
+ feed("a<cr>1<esc>")
+ feed('kJ')
+ -- This shouldn't seg fault
+ screen:expect([[
+ 12345^ 1 |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ |
+ ]])
+ end)
+
+ it('marks move with multiline join #extmarks', function()
+ -- do_join in ops.c
+ feed("a<cr>222<cr>333<cr>444<esc>")
+ set_extmark(ns, marks[1], 3, 0)
+ feed('2GVGJ')
+ check_undo_redo(ns, marks[1], 3, 0, 1, 8)
+ end)
+
+ it('marks move with line deletes #extmarks', function()
+ feed("a<cr>222<cr>333<cr>444<esc>")
+ set_extmark(ns, marks[1], 2, 1)
+ feed('ggjdd')
+ check_undo_redo(ns, marks[1], 2, 1, 1, 1)
+ end)
+
+ it('marks move with multiline deletes #extmarks', function()
+ feed("a<cr>222<cr>333<cr>444<esc>")
+ set_extmark(ns, marks[1], 3, 0)
+ feed('gg2dd')
+ check_undo_redo(ns, marks[1], 3, 0, 1, 0)
+ -- regression test, undoing multiline delete when mark is on row 1
+ feed('ugg3dd')
+ check_undo_redo(ns, marks[1], 3, 0, 0, 0)
+ end)
+
+ it('marks move with open line #extmarks', function()
+ -- open_line in misc1.c
+ -- testing marks below are also moved
+ feed("yyP")
+ set_extmark(ns, marks[1], 0, 4)
+ set_extmark(ns, marks[2], 1, 4)
+ feed('1G<s-o><esc>')
+ check_undo_redo(ns, marks[1], 0, 4, 1, 4)
+ check_undo_redo(ns, marks[2], 1, 4, 2, 4)
+ feed('2Go<esc>')
+ check_undo_redo(ns, marks[1], 1, 4, 1, 4)
+ check_undo_redo(ns, marks[2], 2, 4, 3, 4)
+ end)
+
+ it('marks move with char inserts #extmarks', function()
+ -- insertchar in edit.c (the ins_str branch)
+ set_extmark(ns, marks[1], 0, 3)
+ feed('0')
+ insert('abc')
+ screen:expect([[
+ ab^c12345 |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ |
+ ]])
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, 6}, rv)
+ -- check_undo_redo(ns, marks[1], 0, 2, 0, 5)
+ end)
+
+ -- gravity right as definted in tk library
+ it('marks have gravity right #extmarks', function()
+ -- insertchar in edit.c (the ins_str branch)
+ set_extmark(ns, marks[1], 0, 2)
+ feed('03l')
+ insert("X")
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+
+ -- check multibyte chars
+ feed('03l<esc>')
+ insert("~~")
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ end)
+
+ it('we can insert multibyte chars #extmarks', function()
+ -- insertchar in edit.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 2)
+ -- Insert a fullwidth (two col) tilde, NICE
+ feed('0i~<esc>')
+ check_undo_redo(ns, marks[1], 1, 2, 1, 5)
+ end)
+
+ it('marks move with blockwise inserts #extmarks', function()
+ -- op_insert in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 2)
+ feed('0<c-v>lkI9<esc>')
+ check_undo_redo(ns, marks[1], 1, 2, 1, 3)
+ end)
+
+ it('marks move with line splits (using enter) #extmarks', function()
+ -- open_line in misc1.c
+ -- testing marks below are also moved
+ feed("yyP")
+ set_extmark(ns, marks[1], 0, 4)
+ set_extmark(ns, marks[2], 1, 4)
+ feed('1Gla<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 4, 1, 2)
+ check_undo_redo(ns, marks[2], 1, 4, 2, 4)
+ end)
+
+ it('marks at last line move on insert new line #extmarks', function()
+ -- open_line in misc1.c
+ set_extmark(ns, marks[1], 0, 4)
+ feed('0i<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 4, 1, 4)
+ end)
+
+ it('yet again marks move with line splits #extmarks', function()
+ -- the first test above wasn't catching all errors..
+ feed("A67890<esc>")
+ set_extmark(ns, marks[1], 0, 4)
+ feed("04li<cr><esc>")
+ check_undo_redo(ns, marks[1], 0, 4, 1, 0)
+ end)
+
+ it('and one last time line splits... #extmarks', function()
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 2)
+ feed("02li<cr><esc>")
+ check_undo_redo(ns, marks[1], 0, 1, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 2, 1, 0)
+ end)
+
+ it('multiple marks move with mark splits #extmarks', function()
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 3)
+ feed("0li<cr><esc>")
+ check_undo_redo(ns, marks[1], 0, 1, 1, 0)
+ check_undo_redo(ns, marks[2], 0, 3, 1, 2)
+ end)
+
+ it('deleting right before a mark works #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0lx')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 1)
+ end)
+
+ it('deleting on a mark works #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 2)
+ feed('02lx')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ end)
+
+ it('marks move with char deletes #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 2)
+ feed('02dl')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 0)
+ -- from the other side (nothing should happen)
+ feed('$x')
+ check_undo_redo(ns, marks[1], 0, 0, 0, 0)
+ end)
+
+ it('marks move with char deletes over a range #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ feed('0l3dl<esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 1)
+ -- delete 1, nothing should happen to our marks
+ feed('u')
+ feed('$x')
+ check_undo_redo(ns, marks[2], 0, 3, 0, 3)
+ end)
+
+ it('deleting marks at end of line works #extmarks', function()
+ -- mark_extended.c/extmark_col_adjust_delete
+ set_extmark(ns, marks[1], 0, 4)
+ feed('$x')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 4)
+ -- check the copy happened correctly on delete at eol
+ feed('$x')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 3)
+ feed('u')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 4)
+ end)
+
+ it('marks move with blockwise deletes #extmarks', function()
+ -- op_delete in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 4)
+ feed('h<c-v>hhkd')
+ check_undo_redo(ns, marks[1], 1, 4, 1, 1)
+ end)
+
+ it('marks move with blockwise deletes over a range #extmarks', function()
+ -- op_delete in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 1, 2)
+ feed('0<c-v>k3lx')
+ check_undo_redo(ns, marks[1], 0, 1, 0, 0)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 0)
+ check_undo_redo(ns, marks[3], 1, 2, 1, 0)
+ -- delete 1, nothing should happen to our marks
+ feed('u')
+ feed('$<c-v>jx')
+ check_undo_redo(ns, marks[2], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[3], 1, 2, 1, 2)
+ end)
+
+ it('works with char deletes over multilines #extmarks', function()
+ feed('a<cr>12345<cr>test-me<esc>')
+ set_extmark(ns, marks[1], 2, 5)
+ feed('gg')
+ feed('dv?-m?<cr>')
+ check_undo_redo(ns, marks[1], 2, 5, 0, 0)
+ end)
+
+ it('marks outside of deleted range move with visual char deletes #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 3)
+ feed('0vx<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 2)
+
+ feed("u")
+ feed('0vlx<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 1)
+
+ feed("u")
+ feed('0v2lx<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 0)
+
+ -- from the other side (nothing should happen)
+ feed('$vx')
+ check_undo_redo(ns, marks[1], 0, 0, 0, 0)
+ end)
+
+ it('marks outside of deleted range move with char deletes #extmarks', function()
+ -- op_delete in ops.c
+ set_extmark(ns, marks[1], 0, 3)
+ feed('0x<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 2)
+
+ feed("u")
+ feed('02x<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 1)
+
+ feed("u")
+ feed('0v3lx<esc>')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 0)
+
+ -- from the other side (nothing should happen)
+ feed("u")
+ feed('$vx')
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ end)
+
+ it('marks move with P(backward) paste #extmarks', function()
+ -- do_put in ops.c
+ feed('0iabc<esc>')
+ set_extmark(ns, marks[1], 0, 7)
+ feed('0veyP')
+ check_undo_redo(ns, marks[1], 0, 7, 0, 15)
+ end)
+
+ it('marks move with p(forward) paste #extmarks', function()
+ -- do_put in ops.c
+ feed('0iabc<esc>')
+ set_extmark(ns, marks[1], 0, 7)
+ feed('0veyp')
+ check_undo_redo(ns, marks[1], 0, 7, 0, 14)
+ end)
+
+ it('marks move with blockwise P(backward) paste #extmarks', function()
+ -- do_put in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 4)
+ feed('<c-v>hhkyP<esc>')
+ check_undo_redo(ns, marks[1], 1, 4, 1, 7)
+ end)
+
+ it('marks move with blockwise p(forward) paste #extmarks', function()
+ -- do_put in ops.c
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 4)
+ feed('<c-v>hhkyp<esc>')
+ check_undo_redo(ns, marks[1], 1, 4, 1, 6)
+ end)
+
+ it('replace works #extmarks', function()
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0r2')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ end)
+
+ it('blockwise replace works #extmarks', function()
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0<c-v>llkr1<esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ end)
+
+ it('shift line #extmarks', function()
+ -- shift_line in ops.c
+ feed(':set shiftwidth=4<cr><esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0>>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 6)
+
+ feed('>>')
+ check_undo_redo(ns, marks[1], 0, 6, 0, 10)
+
+ feed('<LT><LT>') -- have to escape, same as <<
+ check_undo_redo(ns, marks[1], 0, 10, 0, 6)
+ end)
+
+ it('blockwise shift #extmarks', function()
+ -- shift_block in ops.c
+ feed(':set shiftwidth=4<cr><esc>')
+ feed('a<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 2)
+ feed('0<c-v>k>')
+ check_undo_redo(ns, marks[1], 1, 2, 1, 6)
+ feed('<c-v>j>')
+ check_undo_redo(ns, marks[1], 1, 6, 1, 10)
+
+ feed('<c-v>j<LT>')
+ check_undo_redo(ns, marks[1], 1, 10, 1, 6)
+ end)
+
+ it('tab works with expandtab #extmarks', function()
+ -- ins_tab in edit.c
+ feed(':set expandtab<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0i<tab><tab><esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 6)
+ end)
+
+ it('tabs work #extmarks', function()
+ -- ins_tab in edit.c
+ feed(':set noexpandtab<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ feed(':set softtabstop=2<cr><esc>')
+ feed(':set tabstop=8<cr><esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ feed('0i<tab><esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 4)
+ feed('0iX<tab><esc>')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 6)
+ end)
+
+ it('marks move when using :move #extmarks', function()
+ set_extmark(ns, marks[1], 0, 0)
+ feed('A<cr>2<esc>:1move 2<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 0, 1, 0)
+ -- test codepath when moving lines up
+ feed(':2move 0<cr><esc>')
+ check_undo_redo(ns, marks[1], 1, 0, 0, 0)
+ end)
+
+ it('marks move when using :move part 2 #extmarks', function()
+ -- make sure we didn't get lucky with the math...
+ feed('A<cr>2<cr>3<cr>4<cr>5<cr>6<esc>')
+ set_extmark(ns, marks[1], 1, 0)
+ feed(':2,3move 5<cr><esc>')
+ check_undo_redo(ns, marks[1], 1, 0, 3, 0)
+ -- test codepath when moving lines up
+ feed(':4,5move 1<cr><esc>')
+ check_undo_redo(ns, marks[1], 3, 0, 1, 0)
+ end)
+
+ it('undo and redo of set and unset marks #extmarks', function()
+ -- Force a new undo head
+ feed('o<esc>')
+ set_extmark(ns, marks[1], 0, 1)
+ feed('o<esc>')
+ set_extmark(ns, marks[2], 0, -1)
+ set_extmark(ns, marks[3], 0, -1)
+
+ feed("u")
+ local rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(1, table.getn(rv))
+
+ feed("<c-r>")
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(3, table.getn(rv))
+
+ -- Test updates
+ feed('o<esc>')
+ set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ rv = get_extmarks(ns, marks[1], marks[1], {limit=1})
+ eq(1, table.getn(rv))
+ feed("u")
+ feed("<c-r>")
+ check_undo_redo(ns, marks[1], 0, 1, positions[1][1], positions[1][2])
+
+ -- Test unset
+ feed('o<esc>')
+ curbufmeths.del_extmark(ns, marks[3])
+ feed("u")
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(3, table.getn(rv))
+ feed("<c-r>")
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(2, table.getn(rv))
+ end)
+
+ it('undo and redo of marks deleted during edits #extmarks', function()
+ -- test extmark_adjust
+ feed('A<cr>12345<esc>')
+ set_extmark(ns, marks[1], 1, 2)
+ feed('dd')
+ check_undo_redo(ns, marks[1], 1, 2, 1, 0)
+ end)
+
+ it('namespaces work properly #extmarks', function()
+ local rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2])
+ eq(1, rv)
+ rv = set_extmark(ns2, marks[1], positions[1][1], positions[1][2])
+ eq(1, rv)
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns2, {0, 0}, {-1, -1})
+ eq(1, table.getn(rv))
+
+ -- Set more marks for testing the ranges
+ set_extmark(ns, marks[2], positions[2][1], positions[2][2])
+ set_extmark(ns, marks[3], positions[3][1], positions[3][2])
+ set_extmark(ns2, marks[2], positions[2][1], positions[2][2])
+ set_extmark(ns2, marks[3], positions[3][1], positions[3][2])
+
+ -- get_next (limit set)
+ rv = get_extmarks(ns, {0, 0}, positions[2], {limit=1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns2, {0, 0}, positions[2], {limit=1})
+ eq(1, table.getn(rv))
+ -- get_prev (limit set)
+ rv = get_extmarks(ns, positions[1], {0, 0}, {limit=1})
+ eq(1, table.getn(rv))
+ rv = get_extmarks(ns2, positions[1], {0, 0}, {limit=1})
+ eq(1, table.getn(rv))
+
+ -- get_next (no limit)
+ rv = get_extmarks(ns, positions[1], positions[2])
+ eq(2, table.getn(rv))
+ rv = get_extmarks(ns2, positions[1], positions[2])
+ eq(2, table.getn(rv))
+ -- get_prev (no limit)
+ rv = get_extmarks(ns, positions[2], positions[1])
+ eq(2, table.getn(rv))
+ rv = get_extmarks(ns2, positions[2], positions[1])
+ eq(2, table.getn(rv))
+
+ curbufmeths.del_extmark(ns, marks[1])
+ rv = get_extmarks(ns, {0, 0}, {-1, -1})
+ eq(2, table.getn(rv))
+ curbufmeths.del_extmark(ns2, marks[1])
+ rv = get_extmarks(ns2, {0, 0}, {-1, -1})
+ eq(2, table.getn(rv))
+ end)
+
+ it('mark set can create unique identifiers #extmarks', function()
+ -- create mark with id 1
+ eq(1, set_extmark(ns, 1, positions[1][1], positions[1][2]))
+ -- ask for unique id, it should be the next one, i e 2
+ eq(2, set_extmark(ns, 0, positions[1][1], positions[1][2]))
+ eq(3, set_extmark(ns, 3, positions[2][1], positions[2][2]))
+ eq(4, set_extmark(ns, 0, positions[1][1], positions[1][2]))
+
+ -- mixing manual and allocated id:s are not recommened, but it should
+ -- do something reasonable
+ eq(6, set_extmark(ns, 6, positions[2][1], positions[2][2]))
+ eq(7, set_extmark(ns, 0, positions[1][1], positions[1][2]))
+ eq(8, set_extmark(ns, 0, positions[1][1], positions[1][2]))
+ end)
+
+ it('auto indenting with enter works #extmarks', function()
+ -- op_reindent in ops.c
+ feed(':set cindent<cr><esc>')
+ feed(':set autoindent<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ feed("0iint <esc>A {1M1<esc>b<esc>")
+ -- Set the mark on the M, should move..
+ set_extmark(ns, marks[1], 0, 12)
+ -- Set the mark before the cursor, should stay there
+ set_extmark(ns, marks[2], 0, 10)
+ feed("i<cr><esc>")
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({1, 3}, rv)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[2])
+ eq({0, 10}, rv)
+ check_undo_redo(ns, marks[1], 0, 12, 1, 3)
+ end)
+
+ it('auto indenting entire line works #extmarks', function()
+ feed(':set cindent<cr><esc>')
+ feed(':set autoindent<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ -- <c-f> will force an indent of 2
+ feed("0iint <esc>A {<cr><esc>0i1M1<esc>")
+ set_extmark(ns, marks[1], 1, 1)
+ feed("0i<c-f><esc>")
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({1, 3}, rv)
+ check_undo_redo(ns, marks[1], 1, 1, 1, 3)
+ -- now check when cursor at eol
+ feed("uA<c-f><esc>")
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({1, 3}, rv)
+ end)
+
+ it('removing auto indenting with <C-D> works #extmarks', function()
+ feed(':set cindent<cr><esc>')
+ feed(':set autoindent<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ feed("0i<tab><esc>")
+ set_extmark(ns, marks[1], 0, 3)
+ feed("bi<c-d><esc>")
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, 1}, rv)
+ check_undo_redo(ns, marks[1], 0, 3, 0, 1)
+ -- check when cursor at eol
+ feed("uA<c-d><esc>")
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, 1}, rv)
+ end)
+
+ it('indenting multiple lines with = works #extmarks', function()
+ feed(':set cindent<cr><esc>')
+ feed(':set autoindent<cr><esc>')
+ feed(':set shiftwidth=2<cr><esc>')
+ feed("0iint <esc>A {<cr><bs>1M1<cr><bs>2M2<esc>")
+ set_extmark(ns, marks[1], 1, 1)
+ set_extmark(ns, marks[2], 2, 1)
+ feed('=gg')
+ check_undo_redo(ns, marks[1], 1, 1, 1, 3)
+ check_undo_redo(ns, marks[2], 2, 1, 2, 5)
+ end)
+
+ it('substitutes by deleting inside the replace matches #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ feed(':s/34/xx<cr>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 4)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 4)
+ end)
+
+ it('substitutes when insert text > deleted #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ feed(':s/34/xxx<cr>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 5)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 5)
+ end)
+
+ it('substitutes when marks around eol #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ set_extmark(ns, marks[1], 0, 4)
+ set_extmark(ns, marks[2], 0, 5)
+ feed(':s/5/xxx<cr>')
+ check_undo_redo(ns, marks[1], 0, 4, 0, 7)
+ check_undo_redo(ns, marks[2], 0, 5, 0, 7)
+ end)
+
+ it('substitutes over range insert text > deleted #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ feed('A<cr>x34xx<esc>')
+ feed('A<cr>xxx34<esc>')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 1, 1)
+ set_extmark(ns, marks[3], 2, 4)
+ feed(':1,3s/34/xxx<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 5)
+ check_undo_redo(ns, marks[2], 1, 1, 1, 4)
+ check_undo_redo(ns, marks[3], 2, 4, 2, 6)
+ end)
+
+ it('substitutes multiple matches in a line #extmarks_sub', function()
+ -- do_sub in ex_cmds.c
+ feed('ddi3x3x3<esc>')
+ set_extmark(ns, marks[1], 0, 0)
+ set_extmark(ns, marks[2], 0, 2)
+ set_extmark(ns, marks[3], 0, 4)
+ feed(':s/3/yy/g<cr><esc>')
+ check_undo_redo(ns, marks[1], 0, 0, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 2, 0, 5)
+ check_undo_redo(ns, marks[3], 0, 4, 0, 8)
+ end)
+
+ it('substitions over multiple lines with newline in pattern #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 1, 0)
+ set_extmark(ns, marks[4], 1, 5)
+ set_extmark(ns, marks[5], 2, 0)
+ feed([[:1,2s:5\n:5 <cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 0, 6)
+ check_undo_redo(ns, marks[3], 1, 0, 0, 6)
+ check_undo_redo(ns, marks[4], 1, 5, 0, 11)
+ check_undo_redo(ns, marks[5], 2, 0, 1, 0)
+ end)
+
+ it('inserting #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 1, 0)
+ set_extmark(ns, marks[4], 1, 5)
+ set_extmark(ns, marks[5], 2, 0)
+ set_extmark(ns, marks[6], 1, 2)
+ feed([[:1,2s:5\n67:X<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 0, 5)
+ check_undo_redo(ns, marks[3], 1, 0, 0, 5)
+ check_undo_redo(ns, marks[4], 1, 5, 0, 8)
+ check_undo_redo(ns, marks[5], 2, 0, 1, 0)
+ check_undo_redo(ns, marks[6], 1, 2, 0, 5)
+ end)
+
+ it('substitions with multiple newlines in pattern #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 4)
+ set_extmark(ns, marks[2], 0, 5)
+ set_extmark(ns, marks[3], 1, 0)
+ set_extmark(ns, marks[4], 1, 5)
+ set_extmark(ns, marks[5], 2, 0)
+ feed([[:1,2s:\n.*\n:X<cr>]])
+ check_undo_redo(ns, marks[1], 0, 4, 0, 4)
+ check_undo_redo(ns, marks[2], 0, 5, 0, 6)
+ check_undo_redo(ns, marks[3], 1, 0, 0, 6)
+ check_undo_redo(ns, marks[4], 1, 5, 0, 6)
+ check_undo_redo(ns, marks[5], 2, 0, 0, 6)
+ end)
+
+ it('substitions over multiple lines with replace in substition #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 2)
+ set_extmark(ns, marks[3], 0, 4)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 2, 0)
+ feed([[:1,2s:3:\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 1, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 2, 1, 0)
+ check_undo_redo(ns, marks[3], 0, 4, 1, 1)
+ check_undo_redo(ns, marks[4], 1, 0, 2, 0)
+ check_undo_redo(ns, marks[5], 2, 0, 3, 0)
+ feed('u')
+ feed([[:1,2s:3:\rxx<cr>]])
+ eq({1, 3}, curbufmeths.get_extmark_by_id(ns, marks[3]))
+ end)
+
+ it('substitions over multiple lines with replace in substition #extmarks_sub', function()
+ feed('A<cr>x3<cr>xx<esc>')
+ set_extmark(ns, marks[1], 1, 0)
+ set_extmark(ns, marks[2], 1, 1)
+ set_extmark(ns, marks[3], 1, 2)
+ feed([[:2,2s:3:\r<cr>]])
+ check_undo_redo(ns, marks[1], 1, 0, 1, 0)
+ check_undo_redo(ns, marks[2], 1, 1, 2, 0)
+ check_undo_redo(ns, marks[3], 1, 2, 2, 0)
+ end)
+
+ it('substitions over multiple lines with replace in substition #extmarks_sub', function()
+ feed('A<cr>x3<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 1)
+ set_extmark(ns, marks[2], 0, 2)
+ set_extmark(ns, marks[3], 0, 4)
+ set_extmark(ns, marks[4], 1, 1)
+ set_extmark(ns, marks[5], 2, 0)
+ feed([[:1,2s:3:\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 1, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 2, 1, 0)
+ check_undo_redo(ns, marks[3], 0, 4, 1, 1)
+ check_undo_redo(ns, marks[4], 1, 1, 3, 0)
+ check_undo_redo(ns, marks[5], 2, 0, 4, 0)
+ feed('u')
+ feed([[:1,2s:3:\rxx<cr>]])
+ check_undo_redo(ns, marks[3], 0, 4, 1, 3)
+ end)
+
+ it('substitions with newline in match and sub, delta is 0 #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 1, 5)
+ set_extmark(ns, marks[6], 2, 0)
+ feed([[:1,1s:5\n:\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 1, 0)
+ check_undo_redo(ns, marks[3], 0, 5, 1, 0)
+ check_undo_redo(ns, marks[4], 1, 0, 1, 0)
+ check_undo_redo(ns, marks[5], 1, 5, 1, 5)
+ check_undo_redo(ns, marks[6], 2, 0, 2, 0)
+ end)
+
+ it('substitions with newline in match and sub, delta > 0 #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 1, 5)
+ set_extmark(ns, marks[6], 2, 0)
+ feed([[:1,1s:5\n:\r\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 2, 0)
+ check_undo_redo(ns, marks[3], 0, 5, 2, 0)
+ check_undo_redo(ns, marks[4], 1, 0, 2, 0)
+ check_undo_redo(ns, marks[5], 1, 5, 2, 5)
+ check_undo_redo(ns, marks[6], 2, 0, 3, 0)
+ end)
+
+ it('substitions with newline in match and sub, delta < 0 #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 1, 5)
+ set_extmark(ns, marks[6], 2, 1)
+ set_extmark(ns, marks[7], 3, 0)
+ feed([[:1,2s:5\n.*\n:\r<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 1, 0)
+ check_undo_redo(ns, marks[3], 0, 5, 1, 0)
+ check_undo_redo(ns, marks[4], 1, 0, 1, 0)
+ check_undo_redo(ns, marks[5], 1, 5, 1, 0)
+ check_undo_redo(ns, marks[6], 2, 1, 1, 1)
+ check_undo_redo(ns, marks[7], 3, 0, 2, 0)
+ end)
+
+ it('substitions with backrefs, newline inserted into sub #extmarks_sub', function()
+ feed('A<cr>67890<cr>xx<cr>xx<esc>')
+ set_extmark(ns, marks[1], 0, 3)
+ set_extmark(ns, marks[2], 0, 4)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 1, 0)
+ set_extmark(ns, marks[5], 1, 5)
+ set_extmark(ns, marks[6], 2, 0)
+ feed([[:1,1s:5\(\n\):\0\1<cr>]])
+ check_undo_redo(ns, marks[1], 0, 3, 0, 3)
+ check_undo_redo(ns, marks[2], 0, 4, 2, 0)
+ check_undo_redo(ns, marks[3], 0, 5, 2, 0)
+ check_undo_redo(ns, marks[4], 1, 0, 2, 0)
+ check_undo_redo(ns, marks[5], 1, 5, 2, 5)
+ check_undo_redo(ns, marks[6], 2, 0, 3, 0)
+ end)
+
+ it('substitions a ^ #extmarks_sub', function()
+ set_extmark(ns, marks[1], 0, 0)
+ set_extmark(ns, marks[2], 0, 1)
+ feed([[:s:^:x<cr>]])
+ check_undo_redo(ns, marks[1], 0, 0, 0, 1)
+ check_undo_redo(ns, marks[2], 0, 1, 0, 2)
+ end)
+
+ it('using <c-a> without increase in order of magnitude #extmarks_inc_dec', function()
+ -- do_addsub in ops.c
+ feed('ddiabc998xxx<esc>Tc')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 0, 6)
+ set_extmark(ns, marks[5], 0, 7)
+ feed('<c-a>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 6)
+ check_undo_redo(ns, marks[3], 0, 5, 0, 6)
+ check_undo_redo(ns, marks[4], 0, 6, 0, 6)
+ check_undo_redo(ns, marks[5], 0, 7, 0, 7)
+ end)
+
+ it('using <c-a> when increase in order of magnitude #extmarks_inc_dec', function()
+ -- do_addsub in ops.c
+ feed('ddiabc999xxx<esc>Tc')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 0, 6)
+ set_extmark(ns, marks[5], 0, 7)
+ feed('<c-a>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 7)
+ check_undo_redo(ns, marks[3], 0, 5, 0, 7)
+ check_undo_redo(ns, marks[4], 0, 6, 0, 7)
+ check_undo_redo(ns, marks[5], 0, 7, 0, 8)
+ end)
+
+ it('using <c-a> when negative and without decrease in order of magnitude #extmarks_inc_dec', function()
+ feed('ddiabc-999xxx<esc>T-')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 6)
+ set_extmark(ns, marks[4], 0, 7)
+ set_extmark(ns, marks[5], 0, 8)
+ feed('<c-a>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 7)
+ check_undo_redo(ns, marks[3], 0, 6, 0, 7)
+ check_undo_redo(ns, marks[4], 0, 7, 0, 7)
+ check_undo_redo(ns, marks[5], 0, 8, 0, 8)
+ end)
+
+ it('using <c-a> when negative and decrease in order of magnitude #extmarks_inc_dec', function()
+ feed('ddiabc-1000xxx<esc>T-')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 7)
+ set_extmark(ns, marks[4], 0, 8)
+ set_extmark(ns, marks[5], 0, 9)
+ feed('<c-a>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 7)
+ check_undo_redo(ns, marks[3], 0, 7, 0, 7)
+ check_undo_redo(ns, marks[4], 0, 8, 0, 7)
+ check_undo_redo(ns, marks[5], 0, 9, 0, 8)
+ end)
+
+ it('using <c-x> without decrease in order of magnitude #extmarks_inc_dec', function()
+ -- do_addsub in ops.c
+ feed('ddiabc999xxx<esc>Tc')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 5)
+ set_extmark(ns, marks[4], 0, 6)
+ set_extmark(ns, marks[5], 0, 7)
+ feed('<c-x>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 6)
+ check_undo_redo(ns, marks[3], 0, 5, 0, 6)
+ check_undo_redo(ns, marks[4], 0, 6, 0, 6)
+ check_undo_redo(ns, marks[5], 0, 7, 0, 7)
+ end)
+
+ it('using <c-x> when decrease in order of magnitude #extmarks_inc_dec', function()
+ -- do_addsub in ops.c
+ feed('ddiabc1000xxx<esc>Tc')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 6)
+ set_extmark(ns, marks[4], 0, 7)
+ set_extmark(ns, marks[5], 0, 8)
+ feed('<c-x>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 6)
+ check_undo_redo(ns, marks[3], 0, 6, 0, 6)
+ check_undo_redo(ns, marks[4], 0, 7, 0, 6)
+ check_undo_redo(ns, marks[5], 0, 8, 0, 7)
+ end)
+
+ it('using <c-x> when negative and without increase in order of magnitude #extmarks_inc_dec', function()
+ feed('ddiabc-998xxx<esc>T-')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 6)
+ set_extmark(ns, marks[4], 0, 7)
+ set_extmark(ns, marks[5], 0, 8)
+ feed('<c-x>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 7)
+ check_undo_redo(ns, marks[3], 0, 6, 0, 7)
+ check_undo_redo(ns, marks[4], 0, 7, 0, 7)
+ check_undo_redo(ns, marks[5], 0, 8, 0, 8)
+ end)
+
+ it('using <c-x> when negative and increase in order of magnitude #extmarks_inc_dec', function()
+ feed('ddiabc-999xxx<esc>T-')
+ set_extmark(ns, marks[1], 0, 2)
+ set_extmark(ns, marks[2], 0, 3)
+ set_extmark(ns, marks[3], 0, 6)
+ set_extmark(ns, marks[4], 0, 7)
+ set_extmark(ns, marks[5], 0, 8)
+ feed('<c-x>')
+ check_undo_redo(ns, marks[1], 0, 2, 0, 2)
+ check_undo_redo(ns, marks[2], 0, 3, 0, 8)
+ check_undo_redo(ns, marks[3], 0, 6, 0, 8)
+ check_undo_redo(ns, marks[4], 0, 7, 0, 8)
+ check_undo_redo(ns, marks[5], 0, 8, 0, 9)
+ end)
+
+ it('throws consistent error codes', function()
+ local ns_invalid = ns2 + 1
+ eq("Invalid ns_id", pcall_err(set_extmark, ns_invalid, marks[1], positions[1][1], positions[1][2]))
+ eq("Invalid ns_id", pcall_err(curbufmeths.del_extmark, ns_invalid, marks[1]))
+ eq("Invalid ns_id", pcall_err(get_extmarks, ns_invalid, positions[1], positions[2]))
+ eq("Invalid ns_id", pcall_err(curbufmeths.get_extmark_by_id, ns_invalid, marks[1]))
+ end)
+
+ it('when col = line-length, set the mark on eol #extmarks', function()
+ set_extmark(ns, marks[1], 0, -1)
+ local rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, init_text:len()}, rv)
+ -- Test another
+ set_extmark(ns, marks[1], 0, -1)
+ rv = curbufmeths.get_extmark_by_id(ns, marks[1])
+ eq({0, init_text:len()}, rv)
+ end)
+
+ it('when col = line-length, set the mark on eol #extmarks', function()
+ local invalid_col = init_text:len() + 1
+ eq("col value outside range", pcall_err(set_extmark, ns, marks[1], 0, invalid_col))
+ end)
+
+ it('fails when line > line_count #extmarks', function()
+ local invalid_col = init_text:len() + 1
+ local invalid_lnum = 3
+ eq('line value outside range', pcall_err(set_extmark, ns, marks[1], invalid_lnum, invalid_col))
+ eq({}, curbufmeths.get_extmark_by_id(ns, marks[1]))
+ end)
+
+ it('bug from check_col in extmark_set #extmarks_sub', function()
+ -- This bug was caused by extmark_set always using check_col. check_col
+ -- always uses the current buffer. This wasn't working during undo so we
+ -- now use check_col and check_lnum only when they are required.
+ feed('A<cr>67890<cr>xx<esc>')
+ feed('A<cr>12345<cr>67890<cr>xx<esc>')
+ set_extmark(ns, marks[1], 3, 4)
+ feed([[:1,5s:5\n:5 <cr>]])
+ check_undo_redo(ns, marks[1], 3, 4, 2, 6)
+ end)
+
+ it('in read-only buffer', function()
+ command("view! runtime/doc/help.txt")
+ eq(true, curbufmeths.get_option('ro'))
+ local id = set_extmark(ns, 0, 0, 2)
+ eq({{id, 0, 2}}, get_extmarks(ns,0, -1))
+ end)
+
+ it('can set a mark to other buffer', function()
+ local buf = request('nvim_create_buf', 0, 1)
+ request('nvim_buf_set_lines', buf, 0, -1, 1, {"", ""})
+ local id = bufmeths.set_extmark(buf, ns, 0, 1, 0, {})
+ eq({{id, 1, 0}}, bufmeths.get_extmarks(buf, ns, 0, -1, {}))
+ end)
+end)
+
+describe('Extmarks buffer api with many marks', function()
+ local ns1
+ local ns2
+ local ns_marks = {}
+ before_each(function()
+ clear()
+ ns1 = request('nvim_create_namespace', "ns1")
+ ns2 = request('nvim_create_namespace', "ns2")
+ ns_marks = {[ns1]={}, [ns2]={}}
+ local lines = {}
+ for i = 1,30 do
+ lines[#lines+1] = string.rep("x ",i)
+ end
+ curbufmeths.set_lines(0, -1, true, lines)
+ local ns = ns1
+ local q = 0
+ for i = 0,29 do
+ for j = 0,i do
+ local id = set_extmark(ns,0, i,j)
+ eq(nil, ns_marks[ns][id])
+ ok(id > 0)
+ ns_marks[ns][id] = {i,j}
+ ns = ns1+ns2-ns
+ q = q + 1
+ end
+ end
+ eq(233, #ns_marks[ns1])
+ eq(232, #ns_marks[ns2])
+
+ end)
+
+ local function get_marks(ns)
+ local mark_list = get_extmarks(ns, 0, -1)
+ local marks = {}
+ for _, mark in ipairs(mark_list) do
+ local id, row, col = unpack(mark)
+ eq(nil, marks[id], "duplicate mark")
+ marks[id] = {row,col}
+ end
+ return marks
+ end
+
+ it("can get marks #extmarks", function()
+ eq(ns_marks[ns1], get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ end)
+
+ it("can clear all marks in ns #extmarks", function()
+ curbufmeths.clear_namespace(ns1, 0, -1)
+ eq({}, get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ curbufmeths.clear_namespace(ns2, 0, -1)
+ eq({}, get_marks(ns1))
+ eq({}, get_marks(ns2))
+ end)
+
+ it("can clear line range #extmarks", function()
+ curbufmeths.clear_namespace(ns1, 10, 20)
+ for id, mark in pairs(ns_marks[ns1]) do
+ if 10 <= mark[1] and mark[1] < 20 then
+ ns_marks[ns1][id] = nil
+ end
+ end
+ eq(ns_marks[ns1], get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ end)
+
+ it("can delete line #extmarks", function()
+ feed('10Gdd')
+ for _, marks in pairs(ns_marks) do
+ for id, mark in pairs(marks) do
+ if mark[1] == 9 then
+ marks[id] = {9,0}
+ elseif mark[1] >= 10 then
+ mark[1] = mark[1] - 1
+ end
+ end
+ end
+ eq(ns_marks[ns1], get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ end)
+
+ it("can delete lines #extmarks", function()
+ feed('10G10dd')
+ for _, marks in pairs(ns_marks) do
+ for id, mark in pairs(marks) do
+ if 9 <= mark[1] and mark[1] < 19 then
+ marks[id] = {9,0}
+ elseif mark[1] >= 19 then
+ mark[1] = mark[1] - 10
+ end
+ end
+ end
+ eq(ns_marks[ns1], get_marks(ns1))
+ eq(ns_marks[ns2], get_marks(ns2))
+ end)
+
+ it("can wipe buffer #extmarks", function()
+ command('bwipe!')
+ eq({}, get_marks(ns1))
+ eq({}, get_marks(ns2))
+ end)
+end)
diff --git a/test/functional/api/menu_spec.lua b/test/functional/api/menu_spec.lua
index 2cfa0e3e47..34a92477f3 100644
--- a/test/functional/api/menu_spec.lua
+++ b/test/functional/api/menu_spec.lua
@@ -15,10 +15,6 @@ describe("update_menu notification", function()
screen:attach()
end)
- after_each(function()
- screen:detach()
- end)
-
local function expect_sent(expected)
screen:expect{condition=function()
if screen.update_menu ~= expected then
diff --git a/test/functional/api/proc_spec.lua b/test/functional/api/proc_spec.lua
index 063d382790..d828bdf948 100644
--- a/test/functional/api/proc_spec.lua
+++ b/test/functional/api/proc_spec.lua
@@ -10,7 +10,7 @@ local request = helpers.request
local retry = helpers.retry
local NIL = helpers.NIL
-describe('api', function()
+describe('API', function()
before_each(clear)
describe('nvim_get_proc_children', function()
diff --git a/test/functional/api/rpc_fixture.lua b/test/functional/api/rpc_fixture.lua
index 87f5a91115..94df751363 100644
--- a/test/functional/api/rpc_fixture.lua
+++ b/test/functional/api/rpc_fixture.lua
@@ -1,12 +1,8 @@
-local deps_prefix = (os.getenv('DEPS_PREFIX') and os.getenv('DEPS_PREFIX')
- or './.deps/usr')
-
-package.path = deps_prefix .. '/share/lua/5.1/?.lua;' ..
- deps_prefix .. '/share/lua/5.1/?/init.lua;' ..
- package.path
-
-package.cpath = deps_prefix .. '/lib/lua/5.1/?.so;' ..
- package.cpath
+--- RPC server fixture.
+--
+-- Lua's paths are passed as arguments to reflect the path in the test itself.
+package.path = arg[1]
+package.cpath = arg[2]
local mpack = require('mpack')
local StdioStream = require('nvim.stdio_stream')
diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua
index e275d8cd35..237a4b01e4 100644
--- a/test/functional/api/server_requests_spec.lua
+++ b/test/functional/api/server_requests_spec.lua
@@ -241,10 +241,14 @@ describe('server -> client', function()
\ 'rpc': v:true
\ }
]])
- meths.set_var("args", {helpers.test_lua_prg,
- 'test/functional/api/rpc_fixture.lua'})
+ meths.set_var("args", {
+ helpers.test_lua_prg,
+ 'test/functional/api/rpc_fixture.lua',
+ package.path,
+ package.cpath,
+ })
jobid = eval("jobstart(g:args, g:job_opts)")
- neq(0, 'jobid')
+ neq(0, jobid)
end)
after_each(function()
@@ -254,7 +258,11 @@ describe('server -> client', function()
if helpers.pending_win32(pending) then return end
it('rpc and text stderr can be combined', function()
- eq("ok",funcs.rpcrequest(jobid, "poll"))
+ local status, rv = pcall(funcs.rpcrequest, jobid, 'poll')
+ if not status then
+ error(string.format('missing nvim Lua module? (%s)', rv))
+ end
+ eq('ok', rv)
funcs.rpcnotify(jobid, "ping")
eq({'notification', 'pong', {}}, next_msg())
eq("done!",funcs.rpcrequest(jobid, "write_stderr", "fluff\n"))
@@ -309,8 +317,7 @@ describe('server -> client', function()
set_session(server)
local status, address = pcall(funcs.serverstart, "127.0.0.1:")
if not status then
- pending('no ipv4 stack', function() end)
- return
+ pending('no ipv4 stack')
end
eq('127.0.0.1:', string.sub(address,1,10))
connect_test(server, 'tcp', address)
@@ -321,8 +328,7 @@ describe('server -> client', function()
set_session(server)
local status, address = pcall(funcs.serverstart, '::1:')
if not status then
- pending('no ipv6 stack', function() end)
- return
+ pending('no ipv6 stack')
end
eq('::1:', string.sub(address,1,4))
connect_test(server, 'tcp', address)
@@ -339,11 +345,6 @@ describe('server -> client', function()
describe('connecting to its own pipe address', function()
it('does not deadlock', function()
- if not helpers.isCI('travis') and helpers.is_os('mac') then
- -- It does, in fact, deadlock on QuickBuild. #6851
- pending("deadlocks on QuickBuild", function() end)
- return
- end
local address = funcs.serverlist()[1]
local first = string.sub(address,1,1)
ok(first == '/' or first == '\\')
diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua
index 8b77dbcaa6..d901a5e2eb 100644
--- a/test/functional/api/vim_spec.lua
+++ b/test/functional/api/vim_spec.lua
@@ -16,6 +16,8 @@ local parse_context = helpers.parse_context
local request = helpers.request
local source = helpers.source
local next_msg = helpers.next_msg
+local tmpname = helpers.tmpname
+local write_file = helpers.write_file
local pcall_err = helpers.pcall_err
local format_string = helpers.format_string
@@ -74,9 +76,127 @@ describe('API', function()
eq({mode='i', blocking=false}, nvim("get_mode"))
end)
+ describe('nvim_exec', function()
+ it('one-line input', function()
+ nvim('exec', "let x1 = 'a'", false)
+ eq('a', nvim('get_var', 'x1'))
+ end)
+
+ it(':verbose set {option}?', function()
+ nvim('exec', 'set nowrap', false)
+ eq('nowrap\n\tLast set from anonymous :source',
+ nvim('exec', 'verbose set wrap?', true))
+ end)
+
+ it('multiline input', function()
+ -- Heredoc + empty lines.
+ nvim('exec', "let x2 = 'a'\n", false)
+ eq('a', nvim('get_var', 'x2'))
+ nvim('exec','lua <<EOF\n\n\n\ny=3\n\n\nEOF', false)
+ eq(3, nvim('eval', "luaeval('y')"))
+
+ eq('', nvim('exec', 'lua <<EOF\ny=3\nEOF', false))
+ eq(3, nvim('eval', "luaeval('y')"))
+
+ -- Multiple statements
+ nvim('exec', 'let x1=1\nlet x2=2\nlet x3=3\n', false)
+ eq(1, nvim('eval', 'x1'))
+ eq(2, nvim('eval', 'x2'))
+ eq(3, nvim('eval', 'x3'))
+
+ -- Functions
+ nvim('exec', 'function Foo()\ncall setline(1,["xxx"])\nendfunction', false)
+ eq(nvim('get_current_line'), '')
+ nvim('exec', 'call Foo()', false)
+ eq(nvim('get_current_line'), 'xxx')
+
+ -- Autocmds
+ nvim('exec','autocmd BufAdd * :let x1 = "Hello"', false)
+ nvim('command', 'new foo')
+ eq('Hello', request('nvim_eval', 'g:x1'))
+ end)
+
+ it('non-ASCII input', function()
+ nvim('exec', [=[
+ new
+ exe "normal! i ax \n Ax "
+ :%s/ax/--a1234--/g | :%s/Ax/--A1234--/g
+ ]=], false)
+ nvim('command', '1')
+ eq(' --a1234-- ', nvim('get_current_line'))
+ nvim('command', '2')
+ eq(' --A1234-- ', nvim('get_current_line'))
+
+ nvim('exec', [[
+ new
+ call setline(1,['xxx'])
+ call feedkeys('r')
+ call feedkeys('ñ', 'xt')
+ ]], false)
+ eq('ñxx', nvim('get_current_line'))
+ end)
+
+ it('execution error', function()
+ eq('Vim:E492: Not an editor command: bogus_command',
+ pcall_err(request, 'nvim_exec', 'bogus_command', false))
+ eq('', nvim('eval', 'v:errmsg')) -- v:errmsg was not updated.
+ eq('', eval('v:exception'))
+
+ eq('Vim(buffer):E86: Buffer 23487 does not exist',
+ pcall_err(request, 'nvim_exec', 'buffer 23487', false))
+ eq('', eval('v:errmsg')) -- v:errmsg was not updated.
+ eq('', eval('v:exception'))
+ end)
+
+ it('recursion', function()
+ local fname = tmpname()
+ write_file(fname, 'let x1 = "set from :source file"\n')
+ -- nvim_exec
+ -- :source
+ -- nvim_exec
+ request('nvim_exec', [[
+ let x2 = substitute('foo','o','X','g')
+ let x4 = 'should be overwritten'
+ call nvim_exec("source ]]..fname..[[\nlet x3 = substitute('foo','foo','set by recursive nvim_exec','g')\nlet x5='overwritten'\nlet x4=x5\n", v:false)
+ ]], false)
+ eq('set from :source file', request('nvim_get_var', 'x1'))
+ eq('fXX', request('nvim_get_var', 'x2'))
+ eq('set by recursive nvim_exec', request('nvim_get_var', 'x3'))
+ eq('overwritten', request('nvim_get_var', 'x4'))
+ eq('overwritten', request('nvim_get_var', 'x5'))
+ os.remove(fname)
+ end)
+
+ it('traceback', function()
+ local fname = tmpname()
+ write_file(fname, 'echo "hello"\n')
+ local sourcing_fname = tmpname()
+ write_file(sourcing_fname, 'call nvim_exec("source '..fname..'", v:false)\n')
+ meths.exec('set verbose=2', false)
+ local traceback_output = 'line 0: sourcing "'..sourcing_fname..'"\n'..
+ 'line 0: sourcing "'..fname..'"\n'..
+ 'hello\n'..
+ 'finished sourcing '..fname..'\n'..
+ 'continuing in nvim_exec() called at '..sourcing_fname..':1\n'..
+ 'finished sourcing '..sourcing_fname..'\n'..
+ 'continuing in nvim_exec() called at nvim_exec():0'
+ eq(traceback_output,
+ meths.exec('call nvim_exec("source '..sourcing_fname..'", v:false)', true))
+ os.remove(fname)
+ os.remove(sourcing_fname)
+ end)
+
+ it('returns output', function()
+ eq('this is spinal tap',
+ nvim('exec', 'lua <<EOF\n\n\nprint("this is spinal tap")\n\n\nEOF', true))
+ eq('', nvim('exec', 'echo', true))
+ eq('foo 42', nvim('exec', 'echo "foo" 42', true))
+ end)
+ end)
+
describe('nvim_command', function()
it('works', function()
- local fname = helpers.tmpname()
+ local fname = tmpname()
nvim('command', 'new')
nvim('command', 'edit '..fname)
nvim('command', 'normal itesting\napi')
@@ -313,41 +433,44 @@ describe('API', function()
end)
end)
- describe('nvim_execute_lua', function()
+ describe('nvim_exec_lua', function()
it('works', function()
- meths.execute_lua('vim.api.nvim_set_var("test", 3)', {})
+ meths.exec_lua('vim.api.nvim_set_var("test", 3)', {})
eq(3, meths.get_var('test'))
- eq(17, meths.execute_lua('a, b = ...\nreturn a + b', {10,7}))
+ eq(17, meths.exec_lua('a, b = ...\nreturn a + b', {10,7}))
- eq(NIL, meths.execute_lua('function xx(a,b)\nreturn a..b\nend',{}))
+ eq(NIL, meths.exec_lua('function xx(a,b)\nreturn a..b\nend',{}))
+ eq("xy", meths.exec_lua('return xx(...)', {'x','y'}))
+
+ -- Deprecated name: nvim_execute_lua.
eq("xy", meths.execute_lua('return xx(...)', {'x','y'}))
end)
it('reports errors', function()
eq([[Error loading lua: [string "<nvim>"]:1: '=' expected near '+']],
- pcall_err(meths.execute_lua, 'a+*b', {}))
+ pcall_err(meths.exec_lua, 'a+*b', {}))
eq([[Error loading lua: [string "<nvim>"]:1: unexpected symbol near '1']],
- pcall_err(meths.execute_lua, '1+2', {}))
+ pcall_err(meths.exec_lua, '1+2', {}))
eq([[Error loading lua: [string "<nvim>"]:1: unexpected symbol]],
- pcall_err(meths.execute_lua, 'aa=bb\0', {}))
+ pcall_err(meths.exec_lua, 'aa=bb\0', {}))
eq([[Error executing lua: [string "<nvim>"]:1: attempt to call global 'bork' (a nil value)]],
- pcall_err(meths.execute_lua, 'bork()', {}))
+ pcall_err(meths.exec_lua, 'bork()', {}))
eq('Error executing lua: [string "<nvim>"]:1: did\nthe\nfail',
- pcall_err(meths.execute_lua, 'error("did\\nthe\\nfail")', {}))
+ pcall_err(meths.exec_lua, 'error("did\\nthe\\nfail")', {}))
end)
it('uses native float values', function()
- eq(2.5, meths.execute_lua("return select(1, ...)", {2.5}))
- eq("2.5", meths.execute_lua("return vim.inspect(...)", {2.5}))
+ eq(2.5, meths.exec_lua("return select(1, ...)", {2.5}))
+ eq("2.5", meths.exec_lua("return vim.inspect(...)", {2.5}))
-- "special" float values are still accepted as return values.
- eq(2.5, meths.execute_lua("return vim.api.nvim_eval('2.5')", {}))
- eq("{\n [false] = 2.5,\n [true] = 3\n}", meths.execute_lua("return vim.inspect(vim.api.nvim_eval('2.5'))", {}))
+ eq(2.5, meths.exec_lua("return vim.api.nvim_eval('2.5')", {}))
+ eq("{\n [false] = 2.5,\n [true] = 3\n}", meths.exec_lua("return vim.inspect(vim.api.nvim_eval('2.5'))", {}))
end)
end)
@@ -453,7 +576,7 @@ describe('API', function()
eq({0,3,14,0}, funcs.getpos('.'))
end)
it('vim.paste() failure', function()
- nvim('execute_lua', 'vim.paste = (function(lines, phase) error("fake fail") end)', {})
+ nvim('exec_lua', 'vim.paste = (function(lines, phase) error("fake fail") end)', {})
eq([[Error executing lua: [string "<nvim>"]:1: fake fail]],
pcall_err(request, 'nvim_paste', 'line 1\nline 2\nline 3', false, 1))
end)
@@ -677,7 +800,7 @@ describe('API', function()
ok(nil ~= string.find(rv, 'noequalalways\n'..
'\tLast set from API client %(channel id %d+%)'))
- nvim('execute_lua', 'vim.api.nvim_set_option("equalalways", true)', {})
+ nvim('exec_lua', 'vim.api.nvim_set_option("equalalways", true)', {})
status, rv = pcall(nvim, 'command_output',
'verbose set equalalways?')
eq(true, status)
@@ -1680,7 +1803,7 @@ describe('API', function()
eq(' 1 %a "[No Name]" line 1\n'..
' 3 h "[Scratch]" line 0\n'..
' 4 h "[Scratch]" line 0',
- meths.command_output("ls"))
+ meths.exec('ls', true))
-- current buffer didn't change
eq({id=1}, meths.get_current_buf())
diff --git a/test/functional/autocmd/cmdline_spec.lua b/test/functional/autocmd/cmdline_spec.lua
index 51b7b819e9..8ec06dc148 100644
--- a/test/functional/autocmd/cmdline_spec.lua
+++ b/test/functional/autocmd/cmdline_spec.lua
@@ -33,7 +33,7 @@ describe('cmdline autocommands', function()
eq({'notification', 'CmdlineEnter', {{cmdtype=':', cmdlevel=1}}}, next_msg())
-- note: feed('bork<c-c>') might not consume 'bork'
- -- due to out-of-band interupt handling
+ -- due to out-of-band interrupt handling
feed('bork<esc>')
eq({'notification', 'CmdlineLeave',
{{cmdtype=':', cmdlevel=1, abort=true}}}, next_msg())
diff --git a/test/functional/autocmd/tabclose_spec.lua b/test/functional/autocmd/tabclose_spec.lua
index b7c33dc3d8..92d860c628 100644
--- a/test/functional/autocmd/tabclose_spec.lua
+++ b/test/functional/autocmd/tabclose_spec.lua
@@ -11,10 +11,10 @@ describe('TabClosed', function()
repeat
nvim('command', 'tabnew')
until nvim('eval', 'tabpagenr()') == 6 -- current tab is now 6
- eq("tabclosed:6:6:5", nvim('command_output', 'tabclose')) -- close last 6, current tab is now 5
- eq("tabclosed:5:5:4", nvim('command_output', 'close')) -- close last window on tab, closes tab
- eq("tabclosed:2:2:3", nvim('command_output', '2tabclose')) -- close tab 2, current tab is now 3
- eq("tabclosed:1:1:2\ntabclosed:1:1:1", nvim('command_output', 'tabonly')) -- close tabs 1 and 2
+ eq("tabclosed:6:6:5", nvim('exec', 'tabclose', true)) -- close last 6, current tab is now 5
+ eq("tabclosed:5:5:4", nvim('exec', 'close', true)) -- close last window on tab, closes tab
+ eq("tabclosed:2:2:3", nvim('exec', '2tabclose', true)) -- close tab 2, current tab is now 3
+ eq("tabclosed:1:1:2\ntabclosed:1:1:1", nvim('exec', 'tabonly', true)) -- close tabs 1 and 2
end)
it('is triggered when closing a window via bdelete from another tab', function()
@@ -23,7 +23,7 @@ describe('TabClosed', function()
nvim('command', '1tabedit Xtestfile')
nvim('command', 'normal! 1gt')
eq({1, 3}, nvim('eval', '[tabpagenr(), tabpagenr("$")]'))
- eq("tabclosed:2:2:1\ntabclosed:2:2:1", nvim('command_output', 'bdelete Xtestfile'))
+ eq("tabclosed:2:2:1\ntabclosed:2:2:1", nvim('exec', 'bdelete Xtestfile', true))
eq({1, 1}, nvim('eval', '[tabpagenr(), tabpagenr("$")]'))
end)
@@ -35,7 +35,7 @@ describe('TabClosed', function()
-- Only one tab is closed, and the alternate file is used for the other.
eq({2, 3}, nvim('eval', '[tabpagenr(), tabpagenr("$")]'))
- eq("tabclosed:2:2:2", nvim('command_output', 'bdelete Xtestfile2'))
+ eq("tabclosed:2:2:2", nvim('exec', 'bdelete Xtestfile2', true))
eq('Xtestfile1', nvim('eval', 'bufname("")'))
end)
end)
@@ -48,9 +48,9 @@ describe('TabClosed', function()
nvim('command', 'tabnew')
until nvim('eval', 'tabpagenr()') == 7 -- current tab is now 7
-- sanity check, we shouldn't match on tabs with numbers other than 2
- eq("tabclosed:7:7:6", nvim('command_output', 'tabclose'))
+ eq("tabclosed:7:7:6", nvim('exec', 'tabclose', true))
-- close tab page 2, current tab is now 5
- eq("tabclosed:2:2:5\ntabclosed:match", nvim('command_output', '2tabclose'))
+ eq("tabclosed:2:2:5\ntabclosed:match", nvim('exec', '2tabclose', true))
end)
end)
@@ -59,7 +59,7 @@ describe('TabClosed', function()
nvim('command', 'au! TabClosed * echom "tabclosed:".expand("<afile>").":".expand("<amatch>").":".tabpagenr()')
nvim('command', 'tabedit Xtestfile')
eq({2, 2}, nvim('eval', '[tabpagenr(), tabpagenr("$")]'))
- eq("tabclosed:2:2:1", nvim('command_output', 'close'))
+ eq("tabclosed:2:2:1", nvim('exec', 'close', true))
eq({1, 1}, nvim('eval', '[tabpagenr(), tabpagenr("$")]'))
end)
end)
diff --git a/test/functional/autocmd/tabnewentered_spec.lua b/test/functional/autocmd/tabnewentered_spec.lua
index 59cac07b34..32e341184d 100644
--- a/test/functional/autocmd/tabnewentered_spec.lua
+++ b/test/functional/autocmd/tabnewentered_spec.lua
@@ -7,15 +7,15 @@ describe('TabNewEntered', function()
it('matches when entering any new tab', function()
clear()
nvim('command', 'au! TabNewEntered * echom "tabnewentered:".tabpagenr().":".bufnr("")')
- eq("tabnewentered:2:2", nvim('command_output', 'tabnew'))
- eq("tabnewentered:3:3", nvim('command_output', 'tabnew test.x2'))
+ eq("tabnewentered:2:2", nvim('exec', 'tabnew', true))
+ eq("tabnewentered:3:3", nvim('exec', 'tabnew test.x2', true))
end)
end)
describe('with FILE as <afile>', function()
it('matches when opening a new tab for FILE', function()
nvim('command', 'au! TabNewEntered Xtest-tabnewentered echom "tabnewentered:match"')
eq('tabnewentered:4:4\ntabnewentered:match',
- nvim('command_output', 'tabnew Xtest-tabnewentered'))
+ nvim('exec', 'tabnew Xtest-tabnewentered', true))
end)
end)
describe('with CTRL-W T', function()
@@ -24,7 +24,7 @@ describe('TabNewEntered', function()
nvim('command', 'au! TabNewEntered * echom "entered"')
nvim('command', 'tabnew test.x2')
nvim('command', 'split')
- eq('entered', nvim('command_output', 'execute "normal \\<C-W>T"'))
+ eq('entered', nvim('exec', 'execute "normal \\<C-W>T"', true))
end)
end)
end)
diff --git a/test/functional/cmdline/ctrl_r_spec.lua b/test/functional/cmdline/ctrl_r_spec.lua
index d2dad23e98..a0f3955282 100644
--- a/test/functional/cmdline/ctrl_r_spec.lua
+++ b/test/functional/cmdline/ctrl_r_spec.lua
@@ -15,7 +15,7 @@ describe('cmdline CTRL-R', function()
-- <CR> inserted between lines, NOT after the final line.
eq('line1abc\rline2somemoretext', funcs.getcmdline())
- -- Yank 2 lines characterwise, then paste to cmdline.
+ -- Yank 2 lines charwise, then paste to cmdline.
feed([[<C-\><C-N>gg05lyvj:<C-R>0]])
-- <CR> inserted between lines, NOT after the final line.
eq('abc\rline2', funcs.getcmdline())
diff --git a/test/functional/cmdline/history_spec.lua b/test/functional/cmdline/history_spec.lua
index 20f9cf06a2..ee2d36f642 100644
--- a/test/functional/cmdline/history_spec.lua
+++ b/test/functional/cmdline/history_spec.lua
@@ -6,7 +6,7 @@ describe('history support code', function()
before_each(clear)
it('correctly clears start of the history', function()
- -- Regression test: check absense of the memory leak when clearing start of
+ -- Regression test: check absence of the memory leak when clearing start of
-- the history using ex_getln.c/clr_history().
eq(1, funcs.histadd(':', 'foo'))
eq(1, funcs.histdel(':'))
@@ -14,7 +14,7 @@ describe('history support code', function()
end)
it('correctly clears end of the history', function()
- -- Regression test: check absense of the memory leak when clearing end of
+ -- Regression test: check absence of the memory leak when clearing end of
-- the history using ex_getln.c/clr_history().
meths.set_option('history', 1)
eq(1, funcs.histadd(':', 'foo'))
diff --git a/test/functional/core/fileio_spec.lua b/test/functional/core/fileio_spec.lua
index e6bce85b8a..f4c476560d 100644
--- a/test/functional/core/fileio_spec.lua
+++ b/test/functional/core/fileio_spec.lua
@@ -9,9 +9,12 @@ local nvim_prog = helpers.nvim_prog
local request = helpers.request
local retry = helpers.retry
local rmdir = helpers.rmdir
+local mkdir = helpers.mkdir
local sleep = helpers.sleep
local read_file = helpers.read_file
local trim = helpers.trim
+local currentdir = helpers.funcs.getcwd
+local iswin = helpers.iswin
describe('fileio', function()
before_each(function()
@@ -24,6 +27,7 @@ describe('fileio', function()
os.remove('Xtest_startup_file2')
os.remove('Xtest_теÑÑ‚.md')
rmdir('Xtest_startup_swapdir')
+ rmdir('Xtest_backupdir')
end)
it('fsync() codepaths #8304', function()
@@ -88,6 +92,27 @@ describe('fileio', function()
eq('foo', bar_contents);
end)
+ it('backup with full path #11214', function()
+ clear()
+ mkdir('Xtest_backupdir')
+ command('set backup')
+ command('set backupdir=Xtest_backupdir//')
+ command('write Xtest_startup_file1')
+ feed('ifoo<esc>')
+ command('write')
+ feed('Abar<esc>')
+ command('write')
+
+ -- Backup filename = fullpath, separators replaced with "%".
+ local backup_file_name = string.gsub(currentdir()..'/Xtest_startup_file1',
+ iswin() and '[:/\\]' or '/', '%%') .. '~'
+ local foo_contents = trim(read_file('Xtest_backupdir/'..backup_file_name))
+ local foobar_contents = trim(read_file('Xtest_startup_file1'))
+
+ eq('foobar', foobar_contents);
+ eq('foo', foo_contents);
+ end)
+
it('readfile() on multibyte filename #10586', function()
clear()
local text = {
diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua
index 9c37e55f42..e5d4444b92 100644
--- a/test/functional/core/job_spec.lua
+++ b/test/functional/core/job_spec.lua
@@ -26,6 +26,7 @@ describe('jobs', function()
before_each(function()
clear()
+
channel = nvim('get_api_info')[1]
nvim('set_var', 'channel', channel)
source([[
@@ -48,6 +49,57 @@ describe('jobs', function()
]])
end)
+ it('must specify env option as a dict', function()
+ command("let g:job_opts.env = v:true")
+ local _, err = pcall(function()
+ if iswin() then
+ nvim('command', "let j = jobstart('set', g:job_opts)")
+ else
+ nvim('command', "let j = jobstart('env', g:job_opts)")
+ end
+ end)
+ ok(string.find(err, "E475: Invalid argument: env") ~= nil)
+ end)
+
+ it('append environment #env', function()
+ nvim('command', "let $VAR = 'abc'")
+ nvim('command', "let g:job_opts.env = {'TOTO': 'hello world'}")
+ if iswin() then
+ nvim('command', [[call jobstart('echo %TOTO% %VAR%', g:job_opts)]])
+ else
+ nvim('command', [[call jobstart('echo $TOTO $VAR', g:job_opts)]])
+ end
+
+ expect_msg_seq({
+ {'notification', 'stdout', {0, {'hello world abc', ''}}},
+ })
+ end)
+
+ it('replace environment #env', function()
+ nvim('command', "let $VAR = 'abc'")
+ nvim('command', "let g:job_opts.env = {'TOTO': 'hello world'}")
+ nvim('command', "let g:job_opts.clear_env = 1")
+
+ -- libuv ensures that certain "required" environment variables are
+ -- preserved if the user doesn't provide them in a custom environment
+ -- https://github.com/libuv/libuv/blob/635e0ce6073c5fbc96040e336b364c061441b54b/src/win/process.c#L672
+ -- https://github.com/libuv/libuv/blob/635e0ce6073c5fbc96040e336b364c061441b54b/src/win/process.c#L48-L60
+ --
+ -- Rather than expecting a completely empty environment, ensure that $VAR
+ -- is *not* in the environment but $TOTO is.
+ if iswin() then
+ nvim('command', [[call jobstart('echo %TOTO% %VAR%', g:job_opts)]])
+ expect_msg_seq({
+ {'notification', 'stdout', {0, {'hello world %VAR%', ''}}}
+ })
+ else
+ nvim('command', [[call jobstart('echo $TOTO $VAR', g:job_opts)]])
+ expect_msg_seq({
+ {'notification', 'stdout', {0, {'hello world', ''}}}
+ })
+ end
+ end)
+
it('uses &shell and &shellcmdflag if passed a string', function()
nvim('command', "let $VAR = 'abc'")
if iswin() then
@@ -202,8 +254,7 @@ describe('jobs', function()
if helpers.isCI('travis') and os.getenv('CC') == 'gcc-4.9'
and helpers.is_os('mac') then
-- XXX: Hangs Travis macOS since e9061117a5b8f195c3f26a5cb94e18ddd7752d86.
- pending("[Hangs on Travis macOS. #5002]", function() end)
- return
+ pending("[Hangs on Travis macOS. #5002]")
end
nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
@@ -963,9 +1014,6 @@ describe("pty process teardown", function()
|
]])
end)
- after_each(function()
- screen:detach()
- end)
it("does not prevent/delay exit. #4798 #4900", function()
if helpers.pending_win32(pending) then return end
diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua
index b793e531c9..37a9f0b836 100644
--- a/test/functional/core/main_spec.lua
+++ b/test/functional/core/main_spec.lua
@@ -67,7 +67,7 @@ describe('Command-line option', function()
|
|
]], {
- [1] = {foreground = 4210943},
+ [1] = {foreground = tonumber('0x4040ff'), fg_indexed=true},
[2] = {bold = true, reverse = true}
})
feed('i:cq<CR>')
diff --git a/test/functional/eval/api_functions_spec.lua b/test/functional/eval/api_functions_spec.lua
index 4fbd08f102..f527aff33f 100644
--- a/test/functional/eval/api_functions_spec.lua
+++ b/test/functional/eval/api_functions_spec.lua
@@ -144,7 +144,6 @@ describe('eval-API', function()
{5:~ }|
|
]])
- screen:detach()
end)
it('cannot be called from sandbox', function()
diff --git a/test/functional/eval/environ_spec.lua b/test/functional/eval/environ_spec.lua
index 4c2adcf1bf..54d2dc960b 100644
--- a/test/functional/eval/environ_spec.lua
+++ b/test/functional/eval/environ_spec.lua
@@ -10,6 +10,7 @@ describe('environment variables', function()
eq("", environ()['EMPTY_VAR'])
eq(nil, environ()['DOES_NOT_EXIST'])
end)
+
it('exists() handles empty env variable', function()
clear({env={EMPTY_VAR=""}})
eq(1, exists('$EMPTY_VAR'))
diff --git a/test/functional/eval/fnamemodify_spec.lua b/test/functional/eval/fnamemodify_spec.lua
index fe6b50a544..d54a6db417 100644
--- a/test/functional/eval/fnamemodify_spec.lua
+++ b/test/functional/eval/fnamemodify_spec.lua
@@ -3,8 +3,14 @@ local clear = helpers.clear
local eq = helpers.eq
local iswin = helpers.iswin
local fnamemodify = helpers.funcs.fnamemodify
+local getcwd = helpers.funcs.getcwd
local command = helpers.command
local write_file = helpers.write_file
+local alter_slashes = helpers.alter_slashes
+
+local function eq_slashconvert(expected, got)
+ eq(alter_slashes(expected), alter_slashes(got))
+end
describe('fnamemodify()', function()
setup(function()
@@ -17,7 +23,7 @@ describe('fnamemodify()', function()
os.remove('Xtest-fnamemodify.txt')
end)
- it('works', function()
+ it('handles the root path', function()
local root = helpers.pathroot()
eq(root, fnamemodify([[/]], ':p:h'))
eq(root, fnamemodify([[/]], ':p'))
@@ -36,4 +42,115 @@ describe('fnamemodify()', function()
it(':8 works', function()
eq('Xtest-fnamemodify.txt', fnamemodify([[Xtest-fnamemodify.txt]], ':8'))
end)
+
+ it('handles examples from ":help filename-modifiers"', function()
+ local filename = "src/version.c"
+ local cwd = getcwd()
+
+ eq_slashconvert(cwd .. '/src/version.c', fnamemodify(filename, ':p'))
+
+ eq_slashconvert('src/version.c', fnamemodify(filename, ':p:.'))
+ eq_slashconvert(cwd .. '/src', fnamemodify(filename, ':p:h'))
+ eq_slashconvert(cwd .. '', fnamemodify(filename, ':p:h:h'))
+ eq('version.c', fnamemodify(filename, ':p:t'))
+ eq_slashconvert(cwd .. '/src/version', fnamemodify(filename, ':p:r'))
+
+ eq_slashconvert(cwd .. '/src/main.c', fnamemodify(filename, ':s?version?main?:p'))
+
+ local converted_cwd = cwd:gsub('/', '\\')
+ eq(converted_cwd .. '\\src\\version.c', fnamemodify(filename, ':p:gs?/?\\\\?'))
+
+ eq('src', fnamemodify(filename, ':h'))
+ eq('version.c', fnamemodify(filename, ':t'))
+ eq_slashconvert('src/version', fnamemodify(filename, ':r'))
+ eq('version', fnamemodify(filename, ':t:r'))
+ eq('c', fnamemodify(filename, ':e'))
+
+ eq_slashconvert('src/main.c', fnamemodify(filename, ':s?version?main?'))
+ end)
+
+ it('handles advanced examples from ":help filename-modifiers"', function()
+ local filename = "src/version.c.gz"
+
+ eq('gz', fnamemodify(filename, ':e'))
+ eq('c.gz', fnamemodify(filename, ':e:e'))
+ eq('c.gz', fnamemodify(filename, ':e:e:e'))
+
+ eq('c', fnamemodify(filename, ':e:e:r'))
+
+ eq_slashconvert('src/version.c', fnamemodify(filename, ':r'))
+ eq('c', fnamemodify(filename, ':r:e'))
+
+ eq_slashconvert('src/version', fnamemodify(filename, ':r:r'))
+ eq_slashconvert('src/version', fnamemodify(filename, ':r:r:r'))
+ end)
+
+ it('handles :h', function()
+ eq('.', fnamemodify('hello.txt', ':h'))
+
+ eq_slashconvert('path/to', fnamemodify('path/to/hello.txt', ':h'))
+ end)
+
+ it('handles :t', function()
+ eq('hello.txt', fnamemodify('hello.txt', ':t'))
+ eq_slashconvert('hello.txt', fnamemodify('path/to/hello.txt', ':t'))
+ end)
+
+ it('handles :r', function()
+ eq('hello', fnamemodify('hello.txt', ':r'))
+ eq_slashconvert('path/to/hello', fnamemodify('path/to/hello.txt', ':r'))
+ end)
+
+ it('handles :e', function()
+ eq('txt', fnamemodify('hello.txt', ':e'))
+ eq_slashconvert('txt', fnamemodify('path/to/hello.txt', ':e'))
+ end)
+
+ it('handles regex replacements', function()
+ eq('content-there-here.txt', fnamemodify('content-here-here.txt', ':s/here/there/'))
+ eq('content-there-there.txt', fnamemodify('content-here-here.txt', ':gs/here/there/'))
+ end)
+
+ it('handles shell escape', function()
+ local expected
+
+ if iswin() then
+ -- we expand with double-quotes on Windows
+ expected = [["hello there! quote ' newline]] .. '\n' .. [["]]
+ else
+ expected = [['hello there! quote '\'' newline]] .. '\n' .. [[']]
+ end
+
+ eq(expected, fnamemodify("hello there! quote ' newline\n", ':S'))
+ end)
+
+ it('can combine :e and :r', function()
+ -- simple, single extension filename
+ eq('c', fnamemodify('a.c', ':e'))
+ eq('c', fnamemodify('a.c', ':e:e'))
+ eq('c', fnamemodify('a.c', ':e:e:r'))
+ eq('c', fnamemodify('a.c', ':e:e:r:r'))
+
+ -- multi extension filename
+ eq('rb', fnamemodify('a.spec.rb', ':e:r'))
+ eq('rb', fnamemodify('a.spec.rb', ':e:r:r'))
+
+ eq('spec', fnamemodify('a.spec.rb', ':e:e:r'))
+ eq('spec', fnamemodify('a.spec.rb', ':e:e:r:r'))
+
+ eq('spec', fnamemodify('a.b.spec.rb', ':e:e:r'))
+ eq('b.spec', fnamemodify('a.b.spec.rb', ':e:e:e:r'))
+ eq('b', fnamemodify('a.b.spec.rb', ':e:e:e:r:r'))
+
+ eq('spec', fnamemodify('a.b.spec.rb', ':r:e'))
+ eq('b', fnamemodify('a.b.spec.rb', ':r:r:e'))
+
+ -- extraneous :e expansions
+ eq('c', fnamemodify('a.b.c.d.e', ':r:r:e'))
+ eq('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e'))
+
+ -- :e never includes the whole filename, so "a.b":e:e:e --> "b"
+ eq('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e'))
+ eq('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e:e'))
+ end)
end)
diff --git a/test/functional/eval/let_spec.lua b/test/functional/eval/let_spec.lua
index 63e18f943f..f8fcdfd41f 100644
--- a/test/functional/eval/let_spec.lua
+++ b/test/functional/eval/let_spec.lua
@@ -59,10 +59,6 @@ describe(':let', function()
end)
it("multibyte env var to child process #8398 #9267", function()
- if (not helpers.iswin()) and helpers.isCI() then
- -- Fails on non-Windows CI. Buffering/timing issue?
- pending('fails on unix CI', function() end)
- end
local cmd_get_child_env = "let g:env_from_child = system(['"..nvim_dir.."/printenv-test', 'NVIM_TEST'])"
command("let $NVIM_TEST = 'AìaB'")
command(cmd_get_child_env)
diff --git a/test/functional/eval/system_spec.lua b/test/functional/eval/system_spec.lua
index 0a478fd05c..1e4d760dbc 100644
--- a/test/functional/eval/system_spec.lua
+++ b/test/functional/eval/system_spec.lua
@@ -84,7 +84,7 @@ describe('system()', function()
it('does NOT run in shell', function()
if iswin() then
- eq("%PATH%\n", eval("system(['powershell', '-NoProfile', '-NoLogo', '-ExecutionPolicy', 'RemoteSigned', '-Command', 'echo', '%PATH%'])"))
+ eq("%PATH%\n", eval("system(['powershell', '-NoProfile', '-NoLogo', '-ExecutionPolicy', 'RemoteSigned', '-Command', 'Write-Output', '%PATH%'])"))
else
eq("* $PATH %PATH%\n", eval("system(['echo', '*', '$PATH', '%PATH%'])"))
end
@@ -121,10 +121,6 @@ describe('system()', function()
screen:attach()
end)
- after_each(function()
- screen:detach()
- end)
-
if iswin() then
local function test_more()
eq('root = true', eval([[get(split(system('"more" ".editorconfig"'), "\n"), 0, '')]]))
@@ -133,7 +129,7 @@ describe('system()', function()
eval([[system('"ping" "-n" "1" "127.0.0.1"')]])
eq(0, eval('v:shell_error'))
eq('"a b"\n', eval([[system('cmd /s/c "cmd /s/c "cmd /s/c "echo "a b""""')]]))
- eq('"a b"\n', eval([[system('powershell -NoProfile -NoLogo -ExecutionPolicy RemoteSigned -Command echo ''\^"a b\^"''')]]))
+ eq('"a b"\n', eval([[system('powershell -NoProfile -NoLogo -ExecutionPolicy RemoteSigned -Command Write-Output ''\^"a b\^"''')]]))
end
it('with shell=cmd.exe', function()
@@ -169,9 +165,9 @@ describe('system()', function()
it('works with powershell', function()
helpers.set_shell_powershell()
- eq('a\nb\n', eval([[system('echo a b')]]))
+ eq('a\nb\n', eval([[system('Write-Output a b')]]))
eq('C:\\\n', eval([[system('cd c:\; (Get-Location).Path')]]))
- eq('a b\n', eval([[system('echo "a b"')]]))
+ eq('a b\n', eval([[system('Write-Output "a b"')]]))
end)
end
diff --git a/test/functional/eval/timer_spec.lua b/test/functional/eval/timer_spec.lua
index 2ccb9cfbac..ef7df69fdb 100644
--- a/test/functional/eval/timer_spec.lua
+++ b/test/functional/eval/timer_spec.lua
@@ -111,7 +111,13 @@ describe('timers', function()
curbufmeths.set_lines(0, -1, true, {"ITEM 1", "ITEM 2"})
source([[
+ let g:cont = 0
func! AddItem(timer)
+ if !g:cont
+ return
+ endif
+ call timer_stop(a:timer)
+
call nvim_buf_set_lines(0, 2, 2, v:true, ['ITEM 3'])
" Meant to test for what Vim tests in Test_peek_and_get_char.
@@ -121,7 +127,7 @@ describe('timers', function()
endfunc
]])
nvim_async("command", "let g:c2 = getchar()")
- nvim_async("command", "call timer_start("..load_adjust(100)..", 'AddItem')")
+ nvim_async("command", "call timer_start("..load_adjust(100)..", 'AddItem', {'repeat': -1})")
screen:expect([[
ITEM 1 |
@@ -131,6 +137,7 @@ describe('timers', function()
{1:~ }|
^ |
]])
+ nvim_async("command", "let g:cont = 1")
screen:expect([[
ITEM 1 |
@@ -222,12 +229,17 @@ describe('timers', function()
source([[
let g:val = 0
func! MyHandler(timer)
+ while !g:val
+ return
+ endwhile
+ call timer_stop(a:timer)
+
echo "evil"
redraw
- let g:val = 1
+ let g:val = 2
endfunc
]])
- command("call timer_start(100, 'MyHandler', {'repeat': 1})")
+ command("call timer_start(100, 'MyHandler', {'repeat': -1})")
feed(":good")
screen:expect([[
|
@@ -237,6 +249,7 @@ describe('timers', function()
{0:~ }|
:good^ |
]])
+ command('let g:val = 1')
screen:expect{grid=[[
|
@@ -247,6 +260,6 @@ describe('timers', function()
:good^ |
]], intermediate=true, timeout=load_adjust(200)}
- eq(1, eval('g:val'))
+ eq(2, eval('g:val'))
end)
end)
diff --git a/test/functional/eval/wait_spec.lua b/test/functional/eval/wait_spec.lua
index ad7d1574cb..ee95e02a7f 100644
--- a/test/functional/eval/wait_spec.lua
+++ b/test/functional/eval/wait_spec.lua
@@ -58,8 +58,11 @@ describe('wait()', function()
endfunction
]])
- nvim('set_var', 'counter', 0)
- eq(-1, call('wait', 20, 'Count() >= 5', 99999))
+ -- XXX: flaky (#11137)
+ helpers.retry(nil, nil, function()
+ nvim('set_var', 'counter', 0)
+ eq(-1, call('wait', 20, 'Count() >= 5', 99999))
+ end)
nvim('set_var', 'counter', 0)
eq(0, call('wait', 10000, 'Count() >= 5', 5))
diff --git a/test/functional/ex_cmds/dict_notifications_spec.lua b/test/functional/ex_cmds/dict_notifications_spec.lua
index 48e7e05e4c..5c67431221 100644
--- a/test/functional/ex_cmds/dict_notifications_spec.lua
+++ b/test/functional/ex_cmds/dict_notifications_spec.lua
@@ -357,4 +357,18 @@ describe('VimL dictionary notifications', function()
eq(2, eval('1+1')) -- Still alive?
end)
+ it('does not cause use-after-free when unletting from callback', function()
+ source([[
+ let g:called = 0
+ function W(...) abort
+ unlet g:d
+ let g:called = 1
+ endfunction
+ let g:d = {}
+ call dictwatcheradd(g:d, '*', function('W'))
+ let g:d.foo = 123
+ ]])
+ eq(1, eval('g:called'))
+ end)
+
end)
diff --git a/test/functional/ex_cmds/drop_spec.lua b/test/functional/ex_cmds/drop_spec.lua
index d6da0d8e88..762ea3d166 100644
--- a/test/functional/ex_cmds/drop_spec.lua
+++ b/test/functional/ex_cmds/drop_spec.lua
@@ -19,10 +19,6 @@ describe(":drop", function()
command("set laststatus=2 shortmess-=F")
end)
- after_each(function()
- screen:detach()
- end)
-
it("works like :e when called with only one window open", function()
feed_command("drop tmp1.vim")
screen:expect([[
diff --git a/test/functional/ex_cmds/highlight_spec.lua b/test/functional/ex_cmds/highlight_spec.lua
index 25968b8204..1cd6759a53 100644
--- a/test/functional/ex_cmds/highlight_spec.lua
+++ b/test/functional/ex_cmds/highlight_spec.lua
@@ -13,10 +13,6 @@ describe(':highlight', function()
screen:attach()
end)
- after_each(function()
- screen:detach()
- end)
-
it('invalid color name', function()
eq('Vim(highlight):E421: Color name or number not recognized: ctermfg=#181818',
exc_exec("highlight normal ctermfg=#181818"))
diff --git a/test/functional/ex_cmds/script_spec.lua b/test/functional/ex_cmds/script_spec.lua
index 4e57d2755d..0a772c559b 100644
--- a/test/functional/ex_cmds/script_spec.lua
+++ b/test/functional/ex_cmds/script_spec.lua
@@ -22,7 +22,7 @@ describe('script_get-based command', function()
%s %s
endif
]])):format(cmd, garbage)))
- eq('', meths.command_output('messages'))
+ eq('', meths.exec('messages', true))
if check_neq then
neq(0, exc_exec(dedent([[
%s %s
@@ -37,7 +37,7 @@ describe('script_get-based command', function()
EOF
endif
]])):format(cmd, garbage)))
- eq('', meths.command_output('messages'))
+ eq('', meths.exec('messages', true))
if check_neq then
eq(true, pcall(source, (dedent([[
let g:exc = 0
diff --git a/test/functional/ex_cmds/sign_spec.lua b/test/functional/ex_cmds/sign_spec.lua
index 9d08a66625..891cfe1670 100644
--- a/test/functional/ex_cmds/sign_spec.lua
+++ b/test/functional/ex_cmds/sign_spec.lua
@@ -16,8 +16,8 @@ describe('sign', function()
nvim('command', 'sign place 34 line=3 name=Foo buffer='..buf2)
-- now unplace without specifying a buffer
nvim('command', 'sign unplace 34')
- eq("--- Signs ---\n", nvim('command_output', 'sign place buffer='..buf1))
- eq("--- Signs ---\n", nvim('command_output', 'sign place buffer='..buf2))
+ eq("--- Signs ---\n", nvim('exec', 'sign place buffer='..buf1, true))
+ eq("--- Signs ---\n", nvim('exec', 'sign place buffer='..buf2, true))
end)
end)
end)
diff --git a/test/functional/ex_cmds/write_spec.lua b/test/functional/ex_cmds/write_spec.lua
index 3f54ff6f41..4f526ddfca 100644
--- a/test/functional/ex_cmds/write_spec.lua
+++ b/test/functional/ex_cmds/write_spec.lua
@@ -41,7 +41,7 @@ describe(':write', function()
command("silent !ln -s test_bkc_file.txt test_bkc_link.txt")
end
if eval('v:shell_error') ~= 0 then
- pending('Cannot create symlink', function()end)
+ pending('Cannot create symlink')
end
source([[
edit test_bkc_link.txt
@@ -61,7 +61,7 @@ describe(':write', function()
command("silent !ln -s test_bkc_file.txt test_bkc_link.txt")
end
if eval('v:shell_error') ~= 0 then
- pending('Cannot create symlink', function()end)
+ pending('Cannot create symlink')
end
source([[
edit test_bkc_link.txt
@@ -75,8 +75,7 @@ describe(':write', function()
it("appends FIFO file", function()
-- mkfifo creates read-only .lnk files on Windows
if iswin() or eval("executable('mkfifo')") == 0 then
- pending('missing "mkfifo" command', function()end)
- return
+ pending('missing "mkfifo" command')
end
local text = "some fifo text from write_spec"
diff --git a/test/functional/example_spec.lua b/test/functional/example_spec.lua
index 883fe4ba63..f07f88b2b6 100644
--- a/test/functional/example_spec.lua
+++ b/test/functional/example_spec.lua
@@ -3,7 +3,10 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
-local clear, feed = helpers.clear, helpers.feed
+local clear = helpers.clear
+local command = helpers.command
+local eq = helpers.eq
+local feed = helpers.feed
describe('example', function()
local screen
@@ -33,4 +36,37 @@ describe('example', function()
|
]])
end)
+
+ it('override UI event-handler', function()
+ -- Example: override the "tabline_update" UI event handler.
+ --
+ -- screen.lua defines default handlers for UI events, but tests
+ -- may sometimes want to override a handler.
+
+ -- The UI must declare that it wants to handle the UI events.
+ -- For this example, we enable `ext_tabline`:
+ screen:detach()
+ screen = Screen.new(25, 5)
+ screen:attach({rgb=true, ext_tabline=true})
+
+ -- From ":help ui" we find that `tabline_update` receives `curtab` and
+ -- `tabs` objects. So we declare the UI handler like this:
+ local event_tabs, event_curtab
+ function screen:_handle_tabline_update(curtab, tabs)
+ event_curtab, event_tabs = curtab, tabs
+ end
+
+ -- Create a tabpage...
+ command('tabedit foo')
+
+ -- Use screen:expect{condition=…} to check the result.
+ screen:expect{condition=function()
+ eq({ id = 2 }, event_curtab)
+ eq({
+ {tab = { id = 1 }, name = '[No Name]'},
+ {tab = { id = 2 }, name = 'foo'},
+ },
+ event_tabs)
+ end}
+ end)
end)
diff --git a/test/functional/fixtures/CMakeLists.txt b/test/functional/fixtures/CMakeLists.txt
index dbcb157956..270540de2e 100644
--- a/test/functional/fixtures/CMakeLists.txt
+++ b/test/functional/fixtures/CMakeLists.txt
@@ -1,12 +1,12 @@
-add_executable(tty-test tty-test.c)
+add_executable(tty-test EXCLUDE_FROM_ALL tty-test.c)
target_link_libraries(tty-test ${LIBUV_LIBRARIES})
-add_executable(shell-test shell-test.c)
-add_executable(printargs-test printargs-test.c)
-add_executable(printenv-test printenv-test.c)
+add_executable(shell-test EXCLUDE_FROM_ALL shell-test.c)
+add_executable(printargs-test EXCLUDE_FROM_ALL printargs-test.c)
+add_executable(printenv-test EXCLUDE_FROM_ALL printenv-test.c)
if(WIN32)
set_target_properties(printenv-test PROPERTIES LINK_FLAGS -municode)
endif()
-add_executable(streams-test streams-test.c)
+add_executable(streams-test EXCLUDE_FROM_ALL streams-test.c)
target_link_libraries(streams-test ${LIBUV_LIBRARIES})
diff --git a/test/functional/fixtures/api_level_6.mpack b/test/functional/fixtures/api_level_6.mpack
index b348f86beb..1c939d5931 100644
--- a/test/functional/fixtures/api_level_6.mpack
+++ b/test/functional/fixtures/api_level_6.mpack
Binary files differ
diff --git a/test/functional/fixtures/lsp-test-rpc-server.lua b/test/functional/fixtures/lsp-test-rpc-server.lua
new file mode 100644
index 0000000000..798883ced0
--- /dev/null
+++ b/test/functional/fixtures/lsp-test-rpc-server.lua
@@ -0,0 +1,459 @@
+local protocol = require 'vim.lsp.protocol'
+
+-- Internal utility methods.
+
+-- TODO replace with a better implementation.
+local function json_encode(data)
+ local status, result = pcall(vim.fn.json_encode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+local function json_decode(data)
+ local status, result = pcall(vim.fn.json_decode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+
+local function message_parts(sep, ...)
+ local parts = {}
+ for i = 1, select("#", ...) do
+ local arg = select(i, ...)
+ if arg ~= nil then
+ table.insert(parts, arg)
+ end
+ end
+ return table.concat(parts, sep)
+end
+
+-- Assert utility methods
+
+local function assert_eq(a, b, ...)
+ if not vim.deep_equal(a, b) then
+ error(message_parts(": ",
+ ..., "assert_eq failed",
+ string.format("left == %q, right == %q", vim.inspect(a), vim.inspect(b))
+ ))
+ end
+end
+
+local function format_message_with_content_length(encoded_message)
+ return table.concat {
+ 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n';
+ encoded_message;
+ }
+end
+
+-- Server utility methods.
+
+local function read_message()
+ local line = io.read("*l")
+ local length = line:lower():match("content%-length:%s*(%d+)")
+ return assert(json_decode(io.read(2 + length):sub(2)), "read_message.json_decode")
+end
+
+local function send(payload)
+ io.stdout:write(format_message_with_content_length(json_encode(payload)))
+end
+
+local function respond(id, err, result)
+ assert(type(id) == 'number', "id must be a number")
+ send { jsonrpc = "2.0"; id = id, error = err, result = result }
+end
+
+local function notify(method, params)
+ assert(type(method) == 'string', "method must be a string")
+ send { method = method, params = params or {} }
+end
+
+local function expect_notification(method, params, ...)
+ local message = read_message()
+ assert_eq(method, message.method,
+ ..., "expect_notification", "method")
+ assert_eq(params, message.params,
+ ..., "expect_notification", method, "params")
+ assert_eq({jsonrpc = "2.0"; method=method, params=params}, message,
+ ..., "expect_notification", "message")
+end
+
+local function expect_request(method, callback, ...)
+ local req = read_message()
+ assert_eq(method, req.method,
+ ..., "expect_request", "method")
+ local err, result = callback(req.params)
+ respond(req.id, err, result)
+end
+
+io.stderr:setvbuf("no")
+
+local function skeleton(config)
+ local on_init = assert(config.on_init)
+ local body = assert(config.body)
+ expect_request("initialize", function(params)
+ return nil, on_init(params)
+ end)
+ expect_notification("initialized", {})
+ body()
+ expect_request("shutdown", function()
+ return nil, {}
+ end)
+ expect_notification("exit", nil)
+end
+
+-- The actual tests.
+
+local tests = {}
+
+function tests.basic_init()
+ skeleton {
+ on_init = function(_params)
+ return { capabilities = {} }
+ end;
+ body = function()
+ notify('test')
+ end;
+ }
+end
+
+function tests.basic_check_capabilities()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ end;
+ }
+end
+
+function tests.basic_finish()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n") .. '\n';
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n") .. '\n';
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "boop"}, "\n") .. '\n'; };
+ }
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change_noeol()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n");
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "boop"}, "\n"); };
+ }
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+function tests.basic_check_buffer_open_and_change_multi()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n") .. '\n';
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "321"}, "\n") .. '\n'; };
+ }
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 4;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "boop"}, "\n") .. '\n'; };
+ }
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change_multi_and_close()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Full;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n") .. '\n';
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "321"}, "\n") .. '\n'; };
+ }
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 4;
+ };
+ contentChanges = {
+ { text = table.concat({"testing"; "boop"}, "\n") .. '\n'; };
+ }
+ })
+ expect_notification('textDocument/didClose', {
+ textDocument = {
+ uri = "file://";
+ };
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change_incremental()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Incremental;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n") .. '\n';
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ {
+ range = {
+ start = { line = 1; character = 0; };
+ ["end"] = { line = 2; character = 0; };
+ };
+ rangeLength = 4;
+ text = "boop\n";
+ };
+ }
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.basic_check_buffer_open_and_change_incremental_editting()
+ skeleton {
+ on_init = function(params)
+ local expected_capabilities = protocol.make_client_capabilities()
+ assert_eq(params.capabilities, expected_capabilities)
+ return {
+ capabilities = {
+ textDocumentSync = protocol.TextDocumentSyncKind.Incremental;
+ }
+ }
+ end;
+ body = function()
+ notify('start')
+ expect_notification('textDocument/didOpen', {
+ textDocument = {
+ languageId = "";
+ text = table.concat({"testing"; "123"}, "\n");
+ uri = "file://";
+ version = 0;
+ };
+ })
+ expect_notification('textDocument/didChange', {
+ textDocument = {
+ uri = "file://";
+ version = 3;
+ };
+ contentChanges = {
+ {
+ range = {
+ start = { line = 0; character = 0; };
+ ["end"] = { line = 1; character = 0; };
+ };
+ rangeLength = 4;
+ text = "testing\n\n";
+ };
+ }
+ })
+ expect_notification("finish")
+ notify('finish')
+ end;
+ }
+end
+
+function tests.invalid_header()
+ io.stdout:write("Content-length: \r\n")
+end
+
+-- Tests will be indexed by TEST_NAME
+
+local kill_timer = vim.loop.new_timer()
+kill_timer:start(_G.TIMEOUT or 1e3, 0, function()
+ kill_timer:stop()
+ kill_timer:close()
+ io.stderr:write("TIMEOUT")
+ os.exit(100)
+end)
+
+local test_name = _G.TEST_NAME -- lualint workaround
+assert(type(test_name) == 'string', 'TEST_NAME must be specified.')
+local status, err = pcall(assert(tests[test_name], "Test not found"))
+kill_timer:stop()
+kill_timer:close()
+if not status then
+ io.stderr:write(err)
+ os.exit(1)
+end
+os.exit(0)
diff --git a/test/functional/fixtures/printenv-test.c b/test/functional/fixtures/printenv-test.c
index 5ac076f653..0e68129543 100644
--- a/test/functional/fixtures/printenv-test.c
+++ b/test/functional/fixtures/printenv-test.c
@@ -44,7 +44,7 @@ int main(int argc, char **argv)
utf8_len,
NULL,
NULL);
- fprintf(stderr, "%s", utf8_value);
+ fprintf(stdout, "%s", utf8_value);
free(utf8_value);
#else
char *value = getenv(argv[1]);
@@ -52,8 +52,8 @@ int main(int argc, char **argv)
fprintf(stderr, "env var not found: %s", argv[1]);
return 1;
}
- // Print to stderr to avoid buffering.
- fprintf(stderr, "%s", value);
+ fprintf(stdout, "%s", value);
#endif
+ fflush(stdout);
return 0;
}
diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua
index b29161e34c..eead1ea3e0 100644
--- a/test/functional/helpers.lua
+++ b/test/functional/helpers.lua
@@ -1,4 +1,5 @@
require('coxpcall')
+local busted = require('busted')
local luv = require('luv')
local lfs = require('lfs')
local mpack = require('mpack')
@@ -28,17 +29,15 @@ local module = {
}
local start_dir = lfs.currentdir()
--- XXX: NVIM_PROG takes precedence, QuickBuild sets it.
module.nvim_prog = (
- os.getenv('NVIM_PROG')
- or os.getenv('NVIM_PRG')
+ os.getenv('NVIM_PRG')
or global_helpers.test_build_dir .. '/bin/nvim'
)
-- Default settings for the test session.
module.nvim_set = (
- 'set shortmess+=IS background=light noswapfile noautoindent'
+ 'set shortmess+=IS background=light noswapfile noautoindent startofline'
..' laststatus=1 undodir=. directory=. viewdir=. backupdir=.'
- ..' belloff= wildoptions-=pum noshowcmd noruler nomore')
+ ..' belloff= wildoptions-=pum noshowcmd noruler nomore redrawdebug=invalid')
module.nvim_argv = {
module.nvim_prog, '-u', 'NONE', '-i', 'NONE',
'--cmd', module.nvim_set, '--embed'}
@@ -187,7 +186,12 @@ function module.expect_msg_seq(...)
if status then
return result
end
- final_error = cat_err(final_error, result)
+ local message = result
+ if type(result) == "table" then
+ -- 'eq' returns several things
+ message = result.message
+ end
+ final_error = cat_err(final_error, message)
end
error(final_error)
end
@@ -385,7 +389,7 @@ function module.retry(max, max_ms, fn)
end
luv.update_time() -- Update cached value of luv.now() (libuv: uv_now()).
if (max and tries >= max) or (luv.now() - start_time > timeout) then
- error("\nretry() attempts: "..tostring(tries).."\n"..tostring(result))
+ busted.fail(string.format("retry() attempts: %d\n%s", tries, tostring(result)), 2)
end
tries = tries + 1
luv.sleep(20) -- Avoid hot loop...
@@ -437,6 +441,7 @@ function module.new_argv(...)
'NVIM_LOG_FILE',
'NVIM_RPLUGIN_MANIFEST',
'GCOV_ERROR_FILE',
+ 'TMPDIR',
}) do
if not env_tbl[k] then
env_tbl[k] = os.getenv(k)
@@ -500,9 +505,16 @@ function module.source(code)
end
function module.set_shell_powershell()
+ local shell = iswin() and 'powershell' or 'pwsh'
+ assert(module.eval('executable("'..shell..'")'))
+ local cmd = 'Remove-Item -Force '..table.concat(iswin()
+ and {'alias:cat', 'alias:echo', 'alias:sleep'}
+ or {'alias:echo'}, ',')..';'
module.source([[
- set shell=powershell shellquote=( shellpipe=\| shellredir=> shellxquote=
- let &shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command Remove-Item -Force alias:sleep; Remove-Item -Force alias:cat;'
+ let &shell = ']]..shell..[['
+ set shellquote= shellpipe=\| shellxquote=
+ let &shellredir = '| Out-File -Encoding UTF8'
+ let &shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command ]]..cmd..[['
]])
end
@@ -699,7 +711,7 @@ module.curwinmeths = module.create_callindex(module.curwin)
module.curtabmeths = module.create_callindex(module.curtab)
function module.exec_lua(code, ...)
- return module.meths.execute_lua(code, {...})
+ return module.meths.exec_lua(code, {...})
end
function module.redir_exec(cmd)
diff --git a/test/functional/legacy/022_line_ending_spec.lua b/test/functional/legacy/022_line_ending_spec.lua
deleted file mode 100644
index fb4b782011..0000000000
--- a/test/functional/legacy/022_line_ending_spec.lua
+++ /dev/null
@@ -1,25 +0,0 @@
--- Tests for file with some lines ending in CTRL-M, some not
-
-local helpers = require('test.functional.helpers')(after_each)
-local clear, feed = helpers.clear, helpers.feed
-local feed_command, expect = helpers.feed_command, helpers.expect
-
-describe('line ending', function()
- setup(clear)
-
- it('is working', function()
- feed('i', [[
- this lines ends in a<C-V><C-M>
- this one doesn't
- this one does<C-V><C-M>
- and the last one doesn't]], '<ESC>')
-
- feed_command('set ta tx')
- feed_command('e!')
-
- expect("this lines ends in a\r\n"..
- "this one doesn't\n"..
- "this one does\r\n"..
- "and the last one doesn't")
- end)
-end)
diff --git a/test/functional/legacy/041_writing_and_reading_hundred_kbyte_spec.lua b/test/functional/legacy/041_writing_and_reading_hundred_kbyte_spec.lua
deleted file mode 100644
index b526d82519..0000000000
--- a/test/functional/legacy/041_writing_and_reading_hundred_kbyte_spec.lua
+++ /dev/null
@@ -1,43 +0,0 @@
--- Test for writing and reading a file of over 100 Kbyte
-
-local helpers = require('test.functional.helpers')(after_each)
-
-local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert
-local command, expect = helpers.command, helpers.expect
-local wait = helpers.wait
-
-describe('writing and reading a file of over 100 Kbyte', function()
- setup(clear)
-
- it('is working', function()
- insert([[
- This is the start
- This is the leader
- This is the middle
- This is the trailer
- This is the end]])
-
- feed('kY3000p2GY3000p')
- wait()
-
- command('w! test.out')
- command('%d')
- command('e! test.out')
- command('yank A')
- command('3003yank A')
- command('6005yank A')
- command('%d')
- command('0put a')
- command('$d')
- command('w!')
-
- expect([[
- This is the start
- This is the middle
- This is the end]])
- end)
-
- teardown(function()
- os.remove('test.out')
- end)
-end)
diff --git a/test/functional/legacy/045_folding_spec.lua b/test/functional/legacy/045_folding_spec.lua
index 6ca1176aea..1e5239ceac 100644
--- a/test/functional/legacy/045_folding_spec.lua
+++ b/test/functional/legacy/045_folding_spec.lua
@@ -14,9 +14,6 @@ describe('folding', function()
screen = Screen.new(20, 8)
screen:attach()
end)
- after_each(function()
- screen:detach()
- end)
it('creation, opening, moving (to the end) and closing', function()
insert([[
diff --git a/test/functional/legacy/063_match_and_matchadd_spec.lua b/test/functional/legacy/063_match_and_matchadd_spec.lua
index 518d79861b..a4f5d6ac5b 100644
--- a/test/functional/legacy/063_match_and_matchadd_spec.lua
+++ b/test/functional/legacy/063_match_and_matchadd_spec.lua
@@ -14,6 +14,10 @@ describe('063: Test for ":match", "matchadd()" and related functions', function(
it('is working', function()
local screen = Screen.new(40, 5)
screen:attach()
+ screen:set_default_attr_ids({
+ [0] = {bold = true, foreground = Screen.colors.Blue},
+ [1] = {background = Screen.colors.Red},
+ })
-- Check that "matcharg()" returns the correct group and pattern if a match
-- is defined.
@@ -126,22 +130,22 @@ describe('063: Test for ":match", "matchadd()" and related functions', function(
command("call matchaddpos('MyGroup1', [[1, 5], [1, 8, 3]], 10, 3)")
screen:expect([[
abcd{1:e}fg{1:hij}klmnop^q |
- ~ |
- ~ |
- ~ |
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
|
- ]], {[1] = {background = Screen.colors.Red}}, {{bold = true, foreground = Screen.colors.Blue}})
+ ]])
command("call clearmatches()")
command("call setline(1, 'abcdΣabcdef')")
command("call matchaddpos('MyGroup1', [[1, 4, 2], [1, 9, 2]])")
screen:expect([[
abc{1:dΣ}ab{1:cd}e^f |
- ~ |
- ~ |
- ~ |
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
|
- ]],{[1] = {background = Screen.colors.Red}}, {{bold = true, foreground = Screen.colors.Blue}})
+ ]])
end)
end)
diff --git a/test/functional/legacy/077_mf_hash_grow_spec.lua b/test/functional/legacy/077_mf_hash_grow_spec.lua
deleted file mode 100644
index 4719a3ecbf..0000000000
--- a/test/functional/legacy/077_mf_hash_grow_spec.lua
+++ /dev/null
@@ -1,52 +0,0 @@
--- Inserts 2 million lines with consecutive integers starting from 1
--- (essentially, the output of GNU's seq 1 2000000), writes them to Xtest
--- and calculates its cksum.
--- We need 2 million lines to trigger a call to mf_hash_grow(). If it would mess
--- up the lines the checksum would differ.
--- cksum is part of POSIX and so should be available on most Unixes.
--- If it isn't available then the test will be skipped.
-
-local helpers = require('test.functional.helpers')(after_each)
-
-local feed = helpers.feed
-local wait = helpers.wait
-local clear = helpers.clear
-local expect = helpers.expect
-local command = helpers.command
-
-describe('mf_hash_grow()', function()
- setup(clear)
-
- -- Check to see if cksum exists, otherwise skip the test
- local null = helpers.iswin() and 'nul' or '/dev/null'
- if os.execute('cksum --help >' .. null .. ' 2>&1') ~= 0 then
- pending('was not tested because cksum was not found', function() end)
- else
- it('is working', function()
- command('set fileformat=unix undolevels=-1')
-
- -- Fill the buffer with numbers 1 - 2000000
- command('let i = 1')
- command('while i <= 2000000 | call append(i, range(i, i + 99)) | let i += 100 | endwhile')
-
- -- Delete empty first line, save to Xtest, and clear buffer
- feed('ggdd<cr>')
- wait()
- command('w! Xtest')
- feed('ggdG<cr>')
- wait()
-
- -- Calculate the cksum of Xtest and delete first line
- command('r !cksum Xtest')
- feed('ggdd<cr>')
-
- -- Assert correct output of cksum.
- expect([[
- 3678979763 14888896 Xtest]])
- end)
- end
-
- teardown(function()
- os.remove('Xtest')
- end)
-end)
diff --git a/test/functional/legacy/084_curswant_spec.lua b/test/functional/legacy/084_curswant_spec.lua
deleted file mode 100644
index 42cb2fc56d..0000000000
--- a/test/functional/legacy/084_curswant_spec.lua
+++ /dev/null
@@ -1,49 +0,0 @@
--- Tests for curswant not changing when setting an option.
-
-local helpers = require('test.functional.helpers')(after_each)
-local insert, source = helpers.insert, helpers.source
-local clear, expect = helpers.clear, helpers.expect
-
-describe('curswant', function()
- setup(clear)
-
- -- luacheck: ignore 621 (Indentation)
- it('is working', function()
- insert([[
- start target options
- tabstop
- timeoutlen
- ttimeoutlen
- end target options]])
-
- source([[
- /^start target options$/+1,/^end target options$/-1 yank
- let target_option_names = split(@0)
- function TestCurswant(option_name)
- normal! ggf8j
- let curswant_before = winsaveview().curswant
- execute 'let' '&'.a:option_name '=' '&'.a:option_name
- let curswant_after = winsaveview().curswant
- return [a:option_name, curswant_before, curswant_after]
- endfunction
-
- new
- put =['1234567890', '12345']
- 1 delete _
- let result = []
- for option_name in target_option_names
- call add(result, TestCurswant(option_name))
- endfor
-
- new
- put =map(copy(result), 'join(v:val, '' '')')
- 1 delete _
- ]])
-
- -- Assert buffer contents.
- expect([[
- tabstop 7 4
- timeoutlen 7 7
- ttimeoutlen 7 7]])
- end)
-end)
diff --git a/test/functional/legacy/098_scrollbind_spec.lua b/test/functional/legacy/098_scrollbind_spec.lua
deleted file mode 100644
index d22aefdcbc..0000000000
--- a/test/functional/legacy/098_scrollbind_spec.lua
+++ /dev/null
@@ -1,48 +0,0 @@
--- Test for 'scrollbind' causing an unexpected scroll of one of the windows.
-
-local helpers = require('test.functional.helpers')(after_each)
-local source = helpers.source
-local clear, expect = helpers.clear, helpers.expect
-
-describe('scrollbind', function()
- setup(clear)
-
- it('is working', function()
- source([[
- set laststatus=0
- let g:totalLines = &lines * 20
- let middle = g:totalLines / 2
- wincmd n
- wincmd o
- for i in range(1, g:totalLines)
- call setline(i, 'LINE ' . i)
- endfor
- exe string(middle)
- normal zt
- normal M
- aboveleft vert new
- for i in range(1, g:totalLines)
- call setline(i, 'line ' . i)
- endfor
- exe string(middle)
- normal zt
- normal M
- setl scb | wincmd p
- setl scb
- wincmd w
- let topLineLeft = line('w0')
- wincmd p
- let topLineRight = line('w0')
- setl noscrollbind
- wincmd p
- setl noscrollbind
- q!
- %del _
- call setline(1, 'Difference between the top lines (left - right): ' . string(topLineLeft - topLineRight))
- brewind
- ]])
-
- -- Assert buffer contents.
- expect("Difference between the top lines (left - right): 0")
- end)
-end)
diff --git a/test/functional/legacy/104_let_assignment_spec.lua b/test/functional/legacy/104_let_assignment_spec.lua
deleted file mode 100644
index a03bb026f6..0000000000
--- a/test/functional/legacy/104_let_assignment_spec.lua
+++ /dev/null
@@ -1,54 +0,0 @@
--- Tests for :let.
-
-local helpers = require('test.functional.helpers')(after_each)
-local clear, source = helpers.clear, helpers.source
-local command, expect = helpers.command, helpers.expect
-
-describe(':let', function()
- setup(clear)
-
- it('is working', function()
- command('set runtimepath+=test/functional/fixtures')
-
- -- Test to not autoload when assigning. It causes internal error.
- source([[
- try
- let Test104#numvar = function('tr')
- $put ='OK: ' . string(Test104#numvar)
- catch
- $put ='FAIL: ' . v:exception
- endtry
- let a = 1
- let b = 2
- for letargs in ['a b', '{0 == 1 ? "a" : "b"}', '{0 == 1 ? "a" : "b"} a', 'a {0 == 1 ? "a" : "b"}']
- try
- redir => messages
- execute 'let' letargs
- redir END
- $put ='OK:'
- $put =split(substitute(messages, '\n', '\0 ', 'g'), '\n')
- catch
- $put ='FAIL: ' . v:exception
- redir END
- endtry
- endfor]])
-
- -- Remove empty line
- command('1d')
-
- -- Assert buffer contents.
- expect([[
- OK: function('tr')
- OK:
- a #1
- b #2
- OK:
- b #2
- OK:
- b #2
- a #1
- OK:
- a #1
- b #2]])
- end)
-end)
diff --git a/test/functional/legacy/assert_spec.lua b/test/functional/legacy/assert_spec.lua
index 8df2d89b70..3cb5d97869 100644
--- a/test/functional/legacy/assert_spec.lua
+++ b/test/functional/legacy/assert_spec.lua
@@ -3,6 +3,7 @@ local nvim, call = helpers.meths, helpers.call
local clear, eq = helpers.clear, helpers.eq
local source, command = helpers.source, helpers.command
local exc_exec = helpers.exc_exec
+local eval = helpers.eval
local function expected_errors(errors)
eq(errors, nvim.get_vvar('errors'))
@@ -47,18 +48,18 @@ describe('assert function:', function()
end)
it('should not change v:errors when expected is equal to actual', function()
- call('assert_equal', '', '')
- call('assert_equal', 'string', 'string')
+ eq(0, call('assert_equal', '', ''))
+ eq(0, call('assert_equal', 'string', 'string'))
expected_empty()
end)
it('should change v:errors when expected is not equal to actual', function()
- call('assert_equal', 0, {0})
+ eq(1, call('assert_equal', 0, {0}))
expected_errors({'Expected 0 but got [0]'})
end)
it('should change v:errors when expected is not equal to actual', function()
- call('assert_equal', 0, "0")
+ eq(1, call('assert_equal', 0, "0"))
expected_errors({"Expected 0 but got '0'"})
end)
@@ -96,13 +97,13 @@ describe('assert function:', function()
-- assert_notequal({expected}, {actual}[, {msg}])
describe('assert_notequal', function()
it('should not change v:errors when expected differs from actual', function()
- call('assert_notequal', 'foo', 4)
- call('assert_notequal', {1, 2, 3}, 'foo')
+ eq(0, call('assert_notequal', 'foo', 4))
+ eq(0, call('assert_notequal', {1, 2, 3}, 'foo'))
expected_empty()
end)
it('should change v:errors when expected is equal to actual', function()
- call('assert_notequal', 'foo', 'foo')
+ eq(1, call('assert_notequal', 'foo', 'foo'))
expected_errors({"Expected not equal to 'foo'"})
end)
end)
@@ -110,13 +111,13 @@ describe('assert function:', function()
-- assert_false({actual}, [, {msg}])
describe('assert_false', function()
it('should not change v:errors when actual is false', function()
- call('assert_false', 0)
- call('assert_false', false)
+ eq(0, call('assert_false', 0))
+ eq(0, call('assert_false', false))
expected_empty()
end)
it('should change v:errors when actual is not false', function()
- call('assert_false', 1)
+ eq(1, call('assert_false', 1))
expected_errors({'Expected False but got 1'})
end)
@@ -129,14 +130,14 @@ describe('assert function:', function()
-- assert_true({actual}, [, {msg}])
describe('assert_true', function()
it('should not change v:errors when actual is true', function()
- call('assert_true', 1)
- call('assert_true', -1) -- In Vim script, non-zero Numbers are TRUE.
- call('assert_true', true)
+ eq(0, call('assert_true', 1))
+ eq(0, call('assert_true', -1)) -- In Vim script, non-zero Numbers are TRUE.
+ eq(0, call('assert_true', true))
expected_empty()
end)
it('should change v:errors when actual is not true', function()
- call('assert_true', 1.5)
+ eq(1, call('assert_true', 1.5))
expected_errors({'Expected True but got 1.5'})
end)
end)
@@ -233,20 +234,31 @@ describe('assert function:', function()
-- assert_fails({cmd}, [, {error}])
describe('assert_fails', function()
it('should change v:errors when error does not match v:errmsg', function()
- command([[call assert_fails('xxx', {})]])
+ eq(1, eval([[assert_fails('xxx', {})]]))
command([[call assert_match("Expected {} but got 'E731:", v:errors[0])]])
expected_errors({"Expected {} but got 'E731: using Dictionary as a String'"})
end)
it('should not change v:errors when cmd errors', function()
- call('assert_fails', 'NonexistentCmd')
+ eq(0, eval([[assert_fails('NonexistentCmd')]]))
expected_empty()
end)
it('should change v:errors when cmd succeeds', function()
- call('assert_fails', 'call empty("")')
+ eq(1, eval([[assert_fails('call empty("")', '')]]))
expected_errors({'command did not fail: call empty("")'})
end)
+
+ it('can specify and get a message about what failed', function()
+ eq(1, eval([[assert_fails('xxx', {}, 'stupid')]]))
+ command([[call assert_match("stupid: Expected {} but got 'E731:", v:errors[0])]])
+ expected_errors({"stupid: Expected {} but got 'E731: using Dictionary as a String'"})
+ end)
+
+ it('can specify and get a message even when cmd succeeds', function()
+ eq(1, eval([[assert_fails('echo', '', 'echo command')]]))
+ expected_errors({'command did not fail: echo command'})
+ end)
end)
-- assert_inrange({lower}, {upper}, {actual}[, {msg}])
@@ -277,7 +289,7 @@ describe('assert function:', function()
-- assert_report({msg})
describe('assert_report()', function()
it('should add a message to v:errors', function()
- command("call assert_report('something is wrong')")
+ eq(1, call('assert_report', 'something is wrong'))
command("call assert_match('something is wrong', v:errors[0])")
command('call remove(v:errors, 0)')
expected_empty()
@@ -291,7 +303,7 @@ describe('assert function:', function()
try
nocommand
catch
- call assert_exception('E492')
+ call assert_equal(0, assert_exception('E492'))
endtry
]])
expected_empty()
@@ -304,9 +316,9 @@ describe('assert function:', function()
catch
try
" illegal argument, get NULL for error
- call assert_exception([])
+ call assert_equal(1, assert_exception([]))
catch
- call assert_exception('E730')
+ call assert_equal(0, assert_exception('E730'))
endtry
endtry
]])
diff --git a/test/functional/legacy/breakindent_spec.lua b/test/functional/legacy/breakindent_spec.lua
deleted file mode 100644
index fd25e809e0..0000000000
--- a/test/functional/legacy/breakindent_spec.lua
+++ /dev/null
@@ -1,214 +0,0 @@
--- Test for breakindent
-
-local helpers = require('test.functional.helpers')(after_each)
-local feed, insert = helpers.feed, helpers.insert
-local clear, feed_command, expect = helpers.clear, helpers.feed_command, helpers.expect
-
-describe('breakindent', function()
- setup(clear)
-
- -- luacheck: ignore 621 (Indentation)
- -- luacheck: ignore 613 (Trailing whitespace in a string)
- -- luacheck: ignore 611 (Line contains only whitespaces)
- it('is working', function()
- insert('dummy text')
-
- feed_command('set wildchar=^E')
- feed_command('10new')
- feed_command('vsp')
- feed_command('vert resize 20')
- feed_command([[put =\"\tabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP\"]])
- feed_command('set ts=4 sw=4 sts=4 breakindent')
- feed_command('fu! ScreenChar(line, width)')
- feed_command(' let c=""')
- feed_command(' for i in range(1,a:width)')
- feed_command(' let c.=nr2char(screenchar(a:line, i))')
- feed_command(' endfor')
- feed_command([[ let c.="\n"]])
- feed_command(' for i in range(1,a:width)')
- feed_command(' let c.=nr2char(screenchar(a:line+1, i))')
- feed_command(' endfor')
- feed_command([[ let c.="\n"]])
- feed_command(' for i in range(1,a:width)')
- feed_command(' let c.=nr2char(screenchar(a:line+2, i))')
- feed_command(' endfor')
- feed_command(' return c')
- feed_command('endfu')
- feed_command('fu DoRecordScreen()')
- feed_command(' wincmd l')
- feed_command([[ $put =printf(\"\n%s\", g:test)]])
- feed_command(' $put =g:line1')
- feed_command(' wincmd p')
- feed_command('endfu')
- feed_command('set briopt=min:0')
- feed_command('let g:test="Test 1: Simple breakindent"')
- feed_command('let line1=ScreenChar(line("."),8)')
- feed_command('call DoRecordScreen()')
- feed_command('let g:test="Test 2: Simple breakindent + sbr=>>"')
- feed_command('set sbr=>>')
- feed_command('let line1=ScreenChar(line("."),8)')
- feed_command('call DoRecordScreen()')
- feed_command('let g:test ="Test 3: Simple breakindent + briopt:sbr"')
- feed_command('set briopt=sbr,min:0 sbr=++')
- feed_command('let line1=ScreenChar(line("."),8)')
- feed_command('call DoRecordScreen()')
- feed_command('let g:test ="Test 4: Simple breakindent + min width: 18"')
- feed_command('set sbr= briopt=min:18')
- feed_command('let line1=ScreenChar(line("."),8)')
- feed_command('call DoRecordScreen()')
- feed_command('let g:test =" Test 5: Simple breakindent + shift by 2"')
- feed_command('set briopt=shift:2,min:0')
- feed_command('let line1=ScreenChar(line("."),8)')
- feed_command('call DoRecordScreen()')
- feed_command('let g:test=" Test 6: Simple breakindent + shift by -1"')
- feed_command('set briopt=shift:-1,min:0')
- feed_command('let line1=ScreenChar(line("."),8)')
- feed_command('call DoRecordScreen()')
- feed_command('let g:test=" Test 7: breakindent + shift by +1 + nu + sbr=? briopt:sbr"')
- feed_command('set briopt=shift:1,sbr,min:0 nu sbr=? nuw=4')
- feed_command('let line1=ScreenChar(line("."),10)')
- feed_command('call DoRecordScreen()')
- feed_command('let g:test=" Test 8: breakindent + shift:1 + nu + sbr=# list briopt:sbr"')
- feed_command('set briopt=shift:1,sbr,min:0 nu sbr=# list lcs&vi')
- feed_command('let line1=ScreenChar(line("."),10)')
- feed_command('call DoRecordScreen()')
- feed_command([[let g:test=" Test 9: breakindent + shift by +1 + 'nu' + sbr=# list"]])
- feed_command('set briopt-=sbr')
- feed_command('let line1=ScreenChar(line("."),10)')
- feed_command('call DoRecordScreen()')
- feed_command([[let g:test=" Test 10: breakindent + shift by +1 + 'nu' + sbr=~ cpo+=n"]])
- feed_command('set cpo+=n sbr=~ nu nuw=4 nolist briopt=sbr,min:0')
- feed_command('let line1=ScreenChar(line("."),10)')
- feed_command('call DoRecordScreen()')
- feed_command('wincmd p')
- feed_command([[let g:test="\n Test 11: strdisplaywidth when breakindent is on"]])
- feed_command('set cpo-=n sbr=>> nu nuw=4 nolist briopt= ts=4')
- -- Skip leading tab when calculating text width.
- feed_command('let text=getline(2)')
- -- Text wraps 3 times.
- feed_command('let width = strlen(text[1:])+indent(2)*4+strlen(&sbr)*3')
- feed_command('$put =g:test')
- feed_command([[$put =printf(\"strdisplaywidth: %d == calculated: %d\", strdisplaywidth(text), width)]])
- feed_command([[let g:str="\t\t\t\t\t{"]])
- feed_command('let g:test=" Test 12: breakindent + long indent"')
- feed_command('wincmd p')
- feed_command('set all& breakindent linebreak briopt=min:10 nu numberwidth=3 ts=4')
- feed_command('$put =g:str')
- feed('zt')
- feed_command('let line1=ScreenChar(1,10)')
- feed_command('wincmd p')
- feed_command('call DoRecordScreen()')
-
- -- Test, that the string " a\tb\tc\td\te" is correctly displayed in a
- -- 20 column wide window (see bug report
- -- https://groups.google.com/d/msg/vim_dev/ZOdg2mc9c9Y/TT8EhFjEy0IJ ).
- feed_command('only')
- feed_command('vert 20new')
- feed_command('set all& breakindent briopt=min:10')
- feed_command([[call setline(1, [" a\tb\tc\td\te", " z y x w v"])]])
- feed_command([[/^\s*a]])
- feed('fbgjyl')
- feed_command('let line1 = @0')
- feed_command([[?^\s*z]])
- feed('fygjyl')
- feed_command('let line2 = @0')
- feed_command('quit!')
- feed_command([[$put ='Test 13: breakindent with wrapping Tab']])
- feed_command('$put =line1')
- feed_command('$put =line2')
-
- feed_command('let g:test="Test 14: breakindent + visual blockwise delete #1"')
- feed_command('set all& breakindent shada+=nX-test-breakindent.shada')
- feed_command('30vnew')
- feed_command('normal! 3a1234567890')
- feed_command('normal! a abcde')
- feed_command([[exec "normal! 0\<C-V>tex"]])
- feed_command('let line1=ScreenChar(line("."),8)')
- feed_command('call DoRecordScreen()')
-
- feed_command('let g:test="Test 15: breakindent + visual blockwise delete #2"')
- feed_command('%d')
- feed_command('normal! 4a1234567890')
- feed_command([[exec "normal! >>\<C-V>3f0x"]])
- feed_command('let line1=ScreenChar(line("."),20)')
- feed_command('call DoRecordScreen()')
- feed_command('quit!')
-
- -- Assert buffer contents.
- expect([[
-
- abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP
-
- Test 1: Simple breakindent
- abcd
- qrst
- GHIJ
-
- Test 2: Simple breakindent + sbr=>>
- abcd
- >>qr
- >>EF
-
- Test 3: Simple breakindent + briopt:sbr
- abcd
- ++ qrst
- ++ GHIJ
-
- Test 4: Simple breakindent + min width: 18
- abcd
- qrstuv
- IJKLMN
-
- Test 5: Simple breakindent + shift by 2
- abcd
- qr
- EF
-
- Test 6: Simple breakindent + shift by -1
- abcd
- qrstu
- HIJKL
-
- Test 7: breakindent + shift by +1 + nu + sbr=? briopt:sbr
- 2 ab
- ? m
- ? x
-
- Test 8: breakindent + shift:1 + nu + sbr=# list briopt:sbr
- 2 ^Iabcd
- # opq
- # BCD
-
- Test 9: breakindent + shift by +1 + 'nu' + sbr=# list
- 2 ^Iabcd
- #op
- #AB
-
- Test 10: breakindent + shift by +1 + 'nu' + sbr=~ cpo+=n
- 2 ab
- ~ mn
- ~ yz
-
- Test 11: strdisplaywidth when breakindent is on
- strdisplaywidth: 46 == calculated: 64
- {
-
- Test 12: breakindent + long indent
- 56
-
- ~
- Test 13: breakindent with wrapping Tab
- d
- w
-
- Test 14: breakindent + visual blockwise delete #1
- e
- ~
- ~
-
- Test 15: breakindent + visual blockwise delete #2
- 1234567890
- ~
- ~ ]])
- end)
-end)
diff --git a/test/functional/legacy/delete_spec.lua b/test/functional/legacy/delete_spec.lua
index 9ea3269828..f2ced8942d 100644
--- a/test/functional/legacy/delete_spec.lua
+++ b/test/functional/legacy/delete_spec.lua
@@ -56,7 +56,7 @@ describe('Test for delete()', function()
endif
]])
if eval('v:shell_error') ~= 0 then
- pending('Cannot create symlink', function()end)
+ pending('Cannot create symlink')
end
-- Delete the link, not the file
eq(0, eval("delete('Xlink')"))
diff --git a/test/functional/legacy/search_spec.lua b/test/functional/legacy/search_spec.lua
index 3ed06a22e7..a207b176d3 100644
--- a/test/functional/legacy/search_spec.lua
+++ b/test/functional/legacy/search_spec.lua
@@ -17,7 +17,10 @@ describe('search cmdline', function()
screen = Screen.new(20, 3)
screen:attach()
screen:set_default_attr_ids({
- inc = {reverse = true}
+ inc = {reverse = true},
+ err = { foreground = Screen.colors.Grey100, background = Screen.colors.Red },
+ more = { bold = true, foreground = Screen.colors.SeaGreen4 },
+ tilde = { bold = true, foreground = Screen.colors.Blue1 },
})
end)
@@ -404,15 +407,7 @@ describe('search cmdline', function()
end)
it('keeps the view after deleting a char from the search', function()
- screen:detach()
- screen = Screen.new(20, 6)
- screen:attach()
- screen:set_default_attr_ids({
- inc = {reverse = true}
- })
- screen:set_default_attr_ignore({
- {bold=true, reverse=true}, {bold=true, foreground=Screen.colors.Blue1}
- })
+ screen:try_resize(20, 6)
tenlines()
feed('/foo')
@@ -448,14 +443,7 @@ describe('search cmdline', function()
end)
it('restores original view after failed search', function()
- screen:detach()
- screen = Screen.new(40, 3)
- screen:attach()
- screen:set_default_attr_ids({
- inc = {reverse = true},
- err = { foreground = Screen.colors.Grey100, background = Screen.colors.Red },
- more = { bold = true, foreground = Screen.colors.SeaGreen4 },
- })
+ screen:try_resize(40, 3)
tenlines()
feed('0')
feed('/foo')
@@ -484,15 +472,7 @@ describe('search cmdline', function()
it("CTRL-G with 'incsearch' and ? goes in the right direction", function()
-- oldtest: Test_search_cmdline4().
- screen:detach()
- screen = Screen.new(40, 4)
- screen:attach()
- screen:set_default_attr_ids({
- inc = {reverse = true},
- err = { foreground = Screen.colors.Grey100, background = Screen.colors.Red },
- more = { bold = true, foreground = Screen.colors.SeaGreen4 },
- tilde = { bold = true, foreground = Screen.colors.Blue1 },
- })
+ screen:try_resize(40, 4)
command('enew!')
funcs.setline(1, {' 1 the first', ' 2 the second', ' 3 the third'})
command('set laststatus=0 shortmess+=s')
diff --git a/test/functional/lua/api_spec.lua b/test/functional/lua/api_spec.lua
index b1dc5c07fd..23167d3ed9 100644
--- a/test/functional/lua/api_spec.lua
+++ b/test/functional/lua/api_spec.lua
@@ -155,41 +155,41 @@ describe('luaeval(vim.api.…)', function()
it('errors out correctly when working with API', function()
-- Conversion errors
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Cannot convert given lua type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua type',
exc_exec([[call luaeval("vim.api.nvim__id(vim.api.nvim__id)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Cannot convert given lua table',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua table',
exc_exec([[call luaeval("vim.api.nvim__id({1, foo=42})")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Cannot convert given lua type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua type',
exc_exec([[call luaeval("vim.api.nvim__id({42, vim.api.nvim__id})")]]))
-- Errors in number of arguments
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected 1 argument',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected 1 argument',
exc_exec([[call luaeval("vim.api.nvim__id()")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected 1 argument',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected 1 argument',
exc_exec([[call luaeval("vim.api.nvim__id(1, 2)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected 2 arguments',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected 2 arguments',
exc_exec([[call luaeval("vim.api.nvim_set_var(1, 2, 3)")]]))
-- Error in argument types
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua string',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua string',
exc_exec([[call luaeval("vim.api.nvim_set_var(1, 2)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua number',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua number',
exc_exec([[call luaeval("vim.api.nvim_buf_get_lines(0, 'test', 1, false)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Number is not integral',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Number is not integral',
exc_exec([[call luaeval("vim.api.nvim_buf_get_lines(0, 1.5, 1, false)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua table',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua table',
exc_exec([[call luaeval("vim.api.nvim__id_float('test')")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Unexpected type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Unexpected type',
exc_exec([[call luaeval("vim.api.nvim__id_float({[vim.type_idx]=vim.types.dictionary})")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua table',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua table',
exc_exec([[call luaeval("vim.api.nvim__id_array(1)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Unexpected type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Unexpected type',
exc_exec([[call luaeval("vim.api.nvim__id_array({[vim.type_idx]=vim.types.dictionary})")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua table',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected lua table',
exc_exec([[call luaeval("vim.api.nvim__id_dictionary(1)")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Unexpected type',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Unexpected type',
exc_exec([[call luaeval("vim.api.nvim__id_dictionary({[vim.type_idx]=vim.types.array})")]]))
-- TODO: check for errors with Tabpage argument
-- TODO: check for errors with Window argument
diff --git a/test/functional/lua/commands_spec.lua b/test/functional/lua/commands_spec.lua
index 26dcbe0534..96eaa7991b 100644
--- a/test/functional/lua/commands_spec.lua
+++ b/test/functional/lua/commands_spec.lua
@@ -13,6 +13,7 @@ local source = helpers.source
local dedent = helpers.dedent
local command = helpers.command
local exc_exec = helpers.exc_exec
+local pcall_err = helpers.pcall_err
local write_file = helpers.write_file
local redir_exec = helpers.redir_exec
local curbufmeths = helpers.curbufmeths
@@ -42,16 +43,16 @@ describe(':lua command', function()
eq({'', 'ETTS', 'TTSE', 'STTE'}, curbufmeths.get_lines(0, 100, false))
end)
it('throws catchable errors', function()
- eq([[Vim(lua):E5104: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unexpected symbol near ')']],
- exc_exec('lua ()'))
- eq([[Vim(lua):E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: TEST]],
+ eq([[Vim(lua):E5107: Error loading lua [string ":lua"]:1: unexpected symbol near ')']],
+ pcall_err(command, 'lua ()'))
+ eq([[Vim(lua):E5108: Error executing lua [string ":lua"]:1: TEST]],
exc_exec('lua error("TEST")'))
- eq([[Vim(lua):E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: Invalid buffer id]],
+ eq([[Vim(lua):E5108: Error executing lua [string ":lua"]:1: Invalid buffer id]],
exc_exec('lua vim.api.nvim_buf_set_lines(-10, 1, 1, false, {"TEST"})'))
eq({''}, curbufmeths.get_lines(0, 100, false))
end)
it('works with NULL errors', function()
- eq([=[Vim(lua):E5105: Error while calling lua chunk: [NULL]]=],
+ eq([=[Vim(lua):E5108: Error executing lua [NULL]]=],
exc_exec('lua error(nil)'))
end)
it('accepts embedded NLs without heredoc', function()
@@ -74,7 +75,7 @@ describe(':lua command', function()
it('works with long strings', function()
local s = ('x'):rep(100500)
- eq('\nE5104: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unfinished string near \'<eof>\'', redir_exec(('lua vim.api.nvim_buf_set_lines(1, 1, 2, false, {"%s})'):format(s)))
+ eq('\nE5107: Error loading lua [string ":lua"]:1: unfinished string near \'<eof>\'', redir_exec(('lua vim.api.nvim_buf_set_lines(1, 1, 2, false, {"%s})'):format(s)))
eq({''}, curbufmeths.get_lines(0, -1, false))
eq('', redir_exec(('lua vim.api.nvim_buf_set_lines(1, 1, 2, false, {"%s"})'):format(s)))
@@ -82,7 +83,7 @@ describe(':lua command', function()
end)
it('can show multiline error messages', function()
- local screen = Screen.new(50,10)
+ local screen = Screen.new(40,10)
screen:attach()
screen:set_default_attr_ids({
[1] = {bold = true, foreground = Screen.colors.Blue1},
@@ -92,51 +93,51 @@ describe(':lua command', function()
})
feed(':lua error("fail\\nmuch error\\nsuch details")<cr>')
- screen:expect([[
- |
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {2: }|
- {3:E5105: Error while calling lua chunk: [string "<Vi}|
- {3:mL compiled string>"]:1: fail} |
- {3:much error} |
- {3:such details} |
- {4:Press ENTER or type command to continue}^ |
- ]])
+ screen:expect{grid=[[
+ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {2: }|
+ {3:E5108: Error executing lua [string ":lua}|
+ {3:"]:1: fail} |
+ {3:much error} |
+ {3:such details} |
+ {4:Press ENTER or type command to continue}^ |
+ ]]}
feed('<cr>')
- screen:expect([[
- ^ |
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {1:~ }|
- |
- ]])
- eq('E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: fail\nmuch error\nsuch details', eval('v:errmsg'))
+ screen:expect{grid=[[
+ ^ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]]}
+ eq('E5108: Error executing lua [string ":lua"]:1: fail\nmuch error\nsuch details', eval('v:errmsg'))
local status, err = pcall(command,'lua error("some error\\nin a\\nAPI command")')
- local expected = 'Vim(lua):E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: some error\nin a\nAPI command'
+ local expected = 'Vim(lua):E5108: Error executing lua [string ":lua"]:1: some error\nin a\nAPI command'
eq(false, status)
eq(expected, string.sub(err, -string.len(expected)))
feed(':messages<cr>')
- screen:expect([[
- |
- {1:~ }|
- {1:~ }|
- {1:~ }|
- {2: }|
- {3:E5105: Error while calling lua chunk: [string "<Vi}|
- {3:mL compiled string>"]:1: fail} |
- {3:much error} |
- {3:such details} |
- {4:Press ENTER or type command to continue}^ |
- ]])
+ screen:expect{grid=[[
+ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {2: }|
+ {3:E5108: Error executing lua [string ":lua}|
+ {3:"]:1: fail} |
+ {3:much error} |
+ {3:such details} |
+ {4:Press ENTER or type command to continue}^ |
+ ]]}
end)
end)
@@ -167,13 +168,13 @@ describe(':luado command', function()
eq({''}, curbufmeths.get_lines(0, -1, false))
end)
it('fails on errors', function()
- eq([[Vim(luado):E5109: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unexpected symbol near ')']],
+ eq([[Vim(luado):E5109: Error loading lua: [string ":luado"]:1: unexpected symbol near ')']],
exc_exec('luado ()'))
- eq([[Vim(luado):E5111: Error while calling lua function: [string "<VimL compiled string>"]:1: attempt to perform arithmetic on global 'liness' (a nil value)]],
+ eq([[Vim(luado):E5111: Error calling lua: [string ":luado"]:1: attempt to perform arithmetic on global 'liness' (a nil value)]],
exc_exec('luado return liness + 1'))
end)
it('works with NULL errors', function()
- eq([=[Vim(luado):E5111: Error while calling lua function: [NULL]]=],
+ eq([=[Vim(luado):E5111: Error calling lua: [NULL]]=],
exc_exec('luado error(nil)'))
end)
it('fails in sandbox when needed', function()
@@ -185,7 +186,7 @@ describe(':luado command', function()
it('works with long strings', function()
local s = ('x'):rep(100500)
- eq('\nE5109: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unfinished string near \'<eof>\'', redir_exec(('luado return "%s'):format(s)))
+ eq('\nE5109: Error loading lua: [string ":luado"]:1: unfinished string near \'<eof>\'', redir_exec(('luado return "%s'):format(s)))
eq({''}, curbufmeths.get_lines(0, -1, false))
eq('', redir_exec(('luado return "%s"'):format(s)))
diff --git a/test/functional/lua/luaeval_spec.lua b/test/functional/lua/luaeval_spec.lua
index 760105df6b..61c8e5c02e 100644
--- a/test/functional/lua/luaeval_spec.lua
+++ b/test/functional/lua/luaeval_spec.lua
@@ -1,13 +1,17 @@
-- Test suite for testing luaeval() function
local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
local redir_exec = helpers.redir_exec
+local pcall_err = helpers.pcall_err
local exc_exec = helpers.exc_exec
+local exec_lua = helpers.exec_lua
local command = helpers.command
local meths = helpers.meths
local funcs = helpers.funcs
local clear = helpers.clear
local eval = helpers.eval
+local feed = helpers.feed
local NIL = helpers.NIL
local eq = helpers.eq
@@ -186,9 +190,9 @@ describe('luaeval()', function()
exc_exec('call luaeval("{1, foo=2}")'))
eq("Vim(call):E5101: Cannot convert given lua type",
exc_exec('call luaeval("vim.api.nvim_buf_get_lines")'))
- startswith("Vim(call):E5107: Error while creating lua chunk for luaeval(): ",
+ startswith("Vim(call):E5107: Error loading lua [string \"luaeval()\"]:",
exc_exec('call luaeval("1, 2, 3")'))
- startswith("Vim(call):E5108: Error while calling lua chunk for luaeval(): ",
+ startswith("Vim(call):E5108: Error executing lua [string \"luaeval()\"]:",
exc_exec('call luaeval("(nil)()")'))
eq("Vim(call):E5101: Cannot convert given lua type",
exc_exec('call luaeval("{42, vim.api}")'))
@@ -237,19 +241,99 @@ describe('luaeval()', function()
it('errors out correctly when doing incorrect things in lua', function()
-- Conversion errors
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: attempt to call field \'xxx_nonexistent_key_xxx\' (a nil value)',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: attempt to call field \'xxx_nonexistent_key_xxx\' (a nil value)',
exc_exec([[call luaeval("vim.xxx_nonexistent_key_xxx()")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: ERROR',
+ eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: ERROR',
exc_exec([[call luaeval("error('ERROR')")]]))
- eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [NULL]',
+ eq('Vim(call):E5108: Error executing lua [NULL]',
exc_exec([[call luaeval("error(nil)")]]))
end)
it('does not leak memory when called with too long line',
function()
local s = ('x'):rep(65536)
- eq('Vim(call):E5107: Error while creating lua chunk for luaeval(): [string "<VimL compiled string>"]:1: unexpected symbol near \')\'',
+ eq('Vim(call):E5107: Error loading lua [string "luaeval()"]:1: unexpected symbol near \')\'',
exc_exec([[call luaeval("(']] .. s ..[[' + )")]]))
eq(s, funcs.luaeval('"' .. s .. '"'))
end)
end)
+
+describe('v:lua', function()
+ before_each(function()
+ exec_lua([[
+ function _G.foo(a,b,n)
+ _G.val = n
+ return a+b
+ end
+ mymod = {}
+ function mymod.noisy(name)
+ vim.api.nvim_set_current_line("hey "..name)
+ end
+ function mymod.crashy()
+ nonexistent()
+ end
+ function mymod.omni(findstart, base)
+ if findstart == 1 then
+ return 5
+ else
+ if base == 'st' then
+ return {'stuff', 'steam', 'strange things'}
+ end
+ end
+ end
+ vim.api.nvim_buf_set_option(0, 'omnifunc', 'v:lua.mymod.omni')
+ ]])
+ end)
+
+ it('works in expressions', function()
+ eq(7, eval('v:lua.foo(3,4,v:null)'))
+ eq(true, exec_lua([[return _G.val == vim.NIL]]))
+ eq(NIL, eval('v:lua.mymod.noisy("eval")'))
+ eq("hey eval", meths.get_current_line())
+
+ eq("Vim:E5108: Error executing lua [string \"<nvim>\"]:10: attempt to call global 'nonexistent' (a nil value)",
+ pcall_err(eval, 'v:lua.mymod.crashy()'))
+ end)
+
+ it('works in :call', function()
+ command(":call v:lua.mymod.noisy('command')")
+ eq("hey command", meths.get_current_line())
+ eq("Vim(call):E5108: Error executing lua [string \"<nvim>\"]:10: attempt to call global 'nonexistent' (a nil value)",
+ pcall_err(command, 'call v:lua.mymod.crashy()'))
+ end)
+
+ it('works in func options', function()
+ local screen = Screen.new(60, 8)
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {background = Screen.colors.WebGray},
+ [3] = {background = Screen.colors.LightMagenta},
+ [4] = {bold = true},
+ [5] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ })
+ screen:attach()
+ feed('isome st<c-x><c-o>')
+ screen:expect{grid=[[
+ some stuff^ |
+ {1:~ }{2: stuff }{1: }|
+ {1:~ }{3: steam }{1: }|
+ {1:~ }{3: strange things }{1: }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {4:-- Omni completion (^O^N^P) }{5:match 1 of 3} |
+ ]]}
+ end)
+
+ it('throw errors for invalid use', function()
+ eq('Vim(let):E15: Invalid expression: v:lua.func', pcall_err(command, "let g:Func = v:lua.func"))
+ eq('Vim(let):E15: Invalid expression: v:lua', pcall_err(command, "let g:Func = v:lua"))
+ eq("Vim(let):E15: Invalid expression: v:['lua']", pcall_err(command, "let g:Func = v:['lua']"))
+
+ eq("Vim:E15: Invalid expression: v:['lua'].foo()", pcall_err(eval, "v:['lua'].foo()"))
+ eq("Vim(call):E461: Illegal variable name: v:['lua']", pcall_err(command, "call v:['lua'].baar()"))
+
+ eq("Vim(let):E46: Cannot change read-only variable \"v:['lua']\"", pcall_err(command, "let v:['lua'] = 'xx'"))
+ eq("Vim(let):E46: Cannot change read-only variable \"v:lua\"", pcall_err(command, "let v:lua = 'xx'"))
+ end)
+end)
diff --git a/test/functional/lua/overrides_spec.lua b/test/functional/lua/overrides_spec.lua
index f6439001ac..1bccc02847 100644
--- a/test/functional/lua/overrides_spec.lua
+++ b/test/functional/lua/overrides_spec.lua
@@ -54,11 +54,12 @@ describe('print', function()
v_tblout = setmetatable({}, meta_tblout)
]])
eq('', redir_exec('luafile ' .. fname))
- eq('\nE5105: Error while calling lua chunk: E5114: Error while converting print argument #2: [NULL]',
+ -- TODO(bfredl): these look weird, print() should not use "E5114:" style errors..
+ eq('\nE5108: Error executing lua E5114: Error while converting print argument #2: [NULL]',
redir_exec('lua print("foo", v_nilerr, "bar")'))
- eq('\nE5105: Error while calling lua chunk: E5114: Error while converting print argument #2: Xtest-functional-lua-overrides-luafile:2: abc',
+ eq('\nE5108: Error executing lua E5114: Error while converting print argument #2: Xtest-functional-lua-overrides-luafile:2: abc',
redir_exec('lua print("foo", v_abcerr, "bar")'))
- eq('\nE5105: Error while calling lua chunk: E5114: Error while converting print argument #2: <Unknown error: lua_tolstring returned NULL for tostring result>',
+ eq('\nE5108: Error executing lua E5114: Error while converting print argument #2: <Unknown error: lua_tolstring returned NULL for tostring result>',
redir_exec('lua print("foo", v_tblout, "bar")'))
end)
it('prints strings with NULs and NLs correctly', function()
@@ -156,7 +157,8 @@ describe('debug.debug', function()
lua_debug> ^ |
]])
feed('<C-c>')
- screen:expect([[
+ screen:expect{grid=[[
+ {0:~ }|
{0:~ }|
{0:~ }|
{0:~ }|
@@ -167,11 +169,10 @@ describe('debug.debug', function()
lua_debug> print("TEST") |
TEST |
|
- {E:E5105: Error while calling lua chunk: [string "<VimL }|
- {E:compiled string>"]:5: attempt to perform arithmetic o}|
- {E:n local 'a' (a nil value)} |
+ {E:E5108: Error executing lua [string ":lua"]:5: attempt}|
+ {E: to perform arithmetic on local 'a' (a nil value)} |
Interrupt: {cr:Press ENTER or type command to continue}^ |
- ]])
+ ]]}
feed('<C-l>:lua Test()\n')
screen:expect([[
{0:~ }|
@@ -190,7 +191,8 @@ describe('debug.debug', function()
lua_debug> ^ |
]])
feed('\n')
- screen:expect([[
+ screen:expect{grid=[[
+ {0:~ }|
{0:~ }|
{0:~ }|
{0:~ }|
@@ -201,11 +203,10 @@ describe('debug.debug', function()
{0:~ }|
nil |
lua_debug> |
- {E:E5105: Error while calling lua chunk: [string "<VimL }|
- {E:compiled string>"]:5: attempt to perform arithmetic o}|
- {E:n local 'a' (a nil value)} |
+ {E:E5108: Error executing lua [string ":lua"]:5: attempt}|
+ {E: to perform arithmetic on local 'a' (a nil value)} |
{cr:Press ENTER or type command to continue}^ |
- ]])
+ ]]}
end)
it("can be safely exited with 'cont'", function()
@@ -298,14 +299,11 @@ describe('package.path/package.cpath', function()
end
return new_paths
end
- local function execute_lua(cmd, ...)
- return meths.execute_lua(cmd, {...})
- end
local function eval_lua(expr, ...)
- return meths.execute_lua('return ' .. expr, {...})
+ return meths.exec_lua('return '..expr, {...})
end
local function set_path(which, value)
- return execute_lua('package[select(1, ...)] = select(2, ...)', which, value)
+ return exec_lua('package[select(1, ...)] = select(2, ...)', which, value)
end
it('contains directories from &runtimepath on first invocation', function()
diff --git a/test/functional/lua/treesitter_spec.lua b/test/functional/lua/treesitter_spec.lua
new file mode 100644
index 0000000000..5a53ca1425
--- /dev/null
+++ b/test/functional/lua/treesitter_spec.lua
@@ -0,0 +1,147 @@
+-- Test suite for testing interactions with API bindings
+local helpers = require('test.functional.helpers')(after_each)
+
+local clear = helpers.clear
+local eq = helpers.eq
+local insert = helpers.insert
+local exec_lua = helpers.exec_lua
+local iswin = helpers.iswin
+local feed = helpers.feed
+local pcall_err = helpers.pcall_err
+local matches = helpers.matches
+
+before_each(clear)
+
+describe('treesitter API', function()
+ -- error tests not requiring a parser library
+ it('handles missing language', function()
+ eq('Error executing lua: .../treesitter.lua: no such language: borklang',
+ pcall_err(exec_lua, "parser = vim.treesitter.create_parser(0, 'borklang')"))
+
+ -- actual message depends on platform
+ matches('Error executing lua: Failed to load parser: uv_dlopen: .+',
+ pcall_err(exec_lua, "parser = vim.treesitter.add_language('borkbork.so', 'borklang')"))
+
+ eq('Error executing lua: [string "<nvim>"]:1: no such language: borklang',
+ pcall_err(exec_lua, "parser = vim.treesitter.inspect_language('borklang')"))
+ end)
+
+ local ts_path = os.getenv("TREE_SITTER_DIR")
+
+ describe('with C parser', function()
+ if ts_path == nil then
+ it("works", function() pending("TREE_SITTER_PATH not set, skipping treesitter parser tests") end)
+ return
+ end
+
+ before_each(function()
+ local path = ts_path .. '/bin/c'..(iswin() and '.dll' or '.so')
+ exec_lua([[
+ local path = ...
+ vim.treesitter.add_language(path,'c')
+ ]], path)
+ end)
+
+ it('parses buffer', function()
+ insert([[
+ int main() {
+ int x = 3;
+ }]])
+
+ exec_lua([[
+ parser = vim.treesitter.get_parser(0, "c")
+ tree = parser:parse()
+ root = tree:root()
+ lang = vim.treesitter.inspect_language('c')
+ ]])
+
+ eq("<tree>", exec_lua("return tostring(tree)"))
+ eq("<node translation_unit>", exec_lua("return tostring(root)"))
+ eq({0,0,3,0}, exec_lua("return {root:range()}"))
+
+ eq(1, exec_lua("return root:child_count()"))
+ exec_lua("child = root:child(0)")
+ eq("<node function_definition>", exec_lua("return tostring(child)"))
+ eq({0,0,2,1}, exec_lua("return {child:range()}"))
+
+ eq("function_definition", exec_lua("return child:type()"))
+ eq(true, exec_lua("return child:named()"))
+ eq("number", type(exec_lua("return child:symbol()")))
+ eq({'function_definition', true}, exec_lua("return lang.symbols[child:symbol()]"))
+
+ exec_lua("anon = root:descendant_for_range(0,8,0,9)")
+ eq("(", exec_lua("return anon:type()"))
+ eq(false, exec_lua("return anon:named()"))
+ eq("number", type(exec_lua("return anon:symbol()")))
+ eq({'(', false}, exec_lua("return lang.symbols[anon:symbol()]"))
+
+ exec_lua("descendant = root:descendant_for_range(1,2,1,12)")
+ eq("<node declaration>", exec_lua("return tostring(descendant)"))
+ eq({1,2,1,12}, exec_lua("return {descendant:range()}"))
+ eq("(declaration type: (primitive_type) declarator: (init_declarator declarator: (identifier) value: (number_literal)))", exec_lua("return descendant:sexpr()"))
+
+ eq(true, exec_lua("return child == child"))
+ -- separate lua object, but represents same node
+ eq(true, exec_lua("return child == root:child(0)"))
+ eq(false, exec_lua("return child == descendant2"))
+ eq(false, exec_lua("return child == nil"))
+ eq(false, exec_lua("return child == tree"))
+
+ feed("2G7|ay")
+ exec_lua([[
+ tree2 = parser:parse()
+ root2 = tree2:root()
+ descendant2 = root2:descendant_for_range(1,2,1,13)
+ ]])
+ eq(false, exec_lua("return tree2 == tree1"))
+ eq(false, exec_lua("return root2 == root"))
+ eq("<node declaration>", exec_lua("return tostring(descendant2)"))
+ eq({1,2,1,13}, exec_lua("return {descendant2:range()}"))
+
+ -- orginal tree did not change
+ eq({1,2,1,12}, exec_lua("return {descendant:range()}"))
+
+ -- unchanged buffer: return the same tree
+ eq(true, exec_lua("return parser:parse() == tree2"))
+ end)
+
+ it('inspects language', function()
+ local keys, fields, symbols = unpack(exec_lua([[
+ local lang = vim.treesitter.inspect_language('c')
+ local keys, symbols = {}, {}
+ for k,_ in pairs(lang) do
+ keys[k] = true
+ end
+
+ -- symbols array can have "holes" and is thus not a valid msgpack array
+ -- but we don't care about the numbers here (checked in the parser test)
+ for _, v in pairs(lang.symbols) do
+ table.insert(symbols, v)
+ end
+ return {keys, lang.fields, symbols}
+ ]]))
+
+ eq({fields=true, symbols=true}, keys)
+
+ local fset = {}
+ for _,f in pairs(fields) do
+ eq("string", type(f))
+ fset[f] = true
+ end
+ eq(true, fset["directive"])
+ eq(true, fset["initializer"])
+
+ local has_named, has_anonymous
+ for _,s in pairs(symbols) do
+ eq("string", type(s[1]))
+ eq("boolean", type(s[2]))
+ if s[1] == "for_statement" and s[2] == true then
+ has_named = true
+ elseif s[1] == "|=" and s[2] == false then
+ has_anonymous = true
+ end
+ end
+ eq({true,true}, {has_named,has_anonymous})
+ end)
+ end)
+end)
diff --git a/test/functional/lua/uri_spec.lua b/test/functional/lua/uri_spec.lua
new file mode 100644
index 0000000000..19b1eb1f61
--- /dev/null
+++ b/test/functional/lua/uri_spec.lua
@@ -0,0 +1,107 @@
+local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
+local exec_lua = helpers.exec_lua
+local eq = helpers.eq
+
+describe('URI methods', function()
+ before_each(function()
+ clear()
+ end)
+
+ describe('file path to uri', function()
+ describe('encode Unix file path', function()
+ it('file path includes only ascii charactors', function()
+ exec_lua("filepath = '/Foo/Bar/Baz.txt'")
+
+ eq('file:///Foo/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+
+ it('file path including white space', function()
+ exec_lua("filepath = '/Foo /Bar/Baz.txt'")
+
+ eq('file:///Foo%20/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+
+ it('file path including Unicode charactors', function()
+ exec_lua("filepath = '/xy/åäö/ɧ/汉语/↥/🤦/🦄/aÌŠ/بÙÙŠÙŽÙ‘.txt'")
+
+ -- The URI encoding should be case-insensitive
+ eq('file:///xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+ end)
+
+ describe('encode Windows filepath', function()
+ it('file path includes only ascii charactors', function()
+ exec_lua([[filepath = 'C:\\Foo\\Bar\\Baz.txt']])
+
+ eq('file:///C:/Foo/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+
+ it('file path including white space', function()
+ exec_lua([[filepath = 'C:\\Foo \\Bar\\Baz.txt']])
+
+ eq('file:///C:/Foo%20/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+
+ it('file path including Unicode charactors', function()
+ exec_lua([[filepath = 'C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\aÌŠ\\بÙÙŠÙŽÙ‘.txt']])
+
+ eq('file:///C:/xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt', exec_lua("return vim.uri_from_fname(filepath)"))
+ end)
+ end)
+ end)
+
+ describe('uri to filepath', function()
+ describe('decode Unix file path', function()
+ it('file path includes only ascii charactors', function()
+ exec_lua("uri = 'file:///Foo/Bar/Baz.txt'")
+
+ eq('/Foo/Bar/Baz.txt', exec_lua("return vim.uri_to_fname(uri)"))
+ end)
+
+ it('file path including white space', function()
+ exec_lua("uri = 'file:///Foo%20/Bar/Baz.txt'")
+
+ eq('/Foo /Bar/Baz.txt', exec_lua("return vim.uri_to_fname(uri)"))
+ end)
+
+ it('file path including Unicode charactors', function()
+ local test_case = [[
+ local uri = 'file:///xy/%C3%A5%C3%A4%C3%B6/%C9%A7/%E6%B1%89%E8%AF%AD/%E2%86%A5/%F0%9F%A4%A6/%F0%9F%A6%84/a%CC%8A/%D8%A8%D9%90%D9%8A%D9%8E%D9%91.txt'
+ return vim.uri_to_fname(uri)
+ ]]
+
+ eq('/xy/åäö/ɧ/汉语/↥/🤦/🦄/aÌŠ/بÙÙŠÙŽÙ‘.txt', exec_lua(test_case))
+ end)
+ end)
+
+ describe('decode Windows filepath', function()
+ it('file path includes only ascii charactors', function()
+ local test_case = [[
+ local uri = 'file:///C:/Foo/Bar/Baz.txt'
+ return vim.uri_to_fname(uri)
+ ]]
+
+ eq('C:\\Foo\\Bar\\Baz.txt', exec_lua(test_case))
+ end)
+
+ it('file path including white space', function()
+ local test_case = [[
+ local uri = 'file:///C:/Foo%20/Bar/Baz.txt'
+ return vim.uri_to_fname(uri)
+ ]]
+
+ eq('C:\\Foo \\Bar\\Baz.txt', exec_lua(test_case))
+ end)
+
+ it('file path including Unicode charactors', function()
+ local test_case = [[
+ local uri = 'file:///C:/xy/%C3%A5%C3%A4%C3%B6/%C9%A7/%E6%B1%89%E8%AF%AD/%E2%86%A5/%F0%9F%A4%A6/%F0%9F%A6%84/a%CC%8A/%D8%A8%D9%90%D9%8A%D9%8E%D9%91.txt'
+ return vim.uri_to_fname(uri)
+ ]]
+
+ eq('C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\aÌŠ\\بÙÙŠÙŽÙ‘.txt', exec_lua(test_case))
+ end)
+ end)
+ end)
+end)
diff --git a/test/functional/lua/utility_functions_spec.lua b/test/functional/lua/utility_functions_spec.lua
deleted file mode 100644
index ea2b1fc8a9..0000000000
--- a/test/functional/lua/utility_functions_spec.lua
+++ /dev/null
@@ -1,291 +0,0 @@
--- Test suite for testing interactions with API bindings
-local helpers = require('test.functional.helpers')(after_each)
-local Screen = require('test.functional.ui.screen')
-
-local funcs = helpers.funcs
-local clear = helpers.clear
-local eq = helpers.eq
-local eval = helpers.eval
-local feed = helpers.feed
-local pcall_err = helpers.pcall_err
-local exec_lua = helpers.exec_lua
-
-before_each(clear)
-
-describe('lua stdlib', function()
- -- İ: `tolower("İ")` is `i` which has length 1 while `İ` itself has
- -- length 2 (in bytes).
- -- Ⱥ: `tolower("Ⱥ")` is `ⱥ` which has length 2 while `Ⱥ` itself has
- -- length 3 (in bytes).
- --
- -- Note: 'i' !=? 'İ' and 'ⱥ' !=? 'Ⱥ' on some systems.
- -- Note: Built-in Nvim comparison (on systems lacking `strcasecmp`) works
- -- only on ASCII characters.
- it('vim.stricmp', function()
- eq(0, funcs.luaeval('vim.stricmp("a", "A")'))
- eq(0, funcs.luaeval('vim.stricmp("A", "a")'))
- eq(0, funcs.luaeval('vim.stricmp("a", "a")'))
- eq(0, funcs.luaeval('vim.stricmp("A", "A")'))
-
- eq(0, funcs.luaeval('vim.stricmp("", "")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0", "\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0", "\\0\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0", "\\0\\0\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0A", "\\0\\0\\0a")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0A")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0a")'))
-
- eq(0, funcs.luaeval('vim.stricmp("a\\0", "A\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("A\\0", "a\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("a\\0", "a\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("A\\0", "A\\0")'))
-
- eq(0, funcs.luaeval('vim.stricmp("\\0a", "\\0A")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0A", "\\0a")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0a", "\\0a")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0A", "\\0A")'))
-
- eq(0, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0A\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0a\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0a\\0")'))
- eq(0, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0A\\0")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("a", "B")'))
- eq(-1, funcs.luaeval('vim.stricmp("A", "b")'))
- eq(-1, funcs.luaeval('vim.stricmp("a", "b")'))
- eq(-1, funcs.luaeval('vim.stricmp("A", "B")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("", "\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0", "\\0\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0\\0", "\\0\\0\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0A", "\\0\\0\\0b")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0B")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0b")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("a\\0", "B\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("A\\0", "b\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("a\\0", "b\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("A\\0", "B\\0")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("\\0a", "\\0B")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0A", "\\0b")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0a", "\\0b")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0A", "\\0B")'))
-
- eq(-1, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0B\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0b\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0b\\0")'))
- eq(-1, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0B\\0")'))
-
- eq(1, funcs.luaeval('vim.stricmp("c", "B")'))
- eq(1, funcs.luaeval('vim.stricmp("C", "b")'))
- eq(1, funcs.luaeval('vim.stricmp("c", "b")'))
- eq(1, funcs.luaeval('vim.stricmp("C", "B")'))
-
- eq(1, funcs.luaeval('vim.stricmp("\\0", "")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0", "\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0", "\\0\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0\\0", "\\0\\0\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0C", "\\0\\0\\0b")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0c", "\\0\\0\\0B")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0c", "\\0\\0\\0b")'))
-
- eq(1, funcs.luaeval('vim.stricmp("c\\0", "B\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("C\\0", "b\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("c\\0", "b\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("C\\0", "B\\0")'))
-
- eq(1, funcs.luaeval('vim.stricmp("c\\0", "B")'))
- eq(1, funcs.luaeval('vim.stricmp("C\\0", "b")'))
- eq(1, funcs.luaeval('vim.stricmp("c\\0", "b")'))
- eq(1, funcs.luaeval('vim.stricmp("C\\0", "B")'))
-
- eq(1, funcs.luaeval('vim.stricmp("\\0c", "\\0B")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0C", "\\0b")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0c", "\\0b")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0C", "\\0B")'))
-
- eq(1, funcs.luaeval('vim.stricmp("\\0c\\0", "\\0B\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0C\\0", "\\0b\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0c\\0", "\\0b\\0")'))
- eq(1, funcs.luaeval('vim.stricmp("\\0C\\0", "\\0B\\0")'))
- end)
-
- it("vim.str_utfindex/str_byteindex", function()
- exec_lua([[_G.test_text = "xy åäö ɧ 汉语 ↥ 🤦x🦄 aÌŠ بÙÙŠÙŽÙ‘"]])
- local indicies32 = {[0]=0,1,2,3,5,7,9,10,12,13,16,19,20,23,24,28,29,33,34,35,37,38,40,42,44,46,48}
- local indicies16 = {[0]=0,1,2,3,5,7,9,10,12,13,16,19,20,23,24,28,28,29,33,33,34,35,37,38,40,42,44,46,48}
- for i,k in pairs(indicies32) do
- eq(k, exec_lua("return vim.str_byteindex(_G.test_text, ...)", i), i)
- end
- for i,k in pairs(indicies16) do
- eq(k, exec_lua("return vim.str_byteindex(_G.test_text, ..., true)", i), i)
- end
- local i32, i16 = 0, 0
- for k = 0,48 do
- if indicies32[i32] < k then
- i32 = i32 + 1
- end
- if indicies16[i16] < k then
- i16 = i16 + 1
- if indicies16[i16+1] == indicies16[i16] then
- i16 = i16 + 1
- end
- end
- eq({i32, i16}, exec_lua("return {vim.str_utfindex(_G.test_text, ...)}", k), k)
- end
- end)
-
- it("vim.schedule", function()
- exec_lua([[
- test_table = {}
- vim.schedule(function()
- table.insert(test_table, "xx")
- end)
- table.insert(test_table, "yy")
- ]])
- eq({"yy","xx"}, exec_lua("return test_table"))
-
- -- type checked args
- eq('Error executing lua: vim.schedule: expected function',
- pcall_err(exec_lua, "vim.schedule('stringly')"))
-
- eq('Error executing lua: vim.schedule: expected function',
- pcall_err(exec_lua, "vim.schedule()"))
-
- exec_lua([[
- vim.schedule(function()
- error("big failure\nvery async")
- end)
- ]])
-
- feed("<cr>")
- eq('Error executing vim.schedule lua callback: [string "<nvim>"]:2: big failure\nvery async', eval("v:errmsg"))
-
- local screen = Screen.new(60,5)
- screen:set_default_attr_ids({
- [1] = {bold = true, foreground = Screen.colors.Blue1},
- [2] = {bold = true, reverse = true},
- [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
- [4] = {bold = true, foreground = Screen.colors.SeaGreen4},
- })
- screen:attach()
- screen:expect{grid=[[
- ^ |
- {1:~ }|
- {1:~ }|
- {1:~ }|
- |
- ]]}
-
- -- nvim_command causes a vimL exception, check that it is properly caught
- -- and propagated as an error message in async contexts.. #10809
- exec_lua([[
- vim.schedule(function()
- vim.api.nvim_command(":echo 'err")
- end)
- ]])
- screen:expect{grid=[[
- |
- {2: }|
- {3:Error executing vim.schedule lua callback: [string "<nvim>"]}|
- {3::2: Vim(echo):E115: Missing quote: 'err} |
- {4:Press ENTER or type command to continue}^ |
- ]]}
- end)
-
- it("vim.split", function()
- local split = function(str, sep)
- return exec_lua('return vim.split(...)', str, sep)
- end
-
- local tests = {
- { "a,b", ",", false, { 'a', 'b' } },
- { ":aa::bb:", ":", false, { '', 'aa', '', 'bb', '' } },
- { "::ee::ff:", ":", false, { '', '', 'ee', '', 'ff', '' } },
- { "ab", ".", false, { '', '', '' } },
- { "a1b2c", "[0-9]", false, { 'a', 'b', 'c' } },
- { "xy", "", false, { 'x', 'y' } },
- { "here be dragons", " ", false, { "here", "be", "dragons"} },
- { "axaby", "ab?", false, { '', 'x', 'y' } },
- { "f v2v v3v w2w ", "([vw])2%1", false, { 'f ', ' v3v ', ' ' } },
- { "x*yz*oo*l", "*", true, { 'x', 'yz', 'oo', 'l' } },
- }
-
- for _, t in ipairs(tests) do
- eq(t[4], split(t[1], t[2], t[3]))
- end
-
- local loops = {
- { "abc", ".-" },
- }
-
- for _, t in ipairs(loops) do
- local status, err = pcall(split, t[1], t[2])
- eq(false, status)
- assert(string.match(err, "Infinite loop detected"))
- end
- end)
-
- it('vim.trim', function()
- local trim = function(s)
- return exec_lua('return vim.trim(...)', s)
- end
-
- local trims = {
- { " a", "a" },
- { " b ", "b" },
- { "\tc" , "c" },
- { "r\n", "r" },
- }
-
- for _, t in ipairs(trims) do
- assert(t[2], trim(t[1]))
- end
-
- local status, err = pcall(trim, 2)
- eq(false, status)
- assert(string.match(err, "Only strings can be trimmed"))
- end)
-
- it('vim.inspect', function()
- -- just make sure it basically works, it has its own test suite
- local inspect = function(t, opts)
- return exec_lua('return vim.inspect(...)', t, opts)
- end
-
- eq('2', inspect(2))
- eq('{+a = {+b = 1+}+}',
- inspect({ a = { b = 1 } }, { newline = '+', indent = '' }))
-
- -- special value vim.inspect.KEY works
- eq('{ KEY_a = "x", KEY_b = "y"}', exec_lua([[
- return vim.inspect({a="x", b="y"}, {newline = '', process = function(item, path)
- if path[#path] == vim.inspect.KEY then
- return 'KEY_'..item
- end
- return item
- end})
- ]]))
- end)
-
- it("vim.deepcopy", function()
- local is_dc = exec_lua([[
- local a = { x = { 1, 2 }, y = 5}
- local b = vim.deepcopy(a)
-
- local count = 0
- for _ in pairs(b) do count = count + 1 end
-
- return b.x[1] == 1 and b.x[2] == 2 and b.y == 5 and count == 2
- and tostring(a) ~= tostring(b)
- ]])
-
- assert(is_dc)
- end)
-
- it('vim.pesc', function()
- eq('foo%-bar', exec_lua([[return vim.pesc('foo-bar')]]))
- eq('foo%%%-bar', exec_lua([[return vim.pesc(vim.pesc('foo-bar'))]]))
- end)
-end)
diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua
new file mode 100644
index 0000000000..17ffcd8d86
--- /dev/null
+++ b/test/functional/lua/vim_spec.lua
@@ -0,0 +1,663 @@
+-- Test suite for testing interactions with API bindings
+local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
+
+local funcs = helpers.funcs
+local meths = helpers.meths
+local command = helpers.command
+local clear = helpers.clear
+local eq = helpers.eq
+local eval = helpers.eval
+local feed = helpers.feed
+local pcall_err = helpers.pcall_err
+local exec_lua = helpers.exec_lua
+local matches = helpers.matches
+local source = helpers.source
+local NIL = helpers.NIL
+local retry = helpers.retry
+
+before_each(clear)
+
+describe('lua stdlib', function()
+ -- İ: `tolower("İ")` is `i` which has length 1 while `İ` itself has
+ -- length 2 (in bytes).
+ -- Ⱥ: `tolower("Ⱥ")` is `ⱥ` which has length 2 while `Ⱥ` itself has
+ -- length 3 (in bytes).
+ --
+ -- Note: 'i' !=? 'İ' and 'ⱥ' !=? 'Ⱥ' on some systems.
+ -- Note: Built-in Nvim comparison (on systems lacking `strcasecmp`) works
+ -- only on ASCII characters.
+ it('vim.stricmp', function()
+ eq(0, funcs.luaeval('vim.stricmp("a", "A")'))
+ eq(0, funcs.luaeval('vim.stricmp("A", "a")'))
+ eq(0, funcs.luaeval('vim.stricmp("a", "a")'))
+ eq(0, funcs.luaeval('vim.stricmp("A", "A")'))
+
+ eq(0, funcs.luaeval('vim.stricmp("", "")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0", "\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0", "\\0\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0", "\\0\\0\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0A", "\\0\\0\\0a")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0A")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0a")'))
+
+ eq(0, funcs.luaeval('vim.stricmp("a\\0", "A\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("A\\0", "a\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("a\\0", "a\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("A\\0", "A\\0")'))
+
+ eq(0, funcs.luaeval('vim.stricmp("\\0a", "\\0A")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0A", "\\0a")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0a", "\\0a")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0A", "\\0A")'))
+
+ eq(0, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0A\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0a\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0a\\0")'))
+ eq(0, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0A\\0")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("a", "B")'))
+ eq(-1, funcs.luaeval('vim.stricmp("A", "b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("a", "b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("A", "B")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("", "\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0", "\\0\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0\\0", "\\0\\0\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0A", "\\0\\0\\0b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0B")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0\\0\\0a", "\\0\\0\\0b")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("a\\0", "B\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("A\\0", "b\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("a\\0", "b\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("A\\0", "B\\0")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("\\0a", "\\0B")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0A", "\\0b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0a", "\\0b")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0A", "\\0B")'))
+
+ eq(-1, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0B\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0b\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0a\\0", "\\0b\\0")'))
+ eq(-1, funcs.luaeval('vim.stricmp("\\0A\\0", "\\0B\\0")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("c", "B")'))
+ eq(1, funcs.luaeval('vim.stricmp("C", "b")'))
+ eq(1, funcs.luaeval('vim.stricmp("c", "b")'))
+ eq(1, funcs.luaeval('vim.stricmp("C", "B")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("\\0", "")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0", "\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0", "\\0\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0\\0", "\\0\\0\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0C", "\\0\\0\\0b")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0c", "\\0\\0\\0B")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0\\0\\0c", "\\0\\0\\0b")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("c\\0", "B\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("C\\0", "b\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("c\\0", "b\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("C\\0", "B\\0")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("c\\0", "B")'))
+ eq(1, funcs.luaeval('vim.stricmp("C\\0", "b")'))
+ eq(1, funcs.luaeval('vim.stricmp("c\\0", "b")'))
+ eq(1, funcs.luaeval('vim.stricmp("C\\0", "B")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("\\0c", "\\0B")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0C", "\\0b")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0c", "\\0b")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0C", "\\0B")'))
+
+ eq(1, funcs.luaeval('vim.stricmp("\\0c\\0", "\\0B\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0C\\0", "\\0b\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0c\\0", "\\0b\\0")'))
+ eq(1, funcs.luaeval('vim.stricmp("\\0C\\0", "\\0B\\0")'))
+ end)
+
+ it('vim.startswith', function()
+ eq(true, funcs.luaeval('vim.startswith("123", "1")'))
+ eq(true, funcs.luaeval('vim.startswith("123", "")'))
+ eq(true, funcs.luaeval('vim.startswith("123", "123")'))
+ eq(true, funcs.luaeval('vim.startswith("", "")'))
+
+ eq(false, funcs.luaeval('vim.startswith("123", " ")'))
+ eq(false, funcs.luaeval('vim.startswith("123", "2")'))
+ eq(false, funcs.luaeval('vim.startswith("123", "1234")'))
+
+ eq("string", type(pcall_err(funcs.luaeval, 'vim.startswith("123", nil)')))
+ eq("string", type(pcall_err(funcs.luaeval, 'vim.startswith(nil, "123")')))
+ end)
+
+ it('vim.endswith', function()
+ eq(true, funcs.luaeval('vim.endswith("123", "3")'))
+ eq(true, funcs.luaeval('vim.endswith("123", "")'))
+ eq(true, funcs.luaeval('vim.endswith("123", "123")'))
+ eq(true, funcs.luaeval('vim.endswith("", "")'))
+
+ eq(false, funcs.luaeval('vim.endswith("123", " ")'))
+ eq(false, funcs.luaeval('vim.endswith("123", "2")'))
+ eq(false, funcs.luaeval('vim.endswith("123", "1234")'))
+
+ eq("string", type(pcall_err(funcs.luaeval, 'vim.endswith("123", nil)')))
+ eq("string", type(pcall_err(funcs.luaeval, 'vim.endswith(nil, "123")')))
+ end)
+
+ it("vim.str_utfindex/str_byteindex", function()
+ exec_lua([[_G.test_text = "xy åäö ɧ 汉语 ↥ 🤦x🦄 aÌŠ بÙÙŠÙŽÙ‘"]])
+ local indicies32 = {[0]=0,1,2,3,5,7,9,10,12,13,16,19,20,23,24,28,29,33,34,35,37,38,40,42,44,46,48}
+ local indicies16 = {[0]=0,1,2,3,5,7,9,10,12,13,16,19,20,23,24,28,28,29,33,33,34,35,37,38,40,42,44,46,48}
+ for i,k in pairs(indicies32) do
+ eq(k, exec_lua("return vim.str_byteindex(_G.test_text, ...)", i), i)
+ end
+ for i,k in pairs(indicies16) do
+ eq(k, exec_lua("return vim.str_byteindex(_G.test_text, ..., true)", i), i)
+ end
+ local i32, i16 = 0, 0
+ for k = 0,48 do
+ if indicies32[i32] < k then
+ i32 = i32 + 1
+ end
+ if indicies16[i16] < k then
+ i16 = i16 + 1
+ if indicies16[i16+1] == indicies16[i16] then
+ i16 = i16 + 1
+ end
+ end
+ eq({i32, i16}, exec_lua("return {vim.str_utfindex(_G.test_text, ...)}", k), k)
+ end
+ end)
+
+ it("vim.schedule", function()
+ exec_lua([[
+ test_table = {}
+ vim.schedule(function()
+ table.insert(test_table, "xx")
+ end)
+ table.insert(test_table, "yy")
+ ]])
+ eq({"yy","xx"}, exec_lua("return test_table"))
+
+ -- Validates args.
+ eq('Error executing lua: vim.schedule: expected function',
+ pcall_err(exec_lua, "vim.schedule('stringly')"))
+ eq('Error executing lua: vim.schedule: expected function',
+ pcall_err(exec_lua, "vim.schedule()"))
+
+ exec_lua([[
+ vim.schedule(function()
+ error("big failure\nvery async")
+ end)
+ ]])
+
+ feed("<cr>")
+ eq('Error executing vim.schedule lua callback: [string "<nvim>"]:2: big failure\nvery async', eval("v:errmsg"))
+
+ local screen = Screen.new(60,5)
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {bold = true, reverse = true},
+ [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ [4] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ })
+ screen:attach()
+ screen:expect{grid=[[
+ ^ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]]}
+
+ -- nvim_command causes a vimL exception, check that it is properly caught
+ -- and propagated as an error message in async contexts.. #10809
+ exec_lua([[
+ vim.schedule(function()
+ vim.api.nvim_command(":echo 'err")
+ end)
+ ]])
+ screen:expect{grid=[[
+ |
+ {2: }|
+ {3:Error executing vim.schedule lua callback: [string "<nvim>"]}|
+ {3::2: Vim(echo):E115: Missing quote: 'err} |
+ {4:Press ENTER or type command to continue}^ |
+ ]]}
+ end)
+
+ it("vim.split", function()
+ local split = function(str, sep, plain)
+ return exec_lua('return vim.split(...)', str, sep, plain)
+ end
+
+ local tests = {
+ { "a,b", ",", false, { 'a', 'b' } },
+ { ":aa::bb:", ":", false, { '', 'aa', '', 'bb', '' } },
+ { "::ee::ff:", ":", false, { '', '', 'ee', '', 'ff', '' } },
+ { "ab", ".", false, { '', '', '' } },
+ { "a1b2c", "[0-9]", false, { 'a', 'b', 'c' } },
+ { "xy", "", false, { 'x', 'y' } },
+ { "here be dragons", " ", false, { "here", "be", "dragons"} },
+ { "axaby", "ab?", false, { '', 'x', 'y' } },
+ { "f v2v v3v w2w ", "([vw])2%1", false, { 'f ', ' v3v ', ' ' } },
+ { "x*yz*oo*l", "*", true, { 'x', 'yz', 'oo', 'l' } },
+ }
+
+ for _, t in ipairs(tests) do
+ eq(t[4], split(t[1], t[2], t[3]))
+ end
+
+ local loops = {
+ { "abc", ".-" },
+ }
+
+ for _, t in ipairs(loops) do
+ matches(".*Infinite loop detected", pcall_err(split, t[1], t[2]))
+ end
+
+ -- Validates args.
+ eq(true, pcall(split, 'string', 'string'))
+ eq('Error executing lua: .../shared.lua: s: expected string, got number',
+ pcall_err(split, 1, 'string'))
+ eq('Error executing lua: .../shared.lua: sep: expected string, got number',
+ pcall_err(split, 'string', 1))
+ eq('Error executing lua: .../shared.lua: plain: expected boolean, got number',
+ pcall_err(split, 'string', 'string', 1))
+ end)
+
+ it('vim.trim', function()
+ local trim = function(s)
+ return exec_lua('return vim.trim(...)', s)
+ end
+
+ local trims = {
+ { " a", "a" },
+ { " b ", "b" },
+ { "\tc" , "c" },
+ { "r\n", "r" },
+ }
+
+ for _, t in ipairs(trims) do
+ assert(t[2], trim(t[1]))
+ end
+
+ -- Validates args.
+ eq('Error executing lua: .../shared.lua: s: expected string, got number',
+ pcall_err(trim, 2))
+ end)
+
+ it('vim.inspect', function()
+ -- just make sure it basically works, it has its own test suite
+ local inspect = function(t, opts)
+ return exec_lua('return vim.inspect(...)', t, opts)
+ end
+
+ eq('2', inspect(2))
+ eq('{+a = {+b = 1+}+}',
+ inspect({ a = { b = 1 } }, { newline = '+', indent = '' }))
+
+ -- special value vim.inspect.KEY works
+ eq('{ KEY_a = "x", KEY_b = "y"}', exec_lua([[
+ return vim.inspect({a="x", b="y"}, {newline = '', process = function(item, path)
+ if path[#path] == vim.inspect.KEY then
+ return 'KEY_'..item
+ end
+ return item
+ end})
+ ]]))
+ end)
+
+ it("vim.deepcopy", function()
+ local is_dc = exec_lua([[
+ local a = { x = { 1, 2 }, y = 5}
+ local b = vim.deepcopy(a)
+
+ local count = 0
+ for _ in pairs(b) do count = count + 1 end
+
+ return b.x[1] == 1 and b.x[2] == 2 and b.y == 5 and count == 2
+ and tostring(a) ~= tostring(b)
+ ]])
+
+ assert(is_dc)
+ end)
+
+ it('vim.pesc', function()
+ eq('foo%-bar', exec_lua([[return vim.pesc('foo-bar')]]))
+ eq('foo%%%-bar', exec_lua([[return vim.pesc(vim.pesc('foo-bar'))]]))
+
+ -- Validates args.
+ eq('Error executing lua: .../shared.lua: s: expected string, got number',
+ pcall_err(exec_lua, [[return vim.pesc(2)]]))
+ end)
+
+ it('vim.tbl_keys', function()
+ eq({}, exec_lua("return vim.tbl_keys({})"))
+ for _, v in pairs(exec_lua("return vim.tbl_keys({'a', 'b', 'c'})")) do
+ eq(true, exec_lua("return vim.tbl_contains({ 1, 2, 3 }, ...)", v))
+ end
+ for _, v in pairs(exec_lua("return vim.tbl_keys({a=1, b=2, c=3})")) do
+ eq(true, exec_lua("return vim.tbl_contains({ 'a', 'b', 'c' }, ...)", v))
+ end
+ end)
+
+ it('vim.tbl_values', function()
+ eq({}, exec_lua("return vim.tbl_values({})"))
+ for _, v in pairs(exec_lua("return vim.tbl_values({'a', 'b', 'c'})")) do
+ eq(true, exec_lua("return vim.tbl_contains({ 'a', 'b', 'c' }, ...)", v))
+ end
+ for _, v in pairs(exec_lua("return vim.tbl_values({a=1, b=2, c=3})")) do
+ eq(true, exec_lua("return vim.tbl_contains({ 1, 2, 3 }, ...)", v))
+ end
+ end)
+
+ it('vim.tbl_islist', function()
+ eq(NIL, exec_lua("return vim.tbl_islist({})"))
+ eq(true, exec_lua("return vim.tbl_islist({'a', 'b', 'c'})"))
+ eq(false, exec_lua("return vim.tbl_islist({'a', '32', a='hello', b='baz'})"))
+ eq(false, exec_lua("return vim.tbl_islist({1, a='hello', b='baz'})"))
+ eq(false, exec_lua("return vim.tbl_islist({a='hello', b='baz', 1})"))
+ eq(false, exec_lua("return vim.tbl_islist({1, 2, nil, a='hello'})"))
+ end)
+
+ it('vim.tbl_isempty', function()
+ eq(true, exec_lua("return vim.tbl_isempty({})"))
+ eq(false, exec_lua("return vim.tbl_isempty({ 1, 2, 3 })"))
+ eq(false, exec_lua("return vim.tbl_isempty({a=1, b=2, c=3})"))
+ end)
+
+ it('vim.deep_equal', function()
+ eq(true, exec_lua [[ return vim.deep_equal({a=1}, {a=1}) ]])
+ eq(true, exec_lua [[ return vim.deep_equal({a={b=1}}, {a={b=1}}) ]])
+ eq(true, exec_lua [[ return vim.deep_equal({a={b={nil}}}, {a={b={}}}) ]])
+ eq(true, exec_lua [[ return vim.deep_equal({a=1, [5]=5}, {nil,nil,nil,nil,5,a=1}) ]])
+ eq(false, exec_lua [[ return vim.deep_equal(1, {nil,nil,nil,nil,5,a=1}) ]])
+ eq(false, exec_lua [[ return vim.deep_equal(1, 3) ]])
+ eq(false, exec_lua [[ return vim.deep_equal(nil, 3) ]])
+ eq(false, exec_lua [[ return vim.deep_equal({a=1}, {a=2}) ]])
+ end)
+
+ it('vim.list_extend', function()
+ eq({1,2,3}, exec_lua [[ return vim.list_extend({1}, {2,3}) ]])
+ eq('Error executing lua: .../shared.lua: src: expected table, got nil',
+ pcall_err(exec_lua, [[ return vim.list_extend({1}, nil) ]]))
+ eq({1,2}, exec_lua [[ return vim.list_extend({1}, {2;a=1}) ]])
+ eq(true, exec_lua [[ local a = {1} return vim.list_extend(a, {2;a=1}) == a ]])
+ eq({2}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 1) ]])
+ eq({}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 2) ]])
+ eq({}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 1, -1) ]])
+ eq({2}, exec_lua [[ return vim.list_extend({}, {2;a=1}, -1, 2) ]])
+ end)
+
+ it('vim.tbl_add_reverse_lookup', function()
+ eq(true, exec_lua [[
+ local a = { A = 1 }
+ vim.tbl_add_reverse_lookup(a)
+ return vim.deep_equal(a, { A = 1; [1] = 'A'; })
+ ]])
+ -- Throw an error for trying to do it twice (run into an existing key)
+ local code = [[
+ local res = {}
+ local a = { A = 1 }
+ vim.tbl_add_reverse_lookup(a)
+ assert(vim.deep_equal(a, { A = 1; [1] = 'A'; }))
+ vim.tbl_add_reverse_lookup(a)
+ ]]
+ matches('Error executing lua: .../shared.lua: The reverse lookup found an existing value for "[1A]" while processing key "[1A]"',
+ pcall_err(exec_lua, code))
+ end)
+
+ it('vim.call, vim.fn', function()
+ eq(true, exec_lua([[return vim.call('sin', 0.0) == 0.0 ]]))
+ eq(true, exec_lua([[return vim.fn.sin(0.0) == 0.0 ]]))
+ -- compat: nvim_call_function uses "special" value for vimL float
+ eq(false, exec_lua([[return vim.api.nvim_call_function('sin', {0.0}) == 0.0 ]]))
+
+ source([[
+ func! FooFunc(test)
+ let g:test = a:test
+ return {}
+ endfunc
+ func! VarArg(...)
+ return a:000
+ endfunc
+ func! Nilly()
+ return [v:null, v:null]
+ endfunc
+ ]])
+ eq(true, exec_lua([[return next(vim.fn.FooFunc(3)) == nil ]]))
+ eq(3, eval("g:test"))
+ -- compat: nvim_call_function uses "special" value for empty dict
+ eq(true, exec_lua([[return next(vim.api.nvim_call_function("FooFunc", {5})) == true ]]))
+ eq(5, eval("g:test"))
+
+ eq({2, "foo", true}, exec_lua([[return vim.fn.VarArg(2, "foo", true)]]))
+
+ eq(true, exec_lua([[
+ local x = vim.fn.Nilly()
+ return #x == 2 and x[1] == vim.NIL and x[2] == vim.NIL
+ ]]))
+ eq({NIL, NIL}, exec_lua([[return vim.fn.Nilly()]]))
+
+ -- error handling
+ eq({false, 'Vim:E714: List required'}, exec_lua([[return {pcall(vim.fn.add, "aa", "bb")}]]))
+ end)
+
+ it('vim.rpcrequest and vim.rpcnotify', function()
+ exec_lua([[
+ chan = vim.fn.jobstart({'cat'}, {rpc=true})
+ vim.rpcrequest(chan, 'nvim_set_current_line', 'meow')
+ ]])
+ eq('meow', meths.get_current_line())
+ command("let x = [3, 'aa', v:true, v:null]")
+ eq(true, exec_lua([[
+ ret = vim.rpcrequest(chan, 'nvim_get_var', 'x')
+ return #ret == 4 and ret[1] == 3 and ret[2] == 'aa' and ret[3] == true and ret[4] == vim.NIL
+ ]]))
+ eq({3, 'aa', true, NIL}, exec_lua([[return ret]]))
+
+ -- error handling
+ eq({false, 'Invalid channel: 23'},
+ exec_lua([[return {pcall(vim.rpcrequest, 23, 'foo')}]]))
+ eq({false, 'Invalid channel: 23'},
+ exec_lua([[return {pcall(vim.rpcnotify, 23, 'foo')}]]))
+
+ eq({false, 'Vim:E121: Undefined variable: foobar'},
+ exec_lua([[return {pcall(vim.rpcrequest, chan, 'nvim_eval', "foobar")}]]))
+
+
+ -- rpcnotify doesn't wait on request
+ eq('meow', exec_lua([[
+ vim.rpcnotify(chan, 'nvim_set_current_line', 'foo')
+ return vim.api.nvim_get_current_line()
+ ]]))
+ retry(10, nil, function()
+ eq('foo', meths.get_current_line())
+ end)
+
+ local screen = Screen.new(50,7)
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {bold = true, reverse = true},
+ [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ [4] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ })
+ screen:attach()
+ exec_lua([[
+ local timer = vim.loop.new_timer()
+ timer:start(20, 0, function ()
+ -- notify ok (executed later when safe)
+ vim.rpcnotify(chan, 'nvim_set_var', 'yy', {3, vim.NIL})
+ -- rpcrequest an error
+ vim.rpcrequest(chan, 'nvim_set_current_line', 'bork')
+ end)
+ ]])
+ screen:expect{grid=[[
+ foo |
+ {1:~ }|
+ {2: }|
+ {3:Error executing luv callback:} |
+ {3:[string "<nvim>"]:6: E5560: rpcrequest must not be}|
+ {3: called in a lua loop callback} |
+ {4:Press ENTER or type command to continue}^ |
+ ]]}
+ feed('<cr>')
+ eq({3, NIL}, meths.get_var('yy'))
+ end)
+
+ it('vim.validate', function()
+ exec_lua("vim.validate{arg1={{}, 'table' }}")
+ exec_lua("vim.validate{arg1={{}, 't' }}")
+ exec_lua("vim.validate{arg1={nil, 't', true }}")
+ exec_lua("vim.validate{arg1={{ foo='foo' }, 't' }}")
+ exec_lua("vim.validate{arg1={{ 'foo' }, 't' }}")
+ exec_lua("vim.validate{arg1={'foo', 'string' }}")
+ exec_lua("vim.validate{arg1={'foo', 's' }}")
+ exec_lua("vim.validate{arg1={'', 's' }}")
+ exec_lua("vim.validate{arg1={nil, 's', true }}")
+ exec_lua("vim.validate{arg1={1, 'number' }}")
+ exec_lua("vim.validate{arg1={1, 'n' }}")
+ exec_lua("vim.validate{arg1={0, 'n' }}")
+ exec_lua("vim.validate{arg1={0.1, 'n' }}")
+ exec_lua("vim.validate{arg1={nil, 'n', true }}")
+ exec_lua("vim.validate{arg1={true, 'boolean' }}")
+ exec_lua("vim.validate{arg1={true, 'b' }}")
+ exec_lua("vim.validate{arg1={false, 'b' }}")
+ exec_lua("vim.validate{arg1={nil, 'b', true }}")
+ exec_lua("vim.validate{arg1={function()end, 'function' }}")
+ exec_lua("vim.validate{arg1={function()end, 'f' }}")
+ exec_lua("vim.validate{arg1={nil, 'f', true }}")
+ exec_lua("vim.validate{arg1={nil, 'nil' }}")
+ exec_lua("vim.validate{arg1={nil, 'nil', true }}")
+ exec_lua("vim.validate{arg1={coroutine.create(function()end), 'thread' }}")
+ exec_lua("vim.validate{arg1={nil, 'thread', true }}")
+ exec_lua("vim.validate{arg1={{}, 't' }, arg2={ 'foo', 's' }}")
+ exec_lua("vim.validate{arg1={2, function(a) return (a % 2) == 0 end, 'even number' }}")
+
+ eq("Error executing lua: .../shared.lua: 1: expected table, got number",
+ pcall_err(exec_lua, "vim.validate{ 1, 'x' }"))
+ eq("Error executing lua: .../shared.lua: invalid type name: x",
+ pcall_err(exec_lua, "vim.validate{ arg1={ 1, 'x' }}"))
+ eq("Error executing lua: .../shared.lua: invalid type name: 1",
+ pcall_err(exec_lua, "vim.validate{ arg1={ 1, 1 }}"))
+ eq("Error executing lua: .../shared.lua: invalid type name: nil",
+ pcall_err(exec_lua, "vim.validate{ arg1={ 1 }}"))
+
+ -- Validated parameters are required by default.
+ eq("Error executing lua: .../shared.lua: arg1: expected string, got nil",
+ pcall_err(exec_lua, "vim.validate{ arg1={ nil, 's' }}"))
+ -- Explicitly required.
+ eq("Error executing lua: .../shared.lua: arg1: expected string, got nil",
+ pcall_err(exec_lua, "vim.validate{ arg1={ nil, 's', false }}"))
+
+ eq("Error executing lua: .../shared.lua: arg1: expected table, got number",
+ pcall_err(exec_lua, "vim.validate{arg1={1, 't'}}"))
+ eq("Error executing lua: .../shared.lua: arg2: expected string, got number",
+ pcall_err(exec_lua, "vim.validate{arg1={{}, 't'}, arg2={1, 's'}}"))
+ eq("Error executing lua: .../shared.lua: arg2: expected string, got nil",
+ pcall_err(exec_lua, "vim.validate{arg1={{}, 't'}, arg2={nil, 's'}}"))
+ eq("Error executing lua: .../shared.lua: arg2: expected string, got nil",
+ pcall_err(exec_lua, "vim.validate{arg1={{}, 't'}, arg2={nil, 's'}}"))
+ eq("Error executing lua: .../shared.lua: arg1: expected even number, got 3",
+ pcall_err(exec_lua, "vim.validate{arg1={3, function(a) return a == 1 end, 'even number'}}"))
+ eq("Error executing lua: .../shared.lua: arg1: expected ?, got 3",
+ pcall_err(exec_lua, "vim.validate{arg1={3, function(a) return a == 1 end}}"))
+ end)
+
+ it('vim.is_callable', function()
+ eq(true, exec_lua("return vim.is_callable(function()end)"))
+ eq(true, exec_lua([[
+ local meta = { __call = function()end }
+ local function new_callable()
+ return setmetatable({}, meta)
+ end
+ local callable = new_callable()
+ return vim.is_callable(callable)
+ ]]))
+
+ eq(false, exec_lua("return vim.is_callable(1)"))
+ eq(false, exec_lua("return vim.is_callable('foo')"))
+ eq(false, exec_lua("return vim.is_callable({})"))
+ end)
+
+ it('vim.g', function()
+ exec_lua [[
+ vim.api.nvim_set_var("testing", "hi")
+ vim.api.nvim_set_var("other", 123)
+ ]]
+ eq('hi', funcs.luaeval "vim.g.testing")
+ eq(123, funcs.luaeval "vim.g.other")
+ eq(NIL, funcs.luaeval "vim.g.nonexistant")
+ end)
+
+ it('vim.env', function()
+ exec_lua [[
+ vim.fn.setenv("A", 123)
+ ]]
+ eq('123', funcs.luaeval "vim.env.A")
+ eq(true, funcs.luaeval "vim.env.B == nil")
+ end)
+
+ it('vim.v', function()
+ eq(funcs.luaeval "vim.api.nvim_get_vvar('progpath')", funcs.luaeval "vim.v.progpath")
+ eq(false, funcs.luaeval "vim.v['false']")
+ eq(NIL, funcs.luaeval "vim.v.null")
+ end)
+
+ it('vim.bo', function()
+ eq('', funcs.luaeval "vim.bo.filetype")
+ exec_lua [[
+ vim.api.nvim_buf_set_option(0, "filetype", "markdown")
+ BUF = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_option(BUF, "modifiable", false)
+ ]]
+ eq(false, funcs.luaeval "vim.bo.modified")
+ eq('markdown', funcs.luaeval "vim.bo.filetype")
+ eq(false, funcs.luaeval "vim.bo[BUF].modifiable")
+ exec_lua [[
+ vim.bo.filetype = ''
+ vim.bo[BUF].modifiable = true
+ ]]
+ eq('', funcs.luaeval "vim.bo.filetype")
+ eq(true, funcs.luaeval "vim.bo[BUF].modifiable")
+ matches("^Error executing lua: .*: Invalid option name: 'nosuchopt'$",
+ pcall_err(exec_lua, 'return vim.bo.nosuchopt'))
+ matches("^Error executing lua: .*: Expected lua string$",
+ pcall_err(exec_lua, 'return vim.bo[0][0].autoread'))
+ end)
+
+ it('vim.wo', function()
+ exec_lua [[
+ vim.api.nvim_win_set_option(0, "cole", 2)
+ vim.cmd "split"
+ vim.api.nvim_win_set_option(0, "cole", 2)
+ ]]
+ eq(2, funcs.luaeval "vim.wo.cole")
+ exec_lua [[
+ vim.wo.conceallevel = 0
+ ]]
+ eq(0, funcs.luaeval "vim.wo.cole")
+ eq(0, funcs.luaeval "vim.wo[0].cole")
+ eq(0, funcs.luaeval "vim.wo[1001].cole")
+ matches("^Error executing lua: .*: Invalid option name: 'notanopt'$",
+ pcall_err(exec_lua, 'return vim.wo.notanopt'))
+ matches("^Error executing lua: .*: Expected lua string$",
+ pcall_err(exec_lua, 'return vim.wo[0][0].list'))
+ eq(2, funcs.luaeval "vim.wo[1000].cole")
+ exec_lua [[
+ vim.wo[1000].cole = 0
+ ]]
+ eq(0, funcs.luaeval "vim.wo[1000].cole")
+ end)
+
+ it('vim.cmd', function()
+ exec_lua [[
+ vim.cmd "autocmd BufNew * ++once lua BUF = vim.fn.expand('<abuf>')"
+ vim.cmd "new"
+ ]]
+ eq('2', funcs.luaeval "BUF")
+ eq(2, funcs.luaeval "#vim.api.nvim_list_bufs()")
+ end)
+end)
diff --git a/test/functional/normal/jump_spec.lua b/test/functional/normal/jump_spec.lua
index 5bed541752..d53b5f7415 100644
--- a/test/functional/normal/jump_spec.lua
+++ b/test/functional/normal/jump_spec.lua
@@ -5,6 +5,7 @@ local command = helpers.command
local eq = helpers.eq
local funcs = helpers.funcs
local feed = helpers.feed
+local redir_exec = helpers.redir_exec
local write_file = helpers.write_file
describe('jumplist', function()
@@ -46,3 +47,93 @@ describe('jumplist', function()
eq(buf1, funcs.bufnr('%'))
end)
end)
+
+describe('jumpoptions=stack behaves like browser history', function()
+ before_each(function()
+ clear()
+ feed(':clearjumps<cr>')
+
+ -- Add lines so that we have locations to jump to.
+ for i = 1,101,1
+ do
+ feed('iLine ' .. i .. '<cr><esc>')
+ end
+
+ -- Jump around to add some locations to the jump list.
+ feed('0gg')
+ feed('10gg')
+ feed('20gg')
+ feed('30gg')
+ feed('40gg')
+ feed('50gg')
+
+ feed(':set jumpoptions=stack<cr>')
+ end)
+
+ after_each(function()
+ feed('set jumpoptions=')
+ end)
+
+ it('discards the tail when navigating from the middle', function()
+ feed('<C-O>')
+ feed('<C-O>')
+
+ eq( '\n'
+ .. ' jump line col file/text\n'
+ .. ' 4 102 0 \n'
+ .. ' 3 1 0 Line 1\n'
+ .. ' 2 10 0 Line 10\n'
+ .. ' 1 20 0 Line 20\n'
+ .. '> 0 30 0 Line 30\n'
+ .. ' 1 40 0 Line 40\n'
+ .. ' 2 50 0 Line 50',
+ redir_exec('jumps'))
+
+ feed('90gg')
+
+ eq( '\n'
+ .. ' jump line col file/text\n'
+ .. ' 5 102 0 \n'
+ .. ' 4 1 0 Line 1\n'
+ .. ' 3 10 0 Line 10\n'
+ .. ' 2 20 0 Line 20\n'
+ .. ' 1 30 0 Line 30\n'
+ .. '>',
+ redir_exec('jumps'))
+ end)
+
+ it('does not add the same location twice adjacently', function()
+ feed('60gg')
+ feed('60gg')
+
+ eq( '\n'
+ .. ' jump line col file/text\n'
+ .. ' 7 102 0 \n'
+ .. ' 6 1 0 Line 1\n'
+ .. ' 5 10 0 Line 10\n'
+ .. ' 4 20 0 Line 20\n'
+ .. ' 3 30 0 Line 30\n'
+ .. ' 2 40 0 Line 40\n'
+ .. ' 1 50 0 Line 50\n'
+ .. '>',
+ redir_exec('jumps'))
+ end)
+
+ it('does add the same location twice nonadjacently', function()
+ feed('10gg')
+ feed('20gg')
+
+ eq( '\n'
+ .. ' jump line col file/text\n'
+ .. ' 8 102 0 \n'
+ .. ' 7 1 0 Line 1\n'
+ .. ' 6 10 0 Line 10\n'
+ .. ' 5 20 0 Line 20\n'
+ .. ' 4 30 0 Line 30\n'
+ .. ' 3 40 0 Line 40\n'
+ .. ' 2 50 0 Line 50\n'
+ .. ' 1 10 0 Line 10\n'
+ .. '>',
+ redir_exec('jumps'))
+ end)
+end)
diff --git a/test/functional/normal/put_spec.lua b/test/functional/normal/put_spec.lua
index 40a4f051e3..357fafec44 100644
--- a/test/functional/normal/put_spec.lua
+++ b/test/functional/normal/put_spec.lua
@@ -307,7 +307,7 @@ describe('put command', function()
-- }}}
-- Conversion functions {{{
- local function convert_characterwise(expect_base, conversion_table,
+ local function convert_charwise(expect_base, conversion_table,
virtualedit_end, visual_put)
expect_base = dedent(expect_base)
-- There is no difference between 'P' and 'p' when VIsual_active
@@ -335,7 +335,7 @@ describe('put command', function()
expect_base = expect_base:gsub('(test_stringx?)"', '%1.')
end
return expect_base
- end -- convert_characterwise()
+ end -- convert_charwise()
local function make_back(string)
local prev_line
@@ -500,7 +500,7 @@ describe('put command', function()
local function run_normal_mode_tests(test_string, base_map, extra_setup,
virtualedit_end, selection_string)
local function convert_closure(e, c)
- return convert_characterwise(e, c, virtualedit_end, selection_string)
+ return convert_charwise(e, c, virtualedit_end, selection_string)
end
local function expect_normal_creator(expect_base, conversion_table)
local test_expect = expect_creator(convert_closure, expect_base, conversion_table)
diff --git a/test/functional/options/chars_spec.lua b/test/functional/options/chars_spec.lua
index 1330c29e61..5439ca3dba 100644
--- a/test/functional/options/chars_spec.lua
+++ b/test/functional/options/chars_spec.lua
@@ -16,10 +16,6 @@ describe("'fillchars'", function()
screen:attach()
end)
- after_each(function()
- screen:detach()
- end)
-
local function shouldfail(val,errval)
errval = errval or val
eq('Vim(set):E474: Invalid argument: fillchars='..errval,
@@ -71,16 +67,29 @@ describe("'fillchars'", function()
shouldfail('eob:xy') -- two ascii chars
shouldfail('eob:\255', 'eob:<ff>') -- invalid UTF-8
end)
- it('is local to window', function()
- clear()
- screen = Screen.new(50, 5)
- screen:attach()
+ it('has global value', function()
+ screen:try_resize(50, 5)
insert("foo\nbar")
command('set laststatus=0')
command('1,2fold')
command('vsplit')
command('set fillchars=fold:x')
screen:expect([[
+ ^+-- 2 lines: fooxxxxxxxx│+-- 2 lines: fooxxxxxxx|
+ ~ │~ |
+ ~ │~ |
+ ~ │~ |
+ |
+ ]])
+ end)
+ it('has local window value', function()
+ screen:try_resize(50, 5)
+ insert("foo\nbar")
+ command('set laststatus=0')
+ command('1,2fold')
+ command('vsplit')
+ command('setl fillchars=fold:x')
+ screen:expect([[
^+-- 2 lines: fooxxxxxxxx│+-- 2 lines: foo·······|
~ │~ |
~ │~ |
@@ -100,16 +109,25 @@ describe("'listchars'", function()
screen:attach()
end)
- after_each(function()
- screen:detach()
+ it('has global value', function()
+ feed('i<tab><tab><tab><esc>')
+ command('set list laststatus=0')
+ command('vsplit')
+ command('set listchars=tab:<->')
+ screen:expect([[
+ <------><------>^<------> │<------><------><------>|
+ ~ │~ |
+ ~ │~ |
+ ~ │~ |
+ |
+ ]])
end)
-
- it('is local to window', function()
+ it('has value local to window', function()
feed('i<tab><tab><tab><esc>')
- command('set laststatus=0')
- command('set list listchars=tab:<->')
+ command('set list laststatus=0')
+ command('setl listchars=tab:<->')
command('vsplit')
- command('set listchars&')
+ command('setl listchars<')
screen:expect([[
> > ^> │<------><------><------>|
~ │~ |
diff --git a/test/functional/options/defaults_spec.lua b/test/functional/options/defaults_spec.lua
index 490a04186d..57e5077989 100644
--- a/test/functional/options/defaults_spec.lua
+++ b/test/functional/options/defaults_spec.lua
@@ -224,9 +224,6 @@ describe('startup defaults', function()
XDG_DATA_HOME=xdgdir,
NVIM_LOG_FILE='', -- Empty is invalid.
}})
- -- server_start() calls ELOG, which tickles log_path_init().
- pcall(command, 'call serverstart(serverlist()[0])')
-
eq(xdgdir..'/'..datasubdir..'/log', string.gsub(eval('$NVIM_LOG_FILE'), '\\', '/'))
end)
it('defaults to stdpath("data")/log if invalid', function()
@@ -235,9 +232,6 @@ describe('startup defaults', function()
XDG_DATA_HOME=xdgdir,
NVIM_LOG_FILE='.', -- Any directory is invalid.
}})
- -- server_start() calls ELOG, which tickles log_path_init().
- pcall(command, 'call serverstart(serverlist()[0])')
-
eq(xdgdir..'/'..datasubdir..'/log', string.gsub(eval('$NVIM_LOG_FILE'), '\\', '/'))
end)
it('defaults to .nvimlog if stdpath("data") is invalid', function()
@@ -245,9 +239,6 @@ describe('startup defaults', function()
XDG_DATA_HOME='Xtest-missing-xdg-dir',
NVIM_LOG_FILE='.', -- Any directory is invalid.
}})
- -- server_start() calls ELOG, which tickles log_path_init().
- pcall(command, 'call serverstart(serverlist()[0])')
-
eq('.nvimlog', eval('$NVIM_LOG_FILE'))
end)
end)
diff --git a/test/functional/options/keymap_spec.lua b/test/functional/options/keymap_spec.lua
index 7f6d623dc7..52a714f7a8 100644
--- a/test/functional/options/keymap_spec.lua
+++ b/test/functional/options/keymap_spec.lua
@@ -30,7 +30,7 @@ describe("'keymap' / :lmap", function()
command('lmapclear <buffer>')
command('set keymap=dvorak')
command('set nomore')
- local bindings = funcs.nvim_command_output('lmap')
+ local bindings = funcs.nvim_exec('lmap', true)
eq(dedent([[
l " @_
diff --git a/test/functional/plugin/health_spec.lua b/test/functional/plugin/health_spec.lua
index 3525e235de..a78ed07876 100644
--- a/test/functional/plugin/health_spec.lua
+++ b/test/functional/plugin/health_spec.lua
@@ -116,8 +116,6 @@ describe('health.vim', function()
screen:set_default_attr_ids({
Ok = { foreground = Screen.colors.Grey3, background = 6291200 },
Error = { foreground = Screen.colors.Grey100, background = Screen.colors.Red },
- })
- screen:set_default_attr_ignore({
Heading = { bold=true, foreground=Screen.colors.Magenta },
Heading2 = { foreground = Screen.colors.SlateBlue },
Bar = { foreground=Screen.colors.Purple },
@@ -126,18 +124,18 @@ describe('health.vim', function()
command("checkhealth foo success1")
command("1tabclose")
command("set laststatus=0")
- screen:expect([[
+ screen:expect{grid=[[
^ |
- health#foo#check |
- ========================================================================|
- - {Error:ERROR:} No healthcheck found for "foo" plugin. |
+ {Heading:health#foo#check} |
+ {Bar:========================================================================}|
+ {Bullet: -} {Error:ERROR:} No healthcheck found for "foo" plugin. |
|
- health#success1#check |
- ========================================================================|
- ## report 1 |
- - {Ok:OK:} everything is fine |
+ {Heading:health#success1#check} |
+ {Bar:========================================================================}|
+ {Heading2:##}{Heading: report 1} |
+ {Bullet: -} {Ok:OK:} everything is fine |
|
- ]])
+ ]]}
end)
it("gracefully handles invalid healthcheck", function()
diff --git a/test/functional/plugin/lsp/lsp_spec.lua b/test/functional/plugin/lsp/lsp_spec.lua
new file mode 100644
index 0000000000..c38c9b72ce
--- /dev/null
+++ b/test/functional/plugin/lsp/lsp_spec.lua
@@ -0,0 +1,682 @@
+local helpers = require('test.functional.helpers')(after_each)
+
+local clear = helpers.clear
+local exec_lua = helpers.exec_lua
+local eq = helpers.eq
+local NIL = helpers.NIL
+
+-- Use these to get access to a coroutine so that I can run async tests and use
+-- yield.
+local run, stop = helpers.run, helpers.stop
+
+if helpers.pending_win32(pending) then return end
+
+local is_windows = require'luv'.os_uname().sysname == "Windows"
+local lsp_test_rpc_server_file = "test/functional/fixtures/lsp-test-rpc-server.lua"
+if is_windows then
+ lsp_test_rpc_server_file = lsp_test_rpc_server_file:gsub("/", "\\")
+end
+
+local function test_rpc_server_setup(test_name, timeout_ms)
+ exec_lua([=[
+ lsp = require('vim.lsp')
+ local test_name, fixture_filename, timeout = ...
+ TEST_RPC_CLIENT_ID = lsp.start_client {
+ cmd = {
+ vim.api.nvim_get_vvar("progpath"), '-Es', '-u', 'NONE', '--headless',
+ "-c", string.format("lua TEST_NAME = %q", test_name),
+ "-c", string.format("lua TIMEOUT = %d", timeout),
+ "-c", "luafile "..fixture_filename,
+ };
+ callbacks = setmetatable({}, {
+ __index = function(t, method)
+ return function(...)
+ return vim.rpcrequest(1, 'callback', ...)
+ end
+ end;
+ });
+ root_dir = vim.loop.cwd();
+ on_init = function(client, result)
+ TEST_RPC_CLIENT = client
+ vim.rpcrequest(1, "init", result)
+ end;
+ on_exit = function(...)
+ vim.rpcnotify(1, "exit", ...)
+ end;
+ }
+ ]=], test_name, lsp_test_rpc_server_file, timeout_ms or 1e3)
+end
+
+local function test_rpc_server(config)
+ if config.test_name then
+ clear()
+ test_rpc_server_setup(config.test_name, config.timeout_ms or 1e3)
+ end
+ local client = setmetatable({}, {
+ __index = function(_, name)
+ -- Workaround for not being able to yield() inside __index for Lua 5.1 :(
+ -- Otherwise I would just return the value here.
+ return function(...)
+ return exec_lua([=[
+ local name = ...
+ if type(TEST_RPC_CLIENT[name]) == 'function' then
+ return TEST_RPC_CLIENT[name](select(2, ...))
+ else
+ return TEST_RPC_CLIENT[name]
+ end
+ ]=], name, ...)
+ end
+ end;
+ })
+ local code, signal
+ local function on_request(method, args)
+ if method == "init" then
+ if config.on_init then
+ config.on_init(client, unpack(args))
+ end
+ return NIL
+ end
+ if method == 'callback' then
+ if config.on_callback then
+ config.on_callback(unpack(args))
+ end
+ end
+ return NIL
+ end
+ local function on_notify(method, args)
+ if method == 'exit' then
+ code, signal = unpack(args)
+ return stop()
+ end
+ end
+ -- TODO specify timeout?
+ -- run(on_request, on_notify, config.on_setup, 1000)
+ run(on_request, on_notify, config.on_setup)
+ if config.on_exit then
+ config.on_exit(code, signal)
+ end
+ stop()
+ if config.test_name then
+ exec_lua("lsp._vim_exit_handler()")
+ end
+end
+
+describe('Language Client API', function()
+ describe('server_name is specified', function()
+ before_each(function()
+ clear()
+ -- Run an instance of nvim on the file which contains our "scripts".
+ -- Pass TEST_NAME to pick the script.
+ local test_name = "basic_init"
+ exec_lua([=[
+ lsp = require('vim.lsp')
+ local test_name, fixture_filename = ...
+ TEST_RPC_CLIENT_ID = lsp.start_client {
+ cmd = {
+ vim.api.nvim_get_vvar("progpath"), '-Es', '-u', 'NONE', '--headless',
+ "-c", string.format("lua TEST_NAME = %q", test_name),
+ "-c", "luafile "..fixture_filename;
+ };
+ root_dir = vim.loop.cwd();
+ }
+ ]=], test_name, lsp_test_rpc_server_file)
+ end)
+
+ after_each(function()
+ exec_lua("lsp._vim_exit_handler()")
+ -- exec_lua("lsp.stop_all_clients(true)")
+ end)
+
+ describe('start_client and stop_client', function()
+ it('should return true', function()
+ for _ = 1, 20 do
+ helpers.sleep(10)
+ if exec_lua("return #lsp.get_active_clients()") > 0 then
+ break
+ end
+ end
+ eq(1, exec_lua("return #lsp.get_active_clients()"))
+ eq(false, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID) == nil"))
+ eq(false, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID).is_stopped()"))
+ exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID).stop()")
+ eq(false, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID).is_stopped()"))
+ for _ = 1, 20 do
+ helpers.sleep(10)
+ if exec_lua("return #lsp.get_active_clients()") == 0 then
+ break
+ end
+ end
+ eq(true, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID) == nil"))
+ end)
+ end)
+ end)
+
+ describe('basic_init test', function()
+ it('should run correctly', function()
+ local expected_callbacks = {
+ {NIL, "test", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "basic_init";
+ on_init = function(client, _init_result)
+ -- client is a dummy object which will queue up commands to be run
+ -- once the server initializes. It can't accept lua callbacks or
+ -- other types that may be unserializable for now.
+ client.stop()
+ end;
+ -- If the program timed out, then code will be nil.
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ -- Note that NIL must be used here.
+ -- on_callback(err, method, result, client_id)
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...})
+ end;
+ }
+ end)
+
+ it('should fail', function()
+ local expected_callbacks = {
+ {NIL, "test", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "basic_init";
+ on_init = function(client)
+ client.notify('test')
+ client.stop()
+ end;
+ on_exit = function(code, signal)
+ eq(1, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...}, "expected callback")
+ end;
+ }
+ end)
+
+ it('should succeed with manual shutdown', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "test", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "basic_init";
+ on_init = function(client)
+ eq(0, client.resolved_capabilities().text_document_did_change)
+ client.request('shutdown')
+ client.notify('exit')
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...}, "expected callback")
+ end;
+ }
+ end)
+
+ it('should verify capabilities sent', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ }
+ test_rpc_server {
+ test_name = "basic_check_capabilities";
+ on_init = function(client)
+ client.stop()
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(...)
+ eq(table.remove(expected_callbacks), {...}, "expected callback")
+ end;
+ }
+ end)
+
+ it('should not send didOpen if the buffer closes before init', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_finish";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ eq(1, exec_lua("return TEST_RPC_CLIENT_ID"))
+ eq(true, exec_lua("return lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)"))
+ eq(true, exec_lua("return lsp.buf_is_attached(BUFFER, TEST_RPC_CLIENT_ID)"))
+ exec_lua [[
+ vim.api.nvim_command(BUFFER.."bwipeout")
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ client.notify('finish')
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body sent attaching before init', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(not lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID), "Shouldn't attach twice")
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body sent attaching after init', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body and didChange full', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ exec_lua [[
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "boop";
+ })
+ ]]
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body and didChange full with noeol', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change_noeol";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ vim.api.nvim_buf_set_option(BUFFER, 'eol', false)
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(full_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ exec_lua [[
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "boop";
+ })
+ ]]
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ -- TODO(askhan) we don't support full for now, so we can disable these tests.
+ pending('should check the body and didChange incremental', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change_incremental";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Incremental")
+ eq(sync_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ exec_lua [[
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "boop";
+ })
+ ]]
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ -- TODO(askhan) we don't support full for now, so we can disable these tests.
+ pending('should check the body and didChange incremental normal mode editting', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change_incremental_editting";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Incremental")
+ eq(sync_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ helpers.command("normal! 1Go")
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body and didChange full with 2 changes', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change_multi";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(sync_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ exec_lua [[
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "321";
+ })
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "boop";
+ })
+ ]]
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ it('should check the body and didChange full lifecycle', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "basic_check_buffer_open_and_change_multi_and_close";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
+ eq(sync_kind, client.resolved_capabilities().text_document_did_change)
+ eq(true, client.resolved_capabilities().text_document_open_close)
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ if method == 'start' then
+ exec_lua [[
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "321";
+ })
+ vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
+ "boop";
+ })
+ vim.api.nvim_command(BUFFER.."bwipeout")
+ ]]
+ client.notify('finish')
+ end
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ if method == 'finish' then
+ client.stop()
+ end
+ end;
+ }
+ end)
+
+ end)
+
+ describe("parsing tests", function()
+ it('should handle invalid content-length correctly', function()
+ local expected_callbacks = {
+ {NIL, "shutdown", {}, 1};
+ {NIL, "finish", {}, 1};
+ {NIL, "start", {}, 1};
+ }
+ local client
+ test_rpc_server {
+ test_name = "invalid_header";
+ on_setup = function()
+ end;
+ on_init = function(_client)
+ client = _client
+ client.stop(true)
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code") eq(0, signal, "exit signal")
+ end;
+ on_callback = function(err, method, params, client_id)
+ eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
+ end;
+ }
+ end)
+
+ end)
+end)
diff --git a/test/functional/plugin/lsp/util_spec.lua b/test/functional/plugin/lsp/util_spec.lua
new file mode 100644
index 0000000000..1cf0e48be4
--- /dev/null
+++ b/test/functional/plugin/lsp/util_spec.lua
@@ -0,0 +1,76 @@
+local helpers = require('test.functional.helpers')(after_each)
+local eq = helpers.eq
+local exec_lua = helpers.exec_lua
+local dedent = helpers.dedent
+local insert = helpers.insert
+local clear = helpers.clear
+
+describe('LSP util', function()
+ local test_text = dedent([[
+ First line of text
+ Second line of text
+ Third line of text
+ Fourth line of text]])
+
+ local function reset()
+ clear()
+ insert(test_text)
+ end
+
+ before_each(reset)
+
+ local function make_edit(y_0, x_0, y_1, x_1, text)
+ return {
+ range = {
+ start = { line = y_0, character = x_0 };
+ ["end"] = { line = y_1, character = x_1 };
+ };
+ newText = type(text) == 'table' and table.concat(text, '\n') or (text or "");
+ }
+ end
+
+ local function buf_lines(bufnr)
+ return exec_lua("return vim.api.nvim_buf_get_lines((...), 0, -1, false)", bufnr)
+ end
+
+ describe('apply_edits', function()
+ it('should apply simple edits', function()
+ local edits = {
+ make_edit(0, 0, 0, 0, {"123"});
+ make_edit(1, 0, 1, 1, {"2"});
+ make_edit(2, 0, 2, 2, {"3"});
+ }
+ exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
+ eq({
+ '123First line of text';
+ '2econd line of text';
+ '3ird line of text';
+ 'Fourth line of text';
+ }, buf_lines(1))
+ end)
+
+ it('should apply complex edits', function()
+ local edits = {
+ make_edit(0, 0, 0, 0, {"", "12"});
+ make_edit(0, 0, 0, 0, {"3", "foo"});
+ make_edit(0, 1, 0, 1, {"bar", "123"});
+ make_edit(0, #"First ", 0, #"First line of text", {"guy"});
+ make_edit(1, 0, 1, #'Second', {"baz"});
+ make_edit(2, #'Th', 2, #"Third", {"e next"});
+ make_edit(3, #'', 3, #"Fourth", {"another line of text", "before this"});
+ make_edit(3, #'Fourth', 3, #"Fourth line of text", {"!"});
+ }
+ exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
+ eq({
+ '';
+ '123';
+ 'fooFbar';
+ '123irst guy';
+ 'baz line of text';
+ 'The next line of text';
+ 'another line of text';
+ 'before this!';
+ }, buf_lines(1))
+ end)
+ end)
+end)
diff --git a/test/functional/plugin/man_spec.lua b/test/functional/plugin/man_spec.lua
index d95995797e..e5b2e7dc1f 100644
--- a/test/functional/plugin/man_spec.lua
+++ b/test/functional/plugin/man_spec.lua
@@ -19,38 +19,32 @@ describe(':Man', function()
u = { underline = true },
bi = { bold = true, italic = true },
biu = { bold = true, italic = true, underline = true },
- })
- screen:set_default_attr_ignore({
- { foreground = Screen.colors.Blue }, -- control chars
- { bold = true, foreground = Screen.colors.Blue } -- empty line '~'s
+ c = { foreground = Screen.colors.Blue }, -- control chars
+ eob = { bold = true, foreground = Screen.colors.Blue } -- empty line '~'s
})
screen:attach()
end)
- after_each(function()
- screen:detach()
- end)
-
it('clears backspaces from text and adds highlights', function()
rawfeed([[
ithis i<C-v><C-h>is<C-v><C-h>s a<C-v><C-h>a test
with _<C-v><C-h>o_<C-v><C-h>v_<C-v><C-h>e_<C-v><C-h>r_<C-v><C-h>s_<C-v><C-h>t_<C-v><C-h>r_<C-v><C-h>u_<C-v><C-h>c_<C-v><C-h>k text<ESC>]])
- screen:expect([[
- this i^His^Hs a^Ha test |
- with _^Ho_^Hv_^He_^Hr_^Hs_^Ht_^Hr_^Hu_^Hc_^Hk tex^t |
- ~ |
- ~ |
- |
- ]])
+ screen:expect{grid=[[
+ this i{c:^H}is{c:^H}s a{c:^H}a test |
+ with _{c:^H}o_{c:^H}v_{c:^H}e_{c:^H}r_{c:^H}s_{c:^H}t_{c:^H}r_{c:^H}u_{c:^H}c_{c:^H}k tex^t |
+ {eob:~ }|
+ {eob:~ }|
+ |
+ ]]}
eval('man#init_pager()')
screen:expect([[
^this {b:is} {b:a} test |
with {u:overstruck} text |
- ~ |
- ~ |
+ {eob:~ }|
+ {eob:~ }|
|
]])
end)
@@ -60,21 +54,21 @@ describe(':Man', function()
ithis <C-v><ESC>[1mis <C-v><ESC>[3ma <C-v><ESC>[4mtest<C-v><ESC>[0m
<C-v><ESC>[4mwith<C-v><ESC>[24m <C-v><ESC>[4mescaped<C-v><ESC>[24m <C-v><ESC>[4mtext<C-v><ESC>[24m<ESC>]])
- screen:expect([=[
- this ^[[1mis ^[[3ma ^[[4mtest^[[0m |
- ^[[4mwith^[[24m ^[[4mescaped^[[24m ^[[4mtext^[[24^m |
- ~ |
- ~ |
- |
- ]=])
+ screen:expect{grid=[=[
+ this {c:^[}[1mis {c:^[}[3ma {c:^[}[4mtest{c:^[}[0m |
+ {c:^[}[4mwith{c:^[}[24m {c:^[}[4mescaped{c:^[}[24m {c:^[}[4mtext{c:^[}[24^m |
+ {eob:~ }|
+ {eob:~ }|
+ |
+ ]=]}
eval('man#init_pager()')
screen:expect([[
^this {b:is }{bi:a }{biu:test} |
{u:with} {u:escaped} {u:text} |
- ~ |
- ~ |
+ {eob:~ }|
+ {eob:~ }|
|
]])
end)
@@ -88,8 +82,8 @@ describe(':Man', function()
screen:expect([[
^this {b:is} {b:ã‚} test |
with {u:överstrũck} te{i:xt¶} |
- ~ |
- ~ |
+ {eob:~ }|
+ {eob:~ }|
|
]])
end)
@@ -105,7 +99,7 @@ describe(':Man', function()
{b:^_begins} |
{b:mid_dle} |
{u:mid_dle} |
- ~ |
+ {eob:~ }|
|
]])
end)
@@ -121,7 +115,7 @@ describe(':Man', function()
^· {b:·} |
{b:·} |
{b:·} double |
- ~ |
+ {eob:~ }|
|
]])
end)
diff --git a/test/functional/preload.lua b/test/functional/preload.lua
index 1107b45d54..24a3977e6b 100644
--- a/test/functional/preload.lua
+++ b/test/functional/preload.lua
@@ -2,3 +2,13 @@
-- Busted started doing this to help provide more isolation. See issue #62
-- for more information about this.
local helpers = require('test.functional.helpers')(nil)
+local iswin = helpers.iswin
+
+if iswin() then
+ local ffi = require('ffi')
+ ffi.cdef[[
+ typedef int errno_t;
+ errno_t _set_fmode(int mode);
+ ]]
+ ffi.C._set_fmode(0x8000)
+end
diff --git a/test/functional/provider/clipboard_spec.lua b/test/functional/provider/clipboard_spec.lua
index b2d75db745..da9dd09129 100644
--- a/test/functional/provider/clipboard_spec.lua
+++ b/test/functional/provider/clipboard_spec.lua
@@ -88,6 +88,11 @@ describe('clipboard', function()
before_each(function()
clear()
screen = Screen.new(72, 4)
+ screen:set_default_attr_ids({
+ [0] = {bold = true, foreground = Screen.colors.Blue},
+ [1] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ [2] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ })
screen:attach()
command("set display-=msgsep")
end)
@@ -103,22 +108,22 @@ describe('clipboard', function()
feed_command('redir @+> | :silent echo system("cat CONTRIBUTING.md") | redir END')
screen:expect([[
^ |
- ~ |
- ~ |
+ {0:~ }|
+ {0:~ }|
clipboard: No provider. Try ":checkhealth" or ":h clipboard". |
- ]], nil, {{bold = true, foreground = Screen.colors.Blue}})
+ ]])
end)
it('`:redir @+>|bogus_cmd|redir END` + invalid g:clipboard must not recurse #7184',
function()
command("let g:clipboard = 'bogus'")
feed_command('redir @+> | bogus_cmd | redir END')
- screen:expect([[
- ~ |
+ screen:expect{grid=[[
+ {0:~ }|
clipboard: No provider. Try ":checkhealth" or ":h clipboard". |
- E492: Not an editor command: bogus_cmd | redir END |
- Press ENTER or type command to continue^ |
- ]], nil, {{bold = true, foreground = Screen.colors.Blue}})
+ {1:E492: Not an editor command: bogus_cmd | redir END} |
+ {2:Press ENTER or type command to continue}^ |
+ ]]}
end)
it('invalid g:clipboard shows hint if :redir is not active', function()
@@ -131,10 +136,10 @@ describe('clipboard', function()
feed_command('let @+="foo"')
screen:expect([[
^ |
- ~ |
- ~ |
+ {0:~ }|
+ {0:~ }|
clipboard: No provider. Try ":checkhealth" or ":h clipboard". |
- ]], nil, {{bold = true, foreground = Screen.colors.Blue}})
+ ]])
end)
it('valid g:clipboard', function()
@@ -266,13 +271,17 @@ describe('clipboard (with fake clipboard.vim)', function()
function()
local screen = Screen.new(72, 4)
screen:attach()
+ screen:set_default_attr_ids({
+ [0] = {bold = true, foreground = Screen.colors.Blue},
+ [1] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ })
feed_command('redir @+> | bogus_cmd | redir END')
screen:expect([[
^ |
- ~ |
- ~ |
- E492: Not an editor command: bogus_cmd | redir END |
- ]], nil, {{bold = true, foreground = Screen.colors.Blue}})
+ {0:~ }|
+ {0:~ }|
+ {1:E492: Not an editor command: bogus_cmd | redir END} |
+ ]])
end)
it('has independent "* and unnamed registers by default', function()
@@ -637,6 +646,9 @@ describe('clipboard (with fake clipboard.vim)', function()
feed_command('set mouse=a')
local screen = Screen.new(30, 5)
+ screen:set_default_attr_ids({
+ [0] = {bold = true, foreground = Screen.colors.Blue},
+ })
screen:attach()
insert([[
the source
@@ -646,10 +658,10 @@ describe('clipboard (with fake clipboard.vim)', function()
screen:expect([[
the ^source |
a target |
- ~ |
- ~ |
+ {0:~ }|
+ {0:~ }|
|
- ]], nil, {{bold = true, foreground = Screen.colors.Blue}})
+ ]])
feed('<MiddleMouse><0,1>')
expect([[
diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua
index 1763574bf9..d40baca871 100644
--- a/test/functional/terminal/buffer_spec.lua
+++ b/test/functional/terminal/buffer_spec.lua
@@ -5,6 +5,7 @@ local wait = helpers.wait
local eval, feed_command, source = helpers.eval, helpers.feed_command, helpers.source
local eq, neq = helpers.eq, helpers.neq
local write_file = helpers.write_file
+local command= helpers.command
describe(':terminal buffer', function()
local screen
@@ -59,7 +60,7 @@ describe(':terminal buffer', function()
end)
it('does not create swap files', function()
- local swapfile = nvim('command_output', 'swapname'):gsub('\n', '')
+ local swapfile = nvim('exec', 'swapname', true):gsub('\n', '')
eq(nil, io.open(swapfile))
end)
@@ -208,22 +209,38 @@ describe(':terminal buffer', function()
feed_command('terminal')
feed('<c-\\><c-n>')
feed_command('confirm bdelete')
- screen:expect{any='Close "term://', attr_ignore=true}
+ screen:expect{any='Close "term://'}
end)
it('with &confirm', function()
feed_command('terminal')
feed('<c-\\><c-n>')
feed_command('bdelete')
- screen:expect{any='E89', attr_ignore=true}
+ screen:expect{any='E89'}
feed('<cr>')
eq('terminal', eval('&buftype'))
feed_command('set confirm | bdelete')
- screen:expect{any='Close "term://', attr_ignore=true}
+ screen:expect{any='Close "term://'}
feed('y')
neq('terminal', eval('&buftype'))
end)
end)
+
+ it('it works with set rightleft #11438', function()
+ local columns = eval('&columns')
+ feed(string.rep('a', columns))
+ command('set rightleft')
+ screen:expect([[
+ ydaer ytt|
+ {1:a}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|
+ |
+ |
+ |
+ |
+ {3:-- TERMINAL --} |
+ ]])
+ command('bdelete!')
+ end)
end)
describe('No heap-buffer-overflow when using', function()
diff --git a/test/functional/terminal/helpers.lua b/test/functional/terminal/helpers.lua
index f6cab6bd1e..d909888613 100644
--- a/test/functional/terminal/helpers.lua
+++ b/test/functional/terminal/helpers.lua
@@ -52,7 +52,7 @@ local function screen_setup(extra_rows, command, cols, opts)
[3] = {bold = true},
[4] = {foreground = 12},
[5] = {bold = true, reverse = true},
- [6] = {background = 11},
+ -- 6 was a duplicate item
[7] = {foreground = 130},
[8] = {foreground = 15, background = 1}, -- error message
[9] = {foreground = 4},
diff --git a/test/functional/terminal/highlight_spec.lua b/test/functional/terminal/highlight_spec.lua
index 06a6fd6f2b..8d3f0218af 100644
--- a/test/functional/terminal/highlight_spec.lua
+++ b/test/functional/terminal/highlight_spec.lua
@@ -121,13 +121,12 @@ it(':terminal highlight has lower precedence than editor #9964', function()
local screen = Screen.new(30, 4)
screen:set_default_attr_ids({
-- "Normal" highlight emitted by the child nvim process.
- N_child = {foreground = tonumber('0x4040ff'), background = tonumber('0xffff40')},
- -- "Search" highlight emitted by the child nvim process.
- S_child = {background = tonumber('0xffff40'), italic = true, foreground = tonumber('0x4040ff')},
+ N_child = {foreground = tonumber('0x4040ff'), background = tonumber('0xffff40'), fg_indexed=true, bg_indexed=true},
-- "Search" highlight in the parent nvim process.
S = {background = Screen.colors.Green, italic = true, foreground = Screen.colors.Red},
-- "Question" highlight in the parent nvim process.
- Q = {background = tonumber('0xffff40'), bold = true, foreground = Screen.colors.SeaGreen4},
+ -- note: bg is indexed as it comes from the (cterm) child, while fg isn't as it comes from (rgb) parent
+ Q = {background = tonumber('0xffff40'), bold = true, foreground = Screen.colors.SeaGreen4, bg_indexed=true},
})
screen:attach({rgb=true})
-- Child nvim process in :terminal (with cterm colors).
@@ -160,6 +159,54 @@ it(':terminal highlight has lower precedence than editor #9964', function()
]])
end)
+describe(':terminal highlight forwarding', function()
+ local screen
+
+ before_each(function()
+ clear()
+ screen = Screen.new(50, 7)
+ screen:set_rgb_cterm(true)
+ screen:set_default_attr_ids({
+ [1] = {{reverse = true}, {reverse = true}},
+ [2] = {{bold = true}, {bold = true}},
+ [3] = {{fg_indexed = true, foreground = tonumber('0xe0e000')}, {foreground = 3}},
+ [4] = {{foreground = tonumber('0xff8000')}, {}},
+ })
+ screen:attach()
+ command('enew | call termopen(["'..nvim_dir..'/tty-test"])')
+ feed('i')
+ screen:expect([[
+ tty ready |
+ {1: } |
+ |
+ |
+ |
+ |
+ {2:-- TERMINAL --} |
+ ]])
+ end)
+
+ it('will handle cterm and rgb attributes', function()
+ if helpers.pending_win32(pending) then return end
+ thelpers.set_fg(3)
+ thelpers.feed_data('text')
+ thelpers.feed_termcode('[38:2:255:128:0m')
+ thelpers.feed_data('color')
+ thelpers.clear_attrs()
+ thelpers.feed_data('text')
+ screen:expect{grid=[[
+ tty ready |
+ {3:text}{4:color}text{1: } |
+ |
+ |
+ |
+ |
+ {2:-- TERMINAL --} |
+ ]]}
+ end)
+end)
+
+
describe(':terminal highlight with custom palette', function()
local screen
@@ -167,7 +214,7 @@ describe(':terminal highlight with custom palette', function()
clear()
screen = Screen.new(50, 7)
screen:set_default_attr_ids({
- [1] = {foreground = tonumber('0x123456')},
+ [1] = {foreground = tonumber('0x123456')}, -- no fg_indexed when overriden
[2] = {foreground = 12},
[3] = {bold = true, reverse = true},
[5] = {background = 11},
diff --git a/test/functional/terminal/mouse_spec.lua b/test/functional/terminal/mouse_spec.lua
index 64f437f206..ee3db7ae97 100644
--- a/test/functional/terminal/mouse_spec.lua
+++ b/test/functional/terminal/mouse_spec.lua
@@ -31,10 +31,6 @@ describe(':terminal mouse', function()
]])
end)
- after_each(function()
- screen:detach()
- end)
-
describe('when the terminal has focus', function()
it('will exit focus on mouse-scroll', function()
eq('t', eval('mode()'))
diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua
index 7413081510..1df8df6f6e 100644
--- a/test/functional/terminal/scrollback_spec.lua
+++ b/test/functional/terminal/scrollback_spec.lua
@@ -21,10 +21,6 @@ describe(':terminal scrollback', function()
screen = thelpers.screen_setup(nil, nil, 30)
end)
- after_each(function()
- screen:detach()
- end)
-
describe('when the limit is exceeded', function()
before_each(function()
local lines = {}
@@ -406,8 +402,6 @@ describe("'scrollback' option", function()
feed_data(nvim_dir..'/shell-test REP 31 line'..(iswin() and '\r' or '\n'))
screen:expect{any='30: line '}
retry(nil, nil, function() expect_lines(7) end)
-
- screen:detach()
end)
it('deletes lines (only) if necessary', function()
@@ -438,14 +432,32 @@ describe("'scrollback' option", function()
command('sleep 100m')
feed_data(nvim_dir.."/shell-test REP 41 line"..(iswin() and '\r' or '\n'))
- screen:expect{any='40: line '}
+ if iswin() then
+ screen:expect{grid=[[
+ 37: line |
+ 38: line |
+ 39: line |
+ 40: line |
+ |
+ ${1: } |
+ {3:-- TERMINAL --} |
+ ]]}
+ else
+ screen:expect{grid=[[
+ 36: line |
+ 37: line |
+ 38: line |
+ 39: line |
+ 40: line |
+ {MATCH:.*}|
+ {3:-- TERMINAL --} |
+ ]]}
+ end
+ expect_lines(58)
- retry(nil, nil, function() expect_lines(58) end)
-- Verify off-screen state
eq((iswin() and '36: line' or '35: line'), eval("getline(line('w0') - 1)"))
eq((iswin() and '27: line' or '26: line'), eval("getline(line('w0') - 10)"))
-
- screen:detach()
end)
it('defaults to 10000 in :terminal buffers', function()
diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua
index c55093cb0f..077e9dc7d5 100644
--- a/test/functional/terminal/tui_spec.lua
+++ b/test/functional/terminal/tui_spec.lua
@@ -45,10 +45,6 @@ describe('TUI', function()
child_session = helpers.connect(child_server)
end)
- after_each(function()
- screen:detach()
- end)
-
-- Wait for mode in the child Nvim (avoid "typeahead race" #10826).
local function wait_for_mode(mode)
retry(nil, nil, function()
@@ -306,6 +302,49 @@ describe('TUI', function()
expect_child_buf_lines({''})
end)
+ it('paste: select-mode', function()
+ feed_data('ithis is line 1\nthis is line 2\nline 3 is here\n\027')
+ wait_for_mode('n')
+ screen:expect{grid=[[
+ this is line 1 |
+ this is line 2 |
+ line 3 is here |
+ {1: } |
+ {5:[No Name] [+] }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
+ -- Select-mode. Use <C-n> to move down.
+ feed_data('gg04lgh\14\14')
+ wait_for_mode('s')
+ feed_data('\027[200~')
+ feed_data('just paste itâ„¢')
+ feed_data('\027[201~')
+ screen:expect{grid=[[
+ thisjust paste itâ„¢{1:3} is here |
+ |
+ {4:~ }|
+ {4:~ }|
+ {5:[No Name] [+] }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
+ -- Undo.
+ feed_data('u')
+ expect_child_buf_lines{
+ 'this is line 1',
+ 'this is line 2',
+ 'line 3 is here',
+ '',
+ }
+ -- Redo.
+ feed_data('\18') -- <C-r>
+ expect_child_buf_lines{
+ 'thisjust paste itâ„¢3 is here',
+ '',
+ }
+ end)
+
it('paste: terminal mode', function()
feed_data(':set statusline=^^^^^^^\n')
feed_data(':terminal '..nvim_dir..'/tty-test\n')
@@ -447,7 +486,7 @@ describe('TUI', function()
end)
it('paste: recovers from vim.paste() failure', function()
- child_session:request('nvim_execute_lua', [[
+ child_session:request('nvim_exec_lua', [[
_G.save_paste_fn = vim.paste
vim.paste = function(lines, phase) error("fake fail") end
]], {})
@@ -505,7 +544,7 @@ describe('TUI', function()
{3:-- TERMINAL --} |
]]}
-- Paste works if vim.paste() succeeds.
- child_session:request('nvim_execute_lua', [[
+ child_session:request('nvim_exec_lua', [[
vim.paste = _G.save_paste_fn
]], {})
feed_data('\027[200~line A\nline B\n\027[201~')
@@ -524,7 +563,7 @@ describe('TUI', function()
it('paste: vim.paste() cancel (retval=false) #10865', function()
-- This test only exercises the "cancel" case. Use-case would be "dangling
-- paste", but that is not implemented yet. #10865
- child_session:request('nvim_execute_lua', [[
+ child_session:request('nvim_exec_lua', [[
vim.paste = function(lines, phase) return false end
]], {})
feed_data('\027[200~line A\nline B\n\027[201~')
@@ -539,7 +578,7 @@ describe('TUI', function()
|
{4:~ }|
{5: }|
- {8:paste: Error executing lua: vim.lua:197: Vim:E21: }|
+ {MATCH:paste: Error executing lua: vim.lua:%d+: Vim:E21: }|
{8:Cannot make changes, 'modifiable' is off} |
{10:Press ENTER or type command to continue}{1: } |
{3:-- TERMINAL --} |
@@ -566,7 +605,6 @@ describe('TUI', function()
feed_data('\027[200~'..expected..'\027[201~')
feed_data(' end')
expected = expected..' end'
- expect_child_buf_lines({expected})
screen:expect([[
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz|
zzzzzzzzzzzzzz end{1: } |
@@ -576,6 +614,24 @@ describe('TUI', function()
{3:-- INSERT --} |
{3:-- TERMINAL --} |
]])
+ expect_child_buf_lines({expected})
+ end)
+
+ it('paste: less-than sign in cmdline #11088', function()
+ local expected = '<'
+ feed_data(':')
+ wait_for_mode('c')
+ -- "bracketed paste"
+ feed_data('\027[200~'..expected..'\027[201~')
+ screen:expect{grid=[[
+ |
+ {4:~ }|
+ {4:~ }|
+ {4:~ }|
+ {5:[No Name] }|
+ :<{1: } |
+ {3:-- TERMINAL --} |
+ ]]}
end)
it('paste: big burst of input', function()
@@ -667,11 +723,11 @@ describe('TUI', function()
screen:set_option('rgb', true)
screen:set_default_attr_ids({
[1] = {reverse = true},
- [2] = {foreground = tonumber('0x4040ff')},
+ [2] = {foreground = tonumber('0x4040ff'), fg_indexed=true},
[3] = {bold = true, reverse = true},
[4] = {bold = true},
- [5] = {reverse = true, foreground = tonumber('0xe0e000')},
- [6] = {foreground = tonumber('0xe0e000')},
+ [5] = {reverse = true, foreground = tonumber('0xe0e000'), fg_indexed=true},
+ [6] = {foreground = tonumber('0xe0e000'), fg_indexed=true},
[7] = {reverse = true, foreground = Screen.colors.SeaGreen4},
[8] = {foreground = Screen.colors.SeaGreen4},
[9] = {bold = true, foreground = Screen.colors.Blue1},
@@ -715,6 +771,54 @@ describe('TUI', function()
]])
end)
+ it('forwards :term palette colors with termguicolors', function()
+ screen:set_rgb_cterm(true)
+ screen:set_default_attr_ids({
+ [1] = {{reverse = true}, {reverse = true}},
+ [2] = {{bold = true, reverse = true}, {bold = true, reverse = true}},
+ [3] = {{bold = true}, {bold = true}},
+ [4] = {{fg_indexed = true, foreground = tonumber('0xe0e000')}, {foreground = 3}},
+ [5] = {{foreground = tonumber('0xff8000')}, {}},
+ })
+
+ feed_data(':set statusline=^^^^^^^\n')
+ feed_data(':set termguicolors\n')
+ feed_data(':terminal '..nvim_dir..'/tty-test\n')
+ -- Depending on platform the above might or might not fit in the cmdline
+ -- so clear it for consistent behavior.
+ feed_data(':\027')
+ screen:expect{grid=[[
+ {1:t}ty ready |
+ |
+ |
+ |
+ {2:^^^^^^^ }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
+ feed_data(':call chansend(&channel, "\\033[38;5;3mtext\\033[38:2:255:128:0mcolor\\033[0;10mtext")\n')
+ screen:expect{grid=[[
+ {1:t}ty ready |
+ {4:text}{5:color}text |
+ |
+ |
+ {2:^^^^^^^ }|
+ |
+ {3:-- TERMINAL --} |
+ ]]}
+
+ feed_data(':set notermguicolors\n')
+ screen:expect{grid=[[
+ {1:t}ty ready |
+ {4:text}colortext |
+ |
+ |
+ {2:^^^^^^^ }|
+ :set notermguicolors |
+ {3:-- TERMINAL --} |
+ ]]}
+ end)
+
it('is included in nvim_list_uis()', function()
feed_data(':echo map(nvim_list_uis(), {k,v -> sort(items(filter(v, {k,v -> k[:3] !=# "ext_" })))})\r')
screen:expect([=[
@@ -911,7 +1015,15 @@ describe('TUI FocusGained/FocusLost', function()
feed_data(':terminal\n')
-- Wait for terminal to be ready.
- screen:expect{any='-- TERMINAL --'}
+ screen:expect{grid=[[
+ {1:r}eady $ |
+ [Process exited 0] |
+ |
+ |
+ |
+ :terminal |
+ {3:-- TERMINAL --} |
+ ]]}
feed_data('\027[I')
screen:expect{grid=[[
@@ -922,7 +1034,7 @@ describe('TUI FocusGained/FocusLost', function()
|
gained |
{3:-- TERMINAL --} |
- ]], timeout=(3 * screen.timeout)}
+ ]], timeout=(4 * screen.timeout)}
feed_data('\027[O')
screen:expect([[
@@ -1266,7 +1378,7 @@ describe("TUI 'term' option", function()
elseif is_macos then
local status, _ = pcall(assert_term, "xterm", "xterm")
if not status then
- pending("macOS: unibilium could not find terminfo", function() end)
+ pending("macOS: unibilium could not find terminfo")
end
else
assert_term("xterm", "xterm")
@@ -1328,125 +1440,26 @@ describe("TUI", function()
end)
-describe('TUI background color', function()
- local screen
-
- before_each(function()
- clear()
- screen = thelpers.screen_setup(0, '["'..nvim_prog
- ..'", "-u", "NONE", "-i", "NONE", "--cmd", "set noswapfile"]')
- end)
-
- it("triggers OptionSet event on terminal-response", function()
- feed_data('\027:autocmd OptionSet background echo "did OptionSet, yay!"\n')
-
- -- Wait for the child Nvim to register the OptionSet handler.
- feed_data('\027:autocmd OptionSet\n')
- screen:expect({any='--- Autocommands ---'})
-
- feed_data('\012') -- CTRL-L: clear the screen
- screen:expect([[
- {1: } |
- {4:~ }|
- {4:~ }|
- {4:~ }|
- {5:[No Name] 0,0-1 All}|
- |
- {3:-- TERMINAL --} |
- ]])
- feed_data('\027]11;rgb:ffff/ffff/ffff\007')
- screen:expect{any='did OptionSet, yay!'}
- end)
-
- it("handles deferred background color", function()
- local last_bg = 'dark'
- local function wait_for_bg(bg)
- -- Retry until the terminal response is handled.
- retry(100, nil, function()
- feed_data(':echo &background\n')
- screen:expect({
- timeout=40,
- grid=string.format([[
- {1: } |
- {4:~ }|
- {4:~ }|
- {4:~ }|
- {5:[No Name] 0,0-1 All}|
- %-5s |
- {3:-- TERMINAL --} |
- ]], bg)
- })
- end)
- last_bg = bg
- end
-
- local function assert_bg(colorspace, color, bg)
- -- Ensure the opposite of the expected bg is active.
- local other_bg = (bg == 'dark' and 'light' or 'dark')
- if last_bg ~= other_bg then
- feed_data(other_bg == 'light' and '\027]11;rgb:f/f/f\007'
- or '\027]11;rgb:0/0/0\007')
- wait_for_bg(other_bg)
- end
+it('TUI bg color triggers OptionSet event on terminal-response', function()
+ -- Only single integration test.
+ -- See test/unit/tui_spec.lua for unit tests.
+ clear()
+ local screen = thelpers.screen_setup(0, '["'..nvim_prog
+ ..'", "-u", "NONE", "-i", "NONE", "--cmd", "set noswapfile", '
+ ..'"-c", "autocmd OptionSet background echo \\"did OptionSet, yay!\\""]')
- feed_data('\027]11;'..colorspace..':'..color..'\007')
- wait_for_bg(bg)
- end
+ screen:expect([[
+ {1: } |
+ {4:~ }|
+ {4:~ }|
+ {4:~ }|
+ {5:[No Name] 0,0-1 All}|
+ |
+ {3:-- TERMINAL --} |
+ ]])
+ feed_data('\027]11;rgb:ffff/ffff/ffff\007')
+ screen:expect{any='did OptionSet, yay!'}
- assert_bg('rgb', '0000/0000/0000', 'dark')
- assert_bg('rgb', 'ffff/ffff/ffff', 'light')
- assert_bg('rgb', '000/000/000', 'dark')
- assert_bg('rgb', 'fff/fff/fff', 'light')
- assert_bg('rgb', '00/00/00', 'dark')
- assert_bg('rgb', 'ff/ff/ff', 'light')
- assert_bg('rgb', '0/0/0', 'dark')
- assert_bg('rgb', 'f/f/f', 'light')
-
- assert_bg('rgb', 'f/0/0', 'dark')
- assert_bg('rgb', '0/f/0', 'light')
- assert_bg('rgb', '0/0/f', 'dark')
-
- assert_bg('rgb', '1/1/1', 'dark')
- assert_bg('rgb', '2/2/2', 'dark')
- assert_bg('rgb', '3/3/3', 'dark')
- assert_bg('rgb', '4/4/4', 'dark')
- assert_bg('rgb', '5/5/5', 'dark')
- assert_bg('rgb', '6/6/6', 'dark')
- assert_bg('rgb', '7/7/7', 'dark')
- assert_bg('rgb', '8/8/8', 'light')
- assert_bg('rgb', '9/9/9', 'light')
- assert_bg('rgb', 'a/a/a', 'light')
- assert_bg('rgb', 'b/b/b', 'light')
- assert_bg('rgb', 'c/c/c', 'light')
- assert_bg('rgb', 'd/d/d', 'light')
- assert_bg('rgb', 'e/e/e', 'light')
-
- assert_bg('rgb', '0/e/0', 'light')
- assert_bg('rgb', '0/d/0', 'light')
- assert_bg('rgb', '0/c/0', 'dark')
- assert_bg('rgb', '0/b/0', 'dark')
-
- assert_bg('rgb', 'f/0/f', 'dark')
- assert_bg('rgb', 'f/1/f', 'dark')
- assert_bg('rgb', 'f/2/f', 'dark')
- assert_bg('rgb', 'f/3/f', 'light')
- assert_bg('rgb', 'f/4/f', 'light')
-
- assert_bg('rgba', '0000/0000/0000/0000', 'dark')
- assert_bg('rgba', '0000/0000/0000/ffff', 'dark')
- assert_bg('rgba', 'ffff/ffff/ffff/0000', 'light')
- assert_bg('rgba', 'ffff/ffff/ffff/ffff', 'light')
- assert_bg('rgba', '000/000/000/000', 'dark')
- assert_bg('rgba', '000/000/000/fff', 'dark')
- assert_bg('rgba', 'fff/fff/fff/000', 'light')
- assert_bg('rgba', 'fff/fff/fff/fff', 'light')
- assert_bg('rgba', '00/00/00/00', 'dark')
- assert_bg('rgba', '00/00/00/ff', 'dark')
- assert_bg('rgba', 'ff/ff/ff/00', 'light')
- assert_bg('rgba', 'ff/ff/ff/ff', 'light')
- assert_bg('rgba', '0/0/0/0', 'dark')
- assert_bg('rgba', '0/0/0/f', 'dark')
- assert_bg('rgba', 'f/f/f/0', 'light')
- assert_bg('rgba', 'f/f/f/f', 'light')
- end)
+ feed_data(':echo "new_bg=".&background\n')
+ screen:expect{any='new_bg=light'}
end)
diff --git a/test/functional/ui/bufhl_spec.lua b/test/functional/ui/bufhl_spec.lua
index bcccef84b6..f589bb0e83 100644
--- a/test/functional/ui/bufhl_spec.lua
+++ b/test/functional/ui/bufhl_spec.lua
@@ -31,13 +31,12 @@ describe('Buffer highlighting', function()
[14] = {background = Screen.colors.Gray90},
[15] = {background = Screen.colors.Gray90, bold = true, foreground = Screen.colors.Brown},
[16] = {foreground = Screen.colors.Magenta, background = Screen.colors.Gray90},
+ [17] = {foreground = Screen.colors.Magenta, background = Screen.colors.LightRed},
+ [18] = {background = Screen.colors.LightRed},
+ [19] = {foreground = Screen.colors.Blue1, background = Screen.colors.LightRed},
})
end)
- after_each(function()
- screen:detach()
- end)
-
local add_highlight = curbufmeths.add_highlight
local clear_namespace = curbufmeths.clear_namespace
@@ -218,6 +217,161 @@ describe('Buffer highlighting', function()
|
]])
end)
+
+ it('and adjusting columns', function()
+ -- insert before
+ feed('ggiquite <esc>')
+ screen:expect{grid=[[
+ quite^ a {5:longer} example |
+ in {6:order} to {7:de}{5:monstr}{7:ate} |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]]}
+
+ feed('u')
+ screen:expect{grid=[[
+ ^a {5:longer} example |
+ in {6:order} to {7:de}{5:monstr}{7:ate} |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ 1 change; before #2 {MATCH:.*}|
+ ]]}
+
+ -- change/insert in the middle
+ feed('+fesAAAA')
+ screen:expect{grid=[[
+ a {5:longer} example |
+ in {6:ordAAAA^r} to {7:de}{5:monstr}{7:ate} |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {7:-- INSERT --} |
+ ]]}
+
+ feed('<esc>tdD')
+ screen:expect{grid=[[
+ a {5:longer} example |
+ in {6:ordAAAAr} t^o |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]]}
+
+ feed('u')
+ screen:expect{grid=[[
+ a {5:longer} example |
+ in {6:ordAAAAr} to^ demonstrate |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ 1 change; before #4 {MATCH:.*}|
+ ]]}
+
+ feed('u')
+ screen:expect{grid=[[
+ a {5:longer} example |
+ in {6:ord^er} to demonstrate |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ 1 change; before #3 {MATCH:.*}|
+ ]]}
+ end)
+
+ it('and joining lines', function()
+ feed('ggJJJ')
+ screen:expect{grid=[[
+ a {5:longer} example in {6:order} to {7:de}{5:monstr}{7:ate}|
+ {7: combin}{8:ing hi}{7:ghlights^ }{8:from diff}{7:erent sou}|
+ {7:rces} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]]}
+
+ -- TODO(bfredl): perhaps better undo
+ feed('uuu')
+ screen:expect{grid=[[
+ ^a longer example |
+ in order to demonstrate |
+ combining highlights |
+ from different sources |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ 1 more line; before #2 {MATCH:.*}|
+ ]]}
+ end)
+
+ it('and splitting lines', function()
+ feed('2Gtti<cr>')
+ screen:expect{grid=[[
+ a {5:longer} example |
+ in {6:order} |
+ ^ to {7:de}{5:monstr}{7:ate} |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {1:~ }|
+ {7:-- INSERT --} |
+ ]]}
+
+ -- TODO(bfredl): keep both "parts" after split, requires proper extmark ranges
+ feed('<esc>tsi<cr>')
+ screen:expect{grid=[[
+ a {5:longer} example |
+ in {6:order} |
+ to {7:de}{5:mo} |
+ ^nstrate |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {7:-- INSERT --} |
+ ]]}
+
+ -- TODO(bfredl): perhaps better undo
+ feed('<esc>u')
+ screen:expect{grid=[[
+ a {5:longer} example |
+ in {6:order} |
+ to demo{7:^nstrat}{8:e} |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {1:~ }|
+ 1 line less; before #3 {MATCH:.*}|
+ ]]}
+
+ feed('<esc>u')
+ screen:expect{grid=[[
+ a {5:longer} example |
+ in order^ to demonstrate |
+ {7:combin}{8:ing}{9: hi}ghlights |
+ {9:from }{8:diff}{7:erent} sources |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ 1 line less; before #2 {MATCH:.*}|
+ ]]}
+ end)
end)
it('prioritizes latest added highlight', function()
@@ -387,6 +541,22 @@ describe('Buffer highlighting', function()
]])
end)
+ it('can be retrieved', function()
+ local get_virtual_text = curbufmeths.get_virtual_text
+ local line_count = curbufmeths.line_count
+
+ local s1 = {{'Köttbullar', 'Comment'}, {'Kräuterbutter'}}
+ local s2 = {{'ã“ã‚“ã«ã¡ã¯', 'Comment'}}
+
+ set_virtual_text(-1, 0, s1, {})
+ eq(s1, get_virtual_text(0))
+
+ set_virtual_text(-1, line_count(), s2, {})
+ eq(s2, get_virtual_text(line_count()))
+
+ eq({}, get_virtual_text(line_count() + 9000))
+ end)
+
it('is not highlighted by visual selection', function()
feed("ggVG")
screen:expect([[
@@ -516,6 +686,32 @@ describe('Buffer highlighting', function()
|
]])
end)
+
+ it('works with color column', function()
+ eq(-1, set_virtual_text(-1, 3, {{"暗x事", "Comment"}}, {}))
+ screen:expect{grid=[[
+ ^1 + 2 {3:=}{2: 3} |
+ 3 + {11:ERROR:} invalid syntax |
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5|
+ , 5, 5, 5, 5, 5, 5, Lorem ipsum dolor s|
+ x = 4 {12:暗x事} |
+ {1:~ }|
+ {1:~ }|
+ |
+ ]]}
+
+ command("set colorcolumn=9")
+ screen:expect{grid=[[
+ ^1 + 2 {3:=}{2: }{17:3} |
+ 3 + {11:ERROR:} invalid syntax |
+ 5, 5, 5,{18: }5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5|
+ , 5, 5, 5, 5, 5, 5, Lorem ipsum dolor s|
+ x = 4 {12:暗}{19:x}{12:事} |
+ {1:~ }|
+ {1:~ }|
+ |
+ ]]}
+ end)
end)
it('and virtual text use the same namespace counter', function()
diff --git a/test/functional/ui/cmdline_highlight_spec.lua b/test/functional/ui/cmdline_highlight_spec.lua
index 052414a43d..9c746b99bd 100644
--- a/test/functional/ui/cmdline_highlight_spec.lua
+++ b/test/functional/ui/cmdline_highlight_spec.lua
@@ -362,7 +362,7 @@ describe('Command-line coloring', function()
{EOB:~ }|
:e^ |
]])
- eq('', meths.command_output('messages'))
+ eq('', meths.exec('messages', true))
end)
it('silences :echon', function()
set_color_cb('Echoning')
@@ -377,7 +377,7 @@ describe('Command-line coloring', function()
{EOB:~ }|
:e^ |
]])
- eq('', meths.command_output('messages'))
+ eq('', meths.exec('messages', true))
end)
it('silences :echomsg', function()
set_color_cb('Echomsging')
@@ -392,7 +392,7 @@ describe('Command-line coloring', function()
{EOB:~ }|
:e^ |
]])
- eq('', meths.command_output('messages'))
+ eq('', meths.exec('messages', true))
end)
it('does the right thing when throwing', function()
set_color_cb('Throwing')
@@ -857,7 +857,7 @@ describe('Ex commands coloring', function()
]])
feed('<CR>')
eq('Error detected while processing :\nE605: Exception not caught: 42\nE749: empty buffer',
- meths.command_output('messages'))
+ meths.exec('messages', true))
end)
it('errors out when failing to get callback', function()
meths.set_var('Nvim_color_cmdline', 42)
diff --git a/test/functional/ui/cmdline_spec.lua b/test/functional/ui/cmdline_spec.lua
index f9769c706f..21c01b3458 100644
--- a/test/functional/ui/cmdline_spec.lua
+++ b/test/functional/ui/cmdline_spec.lua
@@ -25,10 +25,6 @@ local function test_cmdline(linegrid)
screen = new_screen({rgb=true, ext_cmdline=true, ext_linegrid=linegrid})
end)
- after_each(function()
- screen:detach()
- end)
-
it('works', function()
feed(':')
screen:expect{grid=[[
@@ -779,7 +775,7 @@ local function test_cmdline(linegrid)
}}}
-- This used to send an invalid event where pos where larger than the total
- -- lenght of content. Checked in _handle_cmdline_show.
+ -- length of content. Checked in _handle_cmdline_show.
feed('<esc>')
screen:expect([[
^ |
@@ -804,10 +800,6 @@ describe('cmdline redraw', function()
screen = new_screen({rgb=true})
end)
- after_each(function()
- screen:detach()
- end)
-
it('with timer', function()
feed(':012345678901234567890123456789')
screen:expect{grid=[[
@@ -829,8 +821,7 @@ describe('cmdline redraw', function()
it('with <Cmd>', function()
if 'openbsd' == helpers.uname() then
- pending('FIXME #10804', function() end)
- return
+ pending('FIXME #10804')
end
command('cmap a <Cmd>call sin(0)<CR>') -- no-op
feed(':012345678901234567890123456789')
diff --git a/test/functional/ui/cursor_spec.lua b/test/functional/ui/cursor_spec.lua
index 67aba919b0..8ad4182f41 100644
--- a/test/functional/ui/cursor_spec.lua
+++ b/test/functional/ui/cursor_spec.lua
@@ -13,10 +13,6 @@ describe('ui/cursor', function()
screen:attach()
end)
- after_each(function()
- screen:detach()
- end)
-
it("'guicursor' is published as a UI event", function()
local expected_mode_info = {
[1] = {
diff --git a/test/functional/ui/diff_spec.lua b/test/functional/ui/diff_spec.lua
index 8eb2bbf779..252991aca7 100644
--- a/test/functional/ui/diff_spec.lua
+++ b/test/functional/ui/diff_spec.lua
@@ -3,6 +3,8 @@ local Screen = require('test.functional.ui.screen')
local feed = helpers.feed
local clear = helpers.clear
+local command = helpers.command
+local insert = helpers.insert
local write_file = helpers.write_file
describe('Diff mode screen', function()
@@ -957,3 +959,75 @@ int main(int argc, char **argv)
end)
end)
end)
+
+it('win_update redraws lines properly', function()
+ local screen
+ clear()
+ screen = Screen.new(50, 10)
+ screen:attach()
+ screen:set_default_attr_ids({
+ [1] = {bold = true, foreground = Screen.colors.Blue1},
+ [2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ [3] = {background = Screen.colors.Red, foreground = Screen.colors.Grey100, special = Screen.colors.Yellow},
+ [4] = {bold = true, foreground = Screen.colors.SeaGreen4},
+ [5] = {special = Screen.colors.Yellow},
+ [6] = {special = Screen.colors.Yellow, bold = true, foreground = Screen.colors.SeaGreen4},
+ [7] = {foreground = Screen.colors.Grey0, background = Screen.colors.Grey100},
+ [8] = {foreground = Screen.colors.Gray90, background = Screen.colors.Grey100},
+ [9] = {foreground = tonumber('0x00000c'), background = Screen.colors.Grey100},
+ [10] = {background = Screen.colors.Grey100, bold = true, foreground = tonumber('0xe5e5ff')},
+ [11] = {background = Screen.colors.Grey100, bold = true, foreground = tonumber('0x2b8452')},
+ [12] = {bold = true, reverse = true},
+ [13] = {foreground = Screen.colors.DarkBlue, background = Screen.colors.WebGray},
+ [14] = {reverse = true},
+ [15] = {background = Screen.colors.LightBlue},
+ [16] = {background = Screen.colors.LightCyan1, bold = true, foreground = Screen.colors.Blue1},
+ [17] = {bold = true, background = Screen.colors.Red},
+ [18] = {background = Screen.colors.LightMagenta},
+ })
+
+ insert([[
+ 1
+
+
+ 2
+ 1a
+ ]])
+ command("vnew left")
+ insert([[
+ 2
+ 2a
+ 2b
+ ]])
+ command("windo diffthis")
+ command("windo 1")
+ screen:expect{grid=[[
+ {13: }{16:-----------------------}{14:│}{13: }{15:^1 }|
+ {13: }{16:-----------------------}{14:│}{13: }{15: }|
+ {13: }{16:-----------------------}{14:│}{13: }{15: }|
+ {13: }2 {14:│}{13: }2 |
+ {13: }{17:2}{18:a }{14:│}{13: }{17:1}{18:a }|
+ {13: }{15:2b }{14:│}{13: }{16:----------------------}|
+ {13: } {14:│}{13: } |
+ {1:~ }{14:│}{1:~ }|
+ {14:left [+] }{12:[No Name] [+] }|
+ |
+ ]]}
+ feed('<C-e>')
+ feed('<C-e>')
+ feed('<C-y>')
+ feed('<C-y>')
+ feed('<C-y>')
+ screen:expect{grid=[[
+ {13: }{16:-----------------------}{14:│}{13: }{15:1 }|
+ {13: }{16:-----------------------}{14:│}{13: }{15: }|
+ {13: }{16:-----------------------}{14:│}{13: }{15:^ }|
+ {13: }2 {14:│}{13: }2 |
+ {13: }{17:2}{18:a }{14:│}{13: }{17:1}{18:a }|
+ {13: }{15:2b }{14:│}{13: }{16:----------------------}|
+ {13: } {14:│}{13: } |
+ {1:~ }{14:│}{1:~ }|
+ {14:left [+] }{12:[No Name] [+] }|
+ |
+ ]]}
+end)
diff --git a/test/functional/ui/embed_spec.lua b/test/functional/ui/embed_spec.lua
index f3cd223f53..8218c8e12d 100644
--- a/test/functional/ui/embed_spec.lua
+++ b/test/functional/ui/embed_spec.lua
@@ -50,8 +50,7 @@ local function test_embed(ext_linegrid)
it("doesn't erase output when setting color scheme", function()
if 'openbsd' == helpers.uname() then
- pending('FIXME #10804', function() end)
- return
+ pending('FIXME #10804')
end
startup('--cmd', 'echoerr "foo"', '--cmd', 'color default', '--cmd', 'echoerr "bar"')
screen:expect([[
diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua
index dbaf6f802b..7a5569c14b 100644
--- a/test/functional/ui/float_spec.lua
+++ b/test/functional/ui/float_spec.lua
@@ -2,9 +2,11 @@ local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local os = require('os')
local clear, feed = helpers.clear, helpers.feed
+local assert_alive = helpers.assert_alive
local command, feed_command = helpers.command, helpers.feed_command
local eval = helpers.eval
local eq = helpers.eq
+local exec_lua = helpers.exec_lua
local insert = helpers.insert
local meths = helpers.meths
local curbufmeths = helpers.curbufmeths
@@ -12,7 +14,7 @@ local funcs = helpers.funcs
local run = helpers.run
local pcall_err = helpers.pcall_err
-describe('floating windows', function()
+describe('floatwin', function()
before_each(function()
clear()
end)
@@ -39,6 +41,7 @@ describe('floating windows', function()
[19] = {foreground = Screen.colors.DarkBlue, background = Screen.colors.WebGray},
[20] = {bold = true, foreground = Screen.colors.Brown},
[21] = {background = Screen.colors.Gray90},
+ [22] = {background = Screen.colors.LightRed},
}
it('behavior', function()
@@ -55,6 +58,31 @@ describe('floating windows', function()
eq(1000, funcs.win_getid())
end)
+ it('closed immediately by autocmd #11383', function()
+ eq('Error executing lua: [string "<nvim>"]:4: Window was closed immediately',
+ pcall_err(exec_lua, [[
+ local a = vim.api
+ local function crashes(contents)
+ local buf = a.nvim_create_buf(false, true)
+ local floatwin = a.nvim_open_win(buf, true, {
+ relative = 'cursor';
+ style = 'minimal';
+ row = 0; col = 0;
+ height = #contents;
+ width = 10;
+ })
+ a.nvim_buf_set_lines(buf, 0, -1, true, contents)
+ local winnr = vim.fn.win_id2win(floatwin)
+ a.nvim_command('wincmd p')
+ a.nvim_command('autocmd CursorMoved * ++once '..winnr..'wincmd c')
+ return buf, floatwin
+ end
+ crashes{'foo'}
+ crashes{'bar'}
+ ]]))
+ assert_alive()
+ end)
+
local function with_ext_multigrid(multigrid)
local screen
before_each(function()
@@ -398,6 +426,7 @@ describe('floating windows', function()
it("can use 'minimal' style", function()
command('set number')
command('set signcolumn=yes')
+ command('set colorcolumn=1')
command('set cursorline')
command('set foldcolumn=1')
command('hi NormalFloat guibg=#333333')
@@ -414,9 +443,9 @@ describe('floating windows', function()
[2:----------------------------------------]|
[3:----------------------------------------]|
## grid 2
- {19: }{20: 1 }{21:^x }|
- {19: }{14: 2 }y |
- {19: }{14: 3 } |
+ {19: }{20: 1 }{22:^x}{21: }|
+ {19: }{14: 2 }{22:y} |
+ {19: }{14: 3 }{22: } |
{0:~ }|
{0:~ }|
{0:~ }|
@@ -430,9 +459,9 @@ describe('floating windows', function()
]], float_pos={[4] = {{id = 1001}, "NW", 1, 4, 10, true}}}
else
screen:expect{grid=[[
- {19: }{20: 1 }{21:^x }|
- {19: }{14: 2 }y |
- {19: }{14: 3 } {15:x } |
+ {19: }{20: 1 }{22:^x}{21: }|
+ {19: }{14: 2 }{22:y} |
+ {19: }{14: 3 }{22: } {15:x } |
{0:~ }{15:y }{0: }|
{0:~ }{15: }{0: }|
{0:~ }{15: }{0: }|
@@ -454,9 +483,9 @@ describe('floating windows', function()
[2:----------------------------------------]|
[3:----------------------------------------]|
## grid 2
- {19: }{17:ðŒ¢Ì€Ì̂̃̅̄ðŒ¢Ì€Ì̂̃̅̄}{20: 1 }{21:^x }|
- {19: }{14: 2 }y |
- {19: }{14: 3 } |
+ {19: }{17:ðŒ¢Ì€Ì̂̃̅̄ðŒ¢Ì€Ì̂̃̅̄}{20: 1 }{22:^x}{21: }|
+ {19: }{14: 2 }{22:y} |
+ {19: }{14: 3 }{22: } |
{0:~ }|
{0:~ }|
{0:~ }|
@@ -471,9 +500,9 @@ describe('floating windows', function()
else
screen:expect([[
- {19: }{17:ðŒ¢Ì€Ì̂̃̅̄ðŒ¢Ì€Ì̂̃̅̄}{20: 1 }{21:^x }|
- {19: }{14: 2 }y |
- {19: }{14: 3 } {17:ðŒ¢Ì€Ì̂̃̅̄ðŒ¢Ì€Ì̂̃̅̄}{15:x } |
+ {19: }{17:ðŒ¢Ì€Ì̂̃̅̄ðŒ¢Ì€Ì̂̃̅̄}{20: 1 }{22:^x}{21: }|
+ {19: }{14: 2 }{22:y} |
+ {19: }{14: 3 }{22: } {17:ðŒ¢Ì€Ì̂̃̅̄ðŒ¢Ì€Ì̂̃̅̄}{15:x } |
{0:~ }{19: }{15:y }{0: }|
{0:~ }{19: }{15: }{0: }|
{0:~ }{15: }{0: }|
@@ -495,9 +524,9 @@ describe('floating windows', function()
[2:----------------------------------------]|
[3:----------------------------------------]|
## grid 2
- {19: }{20: 1 }{21:^x }|
- {19: }{14: 2 }y |
- {19: }{14: 3 } |
+ {19: }{20: 1 }{22:^x}{21: }|
+ {19: }{14: 2 }{22:y} |
+ {19: }{14: 3 }{22: } |
{0:~ }|
{0:~ }|
{0:~ }|
@@ -511,9 +540,9 @@ describe('floating windows', function()
]], float_pos={[4] = {{id = 1001}, "NW", 1, 4, 10, true}}}
else
screen:expect([[
- {19: }{20: 1 }{21:^x }|
- {19: }{14: 2 }y |
- {19: }{14: 3 } {15: } |
+ {19: }{20: 1 }{22:^x}{21: }|
+ {19: }{14: 2 }{22:y} |
+ {19: }{14: 3 }{22: } {15: } |
{0:~ }{15: }{0: }|
{0:~ }{15: }{0: }|
{0:~ }{15: }{0: }|
diff --git a/test/functional/ui/fold_spec.lua b/test/functional/ui/fold_spec.lua
index c5ef718883..eb81aba131 100644
--- a/test/functional/ui/fold_spec.lua
+++ b/test/functional/ui/fold_spec.lua
@@ -24,10 +24,6 @@ describe("folded lines", function()
})
end)
- after_each(function()
- screen:detach()
- end)
-
it("work with more than one signcolumn", function()
command("set signcolumn=yes:9")
feed("i<cr><esc>")
diff --git a/test/functional/ui/highlight_spec.lua b/test/functional/ui/highlight_spec.lua
index f40f658275..d7791a3107 100644
--- a/test/functional/ui/highlight_spec.lua
+++ b/test/functional/ui/highlight_spec.lua
@@ -35,7 +35,6 @@ describe('highlight: `:syntax manual`', function()
end)
after_each(function()
- screen:detach()
os.remove('Xtest-functional-ui-highlight.tmp.vim')
end)
@@ -97,10 +96,6 @@ describe('highlight defaults', function()
command("set display-=msgsep")
end)
- after_each(function()
- screen:detach()
- end)
-
it('window status bar', function()
screen:set_default_attr_ids({
[0] = {bold=true, foreground=Screen.colors.Blue},
@@ -346,17 +341,10 @@ describe('highlight defaults', function()
end)
describe('highlight', function()
- local screen
-
- before_each(function()
- clear()
- screen = Screen.new(25,10)
- screen:attach()
- end)
+ before_each(clear)
it('visual', function()
- screen:detach()
- screen = Screen.new(20,4)
+ local screen = Screen.new(20,4)
screen:attach()
screen:set_default_attr_ids({
[1] = {background = Screen.colors.LightGrey},
@@ -389,8 +377,7 @@ describe('highlight', function()
end)
it('cterm=standout gui=standout', function()
- screen:detach()
- screen = Screen.new(20,5)
+ local screen = Screen.new(20,5)
screen:attach()
screen:set_default_attr_ids({
[1] = {bold = true, foreground = Screen.colors.Blue1},
@@ -413,8 +400,7 @@ describe('highlight', function()
end)
it('strikethrough', function()
- screen:detach()
- screen = Screen.new(25,6)
+ local screen = Screen.new(25,6)
screen:attach()
feed_command('syntax on')
feed_command('syn keyword TmpKeyword foo')
@@ -438,7 +424,56 @@ describe('highlight', function()
})
end)
+ it('nocombine', function()
+ local screen = Screen.new(25,6)
+ screen:set_default_attr_ids{
+ [1] = {foreground = Screen.colors.SlateBlue, underline = true},
+ [2] = {bold = true, foreground = Screen.colors.Blue1},
+ [3] = {underline = true, reverse = true, foreground = Screen.colors.SlateBlue},
+ [4] = {background = Screen.colors.Yellow, reverse = true, foreground = Screen.colors.SlateBlue},
+ [5] = {foreground = Screen.colors.Red},
+ }
+ screen:attach()
+ feed_command('syntax on')
+ feed_command('hi! Underlined cterm=underline gui=underline')
+ feed_command('syn keyword Underlined foobar')
+ feed_command('hi Search cterm=inverse,nocombine gui=inverse,nocombine')
+ insert([[
+ foobar
+ foobar
+ ]])
+ screen:expect{grid=[[
+ {1:foobar} |
+ {1:foobar} |
+ ^ |
+ {2:~ }|
+ {2:~ }|
+ |
+ ]]}
+
+ feed('/foo')
+ screen:expect{grid=[[
+ {3:foo}{1:bar} |
+ {4:foo}{1:bar} |
+ |
+ {2:~ }|
+ {2:~ }|
+ /foo^ |
+ ]]}
+ feed('<cr>')
+ screen:expect{grid=[[
+ {4:^foo}{1:bar} |
+ {4:foo}{1:bar} |
+ |
+ {2:~ }|
+ {2:~ }|
+ {5:search hit...uing at TOP} |
+ ]]}
+ end)
+
it('guisp (special/undercurl)', function()
+ local screen = Screen.new(25,10)
+ screen:attach()
feed_command('syntax on')
feed_command('syn keyword TmpKeyword neovim')
feed_command('syn keyword TmpKeyword1 special')
@@ -494,10 +529,6 @@ describe("'listchars' highlight", function()
screen:attach()
end)
- after_each(function()
- screen:detach()
- end)
-
it("'cursorline' and 'cursorcolumn'", function()
screen:set_default_attr_ids({
[0] = {bold=true, foreground=Screen.colors.Blue},
@@ -657,6 +688,30 @@ describe("'listchars' highlight", function()
]])
end)
+ it("'listchar' with wrap", function()
+ screen:set_default_attr_ids({
+ [0] = {bold=true, foreground=Screen.colors.Blue},
+ })
+ feed_command('set wrap')
+ feed_command('set listchars=eol:¬,precedes:< list')
+ feed('90ia<esc>')
+ screen:expect([[
+ {0:<}aaaaaaaaaaaaaaaaaaa|
+ aaaaaaaaaaaaaaaaaaaa|
+ aaaaaaaaaaaaaaaaaaaa|
+ aaaaaaaaa^a{0:¬} |
+ |
+ ]])
+ feed('0')
+ screen:expect([[
+ ^aaaaaaaaaaaaaaaaaaaa|
+ aaaaaaaaaaaaaaaaaaaa|
+ aaaaaaaaaaaaaaaaaaaa|
+ aaaaaaaaaaaaaaaaaaaa|
+ |
+ ]])
+ end)
+
it("'listchar' in visual mode", function()
screen:set_default_attr_ids({
[1] = {background=Screen.colors.Grey90},
diff --git a/test/functional/ui/hlstate_spec.lua b/test/functional/ui/hlstate_spec.lua
index d1c115587e..2a567b28ee 100644
--- a/test/functional/ui/hlstate_spec.lua
+++ b/test/functional/ui/hlstate_spec.lua
@@ -181,11 +181,11 @@ describe('ext_hlstate detailed highlights', function()
it("work with :terminal", function()
screen:set_default_attr_ids({
[1] = {{}, {{hi_name = "TermCursorNC", ui_name = "TermCursorNC", kind = "ui"}}},
- [2] = {{foreground = 52479}, {{kind = "term"}}},
- [3] = {{bold = true, foreground = 52479}, {{kind = "term"}}},
- [4] = {{foreground = 52479}, {2, 1}},
- [5] = {{foreground = 4259839}, {{kind = "term"}}},
- [6] = {{foreground = 4259839}, {5, 1}},
+ [2] = {{foreground = tonumber('0x00ccff'), fg_indexed=true}, {{kind = "term"}}},
+ [3] = {{bold = true, foreground = tonumber('0x00ccff'), fg_indexed=true}, {{kind = "term"}}},
+ [4] = {{foreground = tonumber('0x00ccff'), fg_indexed=true}, {2, 1}},
+ [5] = {{foreground = tonumber('0x40ffff'), fg_indexed=true}, {{kind = "term"}}},
+ [6] = {{foreground = tonumber('0x40ffff'), fg_indexed=true}, {5, 1}},
[7] = {{}, {{hi_name = "MsgArea", ui_name = "MsgArea", kind = "ui"}}},
})
command('enew | call termopen(["'..nvim_dir..'/tty-test"])')
@@ -259,7 +259,7 @@ describe('ext_hlstate detailed highlights', function()
it("can use independent cterm and rgb colors", function()
-- tell test module to save all attributes (doesn't change nvim options)
- screen:set_hlstate_cterm(true)
+ screen:set_rgb_cterm(true)
screen:set_default_attr_ids({
[1] = {{bold = true, foreground = Screen.colors.Blue1}, {foreground = 12}, {{hi_name = "NonText", ui_name = "EndOfBuffer", kind = "ui"}}},
diff --git a/test/functional/ui/inccommand_spec.lua b/test/functional/ui/inccommand_spec.lua
index 351c4b4bcf..b841574643 100644
--- a/test/functional/ui/inccommand_spec.lua
+++ b/test/functional/ui/inccommand_spec.lua
@@ -88,14 +88,14 @@ local function common_setup(screen, inccommand, text)
})
end
- command("set inccommand=" .. (inccommand and inccommand or ""))
+ command("set inccommand=" .. (inccommand or ""))
if text then
insert(text)
end
end
-describe(":substitute, inccommand=split", function()
+describe(":substitute, inccommand=split interactivity", function()
before_each(function()
clear()
common_setup(nil, "split", default_text)
@@ -556,7 +556,6 @@ describe(":substitute, 'inccommand' preserves undo", function()
]])
end
end
- screen:detach()
end)
it('with undolevels=2', function()
@@ -647,7 +646,6 @@ describe(":substitute, 'inccommand' preserves undo", function()
Already ...t change |
]])
end
- screen:detach()
end
end)
@@ -713,7 +711,6 @@ describe(":substitute, 'inccommand' preserves undo", function()
Already ...t change |
]])
end
- screen:detach()
end)
end)
@@ -726,19 +723,15 @@ describe(":substitute, inccommand=split", function()
common_setup(screen, "split", default_text .. default_text)
end)
- after_each(function()
- screen:detach()
- end)
-
it("preserves 'modified' buffer flag", function()
feed_command("set nomodified")
feed(":%s/tw")
screen:expect([[
Inc substitution on |
{12:tw}o lines |
+ Inc substitution on |
+ {12:tw}o lines |
|
- {15:~ }|
- {15:~ }|
{11:[No Name] }|
|2| {12:tw}o lines |
|4| {12:tw}o lines |
@@ -786,6 +779,59 @@ describe(":substitute, inccommand=split", function()
{15:~ }|
:silent tabedit %s/tw/to^ |
]])
+ feed('<Esc>')
+
+ -- leading colons
+ feed(':::%s/tw/to')
+ screen:expect{any=[[{12:to}o lines]]}
+ feed('<Esc>')
+ screen:expect{any=[[two lines]]}
+ end)
+
+ it("ignores new-window modifiers when splitting the preview window", function()
+ -- one modifier
+ feed(':topleft %s/tw/to')
+ screen:expect([[
+ Inc substitution on |
+ {12:to}o lines |
+ Inc substitution on |
+ {12:to}o lines |
+ |
+ {11:[No Name] [+] }|
+ |2| {12:to}o lines |
+ |4| {12:to}o lines |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {10:[Preview] }|
+ :topleft %s/tw/to^ |
+ ]])
+ feed('<Esc>')
+ screen:expect{any=[[two lines]]}
+
+ -- multiple modifiers
+ feed(':topleft vert %s/tw/to')
+ screen:expect([[
+ Inc substitution on |
+ {12:to}o lines |
+ Inc substitution on |
+ {12:to}o lines |
+ |
+ {11:[No Name] [+] }|
+ |2| {12:to}o lines |
+ |4| {12:to}o lines |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {10:[Preview] }|
+ :topleft vert %s/tw/to^ |
+ ]])
+ feed('<Esc>')
+ screen:expect{any=[[two lines]]}
end)
it('shows split window when typing the pattern', function()
@@ -793,9 +839,9 @@ describe(":substitute, inccommand=split", function()
screen:expect([[
Inc substitution on |
{12:tw}o lines |
+ Inc substitution on |
+ {12:tw}o lines |
|
- {15:~ }|
- {15:~ }|
{11:[No Name] [+] }|
|2| {12:tw}o lines |
|4| {12:tw}o lines |
@@ -814,9 +860,9 @@ describe(":substitute, inccommand=split", function()
screen:expect([[
Inc substitution on |
o lines |
+ Inc substitution on |
+ o lines |
|
- {15:~ }|
- {15:~ }|
{11:[No Name] [+] }|
|2| o lines |
|4| o lines |
@@ -833,9 +879,9 @@ describe(":substitute, inccommand=split", function()
screen:expect([[
Inc substitution on |
{12:x}o lines |
+ Inc substitution on |
+ {12:x}o lines |
|
- {15:~ }|
- {15:~ }|
{11:[No Name] [+] }|
|2| {12:x}o lines |
|4| {12:x}o lines |
@@ -852,9 +898,9 @@ describe(":substitute, inccommand=split", function()
screen:expect([[
Inc substitution on |
o lines |
+ Inc substitution on |
+ o lines |
|
- {15:~ }|
- {15:~ }|
{11:[No Name] [+] }|
|2| o lines |
|4| o lines |
@@ -874,9 +920,9 @@ describe(":substitute, inccommand=split", function()
screen:expect([[
Inc substitution on |
{12:XX}o lines |
+ Inc substitution on |
+ {12:XX}o lines |
|
- {15:~ }|
- {15:~ }|
{11:[No Name] [+] }|
|2| {12:XX}o lines |
|4| {12:XX}o lines |
@@ -938,11 +984,11 @@ describe(":substitute, inccommand=split", function()
feed(":%s/tw")
-- 'cursorline' is NOT active during preview.
screen:expect([[
+ Inc substitution on |
{12:tw}o lines |
Inc substitution on |
{12:tw}o lines |
|
- {15:~ }|
{11:[No Name] [+] }|
|2| {12:tw}o lines |
|4| {12:tw}o lines |
@@ -1241,10 +1287,6 @@ describe("inccommand=nosplit", function()
common_setup(screen, "nosplit", default_text .. default_text)
end)
- after_each(function()
- if screen then screen:detach() end
- end)
-
it("works with :smagic, :snomagic", function()
feed_command("set hlsearch")
insert("Line *.3.* here")
@@ -1719,10 +1761,6 @@ describe("'inccommand' split windows", function()
common_setup(screen, "split", default_text)
end
- after_each(function()
- screen:detach()
- end)
-
it('work after more splits', function()
refresh()
@@ -2205,11 +2243,11 @@ describe(":substitute", function()
feed("/KKK")
screen:expect([[
+ T T123 T T123 T2T TT T23423424|
+ x |
afa {12:KKK}adf la;lkd {12:KKK}alx |
|
{15:~ }|
- {15:~ }|
- {15:~ }|
{11:[No Name] [+] }|
|3| afa {12:KKK}adf la;lkd {12:KKK}alx |
{15:~ }|
@@ -2485,11 +2523,11 @@ describe(":substitute", function()
wait()
feed([[:%s/\(some\)\@<lt>!thing/one/]])
screen:expect([[
+ something |
every{12:one} |
someone |
{15:~ }|
{15:~ }|
- {15:~ }|
{11:[No Name] [+] }|
|2| every{12:one} |
{15:~ }|
@@ -2527,11 +2565,11 @@ describe(":substitute", function()
wait()
feed([[:%s/some\(thing\)\@!/every/]])
screen:expect([[
+ something |
+ everything |
{12:every}one |
{15:~ }|
{15:~ }|
- {15:~ }|
- {15:~ }|
{11:[No Name] [+] }|
|3| {12:every}one |
{15:~ }|
@@ -2544,6 +2582,49 @@ describe(":substitute", function()
:%s/some\(thing\)\@!/every/^ |
]])
end)
+
+ it("doesn't prompt to swap cmd range", function()
+ screen = Screen.new(50, 8) -- wide to avoid hit-enter prompt
+ common_setup(screen, "split", default_text)
+ feed(':2,1s/tw/MO/g')
+
+ -- substitution preview should have been made, without prompting
+ screen:expect([[
+ {12:MO}o lines |
+ {11:[No Name] [+] }|
+ |2| {12:MO}o lines |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {10:[Preview] }|
+ :2,1s/tw/MO/g^ |
+ ]])
+
+ -- but should be prompted on hitting enter
+ feed('<CR>')
+ screen:expect([[
+ {12:MO}o lines |
+ {11:[No Name] [+] }|
+ |2| {12:MO}o lines |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {10:[Preview] }|
+ {13:Backwards range given, OK to swap (y/n)?}^ |
+ ]])
+
+ feed('y')
+ screen:expect([[
+ Inc substitution on |
+ ^MOo lines |
+ |
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {15:~ }|
+ {13:Backwards range given, OK to swap (y/n)?}y |
+ ]])
+ end)
end)
it(':substitute with inccommand during :terminal activity', function()
diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua
index 875e4092a6..25b38b1feb 100644
--- a/test/functional/ui/messages_spec.lua
+++ b/test/functional/ui/messages_spec.lua
@@ -122,7 +122,7 @@ describe('ui/ext_messages', function()
feed('G$x')
screen:expect{grid=[[
line 1 |
- {IGNORE}|
+ {MATCH:.*}|
{1:~ }|
{1:~ }|
{1:~ }|
@@ -747,7 +747,7 @@ describe('ui/ext_messages', function()
{1:~ }|
{1:~ }|
]], messages={{
- content = {{'E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: such\nmultiline\nerror', 2}},
+ content = {{'E5108: Error executing lua [string ":lua"]:1: such\nmultiline\nerror', 2}},
kind = "lua_error"
}}}
end)
@@ -810,6 +810,7 @@ describe('ui/builtin messages', function()
[4] = {bold = true, foreground = Screen.colors.SeaGreen4},
[5] = {foreground = Screen.colors.Blue1},
[6] = {bold = true, foreground = Screen.colors.Magenta},
+ [7] = {background = Screen.colors.Grey20},
})
end)
@@ -860,7 +861,7 @@ describe('ui/builtin messages', function()
-- screen size doesn't affect internal output #10285
eq('ErrorMsg xxx ctermfg=15 ctermbg=1 guifg=White guibg=Red',
- meths.command_output("hi ErrorMsg"))
+ meths.exec("hi ErrorMsg", true))
end)
it(':syntax list langGroup output', function()
@@ -899,9 +900,44 @@ vimComment xxx match /\s"[^\-:.%#=*].*$/ms=s+1,lc=1 excludenl contains=@vim
match /\<endif\s\+".*$/ms=s+5,lc=5 contains=@vimCommentGroup,vimCommentString
match /\<else\s\+".*$/ms=s+4,lc=4 contains=@vimCommentGroup,vimCommentString
links to Comment]],
- meths.command_output('syntax list vimComment'))
+ meths.exec('syntax list vimComment', true))
-- luacheck: pop
end)
+
+ it('supports ruler with laststatus=0', function()
+ command("set ruler laststatus=0")
+ screen:expect{grid=[[
+ ^ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ 0,0-1 All |
+ ]]}
+
+ command("hi MsgArea guibg=#333333")
+ screen:expect{grid=[[
+ ^ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {7: 0,0-1 All }|
+ ]]}
+
+ command("set rulerformat=%15(%c%V\\ %p%%%)")
+ screen:expect{grid=[[
+ ^ |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ {7: 0,0-1 100% }|
+ ]]}
+ end)
end)
describe('ui/ext_messages', function()
@@ -930,7 +966,7 @@ describe('ui/ext_messages', function()
{1:~ }|
{1:~ }|
{1:~ }|
- {IGNORE}|
+ {MATCH:.*}|
{1:~ }|
{1:~ }Nvim is open source and freely distributable{1: }|
{1:~ }https://neovim.io/#chat{1: }|
@@ -940,8 +976,8 @@ describe('ui/ext_messages', function()
{1:~ }type :q{5:<Enter>} to exit {1: }|
{1:~ }type :help{5:<Enter>} for help {1: }|
{1:~ }|
- {IGNORE}|
- {IGNORE}|
+ {MATCH:.*}|
+ {MATCH:.*}|
{1:~ }|
{1:~ }|
{1:~ }|
@@ -986,7 +1022,7 @@ describe('ui/ext_messages', function()
|
|
|
- {IGNORE}|
+ {MATCH:.*}|
|
Nvim is open source and freely distributable |
https://neovim.io/#chat |
@@ -996,8 +1032,8 @@ describe('ui/ext_messages', function()
type :q{5:<Enter>} to exit |
type :help{5:<Enter>} for help |
|
- {IGNORE}|
- {IGNORE}|
+ {MATCH:.*}|
+ {MATCH:.*}|
|
|
|
@@ -1089,7 +1125,7 @@ aliquip ex ea commodo consequat.]])
it('can be quit', function()
screen:try_resize(25,5)
- feed(':echon join(map(range(0, &lines*2), "v:val"), "\\n")<cr>')
+ feed(':echon join(map(range(0, &lines*10), "v:val"), "\\n")<cr>')
screen:expect{grid=[[
0 |
1 |
@@ -1110,97 +1146,96 @@ aliquip ex ea commodo consequat.]])
it('handles wrapped lines with line scroll', function()
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
feed('j')
screen:expect{grid=[[
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
{2:a aliqua.} |
+ {2:Ut enim ad minim veniam, quis nostr}|
{4:-- More --}^ |
]]}
feed('k')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
feed('j')
screen:expect{grid=[[
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
{2:a aliqua.} |
+ {2:Ut enim ad minim veniam, quis nostr}|
{4:-- More --}^ |
]]}
-
end)
it('handles wrapped lines with page scroll', function()
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
feed('d')
screen:expect{grid=[[
- {2:adipisicing elit, sed do eiusmod te}|
- {2:mpor} |
{2:incididunt ut labore et dolore magn}|
{2:a aliqua.} |
{2:Ut enim ad minim veniam, quis nostr}|
{2:ud xercitation} |
{2:ullamco laboris nisi ut} |
- {4:-- More --}^ |
+ {2:aliquip ex ea commodo consequat.} |
+ {4:Press ENTER or type command to cont}|
+ {4:inue}^ |
]]}
feed('u')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
feed('d')
screen:expect{grid=[[
- {2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
{2:a aliqua.} |
{2:Ut enim ad minim veniam, quis nostr}|
{2:ud xercitation} |
{2:ullamco laboris nisi ut} |
+ {2:aliquip ex ea commodo consequat.} |
{4:-- More --}^ |
]]}
end)
@@ -1210,49 +1245,49 @@ aliquip ex ea commodo consequat.]])
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {3:E5105: Error while calling lua chun}|
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:E5108: Error executing lua [string }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
+ {3:a aliqua.}{5: }|
{6:-- More --}{5:^ }|
]]}
feed('j')
screen:expect{grid=[[
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
{3:a aliqua.}{5: }|
+ {3:Ut enim ad minim veniam, quis nostr}|
{6:-- More --}{5:^ }|
]]}
feed('k')
screen:expect{grid=[[
- {3:E5105: Error while calling lua chun}|
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:E5108: Error executing lua [string }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
+ {3:a aliqua.}{5: }|
{6:-- More --}{5:^ }|
]]}
feed('j')
screen:expect{grid=[[
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
{3:a aliqua.}{5: }|
+ {3:Ut enim ad minim veniam, quis nostr}|
{6:-- More --}{5:^ }|
]]}
end)
@@ -1261,46 +1296,46 @@ aliquip ex ea commodo consequat.]])
command("hi MsgArea guisp=Yellow")
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {3:E5105: Error while calling lua chun}|
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:E5108: Error executing lua [string }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
+ {3:a aliqua.}{5: }|
{6:-- More --}{5:^ }|
]]}
feed('d')
screen:expect{grid=[[
- {3:adipisicing elit, sed do eiusmod te}|
- {3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
{3:a aliqua.}{5: }|
{3:Ut enim ad minim veniam, quis nostr}|
{3:ud xercitation}{5: }|
{3:ullamco laboris nisi ut}{5: }|
- {6:-- More --}{5:^ }|
+ {3:aliquip ex ea commodo consequat.}{5: }|
+ {6:Press ENTER or type command to cont}|
+ {6:inue}{5:^ }|
]]}
feed('u')
screen:expect{grid=[[
- {3:E5105: Error while calling lua chun}|
- {3:k: [string "<VimL compiled string>"}|
- {3:]:1: Lorem ipsum dolor sit amet, co}|
- {3:nsectetur}{5: }|
+ {3:E5108: Error executing lua [string }|
+ {3:":lua"]:1: Lorem ipsum dolor sit am}|
+ {3:et, consectetur}{5: }|
{3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
+ {3:a aliqua.}{5: }|
{6:-- More --}{5:^ }|
]]}
feed('d')
screen:expect{grid=[[
- {3:adipisicing elit, sed do eiusmod te}|
{3:mpor}{5: }|
{3:incididunt ut labore et dolore magn}|
{3:a aliqua.}{5: }|
{3:Ut enim ad minim veniam, quis nostr}|
{3:ud xercitation}{5: }|
{3:ullamco laboris nisi ut}{5: }|
+ {3:aliquip ex ea commodo consequat.}{5: }|
{6:-- More --}{5:^ }|
]]}
end)
@@ -1437,23 +1472,23 @@ aliquip ex ea commodo consequat.]])
it('can be resized', function()
feed(':lua error(_G.x)<cr>')
screen:expect{grid=[[
- {2:E5105: Error while calling lua chun}|
- {2:k: [string "<VimL compiled string>"}|
- {2:]:1: Lorem ipsum dolor sit amet, co}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [string }|
+ {2:":lua"]:1: Lorem ipsum dolor sit am}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusmod te}|
{2:mpor} |
{2:incididunt ut labore et dolore magn}|
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
-- responds to resize, but text is not reflown
screen:try_resize(45, 5)
screen:expect{grid=[[
- {2:nsectetur} |
{2:adipisicing elit, sed do eiusmod te} |
{2:mpor} |
{2:incididunt ut labore et dolore magn} |
+ {2:a aliqua.} |
{4:-- More --}^ |
]]}
@@ -1461,14 +1496,14 @@ aliquip ex ea commodo consequat.]])
-- text is not reflown; existing lines get cut
screen:try_resize(30, 12)
screen:expect{grid=[[
- {2:E5105: Error while calling lua}|
- {2:k: [string "<VimL compiled str}|
- {2:]:1: Lorem ipsum dolor sit ame}|
- {2:nsectetur} |
+ {2:E5108: Error executing lua [st}|
+ {2:":lua"]:1: Lorem ipsum dolor s}|
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusm}|
{2:mpore} |
{2:incididunt ut labore et dolore}|
- {2: magn} |
+ {2:a aliqua.} |
+ |
|
|
|
@@ -1479,18 +1514,18 @@ aliquip ex ea commodo consequat.]])
-- wrapped at the new screen size.
feed('<cr>')
screen:expect{grid=[[
- {2:k: [string "<VimL compiled str}|
- {2:]:1: Lorem ipsum dolor sit ame}|
- {2:nsectetur} |
+ {2:et, consectetur} |
{2:adipisicing elit, sed do eiusm}|
{2:mpore} |
{2:incididunt ut labore et dolore}|
- {2: magna aliqua.} |
+ {2:a aliqua.} |
{2:Ut enim ad minim veniam, quis }|
{2:nostrud xercitation} |
{2:ullamco laboris nisi ut} |
{2:aliquip ex ea commodo consequa}|
- {4:-- More --}^ |
+ {2:t.} |
+ {4:Press ENTER or type command to}|
+ {4: continue}^ |
]]}
feed('q')
diff --git a/test/functional/ui/mode_spec.lua b/test/functional/ui/mode_spec.lua
index 200f6eecdb..9390f268b3 100644
--- a/test/functional/ui/mode_spec.lua
+++ b/test/functional/ui/mode_spec.lua
@@ -3,6 +3,7 @@ local Screen = require('test.functional.ui.screen')
local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert
local command = helpers.command
+local retry = helpers.retry
describe('ui mode_change event', function()
local screen
@@ -61,30 +62,36 @@ describe('ui mode_change event', function()
|
]], mode="normal"}
+ local matchtime = 0
command("set showmatch")
- command("set matchtime=2") -- tenths of seconds
- feed('a(stuff')
- screen:expect{grid=[[
- word(stuff^ |
- {0:~ }|
- {0:~ }|
- {2:-- INSERT --} |
- ]], mode="insert"}
-
- feed(')')
- screen:expect{grid=[[
- word^(stuff) |
- {0:~ }|
- {0:~ }|
- {2:-- INSERT --} |
- ]], mode="showmatch"}
-
- screen:expect{grid=[[
- word(stuff)^ |
- {0:~ }|
- {0:~ }|
- {2:-- INSERT --} |
- ]], mode="insert"}
+ retry(nil, nil, function()
+ matchtime = matchtime + 1
+ local screen_timeout = 1000 * matchtime -- fail faster for retry.
+
+ command("set matchtime=" .. matchtime) -- tenths of seconds
+ feed('a(stuff')
+ screen:expect{grid=[[
+ word(stuff^ |
+ {0:~ }|
+ {0:~ }|
+ {2:-- INSERT --} |
+ ]], mode="insert", timeout=screen_timeout}
+
+ feed(')')
+ screen:expect{grid=[[
+ word^(stuff) |
+ {0:~ }|
+ {0:~ }|
+ {2:-- INSERT --} |
+ ]], mode="showmatch", timeout=screen_timeout}
+
+ screen:expect{grid=[[
+ word(stuff)^ |
+ {0:~ }|
+ {0:~ }|
+ {2:-- INSERT --} |
+ ]], mode="insert", timeout=screen_timeout}
+ end)
end)
it('works in replace mode', function()
diff --git a/test/functional/ui/mouse_spec.lua b/test/functional/ui/mouse_spec.lua
index 3bd6b81ff1..d857b57a31 100644
--- a/test/functional/ui/mouse_spec.lua
+++ b/test/functional/ui/mouse_spec.lua
@@ -12,7 +12,10 @@ describe('ui/mouse/input', function()
clear()
meths.set_option('mouse', 'a')
meths.set_option('list', true)
- meths.set_option('listchars', 'eol:$')
+ -- NB: this is weird, but mostly irrelevant to the test
+ -- So I didn't bother to change it
+ command('set listchars=eol:$')
+ command('setl listchars=nbsp:x')
screen = Screen.new(25, 5)
screen:attach()
screen:set_default_attr_ids({
@@ -26,6 +29,8 @@ describe('ui/mouse/input', function()
},
[4] = {reverse = true},
[5] = {bold = true, reverse = true},
+ [6] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ [7] = {bold = true, foreground = Screen.colors.SeaGreen4},
})
command("set display-=msgsep")
feed('itesting<cr>mouse<cr>support and selection<esc>')
@@ -38,10 +43,6 @@ describe('ui/mouse/input', function()
]])
end)
- after_each(function()
- screen:detach()
- end)
-
it('single left click moves cursor', function()
feed('<LeftMouse><2,1>')
screen:expect([[
@@ -419,9 +420,9 @@ describe('ui/mouse/input', function()
meths.set_option('showtabline', 2)
screen:expect([[
{fill:test-test2 }|
+ testing |
mouse |
support and selectio^n |
- {0:~ }|
|
]])
meths.set_var('reply', {})
@@ -539,9 +540,9 @@ describe('ui/mouse/input', function()
feed_command('tabprevious') -- go to first tab
screen:expect([[
{sel: + foo }{tab: + bar }{fill: }{tab:X}|
+ testing |
mouse |
support and selectio^n |
- {0:~ }|
:tabprevious |
]])
feed('<LeftMouse><10,0><LeftRelease>') -- go to second tab
@@ -620,12 +621,12 @@ describe('ui/mouse/input', function()
meths.set_option('tags', './non-existent-tags-file')
feed('<C-LeftMouse><0,0>')
screen:expect([[
- E433: No tags file |
- E426: tag not found: test|
- ing |
- Press ENTER or type comma|
- nd to continue^ |
- ]],nil,true)
+ {6:E433: No tags file} |
+ {6:E426: tag not found: test}|
+ {6:ing} |
+ {7:Press ENTER or type comma}|
+ {7:nd to continue}^ |
+ ]])
feed('<cr>')
end)
@@ -814,7 +815,8 @@ describe('ui/mouse/input', function()
feed_command('set concealcursor=ni')
feed_command('set nowrap')
- feed_command('set shiftwidth=2 tabstop=4 list listchars=tab:>-')
+ feed_command('set shiftwidth=2 tabstop=4 list')
+ feed_command('setl listchars=tab:>-')
feed_command('syntax match NonText "\\*" conceal')
feed_command('syntax match NonText "cats" conceal cchar=X')
feed_command('syntax match NonText "x" conceal cchar=>')
diff --git a/test/functional/ui/multibyte_spec.lua b/test/functional/ui/multibyte_spec.lua
index 3e63353ad2..8122cb08a3 100644
--- a/test/functional/ui/multibyte_spec.lua
+++ b/test/functional/ui/multibyte_spec.lua
@@ -21,10 +21,6 @@ describe("multibyte rendering", function()
})
end)
- after_each(function()
- screen:detach()
- end)
-
it("works with composed char at start of line", function()
insert([[
ÌŠ
@@ -131,10 +127,6 @@ describe('multibyte rendering: statusline', function()
command('set laststatus=2')
end)
- after_each(function()
- screen:detach()
- end)
-
it('last char shows (multibyte)', function()
command('set statusline=你好')
screen:expect([[
diff --git a/test/functional/ui/multigrid_spec.lua b/test/functional/ui/multigrid_spec.lua
index 30a5b63d89..01ffe80be3 100644
--- a/test/functional/ui/multigrid_spec.lua
+++ b/test/functional/ui/multigrid_spec.lua
@@ -37,10 +37,6 @@ describe('ext_multigrid', function()
})
end)
- after_each(function()
- screen:detach()
- end)
-
it('default initial screen', function()
screen:expect{grid=[[
## grid 1
diff --git a/test/functional/ui/options_spec.lua b/test/functional/ui/options_spec.lua
index 93192934c7..581e196bbb 100644
--- a/test/functional/ui/options_spec.lua
+++ b/test/functional/ui/options_spec.lua
@@ -5,7 +5,7 @@ local command = helpers.command
local eq = helpers.eq
local shallowcopy = helpers.shallowcopy
-describe('ui receives option updates', function()
+describe('UI receives option updates', function()
local screen
local function reset(opts, ...)
@@ -40,10 +40,6 @@ describe('ui receives option updates', function()
return defaults
end
- after_each(function()
- screen:detach()
- end)
-
it("for defaults", function()
local expected = reset()
screen:expect(function()
@@ -51,6 +47,33 @@ describe('ui receives option updates', function()
end)
end)
+ it('on attach #11372', function()
+ clear()
+ local evs = {}
+ screen = Screen.new(20,5)
+ -- Override mouse_on/mouse_off handlers.
+ function screen:_handle_mouse_on()
+ table.insert(evs, 'mouse_on')
+ end
+ function screen:_handle_mouse_off()
+ table.insert(evs, 'mouse_off')
+ end
+ screen:attach()
+ screen:expect(function()
+ eq({'mouse_off'}, evs)
+ end)
+ command("set mouse=nvi")
+ screen:expect(function()
+ eq({'mouse_off','mouse_on'}, evs)
+ end)
+ screen:detach()
+ eq({'mouse_off','mouse_on'}, evs)
+ screen:attach()
+ screen:expect(function()
+ eq({'mouse_off','mouse_on','mouse_on'}, evs)
+ end)
+ end)
+
it("when setting options", function()
local expected = reset()
local defaults = shallowcopy(expected)
diff --git a/test/functional/ui/output_spec.lua b/test/functional/ui/output_spec.lua
index c028f44b44..9b1e803649 100644
--- a/test/functional/ui/output_spec.lua
+++ b/test/functional/ui/output_spec.lua
@@ -10,6 +10,7 @@ local iswin = helpers.iswin
local clear = helpers.clear
local command = helpers.command
local nvim_dir = helpers.nvim_dir
+local set_shell_powershell = helpers.set_shell_powershell
describe("shell command :!", function()
local screen
@@ -30,7 +31,6 @@ describe("shell command :!", function()
after_each(function()
child_session.feed_data("\3") -- Ctrl-C
- screen:detach()
end)
it("displays output without LF/EOF. #4646 #4569 #3772", function()
@@ -51,8 +51,7 @@ describe("shell command :!", function()
it("throttles shell-command output greater than ~10KB", function()
if 'openbsd' == helpers.uname() then
- pending('FIXME #10804', function() end)
- return
+ pending('FIXME #10804')
end
child_session.feed_data(":!"..nvim_dir.."/shell-test REP 30001 foo\n")
@@ -96,8 +95,7 @@ describe("shell command :!", function()
it('handles control codes', function()
if iswin() then
- pending('missing printf', function() end)
- return
+ pending('missing printf')
end
local screen = Screen.new(50, 4)
screen:attach()
@@ -230,4 +228,23 @@ describe("shell command :!", function()
]])
end)
end)
+ if iswin() or eval('executable("pwsh")') == 1 then
+ it('powershell supports literal strings', function()
+ set_shell_powershell()
+ local screen = Screen.new(30, 4)
+ screen:attach()
+ feed_command([[!'Write-Output $a']])
+ screen:expect{any='\nWrite%-Output %$a', timeout=10000}
+ feed_command([[!$a = 1; Write-Output '$a']])
+ screen:expect{any='\n%$a', timeout=10000}
+ feed_command([[!"Write-Output $a"]])
+ screen:expect{any='\nWrite%-Output', timeout=10000}
+ feed_command([[!$a = 1; Write-Output "$a"]])
+ screen:expect{any='\n1', timeout=10000}
+ feed_command(iswin()
+ and [[!& 'C:\\Windows\\system32\\cmd.exe' /c 'echo $a']]
+ or [[!& '/bin/sh' -c 'echo ''$a''']])
+ screen:expect{any='\n%$a', timeout=10000}
+ end)
+ end
end)
diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua
index ae2136f451..fabcc05ce6 100644
--- a/test/functional/ui/popupmenu_spec.lua
+++ b/test/functional/ui/popupmenu_spec.lua
@@ -516,6 +516,7 @@ describe('ui/ext_popupmenu', function()
{1:~ }|
:sign ^ |
]])
+ eq(0, funcs.wildmenumode())
feed('<tab>')
screen:expect{grid=[[
@@ -530,6 +531,7 @@ describe('ui/ext_popupmenu', function()
{1:~ }|
:sign define^ |
]], popupmenu={items=wild_expected, pos=0, anchor={1, 9, 6}}}
+ eq(1, funcs.wildmenumode())
feed('<left>')
screen:expect{grid=[[
@@ -589,6 +591,7 @@ describe('ui/ext_popupmenu', function()
:sign unplace^ |
]], popupmenu={items=wild_expected, pos=5, anchor={1, 9, 6}}}
feed('<esc>')
+ eq(0, funcs.wildmenumode())
-- check positioning with multibyte char in pattern
command("e långfile1")
@@ -1488,20 +1491,20 @@ describe('builtin popupmenu', function()
command("split")
screen:expect([[
+ xx |
choice^ |
- {1:~ }|
{n:word }{1: }|
{s:choice }{4: }|
{n:text } |
- {n:thing }{1: }|
+ {n:thing } |
{3:[No Name] [+] }|
{2:-- INSERT --} |
]])
meths.input_mouse('wheel', 'down', '', 0, 6, 15)
screen:expect{grid=[[
+ xx |
choice^ |
- {1:~ }|
{n:word }{1: }|
{s:choice }{4: }|
{n:text } |
diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua
index 06a2ac3ca2..64f784afe3 100644
--- a/test/functional/ui/screen.lua
+++ b/test/functional/ui/screen.lua
@@ -66,12 +66,12 @@
-- [1] = {reverse = true, bold = true},
-- [2] = {reverse = true}
-- })
--- screen:set_default_attr_ignore( {{}, {bold=true, foreground=NonText}} )
--
-- To help write screen tests, see Screen:snapshot_util().
-- To debug screen tests, see Screen:redraw_debug().
local helpers = require('test.functional.helpers')(nil)
+local busted = require('busted')
local deepcopy = helpers.deepcopy
local shallowcopy = helpers.shallowcopy
local concat_tables = helpers.concat_tables
@@ -169,12 +169,11 @@ function Screen.new(width, height)
ruler = {},
hl_groups = {},
_default_attr_ids = nil,
- _default_attr_ignore = nil,
_mouse_enabled = true,
_attrs = {},
- _hl_info = {},
+ _hl_info = {[0]={}},
_attr_table = {[0]={{},{}}},
- _clear_attrs = {},
+ _clear_attrs = nil,
_new_attrs = false,
_width = width,
_height = height,
@@ -202,12 +201,8 @@ function Screen:get_default_attr_ids()
return deepcopy(self._default_attr_ids)
end
-function Screen:set_default_attr_ignore(attr_ignore)
- self._default_attr_ignore = attr_ignore
-end
-
-function Screen:set_hlstate_cterm(val)
- self._hlstate_cterm = val
+function Screen:set_rgb_cterm(val)
+ self._rgb_cterm = val
end
function Screen:attach(options, session)
@@ -223,7 +218,7 @@ function Screen:attach(options, session)
self._session = session
self._options = options
- self._clear_attrs = (options.ext_linegrid and {{},{}}) or {}
+ self._clear_attrs = (not options.ext_linegrid) and {} or nil
self:_handle_resize(self._width, self._height)
self.uimeths.attach(self._width, self._height, options)
if self._options.rgb == nil then
@@ -265,7 +260,7 @@ local ext_keys = {
-- Asserts that the screen state eventually matches an expected state.
--
-- Can be called with positional args:
--- screen:expect(grid, [attr_ids, attr_ignore])
+-- screen:expect(grid, [attr_ids])
-- screen:expect(condition)
-- or keyword args (supports more options):
-- screen:expect{grid=[[...]], cmdline={...}, condition=function() ... end}
@@ -274,7 +269,7 @@ local ext_keys = {
-- grid: Expected screen state (string). Each line represents a screen
-- row. Last character of each row (typically "|") is stripped.
-- Common indentation is stripped.
--- Lines containing only "{IGNORE}|" are skipped.
+-- "{MATCH:x}|" lines are matched against Lua pattern `x`.
-- attr_ids: Expected text attributes. Screen rows are transformed according
-- to this table, as follows: each substring S composed of
-- characters having the same attributes will be substituted by
@@ -282,8 +277,6 @@ local ext_keys = {
-- attributes in the final state are an error.
-- Use screen:set_default_attr_ids() to define attributes for many
-- expect() calls.
--- attr_ignore: Ignored text attributes, or `true` to ignore all. By default
--- nothing is ignored.
-- condition: Function asserting some arbitrary condition. Return value is
-- ignored, throw an error (use eq() or similar) to signal failure.
-- any: Lua pattern string expected to match a screen line. NB: the
@@ -318,13 +311,13 @@ local ext_keys = {
-- cmdline_block: Expected ext_cmdline block (for function definitions)
-- wildmenu_items: Expected items for ext_wildmenu
-- wildmenu_pos: Expected position for ext_wildmenu
-function Screen:expect(expected, attr_ids, attr_ignore, ...)
+function Screen:expect(expected, attr_ids, ...)
local grid, condition = nil, nil
local expected_rows = {}
assert(next({...}) == nil, "invalid args to expect()")
if type(expected) == "table" then
- assert(not (attr_ids ~= nil or attr_ignore ~= nil))
- local is_key = {grid=true, attr_ids=true, attr_ignore=true, condition=true,
+ assert(not (attr_ids ~= nil))
+ local is_key = {grid=true, attr_ids=true, condition=true,
any=true, mode=true, unchanged=true, intermediate=true,
reset=true, timeout=true, request_cb=true, hl_groups=true}
for _, v in ipairs(ext_keys) do
@@ -337,14 +330,13 @@ function Screen:expect(expected, attr_ids, attr_ignore, ...)
end
grid = expected.grid
attr_ids = expected.attr_ids
- attr_ignore = expected.attr_ignore
condition = expected.condition
assert(not (expected.any ~= nil and grid ~= nil))
elseif type(expected) == "string" then
grid = expected
expected = {}
elseif type(expected) == "function" then
- assert(not (attr_ids ~= nil or attr_ignore ~= nil))
+ assert(not (attr_ids ~= nil))
condition = expected
expected = {}
else
@@ -361,10 +353,9 @@ function Screen:expect(expected, attr_ids, attr_ignore, ...)
end
local attr_state = {
ids = attr_ids or self._default_attr_ids,
- ignore = attr_ignore or self._default_attr_ignore,
}
- if self._options.ext_hlstate then
- attr_state.id_to_index = self:hlstate_check_attrs(attr_state.ids or {})
+ if self._options.ext_linegrid then
+ attr_state.id_to_index = self:linegrid_check_attrs(attr_state.ids or {})
end
self._new_attrs = false
self:_wait(function()
@@ -375,8 +366,8 @@ function Screen:expect(expected, attr_ids, attr_ignore, ...)
end
end
- if self._options.ext_hlstate and self._new_attrs then
- attr_state.id_to_index = self:hlstate_check_attrs(attr_state.ids or {})
+ if self._options.ext_linegrid and self._new_attrs then
+ attr_state.id_to_index = self:linegrid_check_attrs(attr_state.ids or {})
end
local actual_rows = self:render(not expected.any, attr_state)
@@ -399,9 +390,10 @@ function Screen:expect(expected, attr_ids, attr_ignore, ...)
err_msg = "Expected screen height " .. #expected_rows
.. ' differs from actual height ' .. #actual_rows .. '.'
end
- for i = 1, #expected_rows do
- msg_expected_rows[i] = expected_rows[i]
- if expected_rows[i] ~= actual_rows[i] and expected_rows[i] ~= "{IGNORE}|" then
+ for i, row in ipairs(expected_rows) do
+ msg_expected_rows[i] = row
+ local m = (row ~= actual_rows[i] and row:match('{MATCH:(.*)}') or nil)
+ if row ~= actual_rows[i] and (not m or not actual_rows[i]:match(m)) then
msg_expected_rows[i] = '*' .. msg_expected_rows[i]
if i <= #actual_rows then
actual_rows[i] = '*' .. actual_rows[i]
@@ -584,7 +576,7 @@ asynchronous (feed(), nvim_input()) and synchronous API calls.
if err then
- assert(false, err)
+ busted.fail(err, 3)
elseif did_warn then
local tb = debug.traceback()
local index = string.find(tb, '\n%s*%[C]')
@@ -614,17 +606,12 @@ function Screen:_redraw(updates)
for i = 2, #update do
local handler_name = '_handle_'..method
local handler = self[handler_name]
- if handler ~= nil then
- local status, res = pcall(handler, self, unpack(update[i]))
- if not status then
- error(handler_name..' failed'
- ..'\n payload: '..inspect(update)
- ..'\n error: '..tostring(res))
- end
- else
- assert(self._on_event,
- "Add Screen:"..handler_name.." or call Screen:set_on_event_handler")
- self._on_event(method, update[i])
+ assert(handler ~= nil, "missing handler: Screen:"..handler_name)
+ local status, res = pcall(handler, self, unpack(update[i]))
+ if not status then
+ error(handler_name..' failed'
+ ..'\n payload: '..inspect(update)
+ ..'\n error: '..tostring(res))
end
end
if k == #updates and method == "flush" then
@@ -634,10 +621,6 @@ function Screen:_redraw(updates)
return did_flush
end
-function Screen:set_on_event_handler(callback)
- self._on_event = callback
-end
-
function Screen:_handle_resize(width, height)
self:_handle_grid_resize(1, width, height)
self._scroll_region = {
@@ -898,19 +881,16 @@ function Screen:_handle_grid_line(grid, row, col, items)
assert(self._options.ext_linegrid)
local line = self._grids[grid].rows[row+1]
local colpos = col+1
- local hl = self._clear_attrs
local hl_id = 0
for _,item in ipairs(items) do
local text, hl_id_cell, count = unpack(item)
if hl_id_cell ~= nil then
hl_id = hl_id_cell
- hl = self._attr_table[hl_id]
end
for _ = 1, (count or 1) do
local cell = line[colpos]
cell.text = text
cell.hl_id = hl_id
- cell.attrs = hl
colpos = colpos+1
end
end
@@ -1070,6 +1050,7 @@ function Screen:_clear_row_section(grid, rownum, startcol, stopcol, invalid)
for i = startcol, stopcol do
row[i].text = (invalid and '�' or ' ')
row[i].attrs = self._clear_attrs
+ row[i].hl_id = 0
end
end
@@ -1100,11 +1081,7 @@ function Screen:_row_repr(gridnr, rownr, attr_state, cursor)
end
if not did_window then
- local attrs = row[i].attrs
- if self._options.ext_linegrid then
- attrs = attrs[(self._options.rgb and 1) or 2]
- end
- local attr_id = self:_get_attr_id(attr_state, attrs, row[i].hl_id)
+ local attr_id = self:_get_attr_id(attr_state, row[i].attrs, row[i].hl_id)
if current_attr_id and attr_id ~= current_attr_id then
-- close current attribute bracket
table.insert(rv, '}')
@@ -1261,8 +1238,8 @@ function Screen:get_snapshot(attrs, ignore)
attr_state.ids[i] = a
end
end
- if self._options.ext_hlstate then
- attr_state.id_to_index = self:hlstate_check_attrs(attr_state.ids)
+ if self._options.ext_linegrid then
+ attr_state.id_to_index = self:linegrid_check_attrs(attr_state.ids)
end
local lines = self:render(true, attr_state, true)
@@ -1299,8 +1276,8 @@ function Screen:print_snapshot(attrs, ignore)
local attrstrs = {}
for i, a in pairs(attr_state.ids) do
local dict
- if self._options.ext_hlstate then
- dict = self:_pprint_hlstate(a)
+ if self._options.ext_linegrid then
+ dict = self:_pprint_hlitem(a)
else
dict = "{"..self:_pprint_attrs(a).."}"
end
@@ -1328,37 +1305,41 @@ function Screen:_insert_hl_id(attr_state, hl_id)
return attr_state.id_to_index[hl_id]
end
local raw_info = self._hl_info[hl_id]
- local info = {}
- if #raw_info > 1 then
- for i, item in ipairs(raw_info) do
- info[i] = self:_insert_hl_id(attr_state, item.id)
- end
- else
- info[1] = {}
- for k, v in pairs(raw_info[1]) do
- if k ~= "id" then
- info[1][k] = v
+ local info = nil
+ if self._options.ext_hlstate then
+ info = {}
+ if #raw_info > 1 then
+ for i, item in ipairs(raw_info) do
+ info[i] = self:_insert_hl_id(attr_state, item.id)
+ end
+ else
+ info[1] = {}
+ for k, v in pairs(raw_info[1]) do
+ if k ~= "id" then
+ info[1][k] = v
+ end
end
end
end
local entry = self._attr_table[hl_id]
local attrval
- if self._hlstate_cterm then
+ if self._rgb_cterm then
attrval = {entry[1], entry[2], info} -- unpack() doesn't work
- else
+ elseif self._options.ext_hlstate then
attrval = {entry[1], info}
+ else
+ attrval = self._options.rgb and entry[1] or entry[2]
end
-
table.insert(attr_state.ids, attrval)
attr_state.id_to_index[hl_id] = #attr_state.ids
return #attr_state.ids
end
-function Screen:hlstate_check_attrs(attrs)
+function Screen:linegrid_check_attrs(attrs)
local id_to_index = {}
- for i = 1,#self._attr_table do
+ for i, def_attr in pairs(self._attr_table) do
local iinfo = self._hl_info[i]
local matchinfo = {}
if #iinfo > 1 then
@@ -1370,13 +1351,17 @@ function Screen:hlstate_check_attrs(attrs)
end
for k,v in pairs(attrs) do
local attr, info, attr_rgb, attr_cterm
- if self._hlstate_cterm then
+ if self._rgb_cterm then
attr_rgb, attr_cterm, info = unpack(v)
attr = {attr_rgb, attr_cterm}
- else
+ info = info or {}
+ elseif self._options.ext_hlstate then
attr, info = unpack(v)
+ else
+ attr = v
+ info = {}
end
- if self:_equal_attr_def(attr, self._attr_table[i]) then
+ if self:_equal_attr_def(attr, def_attr) then
if #info == #matchinfo then
local match = false
if #info == 1 then
@@ -1397,24 +1382,32 @@ function Screen:hlstate_check_attrs(attrs)
end
end
end
+ if self:_equal_attr_def(self._rgb_cterm and {{}, {}} or {}, def_attr) and #self._hl_info[i] == 0 then
+ id_to_index[i] = ""
+ end
end
return id_to_index
end
-function Screen:_pprint_hlstate(item)
+function Screen:_pprint_hlitem(item)
-- print(inspect(item))
- local attrdict = "{"..self:_pprint_attrs(item[1]).."}, "
+ local multi = self._rgb_cterm or self._options.ext_hlstate
+ local cterm = (not self._rgb_cterm and not self._options.rgb)
+ local attrdict = "{"..self:_pprint_attrs(multi and item[1] or item, cterm).."}"
local attrdict2, hlinfo
- if self._hlstate_cterm then
- attrdict2 = "{"..self:_pprint_attrs(item[2]).."}, "
+ local descdict = ""
+ if self._rgb_cterm then
+ attrdict2 = ", {"..self:_pprint_attrs(item[2], true).."}"
hlinfo = item[3]
else
attrdict2 = ""
hlinfo = item[2]
end
- local descdict = "{"..self:_pprint_hlinfo(hlinfo).."}"
- return "{"..attrdict..attrdict2..descdict.."}"
+ if self._options.ext_hlstate then
+ descdict = ", {"..self:_pprint_hlinfo(hlinfo).."}"
+ end
+ return (multi and "{" or "")..attrdict..attrdict2..descdict..(multi and "}" or "")
end
function Screen:_pprint_hlinfo(states)
@@ -1434,13 +1427,15 @@ function Screen:_pprint_hlinfo(states)
end
-function Screen:_pprint_attrs(attrs)
+function Screen:_pprint_attrs(attrs, cterm)
local items = {}
for f, v in pairs(attrs) do
local desc = tostring(v)
if f == "foreground" or f == "background" or f == "special" then
if Screen.colornames[v] ~= nil then
desc = "Screen.colors."..Screen.colornames[v]
+ elseif cterm then
+ desc = tostring(v)
else
desc = string.format("tonumber('0x%06x')",v)
end
@@ -1464,9 +1459,11 @@ function Screen:_get_attr_id(attr_state, attrs, hl_id)
return
end
- if self._options.ext_hlstate then
+ if self._options.ext_linegrid then
local id = attr_state.id_to_index[hl_id]
- if id ~= nil or hl_id == 0 then
+ if id == "" then -- sentinel for empty it
+ return nil
+ elseif id ~= nil then
return id
end
if attr_state.mutable then
@@ -1476,9 +1473,7 @@ function Screen:_get_attr_id(attr_state, attrs, hl_id)
end
return "UNEXPECTED "..self:_pprint_attrs(self._attr_table[hl_id][1])
else
- if self:_equal_attrs(attrs, {}) or
- attr_state.ignore == true or
- self:_attr_index(attr_state.ignore, attrs) ~= nil then
+ if self:_equal_attrs(attrs, {}) then
-- ignore this attrs
return nil
end
@@ -1497,10 +1492,12 @@ function Screen:_get_attr_id(attr_state, attrs, hl_id)
end
function Screen:_equal_attr_def(a, b)
- if self._hlstate_cterm then
+ if self._rgb_cterm then
return self:_equal_attrs(a[1],b[1]) and self:_equal_attrs(a[2],b[2])
- else
+ elseif self._options.rgb then
return self:_equal_attrs(a,b[1])
+ else
+ return self:_equal_attrs(a,b[2])
end
end
@@ -1510,7 +1507,8 @@ function Screen:_equal_attrs(a, b)
a.italic == b.italic and a.reverse == b.reverse and
a.foreground == b.foreground and a.background == b.background and
a.special == b.special and a.blend == b.blend and
- a.strikethrough == b.strikethrough
+ a.strikethrough == b.strikethrough and
+ a.fg_indexed == b.fg_indexed and a.bg_indexed == b.bg_indexed
end
function Screen:_equal_info(a, b)
diff --git a/test/functional/ui/screen_basic_spec.lua b/test/functional/ui/screen_basic_spec.lua
index 46f0b5060c..150ee2a103 100644
--- a/test/functional/ui/screen_basic_spec.lua
+++ b/test/functional/ui/screen_basic_spec.lua
@@ -915,6 +915,7 @@ local function screen_tests(linegrid)
-- Regression test for #8357
it('does not have artifacts after temporary chars in insert mode', function()
+ command('set timeoutlen=10000')
command('inoremap jk <esc>')
feed('ifooj')
screen:expect([[
diff --git a/test/functional/ui/searchhl_spec.lua b/test/functional/ui/searchhl_spec.lua
index 486de02a09..635ce7392b 100644
--- a/test/functional/ui/searchhl_spec.lua
+++ b/test/functional/ui/searchhl_spec.lua
@@ -442,7 +442,7 @@ describe('search highlighting', function()
feed_command("call matchadd('MyGroup', 'special')")
feed_command("call matchadd('MyGroup2', 'text', 0)")
- -- searchhl and matchadd matches are exclusive, only the higest priority
+ -- searchhl and matchadd matches are exclusive, only the highest priority
-- is used (and matches with lower priorities are not combined)
feed_command("/ial te")
screen:expect([[
diff --git a/test/functional/ui/sign_spec.lua b/test/functional/ui/sign_spec.lua
index 68e675b8e5..0ed62b21b2 100644
--- a/test/functional/ui/sign_spec.lua
+++ b/test/functional/ui/sign_spec.lua
@@ -26,10 +26,6 @@ describe('Signs', function()
} )
end)
- after_each(function()
- screen:detach()
- end)
-
describe(':sign place', function()
it('allows signs with combining characters', function()
feed('ia<cr>b<cr><esc>')
diff --git a/test/functional/ui/spell_spec.lua b/test/functional/ui/spell_spec.lua
index 913f1b9bed..243b737583 100644
--- a/test/functional/ui/spell_spec.lua
+++ b/test/functional/ui/spell_spec.lua
@@ -20,10 +20,6 @@ describe("'spell'", function()
})
end)
- after_each(function()
- screen:detach()
- end)
-
it('joins long lines #7937', function()
feed_command('set spell')
insert([[
diff --git a/test/functional/ui/syntax_conceal_spec.lua b/test/functional/ui/syntax_conceal_spec.lua
index 00e94ef94b..d1af0e955c 100644
--- a/test/functional/ui/syntax_conceal_spec.lua
+++ b/test/functional/ui/syntax_conceal_spec.lua
@@ -1,6 +1,7 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local clear, feed, command = helpers.clear, helpers.feed, helpers.command
+local eq = helpers.eq
local insert = helpers.insert
describe('Screen', function()
@@ -17,13 +18,10 @@ describe('Screen', function()
[3] = {reverse = true},
[4] = {bold = true},
[5] = {background = Screen.colors.Yellow},
+ [6] = {background = Screen.colors.LightGrey},
} )
end)
- after_each(function()
- screen:detach()
- end)
-
describe("match and conceal", function()
before_each(function()
@@ -823,5 +821,96 @@ describe('Screen', function()
]])
end)
end)
+
+ it('redraws properly with concealcursor in visual mode', function()
+ command('set concealcursor=v conceallevel=2')
+
+ feed('10Ofoo barf bar barf eggs<esc>')
+ feed(':3<cr>o a<Esc>ggV')
+ screen:expect{grid=[[
+ ^f{6:oo }{1:b}{6: bar }{1:b}{6: eggs} |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ a |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ {4:-- VISUAL LINE --} |
+ ]]}
+ feed(string.rep('j', 15))
+ screen:expect{grid=[[
+ {6:foo }{1:b}{6: bar }{1:b}{6: eggs} |
+ {6:foo }{1:b}{6: bar }{1:b}{6: eggs} |
+ {6:foo }{1:b}{6: bar }{1:b}{6: eggs} |
+ {6:foo }{1:b}{6: bar }{1:b}{6: eggs} |
+ {6:foo }{1:b}{6: bar }{1:b}{6: eggs} |
+ {6:foo }{1:b}{6: bar }{1:b}{6: eggs} |
+ {6:foo }{1:b}{6: bar }{1:b}{6: eggs} |
+ {6:foo }{1:b}{6: bar }{1:b}{6: eggs} |
+ ^f{6:oo }{1:b}{6: bar }{1:b}{6: eggs} |
+ {4:-- VISUAL LINE --} |
+ ]]}
+ feed(string.rep('k', 15))
+ screen:expect{grid=[[
+ ^f{6:oo }{1:b}{6: bar }{1:b}{6: eggs} |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ a |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ foo {1:b} bar {1:b} eggs |
+ {4:-- VISUAL LINE --} |
+ ]]}
+ end)
+ end)
+
+ it('redraws not too much with conceallevel=1', function()
+ command('set conceallevel=1')
+ command('set redrawdebug+=nodelta')
+
+ insert([[
+ aaa
+ bbb
+ ccc
+ ]])
+ screen:expect{grid=[[
+ aaa |
+ bbb |
+ ccc |
+ ^ |
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ |
+ ]]}
+
+ -- XXX: hack to get notifications, and check only a single line is
+ -- updated. Could use next_msg() also.
+ local orig_handle_grid_line = screen._handle_grid_line
+ local grid_lines = {}
+ function screen._handle_grid_line(self, grid, row, col, items)
+ table.insert(grid_lines, {row, col, items})
+ orig_handle_grid_line(self, grid, row, col, items)
+ end
+ feed('k')
+ screen:expect{grid=[[
+ aaa |
+ bbb |
+ ^ccc |
+ |
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ |
+ ]]}
+ eq(grid_lines, {{2, 0, {{'c', 0, 3}}}})
end)
end)
diff --git a/test/functional/ui/tabline_spec.lua b/test/functional/ui/tabline_spec.lua
index dcab9f7ef4..23aae81745 100644
--- a/test/functional/ui/tabline_spec.lua
+++ b/test/functional/ui/tabline_spec.lua
@@ -10,15 +10,9 @@ describe('ui/ext_tabline', function()
clear()
screen = Screen.new(25, 5)
screen:attach({rgb=true, ext_tabline=true})
- screen:set_on_event_handler(function(name, data)
- if name == "tabline_update" then
- event_curtab, event_tabs = unpack(data)
- end
- end)
- end)
-
- after_each(function()
- screen:detach()
+ function screen:_handle_tabline_update(curtab, tabs)
+ event_curtab, event_tabs = curtab, tabs
+ end
end)
it('publishes UI events', function()
diff --git a/test/helpers.lua b/test/helpers.lua
index ebc0a7d811..98f003f208 100644
--- a/test/helpers.lua
+++ b/test/helpers.lua
@@ -74,7 +74,8 @@ function module.matches(pat, actual)
error(string.format('Pattern does not match.\nPattern:\n%s\nActual:\n%s', pat, actual))
end
--- Invokes `fn` and returns the error string, or raises an error if `fn` succeeds.
+-- Invokes `fn` and returns the error string (may truncate full paths), or
+-- raises an error if `fn` succeeds.
--
-- Usage:
-- -- Match exact string.
@@ -88,7 +89,20 @@ function module.pcall_err(fn, ...)
if status == true then
error('expected failure, but got success')
end
+ -- From this:
+ -- /home/foo/neovim/runtime/lua/vim/shared.lua:186: Expected string, got number
+ -- to this:
+ -- Expected string, got number
local errmsg = tostring(rv):gsub('^[^:]+:%d+: ', '')
+ -- From this:
+ -- Error executing lua: /very/long/foo.lua:186: Expected string, got number
+ -- to this:
+ -- Error executing lua: .../foo.lua:186: Expected string, got number
+ errmsg = errmsg:gsub([[lua: [a-zA-Z]?:?[^:]-[/\]([^:/\]+):%d+: ]], 'lua: .../%1: ')
+ -- Compiled modules will not have a path and will just be a name like
+ -- shared.lua:186, so strip the number.
+ errmsg = errmsg:gsub([[lua: ([^:/\ ]+):%d+: ]], 'lua: .../%1: ')
+ -- ^ Windows drive-letter (C:)
return errmsg
end
@@ -715,11 +729,12 @@ end
function module.isCI(name)
local any = (name == nil)
- assert(any or name == 'appveyor' or name == 'quickbuild' or name == 'travis')
+ assert(any or name == 'appveyor' or name == 'travis' or name == 'sourcehut')
local av = ((any or name == 'appveyor') and nil ~= os.getenv('APPVEYOR'))
local tr = ((any or name == 'travis') and nil ~= os.getenv('TRAVIS'))
- local qb = ((any or name == 'quickbuild') and nil ~= lfs.attributes('/usr/home/quickbuild'))
- return tr or av or qb
+ local sh = ((any or name == 'sourcehut') and nil ~= os.getenv('SOURCEHUT'))
+ return tr or av or sh
+
end
-- Gets the contents of $NVIM_LOG_FILE for printing to the build log.
diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua
index 24dbc65bd0..bacdc54416 100644
--- a/test/unit/helpers.lua
+++ b/test/unit/helpers.lua
@@ -545,7 +545,7 @@ local tracehelp = dedent([[
local function child_sethook(wr)
local trace_level = os.getenv('NVIM_TEST_TRACE_LEVEL')
if not trace_level or trace_level == '' then
- trace_level = 1
+ trace_level = 0
else
trace_level = tonumber(trace_level)
end
@@ -708,7 +708,7 @@ local function check_child_err(rd)
local eres = sc.read(rd, 2)
if eres ~= '$\n' then
if #trace == 0 then
- err = '\nTest crashed, no trace available\n'
+ err = '\nTest crashed, no trace available (check NVIM_TEST_TRACE_LEVEL)\n'
else
err = '\nTest crashed, trace:\n' .. tracehelp
for i = 1, #trace do
diff --git a/test/unit/mbyte_spec.lua b/test/unit/mbyte_spec.lua
index d27f52923a..fdb1bceab0 100644
--- a/test/unit/mbyte_spec.lua
+++ b/test/unit/mbyte_spec.lua
@@ -8,11 +8,6 @@ local mbyte = helpers.cimport("./src/nvim/mbyte.h")
local charset = helpers.cimport('./src/nvim/charset.h')
describe('mbyte', function()
- if helpers.isCI('quickbuild') then
- pending("crashes on quickbuild", function() end)
- return
- end
-
-- Array for composing characters
local intp = ffi.typeof('int[?]')
local function to_intp()
diff --git a/test/unit/os/env_spec.lua b/test/unit/os/env_spec.lua
index c543551607..e24a389d69 100644
--- a/test/unit/os/env_spec.lua
+++ b/test/unit/os/env_spec.lua
@@ -121,7 +121,7 @@ describe('env.c', function()
local name = 'NVIM_UNIT_TEST_GETENV_1N'
local value = 'NVIM_UNIT_TEST_GETENV_1V'
eq(NULL, os_getenv(name))
- -- Use os_setenv because Lua dosen't have setenv.
+ -- Use os_setenv because Lua doesn't have setenv.
os_setenv(name, value, 1)
eq(value, os_getenv(name))
diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua
index 526a09e845..7fd71cb1ae 100644
--- a/test/unit/os/fs_spec.lua
+++ b/test/unit/os/fs_spec.lua
@@ -110,7 +110,7 @@ describe('fs.c', function()
describe('os_chdir', function()
itp('fails with path="~"', function()
- eq(false, os_isdir('~')) -- sanity check: no literal "~" directory.
+ eq(false, os_isdir('~'), 'sanity check: no literal "~" directory')
local length = 4096
local expected_cwd = cstr(length, '')
local cwd = cstr(length, '')
diff --git a/test/unit/path_spec.lua b/test/unit/path_spec.lua
index e8ce660ce1..356c4997fa 100644
--- a/test/unit/path_spec.lua
+++ b/test/unit/path_spec.lua
@@ -66,10 +66,10 @@ describe('path.c', function()
end)
describe('path_full_compare', function()
- local function path_full_compare(s1, s2, cn)
+ local function path_full_compare(s1, s2, cn, ee)
s1 = to_cstr(s1)
s2 = to_cstr(s2)
- return cimp.path_full_compare(s1, s2, cn or 0)
+ return cimp.path_full_compare(s1, s2, cn or 0, ee or 1)
end
local f1 = 'f1.o'
@@ -456,7 +456,7 @@ describe('path.c', function()
end)
itp('fails and uses filename when the path is relative to HOME', function()
- eq(false, cimp.os_isdir('~')) -- sanity check: no literal "~" directory.
+ eq(false, cimp.os_isdir('~'), 'sanity check: no literal "~" directory')
local absolute_path = '~/home.file'
local buflen = string.len(absolute_path) + 1
local do_expand = 1
diff --git a/test/unit/preprocess.lua b/test/unit/preprocess.lua
index 1073855a7d..3786bc2122 100644
--- a/test/unit/preprocess.lua
+++ b/test/unit/preprocess.lua
@@ -89,6 +89,10 @@ local Gcc = {
get_defines_extra_flags = {'-std=c99', '-dM', '-E'},
get_declarations_extra_flags = {'-std=c99', '-P', '-E'},
}
+if ffi.abi("32bit") then
+ table.insert(Gcc.get_defines_extra_flags, '-m32')
+ table.insert(Gcc.get_declarations_extra_flags, '-m32')
+end
function Gcc:define(name, args, val)
local define = '-D' .. name
diff --git a/test/unit/tui_spec.lua b/test/unit/tui_spec.lua
new file mode 100644
index 0000000000..e6b5c889d7
--- /dev/null
+++ b/test/unit/tui_spec.lua
@@ -0,0 +1,136 @@
+local helpers = require("test.unit.helpers")(after_each)
+local cimport = helpers.cimport
+local eq = helpers.eq
+local ffi = helpers.ffi
+local itp = helpers.gen_itp(it)
+local to_cstr = helpers.to_cstr
+
+local cinput = cimport("./src/nvim/tui/input.h")
+local rbuffer = cimport("./test/unit/fixtures/rbuffer.h")
+local globals = cimport("./src/nvim/globals.h")
+local multiqueue = cimport("./test/unit/fixtures/multiqueue.h")
+
+itp('handle_background_color', function()
+ local handle_background_color = cinput.ut_handle_background_color
+ local term_input = ffi.new('TermInput', {})
+ local events = globals.main_loop.thread_events
+
+ -- Short-circuit when not waiting for response.
+ term_input.waiting_for_bg_response = 0
+ eq(false, handle_background_color(term_input))
+
+ local capacity = 100
+ local rbuf = ffi.gc(rbuffer.rbuffer_new(capacity), rbuffer.rbuffer_free)
+ term_input.read_stream.buffer = rbuf
+
+ local function assert_bg(colorspace, color, bg)
+ local term_response = '\027]11;'..colorspace..':'..color..'\007'
+ rbuffer.rbuffer_write(rbuf, to_cstr(term_response), #term_response)
+
+ term_input.waiting_for_bg_response = 1
+ eq(true, handle_background_color(term_input))
+ eq(0, term_input.waiting_for_bg_response)
+ eq(1, multiqueue.multiqueue_size(events))
+
+ local event = multiqueue.multiqueue_get(events)
+ local bg_event = ffi.cast("Event*", event.argv[1])
+ eq(bg, ffi.string(bg_event.argv[0]))
+
+ -- Buffer has been consumed.
+ eq(0, rbuf.size)
+ end
+
+ assert_bg('rgb', '0000/0000/0000', 'dark')
+ assert_bg('rgb', 'ffff/ffff/ffff', 'light')
+ assert_bg('rgb', '000/000/000', 'dark')
+ assert_bg('rgb', 'fff/fff/fff', 'light')
+ assert_bg('rgb', '00/00/00', 'dark')
+ assert_bg('rgb', 'ff/ff/ff', 'light')
+ assert_bg('rgb', '0/0/0', 'dark')
+ assert_bg('rgb', 'f/f/f', 'light')
+
+ assert_bg('rgb', 'f/0/0', 'dark')
+ assert_bg('rgb', '0/f/0', 'light')
+ assert_bg('rgb', '0/0/f', 'dark')
+
+ assert_bg('rgb', '1/1/1', 'dark')
+ assert_bg('rgb', '2/2/2', 'dark')
+ assert_bg('rgb', '3/3/3', 'dark')
+ assert_bg('rgb', '4/4/4', 'dark')
+ assert_bg('rgb', '5/5/5', 'dark')
+ assert_bg('rgb', '6/6/6', 'dark')
+ assert_bg('rgb', '7/7/7', 'dark')
+ assert_bg('rgb', '8/8/8', 'light')
+ assert_bg('rgb', '9/9/9', 'light')
+ assert_bg('rgb', 'a/a/a', 'light')
+ assert_bg('rgb', 'b/b/b', 'light')
+ assert_bg('rgb', 'c/c/c', 'light')
+ assert_bg('rgb', 'd/d/d', 'light')
+ assert_bg('rgb', 'e/e/e', 'light')
+
+ assert_bg('rgb', '0/e/0', 'light')
+ assert_bg('rgb', '0/d/0', 'light')
+ assert_bg('rgb', '0/c/0', 'dark')
+ assert_bg('rgb', '0/b/0', 'dark')
+
+ assert_bg('rgb', 'f/0/f', 'dark')
+ assert_bg('rgb', 'f/1/f', 'dark')
+ assert_bg('rgb', 'f/2/f', 'dark')
+ assert_bg('rgb', 'f/3/f', 'light')
+ assert_bg('rgb', 'f/4/f', 'light')
+
+ assert_bg('rgba', '0000/0000/0000/0000', 'dark')
+ assert_bg('rgba', '0000/0000/0000/ffff', 'dark')
+ assert_bg('rgba', 'ffff/ffff/ffff/0000', 'light')
+ assert_bg('rgba', 'ffff/ffff/ffff/ffff', 'light')
+ assert_bg('rgba', '000/000/000/000', 'dark')
+ assert_bg('rgba', '000/000/000/fff', 'dark')
+ assert_bg('rgba', 'fff/fff/fff/000', 'light')
+ assert_bg('rgba', 'fff/fff/fff/fff', 'light')
+ assert_bg('rgba', '00/00/00/00', 'dark')
+ assert_bg('rgba', '00/00/00/ff', 'dark')
+ assert_bg('rgba', 'ff/ff/ff/00', 'light')
+ assert_bg('rgba', 'ff/ff/ff/ff', 'light')
+ assert_bg('rgba', '0/0/0/0', 'dark')
+ assert_bg('rgba', '0/0/0/f', 'dark')
+ assert_bg('rgba', 'f/f/f/0', 'light')
+ assert_bg('rgba', 'f/f/f/f', 'light')
+
+
+ -- Incomplete sequence: not necessarily correct behavior, but tests it.
+ local term_response = '\027]11;rgba:f/f/f/f' -- missing '\007
+ rbuffer.rbuffer_write(rbuf, to_cstr(term_response), #term_response)
+
+ term_input.waiting_for_bg_response = 1
+ eq(false, handle_background_color(term_input))
+ eq(0, term_input.waiting_for_bg_response)
+
+ eq(0, multiqueue.multiqueue_size(events))
+ eq(0, rbuf.size)
+
+
+ -- Does nothing when not at start of buffer.
+ term_response = '123\027]11;rgba:f/f/f/f\007456'
+ rbuffer.rbuffer_write(rbuf, to_cstr(term_response), #term_response)
+
+ term_input.waiting_for_bg_response = 3
+ eq(false, handle_background_color(term_input))
+ eq(2, term_input.waiting_for_bg_response)
+
+ eq(0, multiqueue.multiqueue_size(events))
+ eq(#term_response, rbuf.size)
+ rbuffer.rbuffer_consumed(rbuf, #term_response)
+
+
+ -- Keeps trailing buffer.
+ term_response = '\027]11;rgba:f/f/f/f\007456'
+ rbuffer.rbuffer_write(rbuf, to_cstr(term_response), #term_response)
+
+ term_input.waiting_for_bg_response = 1
+ eq(true, handle_background_color(term_input))
+ eq(0, term_input.waiting_for_bg_response)
+
+ eq(1, multiqueue.multiqueue_size(events))
+ eq(3, rbuf.size)
+ rbuffer.rbuffer_consumed(rbuf, rbuf.size)
+end)
diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt
index 43cad34aae..90ec475894 100644
--- a/third-party/CMakeLists.txt
+++ b/third-party/CMakeLists.txt
@@ -35,6 +35,7 @@ option(USE_BUNDLED_LIBTERMKEY "Use the bundled libtermkey." ${USE_BUNDLED})
option(USE_BUNDLED_LIBVTERM "Use the bundled libvterm." ${USE_BUNDLED})
option(USE_BUNDLED_LIBUV "Use the bundled libuv." ${USE_BUNDLED})
option(USE_BUNDLED_MSGPACK "Use the bundled msgpack." ${USE_BUNDLED})
+option(USE_BUNDLED_UTF8PROC "Use the bundled utf8proc." ${USE_BUNDLED})
option(USE_BUNDLED_LUAJIT "Use the bundled version of luajit." ${USE_BUNDLED})
option(USE_BUNDLED_LUAROCKS "Use the bundled version of luarocks." ${USE_BUNDLED})
option(USE_BUNDLED_LUV "Use the bundled version of luv." ${USE_BUNDLED})
@@ -134,38 +135,39 @@ include(ExternalProject)
if(WIN32)
# "nvim" branch of https://github.com/neovim/libuv
- set(LIBUV_URL https://github.com/neovim/libuv/archive/eeae18d085de25f138c23966f98a179f0fb609e7.tar.gz)
- set(LIBUV_SHA256 c37d0b7cb1defe69ae8dbb4d712c0d7cf838d6539178e8bcf71c72579ab5b666)
+ set(LIBUV_URL https://github.com/neovim/libuv/archive/d5ff3004d26b9bb863b76247399a9c72a0ff184c.tar.gz)
+ set(LIBUV_SHA256 0f5dfd92269713ed275273966ed73578fc68db669c509b01210cd58c1cf6361d)
else()
- set(LIBUV_URL https://github.com/libuv/libuv/archive/v1.30.0.tar.gz)
- set(LIBUV_SHA256 44c8fdadf3b3f393006a4ac4ba144020673a3f9cd72bed1fbb2c366ebcf0d199)
+ # blueyed/nvim-fixes (for *BSD build fixes).
+ set(LIBUV_URL https://github.com/blueyed/libuv/archive/2af4cf2.tar.gz)
+ set(LIBUV_SHA256 SKIP)
endif()
set(MSGPACK_URL https://github.com/msgpack/msgpack-c/releases/download/cpp-3.0.0/msgpack-3.0.0.tar.gz)
set(MSGPACK_SHA256 bfbb71b7c02f806393bc3cbc491b40523b89e64f83860c58e3e54af47de176e4)
-# https://github.com/LuaJIT/LuaJIT/tree/v2.0
-set(LUAJIT_URL https://github.com/LuaJIT/LuaJIT/archive/61464b0a5b685489bee7b6680c0e9663f2143a84.tar.gz)
-set(LUAJIT_SHA256 033fa4ef19f559ef18a9b9fb017d0cb8be58befe05ab604e92814234910f1c68)
+# https://github.com/LuaJIT/LuaJIT/tree/v2.1
+set(LUAJIT_URL https://github.com/LuaJIT/LuaJIT/archive/f0e865dd4861520258299d0f2a56491bd9d602e1.tar.gz)
+set(LUAJIT_SHA256 ad5077bd861241bf5e50ae4bf543d291c5fcffab95ccc3218401131f503e45bd)
set(LUA_URL https://www.lua.org/ftp/lua-5.1.5.tar.gz)
set(LUA_SHA256 2640fc56a795f29d28ef15e13c34a47e223960b0240e8cb0a82d9b0738695333)
-set(LUAROCKS_URL https://github.com/luarocks/luarocks/archive/v2.4.4.tar.gz)
-set(LUAROCKS_SHA256 9eb3d0738fd02ad8bf39bcedccac4e83e9b5fff2bcca247c3584b925b2075d9c)
+set(LUAROCKS_URL https://github.com/luarocks/luarocks/archive/v3.2.1.tar.gz)
+set(LUAROCKS_SHA256 0cab9f79311083f33e4d8f5a76021604f1d3f7141ce9a2ef1d8b717d92058370)
set(UNIBILIUM_URL https://github.com/neovim/unibilium/archive/92d929f.tar.gz)
set(UNIBILIUM_SHA256 29815283c654277ef77a3adcc8840db79ddbb20a0f0b0c8f648bd8cd49a02e4b)
-set(LIBTERMKEY_URL http://www.leonerd.org.uk/code/libtermkey/libtermkey-0.21.1.tar.gz)
-set(LIBTERMKEY_SHA256 cecbf737f35d18f433c8d7864f63c0f878af41f8bd0255a3ebb16010dc044d5f)
+set(LIBTERMKEY_URL http://www.leonerd.org.uk/code/libtermkey/libtermkey-0.22.tar.gz)
+set(LIBTERMKEY_SHA256 6945bd3c4aaa83da83d80a045c5563da4edd7d0374c62c0d35aec09eb3014600)
-set(LIBVTERM_URL https://github.com/neovim/libvterm/archive/4a5fa43e0dbc0db4fe67d40d788d60852864df9e.tar.gz)
-set(LIBVTERM_SHA256 49b3cf2dcb988b887671b1011cfeac98ff81bb5c23fb4ac34b91a59524992935)
+set(LIBVTERM_URL https://github.com/neovim/libvterm/archive/7c72294d84ce20da4c27362dbd7fa4b08cfc91da.tar.gz)
+set(LIBVTERM_SHA256 f30c4d43e0c6db3e0912daf7188d98fbf6ee88f97589d72f6f304e5db48826a8)
-set(LUV_VERSION 1.30.0-0)
+set(LUV_VERSION 1.30.1-1)
set(LUV_URL https://github.com/luvit/luv/archive/${LUV_VERSION}.tar.gz)
-set(LUV_SHA256 6e468fa17bf222ca8ce0bfffdbdf947fc897da48643a12955db92f80e2c852f5)
+set(LUV_SHA256 2b17921e2e18094df6221e3cd291c82d4569e50d8c081518d3e775dceae267cf)
set(LUA_COMPAT53_URL https://github.com/keplerproject/lua-compat-5.3/archive/v0.7.tar.gz)
set(LUA_COMPAT53_SHA256 bec3a23114a3d9b3218038309657f0f506ad10dfbc03bb54e91da7e5ffdba0a2)
@@ -177,8 +179,8 @@ set(GPERF_SHA256 588546b945bba4b70b6a3a616e80b4ab466e3f33024a352fc2198112cdbb3ae
set(WINTOOLS_URL https://github.com/neovim/deps/raw/2f9acbecf06365c10baa3c0087f34a54c9c6f949/opt/win32tools.zip)
set(WINTOOLS_SHA256 8bfce7e3a365721a027ce842f2ec1cf878f1726233c215c05964aac07300798c)
-set(WINGUI_URL https://github.com/equalsraf/neovim-qt/releases/download/v0.2.14/neovim-qt.zip)
-set(WINGUI_SHA256 dfcb1f7d25d4907dc1d4f20edd71ff9eb4762196225106bec01274dd668fb04c)
+set(WINGUI_URL https://github.com/equalsraf/neovim-qt/releases/download/v0.2.15/neovim-qt.zip)
+set(WINGUI_SHA256 b519ecb80b60522d25043f2d076a55656f5fbe5adf7f7e2943e5d8b161043987)
set(WIN32YANK_X86_URL https://github.com/equalsraf/win32yank/releases/download/v0.0.4/win32yank-x86.zip)
set(WIN32YANK_X86_SHA256 62f34e5a46c5d4a7b3f3b512e1ff7b77fedd432f42581cbe825233a996eed62c)
@@ -194,6 +196,9 @@ set(GETTEXT_SHA256 ff942af0e438ced4a8b0ea4b0b6e0d6d657157c5e2364de57baa279c1c125
set(LIBICONV_URL https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.15.tar.gz)
set(LIBICONV_SHA256 ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178)
+set(UTF8PROC_URL https://github.com/JuliaStrings/utf8proc/archive/v2.2.0.tar.gz)
+set(UTF8PROC_SHA256 3f8fd1dbdb057ee5ba584a539d5cd1b3952141c0338557cb0bdf8cb9cfed5dbf)
+
if(USE_BUNDLED_UNIBILIUM)
include(BuildUnibilium)
endif()
@@ -245,6 +250,10 @@ if(USE_BUNDLED_LIBICONV)
include(BuildLibiconv)
endif()
+if(USE_BUNDLED_UTF8PROC)
+ include(BuildUtf8proc)
+endif()
+
if(WIN32)
include(GetBinaryDeps)
diff --git a/third-party/cmake/BuildLibtermkey.cmake b/third-party/cmake/BuildLibtermkey.cmake
index b2332ed65a..10e98fbab3 100644
--- a/third-party/cmake/BuildLibtermkey.cmake
+++ b/third-party/cmake/BuildLibtermkey.cmake
@@ -48,6 +48,7 @@ ExternalProject_Add(libtermkey
PREFIX=${DEPS_INSTALL_DIR}
PKG_CONFIG_PATH=${DEPS_LIB_DIR}/pkgconfig
CFLAGS=-fPIC
+ LDFLAGS+=-static
${DEFAULT_MAKE_CFLAGS}
install)
endif()
diff --git a/third-party/cmake/BuildLibvterm.cmake b/third-party/cmake/BuildLibvterm.cmake
index e4649986af..61c1c90fa6 100644
--- a/third-party/cmake/BuildLibvterm.cmake
+++ b/third-party/cmake/BuildLibvterm.cmake
@@ -58,6 +58,7 @@ else()
set(LIBVTERM_INSTALL_COMMAND ${MAKE_PRG} CC=${DEPS_C_COMPILER}
PREFIX=${DEPS_INSTALL_DIR}
CFLAGS=-fPIC
+ LDFLAGS+=-static
${DEFAULT_MAKE_CFLAGS}
install)
endif()
diff --git a/third-party/cmake/BuildLuajit.cmake b/third-party/cmake/BuildLuajit.cmake
index 791da4b7df..c0b24fb2a5 100644
--- a/third-party/cmake/BuildLuajit.cmake
+++ b/third-party/cmake/BuildLuajit.cmake
@@ -33,8 +33,21 @@ function(BuildLuajit)
BUILD_IN_SOURCE 1
BUILD_COMMAND "${_luajit_BUILD_COMMAND}"
INSTALL_COMMAND "${_luajit_INSTALL_COMMAND}")
+
+ # Create symlink for development version manually.
+ if(UNIX)
+ add_custom_command(
+ TARGET ${_luajit_TARGET}
+ COMMAND ${CMAKE_COMMAND} -E create_symlink luajit-2.1.0-beta3 ${DEPS_BIN_DIR}/luajit)
+ endif()
endfunction()
+check_c_compiler_flag(-fno-stack-check HAS_NO_STACK_CHECK)
+if(CMAKE_SYSTEM_NAME MATCHES "Darwin" AND HAS_NO_STACK_CHECK)
+ set(NO_STACK_CHECK "CFLAGS+=-fno-stack-check")
+else()
+ set(NO_STACK_CHECK "")
+endif()
if(CMAKE_SYSTEM_NAME MATCHES "OpenBSD")
set(AMD64_ABI "LDFLAGS=-lpthread -lc++abi")
else()
@@ -43,6 +56,7 @@ endif()
set(INSTALLCMD_UNIX ${MAKE_PRG} CFLAGS=-fPIC
CFLAGS+=-DLUA_USE_APICHECK
CFLAGS+=-DLUA_USE_ASSERT
+ ${NO_STACK_CHECK}
${AMD64_ABI}
CCDEBUG+=-g
Q=
@@ -107,8 +121,8 @@ elseif(MINGW)
# Luarocks searches for lua51.dll in lib
COMMAND ${CMAKE_COMMAND} -E copy ${DEPS_BUILD_DIR}/src/luajit/src/lua51.dll ${DEPS_INSTALL_DIR}/lib
COMMAND ${CMAKE_COMMAND} -E copy ${DEPS_BUILD_DIR}/src/luajit/src/libluajit.a ${DEPS_INSTALL_DIR}/lib
- COMMAND ${CMAKE_COMMAND} -E make_directory ${DEPS_INSTALL_DIR}/include/luajit-2.0
- COMMAND ${CMAKE_COMMAND} -DFROM_GLOB=${DEPS_BUILD_DIR}/src/luajit/src/*.h -DTO=${DEPS_INSTALL_DIR}/include/luajit-2.0 -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/CopyFilesGlob.cmake
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${DEPS_INSTALL_DIR}/include/luajit-2.1
+ COMMAND ${CMAKE_COMMAND} -DFROM_GLOB=${DEPS_BUILD_DIR}/src/luajit/src/*.h -DTO=${DEPS_INSTALL_DIR}/include/luajit-2.1 -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/CopyFilesGlob.cmake
)
elseif(MSVC)
@@ -122,8 +136,8 @@ elseif(MSVC)
COMMAND ${CMAKE_COMMAND} -E copy ${DEPS_BUILD_DIR}/src/luajit/src/lua51.lib ${DEPS_INSTALL_DIR}/lib/lua51.lib
# Luv searches for luajit.lib
COMMAND ${CMAKE_COMMAND} -E copy ${DEPS_BUILD_DIR}/src/luajit/src/lua51.lib ${DEPS_INSTALL_DIR}/lib/luajit.lib
- COMMAND ${CMAKE_COMMAND} -E make_directory ${DEPS_INSTALL_DIR}/include/luajit-2.0
- COMMAND ${CMAKE_COMMAND} -DFROM_GLOB=${DEPS_BUILD_DIR}/src/luajit/src/*.h -DTO=${DEPS_INSTALL_DIR}/include/luajit-2.0 -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/CopyFilesGlob.cmake)
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${DEPS_INSTALL_DIR}/include/luajit-2.1
+ COMMAND ${CMAKE_COMMAND} -DFROM_GLOB=${DEPS_BUILD_DIR}/src/luajit/src/*.h -DTO=${DEPS_INSTALL_DIR}/include/luajit-2.1 -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/CopyFilesGlob.cmake)
else()
message(FATAL_ERROR "Trying to build luajit in an unsupported system ${CMAKE_SYSTEM_NAME}/${CMAKE_C_COMPILER_ID}")
diff --git a/third-party/cmake/BuildLuarocks.cmake b/third-party/cmake/BuildLuarocks.cmake
index 87e2946c96..4b1b94a46b 100644
--- a/third-party/cmake/BuildLuarocks.cmake
+++ b/third-party/cmake/BuildLuarocks.cmake
@@ -52,13 +52,17 @@ if(NOT MSVC)
set(LUAROCKS_BUILDARGS CC=${HOSTDEPS_C_COMPILER} LD=${HOSTDEPS_C_COMPILER})
endif()
+# Lua version, used with rocks directories.
+# Defaults to 5.1 for bundled LuaJIT/Lua.
+set(LUA_VERSION "5.1")
+
if(UNIX OR (MINGW AND CMAKE_CROSSCOMPILING))
if(USE_BUNDLED_LUAJIT)
list(APPEND LUAROCKS_OPTS
--with-lua=${HOSTDEPS_INSTALL_DIR}
- --with-lua-include=${HOSTDEPS_INSTALL_DIR}/include/luajit-2.0
- --lua-suffix=jit)
+ --with-lua-include=${HOSTDEPS_INSTALL_DIR}/include/luajit-2.1
+ --with-lua-interpreter=luajit)
elseif(USE_BUNDLED_LUA)
list(APPEND LUAROCKS_OPTS
--with-lua=${HOSTDEPS_INSTALL_DIR})
@@ -66,9 +70,23 @@ if(UNIX OR (MINGW AND CMAKE_CROSSCOMPILING))
find_package(LuaJit)
if(LUAJIT_FOUND)
list(APPEND LUAROCKS_OPTS
- --lua-version=5.1
--with-lua-include=${LUAJIT_INCLUDE_DIRS}
- --lua-suffix=jit)
+ --with-lua-interpreter=luajit)
+ endif()
+
+ # Get LUA_VERSION used with rocks output.
+ if(LUAJIT_FOUND)
+ set(LUA_EXE "luajit")
+ else()
+ set(LUA_EXE "lua")
+ endif()
+ execute_process(
+ COMMAND ${LUA_EXE} -e "print(string.sub(_VERSION, 5))"
+ OUTPUT_VARIABLE LUA_VERSION
+ ERROR_VARIABLE ERR
+ RESULT_VARIABLE RES)
+ if(NOT RES EQUAL 0)
+ message(FATAL_ERROR "Could not get LUA_VERSION with ${LUA_EXE}: ${ERR}")
endif()
endif()
@@ -89,7 +107,7 @@ elseif(MSVC OR MINGW)
/LUA ${DEPS_INSTALL_DIR}
/LIB ${DEPS_LIB_DIR}
/BIN ${DEPS_BIN_DIR}
- /INC ${DEPS_INSTALL_DIR}/include/luajit-2.0
+ /INC ${DEPS_INSTALL_DIR}/include/luajit-2.1
/P ${DEPS_INSTALL_DIR}/luarocks /TREE ${DEPS_INSTALL_DIR}
/SCRIPTS ${DEPS_BIN_DIR}
/CMOD ${DEPS_BIN_DIR}
@@ -111,7 +129,7 @@ if(USE_BUNDLED_LUAJIT)
elseif(USE_BUNDLED_LUA)
add_dependencies(luarocks lua)
endif()
-set(ROCKS_DIR ${HOSTDEPS_LIB_DIR}/luarocks/rocks)
+set(ROCKS_DIR ${HOSTDEPS_LIB_DIR}/luarocks/rocks-${LUA_VERSION})
# mpack
add_custom_command(OUTPUT ${ROCKS_DIR}/mpack
diff --git a/third-party/cmake/BuildLuv.cmake b/third-party/cmake/BuildLuv.cmake
index 967e0a1711..c2a2bbf083 100644
--- a/third-party/cmake/BuildLuv.cmake
+++ b/third-party/cmake/BuildLuv.cmake
@@ -55,7 +55,7 @@ endfunction()
set(LUV_SRC_DIR ${DEPS_BUILD_DIR}/src/luv)
set(LUV_INCLUDE_FLAGS
- "-I${DEPS_INSTALL_DIR}/include -I${DEPS_INSTALL_DIR}/include/luajit-2.0")
+ "-I${DEPS_INSTALL_DIR}/include -I${DEPS_INSTALL_DIR}/include/luajit-2.1")
# Replace luv default rockspec with the alternate one under the "rockspecs"
# directory
diff --git a/third-party/cmake/BuildUnibilium.cmake b/third-party/cmake/BuildUnibilium.cmake
index e9deeb4987..74c1cbddb0 100644
--- a/third-party/cmake/BuildUnibilium.cmake
+++ b/third-party/cmake/BuildUnibilium.cmake
@@ -40,6 +40,7 @@ else()
BUILD_COMMAND ${MAKE_PRG} CC=${DEPS_C_COMPILER}
PREFIX=${DEPS_INSTALL_DIR}
CFLAGS=-fPIC
+ LDFLAGS+=-static
INSTALL_COMMAND ${MAKE_PRG} PREFIX=${DEPS_INSTALL_DIR} install)
endif()
diff --git a/third-party/cmake/BuildUtf8proc.cmake b/third-party/cmake/BuildUtf8proc.cmake
new file mode 100644
index 0000000000..7297913f87
--- /dev/null
+++ b/third-party/cmake/BuildUtf8proc.cmake
@@ -0,0 +1,68 @@
+include(CMakeParseArguments)
+
+# BuildUtf8proc(CONFIGURE_COMMAND ... BUILD_COMMAND ... INSTALL_COMMAND ...)
+# Reusable function to build utf8proc, wraps ExternalProject_Add.
+# Failing to pass a command argument will result in no command being run
+function(BuildUtf8proc)
+ cmake_parse_arguments(_utf8proc
+ ""
+ ""
+ "CONFIGURE_COMMAND;BUILD_COMMAND;INSTALL_COMMAND"
+ ${ARGN})
+
+ if(NOT _utf8proc_CONFIGURE_COMMAND AND NOT _utf8proc_BUILD_COMMAND
+ AND NOT _utf8proc_INSTALL_COMMAND)
+ message(FATAL_ERROR "Must pass at least one of CONFIGURE_COMMAND, BUILD_COMMAND, INSTALL_COMMAND")
+ endif()
+
+ ExternalProject_Add(utf8proc
+ PREFIX ${DEPS_BUILD_DIR}
+ URL ${UTF8PROC_URL}
+ DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/utf8proc
+ DOWNLOAD_COMMAND ${CMAKE_COMMAND}
+ -DPREFIX=${DEPS_BUILD_DIR}
+ -DDOWNLOAD_DIR=${DEPS_DOWNLOAD_DIR}/utf8proc
+ -DURL=${UTF8PROC_URL}
+ -DEXPECTED_SHA256=${UTF8PROC_SHA256}
+ -DTARGET=utf8proc
+ -DUSE_EXISTING_SRC_DIR=${USE_EXISTING_SRC_DIR}
+ -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/DownloadAndExtractFile.cmake
+ CONFIGURE_COMMAND "${_utf8proc_CONFIGURE_COMMAND}"
+ BUILD_COMMAND "${_utf8proc_BUILD_COMMAND}"
+ INSTALL_COMMAND "${_utf8proc_INSTALL_COMMAND}")
+endfunction()
+
+set(UTF8PROC_CONFIGURE_COMMAND ${CMAKE_COMMAND} ${DEPS_BUILD_DIR}/src/utf8proc
+ -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR}
+ -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
+ -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
+ "-DCMAKE_C_FLAGS:STRING=${CMAKE_C_COMPILER_ARG1} -fPIC"
+ -DCMAKE_GENERATOR=${CMAKE_GENERATOR})
+
+set(UTF8PROC_BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${CMAKE_BUILD_TYPE})
+set(UTF8PROC_INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config ${CMAKE_BUILD_TYPE})
+
+if(MINGW AND CMAKE_CROSSCOMPILING)
+ get_filename_component(TOOLCHAIN ${CMAKE_TOOLCHAIN_FILE} REALPATH)
+ set(UTF8PROC_CONFIGURE_COMMAND ${CMAKE_COMMAND} ${DEPS_BUILD_DIR}/src/utf8proc
+ -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR}
+ # Pass toolchain
+ -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN}
+ -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
+ # Hack to avoid -rdynamic in Mingw
+ -DCMAKE_SHARED_LIBRARY_LINK_C_FLAGS="")
+elseif(MSVC)
+ # Same as Unix without fPIC
+ set(UTF8PROC_CONFIGURE_COMMAND ${CMAKE_COMMAND} ${DEPS_BUILD_DIR}/src/utf8proc
+ -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR}
+ -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
+ "-DCMAKE_C_FLAGS:STRING=${CMAKE_C_COMPILER_ARG1}"
+ -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
+ # Make sure we use the same generator, otherwise we may
+ # accidentaly end up using different MSVC runtimes
+ -DCMAKE_GENERATOR=${CMAKE_GENERATOR})
+endif()
+
+BuildUtf8proc(CONFIGURE_COMMAND ${UTF8PROC_CONFIGURE_COMMAND}
+ BUILD_COMMAND ${UTF8PROC_BUILD_COMMAND}
+ INSTALL_COMMAND ${UTF8PROC_INSTALL_COMMAND})