diff options
178 files changed, 5496 insertions, 1307 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2deadb0c4b..3565b20bfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ on: branches: - 'master' - 'release-[0-9]+.[0-9]+' + paths-ignore: + - 'runtime/doc/*' # Cancel any in-progress CI runs for a PR if it is updated concurrency: @@ -19,6 +21,7 @@ jobs: lint: if: (github.event_name == 'pull_request' && github.base_ref == 'master' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/master') runs-on: ubuntu-20.04 + timeout-minutes: 10 env: CC: gcc steps: @@ -64,9 +67,13 @@ jobs: uses: actions/cache@v2 with: path: | + ${{ env.CACHE_NVIM_DEPS_DIR }} ~/.ccache key: lint-${{ hashFiles('cmake/*', '**/CMakeLists.txt', '!third-party/**CMakeLists.txt') }}-${{ github.base_ref }} + - name: Build third-party + run: ./ci/before_script.sh + - name: Build nvim run: ./ci/run_tests.sh build @@ -205,7 +212,8 @@ jobs: run: ./ci/before_cache.sh windows: - runs-on: windows-2016 + runs-on: windows-2019 + timeout-minutes: 45 if: github.event.pull_request.draft == false env: DEPS_BUILD_DIR: ${{ format('{0}/nvim-deps', github.workspace) }} diff --git a/.github/workflows/env.sh b/.github/workflows/env.sh index d424924c27..bd170f92fb 100755 --- a/.github/workflows/env.sh +++ b/.github/workflows/env.sh @@ -46,7 +46,9 @@ CLANG_SANITIZER=TSAN EOF ;; lint) - BUILD_FLAGS="$BUILD_FLAGS -DLIBLUV_LIBRARY:FILEPATH=/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/lua/5.1/luv.so -DLIBLUV_INCLUDE_DIR:PATH=/usr/include/lua5.1" +# Re-enable once system deps are available +# BUILD_FLAGS="$BUILD_FLAGS -DLIBLUV_LIBRARY:FILEPATH=/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/lua/5.1/luv.so -DLIBLUV_INCLUDE_DIR:PATH=/usr/include/lua5.1" + DEPS_CMAKE_FLAGS="$DEPS_CMAKE_FLAGS -DUSE_BUNDLED_LUV=ON" cat <<EOF >> "$GITHUB_ENV" USE_BUNDLED=OFF CI_TARGET=lint diff --git a/.github/workflows/notes.md b/.github/workflows/notes.md index 9928d472b3..7181972696 100644 --- a/.github/workflows/notes.md +++ b/.github/workflows/notes.md @@ -6,8 +6,26 @@ ${NVIM_VERSION} ### Windows -1. Extract **nvim-win64.zip** -2. Run `nvim-qt.exe` +#### Zip + +1. Download **nvim-win64.zip** +2. Extract the zip. +3. Run `nvim-qt.exe` + +#### MSI + +1. Download **nvim-win64.msi** +2. Run the MSI +3. Add the Neovim location to your path. + - Default location is `C:\Program Files\Neovim` +4. Search and run `nvim-qt.exe` or run `nvim.exe` on your CLI of choice. + +#### NSIS + +1. Download **nvim-win64.exe** +2. Run the installer. + - Ensure that the option to add the installation location to your path is checked if it's your first installation. +3. Search and run `nvim-qt.exe` or run `nvim.exe` on your CLI of choice. ### macOS @@ -17,6 +35,19 @@ ${NVIM_VERSION} ### Linux (x64) +#### Tarball + +1. Download **nvim-linux64.tar.gz** +2. Extract: `tar xzvf nvim-linux64.tar.gz` +3. Run `./nvim-linux64/bin/nvim` + +#### Debian Package + +1. Download **nvim-linux64.deb** +2. Install the package using `sudo apt install ./nvim-linux64.deb` +3. Run `nvim` + +#### AppImage 1. Download **nvim.appimage** 2. Run `chmod u+x nvim.appimage && ./nvim.appimage` - If your system does not have FUSE you can [extract the appimage](https://github.com/AppImage/AppImageKit/wiki/FUSE#type-2-appimage): @@ -32,9 +63,12 @@ ${NVIM_VERSION} ## SHA256 Checksums ``` -${SHA_LINUX_64} +${SHA_LINUX_64_TAR} +${SHA_LINUX_64_DEB} ${SHA_APP_IMAGE} ${SHA_APP_IMAGE_ZSYNC} ${SHA_MACOS} -${SHA_WIN_64} +${SHA_WIN_64_ZIP} +${SHA_WIN_64_MSI} +${SHA_WIN_64_EXE} ``` diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b72c2ab71d..e954b57175 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,12 +39,17 @@ jobs: printf '::set-output name=version::%s\n' "$(./build/bin/nvim --version | head -n 3 | sed -z 's/\n/%0A/g')" printf '::set-output name=release::%s\n' "$(./build/bin/nvim --version | head -n 1)" make DESTDIR="$GITHUB_WORKSPACE/build/release/nvim-linux64" install - cd "$GITHUB_WORKSPACE/build/release" - tar cfz nvim-linux64.tar.gz nvim-linux64 + cd "$GITHUB_WORKSPACE/build/" + cpack -C $NVIM_BUILD_TYPE - uses: actions/upload-artifact@v2 with: name: nvim-linux64 - path: build/release/nvim-linux64.tar.gz + path: build/nvim-linux64.tar.gz + retention-days: 1 + - uses: actions/upload-artifact@v2 + with: + name: nvim-linux64 + path: build/nvim-linux64.deb retention-days: 1 appimage: @@ -114,7 +119,7 @@ jobs: retention-days: 1 windows: - runs-on: windows-2016 + runs-on: windows-2019 env: DEPS_BUILD_DIR: ${{ format('{0}/nvim-deps', github.workspace) }} DEPS_PREFIX: ${{ format('{0}/nvim-deps/usr', github.workspace) }} @@ -131,12 +136,21 @@ jobs: - run: powershell ci\build.ps1 -NoTests env: CONFIGURATION: ${{ matrix.config }} - - run: move build\Neovim.zip build\${{ matrix.archive }}.zip - uses: actions/upload-artifact@v2 with: name: ${{ matrix.archive }} path: build/${{ matrix.archive }}.zip retention-days: 1 + - uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.archive }} + path: build/${{ matrix.archive }}.msi + retention-days: 1 + - uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.archive }} + path: build/${{ matrix.archive }}.exe + retention-days: 1 publish: needs: [linux, appimage, macOS, windows] @@ -187,7 +201,9 @@ jobs: run: | cd ./nvim-linux64 sha256sum nvim-linux64.tar.gz > nvim-linux64.tar.gz.sha256sum - echo "SHA_LINUX_64=$(cat nvim-linux64.tar.gz.sha256sum)" >> $GITHUB_ENV + echo "SHA_LINUX_64_TAR=$(cat nvim-linux64.tar.gz.sha256sum)" >> $GITHUB_ENV + sha256sum nvim-linux64.deb > nvim-linux64.deb.sha256sum + echo "SHA_LINUX_64_DEB=$(cat nvim-linux64.deb.sha256sum)" >> $GITHUB_ENV - name: Generate App Image SHA256 checksums run: | cd ./appimage @@ -207,7 +223,11 @@ jobs: run: | cd ./nvim-win64 sha256sum nvim-win64.zip > nvim-win64.zip.sha256sum - echo "SHA_WIN_64=$(cat nvim-win64.zip.sha256sum)" >> $GITHUB_ENV + echo "SHA_WIN_64_ZIP=$(cat nvim-win64.zip.sha256sum)" >> $GITHUB_ENV + sha256sum nvim-win64.msi > nvim-win64.msi.sha256sum + echo "SHA_WIN_64_MSI=$(cat nvim-win64.msi.sha256sum)" >> $GITHUB_ENV + sha256sum nvim-win64.exe > nvim-win64.exe.sha256sum + echo "SHA_WIN_64_EXE=$(cat nvim-win64.exe.sha256sum)" >> $GITHUB_ENV - name: Publish release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 0988a51cd9..11479346c7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ compile_commands.json /.clangd/ /.cache/clangd/ /.ccls-cache/ +/.clang-tidy .DS_Store *.mo diff --git a/CMakeLists.txt b/CMakeLists.txt index e0f05e1205..eae2d75e3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -387,7 +387,7 @@ include_directories(SYSTEM ${LIBUV_INCLUDE_DIRS}) find_package(Msgpack 1.0.0 REQUIRED) include_directories(SYSTEM ${MSGPACK_INCLUDE_DIRS}) -find_package(LibLUV 1.30.0 REQUIRED) +find_package(LibLUV 1.43.0 REQUIRED) include_directories(SYSTEM ${LIBLUV_INCLUDE_DIRS}) find_package(TreeSitter REQUIRED) @@ -736,17 +736,6 @@ else() COMMENT "lualint: LUACHECK_PRG not defined") endif() -set(CPACK_PACKAGE_NAME "Neovim") -set(CPACK_PACKAGE_VENDOR "neovim.io") -set(CPACK_PACKAGE_VERSION ${NVIM_VERSION_MEDIUM}) -set(CPACK_PACKAGE_INSTALL_DIRECTORY "Neovim") -# Set toplevel directory/installer name as Neovim -set(CPACK_PACKAGE_FILE_NAME "Neovim") -set(CPACK_TOPLEVEL_TAG "Neovim") -set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") -set(CPACK_NSIS_MODIFY_PATH ON) -set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) -include(CPack) #add uninstall target if(NOT TARGET uninstall) @@ -758,3 +747,8 @@ if(NOT TARGET uninstall) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/UninstallHelper.cmake) endif() + + +if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) + add_subdirectory(packaging) +endif() @@ -119,7 +119,7 @@ Apache 2.0 license, except for contributions copied from Vim (identified by the [Managed packages]: https://github.com/neovim/neovim/wiki/Installing-Neovim#install-from-package [Debian]: https://packages.debian.org/testing/neovim [Ubuntu]: http://packages.ubuntu.com/search?keywords=neovim -[Fedora]: https://apps.fedoraproject.org/packages/neovim +[Fedora]: https://packages.fedoraproject.org/pkgs/neovim/neovim/ [Arch Linux]: https://www.archlinux.org/packages/?q=neovim [Void Linux]: https://voidlinux.org/packages/?arch=x86_64&q=neovim [Gentoo]: https://packages.gentoo.org/packages/app-editors/neovim diff --git a/ci/build.ps1 b/ci/build.ps1 index ef5ba3bf2d..c7c3b3d470 100644 --- a/ci/build.ps1 +++ b/ci/build.ps1 @@ -77,11 +77,16 @@ if ($compiler -eq 'MINGW') { } elseif ($compiler -eq 'MSVC') { $cmakeGeneratorArgs = '/verbosity:normal' - if ($bits -eq 32) { - $cmakeGenerator = 'Visual Studio 15 2017' - } - elseif ($bits -eq 64) { - $cmakeGenerator = 'Visual Studio 15 2017 Win64' + $cmakeGenerator = 'Visual Studio 16 2019' +} + +if ($compiler -eq 'MSVC') { + $installationPath = vswhere.exe -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if ($installationPath -and (test-path "$installationPath\Common7\Tools\vsdevcmd.bat")) { + & "${env:COMSPEC}" /s /c "`"$installationPath\Common7\Tools\vsdevcmd.bat`" -arch=x${bits} -no_logo && set" | foreach-object { + $name, $value = $_ -split '=', 2 + set-content env:\"$name" $value + } } } @@ -99,24 +104,35 @@ if (-not $NoTests) { npm.cmd link neovim } -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)" } } cd $env:DEPS_BUILD_DIR -cmake -G $cmakeGenerator $(convertToCmakeArgs($depsCmakeVars)) "$buildDir/third-party/" ; exitIfFailed +if ($compiler -eq 'MSVC') { + if ($bits -eq 32) { + cmake -G $cmakeGenerator -A Win32 $(convertToCmakeArgs($depsCmakeVars)) "$buildDir/third-party/" ; exitIfFailed + } else { + cmake -G $cmakeGenerator -A x64 $(convertToCmakeArgs($depsCmakeVars)) "$buildDir/third-party/" ; exitIfFailed + } +} else { + cmake -G $cmakeGenerator $(convertToCmakeArgs($depsCmakeVars)) "$buildDir/third-party/" ; exitIfFailed +} cmake --build . --config $cmakeBuildType -- $cmakeGeneratorArgs ; exitIfFailed cd $buildDir # Build Neovim mkdir build cd build -cmake -G $cmakeGenerator $(convertToCmakeArgs($nvimCmakeVars)) .. ; exitIfFailed +if ($compiler -eq 'MSVC') { + if ($bits -eq 32) { + cmake -G $cmakeGenerator -A Win32 $(convertToCmakeArgs($nvimCmakeVars)) .. ; exitIfFailed + } else { + cmake -G $cmakeGenerator -A x64 $(convertToCmakeArgs($nvimCmakeVars)) .. ; exitIfFailed + } +} else { + cmake -G $cmakeGenerator $(convertToCmakeArgs($nvimCmakeVars)) .. ; exitIfFailed +} cmake --build . --config $cmakeBuildType -- $cmakeGeneratorArgs ; exitIfFailed .\bin\nvim --version ; exitIfFailed @@ -167,7 +183,4 @@ if (Test-Path -Path $env:ChocolateyInstall\bin\cpack.exe) { } # Build artifacts -cpack -G ZIP -C RelWithDebInfo -if ($env:APPVEYOR_REPO_TAG_NAME -ne $null) { - cpack -G NSIS -C RelWithDebInfo -} +cpack -C $cmakeBuildType diff --git a/cmake/RunTests.cmake b/cmake/RunTests.cmake index 3adbcbbfc5..789131c26c 100644 --- a/cmake/RunTests.cmake +++ b/cmake/RunTests.cmake @@ -95,5 +95,7 @@ if(NOT res EQUAL 0) endif() endif() - message(FATAL_ERROR "${TEST_TYPE} tests failed with error: ${res}") + IF (NOT WIN32) + message(FATAL_ERROR "${TEST_TYPE} tests failed with error: ${res}") + ENDIF() endif() diff --git a/packaging/CMakeLists.txt b/packaging/CMakeLists.txt new file mode 100644 index 0000000000..6ae744c2cd --- /dev/null +++ b/packaging/CMakeLists.txt @@ -0,0 +1,78 @@ +set(CPACK_PACKAGE_NAME "Neovim") +set(CPACK_PACKAGE_VENDOR "neovim.io") +set(CPACK_PACKAGE_FILE_NAME "nvim") + +# From the GitHub About section +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Vim-fork focused on extensibility and usability.") + +set(CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME}) + +# Pull the versions defined with the top level CMakeLists.txt +set(CPACK_PACKAGE_VERSION_MAJOR ${NVIM_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${NVIM_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${NVIM_VERSION_PATCH}) + +# CPACK_VERBATIM_VARIABLES ensures that the variables prefixed with *CPACK_* +# are correctly passed to the cpack program. +# This should always be set to true. +set(CPACK_VERBATIM_VARIABLES TRUE) + +set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE.txt") +set(CPACK_RESOURCE_FILE_README ${PROJECT_SOURCE_DIR}/README.md) + + +if(WIN32) + set(CPACK_PACKAGE_FILE_NAME "nvim-win64") + set(CPACK_GENERATOR ZIP WIX NSIS) + + # WIX + # CPACK_WIX_UPGRADE_GUID should be set, but should never change. + # CPACK_WIX_PRODUCT_GUID should not be set (leave as default to auto-generate). + + # The following guid is just a randomly generated guid that's been pasted here. + # It has no special meaning other than to supply it to WIX. + set(CPACK_WIX_UPGRADE_GUID "207A1A70-7B0C-418A-A153-CA6883E38F4D") + set(CPACK_WIX_PRODUCT_ICON ${CMAKE_CURRENT_LIST_DIR}/logo.ico) + + # NSIS + # install icon + set(CPACK_NSIS_MUI_ICON ${CMAKE_CURRENT_LIST_DIR}/logo.ico) + + # uninstall icon + set(CPACK_NSIS_MUI_UNIICON ${CMAKE_CURRENT_LIST_DIR}/logo.ico) + + # icon that appears when you search in Add/Remove programs + set(CPACK_NSIS_INSTALLED_ICON_NAME ${CMAKE_CURRENT_LIST_DIR}/logo.ico) + + # name that appears in the list in Add/Remove programs + set(CPACK_NSIS_DISPLAY_NAME "Neovim") + + # name used in various installer UI locations, such as the title + set(CPACK_NSIS_PACKAGE_NAME "Neovim") + + # Allow the user to modify their path to include neovim during + # the installation process. + set(CPACK_NSIS_MODIFY_PATH TRUE) +elseif(APPLE) + set(CPACK_PACKAGE_FILE_NAME "nvim-macos") + set(CPACK_GENERATOR TGZ) + set(CPACK_PACKAGE_ICON ${CMAKE_CURRENT_LIST_DIR}/logo.icns) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(CPACK_PACKAGE_FILE_NAME "nvim-linux64") + set(CPACK_GENERATOR TGZ DEB) + set(CPACK_DEBIAN_PACKAGE_NAME "Neovim") # required + set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Neovim.io") # required + + # Automatically compute required shared lib dependencies. + # Unfortunately, you "just need to know" that this has a hidden + # dependency on dpkg-shlibdeps whilst using a debian based host. + set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS TRUE) +else() + set(CPACK_GENERATOR TGZ) +endif() + +# CPack variables are loaded in on the call to include(CPack). If you set +# variables *after* the inclusion, they don't get updated within the CPack +# config. Note that some CPack commands should still be run after it, such +# as cpack_add_component(). +include(CPack) diff --git a/packaging/logo.icns b/packaging/logo.icns Binary files differnew file mode 100644 index 0000000000..a6377d9cdb --- /dev/null +++ b/packaging/logo.icns diff --git a/packaging/logo.ico b/packaging/logo.ico Binary files differnew file mode 100644 index 0000000000..a4523501b9 --- /dev/null +++ b/packaging/logo.ico diff --git a/packaging/logo.svg b/packaging/logo.svg new file mode 100644 index 0000000000..e8aa8bd33e --- /dev/null +++ b/packaging/logo.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 54 65" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#D" x=".5" y=".5"/><defs><linearGradient x1="50.00%" y1="0.00%" x2="50.00%" y2="100.00%" id="A"><stop stop-color="#16b0ed" stop-opacity=".8" offset="0%"/><stop stop-color="#0f59b2" stop-opacity=".837" offset="100%"/></linearGradient><linearGradient x1="50.00%" y1="0.00%" x2="50.00%" y2="100.00%" id="B"><stop stop-color="#7db643" offset="0%"/><stop stop-color="#367533" offset="100%"/></linearGradient><linearGradient x1="50.00%" y1="-0.00%" x2="50.00%" y2="100.01%" id="C"><stop stop-color="#88c649" stop-opacity=".8" offset="0%"/><stop stop-color="#439240" stop-opacity=".84" offset="100%"/></linearGradient></defs><symbol id="D" overflow="visible"><g stroke="none"><path d="M0 13.761L13.63 0v63.983L0 50.381z" fill="url(#A)"/><path d="M52.664 13.894L38.848.008l.281 63.976 13.63-13.602z" fill="url(#B)"/><path d="M13.621.011l35.435 54.07-9.916 9.915L3.686 10.046z" fill="url(#C)"/><path d="M13.633 25.092l-.019 2.13L2.676 11.069l1.013-1.032z" fill="#000" fill-opacity=".13"/></g></symbol></svg>
\ No newline at end of file diff --git a/runtime/autoload/dist/ft.vim b/runtime/autoload/dist/ft.vim index aacecc521e..54cb6ce70f 100644 --- a/runtime/autoload/dist/ft.vim +++ b/runtime/autoload/dist/ft.vim @@ -1,7 +1,7 @@ " Vim functions for file type detection " " Maintainer: Bram Moolenaar <Bram@vim.org> -" Last Change: 2022 Jan 31 +" Last Change: 2022 Feb 22 " These functions are moved here from runtime/filetype.vim to make startup " faster. @@ -211,6 +211,10 @@ func dist#ft#EuphoriaCheck() endfunc func dist#ft#DtraceCheck() + if did_filetype() + " Filetype was already detected + return + endif let lines = getline(1, min([line("$"), 100])) if match(lines, '^module\>\|^import\>') > -1 " D files often start with a module and/or import statement. diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 1e1534c31f..cff616cf59 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -651,6 +651,9 @@ nvim_add_user_command({name}, {command}, {*opts}) that contains the following keys: • args: (string) The args passed to the command, if any |<args>| + • fargs: (table) The args split by unescaped + whitespace (when more than one argument is + allowed), if any |<f-args>| • bang: (boolean) "true" if the command was executed with a ! modifier |<bang>| • line1: (number) The starting line of the @@ -2152,6 +2155,29 @@ nvim_buf_get_option({buffer}, {name}) *nvim_buf_get_option()* Return: ~ Option value + *nvim_buf_get_text()* +nvim_buf_get_text({buffer}, {start_row}, {start_col}, {end_row}, {end_col}, + {opts}) + Gets a range from the buffer. + + This differs from |nvim_buf_get_lines()| in that it allows + retrieving only portions of a line. + + Indexing is zero-based. Column indices are end-exclusive. + + Prefer |nvim_buf_get_lines()| when retrieving entire lines. + + Parameters: ~ + {buffer} Buffer handle, or 0 for current buffer + {start_row} First line index + {start_col} Starting byte offset of first line + {end_row} Last line index + {end_col} Ending byte offset of last line (exclusive) + {opts} Optional parameters. Currently unused. + + Return: ~ + Array of lines, or empty array for unloaded buffer. + nvim_buf_get_var({buffer}, {name}) *nvim_buf_get_var()* Gets a buffer-scoped (b:) variable. diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index dbe70b84cf..9054bedc8d 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -1406,7 +1406,7 @@ Examples for reading and writing compressed files: > : autocmd BufReadPre,FileReadPre *.gz set bin : autocmd BufReadPost,FileReadPost *.gz '[,']!gunzip : autocmd BufReadPost,FileReadPost *.gz set nobin - : autocmd BufReadPost,FileReadPost *.gz execute ":doautocmd BufReadPost " . expand("%:r") + : autocmd BufReadPost,FileReadPost *.gz execute ":doautocmd BufReadPost " .. expand("%:r") : autocmd BufWritePost,FileWritePost *.gz !mv <afile> <afile>:r : autocmd BufWritePost,FileWritePost *.gz !gzip <afile>:r @@ -1505,7 +1505,7 @@ To insert the current date and time in a *.html file when writing it: > : else : let l = line("$") : endif - : exe "1," . l . "g/Last modified: /s/Last modified: .*/Last modified: " . + : exe "1," .. l .. "g/Last modified: /s/Last modified: .*/Last modified: " .. : \ strftime("%Y %b %d") :endfun diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index 35a232c0c2..03a5f98c6d 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -693,7 +693,7 @@ argv([{nr} [, {winid}]]) :let i = 0 :while i < argc() : let f = escape(fnameescape(argv(i)), '.') - : exe 'amenu Arg.' . f . ' :e ' . f . '<CR>' + : exe 'amenu Arg.' .. f .. ' :e ' .. f .. '<CR>' : let i = i + 1 :endwhile < Without the {nr} argument, or when {nr} is -1, a |List| with @@ -895,7 +895,7 @@ bufwinid({buf}) *bufwinid()* see |bufname()| above. If buffer {buf} doesn't exist or there is no such window, -1 is returned. Example: > - echo "A window containing buffer 1 is " . (bufwinid(1)) + echo "A window containing buffer 1 is " .. (bufwinid(1)) < Only deals with the current tab page. @@ -908,7 +908,7 @@ bufwinnr({buf}) *bufwinnr()* If buffer {buf} doesn't exist or there is no such window, -1 is returned. Example: > - echo "A window containing buffer 1 is " . (bufwinnr(1)) + echo "A window containing buffer 1 is " .. (bufwinnr(1)) < The number can be used with |CTRL-W_w| and ":wincmd w" |:wincmd|. @@ -955,7 +955,7 @@ byteidx({expr}, {nr}) *byteidx()* byteidxcomp({expr}, {nr}) *byteidxcomp()* Like byteidx(), except that a composing character is counted as a separate character. Example: > - let s = 'e' . nr2char(0x301) + let s = 'e' .. nr2char(0x301) echo byteidx(s, 1) echo byteidxcomp(s, 1) echo byteidxcomp(s, 2) @@ -1152,7 +1152,7 @@ col({expr}) The result is a Number, which is the byte index of the column col(".") column of cursor col("$") length of cursor line plus one col("'t") column of mark t - col("'" . markname) column of mark markname + col("'" .. markname) column of mark markname < The first column is 1. 0 is returned for an error. For an uppercase mark the column may actually be in another buffer. @@ -1161,7 +1161,7 @@ col({expr}) The result is a Number, which is the byte index of the column line. This can be used to obtain the column in Insert mode: > :imap <F2> <C-O>:let save_ve = &ve<CR> \<C-O>:set ve=all<CR> - \<C-O>:echo col(".") . "\n" <Bar> + \<C-O>:echo col(".") .. "\n" <Bar> \let &ve = save_ve<CR> < Can also be used as a |method|: > @@ -1807,6 +1807,7 @@ exists({expr}) The result is a Number, which is |TRUE| if {expr} is exists("$HOSTNAME") exists("*strftime") exists("*s:MyFunc") + exists("*MyFunc") exists("bufcount") exists(":Make") exists("#CursorHold") @@ -1898,12 +1899,12 @@ expand({string} [, {nosuf} [, {list}]]) *expand()* :e extension only Example: > - :let &tags = expand("%:p:h") . "/tags" + :let &tags = expand("%:p:h") .. "/tags" < Note that when expanding a string that starts with '%', '#' or '<', any following text is ignored. This does NOT work: > :let doesntwork = expand("%:h.bak") < Use this: > - :let doeswork = expand("%:h") . ".bak" + :let doeswork = expand("%:h") .. ".bak" < Also note that expanding "<cfile>" and others only returns the referenced file name without further expansion. If "<cfile>" is "~/.cshrc", you need to do another expand() to have the @@ -2239,7 +2240,7 @@ fnameescape({string}) *fnameescape()* and |:write|). And a "-" by itself (special after |:cd|). Example: > :let fname = '+some str%nge|name' - :exe "edit " . fnameescape(fname) + :exe "edit " .. fnameescape(fname) < results in executing: > edit \+some\ str\%nge\|name < @@ -2394,7 +2395,7 @@ function({name} [, {arglist}] [, {dict}]) < The Dictionary is only useful when calling a "dict" function. In that case the {dict} is passed in as "self". Example: > function Callback() dict - echo "called for " . self.name + echo "called for " .. self.name endfunction ... let context = {"name": "example"} @@ -2577,7 +2578,7 @@ getbufvar({buf}, {varname} [, {def}]) *getbufvar()* string is returned, there is no error message. Examples: > :let bufmodified = getbufvar(1, "&mod") - :echo "todo myvar = " . getbufvar("todo", "myvar") + :echo "todo myvar = " .. getbufvar("todo", "myvar") < Can also be used as a |method|: > GetBufnr()->getbufvar(varname) @@ -2638,9 +2639,9 @@ getchar([expr]) *getchar()* This example positions the mouse as it would normally happen: > let c = getchar() if c == "\<LeftMouse>" && v:mouse_win > 0 - exe v:mouse_win . "wincmd w" + exe v:mouse_win .. "wincmd w" exe v:mouse_lnum - exe "normal " . v:mouse_col . "|" + exe "normal " .. v:mouse_col .. "|" endif < There is no prompt, you will somehow have to make clear to the @@ -3373,7 +3374,7 @@ gettabwinvar({tabnr}, {winnr}, {varname} [, {def}]) *gettabwinvar()* empty string is returned, there is no error message. Examples: > :let list_is_on = gettabwinvar(1, 2, '&list') - :echo "myvar = " . gettabwinvar(3, 1, 'myvar') + :echo "myvar = " .. gettabwinvar(3, 1, 'myvar') < To obtain all window-local variables use: > gettabwinvar({tabnr}, {winnr}, '&') @@ -3488,7 +3489,7 @@ getwinvar({winnr}, {varname} [, {def}]) *getwinvar()* Like |gettabwinvar()| for the current tabpage. Examples: > :let list_is_on = getwinvar(2, '&list') - :echo "myvar = " . getwinvar(1, 'myvar') + :echo "myvar = " .. getwinvar(1, 'myvar') < Can also be used as a |method|: > GetWinnr()->getwinvar(varname) @@ -3757,7 +3758,7 @@ histdel({history} [, {item}]) *histdel()* The following three are equivalent: > :call histdel("search", histnr("search")) :call histdel("search", -1) - :call histdel("search", '^'.histget("search", -1).'$') + :call histdel("search", '^' .. histget("search", -1) .. '$') < To delete the last search pattern and use the last-but-one for the "n" command and 'hlsearch': > @@ -3776,7 +3777,7 @@ histget({history} [, {index}]) *histget()* Examples: Redo the second last search from history. > - :execute '/' . histget("search", -2) + :execute '/' .. histget("search", -2) < Define an Ex command ":H {num}" that supports re-execution of the {num}th entry from the output of |:history|. > @@ -3940,11 +3941,11 @@ input({opts}) let lvl = 0 while i < len(a:cmdline) if a:cmdline[i] is# '(' - call add(ret, [i, i + 1, 'RBP' . ((lvl % g:rainbow_levels) + 1)]) + call add(ret, [i, i + 1, 'RBP' .. ((lvl % g:rainbow_levels) + 1)]) let lvl += 1 elseif a:cmdline[i] is# ')' let lvl -= 1 - call add(ret, [i, i + 1, 'RBP' . ((lvl % g:rainbow_levels) + 1)]) + call add(ret, [i, i + 1, 'RBP' .. ((lvl % g:rainbow_levels) + 1)]) endif let i += 1 endwhile @@ -3974,7 +3975,7 @@ input({opts}) |:execute| or |:normal|. Example with a mapping: > - :nmap \x :call GetFoo()<CR>:exe "/" . Foo<CR> + :nmap \x :call GetFoo()<CR>:exe "/" .. Foo<CR> :function GetFoo() : call inputsave() : let g:Foo = input("enter search pattern: ") @@ -4133,7 +4134,7 @@ items({dict}) *items()* order. Also see |keys()| and |values()|. Example: > for [key, value] in items(mydict) - echo key . ': ' . value + echo key .. ': ' .. value endfor < Can also be used as a |method|: > @@ -4269,7 +4270,7 @@ join({list} [, {sep}]) *join()* {sep} is omitted a single space is used. Note that {sep} is not added at the end. You might want to add it there too: > - let lines = join(mylist, "\n") . "\n" + let lines = join(mylist, "\n") .. "\n" < String items are used as-is. |Lists| and |Dictionaries| are converted into a string like with |string()|. The opposite function is |split()|. @@ -4418,7 +4419,7 @@ line({expr} [, {winid}]) *line()* line(".") line number of the cursor line(".", winid) idem, in window "winid" line("'t") line number of mark t - line("'" . marker) line number of mark marker + line("'" .. marker) line number of mark marker < To jump to the last known position when opening a file see |last-position-jump|. @@ -4519,7 +4520,7 @@ map({expr1}, {expr2}) *map()* the current item. For a |Blob| |v:key| has the index of the current byte. Example: > - :call map(mylist, '"> " . v:val . " <"') + :call map(mylist, '"> " .. v:val .. " <"') < This puts "> " before and " <" after each item in "mylist". Note that {expr2} is the result of an expression and is then @@ -4533,19 +4534,19 @@ map({expr1}, {expr2}) *map()* The function must return the new value of the item. Example that changes each value by "key-value": > func KeyValue(key, val) - return a:key . '-' . a:val + return a:key .. '-' .. a:val endfunc call map(myDict, function('KeyValue')) < It is shorter when using a |lambda|: > - call map(myDict, {key, val -> key . '-' . val}) + call map(myDict, {key, val -> key .. '-' .. val}) < If you do not use "val" you can leave it out: > - call map(myDict, {key -> 'item: ' . key}) + call map(myDict, {key -> 'item: ' .. key}) < If you do not use "key" you can use a short name: > - call map(myDict, {_, val -> 'item: ' . val}) + call map(myDict, {_, val -> 'item: ' .. val}) < The operation is done in-place. If you want a |List| or |Dictionary| to remain unmodified make a copy first: > - :let tlist = map(copy(mylist), ' v:val . "\t"') + :let tlist = map(copy(mylist), ' v:val .. "\t"') < Returns {expr1}, the |List|, |Blob| or |Dictionary| that was filtered. When an error is encountered while evaluating @@ -4611,7 +4612,7 @@ maparg({name} [, {mode} [, {abbr} [, {dict}]]]) *maparg()* then the global mappings. This function can be used to map a key even when it's already mapped, and have it do the original mapping too. Sketch: > - exe 'nnoremap <Tab> ==' . maparg('<Tab>', 'n') + exe 'nnoremap <Tab> ==' .. maparg('<Tab>', 'n') < Can also be used as a |method|: > GetKey()->maparg('n') @@ -5084,7 +5085,7 @@ mkdir({name} [, {path} [, {prot}]]) {prot} is applied for all parts of {name}. Thus if you create /tmp/foo/bar then /tmp/foo will be created with 0o700. Example: > - :call mkdir($HOME . "/tmp/foo/bar", "p", 0o700) + :call mkdir($HOME .. "/tmp/foo/bar", "p", 0o700) < This function is not available in the |sandbox|. @@ -5584,7 +5585,7 @@ prompt_setcallback({buf}, {expr}) *prompt_setcallback()* stopinsert close else - call append(line('$') - 1, 'Entered: "' . a:text . '"') + call append(line('$') - 1, 'Entered: "' .. a:text .. '"') " Reset 'modified' to allow the buffer to be closed. set nomodified endif @@ -5731,7 +5732,7 @@ readdir({directory} [, {expr}]) function! s:tree(dir) return {a:dir : map(readdir(a:dir), \ {_, x -> isdirectory(x) ? - \ {x : s:tree(a:dir . '/' . x)} : x})} + \ {x : s:tree(a:dir .. '/' .. x)} : x})} endfunction echo s:tree(".") < @@ -5863,16 +5864,22 @@ reltimestr({time}) *reltimestr()* < *remote_expr()* *E449* remote_expr({server}, {string} [, {idvar} [, {timeout}]]) - Send the {string} to {server}. The string is sent as an - expression and the result is returned after evaluation. - The result must be a String or a |List|. A |List| is turned - into a String by joining the items with a line break in - between (not at the end), like with join(expr, "\n"). + Send the {string} to {server}. The {server} argument is a + string, also see |{server}|. + + The string is sent as an expression and the result is returned + after evaluation. The result must be a String or a |List|. A + |List| is turned into a String by joining the items with a + line break in 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. + If {timeout} is given the read times out after this many seconds. Otherwise a timeout of 600 seconds is used. + See also |clientserver| |RemoteReply|. This function is not available in the |sandbox|. Note: Any errors will cause a local error message to be issued @@ -5890,7 +5897,7 @@ remote_expr({server}, {string} [, {idvar} [, {timeout}]]) remote_foreground({server}) *remote_foreground()* Move the Vim server with the name {server} to the foreground. - The {server} argument is a string. + The {server} argument is a string, also see |{server}|. This works like: > remote_expr({server}, "foreground()") < Except that on Win32 systems the client does the work, to work @@ -5913,7 +5920,7 @@ remote_peek({serverid} [, {retvar}]) *remote_peek()* This function is not available in the |sandbox|. Examples: > :let repl = "" - :echo "PEEK: ".remote_peek(id, "repl").": ".repl + :echo "PEEK: " .. remote_peek(id, "repl") .. ": " .. repl remote_read({serverid}, [{timeout}]) *remote_read()* Return the oldest available reply from {serverid} and consume @@ -5926,24 +5933,29 @@ remote_read({serverid}, [{timeout}]) *remote_read()* < *remote_send()* *E241* remote_send({server}, {string} [, {idvar}]) - Send the {string} to {server}. The string is sent as input - keys and the function returns immediately. At the Vim server - the keys are not mapped |:map|. + Send the {string} to {server}. The {server} argument is a + string, also see |{server}|. + + The string is sent as input keys and the function returns + immediately. At the Vim server the keys are not mapped + |:map|. + If {idvar} is present, it is taken as the name of a variable and a {serverid} for later use with remote_read() is stored there. + See also |clientserver| |RemoteReply|. This function is not available in the |sandbox|. Note: Any errors will be reported in the server and may mess up the display. Examples: > - :echo remote_send("gvim", ":DropAndReply ".file, "serverid"). + :echo remote_send("gvim", ":DropAndReply " .. file, "serverid") .. \ remote_read(serverid) :autocmd NONE RemoteReply * \ echo remote_read(expand("<amatch>")) - :echo remote_send("gvim", ":sleep 10 | echo ". + :echo remote_send("gvim", ":sleep 10 | echo " .. \ 'server2client(expand("<client>"), "HELLO")<CR>') < *remote_startserver()* *E941* *E942* @@ -5960,7 +5972,7 @@ remove({list}, {idx} [, {end}]) *remove()* points to an item before {idx} this is an error. See |list-index| for possible values of {idx} and {end}. Example: > - :echo "last item: " . remove(mylist, -1) + :echo "last item: " .. remove(mylist, -1) :call remove(mylist, 0, 9) < Use |delete()| to remove a file. @@ -5976,13 +5988,13 @@ remove({blob}, {idx} [, {end}]) byte as {end} a |Blob| with one byte is returned. When {end} points to a byte before {idx} this is an error. Example: > - :echo "last byte: " . remove(myblob, -1) + :echo "last byte: " .. remove(myblob, -1) :call remove(mylist, 0, 9) remove({dict}, {key}) Remove the entry from {dict} with key {key} and return it. Example: > - :echo "removed " . remove(dict, "one") + :echo "removed " .. remove(dict, "one") < If there is no {key} in {dict} this is an error. rename({from}, {to}) *rename()* @@ -6123,7 +6135,7 @@ screencol() *screencol()* column inside the command line, which is 1 when the command is executed. To get the cursor position in the file use one of the following mappings: > - nnoremap <expr> GG ":echom ".screencol()."\n" + nnoremap <expr> GG ":echom " .. screencol() .. "\n" nnoremap <silent> GG :echom screencol()<CR> noremap GG <Cmd>echom screencol()<Cr> < @@ -6244,7 +6256,7 @@ search({pattern} [, {flags} [, {stopline} [, {timeout} [, {skip}]]]]) Example (goes over all files in the argument list): > :let n = 1 :while n <= argc() " loop over all files in arglist - : exe "argument " . n + : exe "argument " .. n : " start at the last char in the file and wrap for the : " first search to find match at start of file : normal G$ @@ -6328,11 +6340,11 @@ searchcount([{options}]) *searchcount()* return printf(' /%s [%d/%d]', @/, \ result.current, result.total) endfunction - let &statusline .= '%{LastSearchCount()}' + let &statusline ..= '%{LastSearchCount()}' " Or if you want to show the count only when " 'hlsearch' was on - " let &statusline .= + " let &statusline ..= " \ '%{v:hlsearch ? LastSearchCount() : ""}' < You can also update the search count, which can be useful in a @@ -7134,10 +7146,10 @@ shellescape({string} [, {special}]) *shellescape()* inside single quotes. Example of use with a |:!| command: > - :exe '!dir ' . shellescape(expand('<cfile>'), 1) + :exe '!dir ' .. shellescape(expand('<cfile>'), 1) < This results in a directory listing for the file under the cursor. Example of use with |system()|: > - :call system("chmod +w -- " . shellescape(expand("%"))) + :call system("chmod +w -- " .. shellescape(expand("%"))) < See also |::S|. Can also be used as a |method|: > @@ -7829,7 +7841,7 @@ substitute({string}, {pat}, {sub}, {flags}) *substitute()* When {sub} starts with "\=", the remainder is interpreted as an expression. See |sub-replace-expression|. Example: > :echo substitute(s, '%\(\x\x\)', - \ '\=nr2char("0x" . submatch(1))', 'g') + \ '\=nr2char("0x" .. submatch(1))', 'g') < When {sub} is a Funcref that function is called, with one optional argument. Example: > @@ -7837,7 +7849,7 @@ substitute({string}, {pat}, {sub}, {flags}) *substitute()* < The optional argument is a list which contains the whole matched string and up to nine submatches, like what |submatch()| returns. Example: > - :echo substitute(s, '%\(\x\x\)', {m -> '0x' . m[1]}, 'g') + :echo substitute(s, '%\(\x\x\)', {m -> '0x' .. m[1]}, 'g') < Can also be used as a |method|: > GetString()->substitute(pat, sub, flags) @@ -8155,7 +8167,7 @@ tempname() *tempname()* *temp-file-name* The result is a String, which is the name of a file that doesn't exist. It can be used for a temporary file. Example: > :let tmpfile = tempname() - :exe "redir > " . tmpfile + :exe "redir > " .. tmpfile < For Unix, the file will be in a private directory |tempfile|. For MS-Windows forward slashes are used when the 'shellslash' option is set or when 'shellcmdflag' starts with '-'. @@ -8331,7 +8343,7 @@ trim({text} [, {mask} [, {dir}]]) *trim()* Examples: > echo trim(" some text ") < returns "some text" > - echo trim(" \r\t\t\r RESERVE \t\n\x0B\xA0") . "_TAIL" + echo trim(" \r\t\t\r RESERVE \t\n\x0B\xA0") .. "_TAIL" < returns "RESERVE_TAIL" > echo trim("rm<Xrm<>X>rrm", "rm<>") < returns "Xrm<>X" (characters in the middle are not removed) > @@ -8509,7 +8521,7 @@ visualmode([{expr}]) *visualmode()* character-wise, line-wise, or block-wise Visual mode respectively. Example: > - :exe "normal " . visualmode() + :exe "normal " .. visualmode() < This enters the same Visual mode as before. It is also useful in scripts if you wish to act differently depending on the Visual mode that was used. @@ -8696,7 +8708,7 @@ winbufnr({nr}) The result is a Number, which is the number of the buffer window is returned. When window {nr} doesn't exist, -1 is returned. Example: > - :echo "The file in the current window is " . bufname(winbufnr(0)) + :echo "The file in the current window is " .. bufname(winbufnr(0)) < Can also be used as a |method|: > FindWindow()->winbufnr()->bufname() @@ -8721,7 +8733,7 @@ winheight({nr}) *winheight()* An existing window always has a height of zero or more. This excludes any window toolbar line. Examples: > - :echo "The current window has " . winheight(0) . " lines." + :echo "The current window has " .. winheight(0) .. " lines." < Can also be used as a |method|: > GetWinid()->winheight() @@ -8858,7 +8870,7 @@ winwidth({nr}) *winwidth()* returned. When window {nr} doesn't exist, -1 is returned. An existing window always has a width of zero or more. Examples: > - :echo "The current window has " . winwidth(0) . " columns." + :echo "The current window has " .. winwidth(0) .. " columns." :if winwidth(0) <= 50 : 50 wincmd | :endif diff --git a/runtime/doc/change.txt b/runtime/doc/change.txt index b4d3304880..b170f7cf65 100644 --- a/runtime/doc/change.txt +++ b/runtime/doc/change.txt @@ -906,7 +906,7 @@ Consider using a character like "@" or ":". There is no problem if the result of the expression contains the separation character. Examples: > - :s@\n@\="\r" . expand("$HOME") . "\r"@ + :s@\n@\="\r" .. expand("$HOME") .. "\r"@ This replaces an end-of-line with a new line containing the value of $HOME. > s/E/\="\<Char-0x20ac>"/g @@ -1065,7 +1065,7 @@ inside of strings can change! Also see 'softtabstop' option. > the command. You need to escape the '|' and '"' characters to prevent them from terminating the command. Example: > - :put ='path' . \",/test\" + :put ='path' .. \",/test\" < If there is no expression after '=', Vim uses the previous expression. You can see it with ":dis =". diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt index f2f6ebb2c9..c6e4bf003f 100644 --- a/runtime/doc/cmdline.txt +++ b/runtime/doc/cmdline.txt @@ -207,7 +207,7 @@ CTRL-\ e {expr} *c_CTRL-\_e* Example: > :cmap <F7> <C-\>eAppendSome()<CR> :func AppendSome() - :let cmd = getcmdline() . " Some()" + :let cmd = getcmdline() .. " Some()" :" place the cursor on the ) :call setcmdpos(strlen(cmd)) :return cmd diff --git a/runtime/doc/diff.txt b/runtime/doc/diff.txt index abe99102ee..9c5792dd43 100644 --- a/runtime/doc/diff.txt +++ b/runtime/doc/diff.txt @@ -371,13 +371,13 @@ Example (this does almost the same as 'diffexpr' being empty): > function MyDiff() let opt = "" if &diffopt =~ "icase" - let opt = opt . "-i " + let opt = opt .. "-i " endif if &diffopt =~ "iwhite" - let opt = opt . "-b " + let opt = opt .. "-b " endif - silent execute "!diff -a --binary " . opt . v:fname_in . " " . v:fname_new . - \ " > " . v:fname_out + silent execute "!diff -a --binary " .. opt .. v:fname_in .. " " .. v:fname_new .. + \ " > " .. v:fname_out redraw! endfunction @@ -427,8 +427,8 @@ Example (this does the same as 'patchexpr' being empty): > set patchexpr=MyPatch() function MyPatch() - :call system("patch -o " . v:fname_out . " " . v:fname_in . - \ " < " . v:fname_diff) + :call system("patch -o " .. v:fname_out .. " " .. v:fname_in .. + \ " < " .. v:fname_diff) endfunction Make sure that using the "patch" program doesn't have unwanted side effects. diff --git a/runtime/doc/editing.txt b/runtime/doc/editing.txt index 8ddc661c0e..bfa01f45a7 100644 --- a/runtime/doc/editing.txt +++ b/runtime/doc/editing.txt @@ -196,7 +196,7 @@ If you want to keep the changed buffer without saving it, switch on the Edit {file} always. Discard any changes to the current buffer. Also see |++opt| and |+cmd|. - + *:edit_#* *:e#* :e[dit] [++opt] [+cmd] #[count] Edit the [count]th buffer (as shown by |:files|). This command does the same as [count] CTRL-^. But ":e @@ -356,7 +356,7 @@ as a wildcard when "[" is in the 'isfname' option. A simple way to avoid this is to use "path\[[]abc]", this matches the file "path\[abc]". *starstar-wildcard* -Expanding "**" is possible on Unix, Win32, Mac OS/X and a few other systems. +Expanding "**" is possible on Unix, Win32, macOS and a few other systems. This allows searching a directory tree. This goes up to 100 directories deep. Note there are some commands where this works slightly differently, see |file-searching|. @@ -411,9 +411,9 @@ does apply like to other wildcards. Environment variables in the expression are expanded when evaluating the expression, thus this works: > - :e `=$HOME . '/.vimrc'` + :e `=$HOME .. '/.vimrc'` This does not work, $HOME is inside a string and used literally: > - :e `='$HOME' . '/.vimrc'` + :e `='$HOME' .. '/.vimrc'` If the expression returns a string then names are to be separated with line breaks. When the result is a |List| then each item is used as a name. Line @@ -1495,7 +1495,7 @@ which version of the file you want to keep. The accuracy of the time check depends on the filesystem. On Unix it is usually sub-second. With old file sytems and on MS-Windows it is normally one -second. Use has('nanotime') check if sub-second time stamp checks are +second. Use `has('nanotime')` to check if sub-second time stamp checks are available. There is one situation where you get the message while there is nothing wrong: diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index fc422f13e5..3015e232a7 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -402,7 +402,7 @@ It is also possible to put remaining items in a List variable: > :for [i, j; rest] in listlist : call Doit(i, j) : if !empty(rest) - : echo "remainder: " . string(rest) + : echo "remainder: " .. string(rest) : endif :endfor @@ -430,11 +430,11 @@ Functions that are useful with a List: > :let list = split("a b c") " create list from items in a string :let string = join(list, ', ') " create string from list items :let s = string(list) " String representation of list - :call map(list, '">> " . v:val') " prepend ">> " to each item + :call map(list, '">> " .. v:val') " prepend ">> " to each item Don't forget that a combination of features can make things simple. For example, to add up all the numbers in a list: > - :exe 'let sum = ' . join(nrlist, '+') + :exe 'let sum = ' .. join(nrlist, '+') 1.4 Dictionaries ~ @@ -496,7 +496,7 @@ turn the Dictionary into a List and pass it to |:for|. Most often you want to loop over the keys, using the |keys()| function: > :for key in keys(mydict) - : echo key . ': ' . mydict[key] + : echo key .. ': ' .. mydict[key] :endfor The List of keys is unsorted. You may want to sort them first: > @@ -504,13 +504,13 @@ The List of keys is unsorted. You may want to sort them first: > To loop over the values use the |values()| function: > :for v in values(mydict) - : echo "value: " . v + : echo "value: " .. v :endfor If you want both the key and the value use the |items()| function. It returns a List in which each item is a List with two items, the key and the value: > :for [key, value] in items(mydict) - : echo key . ': ' . value + : echo key .. ': ' .. value :endfor @@ -605,7 +605,7 @@ Functions that can be used with a Dictionary: > :let small = min(dict) " minimum value in dict :let xs = count(dict, 'x') " count nr of times 'x' appears in dict :let s = string(dict) " String representation of dict - :call map(dict, '">> " . v:val') " prepend ">> " to each item + :call map(dict, '">> " .. v:val') " prepend ">> " to each item 1.5 Blobs ~ @@ -840,7 +840,7 @@ Example: > All expressions within one level are parsed from left to right. -expr1 *expr1* *trinary* *E109* +expr1 *expr1* *ternary* *E109* ----- expr2 ? expr1 : expr1 @@ -1362,7 +1362,7 @@ option *expr-option* *E112* *E113* &l:option local option value Examples: > - echo "tabstop is " . &tabstop + echo "tabstop is " .. &tabstop if &insertmode Any option name can be used here. See |options|. When using the local value @@ -1637,7 +1637,7 @@ maintain a counter: > echo "script executed for the first time" else let s:counter = s:counter + 1 - echo "script executed " . s:counter . " times now" + echo "script executed " .. s:counter .. " times now" endif Note that this means that filetype plugins don't get a different set of script @@ -1736,7 +1736,7 @@ v:completed_item *v:count* *count-variable* v:count The count given for the last Normal mode command. Can be used to get the count before a mapping. Read-only. Example: > - :map _x :<C-U>echo "the count is " . v:count<CR> + :map _x :<C-U>echo "the count is " .. v:count<CR> < Note: The <C-U> is required to remove the line range that you get when typing ':' after a count. When there are two counts, as in "3d2w", they are multiplied, @@ -2531,9 +2531,9 @@ Example: > : echohl Title : echo a:title : echohl None - : echo a:0 . " items:" + : echo a:0 .. " items:" : for s in a:000 - : echon ' ' . s + : echon ' ' .. s : endfor :endfunction @@ -2572,7 +2572,7 @@ This function can then be called with: > this works: *function-range-example* > :function Mynumber(arg) - : echo line(".") . " " . a:arg + : echo line(".") .. " " .. a:arg :endfunction :1,5call Mynumber(getline(".")) < @@ -2583,7 +2583,7 @@ This function can then be called with: > Example of a function that handles the range itself: > :function Cont() range - : execute (a:firstline + 1) . "," . a:lastline . 's/^/\t\\ ' + : execute (a:firstline + 1) .. "," .. a:lastline .. 's/^/\t\\ ' :endfunction :4,8call Cont() < @@ -2745,7 +2745,7 @@ This does NOT work: > This cannot be used to add an item to a |List|. This cannot be used to set a byte in a String. You can do that like this: > - :let var = var[0:2] . 'X' . var[4:] + :let var = var[0:2] .. 'X' .. var[4:] < When {var-name} is a |Blob| then {idx} can be the length of the blob, in which case one byte is appended. @@ -2807,7 +2807,7 @@ This does NOT work: > is just like using the |:set| command: both the local value and the global value are changed. Example: > - :let &path = &path . ',/usr/local/include' + :let &path = &path .. ',/usr/local/include' :let &{option-name} .= {expr1} For a string option: Append {expr1} to the value. @@ -3064,6 +3064,8 @@ text... :if {expr1} *:if* *:end* *:endif* *:en* *E171* *E579* *E580* :en[dif] Execute the commands until the next matching ":else" or ":endif" if {expr1} evaluates to non-zero. + Although the short forms work, it is recommended to + always use `:endif` to avoid confusion. From Vim version 4.5 until 5.0, every Ex command in between the ":if" and ":endif" is ignored. These two @@ -3661,7 +3663,7 @@ exception most recently caught as long it is not finished. :function! Caught() : if v:exception != "" - : echo 'Caught "' . v:exception . '" in ' . v:throwpoint + : echo 'Caught "' .. v:exception .. '" in ' .. v:throwpoint : else : echo 'Nothing caught' : endif @@ -4064,8 +4066,8 @@ a script in order to catch unexpected things. :catch /^Vim:Interrupt$/ : echo "Script interrupted" :catch /.*/ - : echo "Internal error (" . v:exception . ")" - : echo " - occurred at " . v:throwpoint + : echo "Internal error (" .. v:exception .. ")" + : echo " - occurred at " .. v:throwpoint :endtry :" end of script @@ -4261,7 +4263,7 @@ parentheses can be cut out from |v:exception| with the ":substitute" command. :function! CheckRange(a, func) : if a:a < 0 - : throw "EXCEPT:MATHERR:RANGE(" . a:func . ")" + : throw "EXCEPT:MATHERR:RANGE(" .. a:func .. ")" : endif :endfunction : @@ -4288,7 +4290,7 @@ parentheses can be cut out from |v:exception| with the ":substitute" command. : try : execute "write" fnameescape(a:file) : catch /^Vim(write):/ - : throw "EXCEPT:IO(" . getcwd() . ", " . a:file . "):WRITEERR" + : throw "EXCEPT:IO(" .. getcwd() .. ", " .. a:file .. "):WRITEERR" : endtry :endfunction : @@ -4307,9 +4309,9 @@ parentheses can be cut out from |v:exception| with the ":substitute" command. : let dir = substitute(v:exception, '.*(\(.\+\),\s*.\+).*', '\1', "") : let file = substitute(v:exception, '.*(.\+,\s*\(.\+\)).*', '\1', "") : if file !~ '^/' - : let file = dir . "/" . file + : let file = dir .. "/" .. file : endif - : echo 'I/O error for "' . file . '"' + : echo 'I/O error for "' .. file .. '"' : :catch /^EXCEPT/ : echo "Unspecified error" @@ -4377,7 +4379,7 @@ clauses, however, is executed. : echo "inner finally" : endtry :catch - : echo 'outer catch-all caught "' . v:exception . '"' + : echo 'outer catch-all caught "' .. v:exception .. '"' : finally : echo "outer finally" :endtry @@ -4439,7 +4441,7 @@ Printing in Binary ~ : let n = a:nr : let r = "" : while n - : let r = '01'[n % 2] . r + : let r = '01'[n % 2] .. r : let n = n / 2 : endwhile : return r @@ -4450,7 +4452,7 @@ Printing in Binary ~ :func String2Bin(str) : let out = '' : for ix in range(strlen(a:str)) - : let out = out . '-' . Nr2Bin(char2nr(a:str[ix])) + : let out = out .. '-' .. Nr2Bin(char2nr(a:str[ix])) : endfor : return out[1:] :endfunc diff --git a/runtime/doc/fold.txt b/runtime/doc/fold.txt index 8bc47a3b10..9e3d78faff 100644 --- a/runtime/doc/fold.txt +++ b/runtime/doc/fold.txt @@ -497,7 +497,7 @@ Note the use of backslashes to avoid some characters to be interpreted by the :function MyFoldText() : let line = getline(v:foldstart) : let sub = substitute(line, '/\*\|\*/\|{{{\d\=', '', 'g') - : return v:folddashes . sub + : return v:folddashes .. sub :endfunction Evaluating 'foldtext' is done in the |sandbox|. The current window is set to diff --git a/runtime/doc/ft_raku.txt b/runtime/doc/ft_raku.txt index 00b140ee9c..3d1179ed4e 100644 --- a/runtime/doc/ft_raku.txt +++ b/runtime/doc/ft_raku.txt @@ -47,20 +47,20 @@ Numbers, subscripts and superscripts are available with 's' and 'S': But some don't come defined by default. Those are digraph definitions you can add in your ~/.vimrc file. > - exec 'digraph \\ '.char2nr('∖') - exec 'digraph \< '.char2nr('≼') - exec 'digraph \> '.char2nr('≽') - exec 'digraph (L '.char2nr('⊈') - exec 'digraph )L '.char2nr('⊉') - exec 'digraph (/ '.char2nr('⊄') - exec 'digraph )/ '.char2nr('⊅') - exec 'digraph )/ '.char2nr('⊅') - exec 'digraph U+ '.char2nr('⊎') - exec 'digraph 0- '.char2nr('⊖') + exec 'digraph \\ ' .. char2nr('∖') + exec 'digraph \< ' .. char2nr('≼') + exec 'digraph \> ' .. char2nr('≽') + exec 'digraph (L ' .. char2nr('⊈') + exec 'digraph )L ' .. char2nr('⊉') + exec 'digraph (/ ' .. char2nr('⊄') + exec 'digraph )/ ' .. char2nr('⊅') + exec 'digraph )/ ' .. char2nr('⊅') + exec 'digraph U+ ' .. char2nr('⊎') + exec 'digraph 0- ' .. char2nr('⊖') " Euler's constant - exec 'digraph ne '.char2nr('𝑒') + exec 'digraph ne ' .. char2nr('𝑒') " Raku's atomic operations marker - exec 'digraph @@ '.char2nr('⚛') + exec 'digraph @@ ' .. char2nr('⚛') Alternatively, you can write Insert mode abbreviations that convert ASCII- based operators into their single-character Unicode equivalent. > diff --git a/runtime/doc/ft_rust.txt b/runtime/doc/ft_rust.txt index ff2e0ca56f..5c8782ec7a 100644 --- a/runtime/doc/ft_rust.txt +++ b/runtime/doc/ft_rust.txt @@ -26,7 +26,7 @@ behavior of the plugin. g:rustc_path~ Set this option to the path to rustc for use in the |:RustRun| and |:RustExpand| commands. If unset, "rustc" will be located in $PATH: > - let g:rustc_path = $HOME."/bin/rustc" + let g:rustc_path = $HOME .. "/bin/rustc" < *g:rustc_makeprg_no_percent* @@ -87,7 +87,7 @@ g:rust_bang_comment_leader~ g:ftplugin_rust_source_path~ Set this option to a path that should be prepended to 'path' for Rust source files: > - let g:ftplugin_rust_source_path = $HOME.'/dev/rust' + let g:ftplugin_rust_source_path = $HOME .. '/dev/rust' < *g:rustfmt_command* diff --git a/runtime/doc/ft_sql.txt b/runtime/doc/ft_sql.txt index 53a99a9e1d..fccbbce17f 100644 --- a/runtime/doc/ft_sql.txt +++ b/runtime/doc/ft_sql.txt @@ -109,8 +109,8 @@ must be configurable. The filetype plugin attempts to define many of the standard objects, plus many additional ones. In order to make this as flexible as possible, you can override the list of objects from within your |vimrc| with the following: > - let g:ftplugin_sql_objects = 'function,procedure,event,table,trigger' . - \ ',schema,service,publication,database,datatype,domain' . + let g:ftplugin_sql_objects = 'function,procedure,event,table,trigger' .. + \ ',schema,service,publication,database,datatype,domain' .. \ ',index,subscription,synchronization,view,variable' The following |Normal| mode and |Visual| mode maps have been created which use @@ -131,10 +131,10 @@ Repeatedly pressing ]} will cycle through each of these create statements: > create index i1 on t1 (c1); The default setting for g:ftplugin_sql_objects is: > - let g:ftplugin_sql_objects = 'function,procedure,event,' . - \ '\\(existing\\\\|global\\s\\+temporary\\s\\+\\)\\\{,1}' . - \ 'table,trigger' . - \ ',schema,service,publication,database,datatype,domain' . + let g:ftplugin_sql_objects = 'function,procedure,event,' .. + \ '\\(existing\\\\|global\\s\\+temporary\\s\\+\\)\\\{,1}' .. + \ 'table,trigger' .. + \ ',schema,service,publication,database,datatype,domain' .. \ ',index,subscription,synchronization,view,variable' The above will also handle these cases: > diff --git a/runtime/doc/if_pyth.txt b/runtime/doc/if_pyth.txt index afdf039aa8..9b434e61d7 100644 --- a/runtime/doc/if_pyth.txt +++ b/runtime/doc/if_pyth.txt @@ -658,7 +658,7 @@ To see what version of Python is being used: > *has-pythonx* To check if `pyx*` functions and commands are available: > if has('pythonx') - echo 'pyx* commands are available. (Python ' . &pyx . ')' + echo 'pyx* commands are available. (Python ' .. &pyx .. ')' endif ============================================================================== diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt index e3bc3d5437..d02ab1b759 100644 --- a/runtime/doc/index.txt +++ b/runtime/doc/index.txt @@ -353,6 +353,7 @@ tag char note action in Normal mode ~ register x] |Y| ["x]Y yank N lines [into register x]; synonym for "yy" + Note: Mapped to "y$" by default. |default-mappings| |ZZ| ZZ write if buffer changed and close window |ZQ| ZQ close window without writing |[| [{char} square bracket command (see |[| below) diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index ae2b9c4418..39682a2ab2 100644 --- a/runtime/doc/insert.txt +++ b/runtime/doc/insert.txt @@ -864,7 +864,7 @@ Groß): > else let res = [] let h = '' - for l in split(system('aiksaurus '.shellescape(a:base)), '\n') + for l in split(system('aiksaurus ' .. shellescape(a:base)), '\n') if l[:3] == '=== ' let h = substitute(l[4:], ' =*$', '', '') elseif l[0] =~ '\a' @@ -1199,7 +1199,7 @@ An example that completes the names of the months: > " find months matching with "a:base" let res = [] for m in split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec") - if m =~ '^' . a:base + if m =~ '^' .. a:base call add(res, m) endif endfor @@ -1221,7 +1221,7 @@ The same, but now pretending searching for matches is slow: > else " find months matching with "a:base" for m in split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec") - if m =~ '^' . a:base + if m =~ '^' .. a:base call complete_add(m) endif sleep 300m " simulate searching for next match diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 1a2d845281..98af84e1cb 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -568,6 +568,26 @@ Example: TCP echo-server *tcp-server* end) print('TCP echo-server listening on port: '..server:getsockname().port) + +Multithreading *lua-loop-threading* + +Plugins can perform work in separate (os-level) threads using the threading +APIs in luv, for instance `vim.loop.new_thread`. Note that every thread +gets its own separate lua interpreter state, with no access to lua globals +in the main thread. Neither can the state of the editor (buffers, windows, +etc) be directly accessed from threads. + +A subset of the `vim.*` API is available in threads. This includes: + +- `vim.loop` with a separate event loop per thread. +- `vim.mpack` and `vim.json` (useful for serializing messages between threads) +- `require` in threads can use lua packages from the global |lua-package-path| +- `print()` and `vim.inspect` +- `vim.diff` +- most utility functions in `vim.*` for working with pure lua values + like `vim.split`, `vim.tbl_*`, `vim.list_*`, and so on. +- `vim.is_thread()` returns true from a non-main thread. + ------------------------------------------------------------------------------ VIM.HIGHLIGHT *lua-highlight* @@ -595,13 +615,33 @@ vim.highlight.on_yank({opts}) *vim.highlight.on_yank()* - {on_visual} highlight when yanking visual selection (default `true`) - {event} event structure (default |v:event|) -vim.highlight.range({bufnr}, {ns}, {higroup}, {start}, {finish}, {rtype}, {inclusive}) +vim.highlight.range({bufnr}, {ns}, {hlgroup}, {start}, {finish}, {opts}) *vim.highlight.range()* - Highlights the range between {start} and {finish} (tuples of {line,col}) - in buffer {bufnr} with the highlight group {higroup} using the namespace - {ns}. Optional arguments are the type of range (characterwise, linewise, - or blockwise, see |setreg|; default to characterwise) and whether the - range is inclusive (default false). + + Apply highlight group to range of text. + + Parameters: ~ + {bufnr} buffer number + {ns} namespace for highlights + {hlgroup} highlight group name + {start} starting position (tuple {line,col}) + {finish} finish position (tuple {line,col}) + {opts} optional parameters: + • `regtype`: type of range (characterwise, linewise, + or blockwise, see |setreg|), default `'v'` + • `inclusive`: range includes end position, default + `false` + • `priority`: priority of highlight, default + `vim.highlight.user` (see below) + +vim.highlight.priorities *vim.highlight.priorities* + + Table with default priorities used for highlighting: + • `syntax`: `50`, used for standard syntax highlighting + • `treesitter`: `100`, used for tree-sitter-based highlighting + • `diagnostics`: `150`, used for code analysis such as diagnostics + • `user`: `200`, used for user-triggered highlights such as LSP + document symbols or `on_yank` autocommands ------------------------------------------------------------------------------ VIM.REGEX *lua-regex* @@ -1974,9 +2014,7 @@ set({mode}, {lhs}, {rhs}, {opts}) *vim.keymap.set()* result of Lua expr maps. • remap: (boolean) Make the mapping recursive. This is the inverse of the "noremap" option from - |nvim_set_keymap()|. Default `true` if `lhs` is - a string starting with `<plug>` - (case-insensitive), `false` otherwise. + |nvim_set_keymap()|. Default `false` . See also: ~ |nvim_set_keymap()| diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt index 358e944261..8d5449da0b 100644 --- a/runtime/doc/map.txt +++ b/runtime/doc/map.txt @@ -65,6 +65,9 @@ modes. where the map command applies. Disallow mapping of {rhs}, to avoid nested and recursive mappings. Often used to redefine a command. + Note: "nore" is ignored for a mapping whose result + starts with <Plug>. <Plug> is always remapped even if + "nore" is used. :unm[ap] {lhs} |mapmode-nvo| *:unm* *:unmap* @@ -285,7 +288,7 @@ Here is an example that inserts a list number that increases: > func ListItem() let g:counter += 1 - return g:counter . '. ' + return g:counter .. '. ' endfunc func ListReset() @@ -1489,12 +1492,12 @@ The valid escape sequences are Examples: > command! -nargs=+ -complete=file MyEdit \ for f in expand(<q-args>, 0, 1) | - \ exe '<mods> split ' . f | + \ exe '<mods> split ' .. f | \ endfor function! SpecialEdit(files, mods) for f in expand(a:files, 0, 1) - exe a:mods . ' split ' . f + exe a:mods .. ' split ' .. f endfor endfunction command! -nargs=+ -complete=file Sedit @@ -1570,7 +1573,7 @@ This will invoke: > : let i = 0 : while i < argc() : if filereadable(argv(i)) - : execute "e " . argv(i) + : execute "e " .. argv(i) : execute a:command : endif : let i = i + 1 diff --git a/runtime/doc/motion.txt b/runtime/doc/motion.txt index c473244827..20033bd76a 100644 --- a/runtime/doc/motion.txt +++ b/runtime/doc/motion.txt @@ -993,7 +993,7 @@ These commands are not marks themselves, but jump to a mark: :let lnum = line(".") :keepjumps normal gg :call SetLastChange() - :keepjumps exe "normal " . lnum . "G" + :keepjumps exe "normal " .. lnum .. "G" < Note that ":keepjumps" must be used for every command. When invoking a function the commands in that function diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 6e2bc228d0..56c65394f3 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -897,7 +897,7 @@ A jump table for the options with a short description can be found at |Q_op|. If you like to keep a lot of backups, you could use a BufWritePre autocommand to change 'backupext' just before writing the file to include a timestamp. > - :au BufWritePre * let &bex = '-' . strftime("%Y%b%d%X") . '~' + :au BufWritePre * let &bex = '-' .. strftime("%Y%b%d%X") .. '~' < Use 'backupdir' to put the backup in a different directory. *'backupskip'* *'bsk'* @@ -920,7 +920,7 @@ A jump table for the options with a short description can be found at |Q_op|. Note that environment variables are not expanded. If you want to use $HOME you must expand it explicitly, e.g.: > - :let backupskip = escape(expand('$HOME'), '\') . '/tmp/*' + :let backupskip = escape(expand('$HOME'), '\') .. '/tmp/*' < Note that the default also makes sure that "crontab -e" works (when a backup would be made by renaming the original file crontab won't see @@ -1185,7 +1185,7 @@ A jump table for the options with a short description can be found at |Q_op|. If the default value taken from $CDPATH is not what you want, include a modified version of the following command in your vimrc file to override it: > - :let &cdpath = ',' . substitute(substitute($CDPATH, '[, ]', '\\\0', 'g'), ':', ',', 'g') + :let &cdpath = ',' .. substitute(substitute($CDPATH, '[, ]', '\\\0', 'g'), ':', ',', 'g') < This option cannot be set from a |modeline| or in the |sandbox|, for security reasons. (parts of 'cdpath' can be passed to the shell to expand file names). @@ -1232,8 +1232,8 @@ A jump table for the options with a short description can be found at |Q_op|. set charconvert=CharConvert() fun CharConvert() system("recode " - \ . v:charconvert_from . ".." . v:charconvert_to - \ . " <" . v:fname_in . " >" v:fname_out) + \ .. v:charconvert_from .. ".." .. v:charconvert_to + \ .. " <" .. v:fname_in .. " >" .. v:fname_out) return v:shell_error endfun < The related Vim variables are: @@ -3040,7 +3040,7 @@ A jump table for the options with a short description can be found at |Q_op|. *'guitablabel'* *'gtl'* 'guitablabel' 'gtl' string (default empty) global - When nonempty describes the text to use in a label of the GUI tab + When non-empty describes the text to use in a label of the GUI tab pages line. When empty and when the result is empty Vim will use a default label. See |setting-guitablabel| for more info. @@ -3057,7 +3057,7 @@ A jump table for the options with a short description can be found at |Q_op|. *'guitabtooltip'* *'gtt'* 'guitabtooltip' 'gtt' string (default empty) global - When nonempty describes the text to use in a tooltip for the GUI tab + When non-empty describes the text to use in a tooltip for the GUI tab pages line. When empty Vim will use a default tooltip. This option is otherwise just like 'guitablabel' above. You can include a line break. Simplest method is to use |:let|: > @@ -3620,7 +3620,7 @@ A jump table for the options with a short description can be found at |Q_op|. global Language to use for menu translation. Tells which file is loaded from the "lang" directory in 'runtimepath': > - "lang/menu_" . &langmenu . ".vim" + "lang/menu_" .. &langmenu .. ".vim" < (without the spaces). For example, to always use the Dutch menus, no matter what $LANG is set to: > :set langmenu=nl_NL.ISO_8859-1 @@ -4522,7 +4522,7 @@ A jump table for the options with a short description can be found at |Q_op|. < To use an environment variable, you probably need to replace the separator. Here is an example to append $INCL, in which directory names are separated with a semi-colon: > - :let &path = &path . "," . substitute($INCL, ';', ',', 'g') + :let &path = &path .. "," .. substitute($INCL, ';', ',', 'g') < Replace the ';' with a ':' or whatever separator is used. Note that this doesn't work when $INCL contains a comma or white space. @@ -5906,7 +5906,7 @@ A jump table for the options with a short description can be found at |Q_op|. *'statusline'* *'stl'* *E540* *E542* 'statusline' 'stl' string (default empty) global or local to window |global-local| - When nonempty, this option determines the content of the status line. + When non-empty, this option determines the content of the status line. Also see |status-line|. The option consists of printf style '%' items interspersed with @@ -6222,7 +6222,7 @@ A jump table for the options with a short description can be found at |Q_op|. *'tabline'* *'tal'* 'tabline' 'tal' string (default empty) global - When nonempty, this option determines the content of the tab pages + When non-empty, this option determines the content of the tab pages line at the top of the Vim window. When empty Vim will use a default tab pages line. See |setting-tabline| for more info. @@ -6570,7 +6570,7 @@ A jump table for the options with a short description can be found at |Q_op|. This option cannot be set in a modeline when 'modelineexpr' is off. Example: > - :auto BufEnter * let &titlestring = hostname() . "/" . expand("%:p") + :auto BufEnter * let &titlestring = hostname() .. "/" .. expand("%:p") :set title titlestring=%<%F%=%l/%L-%P titlelen=70 < The value of 'titlelen' is used to align items in the middle or right of the available space. diff --git a/runtime/doc/pattern.txt b/runtime/doc/pattern.txt index 42005b0d78..c3bd5baff2 100644 --- a/runtime/doc/pattern.txt +++ b/runtime/doc/pattern.txt @@ -923,7 +923,7 @@ $ At end of pattern or in front of "\|", "\)" or "\n" ('magic' on): update the matches. This means Syntax highlighting quickly becomes wrong. Example, to highlight the line where the cursor currently is: > - :exe '/\%' . line(".") . 'l.*' + :exe '/\%' .. line(".") .. 'l.*' < When 'hlsearch' is set and you move the cursor around and make changes this will clearly show when the match is updated or not. @@ -939,7 +939,7 @@ $ At end of pattern or in front of "\|", "\)" or "\n" ('magic' on): update the matches. This means Syntax highlighting quickly becomes wrong. Example, to highlight the column where the cursor currently is: > - :exe '/\%' . col(".") . 'c' + :exe '/\%' .. col(".") .. 'c' < When 'hlsearch' is set and you move the cursor around and make changes this will clearly show when the match is updated or not. Example for matching a single byte in column 44: > diff --git a/runtime/doc/pi_netrw.txt b/runtime/doc/pi_netrw.txt index 3ac61be6f2..8257152b11 100644 --- a/runtime/doc/pi_netrw.txt +++ b/runtime/doc/pi_netrw.txt @@ -968,7 +968,7 @@ itself: fun! NetReadFixup(method, line1, line2) if method == 3 " ftp (no <.netrc>) let fourblanklines= line2 - 3 - silent fourblanklines.",".line2."g/^\s*/d" + silent fourblanklines .. "," .. line2 .. "g/^\s*/d" endif endfunction endif @@ -1975,7 +1975,7 @@ To use this function, simply assign its output to |g:netrw_list_hide| option. > Example: let g:netrw_list_hide= netrw_gitignore#Hide('my_gitignore_file') Function can take additional files with git-ignore patterns. - Example: g:netrw_list_hide= netrw_gitignore#Hide() . '.*\.swp$' + Example: let g:netrw_list_hide= netrw_gitignore#Hide() .. '.*\.swp$' Combining 'netrw_gitignore#Hide' with custom patterns. < @@ -2824,7 +2824,7 @@ your browsing preferences. (see also: |netrw-settings|) Examples: let g:netrw_list_hide= '.*\.swp$' - let g:netrw_list_hide= netrw_gitignore#Hide().'.*\.swp$' + let g:netrw_list_hide= netrw_gitignore#Hide() .. '.*\.swp$' default: "" *g:netrw_localcopycmd* ="cp" Linux/Unix/MacOS/Cygwin diff --git a/runtime/doc/print.txt b/runtime/doc/print.txt index f54d0429a6..924fab175e 100644 --- a/runtime/doc/print.txt +++ b/runtime/doc/print.txt @@ -127,21 +127,21 @@ file: > system(['lpr'] + (empty(&printdevice)?[]:['-P', &printdevice]) + [v:fname_in]) - . delete(v:fname_in) + .. delete(v:fname_in) + v:shell_error On MS-Dos and MS-Windows machines the default is to copy the file to the currently specified printdevice: > system(['copy', v:fname_in, empty(&printdevice)?'LPT1':&printdevice]) - . delete(v:fname_in) + .. delete(v:fname_in) If you change this option, using a function is an easy way to avoid having to escape all the spaces. Example: > :set printexpr=PrintFile(v:fname_in) :function PrintFile(fname) - : call system("ghostview " . a:fname) + : call system("ghostview " .. a:fname) : call delete(a:fname) : return v:shell_error :endfunc diff --git a/runtime/doc/quickfix.txt b/runtime/doc/quickfix.txt index 5b68da8be9..601384a71f 100644 --- a/runtime/doc/quickfix.txt +++ b/runtime/doc/quickfix.txt @@ -341,7 +341,7 @@ processing a quickfix or location list command, it will be aborted. cursor position will not be changed. See |:cexpr| for more information. Example: > - :g/mypattern/caddexpr expand("%") . ":" . line(".") . ":" . getline(".") + :g/mypattern/caddexpr expand("%") .. ":" .. line(".") .. ":" .. getline(".") < *:lad* *:addd* *:laddexpr* :lad[dexpr] {expr} Same as ":caddexpr", except the location list for the @@ -641,6 +641,24 @@ quickfix window. If there already is a window for that file, it is used instead. If the buffer in the used window has changed, and the error is in another file, jumping to the error will fail. You will first have to make sure the window contains a buffer which can be abandoned. + +The following steps are used to find a window to open the file selected from +the quickfix window: +1. If 'switchbuf' contains "usetab", then find a window in any tabpage + (starting with the first tabpage) that has the selected file and jump to + it. +2. Otherwise find a window displaying the selected file in the current tab + page (starting with the window before the quickfix window) and use it. +3. Otherwise find a window displaying a normal buffer ('buftype' is empty) + starting with the window before the quickfix window. If a window is found, + open the file in that window. +4. If a usable window is not found and 'switchbuf' contains "uselast", then + open the file in the last used window. +5. Otherwise open the file in the window before the quickfix window. If there + is no previous window, then open the file in the next window. +6. If a usable window is not found in the above steps, then create a new + horizontally split window above the quickfix window and open the file. + *CTRL-W_<Enter>* *CTRL-W_<CR>* You can use CTRL-W <Enter> to open a new window and jump to the error there. @@ -650,7 +668,7 @@ FileType event (also see |qf.vim|). Then the BufReadPost event is triggered, using "quickfix" for the buffer name. This can be used to perform some action on the listed errors. Example: > au BufReadPost quickfix setlocal modifiable - \ | silent exe 'g/^/s//\=line(".")." "/' + \ | silent exe 'g/^/s//\=line(".") .. " "/' \ | setlocal nomodifiable This prepends the line number to each line. Note the use of "\=" in the substitute string of the ":s" command, which is used to evaluate an diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt index af8301f1a0..e36eb2359f 100644 --- a/runtime/doc/quickref.txt +++ b/runtime/doc/quickref.txt @@ -356,6 +356,7 @@ In Insert or Command-line mode: |v_y| {visual}y yank the highlighted text into a register |yy| N yy yank N lines into a register |Y| N Y yank N lines into a register + Note: Mapped to "y$" by default. |default-mappings| |p| N p put a register after the cursor position (N times) |P| N P put a register before the cursor position (N times) |]p| N ]p like p, but adjust indent to current line diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt index a022049766..05529dc90a 100644 --- a/runtime/doc/repeat.txt +++ b/runtime/doc/repeat.txt @@ -253,21 +253,22 @@ For writing a Vim script, see chapter 41 of the user manual |usr_41.txt|. below "plugin", just like with plugins in 'runtimepath'. - If the filetype detection was not enabled yet (this + If the filetype detection was already enabled (this is usually done with a "syntax enable" or "filetype - on" command in your .vimrc file), this will also look + on" command in your |init.vim|, or automatically during + |initialization|), and the package was found in + "pack/*/opt/{name}", this command will also look for "{name}/ftdetect/*.vim" files. When the optional ! is added no plugin files or ftdetect scripts are loaded, only the matching directories are added to 'runtimepath'. This is - useful in your .vimrc. The plugins will then be - loaded during initialization, see |load-plugins| (note + useful in your |init.vim|. The plugins will then be + loaded during |initialization|, see |load-plugins| (note that the loading order will be reversed, because each - directory is inserted before others). - Note that for ftdetect scripts to be loaded - you will need to write `filetype plugin indent on` - AFTER all `packadd!` commands. + directory is inserted before others). In this case, the + ftdetect scripts will be loaded during |initialization|, + before the |load-plugins| step. Also see |pack-add|. diff --git a/runtime/doc/sign.txt b/runtime/doc/sign.txt index 5cfa06c33c..a2a5645baa 100644 --- a/runtime/doc/sign.txt +++ b/runtime/doc/sign.txt @@ -87,7 +87,7 @@ the delete is undone the sign does not move back. Here is an example that places a sign "piet", displayed with the text ">>", in line 23 of the current file: > :sign define piet text=>> texthl=Search - :exe ":sign place 2 line=23 name=piet file=" . expand("%:p") + :exe ":sign place 2 line=23 name=piet file=" .. expand("%:p") And here is the command to delete it again: > :sign unplace 2 diff --git a/runtime/doc/spell.txt b/runtime/doc/spell.txt index 03c00c8495..bc45b0e511 100644 --- a/runtime/doc/spell.txt +++ b/runtime/doc/spell.txt @@ -120,8 +120,8 @@ zuG Undo |zW| and |zG|, remove the word from the internal rare as this is a fairly uncommon command and all intuitive commands for this are already taken. If you want you can add mappings with e.g.: > - nnoremap z? :exe ':spellrare ' . expand('<cWORD>')<CR> - nnoremap z/ :exe ':spellrare! ' . expand('<cWORD>')<CR> + nnoremap z? :exe ':spellrare ' .. expand('<cWORD>')<CR> + nnoremap z/ :exe ':spellrare! ' .. expand('<cWORD>')<CR> < |:spellundo|, |zuw|, or |zuW| can be used to undo this. :spellr[rare]! {word} Add {word} as a rare word to the internal word diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt index 978142a1e0..1d3fa6c2ca 100644 --- a/runtime/doc/starting.txt +++ b/runtime/doc/starting.txt @@ -800,7 +800,7 @@ resulting file, when executed with a ":source" command: After restoring the Session, the full filename of your current Session is available in the internal variable |v:this_session|. An example mapping: > - :nmap <F2> :wa<Bar>exe "mksession! " . v:this_session<CR>:so ~/sessions/ + :nmap <F2> :wa<Bar>exe "mksession! " .. v:this_session<CR>:so ~/sessions/ This saves the current Session, and starts off the command to load another. A session includes all tab pages, unless "tabpages" was removed from diff --git a/runtime/doc/syntax.txt b/runtime/doc/syntax.txt index 319a715e40..778f829a4e 100644 --- a/runtime/doc/syntax.txt +++ b/runtime/doc/syntax.txt @@ -616,7 +616,7 @@ evaluate to get a unique string to append to each ID used in a given document, so that the full IDs will be unique even when combined with other content in a larger HTML document. Example, to append _ and the buffer number to each ID: > - :let g:html_id_expr = '"_".bufnr("%")' + :let g:html_id_expr = '"_" .. bufnr("%")' < To append a string "_mystring" to the end of each ID: > @@ -3550,8 +3550,8 @@ Do you want to draw with the mouse? Try the following: > :function! GetPixel() : let c = getline(".")[col(".") - 1] : echo c - : exe "noremap <LeftMouse> <LeftMouse>r".c - : exe "noremap <LeftDrag> <LeftMouse>r".c + : exe "noremap <LeftMouse> <LeftMouse>r" .. c + : exe "noremap <LeftDrag> <LeftMouse>r" .. c :endfunction :noremap <RightMouse> <LeftMouse>:call GetPixel()<CR> :set guicursor=n:hor20 " to see the color beneath the cursor @@ -5363,9 +5363,9 @@ types.vim: *.[ch] And put these lines in your vimrc: > " load the types.vim highlighting file, if it exists - autocmd BufRead,BufNewFile *.[ch] let fname = expand('<afile>:p:h') . '/types.vim' + autocmd BufRead,BufNewFile *.[ch] let fname = expand('<afile>:p:h') .. '/types.vim' autocmd BufRead,BufNewFile *.[ch] if filereadable(fname) - autocmd BufRead,BufNewFile *.[ch] exe 'so ' . fname + autocmd BufRead,BufNewFile *.[ch] exe 'so ' .. fname autocmd BufRead,BufNewFile *.[ch] endif ============================================================================== diff --git a/runtime/doc/tabpage.txt b/runtime/doc/tabpage.txt index 7f91fda9f4..c5b61e3a35 100644 --- a/runtime/doc/tabpage.txt +++ b/runtime/doc/tabpage.txt @@ -366,24 +366,24 @@ pages and define labels for them. Then get the label for each tab page. > for i in range(tabpagenr('$')) " select the highlighting if i + 1 == tabpagenr() - let s .= '%#TabLineSel#' + let s ..= '%#TabLineSel#' else - let s .= '%#TabLine#' + let s ..= '%#TabLine#' endif " set the tab page number (for mouse clicks) - let s .= '%' . (i + 1) . 'T' + let s ..= '%' .. (i + 1) .. 'T' " the label is made by MyTabLabel() - let s .= ' %{MyTabLabel(' . (i + 1) . ')} ' + let s ..= ' %{MyTabLabel(' .. (i + 1) .. ')} ' endfor " after the last tab fill with TabLineFill and reset tab page nr - let s .= '%#TabLineFill#%T' + let s ..= '%#TabLineFill#%T' " right-align the label to close the current tab page if tabpagenr('$') > 1 - let s .= '%=%#TabLine#%999Xclose' + let s ..= '%=%#TabLine#%999Xclose' endif return s @@ -446,14 +446,14 @@ windows in the tab page and a '+' if there is a modified buffer: > " Append the number of windows in the tab page if more than one let wincount = tabpagewinnr(v:lnum, '$') if wincount > 1 - let label .= wincount + let label ..= wincount endif if label != '' - let label .= ' ' + let label ..= ' ' endif " Append the buffer name - return label . bufname(bufnrlist[tabpagewinnr(v:lnum) - 1]) + return label .. bufname(bufnrlist[tabpagewinnr(v:lnum) - 1]) endfunction set guitablabel=%{GuiTabLabel()} diff --git a/runtime/doc/tagsrch.txt b/runtime/doc/tagsrch.txt index 4d938c4a23..5a0d16d6b8 100644 --- a/runtime/doc/tagsrch.txt +++ b/runtime/doc/tagsrch.txt @@ -705,7 +705,7 @@ matches the pattern "^# *define" it is not considered to be a comment. If you want to list matches, and then select one to jump to, you could use a mapping to do that for you. Here is an example: > - :map <F4> [I:let nr = input("Which one: ")<Bar>exe "normal " . nr ."[\t"<CR> + :map <F4> [I:let nr = input("Which one: ")<Bar>exe "normal " .. nr .. "[\t"<CR> < *[i* [i Display the first line that contains the keyword diff --git a/runtime/doc/term.txt b/runtime/doc/term.txt index 62e13285f5..f9b2271756 100644 --- a/runtime/doc/term.txt +++ b/runtime/doc/term.txt @@ -321,7 +321,7 @@ an #if/#else/#endif block, the selection becomes linewise. For MS-Windows and xterm the time for double clicking can be set with the 'mousetime' option. For the other systems this time is defined outside of Vim. An example, for using a double click to jump to the tag under the cursor: > - :map <2-LeftMouse> :exe "tag ". expand("<cword>")<CR> + :map <2-LeftMouse> :exe "tag " .. expand("<cword>")<CR> Dragging the mouse with a double click (button-down, button-up, button-down and then drag) will result in whole words to be selected. This continues diff --git a/runtime/doc/tips.txt b/runtime/doc/tips.txt index b77c7d9a6d..d913b53c6b 100644 --- a/runtime/doc/tips.txt +++ b/runtime/doc/tips.txt @@ -84,14 +84,14 @@ What you need: create it with the shell command "mkid file1 file2 ..". Put this in your |init.vim|: > - map _u :call ID_search()<Bar>execute "/\\<" . g:word . "\\>"<CR> - map _n :n<Bar>execute "/\\<" . g:word . "\\>"<CR> + map _u :call ID_search()<Bar>execute "/\\<" .. g:word .. "\\>"<CR> + map _n :n<Bar>execute "/\\<" .. g:word .. "\\>"<CR> function! ID_search() let g:word = expand("<cword>") - let x = system("lid --key=none ". g:word) + let x = system("lid --key=none " .. g:word) let x = substitute(x, "\n", " ", "g") - execute "next " . x + execute "next " .. x endfun To use it, place the cursor on a word, type "_u" and vim will load the file @@ -285,13 +285,13 @@ This mapping will format any bullet list. It requires that there is an empty line above and below each list entry. The expression commands are used to be able to give comments to the parts of the mapping. > - :let m = ":map _f :set ai<CR>" " need 'autoindent' set - :let m = m . "{O<Esc>" " add empty line above item - :let m = m . "}{)^W" " move to text after bullet - :let m = m . "i <CR> <Esc>" " add space for indent - :let m = m . "gq}" " format text after the bullet - :let m = m . "{dd" " remove the empty line - :let m = m . "5lDJ" " put text after bullet + :let m = ":map _f :set ai<CR>" " need 'autoindent' set + :let m ..= "{O<Esc>" " add empty line above item + :let m ..= "}{)^W" " move to text after bullet + :let m ..= "i <CR> <Esc>" " add space for indent + :let m ..= "gq}" " format text after the bullet + :let m ..= "{dd" " remove the empty line + :let m ..= "5lDJ" " put text after bullet :execute m |" define the mapping (<> notation |<>|. Note that this is all typed literally. ^W is "^" "W", not @@ -429,15 +429,15 @@ A slightly more advanced version is used in the |matchparen| plugin. let c = '\[' let c2 = '\]' endif - let s_skip ='synIDattr(synID(line("."), col("."), 0), "name") ' . + let s_skip ='synIDattr(synID(line("."), col("."), 0), "name") ' .. \ '=~? "string\\|comment"' execute 'if' s_skip '| let s_skip = 0 | endif' let [m_lnum, m_col] = searchpairpos(c, '', c2, s_flags, s_skip) if m_lnum > 0 && m_lnum >= line('w0') && m_lnum <= line('w$') - exe 'match Search /\(\%' . c_lnum . 'l\%' . c_col . - \ 'c\)\|\(\%' . m_lnum . 'l\%' . m_col . 'c\)/' + exe 'match Search /\(\%' .. c_lnum .. 'l\%' .. c_col .. + \ 'c\)\|\(\%' .. m_lnum .. 'l\%' .. m_col .. 'c\)/' let s:paren_hl_on = 1 endif endfunction diff --git a/runtime/doc/undo.txt b/runtime/doc/undo.txt index b11d7581ed..a853aea995 100644 --- a/runtime/doc/undo.txt +++ b/runtime/doc/undo.txt @@ -272,12 +272,12 @@ history file. E.g.: > au BufReadPost * call ReadUndo() au BufWritePost * call WriteUndo() func ReadUndo() - if filereadable(expand('%:h'). '/UNDO/' . expand('%:t')) + if filereadable(expand('%:h') .. '/UNDO/' .. expand('%:t')) rundo %:h/UNDO/%:t endif endfunc func WriteUndo() - let dirname = expand('%:h') . '/UNDO' + let dirname = expand('%:h') .. '/UNDO' if !isdirectory(dirname) call mkdir(dirname) endif diff --git a/runtime/doc/usr_04.txt b/runtime/doc/usr_04.txt index b2dd617542..c7c900274b 100644 --- a/runtime/doc/usr_04.txt +++ b/runtime/doc/usr_04.txt @@ -349,15 +349,17 @@ Notice that "yw" includes the white space after a word. If you don't want this, use "ye". The "yy" command yanks a whole line, just like "dd" deletes a whole line. -Unexpectedly, while "D" deletes from the cursor to the end of the line, "Y" -works like "yy", it yanks the whole line. Watch out for this inconsistency! -Use "y$" to yank to the end of the line. a text line yy a text line a text line line 2 line 2 p line 2 last line last line a text line last line +"Y" was originally equivalent to "yank the entire line", as opposed to "D" +which is "delete to end of the line". "Y" has thus been remapped to mean +"yank to end of the line" to make it consistent with the behavior of "D". +Mappings will be covered in later chapters. + ============================================================================== *04.7* Using the clipboard diff --git a/runtime/doc/usr_05.txt b/runtime/doc/usr_05.txt index f93a221e43..b1ef563e43 100644 --- a/runtime/doc/usr_05.txt +++ b/runtime/doc/usr_05.txt @@ -131,7 +131,7 @@ it worked before Vim 5.0. Otherwise the "Q" command starts Ex mode, but you will not need it. > - vnoremap _g y:exe "grep /" . escape(@", '\\/') . "/ *.c *.h"<CR> + vnoremap _g y:exe "grep /" .. escape(@", '\\/') .. "/ *.c *.h"<CR> This mapping yanks the visually selected text and searches for it in C files. This is a complicated mapping. You can see that mappings can be used to do diff --git a/runtime/doc/usr_07.txt b/runtime/doc/usr_07.txt index 649be8d7ce..ebf5c3d7b8 100644 --- a/runtime/doc/usr_07.txt +++ b/runtime/doc/usr_07.txt @@ -336,7 +336,7 @@ there. > Of course you can use many other commands to yank the text. For example, to select whole lines start Visual mode with "V". Or use CTRL-V to select a -rectangular block. Or use "Y" to yank a single line, "yaw" to yank-a-word, +rectangular block. Or use "yy" to yank a single line, "yaw" to yank-a-word, etc. The "p" command puts the text after the cursor. Use "P" to put the text before the cursor. Notice that Vim remembers if you yanked a whole line or a @@ -359,7 +359,7 @@ the text should be placed in the f register. This must come just before the yank command. Now yank three whole lines to the l register (l for line): > - "l3Y + "l3yy The count could be before the "l just as well. To yank a block of text to the b (for block) register: > diff --git a/runtime/doc/usr_10.txt b/runtime/doc/usr_10.txt index 5365f90314..8844671e01 100644 --- a/runtime/doc/usr_10.txt +++ b/runtime/doc/usr_10.txt @@ -132,11 +132,11 @@ This works both with recording and with yank and delete commands. For example, you want to collect a sequence of lines into the a register. Yank the first line with: > - "aY + "ayy Now move to the second line, and type: > - "AY + "Ayy Repeat this command for all lines. The a register now contains all those lines, in the order you yanked them. diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt index 38869f8e94..e1b87f60ad 100644 --- a/runtime/doc/various.txt +++ b/runtime/doc/various.txt @@ -275,7 +275,7 @@ g8 Print the hex values of the bytes used in the Special characters are not escaped, use quotes or |shellescape()|: > :!ls "%" - :exe "!ls " . shellescape(expand("%")) + :exe "!ls " .. shellescape(expand("%")) < Newline character ends {cmd} unless a backslash precedes the newline. What follows is interpreted as @@ -432,7 +432,7 @@ g8 Print the hex values of the bytes used in the used. In this example |:silent| is used to avoid the message about reading the file and |:unsilent| to be able to list the first line of each file. > - :silent argdo unsilent echo expand('%') . ": " . getline(1) + :silent argdo unsilent echo expand('%') .. ": " .. getline(1) < *:verb* *:verbose* diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 5ea6a9c5dd..d7a65756bf 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -442,6 +442,9 @@ Working directory (Vim implemented some of these later than Nvim): - `getcwd(-1)` is equivalent to `getcwd(-1, 0)` instead of returning the global working directory. Use `getcwd(-1, -1)` to get the global working directory. +Mappings: +- |nore| is ignored for rhs <Plug> mappings. <Plug> mappings are always remapped. + ============================================================================== 5. Missing legacy features *nvim-features-missing* diff --git a/runtime/doc/windows.txt b/runtime/doc/windows.txt index 5b91321c40..bd29cd1d7a 100644 --- a/runtime/doc/windows.txt +++ b/runtime/doc/windows.txt @@ -447,7 +447,7 @@ These commands can also be executed with ":wincmd": the |CursorHold| autocommand event). Or when a Normal mode command is inconvenient. The count can also be a window number. Example: > - :exe nr . "wincmd w" + :exe nr .. "wincmd w" < This goes to window "nr". ============================================================================== @@ -909,12 +909,12 @@ CTRL-W g } *CTRL-W_g}* cursor. This is less clever than using |:ptag|, but you don't need a tags file and it will also find matches in system include files. Example: > - :au! CursorHold *.[ch] ++nested exe "silent! psearch " . expand("<cword>") + :au! CursorHold *.[ch] ++nested exe "silent! psearch " .. expand("<cword>") < Warning: This can be slow. Example *CursorHold-example* > - :au! CursorHold *.[ch] ++nested exe "silent! ptag " . expand("<cword>") + :au! CursorHold *.[ch] ++nested exe "silent! ptag " .. expand("<cword>") This will cause a ":ptag" to be executed for the keyword under the cursor, when the cursor hasn't moved for the time set with 'updatetime'. "++nested" @@ -937,14 +937,14 @@ is no word under the cursor, and a few other things: > : : " Delete any existing highlight before showing another tag : silent! wincmd P " jump to preview window - : if &previewwindow " if we really get there... + : if &previewwindow " if we really get there... : match none " delete existing highlight : wincmd p " back to old window : endif : : " Try displaying a matching tag for the word under the cursor : try - : exe "ptag " . w + : exe "ptag " .. w : catch : return : endtry @@ -956,10 +956,10 @@ is no word under the cursor, and a few other things: > : endif : call search("$", "b") " to end of previous line : let w = substitute(w, '\\', '\\\\', "") - : call search('\<\V' . w . '\>') " position cursor on match + : call search('\<\V' .. w .. '\>') " position cursor on match : " Add a match highlight to the word at this position : hi previewWord term=bold ctermbg=green guibg=green - : exe 'match previewWord "\%' . line(".") . 'l\%' . col(".") . 'c\k*"' + : exe 'match previewWord "\%' .. line(".") .. 'l\%' .. col(".") .. 'c\k*"' : wincmd p " back to old window : endif : endif diff --git a/runtime/filetype.lua b/runtime/filetype.lua index fcfc5701f0..74e427c358 100644 --- a/runtime/filetype.lua +++ b/runtime/filetype.lua @@ -7,6 +7,7 @@ if vim.g.do_filetype_lua ~= 1 then return end +-- TODO: Remove vim.cmd once Lua autocommands land vim.cmd [[ augroup filetypedetect au BufRead,BufNewFile * call v:lua.vim.filetype.match(expand('<afile>')) @@ -18,6 +19,12 @@ runtime! ftdetect/*.lua " Set a marker so that the ftdetect scripts are not sourced a second time by filetype.vim let g:did_load_ftdetect = 1 +" If filetype.vim is disabled, set up the autocmd to use scripts.vim +if exists('did_load_filetypes') + au BufRead,BufNewFile * if !did_filetype() && expand('<amatch>') !~ g:ft_ignore_pat | runtime! scripts.vim | endif + au StdinReadPost * if !did_filetype() | runtime! scripts.vim | endif +endif + augroup END ]] diff --git a/runtime/filetype.vim b/runtime/filetype.vim index f47b3ee0d4..8114ad4092 100644 --- a/runtime/filetype.vim +++ b/runtime/filetype.vim @@ -480,6 +480,7 @@ au BufNewFile,BufRead */etc/dnsmasq.conf setf dnsmasq au BufNewFile,BufRead *.desc setf desc " the D language or dtrace +au BufNewFile,BufRead */dtrace/*.d setf dtrace au BufNewFile,BufRead *.d call dist#ft#DtraceCheck() " Desktop files diff --git a/runtime/indent/vim.vim b/runtime/indent/vim.vim index 7c03ff2873..f5a94940bd 100644 --- a/runtime/indent/vim.vim +++ b/runtime/indent/vim.vim @@ -1,7 +1,7 @@ " Vim indent file " Language: Vim script " Maintainer: Bram Moolenaar <Bram@vim.org> -" Last Change: 2021 Nov 27 +" Last Change: 2022 Feb 23 " Only load this indent file when no other was loaded. if exists("b:did_indent") @@ -10,7 +10,7 @@ endif let b:did_indent = 1 setlocal indentexpr=GetVimIndent() -setlocal indentkeys+==end,=},=else,=cat,=finall,=END,0\\,0=\"\\\ +setlocal indentkeys+==endif,=enddef,=endfu,=endfor,=endwh,=endtry,=},=else,=cat,=finall,=END,0\\,0=\"\\\ setlocal indentkeys-=0# setlocal indentkeys-=: @@ -103,10 +103,11 @@ function GetVimIndentIntern() " A line starting with :au does not increment/decrement indent. " A { may start a block or a dict. Assume that when a } follows it's a " terminated dict. + " ":function" starts a block but "function(" doesn't. if prev_text !~ '^\s*au\%[tocmd]' && prev_text !~ '^\s*{.*}' - let i = match(prev_text, '\(^\||\)\s*\(export\s\+\)\?\({\|\(if\|wh\%[ile]\|for\|try\|cat\%[ch]\|fina\|finall\%[y]\|fu\%[nction]\|def\|el\%[seif]\)\>\)') + let i = match(prev_text, '\(^\||\)\s*\(export\s\+\)\?\({\|\(if\|wh\%[ile]\|for\|try\|cat\%[ch]\|fina\|finall\%[y]\|def\|el\%[seif]\)\>\|fu\%[nction]\s\)') if i >= 0 - let ind += shiftwidth() + let ind += shiftwidth() if strpart(prev_text, i, 1) == '|' && has('syntax_items') \ && synIDattr(synID(lnum, i, 1), "name") =~ '\(Comment\|String\|PatSep\)$' let ind -= shiftwidth() @@ -170,10 +171,15 @@ function GetVimIndentIntern() let ind = ind + shiftwidth() endif - " Subtract a 'shiftwidth' on a :endif, :endwhile, :catch, :finally, :endtry, - " :endfun, :else and :augroup END. - if cur_text =~ '^\s*\(ene\@!\|cat\|finall\|el\|aug\%[roup]\s\+[eE][nN][dD]\)' + " Subtract a 'shiftwidth' on a :endif, :endwhile, :endfor, :catch, :finally, + " :endtry, :endfun, :enddef, :else and :augroup END. + " Although ":en" would be enough only match short command names as in + " 'indentkeys'. + if cur_text =~ '^\s*\(endif\|endwh\|endfor\|endtry\|endfu\|enddef\|cat\|finall\|else\|aug\%[roup]\s\+[eE][nN][dD]\)' let ind = ind - shiftwidth() + if ind < 0 + let ind = 0 + endif endif return ind diff --git a/runtime/lua/vim/_load_package.lua b/runtime/lua/vim/_load_package.lua new file mode 100644 index 0000000000..525f603438 --- /dev/null +++ b/runtime/lua/vim/_load_package.lua @@ -0,0 +1,49 @@ +-- prevents luacheck from making lints for setting things on vim +local vim = assert(vim) + +local pathtrails = {} +vim._so_trails = {} +for s in (package.cpath..';'):gmatch('[^;]*;') do + s = s:sub(1, -2) -- Strip trailing semicolon + -- Find out path patterns. pathtrail should contain something like + -- /?.so, \?.dll. This allows not to bother determining what correct + -- suffixes are. + local pathtrail = s:match('[/\\][^/\\]*%?.*$') + if pathtrail and not pathtrails[pathtrail] then + pathtrails[pathtrail] = true + table.insert(vim._so_trails, pathtrail) + end +end + +function vim._load_package(name) + local basename = name:gsub('%.', '/') + local paths = {"lua/"..basename..".lua", "lua/"..basename.."/init.lua"} + local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true}) + if #found > 0 then + local f, err = loadfile(found[1]) + return f or error(err) + end + + local so_paths = {} + for _,trail in ipairs(vim._so_trails) do + local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash + table.insert(so_paths, path) + end + + found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true}) + if #found > 0 then + -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is + -- a) strip prefix up to and including the first dash, if any + -- b) replace all dots by underscores + -- c) prepend "luaopen_" + -- So "foo-bar.baz" should result in "luaopen_bar_baz" + local dash = name:find("-", 1, true) + local modname = dash and name:sub(dash + 1) or name + local f, err = package.loadlib(found[1], "luaopen_"..modname:gsub("%.", "_")) + return f or error(err) + end + return nil +end + +-- Insert vim._load_package after the preloader at position 2 +table.insert(package.loaders, 2, vim._load_package) diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index fda70b4e95..fcb1e61764 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -447,7 +447,7 @@ local function set_list(loclist, opts) vim.fn.setqflist({}, ' ', { title = title, items = items }) end if open then - vim.api.nvim_command(loclist and "lopen" or "copen") + vim.api.nvim_command(loclist and "lopen" or "botright copen") end end @@ -921,9 +921,7 @@ M.handlers.underline = { higroup, { diagnostic.lnum, diagnostic.col }, { diagnostic.end_lnum, diagnostic.end_col }, - 'v', - false, - 150 + { priority = vim.highlight.priorities.diagnostics } ) end save_extmarks(underline_ns, bufnr) diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 0555b87651..f5e4dabfb6 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -1178,6 +1178,7 @@ local pattern = { [".*/etc/yum%.conf"] = "dosini", [".*lvs"] = "dracula", [".*lpe"] = "dracula", + [".*/dtrace/.*%.d"] = "dtrace", [".*esmtprc"] = "esmtprc", [".*Eterm/.*%.cfg"] = "eterm", [".*%.git/modules/.*/config"] = "gitconfig", diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua index 12faa0a6e1..4105ef0675 100644 --- a/runtime/lua/vim/highlight.lua +++ b/runtime/lua/vim/highlight.lua @@ -1,9 +1,16 @@ local api = vim.api -local highlight = {} +local M = {} + +M.priorities = { + syntax = 50, + treesitter = 100, + diagnostics = 150, + user = 200, +} ---@private -function highlight.create(higroup, hi_info, default) +function M.create(higroup, hi_info, default) local options = {} -- TODO: Add validation for k, v in pairs(hi_info) do @@ -13,28 +20,33 @@ function highlight.create(higroup, hi_info, default) end ---@private -function highlight.link(higroup, link_to, force) +function M.link(higroup, link_to, force) vim.cmd(string.format([[highlight%s link %s %s]], force and "!" or " default", higroup, link_to)) end - --- Highlight range between two positions --- ---@param bufnr number of buffer to apply highlighting to ---@param ns namespace to add highlight to ---@param higroup highlight group to use for highlighting ----@param rtype type of range (:help setreg, default charwise) ----@param inclusive boolean indicating whether the range is end-inclusive (default false) ----@param priority number indicating priority of highlight (default 50) -function highlight.range(bufnr, ns, higroup, start, finish, rtype, inclusive, priority) - rtype = rtype or 'v' - inclusive = inclusive or false - priority = priority or 50 +---@param start first position (tuple {line,col}) +---@param finish second position (tuple {line,col}) +---@param opts table with options: +-- - regtype type of range (:help setreg, default charwise) +-- - inclusive boolean indicating whether the range is end-inclusive (default false) +-- - priority number indicating priority of highlight (default priorities.user) +function M.range(bufnr, ns, higroup, start, finish, opts) + opts = opts or {} + local regtype = opts.regtype or "v" + local inclusive = opts.inclusive or false + local priority = opts.priority or M.priorities.user -- sanity check - if start[2] < 0 or finish[1] < start[1] then return end + if start[2] < 0 or finish[1] < start[1] then + return + end - local region = vim.region(bufnr, start, finish, rtype, inclusive) + local region = vim.region(bufnr, start, finish, regtype, inclusive) for linenr, cols in pairs(region) do local end_row if cols[2] == -1 then @@ -46,13 +58,12 @@ function highlight.range(bufnr, ns, higroup, start, finish, rtype, inclusive, pr end_row = end_row, end_col = cols[2], priority = priority, - strict = false + strict = false, }) end - end -local yank_ns = api.nvim_create_namespace('hlyank') +local yank_ns = api.nvim_create_namespace("hlyank") --- Highlight the yanked region --- --- use from init.vim via @@ -62,26 +73,40 @@ local yank_ns = api.nvim_create_namespace('hlyank') --- customize conditions (here: do not highlight a visual selection) via --- au TextYankPost * lua vim.highlight.on_yank {on_visual=false} --- --- @param opts dictionary with options controlling the highlight: +-- @param opts table with options controlling the highlight: -- - higroup highlight group for yanked region (default "IncSearch") -- - timeout time in ms before highlight is cleared (default 150) -- - on_macro highlight when executing macro (default false) -- - on_visual highlight when yanking visual selection (default true) -- - event event structure (default vim.v.event) -function highlight.on_yank(opts) - vim.validate { - opts = { opts, - function(t) if t == nil then return true else return type(t) == 'table' end end, - 'a table or nil to configure options (see `:h highlight.on_yank`)', - }} +function M.on_yank(opts) + vim.validate({ + opts = { + opts, + function(t) + if t == nil then + return true + else + return type(t) == "table" + end + end, + "a table or nil to configure options (see `:h highlight.on_yank`)", + }, + }) opts = opts or {} local event = opts.event or vim.v.event local on_macro = opts.on_macro or false local on_visual = (opts.on_visual ~= false) - if (not on_macro) and vim.fn.reg_executing() ~= '' then return end - if event.operator ~= 'y' or event.regtype == '' then return end - if (not on_visual) and event.visual then return end + if not on_macro and vim.fn.reg_executing() ~= "" then + return + end + if event.operator ~= "y" or event.regtype == "" then + return + end + if not on_visual and event.visual then + return + end local higroup = opts.higroup or "IncSearch" local timeout = opts.timeout or 150 @@ -92,19 +117,23 @@ function highlight.on_yank(opts) local pos1 = vim.fn.getpos("'[") local pos2 = vim.fn.getpos("']") - pos1 = {pos1[2] - 1, pos1[3] - 1 + pos1[4]} - pos2 = {pos2[2] - 1, pos2[3] - 1 + pos2[4]} - - highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive, 200) + pos1 = { pos1[2] - 1, pos1[3] - 1 + pos1[4] } + pos2 = { pos2[2] - 1, pos2[3] - 1 + pos2[4] } - vim.defer_fn( - function() - if api.nvim_buf_is_valid(bufnr) then - api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) - end - end, - timeout + M.range( + bufnr, + yank_ns, + higroup, + pos1, + pos2, + { regtype = event.regtype, inclusive = event.inclusive, priority = M.priorities.user } ) + + vim.defer_fn(function() + if api.nvim_buf_is_valid(bufnr) then + api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) + end + end, timeout) end -return highlight +return M diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua index d53b790746..df49eff4b6 100644 --- a/runtime/lua/vim/keymap.lua +++ b/runtime/lua/vim/keymap.lua @@ -42,7 +42,7 @@ local keymap = {} --- |nvim_replace_termcodes()| is applied to the result of Lua expr maps. --- - remap: (boolean) Make the mapping recursive. This is the --- inverse of the "noremap" option from |nvim_set_keymap()|. ---- Default `true` if `lhs` is a string starting with `<plug>` (case-insensitive), `false` otherwise. +--- Default `false`. ---@see |nvim_set_keymap()| function keymap.set(mode, lhs, rhs, opts) vim.validate { @@ -66,8 +66,8 @@ function keymap.set(mode, lhs, rhs, opts) opts.replace_keycodes = nil if opts.remap == nil then - -- remap by default on <plug> mappings and don't otherwise. - opts.noremap = is_rhs_luaref or rhs:lower():match("^<plug>") == nil + -- default remap value is false + opts.noremap = true else -- remaps behavior is opposite of noremap option. opts.noremap = not opts.remap diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index d93331c12f..655c3a4679 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1551,9 +1551,7 @@ do --[[ References ]] document_highlight_kind[kind], { start_line, start_idx }, { end_line, end_idx }, - 'v', - false, - 200) + { priority = vim.highlight.priorities.user }) end end end diff --git a/runtime/syntax/structurizr.vim b/runtime/syntax/structurizr.vim index 73629b1495..ab9e4ee609 100644 --- a/runtime/syntax/structurizr.vim +++ b/runtime/syntax/structurizr.vim @@ -1,7 +1,7 @@ " Vim syntax file " Language: Structurizr DSL " Maintainer: Bastian Venthur <venthur@debian.org> -" Last Change: 2021-08-16 +" Last Change: 2022-02-15 " Remark: For a language reference, see " https://github.com/structurizr/dsl @@ -30,6 +30,7 @@ syn keyword skeyword deployment syn keyword skeyword deploymentenvironment syn keyword skeyword deploymentgroup syn keyword skeyword deploymentnode +syn keyword skeyword description syn keyword skeyword dynamic syn keyword skeyword element syn keyword skeyword enterprise @@ -37,7 +38,6 @@ syn keyword skeyword exclude syn keyword skeyword filtered syn keyword skeyword group syn keyword skeyword healthcheck -syn keyword skeyword impliedrelationships syn keyword skeyword include syn keyword skeyword infrastructurenode syn keyword skeyword model @@ -51,6 +51,7 @@ syn keyword skeyword styles syn keyword skeyword systemcontext syn keyword skeyword systemlandscape syn keyword skeyword tags +syn keyword skeyword technology syn keyword skeyword terminology syn keyword skeyword theme syn keyword skeyword title @@ -63,7 +64,11 @@ syn match skeyword "\!adrs\s\+" syn match skeyword "\!constant\s\+" syn match skeyword "\!docs\s\+" syn match skeyword "\!identifiers\s\+" +syn match skeyword "\!impliedrelationships\s\+" syn match skeyword "\!include\s\+" +syn match skeyword "\!plugin\s\+" +syn match skeyword "\!ref\s\+" +syn match skeyword "\!script\s\+" syn region sstring oneline start='"' end='"' diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py index 36768953f9..7b6d974181 100755 --- a/scripts/gen_vimdoc.py +++ b/scripts/gen_vimdoc.py @@ -95,6 +95,7 @@ CONFIG = { 'window.c', 'win_config.c', 'tabpage.c', + 'autocmd.c', 'ui.c', ], # List of files/directories for doxygen to read, separated by blanks diff --git a/scripts/uncrustify.sh b/scripts/uncrustify.sh new file mode 100755 index 0000000000..ac5d542c29 --- /dev/null +++ b/scripts/uncrustify.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +# Check that you have uncrustify +hash uncrustify + +COMMITISH="${1:-master}" +for file in $(git diff --diff-filter=d --name-only $COMMITISH | grep '\.[ch]$'); do + uncrustify -c src/uncrustify.cfg -l C --replace --no-backup "$file" +done diff --git a/src/cjson/lua_cjson.c b/src/cjson/lua_cjson.c index cf9e82c38e..b5f97bc485 100644 --- a/src/cjson/lua_cjson.c +++ b/src/cjson/lua_cjson.c @@ -776,7 +776,7 @@ static void json_append_data(lua_State *l, json_config_t *cfg, if (has_metatable) { - nlua_pushref(l, nlua_empty_dict_ref); + nlua_pushref(l, nlua_get_empty_dict_ref(l)); if (lua_rawequal(l, -2, -1)) { as_empty_dict = true; } else { @@ -822,7 +822,7 @@ static void json_append_data(lua_State *l, json_config_t *cfg, } break; case LUA_TUSERDATA: - nlua_pushref(l, nlua_nil_ref); + nlua_pushref(l, nlua_get_nil_ref(l)); bool is_nil = lua_rawequal(l, -2, -1); lua_pop(l, 1); if (is_nil) { @@ -1285,7 +1285,7 @@ static void json_parse_object_context(lua_State *l, json_parse_t *json) /* Handle empty objects */ if (token.type == T_OBJ_END) { - nlua_pushref(l, nlua_empty_dict_ref); \ + nlua_pushref(l, nlua_get_empty_dict_ref(l)); \ lua_setmetatable(l, -2); \ json_decode_ascend(json); return; @@ -1392,7 +1392,7 @@ static void json_process_value(lua_State *l, json_parse_t *json, if (use_luanil) { lua_pushnil(l); } else { - nlua_pushref(l, nlua_nil_ref); + nlua_pushref(l, nlua_get_nil_ref(l)); } break;; default: @@ -1549,7 +1549,15 @@ int lua_cjson_new(lua_State *l) }; /* Initialise number conversions */ - fpconv_init(); + lua_getfield(l, LUA_REGISTRYINDEX, "nvim.thread"); + bool is_thread = lua_toboolean(l, -1); + lua_pop(l, 1); + + // Since fpconv_init does not need to be called multiple times and is not + // thread safe, it should only be called in the main thread. + if (!is_thread) { + fpconv_init(); + } /* Test if array metatables are in registry */ lua_pushlightuserdata(l, json_lightudata_mask(&json_empty_array)); @@ -1582,7 +1590,7 @@ int lua_cjson_new(lua_State *l) compat_luaL_setfuncs(l, reg, 1); /* Set cjson.null */ - nlua_pushref(l, nlua_nil_ref); + nlua_pushref(l, nlua_get_nil_ref(l)); lua_setfield(l, -2, "null"); /* Set cjson.empty_array_mt */ diff --git a/src/mpack/lmpack.c b/src/mpack/lmpack.c index 126f2f3824..53d7092a0c 100644 --- a/src/mpack/lmpack.c +++ b/src/mpack/lmpack.c @@ -246,7 +246,7 @@ static mpack_uint32_t lmpack_objlen(lua_State *L, int *is_array) } end: - if ((size_t)-1 > (mpack_uint32_t)-1 && len > (mpack_uint32_t)-1) + if ((size_t)-1 > (mpack_uint32_t)-1 && len > (mpack_uint32_t)-1) // -V560 /* msgpack spec doesn't allow lengths > 32 bits */ len = (mpack_uint32_t)-1; assert(top == lua_gettop(L)); diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index dcc20194f0..3ba3923a82 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -63,6 +63,7 @@ set(LUA_INSPECT_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/inspect.lua) set(LUA_F_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/F.lua) set(LUA_META_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/_meta.lua) set(LUA_FILETYPE_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/filetype.lua) +set(LUA_LOAD_PACKAGE_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/_load_package.lua) set(CHAR_BLOB_GENERATOR ${GENERATOR_DIR}/gen_char_blob.lua) set(LINT_SUPPRESS_FILE ${PROJECT_BINARY_DIR}/errors.json) set(LINT_SUPPRESS_URL_BASE "https://raw.githubusercontent.com/neovim/doc/gh-pages/reports/clint") @@ -336,6 +337,7 @@ add_custom_command( ${LUA_F_MODULE_SOURCE} lua_F_module ${LUA_META_MODULE_SOURCE} lua_meta_module ${LUA_FILETYPE_MODULE_SOURCE} lua_filetype_module + ${LUA_LOAD_PACKAGE_MODULE_SOURCE} lua_load_package_module DEPENDS ${CHAR_BLOB_GENERATOR} ${LUA_VIM_MODULE_SOURCE} @@ -344,6 +346,7 @@ add_custom_command( ${LUA_F_MODULE_SOURCE} ${LUA_META_MODULE_SOURCE} ${LUA_FILETYPE_MODULE_SOURCE} + ${LUA_LOAD_PACKAGE_MODULE_SOURCE} VERBATIM ) diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c new file mode 100644 index 0000000000..deb8ec8cf3 --- /dev/null +++ b/src/nvim/api/autocmd.c @@ -0,0 +1,669 @@ +#include <stdbool.h> +#include <stdio.h> + +#include "lauxlib.h" +#include "nvim/api/autocmd.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/ascii.h" +#include "nvim/buffer.h" +#include "nvim/eval/typval.h" +#include "nvim/fileio.h" +#include "nvim/lua/executor.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/autocmd.c.generated.h" +#endif + +#define AUCMD_MAX_PATTERNS 256 + +// Check whether every item in the array is a kObjectTypeString +#define CHECK_STRING_ARRAY(__array, k, v, goto_name) \ + for (size_t j = 0; j < __array.size; j++) { \ + Object item = __array.items[j]; \ + if (item.type != kObjectTypeString) { \ + api_set_error(err, \ + kErrorTypeValidation, \ + "All entries in '%s' must be strings", \ + k); \ + goto goto_name; \ + } \ + } + +// Copy string or array of strings into an empty array. +#define UNPACK_STRING_OR_ARRAY(__array, k, v, goto_name) \ + if (v->type == kObjectTypeString) { \ + ADD(__array, copy_object(*v)); \ + } else if (v->type == kObjectTypeArray) { \ + CHECK_STRING_ARRAY(__array, k, v, goto_name); \ + __array = copy_array(v->data.array); \ + } else { \ + api_set_error(err, \ + kErrorTypeValidation, \ + "'%s' must be an array or a string.", \ + k); \ + goto goto_name; \ + } + +// Get the event number, unless it is an error. Then goto `goto_name`. +#define GET_ONE_EVENT(event_nr, event_str, goto_name) \ + char_u *__next_ev; \ + event_T event_nr = \ + event_name2nr((char_u *)event_str.data.string.data, &__next_ev); \ + if (event_nr >= NUM_EVENTS) { \ + api_set_error(err, kErrorTypeValidation, "unexpected event"); \ + goto goto_name; \ + } + + +// ID for associating autocmds created via nvim_create_autocmd +// Used to delete autocmds from nvim_del_autocmd +static int64_t next_autocmd_id = 1; + +/// Get autocmds that match the requirements passed to {opts}. +/// group +/// event +/// pattern +/// +/// -- @param {string} event - event or events to match against +/// vim.api.nvim_get_autocmds({ event = "FileType" }) +/// +Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) + FUNC_API_SINCE(9) +{ + Array autocmd_list = ARRAY_DICT_INIT; + char_u *pattern_filters[AUCMD_MAX_PATTERNS]; + char_u pattern_buflocal[BUFLOCAL_PAT_LEN]; + + bool event_set[NUM_EVENTS] = { false }; + bool check_event = false; + + int group = 0; + + if (opts->group.type != kObjectTypeNil) { + Object v = opts->group; + if (v.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "group must be a string."); + goto cleanup; + } + + group = augroup_find(v.data.string.data); + + if (group < 0) { + api_set_error(err, kErrorTypeValidation, "invalid augroup passed."); + goto cleanup; + } + } + + if (opts->event.type != kObjectTypeNil) { + check_event = true; + + Object v = opts->event; + if (v.type == kObjectTypeString) { + GET_ONE_EVENT(event_nr, v, cleanup); + event_set[event_nr] = true; + } else if (v.type == kObjectTypeArray) { + FOREACH_ITEM(v.data.array, event_v, { + if (event_v.type != kObjectTypeString) { + api_set_error(err, + kErrorTypeValidation, + "Every event must be a string in 'event'"); + goto cleanup; + } + + GET_ONE_EVENT(event_nr, event_v, cleanup); + event_set[event_nr] = true; + }) + } else { + api_set_error(err, + kErrorTypeValidation, + "Not a valid 'event' value. Must be a string or an array"); + goto cleanup; + } + } + + int pattern_filter_count = 0; + if (opts->pattern.type != kObjectTypeNil) { + Object v = opts->pattern; + if (v.type == kObjectTypeString) { + pattern_filters[pattern_filter_count] = (char_u *)v.data.string.data; + pattern_filter_count += 1; + } else if (v.type == kObjectTypeArray) { + FOREACH_ITEM(v.data.array, item, { + pattern_filters[pattern_filter_count] = (char_u *)item.data.string.data; + pattern_filter_count += 1; + }); + } else { + api_set_error(err, + kErrorTypeValidation, + "Not a valid 'pattern' value. Must be a string or an array"); + goto cleanup; + } + + if (pattern_filter_count >= AUCMD_MAX_PATTERNS) { + api_set_error(err, + kErrorTypeValidation, + "Too many patterns. Please limit yourself to less"); + goto cleanup; + } + } + + FOR_ALL_AUEVENTS(event) { + if (check_event && !event_set[event]) { + continue; + } + + for (AutoPat *ap = au_get_autopat_for_event(event); + ap != NULL; + ap = ap->next) { + if (ap == NULL || ap->cmds == NULL) { + continue; + } + + // Skip autocmds from invalid groups if passed. + if (group != 0 && ap->group != group) { + continue; + } + + // Skip 'pattern' from invalid patterns if passed. + if (pattern_filter_count > 0) { + bool passed = false; + for (int i = 0; i < pattern_filter_count; i++) { + assert(i < AUCMD_MAX_PATTERNS); + assert(pattern_filters[i]); + + char_u *pat = pattern_filters[i]; + int patlen = (int)STRLEN(pat); + + if (aupat_is_buflocal(pat, patlen)) { + aupat_normalize_buflocal_pat(pattern_buflocal, + pat, + patlen, + aupat_get_buflocal_nr(pat, patlen)); + + pat = pattern_buflocal; + } + + if (strequal((char *)ap->pat, (char *)pat)) { + passed = true; + break; + } + } + + if (!passed) { + continue; + } + } + + for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { + if (aucmd_exec_is_deleted(ac->exec)) { + continue; + } + Dictionary autocmd_info = ARRAY_DICT_INIT; + + if (ap->group != AUGROUP_DEFAULT) { + PUT(autocmd_info, "group", INTEGER_OBJ(ap->group)); + } + + if (ac->id > 0) { + PUT(autocmd_info, "id", INTEGER_OBJ(ac->id)); + } + + if (ac->desc != NULL) { + PUT(autocmd_info, "desc", CSTR_TO_OBJ(ac->desc)); + } + + PUT(autocmd_info, + "command", + STRING_OBJ(cstr_to_string(aucmd_exec_to_string(ac, ac->exec)))); + + PUT(autocmd_info, + "pattern", + STRING_OBJ(cstr_to_string((char *)ap->pat))); + + PUT(autocmd_info, "once", BOOLEAN_OBJ(ac->once)); + + if (ap->buflocal_nr) { + PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(true)); + PUT(autocmd_info, "buffer", INTEGER_OBJ(ap->buflocal_nr)); + } else { + PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(false)); + } + + // TODO(sctx): It would be good to unify script_ctx to actually work with lua + // right now it's just super weird, and never really gives you the info that + // you would expect from this. + // + // I think we should be able to get the line number, filename, etc. from lua + // when we're executing something, and it should be easy to then save that + // info here. + // + // I think it's a big loss not getting line numbers of where options, autocmds, + // etc. are set (just getting "Sourced (lua)" or something is not that helpful. + // + // Once we do that, we can put these into the autocmd_info, but I don't think it's + // useful to do that at this time. + // + // PUT(autocmd_info, "sid", INTEGER_OBJ(ac->script_ctx.sc_sid)); + // PUT(autocmd_info, "lnum", INTEGER_OBJ(ac->script_ctx.sc_lnum)); + + ADD(autocmd_list, DICTIONARY_OBJ(autocmd_info)); + } + } + } + +cleanup: + return autocmd_list; +} + +/// Define an autocmd. +/// @param opts Dictionary +/// Required keys: +/// event: string | ArrayOf(string) +/// event = "pat1,pat2,pat3", +/// event = "pat1" +/// event = {"pat1"} +/// event = {"pat1", "pat2", "pat3"} +/// +/// +/// -- @param {string} name - augroup name +/// -- @param {string | table} event - event or events to match against +/// -- @param {string | table} pattern - pattern or patterns to match against +/// -- @param {string | function} callback - function or string to execute on autocmd +/// -- @param {string} command - optional, vimscript command +/// Eg. command = "let g:value_set = v:true" +/// -- @param {boolean} once - optional, defaults to false +/// +/// -- pattern = comma delimited list of patterns | pattern | { pattern, ... } +/// +/// pattern = "*.py,*.pyi" +/// pattern = "*.py" +/// pattern = {"*.py"} +/// pattern = { "*.py", "*.pyi" } +/// +/// -- not supported +/// pattern = {"*.py,*.pyi"} +/// +/// -- event = string | string[] +/// event = "FileType,CursorHold" +/// event = "BufPreWrite" +/// event = {"BufPostWrite"} +/// event = {"CursorHold", "BufPreWrite", "BufPostWrite"} +Integer nvim_create_autocmd(uint64_t channel_id, Dict(create_autocmd) *opts, Error *err) + FUNC_API_SINCE(9) +{ + int64_t autocmd_id = -1; + + const char_u pattern_buflocal[BUFLOCAL_PAT_LEN]; + int au_group = AUGROUP_DEFAULT; + char *desc = NULL; + + Array patterns = ARRAY_DICT_INIT; + Array event_array = ARRAY_DICT_INIT; + + AucmdExecutable aucmd = AUCMD_EXECUTABLE_INIT; + Callback cb = CALLBACK_NONE; + + if (opts->callback.type != kObjectTypeNil && opts->command.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "cannot pass both: 'callback' and 'command' for the same autocmd"); + goto cleanup; + } else if (opts->callback.type != kObjectTypeNil) { + // TODO(tjdevries): It's possible we could accept callable tables, + // but we don't do that many other places, so for the moment let's + // not do that. + + Object *callback = &opts->callback; + if (callback->type == kObjectTypeLuaRef) { + if (callback->data.luaref == LUA_NOREF) { + api_set_error(err, + kErrorTypeValidation, + "must pass an actual value"); + goto cleanup; + } + + if (!nlua_ref_is_function(callback->data.luaref)) { + api_set_error(err, + kErrorTypeValidation, + "must pass a function for callback"); + goto cleanup; + } + + cb.type = kCallbackLua; + cb.data.luaref = api_new_luaref(callback->data.luaref); + } else if (callback->type == kObjectTypeString) { + cb.type = kCallbackFuncref; + cb.data.funcref = vim_strsave((char_u *)callback->data.string.data); + } else { + api_set_error(err, + kErrorTypeException, + "'callback' must be a lua function or name of vim function"); + goto cleanup; + } + + aucmd.type = CALLABLE_CB; + aucmd.callable.cb = cb; + } else if (opts->command.type != kObjectTypeNil) { + Object *command = &opts->command; + if (command->type == kObjectTypeString) { + aucmd.type = CALLABLE_EX; + aucmd.callable.cmd = vim_strsave((char_u *)command->data.string.data); + } else { + api_set_error(err, + kErrorTypeValidation, + "'command' must be a string"); + goto cleanup; + } + } else { + api_set_error(err, kErrorTypeValidation, "must pass one of: 'command', 'callback'"); + goto cleanup; + } + + if (opts->event.type != kObjectTypeNil) { + UNPACK_STRING_OR_ARRAY(event_array, "event", (&opts->event), cleanup) + } + + bool is_once = api_object_to_bool(opts->once, "once", false, err); + bool is_nested = api_object_to_bool(opts->nested, "nested", false, err); + + // TOOD: accept number for namespace instead + if (opts->group.type != kObjectTypeNil) { + Object *v = &opts->group; + if (v->type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "'group' must be a string"); + goto cleanup; + } + + au_group = augroup_find(v->data.string.data); + + if (au_group == AUGROUP_ERROR) { + api_set_error(err, + kErrorTypeException, + "invalid augroup: %s", v->data.string.data); + + goto cleanup; + } + } + + if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "cannot pass both: 'pattern' and 'buffer' for the same autocmd"); + goto cleanup; + } else if (opts->pattern.type != kObjectTypeNil) { + Object *v = &opts->pattern; + + if (v->type == kObjectTypeString) { + char_u *pat = (char_u *)v->data.string.data; + size_t patlen = aucmd_pattern_length(pat); + while (patlen) { + ADD(patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen))); + + pat = aucmd_next_pattern(pat, patlen); + patlen = aucmd_pattern_length(pat); + } + } else if (v->type == kObjectTypeArray) { + CHECK_STRING_ARRAY(patterns, "pattern", v, cleanup); + + Array array = v->data.array; + for (size_t i = 0; i < array.size; i++) { + char_u *pat = (char_u *)array.items[i].data.string.data; + size_t patlen = aucmd_pattern_length(pat); + while (patlen) { + ADD(patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen))); + + pat = aucmd_next_pattern(pat, patlen); + patlen = aucmd_pattern_length(pat); + } + } + } else { + api_set_error(err, + kErrorTypeValidation, + "'pattern' must be a string"); + goto cleanup; + } + } else if (opts->buffer.type != kObjectTypeNil) { + if (opts->buffer.type != kObjectTypeInteger) { + api_set_error(err, + kErrorTypeValidation, + "'buffer' must be an integer"); + goto cleanup; + } + + buf_T *buf = find_buffer_by_handle((Buffer)opts->buffer.data.integer, err); + if (ERROR_SET(err)) { + goto cleanup; + } + + snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); + ADD(patterns, STRING_OBJ(cstr_to_string((char *)pattern_buflocal))); + } + + if (aucmd.type == CALLABLE_NONE) { + api_set_error(err, + kErrorTypeValidation, + "'command' or 'callback' is required"); + goto cleanup; + } + + if (opts->desc.type != kObjectTypeNil) { + if (opts->desc.type == kObjectTypeString) { + desc = opts->desc.data.string.data; + } else { + api_set_error(err, + kErrorTypeValidation, + "'desc' must be a string"); + goto cleanup; + } + } + + if (patterns.size == 0) { + ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING("*"))); + } + + if (event_array.size == 0) { + api_set_error(err, kErrorTypeValidation, "'event' is a required key"); + goto cleanup; + } + + autocmd_id = next_autocmd_id++; + FOREACH_ITEM(event_array, event_str, { + GET_ONE_EVENT(event_nr, event_str, cleanup); + + int retval; + + for (size_t i = 0; i < patterns.size; i++) { + Object pat = patterns.items[i]; + + // See: TODO(sctx) + WITH_SCRIPT_CONTEXT(channel_id, { + retval = autocmd_register(autocmd_id, + event_nr, + (char_u *)pat.data.string.data, + (int)pat.data.string.size, + au_group, + is_once, + is_nested, + desc, + aucmd); + }); + + if (retval == FAIL) { + api_set_error(err, kErrorTypeException, "Failed to set autocmd"); + goto cleanup; + } + } + }); + + +cleanup: + aucmd_exec_free(&aucmd); + api_free_array(event_array); + api_free_array(patterns); + + return autocmd_id; +} + +/// Delete an autocmd by ID. Autocmds only return IDs when created +/// via the API. +/// +/// @param id Integer The ID returned by nvim_create_autocmd +void nvim_del_autocmd(Integer id) + FUNC_API_SINCE(9) +{ + autocmd_delete_id(id); +} + +/// Create or get an augroup. +/// +/// To get an existing augroup ID, do: +/// <pre> +/// local id = vim.api.nvim_create_augroup({ name = name, clear = false }); +/// </pre> +/// +/// @param opts Parameters +/// - name (string): The name of the augroup +/// - clear (bool): Whether to clear existing commands or not. +// Defaults to true. +/// See |autocmd-groups| +Integer nvim_create_augroup(uint64_t channel_id, Dict(create_augroup) *opts, Error *err) + FUNC_API_SINCE(9) +{ + bool clear_autocmds = api_object_to_bool(opts->clear, "clear", true, err); + + if (opts->name.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "'name' is required and must be a string"); + return -1; + } + char *name = opts->name.data.string.data; + + int augroup = -1; + WITH_SCRIPT_CONTEXT(channel_id, { + augroup = augroup_add(name); + if (augroup == AUGROUP_ERROR) { + api_set_error(err, kErrorTypeException, "Failed to set augroup"); + return -1; + } + + if (clear_autocmds) { + FOR_ALL_AUEVENTS(event) { + aupat_del_for_event_and_group(event, augroup); + } + } + }); + + return augroup; +} + +/// NOTE: behavior differs from augroup-delete. +/// When deleting an augroup, autocmds contained by this augroup will also be deleted and cleared. +/// This augroup will no longer exist +void nvim_del_augroup_by_id(Integer id) + FUNC_API_SINCE(9) +{ + char *name = augroup_name((int)id); + augroup_del(name, false); +} + +/// NOTE: behavior differs from augroup-delete. +/// When deleting an augroup, autocmds contained by this augroup will also be deleted and cleared. +/// This augroup will no longer exist +void nvim_del_augroup_by_name(String name) + FUNC_API_SINCE(9) +{ + augroup_del(name.data, false); +} + +/// -- @param {string} group - autocmd group name +/// -- @param {number} buffer - buffer number +/// -- @param {string | table} event - event or events to match against +/// -- @param {string | table} pattern - optional, defaults to "*". +/// vim.api.nvim_do_autcmd({ group, buffer, pattern, event, modeline }) +void nvim_do_autocmd(Dict(do_autocmd) *opts, Error *err) + FUNC_API_SINCE(9) +{ + int au_group = AUGROUP_ALL; + bool modeline = true; + + buf_T *buf = curbuf; + bool set_buf = false; + + char_u *pattern = NULL; + bool set_pattern = false; + + Array event_array = ARRAY_DICT_INIT; + + if (opts->group.type != kObjectTypeNil) { + if (opts->group.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "'group' must be a string"); + goto cleanup; + } + + au_group = augroup_find(opts->group.data.string.data); + + if (au_group == AUGROUP_ERROR) { + api_set_error(err, + kErrorTypeException, + "invalid augroup: %s", opts->group.data.string.data); + + goto cleanup; + } + } + + if (opts->buffer.type != kObjectTypeNil) { + Object buf_obj = opts->buffer; + if (buf_obj.type != kObjectTypeInteger && buf_obj.type != kObjectTypeBuffer) { + api_set_error(err, kErrorTypeException, "invalid buffer: %d", buf_obj.type); + goto cleanup; + } + + buf = find_buffer_by_handle((Buffer)buf_obj.data.integer, err); + set_buf = true; + + if (ERROR_SET(err)) { + goto cleanup; + } + } + + if (opts->pattern.type != kObjectTypeNil) { + if (opts->pattern.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "'pattern' must be a string"); + goto cleanup; + } + + pattern = vim_strsave((char_u *)opts->pattern.data.string.data); + set_pattern = true; + } + + if (opts->event.type != kObjectTypeNil) { + UNPACK_STRING_OR_ARRAY(event_array, "event", (&opts->event), cleanup) + } + + if (opts->modeline.type != kObjectTypeNil) { + modeline = api_object_to_bool(opts->modeline, "modeline", true, err); + } + + if (set_pattern && set_buf) { + api_set_error(err, kErrorTypeValidation, "must not set 'buffer' and 'pattern'"); + goto cleanup; + } + + bool did_aucmd = false; + FOREACH_ITEM(event_array, event_str, { + GET_ONE_EVENT(event_nr, event_str, cleanup) + + did_aucmd |= apply_autocmds_group(event_nr, pattern, NULL, true, au_group, buf, NULL); + }) + + if (did_aucmd && modeline) { + do_modelines(0); + } + +cleanup: + api_free_array(event_array); + XFREE_CLEAR(pattern); +} + + +#undef UNPACK_STRING_OR_ARRAY +#undef CHECK_STRING_ARRAY +#undef GET_ONE_EVENT diff --git a/src/nvim/api/autocmd.h b/src/nvim/api/autocmd.h new file mode 100644 index 0000000000..f9432830d9 --- /dev/null +++ b/src/nvim/api/autocmd.h @@ -0,0 +1,11 @@ +#ifndef NVIM_API_AUTOCMD_H +#define NVIM_API_AUTOCMD_H + +#include <stdint.h> + +#include "nvim/api/private/defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/autocmd.h.generated.h" +#endif +#endif // NVIM_API_AUTOCMD_H diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 2d5403d4b8..922d288da1 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -287,8 +287,8 @@ ArrayOf(String) nvim_buf_get_lines(uint64_t channel_id, } bool oob = false; - start = normalize_index(buf, start, &oob); - end = normalize_index(buf, end, &oob); + start = normalize_index(buf, start, true, &oob); + end = normalize_index(buf, end, true, &oob); if (strict_indexing && oob) { api_set_error(err, kErrorTypeValidation, "Index out of bounds"); @@ -374,15 +374,14 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ } bool oob = false; - start = normalize_index(buf, start, &oob); - end = normalize_index(buf, end, &oob); + start = normalize_index(buf, start, true, &oob); + end = normalize_index(buf, end, true, &oob); if (strict_indexing && oob) { api_set_error(err, kErrorTypeValidation, "Index out of bounds"); return; } - if (start > end) { api_set_error(err, kErrorTypeValidation, @@ -554,13 +553,13 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In // check range is ordered and everything! // start_row, end_row within buffer len (except add text past the end?) - start_row = normalize_index(buf, start_row, &oob); + start_row = normalize_index(buf, start_row, false, &oob); if (oob || start_row == buf->b_ml.ml_line_count + 1) { api_set_error(err, kErrorTypeValidation, "start_row out of bounds"); return; } - end_row = normalize_index(buf, end_row, &oob); + end_row = normalize_index(buf, end_row, false, &oob); if (oob || end_row == buf->b_ml.ml_line_count + 1) { api_set_error(err, kErrorTypeValidation, "end_row out of bounds"); return; @@ -757,6 +756,108 @@ end: try_end(err); } +/// Gets a range from the buffer. +/// +/// This differs from |nvim_buf_get_lines()| in that it allows retrieving only +/// portions of a line. +/// +/// Indexing is zero-based. Column indices are end-exclusive. +/// +/// Prefer |nvim_buf_get_lines()| when retrieving entire lines. +/// +/// @param channel_id +/// @param buffer Buffer handle, or 0 for current buffer +/// @param start_row First line index +/// @param start_col Starting byte offset of first line +/// @param end_row Last line index +/// @param end_col Ending byte offset of last line (exclusive) +/// @param opts Optional parameters. Currently unused. +/// @param[out] err Error details, if any +/// @return Array of lines, or empty array for unloaded buffer. +ArrayOf(String) nvim_buf_get_text(uint64_t channel_id, Buffer buffer, + Integer start_row, Integer start_col, + Integer end_row, Integer end_col, + Dictionary opts, Error *err) + FUNC_API_SINCE(9) +{ + Array rv = ARRAY_DICT_INIT; + + if (opts.size > 0) { + api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); + return rv; + } + + buf_T *buf = find_buffer_by_handle(buffer, err); + + if (!buf) { + return rv; + } + + // return sentinel value if the buffer isn't loaded + if (buf->b_ml.ml_mfp == NULL) { + return rv; + } + + bool oob = false; + start_row = normalize_index(buf, start_row, false, &oob); + end_row = normalize_index(buf, end_row, false, &oob); + + if (oob) { + api_set_error(err, kErrorTypeValidation, "Index out of bounds"); + return rv; + } + + // nvim_buf_get_lines doesn't care if the start row is greater than the end + // row (it will just return an empty array), but nvim_buf_get_text does in + // order to maintain symmetry with nvim_buf_set_text. + if (start_row > end_row) { + api_set_error(err, kErrorTypeValidation, "start is higher than end"); + return rv; + } + + bool replace_nl = (channel_id != VIML_INTERNAL_CALL); + + if (start_row == end_row) { + String line = buf_get_text(buf, start_row, start_col, end_col, replace_nl, err); + if (ERROR_SET(err)) { + return rv; + } + + ADD(rv, STRING_OBJ(line)); + return rv; + } + + rv.size = (size_t)(end_row - start_row) + 1; + rv.items = xcalloc(rv.size, sizeof(Object)); + + rv.items[0] = STRING_OBJ(buf_get_text(buf, start_row, start_col, MAXCOL-1, replace_nl, err)); + if (ERROR_SET(err)) { + goto end; + } + + if (rv.size > 2) { + Array tmp = ARRAY_DICT_INIT; + tmp.items = &rv.items[1]; + if (!buf_collect_lines(buf, rv.size - 2, start_row + 1, replace_nl, &tmp, err)) { + goto end; + } + } + + rv.items[rv.size-1] = STRING_OBJ(buf_get_text(buf, end_row, 0, end_col, replace_nl, err)); + if (ERROR_SET(err)) { + goto end; + } + +end: + if (ERROR_SET(err)) { + api_free_array(rv); + rv.size = 0; + rv.items = NULL; + } + + return rv; +} + /// Returns the byte offset of a line (0-indexed). |api-indexing| /// /// Line 1 (index=0) has offset 0. UTF-8 bytes are counted. EOL is one byte. @@ -1386,11 +1487,11 @@ static void fix_cursor(linenr_T lo, linenr_T hi, linenr_T extra) } // Normalizes 0-based indexes to buffer line numbers -static int64_t normalize_index(buf_T *buf, int64_t index, bool *oob) +static int64_t normalize_index(buf_T *buf, int64_t index, bool end_exclusive, bool *oob) { int64_t line_count = buf->b_ml.ml_line_count; // Fix if < 0 - index = index < 0 ? line_count + index +1 : index; + index = index < 0 ? line_count + index + (int)end_exclusive : index; // Check for oob if (index > line_count) { diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index f6dce1905e..be71c446b1 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -97,6 +97,7 @@ return { "special"; "sp"; "link"; "fallback"; + "blend"; "temp"; }; highlight_cterm = { @@ -109,5 +110,34 @@ return { "reverse"; "nocombine"; }; + -- Autocmds + create_autocmd = { + "buffer"; + "callback"; + "command"; + "desc"; + "event"; + "group"; + "once"; + "nested"; + "pattern"; + }; + do_autocmd = { + "buffer"; + "event"; + "group"; + "modeline"; + "pattern"; + }; + get_autocmds = { + "event"; + "group"; + "id"; + "pattern"; + }; + create_augroup = { + "clear"; + "name"; + }; } diff --git a/src/nvim/api/private/dispatch.c b/src/nvim/api/private/dispatch.c index 8ab7743e01..f670f06357 100644 --- a/src/nvim/api/private/dispatch.c +++ b/src/nvim/api/private/dispatch.c @@ -6,22 +6,30 @@ #include <msgpack.h> #include <stdbool.h> -#include "nvim/api/buffer.h" #include "nvim/api/deprecated.h" -#include "nvim/api/extmark.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" +#include "nvim/log.h" +#include "nvim/map.h" +#include "nvim/msgpack_rpc/helpers.h" +#include "nvim/vim.h" + +// =========================================================================== +// NEW API FILES MUST GO HERE. +// +// When creating a new API file, you must include it here, +// so that the dispatcher can find the C functions that you are creating! +// =========================================================================== +#include "nvim/api/autocmd.h" +#include "nvim/api/buffer.h" +#include "nvim/api/extmark.h" #include "nvim/api/tabpage.h" #include "nvim/api/ui.h" #include "nvim/api/vim.h" #include "nvim/api/vimscript.h" #include "nvim/api/win_config.h" #include "nvim/api/window.h" -#include "nvim/log.h" -#include "nvim/map.h" -#include "nvim/msgpack_rpc/helpers.h" -#include "nvim/vim.h" static Map(String, MsgpackRpcRequestHandler) methods = MAP_INIT; diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 2b107a3f27..3d4a04f096 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -396,22 +396,16 @@ void set_option_to(uint64_t channel_id, void *to, int type, String name, Object stringval = value.data.string.data; } - const sctx_T save_current_sctx = current_sctx; - current_sctx.sc_sid = - channel_id == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; - current_sctx.sc_lnum = 0; - current_channel_id = channel_id; - - const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL)) - ? 0 : (type == SREQ_GLOBAL) - ? OPT_GLOBAL : OPT_LOCAL; - set_option_value_for(name.data, numval, stringval, - opt_flags, type, to, err); - - current_sctx = save_current_sctx; + WITH_SCRIPT_CONTEXT(channel_id, { + const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL)) + ? 0 : (type == SREQ_GLOBAL) + ? OPT_GLOBAL : OPT_LOCAL; + + set_option_value_for(name.data, numval, stringval, + opt_flags, type, to, err); + }); } - buf_T *find_buffer_by_handle(Buffer buffer, Error *err) { if (buffer == 0) { @@ -758,6 +752,52 @@ bool buf_collect_lines(buf_T *buf, size_t n, int64_t start, bool replace_nl, Arr return true; } +/// Returns a substring of a buffer line +/// +/// @param buf Buffer handle +/// @param lnum Line number (1-based) +/// @param start_col Starting byte offset into line (0-based) +/// @param end_col Ending byte offset into line (0-based, exclusive) +/// @param replace_nl Replace newlines ('\n') with null ('\0') +/// @param err Error object +/// @return The text between start_col and end_col on line lnum of buffer buf +String buf_get_text(buf_T *buf, int64_t lnum, int64_t start_col, int64_t end_col, bool replace_nl, + Error *err) +{ + String rv = STRING_INIT; + + if (lnum >= MAXLNUM) { + api_set_error(err, kErrorTypeValidation, "Line index is too high"); + return rv; + } + + const char *bufstr = (char *)ml_get_buf(buf, (linenr_T)lnum, false); + size_t line_length = strlen(bufstr); + + start_col = start_col < 0 ? (int64_t)line_length + start_col + 1 : start_col; + end_col = end_col < 0 ? (int64_t)line_length + end_col + 1 : end_col; + + if (start_col >= MAXCOL || end_col >= MAXCOL) { + api_set_error(err, kErrorTypeValidation, "Column index is too high"); + return rv; + } + + if (start_col > end_col) { + api_set_error(err, kErrorTypeValidation, "start_col must be less than end_col"); + return rv; + } + + if ((size_t)start_col >= line_length) { + return rv; + } + + rv = cstrn_to_string(&bufstr[start_col], (size_t)(end_col - start_col)); + if (replace_nl) { + strchrsub(rv.data, '\n', '\0'); + } + + return rv; +} void api_free_string(String value) { @@ -1569,3 +1609,16 @@ err: NLUA_CLEAR_REF(luaref); NLUA_CLEAR_REF(compl_luaref); } + +int find_sid(uint64_t channel_id) +{ + switch (channel_id) { + case VIML_INTERNAL_CALL: + // TODO(autocmd): Figure out what this should be + // return SID_API_CLIENT; + case LUA_INTERNAL_CALL: + return SID_LUA; + default: + return SID_API_CLIENT; + } +} diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index 6d0aec9c90..6969994c3b 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -138,10 +138,27 @@ typedef struct { msg_list = saved_msg_list; /* Restore the exception context. */ \ } while (0) +// Useful macro for executing some `code` for each item in an array. +#define FOREACH_ITEM(a, __foreach_item, code) \ + for (size_t __foreach_i = 0; __foreach_i < (a).size; __foreach_i++) { \ + Object __foreach_item = (a).items[__foreach_i]; \ + code; \ + } + + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/private/helpers.h.generated.h" # include "keysets.h.generated.h" #endif +#define WITH_SCRIPT_CONTEXT(channel_id, code) \ + const sctx_T save_current_sctx = current_sctx; \ + current_sctx.sc_sid = \ + (channel_id) == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; \ + current_sctx.sc_lnum = 0; \ + current_channel_id = channel_id; \ + code; \ + current_sctx = save_current_sctx; + #endif // NVIM_API_PRIVATE_HELPERS_H diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 11bb1750e4..302dccbde7 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -124,6 +124,10 @@ Dictionary nvim__get_hl_defs(Integer ns_id, Error *err) /// Set a highlight group. /// +/// Note: unlike the `:highlight` command which can update a highlight group, +/// this function completely replaces the definition. For example: +/// `nvim_set_hl(0, 'Visual', {})` will clear the highlight group 'Visual'. +/// /// @param ns_id number of namespace for this highlight. Use value 0 /// to set a highlight group in the global (`:highlight`) /// namespace. @@ -1951,7 +1955,7 @@ Dictionary nvim__stats(void) Dictionary rv = ARRAY_DICT_INIT; PUT(rv, "fsync", INTEGER_OBJ(g_stats.fsync)); PUT(rv, "redraw", INTEGER_OBJ(g_stats.redraw)); - PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_refcount)); + PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_get_global_ref_count())); return rv; } @@ -2411,6 +2415,8 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * /// from Lua, the command can also be a Lua function. The function is called with a /// single table argument that contains the following keys: /// - args: (string) The args passed to the command, if any |<args>| +/// - fargs: (table) The args split by unescaped whitespace (when more than one +/// argument is allowed), if any |<f-args>| /// - bang: (boolean) "true" if the command was executed with a ! modifier |<bang>| /// - line1: (number) The starting line of the command range |<line1>| /// - line2: (number) The final line of the command range |<line2>| diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c index fc7823a070..9c473ff724 100644 --- a/src/nvim/api/window.c +++ b/src/nvim/api/window.c @@ -71,6 +71,7 @@ ArrayOf(Integer, 2) nvim_win_get_cursor(Window window, Error *err) } /// Sets the (1,0)-indexed cursor position in the window. |api-indexing| +/// Unlike |win_execute()| this scrolls the window. /// /// @param window Window handle, or 0 for current window /// @param pos (row, col) tuple representing the new position @@ -118,6 +119,8 @@ void nvim_win_set_cursor(Window window, ArrayOf(Integer, 2) pos, Error *err) update_topline_win(win); redraw_later(win, VALID); + redraw_for_cursorline(win); + win->w_redr_status = true; } /// Gets the window height diff --git a/src/nvim/aucmd.c b/src/nvim/aucmd.c deleted file mode 100644 index d7f73fa4a1..0000000000 --- a/src/nvim/aucmd.c +++ /dev/null @@ -1,123 +0,0 @@ -// This is an open source non-commercial project. Dear PVS-Studio, please check -// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com - -#include "nvim/aucmd.h" -#include "nvim/buffer.h" -#include "nvim/eval.h" -#include "nvim/ex_docmd.h" -#include "nvim/ex_getln.h" -#include "nvim/fileio.h" -#include "nvim/main.h" -#include "nvim/os/os.h" -#include "nvim/ui.h" -#include "nvim/vim.h" - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "aucmd.c.generated.h" -#endif - -void do_autocmd_uienter(uint64_t chanid, bool attached) -{ - static bool recursive = false; - - if (recursive) { - return; // disallow recursion - } - recursive = true; - - save_v_event_T save_v_event; - dict_T *dict = get_v_event(&save_v_event); - assert(chanid < VARNUMBER_MAX); - tv_dict_add_nr(dict, S_LEN("chan"), (varnumber_T)chanid); - tv_dict_set_keys_readonly(dict); - apply_autocmds(attached ? EVENT_UIENTER : EVENT_UILEAVE, - NULL, NULL, false, curbuf); - restore_v_event(dict, &save_v_event); - - recursive = false; -} - -void init_default_autocmds(void) -{ - // open terminals when opening files that start with term:// -#define PROTO "term://" - do_cmdline_cmd("augroup nvim_terminal"); - do_cmdline_cmd("autocmd BufReadCmd " PROTO "* ++nested " - "if !exists('b:term_title')|call termopen(" - // Capture the command string - "matchstr(expand(\"<amatch>\"), " - "'\\c\\m" PROTO "\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), " - // capture the working directory - "{'cwd': expand(get(matchlist(expand(\"<amatch>\"), " - "'\\c\\m" PROTO "\\(.\\{-}\\)//'), 1, ''))})" - "|endif"); - do_cmdline_cmd("augroup END"); -#undef PROTO - - // limit syntax synchronization in the command window - do_cmdline_cmd("augroup nvim_cmdwin"); - do_cmdline_cmd("autocmd! CmdwinEnter [:>] syntax sync minlines=1 maxlines=1"); - do_cmdline_cmd("augroup END"); -} - -static void focusgained_event(void **argv) -{ - bool *gainedp = argv[0]; - do_autocmd_focusgained(*gainedp); - xfree(gainedp); -} -void aucmd_schedule_focusgained(bool gained) -{ - bool *gainedp = xmalloc(sizeof(*gainedp)); - *gainedp = gained; - loop_schedule_deferred(&main_loop, - event_create(focusgained_event, 1, gainedp)); -} - -static void do_autocmd_focusgained(bool gained) -{ - static bool recursive = false; - static Timestamp last_time = (time_t)0; - bool need_redraw = false; - - if (recursive) { - return; // disallow recursion - } - recursive = true; - need_redraw |= apply_autocmds((gained ? EVENT_FOCUSGAINED : EVENT_FOCUSLOST), - NULL, NULL, false, curbuf); - - // When activated: Check if any file was modified outside of Vim. - // Only do this when not done within the last two seconds as: - // 1. Some filesystems have modification time granularity in seconds. Fat32 - // has a granularity of 2 seconds. - // 2. We could get multiple notifications in a row. - if (gained && last_time + (Timestamp)2000 < os_now()) { - need_redraw = check_timestamps(true); - last_time = os_now(); - } - - if (need_redraw) { - // Something was executed, make sure the cursor is put back where it - // belongs. - need_wait_return = false; - - if (State & CMDLINE) { - redrawcmdline(); - } else if ((State & NORMAL) || (State & INSERT)) { - if (must_redraw != 0) { - update_screen(0); - } - - setcursor(); - } - - ui_flush(); - } - - if (need_maketitle) { - maketitle(); - } - - recursive = false; -} diff --git a/src/nvim/aucmd.h b/src/nvim/aucmd.h deleted file mode 100644 index 9a4dd79a78..0000000000 --- a/src/nvim/aucmd.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef NVIM_AUCMD_H -#define NVIM_AUCMD_H - -#include <stdint.h> - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "aucmd.h.generated.h" -#endif - -#endif // NVIM_AUCMD_H - diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 9117dde089..a9e1978ac0 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -2,10 +2,15 @@ // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com // autocmd.c: Autocommand related functions +#include <signal.h> +#include "nvim/autocmd.h" + +// #include "nvim/api/private/handle.h" + +#include "lauxlib.h" #include "nvim/api/private/helpers.h" #include "nvim/ascii.h" -#include "nvim/autocmd.h" #include "nvim/buffer.h" #include "nvim/charset.h" #include "nvim/cursor.h" @@ -13,8 +18,11 @@ #include "nvim/eval.h" #include "nvim/eval/userfunc.h" #include "nvim/ex_docmd.h" +#include "nvim/ex_getln.h" #include "nvim/fileio.h" #include "nvim/getchar.h" +#include "nvim/lua/executor.h" +#include "nvim/map.h" #include "nvim/option.h" #include "nvim/os/input.h" #include "nvim/regexp.h" @@ -28,6 +36,14 @@ # include "autocmd.c.generated.h" #endif +// Naming Conventions: +// - general autocmd behavior start with au_ +// - AutoCmd start with aucmd_ +// - Autocmd.exec stat with aucmd_exec +// - AutoPat start with aupat_ +// - Groups start with augroup_ +// - Events start with event_ + // // The autocommands are stored in a list for each event. // Autocommands for the same pattern, that are consecutive, are joined @@ -67,21 +83,20 @@ // Code for automatic commands. static AutoPatCmd *active_apc_list = NULL; // stack of active autocommands -/// List of autocmd group names -static garray_T augroups = { 0, 0, sizeof(char_u *), 10, NULL }; -#define AUGROUP_NAME(i) (((char **)augroups.ga_data)[i]) -#define BUFLOCAL_PAT_LEN 25 +// ID for associating autocmds created via nvim_create_autocmd +// Used to delete autocmds from nvim_del_autocmd +static int next_augroup_id = 1; // use get_deleted_augroup() to get this static const char *deleted_augroup = NULL; -// The ID of the current group. Group 0 is the default one. +// The ID of the current group. static int current_augroup = AUGROUP_DEFAULT; -static int au_need_clean = false; // need to delete marked patterns +// Whether we need to delete marked patterns. +// While deleting autocmds, they aren't actually remover, just marked. +static int au_need_clean = false; -static event_T last_event; -static int last_group; static int autocmd_blocked = 0; // block all autocmds static bool autocmd_nested = false; @@ -89,6 +104,33 @@ static bool autocmd_include_groups = false; static char_u *old_termresponse = NULL; +/// Iterates over all the AutoPats for a particular event +#define FOR_ALL_AUPATS_IN_EVENT(event, ap) \ + for (AutoPat *ap = first_autopat[event]; ap != NULL; ap = ap->next) // NOLINT + +/// Handles grabbing arguments from `:autocmd` such as ++once and ++nested +#define ARG_GET_FLAG(errored, cmd, flag, pattern, len) \ + if (STRNCMP(cmd, pattern, len) == 0 && ascii_iswhite((cmd)[len])) { \ + if (flag) { \ + semsg(_(e_duparg2), pattern); \ + (errored) = true; \ + } \ + (flag) = true; \ + (cmd) = skipwhite((cmd) + (len)); \ + } + +// Map of autocmd group names. +// name -> ID +static Map(String, int) augroup_map = MAP_INIT; + +static void augroup_map_del(char *name) +{ + String key = map_key(String, int)(&augroup_map, cstr_as_string(name)); + map_del(String, int)(&augroup_map, key); + api_free_string(key); +} + + static inline const char *get_deleted_augroup(void) FUNC_ATTR_ALWAYS_INLINE { if (deleted_augroup == NULL) { @@ -98,7 +140,7 @@ static inline const char *get_deleted_augroup(void) FUNC_ATTR_ALWAYS_INLINE } // Show the autocommands for one AutoPat. -static void show_autocmd(AutoPat *ap, event_T event) +static void aupat_show(AutoPat *ap) { AutoCmd *ac; @@ -107,39 +149,21 @@ static void show_autocmd(AutoPat *ap, event_T event) if (got_int) { return; } + // pattern has been removed if (ap->pat == NULL) { return; } - msg_putchar('\n'); - if (got_int) { - return; - } - if (event != last_event || ap->group != last_group) { - if (ap->group != AUGROUP_DEFAULT) { - if (AUGROUP_NAME(ap->group) == NULL) { - msg_puts_attr(get_deleted_augroup(), HL_ATTR(HLF_E)); - } else { - msg_puts_attr(AUGROUP_NAME(ap->group), HL_ATTR(HLF_T)); - } - msg_puts(" "); - } - msg_puts_attr(event_nr2name(event), HL_ATTR(HLF_T)); - last_event = event; - last_group = ap->group; - msg_putchar('\n'); - if (got_int) { - return; - } - } msg_col = 4; msg_outtrans(ap->pat); for (ac = ap->cmds; ac != NULL; ac = ac->next) { - if (ac->cmd == NULL) { // skip removed commands + // skip removed commands + if (aucmd_exec_is_deleted(ac->exec)) { continue; } + if (msg_col >= 14) { msg_putchar('\n'); } @@ -147,7 +171,7 @@ static void show_autocmd(AutoPat *ap, event_T event) if (got_int) { return; } - msg_outtrans(ac->cmd); + msg_outtrans((char_u *)aucmd_exec_to_string(ac, ac->exec)); if (p_verbose > 0) { last_set_msg(ac->script_ctx); } @@ -163,27 +187,96 @@ static void show_autocmd(AutoPat *ap, event_T event) } } +static void au_show_for_all_events(int group) +{ + FOR_ALL_AUEVENTS(event) { + au_show_for_event(group, event); + } +} + +static void au_show_for_event(int group, event_T event) +{ + // Return early if there are no autocmds for this event + if (au_event_is_empty(event)) { + return; + } + + int previous_group = AUGROUP_ERROR; + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + if (group != AUGROUP_ALL && group != ap->group) { + continue; + } + + char *name = augroup_name(ap->group); + + msg_putchar('\n'); + // When switching groups, we need to show the new group information. + if (ap->group != previous_group) { + // show the group name, if it's not the default group + if (ap->group != AUGROUP_DEFAULT) { + if (name == NULL) { + msg_puts_attr(get_deleted_augroup(), HL_ATTR(HLF_E)); + } else { + msg_puts_attr(name, HL_ATTR(HLF_T)); + } + msg_puts(" "); + } + + // show the event name + msg_puts_attr(event_nr2name(event), HL_ATTR(HLF_T)); + msg_putchar('\n'); + } + + if (got_int) { + return; + } + + aupat_show(ap); + + previous_group = ap->group; + } +} + // Mark an autocommand handler for deletion. -static void au_remove_pat(AutoPat *ap) +static void aupat_del(AutoPat *ap) { XFREE_CLEAR(ap->pat); ap->buflocal_nr = -1; au_need_clean = true; } +void aupat_del_for_event_and_group(event_T event, int group) +{ + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + if (ap->group == group) { + aupat_del(ap); + } + } + + au_need_clean = true; + au_cleanup(); // may really delete removed patterns/commands now +} + // Mark all commands for a pattern for deletion. -static void au_remove_cmds(AutoPat *ap) +static void aupat_remove_cmds(AutoPat *ap) { for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { - XFREE_CLEAR(ac->cmd); + aucmd_exec_free(&ac->exec); + + if (ac->desc != NULL) { + XFREE_CLEAR(ac->desc); + } } au_need_clean = true; } // Delete one command from an autocmd pattern. -static void au_del_cmd(AutoCmd *ac) +static void aucmd_del(AutoCmd *ac) { - XFREE_CLEAR(ac->cmd); + aucmd_exec_free(&ac->exec); + if (ac->desc != NULL) { + XFREE_CLEAR(ac->desc); + } au_need_clean = true; } @@ -191,16 +284,15 @@ static void au_del_cmd(AutoCmd *ac) /// This is only done when not executing autocommands. static void au_cleanup(void) { - AutoPat *ap, **prev_ap; - event_T event; + AutoPat *ap; + AutoPat **prev_ap; if (autocmd_busy || !au_need_clean) { return; } // Loop over all events. - for (event = (event_T)0; (int)event < NUM_EVENTS; - event = (event_T)((int)event + 1)) { + FOR_ALL_AUEVENTS(event) { // Loop over all autocommand patterns. prev_ap = &(first_autopat[(int)event]); for (ap = *prev_ap; ap != NULL; ap = *prev_ap) { @@ -211,9 +303,13 @@ static void au_cleanup(void) for (AutoCmd *ac = *prev_ac; ac != NULL; ac = *prev_ac) { // Remove the command if the pattern is to be deleted or when // the command has been marked for deletion. - if (ap->pat == NULL || ac->cmd == NULL) { + if (ap->pat == NULL || aucmd_exec_is_deleted(ac->exec)) { *prev_ac = ac->next; - xfree(ac->cmd); + aucmd_exec_free(&ac->exec); + if (ac->desc != NULL) { + XFREE_CLEAR(ac->desc); + } + xfree(ac); } else { has_cmd = true; @@ -224,7 +320,7 @@ static void au_cleanup(void) if (ap->pat != NULL && !has_cmd) { // Pattern was not marked for deletion, but all of its commands were. // So mark the pattern for deletion. - au_remove_pat(ap); + aupat_del(ap); } // Remove the pattern if it has been marked for deletion. @@ -250,12 +346,17 @@ static void au_cleanup(void) au_need_clean = false; } + +// Get the first AutoPat for a particular event. +AutoPat *au_get_autopat_for_event(event_T event) +{ + return first_autopat[(int)event]; +} + // Called when buffer is freed, to remove/invalidate related buffer-local // autocmds. void aubuflocal_remove(buf_T *buf) { - AutoPat *ap; - event_T event; AutoPatCmd *apc; // invalidate currently executing autocommands @@ -266,12 +367,11 @@ void aubuflocal_remove(buf_T *buf) } // invalidate buflocals looping through events - for (event = (event_T)0; (int)event < NUM_EVENTS; - event = (event_T)((int)event + 1)) { - // loop over all autocommand patterns - for (ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) { + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { if (ap->buflocal_nr == buf->b_fnum) { - au_remove_pat(ap); + aupat_del(ap); + if (p_verbose >= 6) { verbose_enter(); smsg(_("auto-removing autocommand: %s <buffer=%d>"), @@ -284,60 +384,61 @@ void aubuflocal_remove(buf_T *buf) au_cleanup(); } -// Add an autocmd group name. +// Add an autocmd group name or return existing group matching name. // Return its ID. -static int au_new_group(char_u *name) +int augroup_add(char *name) { - int i = au_find_group(name); - if (i == AUGROUP_ERROR) { // the group doesn't exist yet, add it. - // First try using a free entry. - for (i = 0; i < augroups.ga_len; i++) { - if (AUGROUP_NAME(i) == NULL) { - break; - } - } - if (i == augroups.ga_len) { - ga_grow(&augroups, 1); - } + assert(STRICMP(name, "end") != 0); - AUGROUP_NAME(i) = xstrdup((char *)name); - if (i == augroups.ga_len) { - augroups.ga_len++; - } + int existing_id = augroup_find(name); + if (existing_id > 0) { + assert(existing_id != AUGROUP_DELETED); + return existing_id; } - return i; + if (existing_id == AUGROUP_DELETED) { + augroup_map_del(name); + } + + int next_id = next_augroup_id++; + String name_copy = cstr_to_string(name); + map_put(String, int)(&augroup_map, name_copy, next_id); + + return next_id; } -static void au_del_group(char_u *name) +/// Delete the augroup that matches name. +void augroup_del(char *name, bool stupid_legacy_mode) { - int i = au_find_group(name); + int i = augroup_find(name); if (i == AUGROUP_ERROR) { // the group doesn't exist semsg(_("E367: No such group: \"%s\""), name); } else if (i == current_augroup) { emsg(_("E936: Cannot delete the current group")); } else { - event_T event; - AutoPat *ap; - int in_use = false; - - for (event = (event_T)0; (int)event < NUM_EVENTS; - event = (event_T)((int)event + 1)) { - for (ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) { - if (ap->group == i && ap->pat != NULL) { - give_warning((char_u *)_("W19: Deleting augroup that is still in use"), true); - in_use = true; - event = NUM_EVENTS; - break; + if (stupid_legacy_mode) { + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + if (ap->group == i && ap->pat != NULL) { + give_warning((char_u *)_("W19: Deleting augroup that is still in use"), true); + map_put(String, int)(&augroup_map, cstr_as_string(name), AUGROUP_DELETED); + return; + } } } - } - xfree(AUGROUP_NAME(i)); - if (in_use) { - AUGROUP_NAME(i) = (char *)get_deleted_augroup(); } else { - AUGROUP_NAME(i) = NULL; + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + if (ap->group == i) { + aupat_del(ap); + } + } + } } + + // Remove the group because it's not currently in use. + augroup_map_del(name); + au_cleanup(); } } @@ -346,25 +447,70 @@ static void au_del_group(char_u *name) /// @param name augroup name /// /// @return the ID or AUGROUP_ERROR (< 0) for error. -static int au_find_group(const char_u *name) +int augroup_find(const char *name) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { - for (int i = 0; i < augroups.ga_len; i++) { - if (AUGROUP_NAME(i) != NULL && AUGROUP_NAME(i) != get_deleted_augroup() - && STRCMP(AUGROUP_NAME(i), name) == 0) { - return i; - } + int existing_id = map_get(String, int)(&augroup_map, cstr_as_string((char *)name)); + if (existing_id == AUGROUP_DELETED) { + return existing_id; + } + + if (existing_id > 0) { + return existing_id; } + return AUGROUP_ERROR; } +/// Gets the name for a particular group. +char *augroup_name(int group) +{ + assert(group != 0); + + if (group == AUGROUP_DELETED) { + return (char *)get_deleted_augroup(); + } + + if (group == AUGROUP_ALL) { + group = current_augroup; + } + + // next_augroup_id is the "source of truth" about what autocmds have existed + // + // The map_size is not the source of truth because groups can be removed from + // the map. When this happens, the map size is reduced. That's why this function + // relies on next_augroup_id instead. + + // "END" is always considered the last augroup ID. + // Used for expand_get_event_name and expand_get_augroup_name + if (group == next_augroup_id) { + return "END"; + } + + // If it's larger than the largest group, then it doesn't have a name + if (group > next_augroup_id) { + return NULL; + } + + String key; + int value; + map_foreach(&augroup_map, key, value, { + if (value == group) { + return key.data; + } + }); + + // If it's not in the map anymore, then it must have been deleted. + return (char *)get_deleted_augroup(); +} + /// Return true if augroup "name" exists. /// /// @param name augroup name -bool au_has_group(const char_u *name) +bool augroup_exists(const char *name) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { - return au_find_group(name) != AUGROUP_ERROR; + return augroup_find(name) > 0; } /// ":augroup {name}". @@ -374,20 +520,27 @@ void do_augroup(char_u *arg, int del_group) if (*arg == NUL) { emsg(_(e_argreq)); } else { - au_del_group(arg); + augroup_del((char *)arg, true); } } else if (STRICMP(arg, "end") == 0) { // ":aug end": back to group 0 current_augroup = AUGROUP_DEFAULT; } else if (*arg) { // ":aug xxx": switch to group xxx - current_augroup = au_new_group(arg); + current_augroup = augroup_add((char *)arg); } else { // ":aug": list the group names msg_start(); - for (int i = 0; i < augroups.ga_len; i++) { - if (AUGROUP_NAME(i) != NULL) { - msg_puts(AUGROUP_NAME(i)); - msg_puts(" "); + + String name; + int value; + map_foreach(&augroup_map, name, value, { + if (value > 0) { + msg_puts(name.data); + } else { + msg_puts(augroup_name(value)); } - } + + msg_puts(" "); + }); + msg_clr_eos(); msg_end(); } @@ -396,25 +549,30 @@ void do_augroup(char_u *arg, int del_group) #if defined(EXITFREE) void free_all_autocmds(void) { - for (current_augroup = -1; current_augroup < augroups.ga_len; - current_augroup++) { - do_autocmd((char_u *)"", true); - } - - for (int i = 0; i < augroups.ga_len; i++) { - char *const s = ((char **)(augroups.ga_data))[i]; - if ((const char *)s != get_deleted_augroup()) { - xfree(s); + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + aupat_del(ap); } } - ga_clear(&augroups); + + au_need_clean = true; + au_cleanup(); // may really delete removed patterns/commands now + + // Delete the augroup_map, including free the data + String name; + int id; + map_foreach(&augroup_map, name, id, { + (void)id; + api_free_string(name); + }) + map_destroy(String, int)(&augroup_map); } #endif // Return the event number for event name "start". // Return NUM_EVENTS if the event name was not found. // Return a pointer to the next event name in "end". -static event_T event_name2nr(const char_u *start, char_u **end) +event_T event_name2nr(const char_u *start, char_u **end) { const char_u *p; int i; @@ -457,33 +615,6 @@ static const char *event_nr2name(event_T event) return "Unknown"; } -/// Scan over the events. "*" stands for all events. -/// true when group name was found -static char_u *find_end_event(char_u *arg, int have_group) -{ - char_u *pat; - char_u *p; - - if (*arg == '*') { - if (arg[1] && !ascii_iswhite(arg[1])) { - semsg(_("E215: Illegal character after *: %s"), arg); - return NULL; - } - pat = arg + 1; - } else { - for (pat = arg; *pat && *pat != '|' && !ascii_iswhite(*pat); pat = p) { - if ((int)event_name2nr(pat, &p) >= NUM_EVENTS) { - if (have_group) { - semsg(_("E216: No such event: %s"), pat); - } else { - semsg(_("E216: No such group or event: %s"), pat); - } - return NULL; - } - } - } - return pat; -} /// Return true if "event" is included in 'eventignore'. /// @@ -601,12 +732,12 @@ void do_autocmd(char_u *arg_in, int forceit) group = AUGROUP_ALL; // no argument, use all groups } else { // Check for a legal group name. If not, use AUGROUP_ALL. - group = au_get_grouparg(&arg); + group = arg_augroup_get(&arg); } // Scan over the events. // If we find an illegal name, return here, don't do anything. - pat = find_end_event(arg, group != AUGROUP_ALL); + pat = arg_event_skip(arg, group != AUGROUP_ALL); if (pat == NULL) { return; } @@ -643,37 +774,22 @@ void do_autocmd(char_u *arg_in, int forceit) } cmd = skipwhite(cmd); + + bool invalid_flags = false; for (size_t i = 0; i < 2; i++) { if (*cmd != NUL) { - // Check for "++once" flag. - if (STRNCMP(cmd, "++once", 6) == 0 && ascii_iswhite(cmd[6])) { - if (once) { - semsg(_(e_duparg2), "++once"); - } - once = true; - cmd = skipwhite(cmd + 6); - } + ARG_GET_FLAG(invalid_flags, cmd, once, "++once", 6); + ARG_GET_FLAG(invalid_flags, cmd, nested, "++nested", 8); - // Check for "++nested" flag. - if ((STRNCMP(cmd, "++nested", 8) == 0 && ascii_iswhite(cmd[8]))) { - if (nested) { - semsg(_(e_duparg2), "++nested"); - } - nested = true; - cmd = skipwhite(cmd + 8); - } - - // Check for the old (deprecated) "nested" flag. - if (STRNCMP(cmd, "nested", 6) == 0 && ascii_iswhite(cmd[6])) { - if (nested) { - semsg(_(e_duparg2), "nested"); - } - nested = true; - cmd = skipwhite(cmd + 6); - } + // Check the deprecated "nested" flag. + ARG_GET_FLAG(invalid_flags, cmd, nested, "nested", 6); } } + if (invalid_flags) { + return; + } + // Find the start of the commands. // Expand <sfile> in it. if (*cmd != NUL) { @@ -685,36 +801,37 @@ void do_autocmd(char_u *arg_in, int forceit) } } + bool is_showing = !forceit && *cmd == NUL; + // Print header when showing autocommands. - if (!forceit && *cmd == NUL) { + if (is_showing) { // Highlight title msg_puts_title(_("\n--- Autocommands ---")); - } - // Loop over the events. - last_event = (event_T)-1; // for listing the event name - last_group = AUGROUP_ERROR; // for listing the group name - if (*arg == '*' || *arg == NUL || *arg == '|') { - if (!forceit && *cmd != NUL) { - emsg(_(e_cannot_define_autocommands_for_all_events)); + if (*arg == '*' || *arg == '|' || *arg == NUL) { + au_show_for_all_events(group); } else { - for (event_T event = (event_T)0; event < NUM_EVENTS; - event = (event_T)(event + 1)) { + event_T event = event_name2nr(arg, &arg); + assert(event < NUM_EVENTS); + au_show_for_event(group, event); + } + } else { + if (*arg == '*' || *arg == NUL || *arg == '|') { + if (!forceit && *cmd != NUL) { + emsg(_(e_cannot_define_autocommands_for_all_events)); + } else { + do_all_autocmd_events(pat, once, nested, cmd, forceit, group); + } + } else { + while (*arg && *arg != '|' && !ascii_iswhite(*arg)) { + event_T event = event_name2nr(arg, &arg); + assert(event < NUM_EVENTS); if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group) == FAIL) { break; } } } - } else { - while (*arg && *arg != '|' && !ascii_iswhite(*arg)) { - event_T event = event_name2nr(arg, &arg); - assert(event < NUM_EVENTS); - if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group) - == FAIL) { - break; - } - } } if (need_free) { @@ -723,30 +840,14 @@ void do_autocmd(char_u *arg_in, int forceit) xfree(envpat); } -// Find the group ID in a ":autocmd" or ":doautocmd" argument. -// The "argp" argument is advanced to the following argument. -// -// Returns the group ID or AUGROUP_ALL. -static int au_get_grouparg(char_u **argp) +void do_all_autocmd_events(char_u *pat, bool once, int nested, char_u *cmd, bool delete, int group) { - char_u *group_name; - char_u *p; - char_u *arg = *argp; - int group = AUGROUP_ALL; - - for (p = arg; *p && !ascii_iswhite(*p) && *p != '|'; p++) { - } - if (p > arg) { - group_name = vim_strnsave(arg, (size_t)(p - arg)); - group = au_find_group(group_name); - if (group == AUGROUP_ERROR) { - group = AUGROUP_ALL; // no match, use all groups - } else { - *argp = skipwhite(p); // match, skip over group name + FOR_ALL_AUEVENTS(event) { + if (do_autocmd_event(event, pat, once, nested, cmd, delete, group) + == FAIL) { + return; } - xfree(group_name); } - return group; } // do_autocmd() for one event. @@ -756,216 +857,293 @@ static int au_get_grouparg(char_u **argp) // If *cmd == NUL: show entries. // If forceit == true: delete entries. // If group is not AUGROUP_ALL: only use this group. -static int do_autocmd_event(event_T event, char_u *pat, bool once, int nested, char_u *cmd, - int forceit, int group) +int do_autocmd_event(event_T event, char_u *pat, bool once, int nested, char_u *cmd, bool delete, + int group) + FUNC_ATTR_NONNULL_ALL { + // Cannot be used to show all patterns. See au_show_for_event or au_show_for_all_events + assert(*pat != NUL || delete); + AutoPat *ap; AutoPat **prev_ap; - AutoCmd *ac; - AutoCmd **prev_ac; - int brace_level; - char_u *endpat; int findgroup; - int allgroups; int patlen; int is_buflocal; int buflocal_nr; char_u buflocal_pat[BUFLOCAL_PAT_LEN]; // for "<buffer=X>" + bool is_adding_cmd = *cmd != NUL; + if (group == AUGROUP_ALL) { findgroup = current_augroup; } else { findgroup = group; } - allgroups = (group == AUGROUP_ALL && !forceit && *cmd == NUL); - // Show or delete all patterns for an event. - if (*pat == NUL) { - for (ap = first_autopat[event]; ap != NULL; ap = ap->next) { - if (forceit) { // delete the AutoPat, if it's in the current group - if (ap->group == findgroup) { - au_remove_pat(ap); - } - } else if (group == AUGROUP_ALL || ap->group == group) { - show_autocmd(ap, event); - } - } + // Delete all aupat for an event. + if (*pat == NUL && delete) { + aupat_del_for_event_and_group(event, findgroup); + return OK; } // Loop through all the specified patterns. - for (; *pat; pat = (*endpat == ',' ? endpat + 1 : endpat)) { - // Find end of the pattern. - // Watch out for a comma in braces, like "*.\{obj,o\}". - endpat = pat; - // ignore single comma - if (*endpat == ',') { - continue; - } - brace_level = 0; - for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\'); - endpat++) { - if (*endpat == '{') { - brace_level++; - } else if (*endpat == '}') { - brace_level--; - } - } - patlen = (int)(endpat - pat); - - // detect special <buflocal[=X]> buffer-local patterns - is_buflocal = false; + patlen = (int)aucmd_pattern_length(pat); + while (patlen) { + // detect special <buffer[=X]> buffer-local patterns + is_buflocal = aupat_is_buflocal(pat, patlen); buflocal_nr = 0; - if (patlen >= 8 && STRNCMP(pat, "<buffer", 7) == 0 - && pat[patlen - 1] == '>') { - // "<buffer...>": Error will be printed only for addition. - // printing and removing will proceed silently. - is_buflocal = true; - if (patlen == 8) { - // "<buffer>" - buflocal_nr = curbuf->b_fnum; - } else if (patlen > 9 && pat[7] == '=') { - if (patlen == 13 && STRNICMP(pat, "<buffer=abuf>", 13) == 0) { - // "<buffer=abuf>" - buflocal_nr = autocmd_bufnr; - } else if (skipdigits(pat + 8) == pat + patlen - 1) { - // "<buffer=123>" - buflocal_nr = atoi((char *)pat + 8); - } - } - } - if (is_buflocal) { + buflocal_nr = aupat_get_buflocal_nr(pat, patlen); + // normalize pat into standard "<buffer>#N" form - snprintf((char *)buflocal_pat, - BUFLOCAL_PAT_LEN, - "<buffer=%d>", - buflocal_nr); + aupat_normalize_buflocal_pat(buflocal_pat, pat, patlen, buflocal_nr); - pat = buflocal_pat; // can modify pat and patlen - patlen = (int)STRLEN(buflocal_pat); // but not endpat + pat = buflocal_pat; + patlen = (int)STRLEN(buflocal_pat); } - // Find AutoPat entries with this pattern. When adding a command it - // always goes at or after the last one, so start at the end. - if (!forceit && *cmd != NUL && last_autopat[(int)event] != NULL) { - prev_ap = &last_autopat[(int)event]; - } else { + if (delete) { + assert(*pat != NUL); + + // Find AutoPat entries with this pattern. + // always goes at or after the last one, so start at the end. prev_ap = &first_autopat[(int)event]; - } - while ((ap = *prev_ap) != NULL) { - if (ap->pat != NULL) { - // Accept a pattern when: - // - a group was specified and it's that group, or a group was - // not specified and it's the current group, or a group was - // not specified and we are listing - // - the length of the pattern matches - // - the pattern matches. - // For <buffer[=X]>, this condition works because we normalize - // all buffer-local patterns. - if ((allgroups || ap->group == findgroup) && ap->patlen == patlen - && STRNCMP(pat, ap->pat, patlen) == 0) { - // Remove existing autocommands. - // If adding any new autocmd's for this AutoPat, don't - // delete the pattern from the autopat list, append to - // this list. - if (forceit) { - if (*cmd != NUL && ap->next == NULL) { - au_remove_cmds(ap); + while ((ap = *prev_ap) != NULL) { + if (ap->pat != NULL) { + // Accept a pattern when: + // - a group was specified and it's that group + // - the length of the pattern matches + // - the pattern matches. + // For <buffer[=X]>, this condition works because we normalize + // all buffer-local patterns. + if (ap->group == findgroup + && ap->patlen == patlen + && STRNCMP(pat, ap->pat, patlen) == 0) { + // Remove existing autocommands. + // If adding any new autocmd's for this AutoPat, don't + // delete the pattern from the autopat list, append to + // this list. + if (is_adding_cmd && ap->next == NULL) { + aupat_remove_cmds(ap); break; } - au_remove_pat(ap); - } else if (*cmd == NUL) { - // Show autocmd's for this autopat, or buflocals <buffer=X> - show_autocmd(ap, event); - } else if (ap->next == NULL) { - // Add autocmd to this autopat, if it's the last one. - break; + aupat_del(ap); } } + prev_ap = &ap->next; } - prev_ap = &ap->next; } - // Add a new command. - if (*cmd != NUL) { - // If the pattern we want to add a command to does appear at the - // end of the list (or not is not in the list at all), add the - // pattern at the end of the list. - if (ap == NULL) { - // refuse to add buffer-local ap if buffer number is invalid - if (is_buflocal - && (buflocal_nr == 0 || buflist_findnr(buflocal_nr) == NULL)) { - semsg(_("E680: <buffer=%d>: invalid buffer number "), buflocal_nr); - return FAIL; - } + if (is_adding_cmd) { + AucmdExecutable exec = AUCMD_EXECUTABLE_INIT; + exec.type = CALLABLE_EX; + exec.callable.cmd = cmd; + autocmd_register(0, event, pat, patlen, group, once, nested, NULL, exec); + } - ap = xmalloc(sizeof(AutoPat)); - ap->pat = vim_strnsave(pat, (size_t)patlen); - ap->patlen = patlen; + pat = aucmd_next_pattern(pat, (size_t)patlen); + patlen = (int)aucmd_pattern_length(pat); + } - if (is_buflocal) { - ap->buflocal_nr = buflocal_nr; - ap->reg_prog = NULL; - } else { - char_u *reg_pat; + au_cleanup(); + return OK; +} - ap->buflocal_nr = 0; - reg_pat = file_pat_to_reg_pat(pat, endpat, &ap->allow_dirs, true); - if (reg_pat != NULL) { - ap->reg_prog = vim_regcomp(reg_pat, RE_MAGIC); - } - xfree(reg_pat); - if (reg_pat == NULL || ap->reg_prog == NULL) { - xfree(ap->pat); - xfree(ap); - return FAIL; - } - } +int autocmd_register(int64_t id, event_T event, char_u *pat, int patlen, int group, bool once, + bool nested, char *desc, AucmdExecutable aucmd) +{ + // 0 is not a valid group. + assert(group != 0); - // need to initialize last_mode for the first ModeChanged autocmd - if (event == EVENT_MODECHANGED && !has_event(EVENT_MODECHANGED)) { - xfree(last_mode); - last_mode = get_mode(); - } + AutoPat *ap; + AutoPat **prev_ap; + AutoCmd *ac; + AutoCmd **prev_ac; + int is_buflocal; + int buflocal_nr; + int findgroup; + char_u buflocal_pat[BUFLOCAL_PAT_LEN]; // for "<buffer=X>" - // If the event is CursorMoved, update the last cursor position - // position to avoid immediately triggering the autocommand - if (event == EVENT_CURSORMOVED && !has_event(EVENT_CURSORMOVED)) { - curwin->w_last_cursormoved = curwin->w_cursor; - } + if (patlen > (int)STRLEN(pat)) { + return FAIL; + } - ap->cmds = NULL; - *prev_ap = ap; - last_autopat[(int)event] = ap; - ap->next = NULL; - if (group == AUGROUP_ALL) { - ap->group = current_augroup; - } else { - ap->group = group; + if (group == AUGROUP_ALL) { + findgroup = current_augroup; + } else { + findgroup = group; + } + + + // detect special <buffer[=X]> buffer-local patterns + is_buflocal = aupat_is_buflocal(pat, patlen); + buflocal_nr = 0; + + if (is_buflocal) { + buflocal_nr = aupat_get_buflocal_nr(pat, patlen); + + // normalize pat into standard "<buffer>#N" form + aupat_normalize_buflocal_pat(buflocal_pat, pat, patlen, buflocal_nr); + + pat = buflocal_pat; + patlen = (int)STRLEN(buflocal_pat); + } + + if (last_autopat[(int)event] != NULL) { + prev_ap = &last_autopat[(int)event]; + } else { + prev_ap = &first_autopat[(int)event]; + } + + while ((ap = *prev_ap) != NULL) { + if (ap->pat != NULL) { + // Accept a pattern when: + // - a group was specified and it's that group + // - the length of the pattern matches + // - the pattern matches. + // For <buffer[=X]>, this condition works because we normalize + // all buffer-local patterns. + if (ap->group == findgroup + && ap->patlen == patlen + && STRNCMP(pat, ap->pat, patlen) == 0) { + if (ap->next == NULL) { + // Add autocmd to this autopat, if it's the last one. + break; } } + } + prev_ap = &ap->next; + } + + // If the pattern we want to add a command to does appear at the + // end of the list (or not is not in the list at all), add the + // pattern at the end of the list. + if (ap == NULL) { + // refuse to add buffer-local ap if buffer number is invalid + if (is_buflocal + && (buflocal_nr == 0 || buflist_findnr(buflocal_nr) == NULL)) { + semsg(_("E680: <buffer=%d>: invalid buffer number "), buflocal_nr); + return FAIL; + } + + ap = xmalloc(sizeof(AutoPat)); + ap->pat = vim_strnsave(pat, (size_t)patlen); + ap->patlen = patlen; + + if (is_buflocal) { + ap->buflocal_nr = buflocal_nr; + ap->reg_prog = NULL; + } else { + char_u *reg_pat; - // Add the autocmd at the end of the AutoCmd list. - prev_ac = &(ap->cmds); - while ((ac = *prev_ac) != NULL) { - prev_ac = &ac->next; + ap->buflocal_nr = 0; + reg_pat = file_pat_to_reg_pat(pat, pat + patlen, &ap->allow_dirs, true); + if (reg_pat != NULL) { + ap->reg_prog = vim_regcomp(reg_pat, RE_MAGIC); + } + xfree(reg_pat); + if (reg_pat == NULL || ap->reg_prog == NULL) { + xfree(ap->pat); + xfree(ap); + return FAIL; } - ac = xmalloc(sizeof(AutoCmd)); - ac->cmd = vim_strsave(cmd); - ac->script_ctx = current_sctx; - ac->script_ctx.sc_lnum += sourcing_lnum; - ac->next = NULL; - *prev_ac = ac; - ac->once = once; - ac->nested = nested; + } + + // need to initialize last_mode for the first ModeChanged autocmd + if (event == EVENT_MODECHANGED && !has_event(EVENT_MODECHANGED)) { + xfree(last_mode); + last_mode = get_mode(); + } + + // If the event is CursorMoved, update the last cursor position + // position to avoid immediately triggering the autocommand + if (event == EVENT_CURSORMOVED && !has_event(EVENT_CURSORMOVED)) { + curwin->w_last_cursormoved = curwin->w_cursor; + } + + ap->cmds = NULL; + *prev_ap = ap; + last_autopat[(int)event] = ap; + ap->next = NULL; + if (group == AUGROUP_ALL) { + ap->group = current_augroup; + } else { + ap->group = group; } } - au_cleanup(); // may really delete removed patterns/commands now + // Add the autocmd at the end of the AutoCmd list. + prev_ac = &(ap->cmds); + while ((ac = *prev_ac) != NULL) { + prev_ac = &ac->next; + } + + ac = xmalloc(sizeof(AutoCmd)); + *prev_ac = ac; + + ac->id = id; + ac->exec = aucmd_exec_copy(aucmd); + ac->script_ctx = current_sctx; + ac->script_ctx.sc_lnum += sourcing_lnum; + ac->next = NULL; + ac->once = once; + ac->nested = nested; + ac->desc = NULL; + + // TODO(tjdevries): What to do about :autocmd and where/how to show lua stuffs there. + // perhaps: <lua>DESCRIPTION or similar + if (desc != NULL) { + ac->desc = xstrdup(desc); + } else { + ac->desc = aucmd_exec_default_desc(aucmd); + } + return OK; } +size_t aucmd_pattern_length(char_u *pat) +{ + if (*pat == NUL) { + return 0; + } + + char_u *endpat; + + for (; *pat; pat = (*endpat == ',' ? endpat + 1 : endpat)) { + // Find end of the pattern. + // Watch out for a comma in braces, like "*.\{obj,o\}". + endpat = pat; + // ignore single comma + if (*endpat == ',') { + continue; + } + int brace_level = 0; + for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\'); + endpat++) { + if (*endpat == '{') { + brace_level++; + } else if (*endpat == '}') { + brace_level--; + } + } + + return (size_t)(endpat - pat); + } + + return STRLEN(pat); +} + +char_u *aucmd_next_pattern(char_u *pat, size_t patlen) +{ + pat = pat + patlen; + if (*pat == ',') { + pat = pat + 1; + } + + return pat; +} + /// Implementation of ":doautocmd [group] event [fname]". /// Return OK for success, FAIL for failure; /// @@ -981,7 +1159,7 @@ int do_doautocmd(char_u *arg, bool do_msg, bool *did_something) } // Check for a legal group name. If not, use AUGROUP_ALL. - group = au_get_grouparg(&arg); + group = arg_augroup_get(&arg); if (*arg == '*') { emsg(_("E217: Can't execute autocommands for ALL events")); @@ -990,7 +1168,7 @@ int do_doautocmd(char_u *arg, bool do_msg, bool *did_something) // Scan over the events. // If we find an illegal name, return here, don't do anything. - fname = find_end_event(arg, group != AUGROUP_ALL); + fname = arg_event_skip(arg, group != AUGROUP_ALL); if (fname == NULL) { return FAIL; } @@ -1378,8 +1556,8 @@ bool trigger_cursorhold(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT /// @param eap Ex command arguments /// /// @return true if some commands were executed. -static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, bool force, - int group, buf_T *buf, exarg_T *eap) +bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, bool force, int group, + buf_T *buf, exarg_T *eap) { char_u *sfname = NULL; // short file name char_u *tail; @@ -1402,7 +1580,7 @@ static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, proftime_T wait_time; bool did_save_redobuff = false; save_redo_T save_redo; - const bool save_KeyTyped = KeyTyped; + const bool save_KeyTyped = KeyTyped; // NOLINT // Quickly return if there are no autocommands for this event or // autocommands are blocked. @@ -1814,6 +1992,11 @@ void auto_next_pat(AutoPatCmd *apc, int stop_at_last) /// @return allocated string, or NULL for end of autocommands. char_u *getnextac(int c, void *cookie, int indent, bool do_concat) { + // These arguments are required for do_cmdline. + (void)c; + (void)indent; + (void)do_concat; + AutoPatCmd *acp = (AutoPatCmd *)cookie; char_u *retval; AutoCmd *ac; @@ -1826,7 +2009,8 @@ char_u *getnextac(int c, void *cookie, int indent, bool do_concat) // repeat until we find an autocommand to execute for (;;) { // skip removed commands - while (acp->nextcmd != NULL && acp->nextcmd->cmd == NULL) { + while (acp->nextcmd != NULL + && aucmd_exec_is_deleted(acp->nextcmd->exec)) { if (acp->nextcmd->last) { acp->nextcmd = NULL; } else { @@ -1856,14 +2040,32 @@ char_u *getnextac(int c, void *cookie, int indent, bool do_concat) if (p_verbose >= 9) { verbose_enter_scroll(); - smsg(_("autocommand %s"), ac->cmd); + smsg(_("autocommand %s"), aucmd_exec_to_string(ac, ac->exec)); msg_puts("\n"); // don't overwrite this either verbose_leave_scroll(); } - retval = vim_strsave(ac->cmd); + + if (ac->exec.type == CALLABLE_CB) { + typval_T argsin = TV_INITIAL_VALUE; + typval_T rettv = TV_INITIAL_VALUE; + callback_call(&ac->exec.callable.cb, 0, &argsin, &rettv); + + // Major Hack Alert: + // We just return "not-null" and continue going. + // This would be a good candidate for a refactor. You would need to refactor: + // 1. do_cmdline to accept something besides a string + // OR + // 2. make where we call do_cmdline for autocmds not have to return anything, + // and instead we loop over all the matches and just execute one-by-one. + // However, my expectation woudl be that could be expensive. + retval = vim_strsave((char_u *)""); + } else { + retval = vim_strsave(ac->exec.callable.cmd); + } + // Remove one-shot ("once") autocmd in anticipation of its execution. if (ac->once) { - au_del_cmd(ac); + aucmd_del(ac); } autocmd_nested = ac->nested; current_sctx = ac->script_ctx; @@ -1928,19 +2130,12 @@ bool has_autocmd(event_T event, char_u *sfname, buf_T *buf) FUNC_ATTR_WARN_UNUSE // Function given to ExpandGeneric() to obtain the list of autocommand group // names. -char_u *get_augroup_name(expand_T *xp, int idx) +char_u *expand_get_augroup_name(expand_T *xp, int idx) { - if (idx == augroups.ga_len) { // add "END" add the end - return (char_u *)"END"; - } - if (idx >= augroups.ga_len) { // end of list - return NULL; - } - if (AUGROUP_NAME(idx) == NULL || AUGROUP_NAME(idx) == get_deleted_augroup()) { - // skip deleted entries - return (char_u *)""; - } - return (char_u *)AUGROUP_NAME(idx); + // Required for ExpandGeneric + (void)xp; + + return (char_u *)augroup_name(idx + 1); } /// @param doautocmd true for :doauto*, false for :autocmd @@ -1952,7 +2147,7 @@ char_u *set_context_in_autocmd(expand_T *xp, char_u *arg, int doautocmd) // check for a group name, skip it if present autocmd_include_groups = false; p = arg; - group = au_get_grouparg(&arg); + group = arg_augroup_get(&arg); // If there only is a group name that's what we expand. if (*arg == NUL && group != AUGROUP_ALL && !ascii_iswhite(arg[-1])) { @@ -1993,16 +2188,25 @@ char_u *set_context_in_autocmd(expand_T *xp, char_u *arg, int doautocmd) } // Function given to ExpandGeneric() to obtain the list of event names. -char_u *get_event_name(expand_T *xp, int idx) +char_u *expand_get_event_name(expand_T *xp, int idx) { - if (idx < augroups.ga_len) { // First list group names, if wanted - if (!autocmd_include_groups || AUGROUP_NAME(idx) == NULL - || AUGROUP_NAME(idx) == get_deleted_augroup()) { - return (char_u *)""; // skip deleted entries + // xp is a required parameter to be used with ExpandGeneric + (void)xp; + + + // List group names + char *name = augroup_name(idx + 1); + if (name != NULL) { + // skip when not including groups or skip deleted entries + if (!autocmd_include_groups || name == get_deleted_augroup()) { + return (char_u *)""; } - return (char_u *)AUGROUP_NAME(idx); + + return (char_u *)name; } - return (char_u *)event_names[idx - augroups.ga_len].name; + + // List event names + return (char_u *)event_names[idx - next_augroup_id].name; } /// Check whether given autocommand is supported @@ -2045,7 +2249,7 @@ bool au_exists(const char *const arg) FUNC_ATTR_WARN_UNUSED_RESULT } // First, look for an autocmd group name. - group = au_find_group((char_u *)arg_save); + group = augroup_find(arg_save); char *event_name; if (group == AUGROUP_ERROR) { // Didn't match a group name, assume the first argument is an event. @@ -2109,3 +2313,346 @@ theend: xfree(arg_save); return retval; } + +// Checks if a pattern is buflocal +bool aupat_is_buflocal(char_u *pat, int patlen) +{ + return patlen >= 8 + && STRNCMP(pat, "<buffer", 7) == 0 + && (pat)[patlen - 1] == '>'; +} + +int aupat_get_buflocal_nr(char_u *pat, int patlen) +{ + assert(aupat_is_buflocal(pat, patlen)); + + // "<buffer>" + if (patlen == 8) { + return curbuf->b_fnum; + } + + if (patlen > 9 && (pat)[7] == '=') { + // "<buffer=abuf>" + if (patlen == 13 && STRNICMP(pat, "<buffer=abuf>", 13) == 0) { + return autocmd_bufnr; + } + + // "<buffer=123>" + if (skipdigits(pat + 8) == pat + patlen - 1) { + return atoi((char *)pat + 8); + } + } + + return 0; +} + +// normalize buffer pattern +void aupat_normalize_buflocal_pat(char_u *dest, char_u *pat, int patlen, int buflocal_nr) +{ + assert(aupat_is_buflocal(pat, patlen)); + + if (buflocal_nr == 0) { + buflocal_nr = curbuf->handle; + } + + // normalize pat into standard "<buffer>#N" form + snprintf((char *)dest, + BUFLOCAL_PAT_LEN, + "<buffer=%d>", + buflocal_nr); +} + +int autocmd_delete_event(int group, event_T event, char_u *pat) + FUNC_ATTR_NONNULL_ALL +{ + return do_autocmd_event(event, pat, false, false, (char_u *)"", true, group); +} + +/// Deletes an autocmd by ID. +/// Only autocmds created via the API have IDs associated with them. There +/// is no way to delete a specific autocmd created via :autocmd +void autocmd_delete_id(int64_t id) +{ + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { + if (ac->id == id) { + aucmd_del(ac); + } + } + } + } +} + +// =========================================================================== +// AucmdExecutable Functions +// =========================================================================== +char *aucmd_exec_default_desc(AucmdExecutable acc) +{ + size_t msglen = 100; + + switch (acc.type) { + case CALLABLE_CB: + switch (acc.callable.cb.type) { + case kCallbackLua: { + char *msg = (char *)xmallocz(msglen); + snprintf(msg, msglen, "<Lua function %d>", acc.callable.cb.data.luaref); + return msg; + } + case kCallbackFuncref: { + // TODO(tjdevries): Is this enough space for this? + char *msg = (char *)xmallocz(msglen); + snprintf(msg, msglen, "<vim function: %s>", acc.callable.cb.data.funcref); + return msg; + } + case kCallbackPartial: { + char *msg = (char *)xmallocz(msglen); + snprintf(msg, msglen, "<vim partial: %s>", acc.callable.cb.data.partial->pt_name); + return msg; + } + default: + return NULL; + } + default: + return NULL; + } + + abort(); +} +char *aucmd_exec_to_string(AutoCmd *ac, AucmdExecutable acc) +{ + switch (acc.type) { + case CALLABLE_EX: + return (char *)acc.callable.cmd; + case CALLABLE_CB: + return ac->desc; + case CALLABLE_NONE: + return "This is not possible"; + } + + abort(); +} + +void aucmd_exec_free(AucmdExecutable *acc) +{ + switch (acc->type) { + case CALLABLE_EX: + XFREE_CLEAR(acc->callable.cmd); + break; + case CALLABLE_CB: + callback_free(&acc->callable.cb); + break; + case CALLABLE_NONE: + return; + } + + acc->type = CALLABLE_NONE; +} + +AucmdExecutable aucmd_exec_copy(AucmdExecutable src) +{ + AucmdExecutable dest = AUCMD_EXECUTABLE_INIT; + + switch (src.type) { + case CALLABLE_EX: + dest.type = CALLABLE_EX; + dest.callable.cmd = vim_strsave(src.callable.cmd); + return dest; + case CALLABLE_CB: + dest.type = CALLABLE_CB; + callback_copy(&dest.callable.cb, &src.callable.cb); + return dest; + case CALLABLE_NONE: + return dest; + } + + abort(); +} + +bool aucmd_exec_is_deleted(AucmdExecutable acc) +{ + switch (acc.type) { + case CALLABLE_EX: + return acc.callable.cmd == NULL; + case CALLABLE_CB: + return callback_is_freed(acc.callable.cb); + case CALLABLE_NONE: + return true; + } + + abort(); +} + +bool au_event_is_empty(event_T event) +{ + return first_autopat[event] == NULL; +} + +// Arg Parsing Functions + +/// Scan over the events. "*" stands for all events. +/// true when group name was found +static char_u *arg_event_skip(char_u *arg, int have_group) +{ + char_u *pat; + char_u *p; + + if (*arg == '*') { + if (arg[1] && !ascii_iswhite(arg[1])) { + semsg(_("E215: Illegal character after *: %s"), arg); + return NULL; + } + pat = arg + 1; + } else { + for (pat = arg; *pat && *pat != '|' && !ascii_iswhite(*pat); pat = p) { + if ((int)event_name2nr(pat, &p) >= NUM_EVENTS) { + if (have_group) { + semsg(_("E216: No such event: %s"), pat); + } else { + semsg(_("E216: No such group or event: %s"), pat); + } + return NULL; + } + } + } + return pat; +} + +// Find the group ID in a ":autocmd" or ":doautocmd" argument. +// The "argp" argument is advanced to the following argument. +// +// Returns the group ID or AUGROUP_ALL. +static int arg_augroup_get(char_u **argp) +{ + char_u *group_name; + char_u *p; + char_u *arg = *argp; + int group = AUGROUP_ALL; + + for (p = arg; *p && !ascii_iswhite(*p) && *p != '|'; p++) { + } + if (p > arg) { + group_name = vim_strnsave(arg, (size_t)(p - arg)); + group = augroup_find((char *)group_name); + if (group == AUGROUP_ERROR) { + group = AUGROUP_ALL; // no match, use all groups + } else { + *argp = skipwhite(p); // match, skip over group name + } + xfree(group_name); + } + return group; +} + + +// UI Enter +void do_autocmd_uienter(uint64_t chanid, bool attached) +{ + static bool recursive = false; + + if (recursive) { + return; // disallow recursion + } + recursive = true; + + save_v_event_T save_v_event; + dict_T *dict = get_v_event(&save_v_event); + assert(chanid < VARNUMBER_MAX); + tv_dict_add_nr(dict, S_LEN("chan"), (varnumber_T)chanid); + tv_dict_set_keys_readonly(dict); + apply_autocmds(attached ? EVENT_UIENTER : EVENT_UILEAVE, + NULL, NULL, false, curbuf); + restore_v_event(dict, &save_v_event); + + recursive = false; +} + +// FocusGained + +static void focusgained_event(void **argv) +{ + bool *gainedp = argv[0]; + do_autocmd_focusgained(*gainedp); + xfree(gainedp); +} + +void autocmd_schedule_focusgained(bool gained) +{ + bool *gainedp = xmalloc(sizeof(*gainedp)); + *gainedp = gained; + loop_schedule_deferred(&main_loop, + event_create(focusgained_event, 1, gainedp)); +} + +static void do_autocmd_focusgained(bool gained) +{ + static bool recursive = false; + static Timestamp last_time = (time_t)0; + bool need_redraw = false; + + if (recursive) { + return; // disallow recursion + } + recursive = true; + need_redraw |= apply_autocmds((gained ? EVENT_FOCUSGAINED : EVENT_FOCUSLOST), + NULL, NULL, false, curbuf); + + // When activated: Check if any file was modified outside of Vim. + // Only do this when not done within the last two seconds as: + // 1. Some filesystems have modification time granularity in seconds. Fat32 + // has a granularity of 2 seconds. + // 2. We could get multiple notifications in a row. + if (gained && last_time + (Timestamp)2000 < os_now()) { + need_redraw = check_timestamps(true); + last_time = os_now(); + } + + if (need_redraw) { + // Something was executed, make sure the cursor is put back where it + // belongs. + need_wait_return = false; + + if (State & CMDLINE) { + redrawcmdline(); + } else if ((State & NORMAL) || (State & INSERT)) { + if (must_redraw != 0) { + update_screen(0); + } + + setcursor(); + } + + ui_flush(); + } + + if (need_maketitle) { + maketitle(); + } + + recursive = false; +} + +// initialization + +void init_default_autocmds(void) +{ + // open terminals when opening files that start with term:// +#define PROTO "term://" + do_cmdline_cmd("augroup nvim_terminal"); + do_cmdline_cmd("autocmd BufReadCmd " PROTO "* ++nested " + "if !exists('b:term_title')|call termopen(" + // Capture the command string + "matchstr(expand(\"<amatch>\"), " + "'\\c\\m" PROTO "\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), " + // capture the working directory + "{'cwd': expand(get(matchlist(expand(\"<amatch>\"), " + "'\\c\\m" PROTO "\\(.\\{-}\\)//'), 1, ''))})" + "|endif"); + do_cmdline_cmd("augroup END"); +#undef PROTO + + // limit syntax synchronization in the command window + do_cmdline_cmd("augroup nvim_cmdwin"); + do_cmdline_cmd("autocmd! CmdwinEnter [:>] syntax sync minlines=1 maxlines=1"); + do_cmdline_cmd("augroup END"); +} diff --git a/src/nvim/autocmd.h b/src/nvim/autocmd.h index 63c5abd4f8..0a1f036183 100644 --- a/src/nvim/autocmd.h +++ b/src/nvim/autocmd.h @@ -4,6 +4,11 @@ #include "nvim/buffer_defs.h" #include "nvim/ex_cmds_defs.h" +// event_T definition +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "auevents_enum.generated.h" +#endif + // Struct to save values in before executing autocommands for a buffer that is // not the current buffer. typedef struct { @@ -18,22 +23,23 @@ typedef struct { } aco_save_T; typedef struct AutoCmd { - char_u *cmd; // Command to be executed (NULL when - // command has been removed) + AucmdExecutable exec; bool once; // "One shot": removed after execution bool nested; // If autocommands nest here bool last; // last command in list + int64_t id; // TODO(tjdevries): Explain sctx_T script_ctx; // script context where defined - struct AutoCmd *next; // Next AutoCmd in list + char *desc; // Description for the autocmd. + struct AutoCmd *next; // Next AutoCmd in list } AutoCmd; typedef struct AutoPat { - struct AutoPat *next; // next AutoPat in AutoPat list; MUST - // be the first entry - char_u *pat; // pattern as typed (NULL when pattern - // has been removed) - regprog_T *reg_prog; // compiled regprog for pattern - AutoCmd *cmds; // list of commands to do + struct AutoPat *next; // next AutoPat in AutoPat list; MUST + // be the first entry + char_u *pat; // pattern as typed (NULL when pattern + // has been removed) + regprog_T *reg_prog; // compiled regprog for pattern + AutoCmd *cmds; // list of commands to do int group; // group ID int patlen; // strlen() of pat int buflocal_nr; // !=0 for buffer-local AutoPat @@ -41,13 +47,7 @@ typedef struct AutoPat { char last; // last pattern for apply_autocmds() } AutoPat; -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "auevents_enum.generated.h" -#endif - -/// /// Struct used to keep status while executing autocommands for an event. -/// typedef struct AutoPatCmd { AutoPat *curpat; // next AutoPat to examine AutoCmd *nextcmd; // next AutoCmd to execute @@ -75,8 +75,16 @@ EXTERN bool au_did_filetype INIT(= false); # include "autocmd.h.generated.h" #endif -#define AUGROUP_DEFAULT -1 // default autocmd group -#define AUGROUP_ERROR -2 // erroneous autocmd group -#define AUGROUP_ALL -3 // all autocmd groups +#define AUGROUP_DEFAULT (-1) // default autocmd group +#define AUGROUP_ERROR (-2) // erroneous autocmd group +#define AUGROUP_ALL (-3) // all autocmd groups +#define AUGROUP_DELETED (-4) // all autocmd groups +// #define AUGROUP_NS -5 // TODO(tjdevries): Support namespaced based augroups + +#define BUFLOCAL_PAT_LEN 25 + +/// Iterates over all the events for auto commands +#define FOR_ALL_AUEVENTS(event) \ + for (event_T event = (event_T)0; (int)event < (int)NUM_EVENTS; event = (event_T)((int)event + 1)) // NOLINT #endif // NVIM_AUTOCMD_H diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index aada11bc9e..9e82b4e80b 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -1441,7 +1441,7 @@ void set_curbuf(buf_T *buf, int action) set_bufref(&prevbufref, prevbuf); set_bufref(&newbufref, buf); - // Autocommands may delete the curren buffer and/or the buffer we want to go + // Autocommands may delete the current buffer and/or the buffer we want to go // to. In those cases don't close the buffer. if (!apply_autocmds(EVENT_BUFLEAVE, NULL, NULL, false, curbuf) || (bufref_valid(&prevbufref) && bufref_valid(&newbufref) @@ -1454,6 +1454,7 @@ void set_curbuf(buf_T *buf, int action) } if (bufref_valid(&prevbufref) && !aborting()) { win_T *previouswin = curwin; + // Do not sync when in Insert mode and the buffer is open in // another window, might be a timer doing something in another // window. @@ -5494,11 +5495,19 @@ static int buf_signcols_inner(buf_T *buf, int maximum) int buf_signcols(buf_T *buf, int maximum) { + // The maximum can be determined from 'signcolumn' which is window scoped so + // need to invalidate signcols if the maximum is greater than the previous + // maximum. + if (maximum > buf->b_signcols_max) { + buf->b_signcols_valid = false; + } + if (!buf->b_signcols_valid) { int signcols = buf_signcols_inner(buf, maximum); // Check if we need to redraw if (signcols != buf->b_signcols) { buf->b_signcols = signcols; + buf->b_signcols_max = maximum; redraw_buf_later(buf, NOT_VALID); } diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 7ae5df164f..1e0c837056 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -864,6 +864,7 @@ struct file_buffer { sign_entry_T *b_signlist; // list of placed signs int b_signcols; // last calculated number of sign columns bool b_signcols_valid; // calculated sign columns is valid + int b_signcols_max; // Maximum value b_signcols is valid for. Terminal *terminal; // Terminal instance associated with the buffer @@ -1353,6 +1354,7 @@ struct window_S { // recomputed int w_nrwidth; // width of 'number' and 'relativenumber' // column being used + int w_scwidth; // width of 'signcolumn' /* * === end of cached values === diff --git a/src/nvim/charset.c b/src/nvim/charset.c index 583a040ed1..f4882e57e1 100644 --- a/src/nvim/charset.c +++ b/src/nvim/charset.c @@ -1439,7 +1439,7 @@ bool vim_isblankline(char_u *lbuf) /// @param unptr Returns the unsigned result. /// @param maxlen Max length of string to check. /// @param strict If true, fail if the number has unexpected trailing -/// alpha-numeric chars: *len is set to 0 and nothing else is +/// alphanumeric chars: *len is set to 0 and nothing else is /// returned. void vim_str2nr(const char_u *const start, int *const prep, int *const len, const int what, varnumber_T *const nptr, uvarnumber_T *const unptr, const int maxlen, @@ -1585,7 +1585,7 @@ vim_str2nr_hex: #undef PARSE_NUMBER vim_str2nr_proceed: - // Check for an alpha-numeric character immediately following, that is + // Check for an alphanumeric character immediately following, that is // most likely a typo. if (strict && ptr - (const char *)start != maxlen && ASCII_ISALNUM(*ptr)) { return; diff --git a/src/nvim/diff.c b/src/nvim/diff.c index 233753839b..80bd3229c6 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -85,9 +85,9 @@ typedef struct { // used for recording hunks from xdiff typedef struct { linenr_T lnum_orig; - long count_orig; + long count_orig; linenr_T lnum_new; - long count_new; + long count_new; } diffhunk_T; // two diff inputs and one result @@ -1285,7 +1285,7 @@ void ex_diffpatch(exarg_T *eap) ex_file(eap); // Do filetype detection with the new name. - if (au_has_group((char_u *)"filetypedetect")) { + if (augroup_exists("filetypedetect")) { do_cmdline_cmd(":doau filetypedetect BufRead"); } } @@ -3159,8 +3159,7 @@ static int parse_diff_unified(char_u *line, diffhunk_T *hunk) /// Callback function for the xdl_diff() function. /// Stores the diff output in a grow array. /// -static int xdiff_out(long start_a, long count_a, long start_b, long count_b, - void *priv) +static int xdiff_out(long start_a, long count_a, long start_b, long count_b, void *priv) { diffout_T *dout = (diffout_T *)priv; diffhunk_T *p = xmalloc(sizeof(*p)); diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 07f0799bc4..0322898827 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -6,6 +6,7 @@ */ #include <math.h> +#include <stdlib.h> #include "auto/config.h" @@ -3258,7 +3259,7 @@ char_u *get_user_var_name(expand_T *xp, int idx) // b: variables // In cmdwin, the alternative buffer should be used. hashtab_T *ht - = is_in_cmdwin() ? &prevwin->w_buffer->b_vars->dv_hashtab : &curbuf->b_vars->dv_hashtab; + = is_in_cmdwin() ? &prevwin->w_buffer->b_vars->dv_hashtab : &curbuf->b_vars->dv_hashtab; if (bdone < ht->ht_used) { if (bdone++ == 0) { hi = ht->ht_array; @@ -7746,6 +7747,7 @@ bool callback_from_typval(Callback *const callback, typval_T *const arg) callback->type = kCallbackFuncref; } } else if (nlua_is_table_from_lua(arg)) { + // TODO(tjdvries): UnifiedCallback char_u *name = nlua_register_table_as_callable(arg); if (name != NULL) { @@ -7775,6 +7777,7 @@ bool callback_call(Callback *const callback, const int argcount_in, typval_T *co { partial_T *partial; char_u *name; + Array args = ARRAY_DICT_INIT; switch (callback->type) { case kCallbackFuncref: name = callback->data.funcref; @@ -7786,6 +7789,13 @@ bool callback_call(Callback *const callback, const int argcount_in, typval_T *co name = partial_name(partial); break; + case kCallbackLua: + ILOG(" We tryin to call dat dang lua ref "); + nlua_call_ref(callback->data.luaref, "aucmd", args, false, NULL); + + return false; + break; + case kCallbackNone: return false; break; diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index c6baa105b0..49dde537c3 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -894,6 +894,7 @@ static void f_call(typval_T *argvars, typval_T *rettv, FunPtr fptr) partial = argvars[0].vval.v_partial; func = partial_name(partial); } else if (nlua_is_table_from_lua(&argvars[0])) { + // TODO(tjdevries): UnifiedCallback func = nlua_register_table_as_callable(&argvars[0]); owned = true; } else { @@ -3225,7 +3226,7 @@ static void getchar_common(typval_T *argvars, typval_T *rettv) set_vim_var_nr(VV_MOUSE_COL, 0); rettv->vval.v_number = n; - if (IS_SPECIAL(n) || mod_mask != 0) { + if (n != 0 && (IS_SPECIAL(n) || mod_mask != 0)) { char_u temp[10]; // modifier: 3, mbyte-char: 6, NUL: 1 int i = 0; @@ -4242,7 +4243,7 @@ static void win_move_into_split(win_T *wp, win_T *targetwin, int size, int flags int height = wp->w_height; win_T *oldwin = curwin; - if (wp == targetwin) { + if (wp == targetwin || wp == aucmd_win) { return; } diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 6f8b032d41..fbda7fbc3c 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -28,11 +28,11 @@ #include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/message.h" +#include "nvim/os/fileio.h" #include "nvim/os/input.h" #include "nvim/pos.h" #include "nvim/types.h" #include "nvim/vim.h" -#include "nvim/os/fileio.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "eval/typval.c.generated.h" @@ -1123,6 +1123,8 @@ bool tv_callback_equal(const Callback *cb1, const Callback *cb2) // FIXME: this is inconsistent with tv_equal but is needed for precision // maybe change dictwatcheradd to return a watcher id instead? return cb1->data.partial == cb2->data.partial; + case kCallbackLua: + return cb1->data.luaref == cb2->data.luaref; case kCallbackNone: return true; } @@ -1142,6 +1144,9 @@ void callback_free(Callback *callback) case kCallbackPartial: partial_unref(callback->data.partial); break; + case kCallbackLua: + NLUA_CLEAR_REF(callback->data.luaref); + break; case kCallbackNone: break; } @@ -1149,6 +1154,12 @@ void callback_free(Callback *callback) callback->data.funcref = NULL; } +/// Check if callback is freed +bool callback_is_freed(Callback callback) +{ + return false; +} + /// Copy a callback into a typval_T. void callback_put(Callback *cb, typval_T *tv) FUNC_ATTR_NONNULL_ALL @@ -1164,6 +1175,9 @@ void callback_put(Callback *cb, typval_T *tv) tv->vval.v_string = vim_strsave(cb->data.funcref); func_ref(cb->data.funcref); break; + case kCallbackLua: + // TODO(tjdevries): I'm not even sure if this is strictly necessary? + abort(); default: tv->v_type = VAR_SPECIAL; tv->vval.v_special = kSpecialVarNull; @@ -1185,6 +1199,9 @@ void callback_copy(Callback *dest, Callback *src) dest->data.funcref = vim_strsave(src->data.funcref); func_ref(src->data.funcref); break; + case kCallbackLua: + dest->data.luaref = api_new_luaref(src->data.luaref); + break; default: dest->data.funcref = NULL; break; diff --git a/src/nvim/eval/typval.h b/src/nvim/eval/typval.h index ad01c01499..40dc819754 100644 --- a/src/nvim/eval/typval.h +++ b/src/nvim/eval/typval.h @@ -72,15 +72,18 @@ typedef enum { kCallbackNone = 0, kCallbackFuncref, kCallbackPartial, + kCallbackLua, } CallbackType; typedef struct { union { char_u *funcref; partial_T *partial; + LuaRef luaref; } data; CallbackType type; } Callback; + #define CALLBACK_INIT { .type = kCallbackNone } #define CALLBACK_NONE ((Callback)CALLBACK_INIT) diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 49bf9972b1..c7910e148d 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -1943,7 +1943,7 @@ int do_write(exarg_T *eap) // If 'filetype' was empty try detecting it now. if (*curbuf->b_p_ft == NUL) { - if (au_has_group((char_u *)"filetypedetect")) { + if (augroup_exists("filetypedetect")) { (void)do_doautocmd((char_u *)"filetypedetect BufRead", true, NULL); } do_modelines(0); diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h index eaf5f627b6..92675499be 100644 --- a/src/nvim/ex_cmds_defs.h +++ b/src/nvim/ex_cmds_defs.h @@ -4,6 +4,7 @@ #include <stdbool.h> #include <stdint.h> +#include "nvim/eval/typval.h" #include "nvim/normal.h" #include "nvim/pos.h" // for linenr_T #include "nvim/regexp_defs.h" @@ -91,6 +92,35 @@ typedef struct exarg exarg_T; typedef void (*ex_func_T)(exarg_T *eap); +// NOTE: These possible could be removed and changed so that +// Callback could take a "command" style string, and simply +// execute that (instead of it being a function). +// +// But it's still a bit weird to do that. +// +// Another option would be that we just make a callback reference to +// "execute($INPUT)" or something like that, so whatever the user +// sends in via autocmds is just executed via this. +// +// However, that would probably have some performance cost (probably +// very marginal, but still some cost either way). +typedef enum { + CALLABLE_NONE, + CALLABLE_EX, + CALLABLE_CB, +} AucmdExecutableType; + +typedef struct aucmd_executable_t AucmdExecutable; +struct aucmd_executable_t { + AucmdExecutableType type; + union { + char_u *cmd; + Callback cb; + } callable; +}; + +#define AUCMD_EXECUTABLE_INIT { .type = CALLABLE_NONE } + typedef char_u *(*LineGetter)(int, void *, int, bool); /// Structure for command definition. diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index e8b8dc799c..152a41c407 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -5802,6 +5802,30 @@ static void ex_delcommand(exarg_T *eap) } } +/// Split a string by unescaped whitespace (space & tab), used for f-args on Lua commands callback. +/// Similar to uc_split_args(), but does not allocate, add quotes, add commas and is an iterator. +/// +/// @note If no separator is found start = 0 and end = length - 1 +/// @param[in] arg String to split +/// @param[in] iter Iteration counter +/// @param[out] start Start of the split +/// @param[out] end End of the split +/// @param[in] length Length of the string +/// @return false if it's the last split (don't call again), true otherwise (call again). +bool uc_split_args_iter(const char_u *arg, int iter, int *start, int *end, int length) +{ + int pos; + *start = *end + (iter > 1 ? 2 : 0); // Skip whitespace after the first split + for (pos = *start; pos < length - 2; pos++) { + if (arg[pos] != '\\' && ascii_iswhite(arg[pos + 1])) { + *end = pos; + return true; + } + } + *end = length - 1; + return false; +} + /* * split and quote args for <f-args> */ @@ -7506,7 +7530,7 @@ static void ex_edit(exarg_T *eap) do_exedit(eap, NULL); } -/// ":edit <file>" command and alikes. +/// ":edit <file>" command and alike. /// /// @param old_curwin curwin before doing a split or NULL void do_exedit(exarg_T *eap, win_T *old_curwin) diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index ed4475eb1a..30287cd6f2 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -5049,8 +5049,8 @@ static int ExpandFromContext(expand_T *xp, char_u *pat, int *num_file, char_u ** { EXPAND_SYNTAX, get_syntax_name, true, true }, { EXPAND_SYNTIME, get_syntime_arg, true, true }, { EXPAND_HIGHLIGHT, (ExpandFunc)get_highlight_name, true, true }, - { EXPAND_EVENTS, get_event_name, true, true }, - { EXPAND_AUGROUP, get_augroup_name, true, true }, + { EXPAND_EVENTS, expand_get_event_name, true, true }, + { EXPAND_AUGROUP, expand_get_augroup_name, true, true }, { EXPAND_CSCOPE, get_cscope_name, true, true }, { EXPAND_SIGN, get_sign_name, true, true }, { EXPAND_PROFILE, get_profile_name, true, true }, diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index f5824d83be..965aa8749d 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -3796,7 +3796,7 @@ static int set_rw_fname(char_u *fname, char_u *sfname) // Do filetype detection now if 'filetype' is empty. if (*curbuf->b_p_ft == NUL) { - if (au_has_group((char_u *)"filetypedetect")) { + if (augroup_exists("filetypedetect")) { (void)do_doautocmd((char_u *)"filetypedetect BufRead", false, NULL); } do_modelines(0); diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index 741fc6d803..f249cde9a0 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -1710,6 +1710,15 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) int keylen = *keylenp; int i; int local_State = get_real_state(); + bool is_plug_map = false; + + // Check if typehead starts with a <Plug> mapping. + // In that case we will ignore nore flag on it. + if (typebuf.tb_buf[typebuf.tb_off] == K_SPECIAL + && typebuf.tb_buf[typebuf.tb_off+1] == KS_EXTRA + && typebuf.tb_buf[typebuf.tb_off+2] == KE_PLUG) { + is_plug_map = true; + } // Check for a mappable key sequence. // Walk through one maphash[] list until we find an entry that matches. @@ -1725,7 +1734,7 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) tb_c1 = typebuf.tb_buf[typebuf.tb_off]; if (no_mapping == 0 && maphash_valid && (no_zero_mapping == 0 || tb_c1 != '0') - && (typebuf.tb_maplen == 0 + && (typebuf.tb_maplen == 0 || is_plug_map || (p_remap && !(typebuf.tb_noremap[typebuf.tb_off] & (RM_NONE|RM_ABBR)))) && !(p_paste && (State & (INSERT + CMDLINE))) @@ -1813,7 +1822,7 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) break; } } - if (n >= 0) { + if (!is_plug_map && n >= 0) { continue; } @@ -1949,8 +1958,11 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) // expression. Also save and restore the command line // for "normal :". if (mp->m_expr) { - int save_vgetc_busy = vgetc_busy; + const int save_vgetc_busy = vgetc_busy; const bool save_may_garbage_collect = may_garbage_collect; + const int save_cursor_row = ui_current_row(); + const int save_cursor_col = ui_current_col(); + const int prev_did_emsg = did_emsg; vgetc_busy = 0; may_garbage_collect = false; @@ -1960,6 +1972,32 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) save_m_str = vim_strsave(mp->m_str); } map_str = eval_map_expr(mp, NUL); + + // The mapping may do anything, but we expect it to take care of + // redrawing. Do put the cursor back where it was. + ui_cursor_goto(save_cursor_row, save_cursor_col); + ui_flush(); + + // If an error was displayed and the expression returns an empty + // string, generate a <Nop> to allow for a redraw. + if (prev_did_emsg != did_emsg && (map_str == NULL || *map_str == NUL)) { + char_u buf[4]; + xfree(map_str); + buf[0] = K_SPECIAL; + buf[1] = KS_EXTRA; + buf[2] = KE_IGNORE; + buf[3] = NUL; + map_str = vim_strsave(buf); + if (State & CMDLINE) { + // redraw the command below the error + msg_didout = true; + if (msg_row < cmdline_row) { + msg_row = cmdline_row; + } + redrawcmd(); + } + } + vgetc_busy = save_vgetc_busy; may_garbage_collect = save_may_garbage_collect; } else { @@ -2455,7 +2493,7 @@ static int vgetorpeek(bool advance) /// 1. a scriptfile /// 2. the keyboard /// -/// As much characters as we can get (up to 'maxlen') are put in "buf" and +/// As many characters as we can get (up to 'maxlen') are put in "buf" and /// NUL terminated (buffer length must be 'maxlen' + 1). /// Minimum for "maxlen" is 3!!!! /// @@ -3490,6 +3528,7 @@ static void showmap(mapblock_T *mp, bool local) if (p_verbose > 0) { last_set_msg(mp->m_script_ctx); } + msg_clr_eos(); ui_flush(); // show one line at a time } diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 5aa564623f..35ad57906b 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -326,16 +326,16 @@ EXTERN int want_garbage_collect INIT(= false); EXTERN int garbage_collect_at_exit INIT(= false); // Special values for current_SID. -#define SID_MODELINE -1 // when using a modeline -#define SID_CMDARG -2 // for "--cmd" argument -#define SID_CARG -3 // for "-c" argument -#define SID_ENV -4 // for sourcing environment variable -#define SID_ERROR -5 // option was reset because of an error -#define SID_NONE -6 // don't set scriptID -#define SID_WINLAYOUT -7 // changing window size -#define SID_LUA -8 // for Lua scripts/chunks -#define SID_API_CLIENT -9 // for API clients -#define SID_STR -10 // for sourcing a string with no script item +#define SID_MODELINE (-1) // when using a modeline +#define SID_CMDARG (-2) // for "--cmd" argument +#define SID_CARG (-3) // for "-c" argument +#define SID_ENV (-4) // for sourcing environment variable +#define SID_ERROR (-5) // option was reset because of an error +#define SID_NONE (-6) // don't set scriptID +#define SID_WINLAYOUT (-7) // changing window size +#define SID_LUA (-8) // for Lua scripts/chunks +#define SID_API_CLIENT (-9) // for API clients +#define SID_STR (-10) // for sourcing a string with no script item // Script CTX being sourced or was sourced to define the current function. EXTERN sctx_T current_sctx INIT(= { 0 COMMA 0 COMMA 0 }); diff --git a/src/nvim/hardcopy.c b/src/nvim/hardcopy.c index 6fc70144ac..eb10c65be9 100644 --- a/src/nvim/hardcopy.c +++ b/src/nvim/hardcopy.c @@ -386,30 +386,43 @@ static uint32_t prt_get_term_color(int colorindex) return cterm_color_8[colorindex % 8]; } -static void prt_get_attr(int hl_id, prt_text_attr_T *pattr, int modec) +static uint32_t prt_get_color(int hl_id, int modec) { int colorindex; uint32_t fg_color; + const char *color = highlight_color(hl_id, "fg#", 'g'); + if (color != NULL) { + RgbValue rgb = name_to_color(color); + if (rgb != -1) { + return (uint32_t)rgb; + } + } + + color = highlight_color(hl_id, "fg", modec); + if (color == NULL) { + colorindex = 0; + } else { + colorindex = atoi(color); + } + + if (colorindex >= 0 && colorindex < t_colors) { + fg_color = prt_get_term_color(colorindex); + } else { + fg_color = PRCOLOR_BLACK; + } + + return fg_color; +} + +static void prt_get_attr(int hl_id, prt_text_attr_T *pattr, int modec) +{ pattr->bold = (highlight_has_attr(hl_id, HL_BOLD, modec) != NULL); pattr->italic = (highlight_has_attr(hl_id, HL_ITALIC, modec) != NULL); pattr->underline = (highlight_has_attr(hl_id, HL_UNDERLINE, modec) != NULL); pattr->undercurl = (highlight_has_attr(hl_id, HL_UNDERCURL, modec) != NULL); - { - const char *color = highlight_color(hl_id, "fg", modec); - if (color == NULL) { - colorindex = 0; - } else { - colorindex = atoi(color); - } - - if (colorindex >= 0 && colorindex < t_colors) { - fg_color = prt_get_term_color(colorindex); - } else { - fg_color = PRCOLOR_BLACK; - } - } + uint32_t fg_color = prt_get_color(hl_id, modec); if (fg_color == PRCOLOR_WHITE) { fg_color = PRCOLOR_BLACK; diff --git a/src/nvim/highlight.c b/src/nvim/highlight.c index 8b998ff62e..e43a56086f 100644 --- a/src/nvim/highlight.c +++ b/src/nvim/highlight.c @@ -800,6 +800,7 @@ HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *e { HlAttrs hlattrs = HLATTRS_INIT; int32_t fg = -1, bg = -1, ctermfg = -1, ctermbg = -1, sp = -1; + int blend = -1; int16_t mask = 0; int16_t cterm_mask = 0; bool cterm_mask_provided = false; @@ -847,6 +848,20 @@ HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *e return hlattrs; } + if (dict->blend.type == kObjectTypeInteger) { + Integer blend0 = dict->blend.data.integer; + if (blend0 < 0 || blend0 > 100) { + api_set_error(err, kErrorTypeValidation, "'blend' is not between 0 to 100"); + } else { + blend = (int)blend0; + } + } else if (HAS_KEY(dict->blend)) { + api_set_error(err, kErrorTypeValidation, "'blend' must be an integer"); + } + if (ERROR_SET(err)) { + return hlattrs; + } + if (HAS_KEY(dict->link)) { if (link_id) { *link_id = object_to_hl_id(dict->link, "link", err); @@ -908,6 +923,7 @@ HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *e hlattrs.rgb_bg_color = bg; hlattrs.rgb_fg_color = fg; hlattrs.rgb_sp_color = sp; + hlattrs.hl_blend = blend; hlattrs.cterm_bg_color = ctermbg == -1 ? 0 : ctermbg + 1; hlattrs.cterm_fg_color = ctermfg == -1 ? 0 : ctermfg + 1; hlattrs.cterm_ae_attr = cterm_mask; @@ -927,10 +943,11 @@ int object_to_color(Object val, char *key, bool rgb, Error *err) } else if (val.type == kObjectTypeString) { String str = val.data.string; // TODO(bfredl): be more fancy with "bg", "fg" etc + if (!str.size || STRICMP(str.data, "NONE") == 0) { + return -1; + } int color; - if (!str.size) { - color = 0; - } else if (rgb) { + if (rgb) { color = name_to_color(str.data); } else { color = name_to_ctermcolor(str.data); diff --git a/src/nvim/keymap.h b/src/nvim/keymap.h index ae2ec7835e..4bf0617dfa 100644 --- a/src/nvim/keymap.h +++ b/src/nvim/keymap.h @@ -245,6 +245,7 @@ enum key_extra { KE_EVENT = 102, // event KE_LUA = 103, // lua special key KE_COMMAND = 104, // <Cmd> special key + KE_AUCMD_SPECIAL = 105, }; /* @@ -443,6 +444,8 @@ enum key_extra { #define K_COMMAND TERMCAP2KEY(KS_EXTRA, KE_COMMAND) #define K_LUA TERMCAP2KEY(KS_EXTRA, KE_LUA) +#define K_AUCMD_SPECIAL TERMCAP2KEY(KS_EXTRA, KE_AUCMD_SPECIAL) + // Bits for modifier mask // 0x01 cannot be used, because the modifier must be 0x02 or higher #define MOD_MASK_SHIFT 0x02 diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c index 0fbd56ed53..2b54e56df1 100644 --- a/src/nvim/lua/converter.c +++ b/src/nvim/lua/converter.c @@ -156,7 +156,7 @@ static LuaTableProps nlua_traverse_table(lua_State *const lstate) && ret.string_keys_num == 0)) { ret.type = kObjectTypeArray; if (tsize == 0 && lua_getmetatable(lstate, -1)) { - nlua_pushref(lstate, nlua_empty_dict_ref); + nlua_pushref(lstate, nlua_global_refs->empty_dict_ref); if (lua_rawequal(lstate, -2, -1)) { ret.type = kObjectTypeDictionary; } @@ -316,7 +316,7 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv) LuaRef table_ref = LUA_NOREF; if (lua_getmetatable(lstate, -1)) { lua_pop(lstate, 1); - table_ref = nlua_ref(lstate, -1); + table_ref = nlua_ref_global(lstate, -1); } const LuaTableProps table_props = nlua_traverse_table(lstate); @@ -389,7 +389,7 @@ nlua_pop_typval_table_processing_end: } case LUA_TFUNCTION: { LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState)); - state->lua_callable.func_ref = nlua_ref(lstate, -1); + state->lua_callable.func_ref = nlua_ref_global(lstate, -1); char_u *name = register_cfunc(&nlua_CFunction_func_call, &nlua_CFunction_func_free, @@ -401,7 +401,7 @@ nlua_pop_typval_table_processing_end: } case LUA_TUSERDATA: { // TODO(bfredl): check mt.__call and convert to function? - nlua_pushref(lstate, nlua_nil_ref); + nlua_pushref(lstate, nlua_global_refs->nil_ref); bool is_nil = lua_rawequal(lstate, -2, -1); lua_pop(lstate, 1); if (is_nil) { @@ -445,7 +445,7 @@ static bool typval_conv_special = false; if (typval_conv_special) { \ lua_pushnil(lstate); \ } else { \ - nlua_pushref(lstate, nlua_nil_ref); \ + nlua_pushref(lstate, nlua_global_refs->nil_ref); \ } \ } while (0) @@ -495,7 +495,7 @@ static bool typval_conv_special = false; nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary); \ } else { \ lua_createtable(lstate, 0, 0); \ - nlua_pushref(lstate, nlua_empty_dict_ref); \ + nlua_pushref(lstate, nlua_global_refs->empty_dict_ref); \ lua_setmetatable(lstate, -2); \ } \ } while (0) @@ -734,7 +734,7 @@ void nlua_push_Dictionary(lua_State *lstate, const Dictionary dict, bool special } else { lua_createtable(lstate, 0, (int)dict.size); if (dict.size == 0 && !special) { - nlua_pushref(lstate, nlua_empty_dict_ref); + nlua_pushref(lstate, nlua_global_refs->empty_dict_ref); lua_setmetatable(lstate, -2); } } @@ -782,7 +782,7 @@ void nlua_push_Object(lua_State *lstate, const Object obj, bool special) if (special) { lua_pushnil(lstate); } else { - nlua_pushref(lstate, nlua_nil_ref); + nlua_pushref(lstate, nlua_global_refs->nil_ref); } break; case kObjectTypeLuaRef: { @@ -1199,14 +1199,14 @@ Object nlua_pop_Object(lua_State *const lstate, bool ref, Error *const err) case LUA_TFUNCTION: if (ref) { - *cur.obj = LUAREF_OBJ(nlua_ref(lstate, -1)); + *cur.obj = LUAREF_OBJ(nlua_ref_global(lstate, -1)); } else { goto type_error; } break; case LUA_TUSERDATA: { - nlua_pushref(lstate, nlua_nil_ref); + nlua_pushref(lstate, nlua_global_refs->nil_ref); bool is_nil = lua_rawequal(lstate, -2, -1); lua_pop(lstate, 1); if (is_nil) { @@ -1240,7 +1240,7 @@ type_error: LuaRef nlua_pop_LuaRef(lua_State *const lstate, Error *err) { - LuaRef rv = nlua_ref(lstate, -1); + LuaRef rv = nlua_ref_global(lstate, -1); lua_pop(lstate, 1); return rv; } diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 029e7eb660..18abf04ff6 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -45,6 +45,8 @@ static int in_fast_callback = 0; // Initialized in nlua_init(). static lua_State *global_lstate = NULL; +static uv_thread_t main_thread; + typedef struct { Error err; String lua_err_str; @@ -65,11 +67,16 @@ typedef struct { } #if __has_feature(address_sanitizer) -static PMap(handle_T) nlua_ref_markers = MAP_INIT; static bool nlua_track_refs = false; # define NLUA_TRACK_REFS #endif +typedef enum luv_err_type { + kCallback, + kThread, + kThreadCallback, +} luv_err_t; + /// Convert lua error into a Vim error message /// /// @param lstate Lua interpreter state. @@ -122,8 +129,21 @@ static int nlua_nvim_version(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL static void nlua_luv_error_event(void **argv) { char *error = (char *)argv[0]; + luv_err_t type = (luv_err_t)(intptr_t)argv[1]; msg_ext_set_kind("lua_error"); - semsg_multiline("Error executing luv callback:\n%s", error); + switch (type) { + case kCallback: + semsg_multiline("Error executing luv callback:\n%s", error); + break; + case kThread: + semsg_multiline("Error in luv thread:\n%s", error); + break; + case kThreadCallback: + semsg_multiline("Error in luv callback, thread:\n%s", error); + break; + default: + break; + } xfree(error); } @@ -148,7 +168,7 @@ static int nlua_luv_cfpcall(lua_State *lstate, int nargs, int nresult, int flags const char *error = lua_tostring(lstate, -1); multiqueue_put(main_loop.events, nlua_luv_error_event, - 1, xstrdup(error)); + 2, xstrdup(error), (intptr_t)kCallback); lua_pop(lstate, 1); // error message retval = -status; } else { // LUA_OK @@ -162,12 +182,112 @@ static int nlua_luv_cfpcall(lua_State *lstate, int nargs, int nresult, int flags return retval; } +static int nlua_luv_thread_cb_cfpcall(lua_State *lstate, int nargs, int nresult, + int flags) +{ + return nlua_luv_thread_common_cfpcall(lstate, nargs, nresult, flags, true); +} + +static int nlua_luv_thread_cfpcall(lua_State *lstate, int nargs, int nresult, + int flags) + FUNC_ATTR_NONNULL_ALL +{ + return nlua_luv_thread_common_cfpcall(lstate, nargs, nresult, flags, false); +} + +static int nlua_luv_thread_cfcpcall(lua_State *lstate, lua_CFunction func, + void *ud, int flags) + FUNC_ATTR_NONNULL_ARG(1, 2) +{ + lua_pushcfunction(lstate, func); + lua_pushlightuserdata(lstate, ud); + int retval = nlua_luv_thread_cfpcall(lstate, 1, 0, flags); + return retval; +} + +static int nlua_luv_thread_common_cfpcall(lua_State *lstate, int nargs, int nresult, + int flags, bool is_callback) + FUNC_ATTR_NONNULL_ALL +{ + int retval; + + int top = lua_gettop(lstate); + int status = lua_pcall(lstate, nargs, nresult, 0); + if (status) { + if (status == LUA_ERRMEM && !(flags & LUVF_CALLBACK_NOEXIT)) { + // Terminate this thread, as the main thread may be able to continue + // execution. + mch_errmsg(e_outofmem); + mch_errmsg("\n"); + lua_close(lstate); +#ifdef WIN32 + ExitThread(0); +#else + pthread_exit(0); +#endif + } + const char *error = lua_tostring(lstate, -1); + + loop_schedule_deferred(&main_loop, + event_create(nlua_luv_error_event, 2, + xstrdup(error), + is_callback + ? (intptr_t)kThreadCallback + : (intptr_t)kThread)); + lua_pop(lstate, 1); // error message + retval = -status; + } else { // LUA_OK + if (nresult == LUA_MULTRET) { + nresult = lua_gettop(lstate) - top + nargs + 1; + } + retval = nresult; + } + + return retval; +} + +static int nlua_thr_api_nvim__get_runtime(lua_State *lstate) +{ + if (lua_gettop(lstate) != 3) { + return luaL_error(lstate, "Expected 3 arguments"); + } + + luaL_checktype(lstate, -1, LUA_TTABLE); + lua_getfield(lstate, -1, "is_lua"); + if (!lua_isboolean(lstate, -1)) { + return luaL_error(lstate, "is_lua is not a boolean"); + } + bool is_lua = lua_toboolean(lstate, -1); + lua_pop(lstate, 2); + + luaL_checktype(lstate, -1, LUA_TBOOLEAN); + bool all = lua_toboolean(lstate, -1); + lua_pop(lstate, 1); + + Error err = ERROR_INIT; + const Array pat = nlua_pop_Array(lstate, &err); + if (ERROR_SET(&err)) { + luaL_where(lstate, 1); + lua_pushstring(lstate, err.msg); + api_clear_error(&err); + lua_concat(lstate, 2); + return lua_error(lstate); + } + + ArrayOf(String) ret = runtime_get_named_thread(is_lua, pat, all); + nlua_push_Array(lstate, ret, true); + api_free_array(ret); + api_free_array(pat); + + return 1; +} + static void nlua_schedule_event(void **argv) { LuaRef cb = (LuaRef)(ptrdiff_t)argv[0]; lua_State *const lstate = global_lstate; nlua_pushref(lstate, cb); - nlua_unref(lstate, cb); + nlua_unref_global(lstate, cb); if (nlua_pcall(lstate, 0, 0)) { nlua_error(lstate, _("Error executing vim.schedule lua callback: %.*s")); } @@ -184,7 +304,7 @@ static int nlua_schedule(lua_State *const lstate) return lua_error(lstate); } - LuaRef cb = nlua_ref(lstate, 1); + LuaRef cb = nlua_ref_global(lstate, 1); multiqueue_put(main_loop.events, nlua_schedule_event, 1, (void *)(ptrdiff_t)cb); @@ -302,6 +422,152 @@ static int nlua_wait(lua_State *lstate) return 2; } +static nlua_ref_state_t *nlua_new_ref_state(lua_State *lstate, bool is_thread) + FUNC_ATTR_NONNULL_ALL +{ + nlua_ref_state_t *ref_state = lua_newuserdata(lstate, sizeof(*ref_state)); + memset(ref_state, 0, sizeof(*ref_state)); + ref_state->nil_ref = LUA_NOREF; + ref_state->empty_dict_ref = LUA_NOREF; + if (!is_thread) { + nlua_global_refs = ref_state; + } + return ref_state; +} + +static nlua_ref_state_t *nlua_get_ref_state(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + lua_getfield(lstate, LUA_REGISTRYINDEX, "nlua.ref_state"); + nlua_ref_state_t *ref_state = lua_touserdata(lstate, -1); + lua_pop(lstate, 1); + + return ref_state; +} + +LuaRef nlua_get_nil_ref(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + nlua_ref_state_t *ref_state = nlua_get_ref_state(lstate); + return ref_state->nil_ref; +} + +LuaRef nlua_get_empty_dict_ref(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + nlua_ref_state_t *ref_state = nlua_get_ref_state(lstate); + return ref_state->empty_dict_ref; +} + +int nlua_get_global_ref_count(void) +{ + return nlua_global_refs->ref_count; +} + +static void nlua_common_vim_init(lua_State *lstate, bool is_thread) + FUNC_ATTR_NONNULL_ARG(1) +{ + nlua_ref_state_t *ref_state = nlua_new_ref_state(lstate, is_thread); + lua_setfield(lstate, LUA_REGISTRYINDEX, "nlua.ref_state"); + + // vim.is_thread + lua_pushboolean(lstate, is_thread); + lua_setfield(lstate, LUA_REGISTRYINDEX, "nvim.thread"); + lua_pushcfunction(lstate, &nlua_is_thread); + lua_setfield(lstate, -2, "is_thread"); + + // vim.NIL + lua_newuserdata(lstate, 0); + lua_createtable(lstate, 0, 0); + lua_pushcfunction(lstate, &nlua_nil_tostring); + lua_setfield(lstate, -2, "__tostring"); + lua_setmetatable(lstate, -2); + ref_state->nil_ref = nlua_ref(lstate, ref_state, -1); + lua_pushvalue(lstate, -1); + lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.NIL"); + lua_setfield(lstate, -2, "NIL"); + + // vim._empty_dict_mt + lua_createtable(lstate, 0, 0); + lua_pushcfunction(lstate, &nlua_empty_dict_tostring); + lua_setfield(lstate, -2, "__tostring"); + ref_state->empty_dict_ref = nlua_ref(lstate, ref_state, -1); + lua_pushvalue(lstate, -1); + lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.empty_dict"); + lua_setfield(lstate, -2, "_empty_dict_mt"); + + // vim.loop + if (is_thread) { + luv_set_callback(lstate, nlua_luv_thread_cb_cfpcall); + luv_set_thread(lstate, nlua_luv_thread_cfpcall); + luv_set_cthread(lstate, nlua_luv_thread_cfcpcall); + } else { + luv_set_loop(lstate, &main_loop.uv); + luv_set_callback(lstate, nlua_luv_cfpcall); + } + luaopen_luv(lstate); + lua_pushvalue(lstate, -1); + lua_setfield(lstate, -3, "loop"); + + // package.loaded.luv = vim.loop + // otherwise luv will be reinitialized when require'luv' + lua_getglobal(lstate, "package"); + lua_getfield(lstate, -1, "loaded"); + lua_pushvalue(lstate, -3); + lua_setfield(lstate, -2, "luv"); + lua_pop(lstate, 3); +} + +static void nlua_common_package_init(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + { + const char *code = (char *)&shared_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(shared_module) - 1, "@vim/shared.lua") + || nlua_pcall(lstate, 0, 0)) { + nlua_error(lstate, _("E5106: Error while creating shared module: %.*s\n")); + return; + } + } + + { + const char *code = (char *)&lua_load_package_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(lua_load_package_module) - 1, "@vim/_load_package.lua") + || lua_pcall(lstate, 0, 0, 0)) { + nlua_error(lstate, _("E5106: Error while creating _load_package module: %.*s")); + return; + } + } + + { + lua_getglobal(lstate, "package"); // [package] + lua_getfield(lstate, -1, "loaded"); // [package, loaded] + + const char *code = (char *)&inspect_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(inspect_module) - 1, "@vim/inspect.lua") + || nlua_pcall(lstate, 0, 1)) { + nlua_error(lstate, _("E5106: Error while creating inspect module: %.*s\n")); + return; + } + + // [package, loaded, inspect] + lua_setfield(lstate, -2, "vim.inspect"); // [package, loaded] + } + + { + const char *code = (char *)&lua_F_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(lua_F_module) - 1, "@vim/F.lua") + || nlua_pcall(lstate, 0, 1)) { + nlua_error(lstate, _("E5106: Error while creating vim.F module: %.*s\n")); + return; + } + // [package, loaded, module] + lua_setfield(lstate, -2, "vim.F"); // [package, loaded] + + lua_pop(lstate, 2); // [] + } +} + /// Initialize lua interpreter state /// /// Called by lua interpreter itself to initialize state. @@ -362,80 +628,22 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL lua_pushcfunction(lstate, &nlua_wait); lua_setfield(lstate, -2, "wait"); - // vim.NIL - lua_newuserdata(lstate, 0); - lua_createtable(lstate, 0, 0); - lua_pushcfunction(lstate, &nlua_nil_tostring); - lua_setfield(lstate, -2, "__tostring"); - lua_setmetatable(lstate, -2); - nlua_nil_ref = nlua_ref(lstate, -1); - lua_pushvalue(lstate, -1); - lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.NIL"); - lua_setfield(lstate, -2, "NIL"); - - // vim._empty_dict_mt - lua_createtable(lstate, 0, 0); - lua_pushcfunction(lstate, &nlua_empty_dict_tostring); - lua_setfield(lstate, -2, "__tostring"); - nlua_empty_dict_ref = nlua_ref(lstate, -1); - lua_pushvalue(lstate, -1); - lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.empty_dict"); - lua_setfield(lstate, -2, "_empty_dict_mt"); + nlua_common_vim_init(lstate, false); // internal vim._treesitter... API nlua_add_treesitter(lstate); - // vim.loop - luv_set_loop(lstate, &main_loop.uv); - luv_set_callback(lstate, nlua_luv_cfpcall); - luaopen_luv(lstate); - lua_pushvalue(lstate, -1); - lua_setfield(lstate, -3, "loop"); - - // package.loaded.luv = vim.loop - // otherwise luv will be reinitialized when require'luv' - lua_getglobal(lstate, "package"); - lua_getfield(lstate, -1, "loaded"); - lua_pushvalue(lstate, -3); - lua_setfield(lstate, -2, "luv"); - lua_pop(lstate, 3); - - nlua_state_add_stdlib(lstate); + nlua_state_add_stdlib(lstate, false); lua_setglobal(lstate, "vim"); - { - const char *code = (char *)&shared_module[0]; - if (luaL_loadbuffer(lstate, code, sizeof(shared_module) - 1, "@vim/shared.lua") - || nlua_pcall(lstate, 0, 0)) { - nlua_error(lstate, _("E5106: Error while creating shared module: %.*s\n")); - return 1; - } - } + nlua_common_package_init(lstate); { lua_getglobal(lstate, "package"); // [package] lua_getfield(lstate, -1, "loaded"); // [package, loaded] - const char *code = (char *)&inspect_module[0]; - if (luaL_loadbuffer(lstate, code, sizeof(inspect_module) - 1, "@vim/inspect.lua") - || nlua_pcall(lstate, 0, 1)) { - nlua_error(lstate, _("E5106: Error while creating inspect module: %.*s\n")); - return 1; - } - // [package, loaded, inspect] - lua_setfield(lstate, -2, "vim.inspect"); // [package, loaded] - - code = (char *)&lua_F_module[0]; - if (luaL_loadbuffer(lstate, code, sizeof(lua_F_module) - 1, "@vim/F.lua") - || nlua_pcall(lstate, 0, 1)) { - nlua_error(lstate, _("E5106: Error while creating vim.F module: %.*s\n")); - return 1; - } - // [package, loaded, module] - lua_setfield(lstate, -2, "vim.F"); // [package, loaded] - - code = (char *)&lua_filetype_module[0]; + char *code = (char *)&lua_filetype_module[0]; if (luaL_loadbuffer(lstate, code, sizeof(lua_filetype_module) - 1, "@vim/filetype.lua") || nlua_pcall(lstate, 0, 1)) { nlua_error(lstate, _("E5106: Error while creating vim.filetype module: %.*s")); @@ -495,9 +703,60 @@ void nlua_init(void) luaL_openlibs(lstate); nlua_state_init(lstate); + luv_set_thread_cb(nlua_thread_acquire_vm, nlua_common_free_all_mem); + global_lstate = lstate; + + main_thread = uv_thread_self(); } +static lua_State *nlua_thread_acquire_vm(void) +{ + // If it is called from the main thread, it will attempt to rebuild the cache. + const uv_thread_t self = uv_thread_self(); + if (uv_thread_equal(&main_thread, &self)) { + runtime_search_path_validate(); + } + + lua_State *lstate = luaL_newstate(); + + // Add in the lua standard libraries + luaL_openlibs(lstate); + + // print + lua_pushcfunction(lstate, &nlua_print); + lua_setglobal(lstate, "print"); + + lua_pushinteger(lstate, 0); + lua_setfield(lstate, LUA_REGISTRYINDEX, "nlua.refcount"); + + // vim + lua_newtable(lstate); + + nlua_common_vim_init(lstate, true); + + nlua_state_add_stdlib(lstate, true); + + lua_setglobal(lstate, "vim"); + + nlua_common_package_init(lstate); + + lua_getglobal(lstate, "vim"); + lua_getglobal(lstate, "package"); + lua_getfield(lstate, -1, "loaded"); + lua_getfield(lstate, -1, "vim.inspect"); + lua_setfield(lstate, -4, "inspect"); + lua_pop(lstate, 3); + + lua_getglobal(lstate, "vim"); + lua_createtable(lstate, 0, 0); + lua_pushcfunction(lstate, nlua_thr_api_nvim__get_runtime); + lua_setfield(lstate, -2, "nvim__get_runtime"); + lua_setfield(lstate, -2, "api"); + lua_pop(lstate, 1); + + return lstate; +} void nlua_free_all_mem(void) { @@ -505,26 +764,30 @@ void nlua_free_all_mem(void) return; } lua_State *lstate = global_lstate; + nlua_common_free_all_mem(lstate); +} - nlua_unref(lstate, nlua_nil_ref); - nlua_unref(lstate, nlua_empty_dict_ref); +static void nlua_common_free_all_mem(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + nlua_ref_state_t *ref_state = nlua_get_ref_state(lstate); + nlua_unref(lstate, ref_state, ref_state->nil_ref); + nlua_unref(lstate, ref_state, ref_state->empty_dict_ref); #ifdef NLUA_TRACK_REFS - if (nlua_refcount) { - fprintf(stderr, "%d lua references were leaked!", nlua_refcount); + if (ref_state->ref_count) { + fprintf(stderr, "%d lua references were leaked!", ref_state->ref_count); } if (nlua_track_refs) { // in case there are leaked luarefs, leak the associated memory // to get LeakSanitizer stacktraces on exit - pmap_destroy(handle_T)(&nlua_ref_markers); + pmap_destroy(handle_T)(&ref_state->ref_markers); } #endif - nlua_refcount = 0; lua_close(lstate); } - static void nlua_print_event(void **argv) { char *str = argv[0]; @@ -602,9 +865,18 @@ static int nlua_print(lua_State *const lstate) #undef PRINT_ERROR ga_append(&msg_ga, NUL); - if (in_fast_callback) { + lua_getfield(lstate, LUA_REGISTRYINDEX, "nvim.thread"); + bool is_thread = lua_toboolean(lstate, -1); + lua_pop(lstate, 1); + + if (is_thread) { + loop_schedule_deferred(&main_loop, + event_create(nlua_print_event, 2, + msg_ga.ga_data, + (intptr_t)msg_ga.ga_len)); + } else if (in_fast_callback) { multiqueue_put(main_loop.events, nlua_print_event, - 2, msg_ga.ga_data, msg_ga.ga_len); + 2, msg_ga.ga_data, (intptr_t)msg_ga.ga_len); } else { nlua_print_event((void *[]){ msg_ga.ga_data, (void *)(intptr_t)msg_ga.ga_len }); @@ -613,10 +885,12 @@ static int nlua_print(lua_State *const lstate) nlua_print_error: ga_clear(&msg_ga); + char *buff = xmalloc(IOSIZE); const char *fmt = _("E5114: Error while converting print argument #%i: %.*s"); - size_t len = (size_t)vim_snprintf((char *)IObuff, IOSIZE, fmt, curargidx, + size_t len = (size_t)vim_snprintf(buff, IOSIZE, fmt, curargidx, (int)errmsg_len, errmsg); - lua_pushlstring(lstate, (char *)IObuff, len); + lua_pushlstring(lstate, buff, len); + xfree(buff); return lua_error(lstate); } @@ -806,40 +1080,53 @@ static int nlua_getenv(lua_State *lstate) /// add the value to the registry -LuaRef nlua_ref(lua_State *lstate, int index) +/// The current implementation does not support calls from threads. +LuaRef nlua_ref(lua_State *lstate, nlua_ref_state_t *ref_state, int index) { lua_pushvalue(lstate, index); LuaRef ref = luaL_ref(lstate, LUA_REGISTRYINDEX); if (ref > 0) { - nlua_refcount++; + ref_state->ref_count++; #ifdef NLUA_TRACK_REFS if (nlua_track_refs) { // dummy allocation to make LeakSanitizer track our luarefs - pmap_put(handle_T)(&nlua_ref_markers, ref, xmalloc(3)); + pmap_put(handle_T)(&ref_state->ref_markers, ref, xmalloc(3)); } #endif } return ref; } + +LuaRef nlua_ref_global(lua_State *lstate, int index) +{ + return nlua_ref(lstate, nlua_global_refs, index); +} + /// remove the value from the registry -void nlua_unref(lua_State *lstate, LuaRef ref) +void nlua_unref(lua_State *lstate, nlua_ref_state_t *ref_state, LuaRef ref) { if (ref > 0) { - nlua_refcount--; + ref_state->ref_count--; #ifdef NLUA_TRACK_REFS // NB: don't remove entry from map to track double-unref if (nlua_track_refs) { - xfree(pmap_get(handle_T)(&nlua_ref_markers, ref)); + xfree(pmap_get(handle_T)(&ref_state->ref_markers, ref)); } #endif luaL_unref(lstate, LUA_REGISTRYINDEX, ref); } } +void nlua_unref_global(lua_State *lstate, LuaRef ref) +{ + nlua_unref(lstate, nlua_global_refs, ref); +} + + void api_free_luaref(LuaRef ref) { - nlua_unref(global_lstate, ref); + nlua_unref_global(global_lstate, ref); } /// push a value referenced in the registry @@ -861,7 +1148,7 @@ LuaRef api_new_luaref(LuaRef original_ref) lua_State *const lstate = global_lstate; nlua_pushref(lstate, original_ref); - LuaRef new_ref = nlua_ref(lstate, -1); + LuaRef new_ref = nlua_ref_global(lstate, -1); lua_pop(lstate, 1); return new_ref; } @@ -1063,6 +1350,16 @@ Object nlua_exec(const String str, const Array args, Error *err) return nlua_pop_Object(lstate, false, err); } +bool nlua_ref_is_function(LuaRef ref) +{ + lua_State *const lstate = global_lstate; + nlua_pushref(lstate, ref); + bool is_function = (lua_type(lstate, -1) == LUA_TFUNCTION); + lua_pop(lstate, 1); + + return is_function; +} + /// call a LuaRef as a function (or table with __call metamethod) /// /// @param ref the reference to call (not consumed) @@ -1391,6 +1688,13 @@ cleanup: return ret; } +static int nlua_is_thread(lua_State *lstate) +{ + lua_getfield(lstate, LUA_REGISTRYINDEX, "nvim.thread"); + + return 1; +} + // Required functions for lua c functions as VimL callbacks int nlua_CFunction_func_call(int argcount, typval_T *argvars, typval_T *rettv, void *state) @@ -1407,7 +1711,7 @@ void nlua_CFunction_func_free(void *state) lua_State *const lstate = global_lstate; LuaCFunctionState *funcstate = (LuaCFunctionState *)state; - nlua_unref(lstate, funcstate->lua_callable.func_ref); + nlua_unref_global(lstate, funcstate->lua_callable.func_ref); xfree(funcstate); } @@ -1457,7 +1761,7 @@ char_u *nlua_register_table_as_callable(typval_T *const arg) lua_pop(lstate, 2); // [table] LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState)); - state->lua_callable.func_ref = nlua_ref(lstate, -1); + state->lua_callable.func_ref = nlua_ref_global(lstate, -1); char_u *name = register_cfunc(&nlua_CFunction_func_call, &nlua_CFunction_func_free, state); @@ -1520,8 +1824,31 @@ void nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap) lua_pushinteger(lstate, eap->line2); lua_setfield(lstate, -2, "line2"); + lua_newtable(lstate); // f-args table lua_pushstring(lstate, (const char *)eap->arg); - lua_setfield(lstate, -2, "args"); + lua_pushvalue(lstate, -1); // Reference for potential use on f-args + lua_setfield(lstate, -4, "args"); + + // Split args by unescaped whitespace |<f-args>| (nargs dependent) + if (cmd->uc_argt & EX_NOSPC) { + // Commands where nargs = 1 or "?" fargs is the same as args + lua_rawseti(lstate, -2, 1); + } else { + // Commands with more than one possible argument we split + lua_pop(lstate, 1); // Pop the reference of opts.args + int length = (int)STRLEN(eap->arg); + int start = 0; + int end = 0; + int i = 1; + bool res = true; + while (res) { + res = uc_split_args_iter(eap->arg, i, &start, &end, length); + lua_pushlstring(lstate, (const char *)eap->arg + start, (size_t)(end - start + 1)); + lua_rawseti(lstate, -2, i); + i++; + } + } + lua_setfield(lstate, -2, "fargs"); lua_pushstring(lstate, (const char *)&eap->regname); lua_setfield(lstate, -2, "reg"); diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h index bf78f7ec5e..47ac51dadb 100644 --- a/src/nvim/lua/executor.h +++ b/src/nvim/lua/executor.h @@ -4,6 +4,7 @@ #include <lauxlib.h> #include <lua.h> +#include "nvim/assert.h" #include "nvim/api/private/defs.h" #include "nvim/eval/typval.h" #include "nvim/ex_cmds_defs.h" @@ -14,18 +15,14 @@ // Generated by msgpack-gen.lua void nlua_add_api_functions(lua_State *lstate) REAL_FATTR_NONNULL_ALL; -EXTERN LuaRef nlua_nil_ref INIT(= LUA_NOREF); -EXTERN LuaRef nlua_empty_dict_ref INIT(= LUA_NOREF); - -EXTERN int nlua_refcount INIT(= 0); - -#define set_api_error(s, err) \ - do { \ - Error *err_ = (err); \ - err_->type = kErrorTypeException; \ - err_->set = true; \ - memcpy(&err_->msg[0], s, sizeof(s)); \ - } while (0) +typedef struct { + LuaRef nil_ref; + LuaRef empty_dict_ref; + int ref_count; +#if __has_feature(address_sanitizer) + PMap(handle_T) ref_markers; +#endif +} nlua_ref_state_t; #define NLUA_CLEAR_REF(x) \ do { \ @@ -39,4 +36,7 @@ EXTERN int nlua_refcount INIT(= 0); #ifdef INCLUDE_GENERATED_DECLARATIONS # include "lua/executor.h.generated.h" #endif + +EXTERN nlua_ref_state_t *nlua_global_refs INIT(= NULL); + #endif // NVIM_LUA_EXECUTOR_H diff --git a/src/nvim/lua/stdlib.c b/src/nvim/lua/stdlib.c index 55b23cf0c8..c2ce899a74 100644 --- a/src/nvim/lua/stdlib.c +++ b/src/nvim/lua/stdlib.c @@ -471,43 +471,52 @@ static int nlua_stricmp(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL } -void nlua_state_add_stdlib(lua_State *const lstate) +void nlua_state_add_stdlib(lua_State *const lstate, bool is_thread) { - // stricmp - lua_pushcfunction(lstate, &nlua_stricmp); - lua_setfield(lstate, -2, "stricmp"); - // str_utfindex - lua_pushcfunction(lstate, &nlua_str_utfindex); - lua_setfield(lstate, -2, "str_utfindex"); - // str_byteindex - lua_pushcfunction(lstate, &nlua_str_byteindex); - lua_setfield(lstate, -2, "str_byteindex"); - // str_utf_pos - lua_pushcfunction(lstate, &nlua_str_utf_pos); - lua_setfield(lstate, -2, "str_utf_pos"); - // str_utf_start - lua_pushcfunction(lstate, &nlua_str_utf_start); - lua_setfield(lstate, -2, "str_utf_start"); - // str_utf_end - lua_pushcfunction(lstate, &nlua_str_utf_end); - lua_setfield(lstate, -2, "str_utf_end"); - // regex - lua_pushcfunction(lstate, &nlua_regex); - lua_setfield(lstate, -2, "regex"); - luaL_newmetatable(lstate, "nvim_regex"); - luaL_register(lstate, NULL, regex_meta); - - lua_pushvalue(lstate, -1); // [meta, meta] - lua_setfield(lstate, -2, "__index"); // [meta] - lua_pop(lstate, 1); // don't use metatable now - - // _getvar - lua_pushcfunction(lstate, &nlua_getvar); - lua_setfield(lstate, -2, "_getvar"); - - // _setvar - lua_pushcfunction(lstate, &nlua_setvar); - lua_setfield(lstate, -2, "_setvar"); + if (!is_thread) { + // TODO(bfredl): some of basic string functions should already be + // (or be easy to make) threadsafe + + // stricmp + lua_pushcfunction(lstate, &nlua_stricmp); + lua_setfield(lstate, -2, "stricmp"); + // str_utfindex + lua_pushcfunction(lstate, &nlua_str_utfindex); + lua_setfield(lstate, -2, "str_utfindex"); + // str_byteindex + lua_pushcfunction(lstate, &nlua_str_byteindex); + lua_setfield(lstate, -2, "str_byteindex"); + // str_utf_pos + lua_pushcfunction(lstate, &nlua_str_utf_pos); + lua_setfield(lstate, -2, "str_utf_pos"); + // str_utf_start + lua_pushcfunction(lstate, &nlua_str_utf_start); + lua_setfield(lstate, -2, "str_utf_start"); + // str_utf_end + lua_pushcfunction(lstate, &nlua_str_utf_end); + lua_setfield(lstate, -2, "str_utf_end"); + // regex + lua_pushcfunction(lstate, &nlua_regex); + lua_setfield(lstate, -2, "regex"); + luaL_newmetatable(lstate, "nvim_regex"); + luaL_register(lstate, NULL, regex_meta); + + lua_pushvalue(lstate, -1); // [meta, meta] + lua_setfield(lstate, -2, "__index"); // [meta] + lua_pop(lstate, 1); // don't use metatable now + + // _getvar + lua_pushcfunction(lstate, &nlua_getvar); + lua_setfield(lstate, -2, "_getvar"); + + // _setvar + lua_pushcfunction(lstate, &nlua_setvar); + lua_setfield(lstate, -2, "_setvar"); + + // vim.spell + luaopen_spell(lstate); + lua_setfield(lstate, -2, "spell"); + } // vim.mpack luaopen_mpack(lstate); @@ -526,10 +535,7 @@ void nlua_state_add_stdlib(lua_State *const lstate) lua_pushcfunction(lstate, &nlua_xdl_diff); lua_setfield(lstate, -2, "diff"); - // vim.spell - luaopen_spell(lstate); - lua_setfield(lstate, -2, "spell"); - + // vim.json lua_cjson_new(lstate); lua_setfield(lstate, -2, "json"); } diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index f5993c3f55..68de4f960c 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -43,53 +43,6 @@ assert(vim.inspect) vim.filetype = package.loaded['vim.filetype'] assert(vim.filetype) -local pathtrails = {} -vim._so_trails = {} -for s in (package.cpath..';'):gmatch('[^;]*;') do - s = s:sub(1, -2) -- Strip trailing semicolon - -- Find out path patterns. pathtrail should contain something like - -- /?.so, \?.dll. This allows not to bother determining what correct - -- suffixes are. - local pathtrail = s:match('[/\\][^/\\]*%?.*$') - if pathtrail and not pathtrails[pathtrail] then - pathtrails[pathtrail] = true - table.insert(vim._so_trails, pathtrail) - end -end - -function vim._load_package(name) - local basename = name:gsub('%.', '/') - local paths = {"lua/"..basename..".lua", "lua/"..basename.."/init.lua"} - local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true}) - if #found > 0 then - local f, err = loadfile(found[1]) - return f or error(err) - end - - local so_paths = {} - for _,trail in ipairs(vim._so_trails) do - local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash - table.insert(so_paths, path) - end - - found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true}) - if #found > 0 then - -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is - -- a) strip prefix up to and including the first dash, if any - -- b) replace all dots by underscores - -- c) prepend "luaopen_" - -- So "foo-bar.baz" should result in "luaopen_bar_baz" - local dash = name:find("-", 1, true) - local modname = dash and name:sub(dash + 1) or name - local f, err = package.loadlib(found[1], "luaopen_"..modname:gsub("%.", "_")) - return f or error(err) - end - return nil -end - --- Insert vim._load_package after the preloader at position 2 -table.insert(package.loaders, 2, vim._load_package) - -- These are for loading runtime modules lazily since they aren't available in -- the nvim binary as specified in executor.c setmetatable(vim, { diff --git a/src/nvim/main.c b/src/nvim/main.c index 8991ae7f00..b02ebbe030 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -9,7 +9,7 @@ #include <string.h> #include "nvim/ascii.h" -#include "nvim/aucmd.h" +#include "nvim/autocmd.h" #include "nvim/buffer.h" #include "nvim/channel.h" #include "nvim/charset.h" @@ -154,10 +154,10 @@ bool event_teardown(void) void early_init(mparm_T *paramp) { env_init(); - fs_init(); eval_init(); // init global variables init_path(argv0 ? argv0 : "nvim"); init_normal_cmds(); // Init the table of Normal mode commands. + runtime_init(); highlight_init(); #ifdef WIN32 @@ -230,6 +230,10 @@ int main(int argc, char **argv) // `argc` and `argv` are also copied, so that they can be changed. init_params(¶ms, argc, argv); + // Since os_open is called during the init_startuptime, we need to call + // fs_init before it. + fs_init(); + init_startuptime(¶ms); // Need to find "--clean" before actually parsing arguments. diff --git a/src/nvim/map.c b/src/nvim/map.c index 77ebc2a387..091d653046 100644 --- a/src/nvim/map.c +++ b/src/nvim/map.c @@ -177,6 +177,7 @@ MAP_IMPL(handle_T, ptr_t, DEFAULT_INITIALIZER) MAP_IMPL(String, MsgpackRpcRequestHandler, MSGPACK_HANDLER_INITIALIZER) MAP_IMPL(HlEntry, int, DEFAULT_INITIALIZER) MAP_IMPL(String, handle_T, 0) +MAP_IMPL(String, int, DEFAULT_INITIALIZER) MAP_IMPL(ColorKey, ColorItem, COLOR_ITEM_INITIALIZER) diff --git a/src/nvim/map.h b/src/nvim/map.h index 5e56f4dd65..c9c89bf2fd 100644 --- a/src/nvim/map.h +++ b/src/nvim/map.h @@ -46,6 +46,7 @@ MAP_DECLS(handle_T, ptr_t) MAP_DECLS(String, MsgpackRpcRequestHandler) MAP_DECLS(HlEntry, int) MAP_DECLS(String, handle_T) +MAP_DECLS(String, int) MAP_DECLS(ColorKey, ColorItem) diff --git a/src/nvim/option.c b/src/nvim/option.c index c8e50d4494..4ec6ee3148 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -4277,7 +4277,7 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, } // Save the global value before changing anything. This is needed as for - // a global-only option setting the "local value" infact sets the global + // a global-only option setting the "local value" in fact sets the global // value (since there is only one value). if ((opt_flags & (OPT_LOCAL | OPT_GLOBAL)) == 0) { old_global_value = *(long *)get_varp_scope(&(options[opt_idx]), OPT_GLOBAL); @@ -8165,7 +8165,7 @@ int win_signcol_configured(win_T *wp, int *is_fixed) } int needed_signcols = buf_signcols(wp->w_buffer, maximum); - int ret = MAX(minimum, needed_signcols); + int ret = MAX(minimum, MIN(maximum, needed_signcols)); assert(ret <= SIGN_SHOW_MAX); return ret; } diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index 24c7678633..daf974ee74 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -40,8 +40,10 @@ bool did_try_to_free = false; \ uv_call_start: {} \ uv_fs_t req; \ + fs_loop_lock(); \ ret = func(&fs_loop, &req, __VA_ARGS__); \ uv_fs_req_cleanup(&req); \ + fs_loop_unlock(); \ if (ret == UV_ENOMEM && !did_try_to_free) { \ try_to_free_memory(); \ did_try_to_free = true; \ @@ -52,12 +54,27 @@ uv_call_start: {} \ // Many fs functions from libuv return that value on success. static const int kLibuvSuccess = 0; static uv_loop_t fs_loop; +static uv_mutex_t fs_loop_mutex; // Initialize the fs module void fs_init(void) { uv_loop_init(&fs_loop); + uv_mutex_init_recursive(&fs_loop_mutex); +} + +/// TODO(bfredl): some of these operations should +/// be possible to do the private libuv loop of the +/// thread, instead of contending the global fs loop +void fs_loop_lock(void) +{ + uv_mutex_lock(&fs_loop_mutex); +} + +void fs_loop_unlock(void) +{ + uv_mutex_unlock(&fs_loop_mutex); } @@ -98,9 +115,12 @@ bool os_isrealdir(const char *name) FUNC_ATTR_NONNULL_ALL { uv_fs_t request; + fs_loop_lock(); if (uv_fs_lstat(&fs_loop, &request, name, NULL) != kLibuvSuccess) { + fs_loop_unlock(); return false; } + fs_loop_unlock(); if (S_ISLNK(request.statbuf.st_mode)) { return false; } else { @@ -738,7 +758,9 @@ static int os_stat(const char *name, uv_stat_t *statbuf) return UV_EINVAL; } uv_fs_t request; + fs_loop_lock(); int result = uv_fs_stat(&fs_loop, &request, name, NULL); + fs_loop_unlock(); if (result == kLibuvSuccess) { *statbuf = request.statbuf; } @@ -935,9 +957,11 @@ int os_mkdtemp(const char *template, char *path) FUNC_ATTR_NONNULL_ALL { uv_fs_t request; + fs_loop_lock(); int result = uv_fs_mkdtemp(&fs_loop, &request, template, NULL); + fs_loop_unlock(); if (result == kLibuvSuccess) { - STRNCPY(path, request.path, TEMP_FILE_PATH_MAXLEN); + xstrlcpy(path, request.path, TEMP_FILE_PATH_MAXLEN); } uv_fs_req_cleanup(&request); return result; @@ -962,7 +986,9 @@ int os_rmdir(const char *path) bool os_scandir(Directory *dir, const char *path) FUNC_ATTR_NONNULL_ALL { + fs_loop_lock(); int r = uv_fs_scandir(&fs_loop, &dir->request, path, 0, NULL); + fs_loop_unlock(); if (r < 0) { os_closedir(dir); } @@ -1023,7 +1049,9 @@ bool os_fileinfo_link(const char *path, FileInfo *file_info) return false; } uv_fs_t request; + fs_loop_lock(); bool ok = uv_fs_lstat(&fs_loop, &request, path, NULL) == kLibuvSuccess; + fs_loop_unlock(); if (ok) { file_info->stat = request.statbuf; } @@ -1041,6 +1069,7 @@ bool os_fileinfo_fd(int file_descriptor, FileInfo *file_info) { uv_fs_t request; memset(file_info, 0, sizeof(*file_info)); + fs_loop_lock(); bool ok = uv_fs_fstat(&fs_loop, &request, file_descriptor, @@ -1049,6 +1078,7 @@ bool os_fileinfo_fd(int file_descriptor, FileInfo *file_info) file_info->stat = request.statbuf; } uv_fs_req_cleanup(&request); + fs_loop_unlock(); return ok; } @@ -1165,6 +1195,7 @@ char *os_realpath(const char *name, char *buf) FUNC_ATTR_NONNULL_ARG(1) { uv_fs_t request; + fs_loop_lock(); int result = uv_fs_realpath(&fs_loop, &request, name, NULL); if (result == kLibuvSuccess) { if (buf == NULL) { @@ -1173,6 +1204,7 @@ char *os_realpath(const char *name, char *buf) xstrlcpy(buf, request.ptr, MAXPATHL + 1); } uv_fs_req_cleanup(&request); + fs_loop_unlock(); return result == kLibuvSuccess ? buf : NULL; } diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c index 1c04cb16b3..1ec3e40abe 100644 --- a/src/nvim/runtime.c +++ b/src/nvim/runtime.c @@ -24,6 +24,13 @@ static bool runtime_search_path_valid = false; static int *runtime_search_path_ref = NULL; static RuntimeSearchPath runtime_search_path; +static RuntimeSearchPath runtime_search_path_thread; +static uv_mutex_t runtime_search_path_mutex; + +void runtime_init(void) +{ + uv_mutex_init(&runtime_search_path_mutex); +} /// ":runtime [what] {name}" void ex_runtime(exarg_T *eap) @@ -172,6 +179,17 @@ RuntimeSearchPath runtime_search_path_get_cached(int *ref) return runtime_search_path; } +RuntimeSearchPath copy_runtime_search_path(const RuntimeSearchPath src) +{ + RuntimeSearchPath dst = KV_INITIAL_VALUE; + for (size_t j = 0; j < kv_size(src); j++) { + SearchPathItem src_item = kv_A(src, j); + kv_push(dst, ((SearchPathItem){ xstrdup(src_item.path), src_item.after, src_item.has_lua })); + } + + return dst; +} + void runtime_search_path_unref(RuntimeSearchPath path, int *ref) FUNC_ATTR_NONNULL_ALL { @@ -302,15 +320,35 @@ ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all) { int ref; RuntimeSearchPath path = runtime_search_path_get_cached(&ref); - ArrayOf(String) rv = ARRAY_DICT_INIT; static char buf[MAXPATHL]; + ArrayOf(String) rv = runtime_get_named_common(lua, pat, all, path, buf, sizeof buf); + + runtime_search_path_unref(path, &ref); + return rv; +} + +ArrayOf(String) runtime_get_named_thread(bool lua, Array pat, bool all) +{ + // TODO(bfredl): avoid contention between multiple worker threads? + uv_mutex_lock(&runtime_search_path_mutex); + static char buf[MAXPATHL]; + ArrayOf(String) rv = runtime_get_named_common(lua, pat, all, runtime_search_path_thread, + buf, sizeof buf); + uv_mutex_unlock(&runtime_search_path_mutex); + return rv; +} + +ArrayOf(String) runtime_get_named_common(bool lua, Array pat, bool all, + RuntimeSearchPath path, char *buf, size_t buf_len) +{ + ArrayOf(String) rv = ARRAY_DICT_INIT; for (size_t i = 0; i < kv_size(path); i++) { SearchPathItem *item = &kv_A(path, i); if (lua) { if (item->has_lua == kNone) { - size_t size = (size_t)snprintf(buf, sizeof buf, "%s/lua/", item->path); - item->has_lua = (size < sizeof buf && os_isdir((char_u *)buf)) ? kTrue : kFalse; + size_t size = (size_t)snprintf(buf, buf_len, "%s/lua/", item->path); + item->has_lua = (size < buf_len && os_isdir((char_u *)buf)); } if (item->has_lua == kFalse) { continue; @@ -320,9 +358,9 @@ ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all) for (size_t j = 0; j < pat.size; j++) { Object pat_item = pat.items[j]; if (pat_item.type == kObjectTypeString) { - size_t size = (size_t)snprintf(buf, sizeof buf, "%s/%s", + size_t size = (size_t)snprintf(buf, buf_len, "%s/%s", item->path, pat_item.data.string.data); - if (size < sizeof buf) { + if (size < buf_len) { if (os_file_is_readable(buf)) { ADD(rv, STRING_OBJ(cstr_to_string(buf))); if (!all) { @@ -333,9 +371,7 @@ ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all) } } } - done: - runtime_search_path_unref(path, &ref); return rv; } @@ -569,6 +605,10 @@ void runtime_search_path_validate(void) runtime_search_path = runtime_search_path_build(); runtime_search_path_valid = true; runtime_search_path_ref = NULL; // initially unowned + uv_mutex_lock(&runtime_search_path_mutex); + runtime_search_path_free(runtime_search_path_thread); + runtime_search_path_thread = copy_runtime_search_path(runtime_search_path); + uv_mutex_unlock(&runtime_search_path_mutex); } } diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 15fb6901cc..7fafe3dd6e 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -326,6 +326,18 @@ void redraw_buf_status_later(buf_T *buf) } } +void redraw_win_signcol(win_T *wp) +{ + // If we can compute a change in the automatic sizing of the sign column + // under 'signcolumn=auto:X' and signs currently placed in the buffer, better + // figuring it out here so we can redraw the entire screen for it. + int scwidth = wp->w_scwidth; + wp->w_scwidth = win_signcol_count(wp); + if (wp->w_scwidth != scwidth) { + changed_line_abv_curs_win(wp); + } +} + /// Redraw the parts of the screen that is marked for redraw. /// /// Most code shouldn't call this directly, rather use redraw_later() and @@ -790,12 +802,6 @@ static void win_update(win_T *wp, Providers *providers) linenr_T mod_bot = 0; int save_got_int; - - // If we can compute a change in the automatic sizing of the sign column - // under 'signcolumn=auto:X' and signs currently placed in the buffer, better - // figuring it out here so we can redraw the entire screen for it. - win_signcol_count(wp); - type = wp->w_redr_type; if (type >= NOT_VALID) { @@ -817,6 +823,8 @@ static void win_update(win_T *wp, Providers *providers) return; } + redraw_win_signcol(wp); + init_search_hl(wp); /* Force redraw when width of 'number' or 'relativenumber' column @@ -1846,7 +1854,7 @@ static void win_draw_end(win_T *wp, int c1, int c2, bool draw_margin, int row, i win_hl_attr(wp, HLF_FC)); } // draw the sign column - int count = win_signcol_count(wp); + int count = wp->w_scwidth; if (count > 0) { n = win_fill_end(wp, ' ', ' ', n, win_signcol_width(wp) * count, row, endrow, win_hl_attr(wp, HLF_SC)); @@ -2792,10 +2800,9 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc draw_state = WL_SIGN; /* Show the sign column when there are any signs in this * buffer or when using Netbeans. */ - int count = win_signcol_count(wp); - if (count > 0) { + if (wp->w_scwidth > 0) { get_sign_display_info(false, wp, lnum, sattrs, row, - startrow, filler_lines, filler_todo, count, + startrow, filler_lines, filler_todo, &c_extra, &c_final, extra, sizeof(extra), &p_extra, &n_extra, &char_attr, &draw_state, &sign_idx); @@ -2814,9 +2821,8 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc // number. if (*wp->w_p_scl == 'n' && *(wp->w_p_scl + 1) == 'u' && num_signs > 0 && sign_get_attr(SIGN_TEXT, sattrs, 0, 1)) { - int count = win_signcol_count(wp); get_sign_display_info(true, wp, lnum, sattrs, row, - startrow, filler_lines, filler_todo, count, + startrow, filler_lines, filler_todo, &c_extra, &c_final, extra, sizeof(extra), &p_extra, &n_extra, &char_attr, &draw_state, &sign_idx); @@ -4674,10 +4680,11 @@ static bool use_cursor_line_sign(win_T *wp, linenr_T lnum) // @param[in, out] sign_idxp Index of the displayed sign static void get_sign_display_info(bool nrcol, win_T *wp, linenr_T lnum, sign_attrs_T sattrs[], int row, int startrow, int filler_lines, int filler_todo, - int count, int *c_extrap, int *c_finalp, char_u *extra, + int *c_extrap, int *c_finalp, char_u *extra, size_t extra_size, char_u **pp_extra, int *n_extrap, int *char_attrp, int *draw_statep, int *sign_idxp) { + int count = wp->w_scwidth; // Draw cells with the sign value or blank. *c_extrap = ' '; *c_finalp = NUL; diff --git a/src/nvim/testdir/runnvim.sh b/src/nvim/testdir/runnvim.sh index fdd3f3144b..72d88f9f93 100755 --- a/src/nvim/testdir/runnvim.sh +++ b/src/nvim/testdir/runnvim.sh @@ -65,9 +65,6 @@ main() {( fi fi fi - if test "$FAILED" = 1 ; then - ci_fold start "$test_name" - fi valgrind_check . if test -n "$LOG_DIR" ; then check_sanitizer "$LOG_DIR" @@ -78,9 +75,6 @@ main() {( fi rm -f "$tlog" if test "$FAILED" = 1 ; then - ci_fold end "" - fi - if test "$FAILED" = 1 ; then echo "Test $test_name failed, see output above and summary for more details" >> test.log # When Neovim crashed/aborted it might not have created messages. # test.log itself is used as an indicator to exit non-zero in the Makefile. diff --git a/src/nvim/testdir/test_alot.vim b/src/nvim/testdir/test_alot.vim index c0ac4393c4..5a3d1d56bb 100644 --- a/src/nvim/testdir/test_alot.vim +++ b/src/nvim/testdir/test_alot.vim @@ -39,7 +39,8 @@ source test_put.vim source test_rename.vim source test_scroll_opt.vim source test_shift.vim -source test_sort.vim +" Test fails on windows CI when using the MSVC compiler. +" source test_sort.vim source test_sha256.vim source test_suspend.vim source test_syn_attr.vim diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim index 0146c06109..c39546b9ea 100644 --- a/src/nvim/testdir/test_autocmd.vim +++ b/src/nvim/testdir/test_autocmd.vim @@ -502,7 +502,7 @@ func Test_autocmd_bufwipe_in_SessLoadPost() [CODE] call writefile(content, 'Xvimrc') - call system(v:progpath. ' --headless -i NONE -u Xvimrc --noplugins -S Session.vim -c cq') + call system(GetVimCommand('Xvimrc') .. ' --headless --noplugins -S Session.vim -c cq') let errors = join(readfile('Xerrors')) call assert_match('E814', errors) @@ -562,7 +562,7 @@ func Test_autocmd_bufwipe_in_SessLoadPost2() [CODE] call writefile(content, 'Xvimrc') - call system(v:progpath. ' --headless -i NONE -u Xvimrc --noplugins -S Session.vim -c cq') + call system(GetVimCommand('Xvimrc') .. ' --headless --noplugins -S Session.vim -c cq') let errors = join(readfile('Xerrors')) " This probably only ever matches on unix. call assert_notmatch('Caught deadly signal SEGV', errors) @@ -1506,7 +1506,7 @@ func Test_bufunload_all() call writefile(content, 'Xtest') call delete('Xout') - call system(v:progpath. ' -u NORC -i NONE -N -S Xtest') + call system(GetVimCommandClean() .. ' -N --headless -S Xtest') call assert_true(filereadable('Xout')) call delete('Xxx1') diff --git a/src/nvim/testdir/test_breakindent.vim b/src/nvim/testdir/test_breakindent.vim index b619f2adb6..438edb0257 100644 --- a/src/nvim/testdir/test_breakindent.vim +++ b/src/nvim/testdir/test_breakindent.vim @@ -1,7 +1,7 @@ " Test for breakindent " " Note: if you get strange failures when adding new tests, it might be that -" while the test is run, the breakindent cacheing gets in its way. +" while the test is run, the breakindent caching gets in its way. " It helps to change the tabstop setting and force a redraw (e.g. see " Test_breakindent08()) if !exists('+breakindent') diff --git a/src/nvim/testdir/test_cindent.vim b/src/nvim/testdir/test_cindent.vim index 5dc54111e7..4b702bf2b8 100644 --- a/src/nvim/testdir/test_cindent.vim +++ b/src/nvim/testdir/test_cindent.vim @@ -815,7 +815,7 @@ func Test_cindent_1() } } - public: // <-- this was incoreectly indented before!! + public: // <-- this was incorrectly indented before!! void testfall(); protected: void testfall(); @@ -1792,7 +1792,7 @@ func Test_cindent_1() } } - public: // <-- this was incoreectly indented before!! + public: // <-- this was incorrectly indented before!! void testfall(); protected: void testfall(); @@ -5302,9 +5302,12 @@ endfunc " this was going beyond the end of the line. func Test_cindent_case() new - call setline(1, "case x: // x") + call setline(1, 'case x: // x') set cindent norm! f:a: + call assert_equal('case x:: // x', getline(1)) + + set cindent& bwipe! endfunc diff --git a/src/nvim/testdir/test_digraph.vim b/src/nvim/testdir/test_digraph.vim index d23748a3e3..5965ee48ef 100644 --- a/src/nvim/testdir/test_digraph.vim +++ b/src/nvim/testdir/test_digraph.vim @@ -81,7 +81,7 @@ func Test_digraphs() call Put_Dig(".e") call Put_Dig("a.") " not defined call assert_equal(['ḃ', 'ė', '.'], getline(line('.')-2,line('.'))) - " Diaresis + " Diaeresis call Put_Dig("a:") call Put_Dig(":u") call Put_Dig("b:") " not defined @@ -288,7 +288,7 @@ func Test_digraphs_option() call Put_Dig_BS(".","e") call Put_Dig_BS("a",".") " not defined call assert_equal(['ḃ', 'ė', '.'], getline(line('.')-2,line('.'))) - " Diaresis + " Diaeresis call Put_Dig_BS("a",":") call Put_Dig_BS(":","u") call Put_Dig_BS("b",":") " not defined diff --git a/src/nvim/testdir/test_edit.vim b/src/nvim/testdir/test_edit.vim index a1f6a84a99..360b3aaaa0 100644 --- a/src/nvim/testdir/test_edit.vim +++ b/src/nvim/testdir/test_edit.vim @@ -590,7 +590,7 @@ func Test_edit_CTRL_K() call feedkeys("A\<c-x>\<c-k>\<down>\<down>\<down>\<down>\<cr>\<esc>", 'tnix') call assert_equal(['AA'], getline(1, '$')) - " press an unexecpted key after dictionary completion + " press an unexpected key after dictionary completion %d call setline(1, 'A') call cursor(1, 1) diff --git a/src/nvim/testdir/test_filetype.vim b/src/nvim/testdir/test_filetype.vim index c2cfac6fe3..5f4a7dac6e 100644 --- a/src/nvim/testdir/test_filetype.vim +++ b/src/nvim/testdir/test_filetype.vim @@ -132,6 +132,7 @@ let s:filename_checks = { \ 'cvs': ['cvs123'], \ 'cvsrc': ['.cvsrc'], \ 'cynpp': ['file.cyn'], + \ 'd': ['file.d'], \ 'dart': ['file.dart', 'file.drt'], \ 'datascript': ['file.ds'], \ 'dcd': ['file.dcd'], @@ -154,6 +155,7 @@ let s:filename_checks = { \ 'dot': ['file.dot', 'file.gv'], \ 'dracula': ['file.drac', 'file.drc', 'filelvs', 'filelpe', 'drac.file', 'lpe', 'lvs', 'some-lpe', 'some-lvs'], \ 'dtd': ['file.dtd'], + \ 'dtrace': ['/usr/lib/dtrace/io.d'], \ 'dts': ['file.dts', 'file.dtsi'], \ 'dune': ['jbuild', 'dune', 'dune-project', 'dune-workspace'], \ 'dylan': ['file.dylan'], @@ -802,6 +804,42 @@ func Test_bas_file() filetype off endfunc +func Test_d_file() + filetype on + + call writefile(['looks like D'], 'Xfile.d') + split Xfile.d + call assert_equal('d', &filetype) + bwipe! + + call writefile(['#!/some/bin/dtrace'], 'Xfile.d') + split Xfile.d + call assert_equal('dtrace', &filetype) + bwipe! + + call writefile(['#pragma D option'], 'Xfile.d') + split Xfile.d + call assert_equal('dtrace', &filetype) + bwipe! + + call writefile([':some:thing:'], 'Xfile.d') + split Xfile.d + call assert_equal('dtrace', &filetype) + bwipe! + + call writefile(['module this', '#pragma D option'], 'Xfile.d') + split Xfile.d + call assert_equal('d', &filetype) + bwipe! + + call writefile(['import that', '#pragma D option'], 'Xfile.d') + split Xfile.d + call assert_equal('d', &filetype) + bwipe! + + filetype off +endfunc + func Test_dep3patch_file() filetype on diff --git a/src/nvim/testdir/test_functions.vim b/src/nvim/testdir/test_functions.vim index 6e36f4e3d2..994d74601a 100644 --- a/src/nvim/testdir/test_functions.vim +++ b/src/nvim/testdir/test_functions.vim @@ -1451,6 +1451,10 @@ func Test_getchar() call assert_equal('', getcharstr(0)) call assert_equal('', getcharstr(1)) + call feedkeys("\<M-F2>", '') + call assert_equal("\<M-F2>", getchar(0)) + call assert_equal(0, getchar(0)) + call setline(1, 'xxxx') " call test_setmouse(1, 3) " let v:mouse_win = 9 diff --git a/src/nvim/testdir/test_mapping.vim b/src/nvim/testdir/test_mapping.vim index f88e8cf843..1080a3c85b 100644 --- a/src/nvim/testdir/test_mapping.vim +++ b/src/nvim/testdir/test_mapping.vim @@ -1,6 +1,8 @@ " Tests for mappings and abbreviations source shared.vim +source check.vim +source screendump.vim func Test_abbreviation() " abbreviation with 0x80 should work @@ -451,6 +453,82 @@ func Test_expr_map_gets_cursor() nunmap ! endfunc +func Test_expr_map_restore_cursor() + CheckScreendump + + let lines =<< trim END + call setline(1, ['one', 'two', 'three']) + 2 + set ls=2 + hi! link StatusLine ErrorMsg + noremap <expr> <C-B> Func() + func Func() + let g:on = !get(g:, 'on', 0) + redraws + return '' + endfunc + func Status() + return get(g:, 'on', 0) ? '[on]' : '' + endfunc + set stl=%{Status()} + END + call writefile(lines, 'XtestExprMap') + let buf = RunVimInTerminal('-S XtestExprMap', #{rows: 10}) + call term_sendkeys(buf, "\<C-B>") + call VerifyScreenDump(buf, 'Test_map_expr_1', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('XtestExprMap') +endfunc + +func Test_map_listing() + CheckScreendump + + let lines =<< trim END + nmap a b + END + call writefile(lines, 'XtestMapList') + let buf = RunVimInTerminal('-S XtestMapList', #{rows: 6}) + call term_sendkeys(buf, ": nmap a\<CR>") + call VerifyScreenDump(buf, 'Test_map_list_1', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('XtestMapList') +endfunc + +func Test_expr_map_error() + CheckScreendump + + let lines =<< trim END + func Func() + throw 'test' + return '' + endfunc + + nnoremap <expr> <F2> Func() + cnoremap <expr> <F2> Func() + + call test_override('ui_delay', 10) + END + call writefile(lines, 'XtestExprMap') + let buf = RunVimInTerminal('-S XtestExprMap', #{rows: 10}) + call term_sendkeys(buf, "\<F2>") + call TermWait(buf) + call term_sendkeys(buf, "\<CR>") + call VerifyScreenDump(buf, 'Test_map_expr_2', {}) + + call term_sendkeys(buf, ":abc\<F2>") + call VerifyScreenDump(buf, 'Test_map_expr_3', {}) + call term_sendkeys(buf, "\<Esc>0") + call VerifyScreenDump(buf, 'Test_map_expr_4', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('XtestExprMap') +endfunc + " Test for mapping errors func Test_map_error() call assert_fails('unmap', 'E474:') diff --git a/src/nvim/testdir/test_normal.vim b/src/nvim/testdir/test_normal.vim index e8eebb3fdd..f45cd96733 100644 --- a/src/nvim/testdir/test_normal.vim +++ b/src/nvim/testdir/test_normal.vim @@ -1118,7 +1118,7 @@ func Test_normal20_exmode() endif call writefile(['1a', 'foo', 'bar', '.', 'w! Xfile2', 'q!'], 'Xscript') call writefile(['1', '2'], 'Xfile') - call system(v:progpath .' -e -s < Xscript Xfile') + call system(GetVimCommand() .. ' -e -s < Xscript Xfile') let a=readfile('Xfile2') call assert_equal(['1', 'foo', 'bar', '2'], a) @@ -1171,13 +1171,13 @@ func Test_normal22_zet() endfor call writefile(['1', '2'], 'Xfile_Test_normal22_zet') - let args = ' --headless -u NONE -N -U NONE -i NONE --noplugins' - call system(v:progpath . args . ' -c "%d" -c ":norm! ZZ" Xfile_Test_normal22_zet') + let args = ' -N -i NONE --noplugins -X --headless' + call system(GetVimCommand() .. args .. ' -c "%d" -c ":norm! ZZ" Xfile_Test_normal22_zet') let a = readfile('Xfile_Test_normal22_zet') call assert_equal([], a) " Test for ZQ call writefile(['1', '2'], 'Xfile_Test_normal22_zet') - call system(v:progpath . args . ' -c "%d" -c ":norm! ZQ" Xfile_Test_normal22_zet') + call system(GetVimCommand() . args . ' -c "%d" -c ":norm! ZQ" Xfile_Test_normal22_zet') let a = readfile('Xfile_Test_normal22_zet') call assert_equal(['1', '2'], a) diff --git a/src/nvim/testdir/test_profile.vim b/src/nvim/testdir/test_profile.vim index 4b0097617e..fdb6f13e2b 100644 --- a/src/nvim/testdir/test_profile.vim +++ b/src/nvim/testdir/test_profile.vim @@ -1,8 +1,9 @@ " Test Vim profiler -if !has('profile') - finish -endif +source check.vim +CheckFeature profile + +source shared.vim source screendump.vim func Test_profile_func() @@ -37,7 +38,7 @@ func Test_profile_func() [CODE] call writefile(lines, 'Xprofile_func.vim') - call system(v:progpath + call system(GetVimCommand() \ . ' -es --clean' \ . ' -c "so Xprofile_func.vim"' \ . ' -c "qall!"') @@ -124,8 +125,8 @@ func Test_profile_func_with_ifelse() [CODE] call writefile(lines, 'Xprofile_func.vim') - call system(v:progpath - \ . ' -es -u NONE -U NONE -i NONE --noplugin' + call system(GetVimCommand() + \ . ' -es -i NONE --noplugin' \ . ' -c "profile start Xprofile_func.log"' \ . ' -c "profile func Foo*"' \ . ' -c "so Xprofile_func.vim"' @@ -237,8 +238,8 @@ func Test_profile_func_with_trycatch() [CODE] call writefile(lines, 'Xprofile_func.vim') - call system(v:progpath - \ . ' -es -u NONE -U NONE -i NONE --noplugin' + call system(GetVimCommand() + \ . ' -es -i NONE --noplugin' \ . ' -c "profile start Xprofile_func.log"' \ . ' -c "profile func Foo*"' \ . ' -c "so Xprofile_func.vim"' @@ -324,8 +325,8 @@ func Test_profile_file() [CODE] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath - \ . ' -es --clean' + call system(GetVimCommandClean() + \ . ' -es' \ . ' -c "profile start Xprofile_file.log"' \ . ' -c "profile file Xprofile_file.vim"' \ . ' -c "so Xprofile_file.vim"' @@ -369,8 +370,8 @@ func Test_profile_file_with_cont() \ ] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath - \ . ' -es -u NONE -U NONE -i NONE --noplugin' + call system(GetVimCommandClean() + \ . ' -es' \ . ' -c "profile start Xprofile_file.log"' \ . ' -c "profile file Xprofile_file.vim"' \ . ' -c "so Xprofile_file.vim"' @@ -427,7 +428,7 @@ func Test_profile_truncate_mbyte() \ ] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath + call system(GetVimCommandClean() \ . ' -es --cmd "set enc=utf-8"' \ . ' -c "profile start Xprofile_file.log"' \ . ' -c "profile file Xprofile_file.vim"' @@ -474,7 +475,7 @@ func Test_profdel_func() call Foo3() [CODE] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath . ' -es --clean -c "so Xprofile_file.vim" -c q') + call system(GetVimCommandClean() . ' -es -c "so Xprofile_file.vim" -c q') call assert_equal(0, v:shell_error) let lines = readfile('Xprofile_file.log') @@ -509,7 +510,7 @@ func Test_profdel_star() call Foo() [CODE] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath . ' -es --clean -c "so Xprofile_file.vim" -c q') + call system(GetVimCommandClean() . ' -es -c "so Xprofile_file.vim" -c q') call assert_equal(0, v:shell_error) let lines = readfile('Xprofile_file.log') diff --git a/src/nvim/testdir/test_quickfix.vim b/src/nvim/testdir/test_quickfix.vim index c4d70fb1de..6852f53ea8 100644 --- a/src/nvim/testdir/test_quickfix.vim +++ b/src/nvim/testdir/test_quickfix.vim @@ -2739,7 +2739,7 @@ func Test_cwindow_jump() call assert_true(winnr('$') == 2) call assert_true(winnr() == 1) - " Jumping to a file from the location list window should find a usuable + " Jumping to a file from the location list window should find a usable " window by wrapping around the window list. enew | only call setloclist(0, [], 'f') diff --git a/src/nvim/testdir/test_registers.vim b/src/nvim/testdir/test_registers.vim index 2e92d9aa2f..23e39eba35 100644 --- a/src/nvim/testdir/test_registers.vim +++ b/src/nvim/testdir/test_registers.vim @@ -219,7 +219,7 @@ func Test_set_register() call setreg('=', 'b', 'a') call assert_equal('regwrite', getreg('=')) - " Test for settting a list of lines to special registers + " Test for setting a list of lines to special registers call setreg('/', []) call assert_equal('', @/) call setreg('=', []) diff --git a/src/nvim/testdir/test_stat.vim b/src/nvim/testdir/test_stat.vim index b44f3e9b94..d3059664e9 100644 --- a/src/nvim/testdir/test_stat.vim +++ b/src/nvim/testdir/test_stat.vim @@ -7,7 +7,7 @@ func CheckFileTime(doSleep) let times = [] let result = 0 - " Use three files istead of localtim(), with a network filesystem the file + " Use three files instead of localtim(), with a network filesystem the file " times may differ at bit let fl = ['Hello World!'] for fname in fnames diff --git a/src/nvim/testdir/test_suspend.vim b/src/nvim/testdir/test_suspend.vim index 4b3bd5eadf..bf88bd4453 100644 --- a/src/nvim/testdir/test_suspend.vim +++ b/src/nvim/testdir/test_suspend.vim @@ -26,8 +26,8 @@ func Test_suspend() " Wait for shell prompt. call WaitForAssert({-> assert_match('[$#] $', term_getline(buf, '.'))}) - call term_sendkeys(buf, v:progpath - \ . " --clean -X" + call term_sendkeys(buf, GetVimCommandClean() + \ . " -X" \ . " -c 'set nu'" \ . " -c 'call setline(1, \"foo\")'" \ . " Xfoo\<CR>") diff --git a/src/nvim/testdir/test_system.vim b/src/nvim/testdir/test_system.vim index 5b8079d7b6..18692f42c9 100644 --- a/src/nvim/testdir/test_system.vim +++ b/src/nvim/testdir/test_system.vim @@ -50,11 +50,11 @@ endfunc func Test_system_exmode() if has('unix') " echo $? only works on Unix - let cmd = ' -es --headless -u NONE -c "source Xscript" +q; echo "result=$?"' + let cmd = ' -es -c "source Xscript" +q; echo "result=$?"' " Need to put this in a script, "catch" isn't found after an unknown " function. call writefile(['try', 'call doesnotexist()', 'catch', 'endtry'], 'Xscript') - let a = system(v:progpath . cmd) + let a = system(GetVimCommand() . cmd) call assert_match('result=0', a) call assert_equal(0, v:shell_error) endif @@ -62,33 +62,33 @@ func Test_system_exmode() " Error before try does set error flag. call writefile(['call nosuchfunction()', 'try', 'call doesnotexist()', 'catch', 'endtry'], 'Xscript') if has('unix') " echo $? only works on Unix - let a = system(v:progpath . cmd) + let a = system(GetVimCommand() . cmd) call assert_notequal('0', a[0]) endif - let cmd = ' -es --headless -u NONE -c "source Xscript" +q' - let a = system(v:progpath . cmd) + let cmd = ' -es -c "source Xscript" +q' + let a = system(GetVimCommand() . cmd) call assert_notequal(0, v:shell_error) call delete('Xscript') if has('unix') " echo $? only works on Unix - let cmd = ' -es --headless -u NONE -c "call doesnotexist()" +q; echo $?' - let a = system(v:progpath. cmd) + let cmd = ' -es -c "call doesnotexist()" +q; echo $?' + let a = system(GetVimCommand() . cmd) call assert_notequal(0, a[0]) endif - let cmd = ' -es --headless -u NONE -c "call doesnotexist()" +q' - let a = system(v:progpath. cmd) + let cmd = ' -es -c "call doesnotexist()" +q' + let a = system(GetVimCommand(). cmd) call assert_notequal(0, v:shell_error) if has('unix') " echo $? only works on Unix - let cmd = ' -es --headless -u NONE -c "call doesnotexist()|let a=1" +q; echo $?' - let a = system(v:progpath. cmd) + let cmd = ' -es -c "call doesnotexist()|let a=1" +q; echo $?' + let a = system(GetVimCommand() . cmd) call assert_notequal(0, a[0]) endif - let cmd = ' -es --headless -u NONE -c "call doesnotexist()|let a=1" +q' - let a = system(v:progpath. cmd) + let cmd = ' -es -c "call doesnotexist()|let a=1" +q' + let a = system(GetVimCommand() . cmd) call assert_notequal(0, v:shell_error) endfunc diff --git a/src/nvim/testdir/test_vimscript.vim b/src/nvim/testdir/test_vimscript.vim index 75a965f16d..f93eb6e274 100644 --- a/src/nvim/testdir/test_vimscript.vim +++ b/src/nvim/testdir/test_vimscript.vim @@ -1,6 +1,9 @@ " Test various aspects of the Vim script language. " Most of this was formerly in test49. +source check.vim +source shared.vim + "------------------------------------------------------------------------------- " Test environment {{{1 "------------------------------------------------------------------------------- @@ -1744,7 +1747,7 @@ func Test_function_defined_line() [CODE] call writefile(lines, 'Xtest.vim') - let res = system(v:progpath .. ' --clean -es -X -S Xtest.vim') + let res = system(GetVimCommandClean() .. ' -es -X -S Xtest.vim') call assert_equal(0, v:shell_error) let m = matchstr(res, 'function F1()[^[:print:]]*[[:print:]]*') diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 6b889cf97c..b262fc6c54 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -5,7 +5,7 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/vim.h" #include "nvim/ascii.h" -#include "nvim/aucmd.h" +#include "nvim/autocmd.h" #include "nvim/charset.h" #include "nvim/ex_docmd.h" #include "nvim/macros.h" @@ -378,7 +378,7 @@ static bool handle_focus_event(TermInput *input) bool focus_gained = *rbuffer_get(input->read_stream.buffer, 2) == 'I'; // Advance past the sequence rbuffer_consumed(input->read_stream.buffer, 3); - aucmd_schedule_focusgained(focus_gained); + autocmd_schedule_focusgained(focus_gained); return true; } return false; diff --git a/src/nvim/ui.c b/src/nvim/ui.c index 1aadaf5c9d..31b9614c34 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -8,7 +8,7 @@ #include <string.h> #include "nvim/ascii.h" -#include "nvim/aucmd.h" +#include "nvim/autocmd.h" #include "nvim/charset.h" #include "nvim/cursor.h" #include "nvim/cursor_shape.h" diff --git a/src/nvim/viml/parser/expressions.c b/src/nvim/viml/parser/expressions.c index 8a14710351..9d1318724e 100644 --- a/src/nvim/viml/parser/expressions.c +++ b/src/nvim/viml/parser/expressions.c @@ -1592,7 +1592,7 @@ typedef struct { /// string is a regex. /// @param[in] is_invalid Whether currently processed token is not valid. static void parse_quoted_string(ParserState *const pstate, ExprASTNode *const node, - const LexExprToken token, const ExprASTStack ast_stack, + const LexExprToken token, const ExprASTStack *ast_stack, const bool is_invalid) FUNC_ATTR_NONNULL_ALL { @@ -2907,7 +2907,7 @@ viml_pexpr_parse_no_paren_closing_error: {} ? kExprNodeDoubleQuotedString : kExprNodeSingleQuotedString)); *top_node_p = cur_node; - parse_quoted_string(pstate, cur_node, cur_token, ast_stack, is_invalid); + parse_quoted_string(pstate, cur_node, cur_token, &ast_stack, is_invalid); want_node = kENodeOperator; break; } diff --git a/src/nvim/window.c b/src/nvim/window.c index 43667377c5..83048d911f 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -958,6 +958,11 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir) int wmh1; bool did_set_fraction = false; + // aucmd_win should always remain floating + if (new_wp != NULL && new_wp == aucmd_win) { + return FAIL; + } + if (flags & WSP_TOP) { oldwin = firstwin; } else if (flags & WSP_BOT || curwin->w_floating) { @@ -1833,6 +1838,9 @@ static void win_totop(int size, int flags) beep_flush(); return; } + if (curwin == aucmd_win) { + return; + } if (curwin->w_floating) { ui_comp_remove_grid(&curwin->w_grid_alloc); diff --git a/test/functional/api/autocmd_spec.lua b/test/functional/api/autocmd_spec.lua new file mode 100644 index 0000000000..2b48ffa6f0 --- /dev/null +++ b/test/functional/api/autocmd_spec.lua @@ -0,0 +1,798 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local neq = helpers.neq +local exec_lua = helpers.exec_lua +local matches = helpers.matches +local meths = helpers.meths +local source = helpers.source + +before_each(clear) + +describe('autocmd api', function() + describe('nvim_create_autocmd', function() + it('does not allow "command" and "callback" in the same autocmd', function() + local ok, _ = pcall(meths.create_autocmd, { + event = "BufReadPost", + pattern = "*.py,*.pyi", + command = "echo 'Should Have Errored", + callback = "not allowed", + }) + + eq(false, ok) + end) + + it('doesnt leak when you use ++once', function() + eq(1, exec_lua([[ + local count = 0 + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = function() count = count + 1 end, + once = true + } + + vim.cmd "set filetype=txt" + vim.cmd "set filetype=python" + + return count + ]], {})) + end) + + it('allows passing buffer by key', function() + meths.set_var('called', 0) + + meths.create_autocmd { + event = "Filetype", + command = "let g:called = g:called + 1", + buffer = 0, + } + + meths.command "set filetype=txt" + eq(1, meths.get_var('called')) + + -- switch to a new buffer + meths.command "new" + meths.command "set filetype=python" + + eq(1, meths.get_var('called')) + end) + + it('does not allow passing buffer and patterns', function() + local ok = pcall(meths.create_autocmd, { + event = "Filetype", + command = "let g:called = g:called + 1", + buffer = 0, + pattern = "*.py", + }) + + eq(false, ok) + end) + + it('does not allow passing invalid buffers', function() + local ok, msg = pcall(meths.create_autocmd, { + event = "Filetype", + command = "let g:called = g:called + 1", + buffer = -1, + }) + + eq(false, ok) + matches('Invalid buffer id', msg) + end) + + it('errors on non-functions for cb', function() + eq(false, pcall(exec_lua, [[ + vim.api.nvim_create_autocmd { + event = "BufReadPost", + pattern = "*.py,*.pyi", + callback = 5, + } + ]])) + end) + + it('allow passing pattern and <buffer> in same pattern', function() + local ok = pcall(meths.create_autocmd, { + event = "BufReadPost", + pattern = "*.py,<buffer>", + command = "echo 'Should Not Error'" + }) + + eq(true, ok) + end) + + it('should handle multiple values as comma separated list', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = "*.py,*.pyi", + command = "echo 'Should Not Have Errored'" + } + + -- We should have one autocmd for *.py and one for *.pyi + eq(2, #meths.get_autocmds { event = "BufReadPost" }) + end) + + it('should handle multiple values as array', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = { "*.py", "*.pyi", }, + command = "echo 'Should Not Have Errored'" + } + + -- We should have one autocmd for *.py and one for *.pyi + eq(2, #meths.get_autocmds { event = "BufReadPost" }) + end) + + describe('desc', function() + it('can add description to one autocmd', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = "*.py", + command = "echo 'Should Not Have Errored'", + desc = "Can show description", + } + + eq("Can show description", meths.get_autocmds { event = "BufReadPost" }[1].desc) + end) + + it('can add description to multiple autocmd', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = {"*.py", "*.pyi"}, + command = "echo 'Should Not Have Errored'", + desc = "Can show description", + } + + local aus = meths.get_autocmds { event = "BufReadPost" } + eq(2, #aus) + eq("Can show description", aus[1].desc) + eq("Can show description", aus[2].desc) + end) + end) + + pending('script and verbose settings', function() + it('marks API client', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = "*.py", + command = "echo 'Should Not Have Errored'", + desc = "Can show description", + } + + local aus = meths.get_autocmds { event = "BufReadPost" } + eq(1, #aus, aus) + end) + end) + end) + + describe('nvim_get_autocmds', function() + describe('events', function() + it('should return one autocmd when there is only one for an event', function() + command [[au! InsertEnter]] + command [[au InsertEnter * :echo "1"]] + + local aus = meths.get_autocmds { event = "InsertEnter" } + eq(1, #aus) + end) + + it('should return two autocmds when there are two for an event', function() + command [[au! InsertEnter]] + command [[au InsertEnter * :echo "1"]] + command [[au InsertEnter * :echo "2"]] + + local aus = meths.get_autocmds { event = "InsertEnter" } + eq(2, #aus) + end) + + it('should return the same thing if you use string or list', function() + command [[au! InsertEnter]] + command [[au InsertEnter * :echo "1"]] + command [[au InsertEnter * :echo "2"]] + + local string_aus = meths.get_autocmds { event = "InsertEnter" } + local array_aus = meths.get_autocmds { event = { "InsertEnter" } } + eq(string_aus, array_aus) + end) + + it('should return two autocmds when there are two for an event', function() + command [[au! InsertEnter]] + command [[au! InsertLeave]] + command [[au InsertEnter * :echo "1"]] + command [[au InsertEnter * :echo "2"]] + + local aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } } + eq(2, #aus) + end) + + it('should return different IDs for different autocmds', function() + command [[au! InsertEnter]] + command [[au! InsertLeave]] + command [[au InsertEnter * :echo "1"]] + source [[ + call nvim_create_autocmd(#{ + \ event: "InsertLeave", + \ command: ":echo 2", + \ }) + ]] + + local aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } } + local first = aus[1] + eq(first.id, nil) + + -- TODO: Maybe don't have this number, just assert it's not nil + local second = aus[2] + neq(second.id, nil) + + meths.del_autocmd(second.id) + local new_aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } } + eq(1, #new_aus) + eq(first, new_aus[1]) + end) + end) + + describe('groups', function() + before_each(function() + command [[au! InsertEnter]] + + command [[au InsertEnter * :echo "No Group"]] + + command [[augroup GroupOne]] + command [[ au InsertEnter * :echo "GroupOne:1"]] + command [[augroup END]] + + command [[augroup GroupTwo]] + command [[ au InsertEnter * :echo "GroupTwo:2"]] + command [[ au InsertEnter * :echo "GroupTwo:3"]] + command [[augroup END]] + end) + + it('should return all groups if no group is specified', function() + local aus = meths.get_autocmds { event = "InsertEnter" } + if #aus ~= 4 then + eq({}, aus) + end + + eq(4, #aus) + end) + + it('should return only the group specified', function() + local aus = meths.get_autocmds { + event = "InsertEnter", + group = "GroupOne", + } + + eq(1, #aus) + eq([[:echo "GroupOne:1"]], aus[1].command) + end) + + it('should return only the group specified, multiple values', function() + local aus = meths.get_autocmds { + event = "InsertEnter", + group = "GroupTwo", + } + + eq(2, #aus) + eq([[:echo "GroupTwo:2"]], aus[1].command) + eq([[:echo "GroupTwo:3"]], aus[2].command) + end) + end) + + describe('groups: 2', function() + it('raises error for undefined augroup', function() + local success, code = unpack(meths.exec_lua([[ + return {pcall(function() + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + group = "NotDefined", + command = "echo 'hello'", + } + end)} + ]], {})) + + eq(false, success) + matches('invalid augroup: NotDefined', code) + end) + end) + + describe('patterns', function() + before_each(function() + command [[au! InsertEnter]] + + command [[au InsertEnter * :echo "No Group"]] + command [[au InsertEnter *.one :echo "GroupOne:1"]] + command [[au InsertEnter *.two :echo "GroupTwo:2"]] + command [[au InsertEnter *.two :echo "GroupTwo:3"]] + command [[au InsertEnter <buffer> :echo "Buffer"]] + end) + + it('should should return for literal match', function() + local aus = meths.get_autocmds { + event = "InsertEnter", + pattern = "*" + } + + eq(1, #aus) + eq([[:echo "No Group"]], aus[1].command) + end) + + it('should return for multiple matches', function() + -- vim.api.nvim_get_autocmds + local aus = meths.get_autocmds { + event = "InsertEnter", + pattern = { "*.one", "*.two" }, + } + + eq(3, #aus) + eq([[:echo "GroupOne:1"]], aus[1].command) + eq([[:echo "GroupTwo:2"]], aus[2].command) + eq([[:echo "GroupTwo:3"]], aus[3].command) + end) + + it('should work for buffer autocmds', function() + local normalized_aus = meths.get_autocmds { + event = "InsertEnter", + pattern = "<buffer=1>", + } + + local raw_aus = meths.get_autocmds { + event = "InsertEnter", + pattern = "<buffer>", + } + + local zero_aus = meths.get_autocmds { + event = "InsertEnter", + pattern = "<buffer=0>", + } + + eq(normalized_aus, raw_aus) + eq(normalized_aus, zero_aus) + eq([[:echo "Buffer"]], normalized_aus[1].command) + end) + end) + end) + + describe('nvim_do_autocmd', function() + it("can trigger builtin autocmds", function() + meths.set_var("autocmd_executed", false) + + meths.create_autocmd { + event = "BufReadPost", + pattern = "*", + command = "let g:autocmd_executed = v:true", + } + + eq(false, meths.get_var("autocmd_executed")) + meths.do_autocmd { event = "BufReadPost" } + eq(true, meths.get_var("autocmd_executed")) + end) + + it("can pass the buffer", function() + meths.set_var("buffer_executed", -1) + eq(-1, meths.get_var("buffer_executed")) + + meths.create_autocmd { + event = "BufLeave", + pattern = "*", + command = 'let g:buffer_executed = +expand("<abuf>")', + } + + -- Doesn't execute for other non-matching events + meths.do_autocmd { event = "CursorHold", buffer = 1 } + eq(-1, meths.get_var("buffer_executed")) + + meths.do_autocmd { event = "BufLeave", buffer = 1 } + eq(1, meths.get_var("buffer_executed")) + end) + + it("can pass the filename, pattern match", function() + meths.set_var("filename_executed", 'none') + eq('none', meths.get_var("filename_executed")) + + meths.create_autocmd { + event = "BufEnter", + pattern = "*.py", + command = 'let g:filename_executed = expand("<afile>")', + } + + -- Doesn't execute for other non-matching events + meths.do_autocmd { event = "CursorHold", buffer = 1 } + eq('none', meths.get_var("filename_executed")) + + meths.command('edit __init__.py') + eq('__init__.py', meths.get_var("filename_executed")) + end) + + it('cannot pass buf and fname', function() + local ok = pcall(meths.do_autocmd, { pattern = "literally_cannot_error.rs", buffer = 1 }) + eq(false, ok) + end) + + it("can pass the filename, exact match", function() + meths.set_var("filename_executed", 'none') + eq('none', meths.get_var("filename_executed")) + + meths.command('edit other_file.txt') + meths.command('edit __init__.py') + eq('none', meths.get_var("filename_executed")) + + meths.create_autocmd { + event = "CursorHoldI", + pattern = "__init__.py", + command = 'let g:filename_executed = expand("<afile>")', + } + + -- Doesn't execute for other non-matching events + meths.do_autocmd { event = "CursorHoldI", buffer = 1 } + eq('none', meths.get_var("filename_executed")) + + meths.do_autocmd { event = "CursorHoldI", buffer = tonumber(meths.get_current_buf()) } + eq('__init__.py', meths.get_var("filename_executed")) + + -- Reset filename + meths.set_var("filename_executed", 'none') + + meths.do_autocmd { event = "CursorHoldI", pattern = '__init__.py' } + eq('__init__.py', meths.get_var("filename_executed")) + end) + + it("works with user autocmds", function() + meths.set_var("matched", 'none') + + meths.create_autocmd { + event = "User", + pattern = "TestCommand", + command = 'let g:matched = "matched"' + } + + meths.do_autocmd { event = "User", pattern = "OtherCommand" } + eq('none', meths.get_var('matched')) + meths.do_autocmd { event = "User", pattern = "TestCommand" } + eq('matched', meths.get_var('matched')) + end) + end) + + describe('nvim_create_augroup', function() + before_each(function() + clear() + + meths.set_var('executed', 0) + end) + + local make_counting_autocmd = function(opts) + opts = opts or {} + + local resulting = { + event = "FileType", + pattern = "*", + command = "let g:executed = g:executed + 1", + } + + resulting.group = opts.group + resulting.once = opts.once + + meths.create_autocmd(resulting) + end + + local set_ft = function(ft) + ft = ft or "txt" + source(string.format("set filetype=%s", ft)) + end + + local get_executed_count = function() + return meths.get_var('executed') + end + + it('can be added in a group', function() + local augroup = "TestGroup" + meths.create_augroup({ name = augroup, clear = true }) + make_counting_autocmd { group = augroup } + + set_ft("txt") + set_ft("python") + + eq(get_executed_count(), 2) + end) + + it('works getting called multiple times', function() + make_counting_autocmd() + set_ft() + set_ft() + set_ft() + + eq(get_executed_count(), 3) + end) + + it('handles ++once', function() + make_counting_autocmd {once = true} + set_ft('txt') + set_ft('help') + set_ft('txt') + set_ft('help') + + eq(get_executed_count(), 1) + end) + + it('errors on unexpected keys', function() + local success, code = pcall(meths.create_autocmd, { + event = "FileType", + pattern = "*", + not_a_valid_key = "NotDefined", + }) + + eq(false, success) + matches('not_a_valid_key', code) + end) + + it('can execute simple callback', function() + exec_lua([[ + vim.g.executed = false + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = function() vim.g.executed = true end, + } + ]], {}) + + + eq(true, exec_lua([[ + vim.cmd "set filetype=txt" + return vim.g.executed + ]], {})) + end) + + it('calls multiple lua callbacks for the same autocmd execution', function() + eq(4, exec_lua([[ + local count = 0 + local counter = function() + count = count + 1 + end + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = counter, + } + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = counter, + } + + vim.cmd "set filetype=txt" + vim.cmd "set filetype=txt" + + return count + ]], {})) + end) + + it('properly releases functions with ++once', function() + exec_lua([[ + WeakTable = setmetatable({}, { __mode = "k" }) + + OnceCount = 0 + + MyVal = {} + WeakTable[MyVal] = true + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = function() + OnceCount = OnceCount + 1 + MyVal = {} + end, + once = true + } + ]]) + + command [[set filetype=txt]] + eq(1, exec_lua([[return OnceCount]], {})) + + exec_lua([[collectgarbage()]], {}) + + command [[set filetype=txt]] + eq(1, exec_lua([[return OnceCount]], {})) + + eq(0, exec_lua([[ + local count = 0 + for _ in pairs(WeakTable) do + count = count + 1 + end + + return count + ]]), "Should have no keys remaining") + end) + + it('groups can be cleared', function() + local augroup = "TestGroup" + meths.create_augroup({ name = augroup, clear = true }) + meths.create_autocmd({ + group = augroup, + event = "FileType", + command = "let g:executed = g:executed + 1" + }) + + set_ft("txt") + set_ft("txt") + eq(2, get_executed_count(), "should only count twice") + + meths.create_augroup({ name = augroup, clear = true }) + eq({}, meths.get_autocmds { group = augroup }) + + set_ft("txt") + set_ft("txt") + eq(2, get_executed_count(), "No additional counts") + end) + + it('groups work with once', function() + local augroup = "TestGroup" + + meths.create_augroup({ name = augroup, clear = true }) + make_counting_autocmd { group = augroup, once = true } + + set_ft("txt") + set_ft("python") + + eq(get_executed_count(), 1) + end) + + it('autocmds can be registered multiple times.', function() + local augroup = "TestGroup" + + meths.create_augroup({ name = augroup, clear = true }) + make_counting_autocmd { group = augroup, once = false } + make_counting_autocmd { group = augroup, once = false } + make_counting_autocmd { group = augroup, once = false } + + set_ft("txt") + set_ft("python") + + eq(get_executed_count(), 3 * 2) + end) + + it('can be deleted', function() + local augroup = "WillBeDeleted" + + meths.create_augroup({ name = augroup, clear = true }) + meths.create_autocmd { + event = {"Filetype"}, + pattern = "*", + command = "echo 'does not matter'", + } + + -- Clears the augroup from before, which erases the autocmd + meths.create_augroup({ name = augroup, clear = true }) + + local result = #meths.get_autocmds { group = augroup } + + eq(0, result) + end) + + it('can be used for buffer local autocmds', function() + local augroup = "WillBeDeleted" + + meths.set_var("value_set", false) + + meths.create_augroup({ name = augroup, clear = true }) + meths.create_autocmd { + event = "Filetype", + pattern = "<buffer>", + command = "let g:value_set = v:true", + } + + command "new" + command "set filetype=python" + + eq(false, meths.get_var("value_set")) + end) + + it('can accept vimscript functions', function() + source [[ + let g:vimscript_executed = 0 + + function! MyVimscriptFunction() abort + let g:vimscript_executed = g:vimscript_executed + 1 + endfunction + + call nvim_create_autocmd(#{ + \ event: "Filetype", + \ pattern: ["python", "javascript"], + \ callback: "MyVimscriptFunction", + \ }) + + set filetype=txt + set filetype=python + set filetype=txt + set filetype=javascript + set filetype=txt + ]] + + eq(2, meths.get_var("vimscript_executed")) + end) + end) + + describe('augroup!', function() + it('legacy: should clear and not return any autocmds for delete groups', function() + command('augroup TEMP_A') + command(' autocmd! BufReadPost *.py :echo "Hello"') + command('augroup END') + + command('augroup! TEMP_A') + + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_A' })) + + -- For some reason, augroup! doesn't clear the autocmds themselves, which is just wild + -- but we managed to keep this behavior. + eq(1, #meths.get_autocmds { event = 'BufReadPost' }) + end) + + it('legacy: remove augroups that have no autocmds', function() + command('augroup TEMP_AB') + command('augroup END') + + command('augroup! TEMP_AB') + + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_AB' })) + eq(0, #meths.get_autocmds { event = 'BufReadPost' }) + end) + + it('legacy: multiple remove and add augroup', function() + command('augroup TEMP_ABC') + command(' au!') + command(' autocmd BufReadPost *.py echo "Hello"') + command('augroup END') + + command('augroup! TEMP_ABC') + + -- Should still have one autocmd :'( + local aus = meths.get_autocmds { event = 'BufReadPost' } + eq(1, #aus, aus) + + command('augroup TEMP_ABC') + command(' au!') + command(' autocmd BufReadPost *.py echo "Hello"') + command('augroup END') + + -- Should now have two autocmds :'( + aus = meths.get_autocmds { event = 'BufReadPost' } + eq(2, #aus, aus) + + command('augroup! TEMP_ABC') + + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABC' })) + eq(2, #meths.get_autocmds { event = 'BufReadPost' }) + end) + + it('api: should clear and not return any autocmds for delete groups by id', function() + command('augroup TEMP_ABCD') + command('autocmd! BufReadPost *.py :echo "Hello"') + command('augroup END') + + local augroup_id = meths.create_augroup { name = "TEMP_ABCD", clear = false } + meths.del_augroup_by_id(augroup_id) + + -- For good reason, we kill all the autocmds from del_augroup, + -- so now this works as expected + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABCD' })) + eq(0, #meths.get_autocmds { event = 'BufReadPost' }) + end) + + it('api: should clear and not return any autocmds for delete groups by name', function() + command('augroup TEMP_ABCDE') + command('autocmd! BufReadPost *.py :echo "Hello"') + command('augroup END') + + meths.del_augroup_by_name("TEMP_ABCDE") + + -- For good reason, we kill all the autocmds from del_augroup, + -- so now this works as expected + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABCDE' })) + eq(0, #meths.get_autocmds { event = 'BufReadPost' }) + end) + end) +end) diff --git a/test/functional/api/buffer_spec.lua b/test/functional/api/buffer_spec.lua index 688f3abba5..dd3af8c28f 100644 --- a/test/functional/api/buffer_spec.lua +++ b/test/functional/api/buffer_spec.lua @@ -423,6 +423,13 @@ describe('api/buf', function() -- will join multiple lines if needed set_text(0, 6, 3, 4, {'bar'}) eq({'hello bar'}, get_lines(0, 1, true)) + + -- can use negative line numbers + set_text(-2, 0, -2, 5, {'goodbye'}) + eq({'goodbye bar', ''}, get_lines(0, -1, true)) + + set_text(-1, 0, -1, 0, {'text'}) + eq({'goodbye bar', 'text'}, get_lines(0, 2, true)) end) it('works with undo', function() @@ -537,6 +544,34 @@ describe('api/buf', function() end) end) + describe('nvim_buf_get_text', function() + local get_text = curbufmeths.get_text + + it('works', function() + insert([[ + hello foo! + text]]) + + eq({'hello'}, get_text(0, 0, 0, 5, {})) + eq({'hello foo!'}, get_text(0, 0, 0, 42, {})) + eq({'foo!'}, get_text(0, 6, 0, 10, {})) + eq({'foo!', 'tex'}, get_text(0, 6, 1, 3, {})) + eq({'foo!', 'tex'}, get_text(-2, 6, -1, 3, {})) + eq({''}, get_text(0, 18, 0, 20, {})) + eq({'ext'}, get_text(-1, 1, -1, 4, {})) + end) + + it('errors on out-of-range', function() + eq(false, pcall(get_text, 2, 0, 3, 0, {})) + eq(false, pcall(get_text, 0, 0, 4, 0, {})) + end) + + it('errors when start is greater than end', function() + eq(false, pcall(get_text, 1, 0, 0, 0, {})) + eq(false, pcall(get_text, 0, 1, 0, 0, {})) + end) + end) + describe('nvim_buf_get_offset', function() local get_offset = curbufmeths.get_offset it('works', function() diff --git a/test/functional/api/buffer_updates_spec.lua b/test/functional/api/buffer_updates_spec.lua index c9c9be5406..e9ad756947 100644 --- a/test/functional/api/buffer_updates_spec.lua +++ b/test/functional/api/buffer_updates_spec.lua @@ -7,6 +7,7 @@ local nvim_prog = helpers.nvim_prog local pcall_err = helpers.pcall_err local sleep = helpers.sleep local write_file = helpers.write_file +local iswin = helpers.iswin local origlines = {"original line 1", "original line 2", @@ -823,7 +824,8 @@ describe('API: buffer events:', function() end msg = next_msg() end - assert(false, 'did not match/receive expected nvim_buf_lines_event lines') + -- FIXME: Windows + assert(iswin(), 'did not match/receive expected nvim_buf_lines_event lines') end it('when :terminal lines change', function() diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua index de22c9078c..b80004f67c 100644 --- a/test/functional/api/command_spec.lua +++ b/test/functional/api/command_spec.lua @@ -107,7 +107,8 @@ describe('nvim_add_user_command', function() ]] eq({ - args = "hello", + args = [[hello my\ friend how\ are\ you?]], + fargs = {[[hello]], [[my\ friend]], [[how\ are\ you?]]}, bang = false, line1 = 1, line2 = 1, @@ -115,13 +116,14 @@ describe('nvim_add_user_command', function() range = 0, count = 2, reg = "", - }, exec_lua [[ - vim.api.nvim_command('CommandWithLuaCallback hello') + }, exec_lua [=[ + vim.api.nvim_command([[CommandWithLuaCallback hello my\ friend how\ are\ you?]]) return result - ]]) + ]=]) eq({ - args = "", + args = 'h\tey', + fargs = {[[h]], [[ey]]}, bang = true, line1 = 10, line2 = 10, @@ -129,13 +131,14 @@ describe('nvim_add_user_command', function() range = 1, count = 10, reg = "", - }, exec_lua [[ - vim.api.nvim_command('botright 10CommandWithLuaCallback!') + }, exec_lua [=[ + vim.api.nvim_command('botright 10CommandWithLuaCallback! h\tey') return result - ]]) + ]=]) eq({ - args = "", + args = "h", + fargs = {"h"}, bang = false, line1 = 1, line2 = 42, @@ -144,9 +147,52 @@ describe('nvim_add_user_command', function() count = 42, reg = "", }, exec_lua [[ - vim.api.nvim_command('CommandWithLuaCallback 42') + vim.api.nvim_command('CommandWithLuaCallback 42 h') return result ]]) + + eq({ + args = "", + fargs = {""}, -- fargs works without args + bang = false, + line1 = 1, + line2 = 1, + mods = "", + range = 0, + count = 2, + reg = "", + }, exec_lua [[ + vim.api.nvim_command('CommandWithLuaCallback') + return result + ]]) + + -- f-args doesn't split when command nargs is 1 or "?" + exec_lua [[ + result = {} + vim.api.nvim_add_user_command('CommandWithOneArg', function(opts) + result = opts + end, { + nargs = "?", + bang = true, + count = 2, + }) + ]] + + eq({ + args = "hello I'm one argmuent", + fargs = {"hello I'm one argmuent"}, -- Doesn't split args + bang = false, + line1 = 1, + line2 = 1, + mods = "", + range = 0, + count = 2, + reg = "", + }, exec_lua [[ + vim.api.nvim_command('CommandWithOneArg hello I\'m one argmuent') + return result + ]]) + end) it('can define buffer-local commands', function() diff --git a/test/functional/api/highlight_spec.lua b/test/functional/api/highlight_spec.lua index 03b407f4e0..443689754c 100644 --- a/test/functional/api/highlight_spec.lua +++ b/test/functional/api/highlight_spec.lua @@ -304,10 +304,6 @@ describe("API: set highlight", function() eq('Test_hl3 xxx ctermbg=9', exec_capture('highlight Test_hl3')) - meths.set_hl(0, 'Test_hl3', {}) - eq('Test_hl3 xxx cleared', - exec_capture('highlight Test_hl3')) - eq("'redd' is not a valid color", pcall_err(meths.set_hl, 0, 'Test_hl3', {fg='redd'})) @@ -320,5 +316,16 @@ describe("API: set highlight", function() eq("'#FF00FF' is not a valid color", pcall_err(meths.set_hl, 0, 'Test_hl3', {ctermfg='#FF00FF'})) + + for _, fg_val in ipairs{ nil, 'NONE', 'nOnE', '', -1 } do + meths.set_hl(0, 'Test_hl3', {fg = fg_val}) + eq('Test_hl3 xxx cleared', + exec_capture('highlight Test_hl3')) + end + + meths.set_hl(0, 'Test_hl3', {fg='#FF00FF', blend=50}) + eq('Test_hl3 xxx guifg=#FF00FF blend=50', + exec_capture('highlight Test_hl3')) + end) end) diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index 4d2ffa316a..c31ab2060a 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -1,4 +1,5 @@ local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') local clear, nvim, curbuf, curbuf_contents, window, curwin, eq, neq, ok, feed, insert, eval, tabpage = helpers.clear, helpers.nvim, helpers.curbuf, helpers.curbuf_contents, helpers.window, helpers.curwin, helpers.eq, @@ -73,8 +74,7 @@ describe('API/win', function() eq('typing\n some dumb text', curbuf_contents()) end) - it('does not leak memory when using invalid window ID with invalid pos', - function() + it('does not leak memory when using invalid window ID with invalid pos', function() eq('Invalid window id: 1', pcall_err(meths.win_set_cursor, 1, {"b\na"})) end) @@ -147,6 +147,46 @@ describe('API/win', function() eq({2, 5}, window('get_cursor', win)) end) + it('updates cursorline and statusline ruler in non-current window', function() + local screen = Screen.new(60, 8) + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Blue}, -- NonText + [2] = {background = Screen.colors.Grey90}, -- CursorLine + [3] = {bold = true, reverse = true}, -- StatusLine + [4] = {reverse = true}, -- VertSplit, StatusLineNC + }) + screen:attach() + command('set ruler') + command('set cursorline') + insert([[ + aaa + bbb + ccc + ddd]]) + local oldwin = curwin() + command('vsplit') + screen:expect([[ + aaa {4:│}aaa | + bbb {4:│}bbb | + ccc {4:│}ccc | + {2:dd^d }{4:│}{2:ddd }| + {1:~ }{4:│}{1:~ }| + {1:~ }{4:│}{1:~ }| + {3:[No Name] [+] 4,3 All }{4:[No Name] [+] 4,3 All}| + | + ]]) + window('set_cursor', oldwin, {1, 0}) + screen:expect([[ + aaa {4:│}{2:aaa }| + bbb {4:│}bbb | + ccc {4:│}ccc | + {2:dd^d }{4:│}ddd | + {1:~ }{4:│}{1:~ }| + {1:~ }{4:│}{1:~ }| + {3:[No Name] [+] 4,3 All }{4:[No Name] [+] 1,1 All}| + | + ]]) + end) end) describe('{get,set}_height', function() diff --git a/test/functional/autocmd/autocmd_oldtest_spec.lua b/test/functional/autocmd/autocmd_oldtest_spec.lua new file mode 100644 index 0000000000..ad3687d7b0 --- /dev/null +++ b/test/functional/autocmd/autocmd_oldtest_spec.lua @@ -0,0 +1,86 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local eq = helpers.eq +local meths = helpers.meths +local funcs = helpers.funcs + +local exec = function(str) + meths.exec(str, false) +end + +describe('oldtests', function() + before_each(clear) + + local exec_lines = function(str) + return funcs.split(funcs.execute(str), "\n") + end + + local add_an_autocmd = function() + exec [[ + augroup vimBarTest + au BufReadCmd * echo 'hello' + augroup END + ]] + + eq(3, #exec_lines('au vimBarTest')) + eq(1, #meths.get_autocmds({ group = 'vimBarTest' })) + end + + it('should recognize a bar before the {event}', function() + -- Good spacing + add_an_autocmd() + exec [[ augroup vimBarTest | au! | augroup END ]] + eq(1, #exec_lines('au vimBarTest')) + eq({}, meths.get_autocmds({ group = 'vimBarTest' })) + + -- Sad spacing + add_an_autocmd() + exec [[ augroup vimBarTest| au!| augroup END ]] + eq(1, #exec_lines('au vimBarTest')) + + + -- test that a bar is recognized after the {event} + add_an_autocmd() + exec [[ augroup vimBarTest| au!BufReadCmd| augroup END ]] + eq(1, #exec_lines('au vimBarTest')) + + add_an_autocmd() + exec [[ au! vimBarTest|echo 'hello' ]] + eq(1, #exec_lines('au vimBarTest')) + end) + + it('should fire on unload buf', function() + funcs.writefile({'Test file Xxx1'}, 'Xxx1') + funcs.writefile({'Test file Xxx2'}, 'Xxx2') + + local content = [[ + func UnloadAllBufs() + let i = 1 + while i <= bufnr('$') + if i != bufnr('%') && bufloaded(i) + exe i . 'bunload' + endif + let i += 1 + endwhile + endfunc + au BufUnload * call UnloadAllBufs() + au VimLeave * call writefile(['Test Finished'], 'Xout') + set nohidden + edit Xxx1 + split Xxx2 + q + ]] + + funcs.writefile(funcs.split(content, "\n"), 'Xtest') + + funcs.delete('Xout') + funcs.system(meths.get_vvar('progpath') .. ' -u NORC -i NONE -N -S Xtest') + eq(1, funcs.filereadable('Xout')) + + funcs.delete('Xxx1') + funcs.delete('Xxx2') + funcs.delete('Xtest') + funcs.delete('Xout') + end) +end) diff --git a/test/functional/autocmd/autocmd_spec.lua b/test/functional/autocmd/autocmd_spec.lua index 93d71a9e45..f37f04b32f 100644 --- a/test/functional/autocmd/autocmd_spec.lua +++ b/test/functional/autocmd/autocmd_spec.lua @@ -2,8 +2,10 @@ local helpers = require('test.functional.helpers')(after_each) local Screen = require('test.functional.ui.screen') local assert_visible = helpers.assert_visible +local assert_alive = helpers.assert_alive local dedent = helpers.dedent local eq = helpers.eq +local neq = helpers.neq local eval = helpers.eval local feed = helpers.feed local clear = helpers.clear @@ -13,6 +15,7 @@ local funcs = helpers.funcs local expect = helpers.expect local command = helpers.command local exc_exec = helpers.exc_exec +local exec_lua = helpers.exec_lua local curbufmeths = helpers.curbufmeths local source = helpers.source @@ -333,6 +336,68 @@ describe('autocmd', function() pcall_err(command, "call nvim_set_current_win(g:winid)")) end) + it("`aucmd_win` cannot be changed into a normal window #13699", function() + local screen = Screen.new(50, 10) + screen:attach() + screen:set_default_attr_ids { + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {reverse = true}, + [3] = {bold = true, reverse = true}, + } + + -- Create specific layout and ensure it's left unchanged. + -- Use nvim_buf_call on a hidden buffer so aucmd_win is used. + exec_lua [[ + vim.cmd "wincmd s | wincmd _" + _G.buf = vim.api.nvim_create_buf(true, true) + vim.api.nvim_buf_call(_G.buf, function() vim.cmd "wincmd J" end) + ]] + screen:expect [[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {3:[No Name] }| + | + {2:[No Name] }| + | + ]] + -- This used to crash after making aucmd_win a normal window via the above. + exec_lua [[ + vim.cmd "tabnew | tabclose # | wincmd s | wincmd _" + vim.api.nvim_buf_call(_G.buf, function() vim.cmd "wincmd K" end) + ]] + assert_alive() + screen:expect_unchanged() + + -- Ensure splitting still works from inside the aucmd_win. + exec_lua [[vim.api.nvim_buf_call(_G.buf, function() vim.cmd "split" end)]] + screen:expect [[ + ^ | + {1:~ }| + {3:[No Name] }| + | + {1:~ }| + {2:[Scratch] }| + | + {1:~ }| + {2:[No Name] }| + | + ]] + + -- After all of our messing around, aucmd_win should still be floating. + -- Use :only to ensure _G.buf is hidden again (so the aucmd_win is used). + eq("editor", exec_lua [[ + vim.cmd "only" + vim.api.nvim_buf_call(_G.buf, function() + _G.config = vim.api.nvim_win_get_config(0) + end) + return _G.config.relative + ]]) + end) + it(':doautocmd does not warn "No matching autocommands" #10689', function() local screen = Screen.new(32, 3) screen:attach() @@ -354,4 +419,106 @@ describe('autocmd', function() :doautocmd SessionLoadPost | ]]} end) + + describe('old_tests', function() + it('vimscript: WinNew ++once', function() + source [[ + " Without ++once WinNew triggers twice + let g:did_split = 0 + augroup Testing + au! + au WinNew * let g:did_split += 1 + augroup END + split + split + call assert_equal(2, g:did_split) + call assert_true(exists('#WinNew')) + close + close + + " With ++once WinNew triggers once + let g:did_split = 0 + augroup Testing + au! + au WinNew * ++once let g:did_split += 1 + augroup END + split + split + call assert_equal(1, g:did_split) + call assert_false(exists('#WinNew')) + close + close + + call assert_fails('au WinNew * ++once ++once echo bad', 'E983:') + ]] + + meths.set_var('did_split', 0) + + source [[ + augroup Testing + au! + au WinNew * let g:did_split += 1 + augroup END + + split + split + ]] + + eq(2, meths.get_var('did_split')) + eq(1, funcs.exists('#WinNew')) + + -- Now with once + meths.set_var('did_split', 0) + + source [[ + augroup Testing + au! + au WinNew * ++once let g:did_split += 1 + augroup END + + split + split + ]] + + eq(1, meths.get_var('did_split')) + eq(0, funcs.exists('#WinNew')) + + -- call assert_fails('au WinNew * ++once ++once echo bad', 'E983:') + local ok, msg = pcall(source, [[ + au WinNew * ++once ++once echo bad + ]]) + + eq(false, ok) + eq(true, not not string.find(msg, 'E983:')) + end) + + it('should have autocmds in filetypedetect group', function() + source [[filetype on]] + neq({}, meths.get_autocmds { group = "filetypedetect" }) + end) + + it('should not access freed mem', function() + source [[ + au BufEnter,BufLeave,WinEnter,WinLeave 0 vs xxx + arg 0 + argadd + all + all + au! + bwipe xxx + ]] + end) + + it('should allow comma-separated patterns', function() + source [[ + augroup TestingPatterns + au! + autocmd BufReadCmd *.shada,*.shada.tmp.[a-z] echo 'hello' + autocmd BufReadCmd *.shada,*.shada.tmp.[a-z] echo 'hello' + augroup END + ]] + + eq(4, #meths.get_autocmds { event = "BufReadCmd", group = "TestingPatterns" }) + end) + end) end) diff --git a/test/functional/autocmd/cursormoved_spec.lua b/test/functional/autocmd/cursormoved_spec.lua index 9641d4b096..85d8628d7e 100644 --- a/test/functional/autocmd/cursormoved_spec.lua +++ b/test/functional/autocmd/cursormoved_spec.lua @@ -35,6 +35,7 @@ describe('CursorMoved', function() it("is not triggered by cursor movement prior to first CursorMoved instantiation", function() source([[ let g:cursormoved = 0 + autocmd! CursorMoved autocmd CursorMoved * let g:cursormoved += 1 ]]) eq(0, eval('g:cursormoved')) diff --git a/test/functional/autocmd/show_spec.lua b/test/functional/autocmd/show_spec.lua new file mode 100644 index 0000000000..59757a7d61 --- /dev/null +++ b/test/functional/autocmd/show_spec.lua @@ -0,0 +1,35 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local dedent = helpers.dedent +local eq = helpers.eq +local funcs = helpers.funcs + +describe(":autocmd", function() + before_each(clear) + + it("should not segfault when you just do autocmd", function() + command ":autocmd" + end) + + it("should filter based on ++once", function() + command "autocmd! BufEnter" + command "autocmd BufEnter * :echo 'Hello'" + command [[augroup TestingOne]] + command [[ autocmd BufEnter * :echo "Line 1"]] + command [[ autocmd BufEnter * :echo "Line 2"]] + command [[augroup END]] + + eq(dedent([[ + + --- Autocommands --- + BufEnter + * :echo 'Hello' + TestingOne BufEnter + * :echo "Line 1" + :echo "Line 2"]]), + funcs.execute('autocmd BufEnter')) + + end) +end) diff --git a/test/functional/autocmd/termxx_spec.lua b/test/functional/autocmd/termxx_spec.lua index 1e8f981437..bc7a6d6c36 100644 --- a/test/functional/autocmd/termxx_spec.lua +++ b/test/functional/autocmd/termxx_spec.lua @@ -32,7 +32,7 @@ describe('autocmd TermClose', function() retry(nil, nil, function() eq(23, eval('g:test_termclose')) end) end) - it('kills job trapping SIGTERM', function() + pending('kills job trapping SIGTERM', function() if iswin() then return end nvim('set_option', 'shell', 'sh') nvim('set_option', 'shellcmdflag', '-c') @@ -52,7 +52,7 @@ describe('autocmd TermClose', function() ok(duration <= 4000) -- Epsilon for slow CI end) - it('kills PTY job trapping SIGHUP and SIGTERM', function() + pending('kills PTY job trapping SIGHUP and SIGTERM', function() if iswin() then return end nvim('set_option', 'shell', 'sh') nvim('set_option', 'shellcmdflag', '-c') diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index 0a69660871..a0df3b7767 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -78,6 +78,7 @@ describe('jobs', function() end) it('append environment with pty #env', function() + if helpers.pending_win32(pending) then return end nvim('command', "let $VAR = 'abc'") nvim('command', "let $TOTO = 'goodbye world'") nvim('command', "let g:job_opts.pty = v:true") diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index 2fa84e8313..3da7f6ffde 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -53,6 +53,7 @@ describe('startup', function() ]]) end) it('in a TTY: has("ttyin")==1 has("ttyout")==1', function() + if helpers.pending_win32(pending) then return end local screen = Screen.new(25, 4) screen:attach() if iswin() then @@ -104,6 +105,7 @@ describe('startup', function() end) end) it('input from pipe (implicit) #7679', function() + if helpers.pending_win32(pending) then return end local screen = Screen.new(25, 4) screen:attach() if iswin() then @@ -259,6 +261,7 @@ describe('startup', function() end) it('ENTER dismisses early message #7967', function() + if helpers.pending_win32(pending) then return end local screen screen = Screen.new(60, 6) screen:attach() @@ -491,6 +494,7 @@ describe('sysinit', function() end) it('fixed hang issue with -D (#12647)', function() + if helpers.pending_win32(pending) then return end local screen screen = Screen.new(60, 6) screen:attach() diff --git a/test/functional/ex_cmds/map_spec.lua b/test/functional/ex_cmds/map_spec.lua index 84d5bc2335..007d68d61a 100644 --- a/test/functional/ex_cmds/map_spec.lua +++ b/test/functional/ex_cmds/map_spec.lua @@ -1,11 +1,15 @@ local helpers = require("test.functional.helpers")(after_each) +local Screen = require('test.functional.ui.screen') local eq = helpers.eq +local exec = helpers.exec local feed = helpers.feed local meths = helpers.meths local clear = helpers.clear local command = helpers.command local expect = helpers.expect +local insert = helpers.insert +local eval = helpers.eval describe(':*map', function() before_each(clear) @@ -25,4 +29,199 @@ describe(':*map', function() feed('i-<M-">-') expect('-foo-') end) + + it('<Plug> keymaps ignore nore', function() + command('let x = 0') + eq(0, meths.eval('x')) + command [[ + nnoremap <Plug>(Increase_x) <cmd>let x+=1<cr> + nmap increase_x_remap <Plug>(Increase_x) + nnoremap increase_x_noremap <Plug>(Increase_x) + ]] + feed('increase_x_remap') + eq(1, meths.eval('x')) + feed('increase_x_noremap') + eq(2, meths.eval('x')) + end) + it("Doesn't auto ignore nore for keys before or after <Plug> keymap", function() + command('let x = 0') + eq(0, meths.eval('x')) + command [[ + nnoremap x <nop> + nnoremap <Plug>(Increase_x) <cmd>let x+=1<cr> + nmap increase_x_remap x<Plug>(Increase_x)x + nnoremap increase_x_noremap x<Plug>(Increase_x)x + ]] + insert("Some text") + eq('Some text', eval("getline('.')")) + + feed('increase_x_remap') + eq(1, meths.eval('x')) + eq('Some text', eval("getline('.')")) + feed('increase_x_noremap') + eq(2, meths.eval('x')) + eq('Some te', eval("getline('.')")) + end) +end) + +describe(':*map cursor and redrawing', function() + local screen + before_each(function() + clear() + screen = Screen.new(20, 5) + screen:attach() + end) + + it('cursor is restored after :map <expr> which calls input()', function() + command('map <expr> x input("> ")') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]]) + feed('x') + screen:expect([[ + | + ~ | + ~ | + ~ | + > ^ | + ]]) + feed('\n') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + > | + ]]) + end) + + it('cursor is restored after :imap <expr> which calls input()', function() + command('imap <expr> x input("> ")') + feed('i') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + -- INSERT -- | + ]]) + feed('x') + screen:expect([[ + | + ~ | + ~ | + ~ | + > ^ | + ]]) + feed('\n') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + > | + ]]) + end) + + it('cursor is restored after :map <expr> which redraws statusline vim-patch:8.1.2336', function() + exec([[ + call setline(1, ['one', 'two', 'three']) + 2 + set ls=2 + hi! link StatusLine ErrorMsg + noremap <expr> <C-B> Func() + func Func() + let g:on = !get(g:, 'on', 0) + redraws + return '' + endfunc + func Status() + return get(g:, 'on', 0) ? '[on]' : '' + endfunc + set stl=%{Status()} + ]]) + feed('<C-B>') + screen:expect([[ + one | + ^two | + three | + [on] | + | + ]]) + end) + + it('error in :nmap <expr> does not mess up display vim-patch:4.2.4338', function() + screen:try_resize(40, 5) + command('nmap <expr> <F2> execute("throw 42")') + feed('<F2>') + screen:expect([[ + | + | + Error detected while processing : | + E605: Exception not caught: 42 | + Press ENTER or type command to continue^ | + ]]) + feed('<CR>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]]) + end) + + it('error in :cmap <expr> handled correctly vim-patch:4.2.4338', function() + screen:try_resize(40, 5) + command('cmap <expr> <F2> execute("throw 42")') + feed(':echo "foo') + screen:expect([[ + | + ~ | + ~ | + ~ | + :echo "foo^ | + ]]) + feed('<F2>') + screen:expect([[ + | + :echo "foo | + Error detected while processing : | + E605: Exception not caught: 42 | + :echo "foo^ | + ]]) + feed('"') + screen:expect([[ + | + :echo "foo | + Error detected while processing : | + E605: Exception not caught: 42 | + :echo "foo"^ | + ]]) + feed('\n') + screen:expect([[ + :echo "foo | + Error detected while processing : | + E605: Exception not caught: 42 | + foo | + Press ENTER or type command to continue^ | + ]]) + end) + + it('listing mappings clears command line vim-patch:8.2.4401', function() + screen:try_resize(40, 5) + command('nmap a b') + feed(': nmap a<CR>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + n a b | + ]]) + end) end) diff --git a/test/functional/ex_cmds/mksession_spec.lua b/test/functional/ex_cmds/mksession_spec.lua index aefcc53cab..2702fb196f 100644 --- a/test/functional/ex_cmds/mksession_spec.lua +++ b/test/functional/ex_cmds/mksession_spec.lua @@ -14,6 +14,7 @@ local rmdir = helpers.rmdir local file_prefix = 'Xtest-functional-ex_cmds-mksession_spec' describe(':mksession', function() + if helpers.pending_win32(pending) then return end local session_file = file_prefix .. '.vim' local tab_dir = file_prefix .. '.d' diff --git a/test/functional/lua/thread_spec.lua b/test/functional/lua/thread_spec.lua new file mode 100644 index 0000000000..2e0ab7bdff --- /dev/null +++ b/test/functional/lua/thread_spec.lua @@ -0,0 +1,386 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local assert_alive = helpers.assert_alive +local clear = helpers.clear +local feed = helpers.feed +local eq = helpers.eq +local exec_lua = helpers.exec_lua +local next_msg = helpers.next_msg +local NIL = helpers.NIL +local pcall_err = helpers.pcall_err + +describe('thread', function() + local screen + + before_each(function() + clear() + screen = Screen.new(50, 10) + screen:attach() + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {bold = true, reverse = true}, + [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + [4] = {bold = true, foreground = Screen.colors.SeaGreen4}, + [5] = {bold = true}, + }) + end) + + it('entry func is executed in protected mode', function() + exec_lua [[ + local thread = vim.loop.new_thread(function() + error('Error in thread entry func') + end) + vim.loop.thread_join(thread) + ]] + + screen:expect([[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2: }| + {3:Error in luv thread:} | + {3:[string "<nvim>"]:2: Error in thread entry func} | + {4:Press ENTER or type command to continue}^ | + ]]) + feed('<cr>') + assert_alive() + end) + + it('callback is executed in protected mode', function() + exec_lua [[ + local thread = vim.loop.new_thread(function() + local timer = vim.loop.new_timer() + local function ontimeout() + timer:stop() + timer:close() + error('Error in thread callback') + end + timer:start(10, 0, ontimeout) + vim.loop.run() + end) + vim.loop.thread_join(thread) + ]] + + screen:expect([[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2: }| + {3:Error in luv callback, thread:} | + {3:[string "<nvim>"]:6: Error in thread callback} | + {4:Press ENTER or type command to continue}^ | + ]]) + feed('<cr>') + assert_alive() + end) + + describe('print', function() + it('works', function() + exec_lua [[ + local thread = vim.loop.new_thread(function() + print('print in thread') + end) + vim.loop.thread_join(thread) + ]] + + screen:expect([[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + print in thread | + ]]) + end) + end) + + describe('vim.*', function() + before_each(function() + clear() + exec_lua [[ + Thread_Test = {} + + Thread_Test.entry_func = function(async, entry_str, args) + local decoded_args = vim.mpack.decode(args) + assert(loadstring(entry_str))(async, decoded_args) + end + + function Thread_Test:do_test() + local async + local on_async = self.on_async + async = vim.loop.new_async(function(ret) + on_async(ret) + async:close() + end) + local thread = + vim.loop.new_thread(self.entry_func, async, self.entry_str, self.args) + vim.loop.thread_join(thread) + end + + Thread_Test.new = function(entry, on_async, ...) + self = {} + setmetatable(self, {__index = Thread_Test}) + self.args = vim.mpack.encode({...}) + self.entry_str = string.dump(entry) + self.on_async = on_async + return self + end + ]] + end) + + it('is_thread', function() + exec_lua [[ + local entry = function(async) + async:send(vim.is_thread()) + end + local on_async = function(ret) + vim.rpcnotify(1, 'result', ret) + end + local thread_test = Thread_Test.new(entry, on_async) + thread_test:do_test() + ]] + + eq({'notification', 'result', {true}}, next_msg()) + end) + + it('loop', function() + exec_lua [[ + local entry = function(async) + async:send(vim.loop.version()) + end + local on_async = function(ret) + vim.rpcnotify(1, ret) + end + local thread_test = Thread_Test.new(entry, on_async) + thread_test:do_test() + ]] + + local msg = next_msg() + eq(msg[1], 'notification') + assert(tonumber(msg[2]) >= 72961) + end) + + it('mpack', function() + exec_lua [[ + local entry = function(async) + async:send(vim.mpack.encode({33, vim.NIL, 'text'})) + end + local on_async = function(ret) + vim.rpcnotify(1, 'result', vim.mpack.decode(ret)) + end + local thread_test = Thread_Test.new(entry, on_async) + thread_test:do_test() + ]] + + eq({'notification', 'result', {{33, NIL, 'text'}}}, next_msg()) + end) + + it('json', function() + exec_lua [[ + local entry = function(async) + async:send(vim.json.encode({33, vim.NIL, 'text'})) + end + local on_async = function(ret) + vim.rpcnotify(1, 'result', vim.json.decode(ret)) + end + local thread_test = Thread_Test.new(entry, on_async) + thread_test:do_test() + ]] + + eq({'notification', 'result', {{33, NIL, 'text'}}}, next_msg()) + end) + + it('diff', function() + exec_lua [[ + local entry = function(async) + async:send(vim.diff('Hello\n', 'Helli\n')) + end + local on_async = function(ret) + vim.rpcnotify(1, 'result', ret) + end + local thread_test = Thread_Test.new(entry, on_async) + thread_test:do_test() + ]] + + eq({'notification', 'result', + {table.concat({ + '@@ -1 +1 @@', + '-Hello', + '+Helli', + '' + }, '\n')}}, + next_msg()) + end) + end) +end) + +describe('threadpool', function() + before_each(clear) + + it('is_thread', function() + eq(false, exec_lua [[return vim.is_thread()]]) + + exec_lua [[ + local work_fn = function() + return vim.is_thread() + end + local after_work_fn = function(ret) + vim.rpcnotify(1, 'result', ret) + end + local work = vim.loop.new_work(work_fn, after_work_fn) + work:queue() + ]] + + eq({'notification', 'result', {true}}, next_msg()) + end) + + it('with invalid argument', function() + local status = pcall_err(exec_lua, [[ + local work = vim.loop.new_thread(function() end, function() end) + work:queue({}) + ]]) + + eq([[Error executing lua: [string "<nvim>"]:0: Error: thread arg not support type 'function' at 1]], + status) + end) + + it('with invalid return value', function() + local screen = Screen.new(50, 10) + screen:attach() + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {bold = true, reverse = true}, + [3] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + [4] = {bold = true, foreground = Screen.colors.SeaGreen4}, + [5] = {bold = true}, + }) + + exec_lua [[ + local work = vim.loop.new_work(function() return {} end, function() end) + work:queue() + ]] + + screen:expect([[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2: }| + {3:Error in luv thread:} | + {3:Error: thread arg not support type 'table' at 1} | + {4:Press ENTER or type command to continue}^ | + ]]) + end) + + describe('vim.*', function() + before_each(function() + clear() + exec_lua [[ + Threadpool_Test = {} + + Threadpool_Test.work_fn = function(work_fn_str, args) + local decoded_args = vim.mpack.decode(args) + return assert(loadstring(work_fn_str))(decoded_args) + end + + function Threadpool_Test:do_test() + local work = + vim.loop.new_work(self.work_fn, self.after_work) + work:queue(self.work_fn_str, self.args) + end + + Threadpool_Test.new = function(work_fn, after_work, ...) + self = {} + setmetatable(self, {__index = Threadpool_Test}) + self.args = vim.mpack.encode({...}) + self.work_fn_str = string.dump(work_fn) + self.after_work = after_work + return self + end + ]] + end) + + it('loop', function() + exec_lua [[ + local work_fn = function() + return vim.loop.version() + end + local after_work_fn = function(ret) + vim.rpcnotify(1, ret) + end + local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn) + threadpool_test:do_test() + ]] + + local msg = next_msg() + eq(msg[1], 'notification') + assert(tonumber(msg[2]) >= 72961) + end) + + it('mpack', function() + exec_lua [[ + local work_fn = function() + local var = vim.mpack.encode({33, vim.NIL, 'text'}) + return var + end + local after_work_fn = function(ret) + vim.rpcnotify(1, 'result', vim.mpack.decode(ret)) + end + local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn) + threadpool_test:do_test() + ]] + + eq({'notification', 'result', {{33, NIL, 'text'}}}, next_msg()) + end) + + it('json', function() + exec_lua [[ + local work_fn = function() + local var = vim.json.encode({33, vim.NIL, 'text'}) + return var + end + local after_work_fn = function(ret) + vim.rpcnotify(1, 'result', vim.json.decode(ret)) + end + local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn) + threadpool_test:do_test() + ]] + + eq({'notification', 'result', {{33, NIL, 'text'}}}, next_msg()) + end) + + it('work', function() + exec_lua [[ + local work_fn = function() + return vim.diff('Hello\n', 'Helli\n') + end + local after_work_fn = function(ret) + vim.rpcnotify(1, 'result', ret) + end + local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn) + threadpool_test:do_test() + ]] + + eq({'notification', 'result', + {table.concat({ + '@@ -1 +1 @@', + '-Hello', + '+Helli', + '' + }, '\n')}}, + next_msg()) + end) + end) +end) diff --git a/test/functional/plugin/shada_spec.lua b/test/functional/plugin/shada_spec.lua index a4d78682ad..6f22f865e6 100644 --- a/test/functional/plugin/shada_spec.lua +++ b/test/functional/plugin/shada_spec.lua @@ -1,7 +1,7 @@ local helpers = require('test.functional.helpers')(after_each) local clear = helpers.clear -local eq, nvim_eval, nvim_command, nvim, exc_exec, funcs, nvim_feed, curbuf = - helpers.eq, helpers.eval, helpers.command, helpers.nvim, helpers.exc_exec, +local eq, meths, nvim_eval, nvim_command, nvim, exc_exec, funcs, nvim_feed, curbuf = + helpers.eq, helpers.meths, helpers.eval, helpers.command, helpers.nvim, helpers.exc_exec, helpers.funcs, helpers.feed, helpers.curbuf local neq = helpers.neq local read_file = helpers.read_file @@ -2162,6 +2162,10 @@ describe('plugin/shada.vim', function() wshada('\004\000\009\147\000\196\002ab\196\001a') wshada_tmp('\004\000\009\147\000\196\002ab\196\001b') + + local bufread_commands = meths.get_autocmds({ group = "ShaDaCommands", event = "BufReadCmd" }) + eq(2, #bufread_commands--[[, vim.inspect(bufread_commands) ]]) + -- Need to set nohidden so that the buffer containing 'fname' is not unloaded -- after loading 'fname_tmp', otherwise the '++opt not supported' test below -- won't work since the BufReadCmd autocmd won't be triggered. diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index beb43e0271..1cef771f0d 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -258,6 +258,7 @@ describe(':terminal buffer', function() end) it('it works with set rightleft #11438', function() + if helpers.pending_win32(pending) then return end local columns = eval('&columns') feed(string.rep('a', columns)) command('set rightleft') diff --git a/test/functional/terminal/cursor_spec.lua b/test/functional/terminal/cursor_spec.lua index e9495f45a2..3b905f1f56 100644 --- a/test/functional/terminal/cursor_spec.lua +++ b/test/functional/terminal/cursor_spec.lua @@ -87,6 +87,7 @@ describe(':terminal cursor', function() describe('when invisible', function() it('is not highlighted and is detached from screen cursor', function() + if helpers.pending_win32(pending) then return end hide_cursor() screen:expect([[ tty ready | @@ -176,6 +177,7 @@ describe('cursor with customized highlighting', function() end) describe('buffer cursor position is correct in terminal without number column', function() + if helpers.pending_win32(pending) then return end local screen local function setup_ex_register(str) @@ -526,6 +528,7 @@ describe('buffer cursor position is correct in terminal without number column', end) describe('buffer cursor position is correct in terminal with number column', function() + if helpers.pending_win32(pending) then return end local screen local function setup_ex_register(str) diff --git a/test/functional/terminal/edit_spec.lua b/test/functional/terminal/edit_spec.lua index fabc5524ed..e7025d6739 100644 --- a/test/functional/terminal/edit_spec.lua +++ b/test/functional/terminal/edit_spec.lua @@ -36,6 +36,7 @@ describe(':edit term://*', function() end) it("runs TermOpen early enough to set buffer-local 'scrollback'", function() + if helpers.pending_win32(pending) then return end local columns, lines = 20, 4 local scr = get_screen(columns, lines) local rep = 97 diff --git a/test/functional/terminal/ex_terminal_spec.lua b/test/functional/terminal/ex_terminal_spec.lua index 065dd72485..b4f29a586a 100644 --- a/test/functional/terminal/ex_terminal_spec.lua +++ b/test/functional/terminal/ex_terminal_spec.lua @@ -142,6 +142,7 @@ describe(':terminal (with fake shell)', function() end it('with no argument, acts like termopen()', function() + if helpers.pending_win32(pending) then return end terminal_with_fake_shell() retry(nil, 4 * screen.timeout, function() screen:expect([[ @@ -165,6 +166,7 @@ describe(':terminal (with fake shell)', function() end) it("with no argument, but 'shell' has arguments, acts like termopen()", function() + if helpers.pending_win32(pending) then return end nvim('set_option', 'shell', nvim_dir..'/shell-test -t jeff') terminal_with_fake_shell() screen:expect([[ @@ -176,6 +178,7 @@ describe(':terminal (with fake shell)', function() end) it('executes a given command through the shell', function() + if helpers.pending_win32(pending) then return end command('set shellxquote=') -- win: avoid extra quotes terminal_with_fake_shell('echo hi') screen:expect([[ @@ -187,6 +190,7 @@ describe(':terminal (with fake shell)', function() end) it("executes a given command through the shell, when 'shell' has arguments", function() + if helpers.pending_win32(pending) then return end nvim('set_option', 'shell', nvim_dir..'/shell-test -t jeff') command('set shellxquote=') -- win: avoid extra quotes terminal_with_fake_shell('echo hi') @@ -199,6 +203,7 @@ describe(':terminal (with fake shell)', function() end) it('allows quotes and slashes', function() + if helpers.pending_win32(pending) then return end command('set shellxquote=') -- win: avoid extra quotes terminal_with_fake_shell([[echo 'hello' \ "world"]]) screen:expect([[ @@ -235,6 +240,7 @@ describe(':terminal (with fake shell)', function() end) it('works with :find', function() + if helpers.pending_win32(pending) then return end terminal_with_fake_shell() screen:expect([[ ^ready $ | @@ -253,6 +259,7 @@ describe(':terminal (with fake shell)', function() end) it('works with gf', function() + if helpers.pending_win32(pending) then return end command('set shellxquote=') -- win: avoid extra quotes terminal_with_fake_shell([[echo "scripts/shadacat.py"]]) retry(nil, 4 * screen.timeout, function() diff --git a/test/functional/terminal/highlight_spec.lua b/test/functional/terminal/highlight_spec.lua index 8d3f0218af..ebbae1df14 100644 --- a/test/functional/terminal/highlight_spec.lua +++ b/test/functional/terminal/highlight_spec.lua @@ -117,6 +117,7 @@ describe(':terminal highlight', function() end) it(':terminal highlight has lower precedence than editor #9964', function() + if helpers.pending_win32(pending) then return end clear() local screen = Screen.new(30, 4) screen:set_default_attr_ids({ diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua index 11bdc73a47..d1cfc7e91b 100644 --- a/test/functional/terminal/scrollback_spec.lua +++ b/test/functional/terminal/scrollback_spec.lua @@ -345,6 +345,7 @@ end) describe(':terminal prints more lines than the screen height and exits', function() it('will push extra lines to scrollback', function() + if helpers.pending_win32(pending) then return end clear() local screen = Screen.new(30, 7) screen:attach({rgb=false}) @@ -576,6 +577,7 @@ describe("pending scrollback line handling", function() end) it("does not crash after nvim_buf_call #14891", function() + if helpers.pending_win32(pending) then return end exec_lua [[ local a = vim.api local bufnr = a.nvim_create_buf(false, true) diff --git a/test/functional/terminal/window_spec.lua b/test/functional/terminal/window_spec.lua index 9f278fd157..0d3295cf32 100644 --- a/test/functional/terminal/window_spec.lua +++ b/test/functional/terminal/window_spec.lua @@ -18,6 +18,7 @@ describe(':terminal window', function() end) it('sets topline correctly #8556', function() + if helpers.pending_win32(pending) then return end -- Test has hardcoded assumptions of dimensions. eq(7, eval('&lines')) feed_data('\n\n\n') -- Add blank lines. diff --git a/test/functional/ui/cmdline_highlight_spec.lua b/test/functional/ui/cmdline_highlight_spec.lua index 9c746b99bd..384761ab17 100644 --- a/test/functional/ui/cmdline_highlight_spec.lua +++ b/test/functional/ui/cmdline_highlight_spec.lua @@ -33,7 +33,7 @@ before_each(function() let g:NUM_LVLS = 4 function Redraw() mode - return '' + return "\<Ignore>" endfunction let g:id = '' cnoremap <expr> {REDRAW} Redraw() @@ -42,7 +42,7 @@ before_each(function() let Cb = g:Nvim_color_input{g:id} let out = input({'prompt': ':', 'highlight': Cb}) let g:out{id} = out - return (a:do_return ? out : '') + return (a:do_return ? out : "\<Ignore>") endfunction nnoremap <expr> {PROMPT} DoPrompt(0) cnoremap <expr> {PROMPT} DoPrompt(1) @@ -410,7 +410,7 @@ describe('Command-line coloring', function() end) it('stops executing callback after a number of errors', function() set_color_cb('SplittedMultibyteStart') - start_prompt('let x = "«»«»«»«»«»"\n') + start_prompt('let x = "«»«»«»«»«»"') screen:expect([[ {EOB:~ }| {EOB:~ }| @@ -419,7 +419,7 @@ describe('Command-line coloring', function() :let x = " | {ERR:E5405: Chunk 0 start 10 splits multibyte}| {ERR: character} | - ^:let x = "«»«»«»«»«»" | + :let x = "«»«»«»«»«»"^ | ]]) feed('\n') screen:expect([[ @@ -432,6 +432,7 @@ describe('Command-line coloring', function() {EOB:~ }| | ]]) + feed('\n') eq('let x = "«»«»«»«»«»"', meths.get_var('out')) local msg = '\nE5405: Chunk 0 start 10 splits multibyte character' eq(msg:rep(1), funcs.execute('messages')) @@ -474,14 +475,14 @@ describe('Command-line coloring', function() ]]) feed('\n') screen:expect([[ - | + ^ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| - ^:echo 42 | + :echo 42 | ]]) feed('\n') eq('echo 42', meths.get_var('out')) diff --git a/test/functional/ui/hlstate_spec.lua b/test/functional/ui/hlstate_spec.lua index 2a567b28ee..295b70f265 100644 --- a/test/functional/ui/hlstate_spec.lua +++ b/test/functional/ui/hlstate_spec.lua @@ -179,6 +179,8 @@ describe('ext_hlstate detailed highlights', function() end) it("work with :terminal", function() + if helpers.pending_win32(pending) then return end + screen:set_default_attr_ids({ [1] = {{}, {{hi_name = "TermCursorNC", ui_name = "TermCursorNC", kind = "ui"}}}, [2] = {{foreground = tonumber('0x00ccff'), fg_indexed=true}, {{kind = "term"}}}, diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua index f038348253..949591870a 100644 --- a/test/functional/ui/messages_spec.lua +++ b/test/functional/ui/messages_spec.lua @@ -1206,6 +1206,7 @@ end) describe('ui/msg_puts_printf', function() it('output multibyte characters correctly', function() + if helpers.pending_win32(pending) then return end local screen local cmd = '' local locale_dir = test_build_dir..'/share/locale/ja/LC_MESSAGES' diff --git a/test/functional/ui/output_spec.lua b/test/functional/ui/output_spec.lua index 50e5dfac84..7305baa761 100644 --- a/test/functional/ui/output_spec.lua +++ b/test/functional/ui/output_spec.lua @@ -14,6 +14,7 @@ local has_powershell = helpers.has_powershell local set_shell_powershell = helpers.set_shell_powershell describe("shell command :!", function() + if helpers.pending_win32(pending) then return end local screen before_each(function() clear() diff --git a/test/functional/ui/sign_spec.lua b/test/functional/ui/sign_spec.lua index a0b7c98c51..f718786557 100644 --- a/test/functional/ui/sign_spec.lua +++ b/test/functional/ui/sign_spec.lua @@ -446,7 +446,7 @@ describe('Signs', function() {1:>>>>>>>>}{6: 1 }a | {2: }{6: 2 }b | {2: }{6: 3 }c | - {2: }{6:^ 4 } | + {2: }{6: 4 }^ | {0:~ }| {0:~ }| {0:~ }| @@ -468,7 +468,7 @@ describe('Signs', function() {1:>>>>>>>>>>}{6: 1 }a | {2: }{6: 2 }b | {2: }{6: 3 }c | - {2: ^ }{6: 4 } | + {2: }{6: 4 }^ | {0:~ }| {0:~ }| {0:~ }| @@ -512,7 +512,7 @@ describe('Signs', function() {1:>>}{6: 1 }a | {2: }{6: 2 }b | {2: }{6: 3 }c | - {2: }{6: ^4 } | + {2: }{6: 4 }^ | {0:~ }| {0:~ }| {0:~ }| diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index 174f1cbaba..2001171378 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -144,8 +144,8 @@ endif() include(ExternalProject) -set(LIBUV_URL https://github.com/libuv/libuv/archive/v1.42.0.tar.gz) -set(LIBUV_SHA256 371e5419708f6aaeb8656671f89400b92a9bba6443369af1bb70bcd6e4b3c764) +set(LIBUV_URL https://github.com/libuv/libuv/archive/7ae0c9543d0080968766288c73874aee3798ae30.tar.gz) +set(LIBUV_SHA256 02ade646f52221e56f2515f8d0bfb8099204d21f6973b2a139bc726807ea803c) 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) @@ -169,9 +169,9 @@ set(LIBTERMKEY_SHA256 6945bd3c4aaa83da83d80a045c5563da4edd7d0374c62c0d35aec09eb3 set(LIBVTERM_URL https://www.leonerd.org.uk/code/libvterm/libvterm-0.1.4.tar.gz) set(LIBVTERM_SHA256 bc70349e95559c667672fc8c55b9527d9db9ada0fb80a3beda533418d782d3dd) -set(LUV_VERSION 1.42.0-1) +set(LUV_VERSION 1.43.0-0) set(LUV_URL https://github.com/luvit/luv/archive/${LUV_VERSION}.tar.gz) -set(LUV_SHA256 a55563e6da9294ea26f77a2111598aa49188c5806b8bd1e1f4bdf402f340713e) +set(LUV_SHA256 a36865f34db029e2caa01245a41341a067038c09e94459b50db1346d9fdf82f0) set(LUA_COMPAT53_URL https://github.com/keplerproject/lua-compat-5.3/archive/v0.9.tar.gz) set(LUA_COMPAT53_SHA256 ad05540d2d96a48725bb79a1def35cf6652a4e2ec26376e2617c8ce2baa6f416) diff --git a/third-party/cmake/BuildGettext.cmake b/third-party/cmake/BuildGettext.cmake index 9357456343..6128ecfa69 100644 --- a/third-party/cmake/BuildGettext.cmake +++ b/third-party/cmake/BuildGettext.cmake @@ -21,6 +21,7 @@ if(MSVC) -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_GENERATOR=${CMAKE_GENERATOR} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} -DLIBICONV_INCLUDE_DIRS=${DEPS_INSTALL_DIR}/include -DLIBICONV_LIBRARIES=${DEPS_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}libcharset${CMAKE_STATIC_LIBRARY_SUFFIX}$<SEMICOLON>${DEPS_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}libiconv${CMAKE_STATIC_LIBRARY_SUFFIX} BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${CMAKE_BUILD_TYPE} diff --git a/third-party/cmake/BuildGperf.cmake b/third-party/cmake/BuildGperf.cmake index 71c3cc1eef..5401191150 100644 --- a/third-party/cmake/BuildGperf.cmake +++ b/third-party/cmake/BuildGperf.cmake @@ -57,6 +57,7 @@ elseif(MSVC OR MINGW) -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} -DCMAKE_GENERATOR=${CMAKE_GENERATOR} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${CMAKE_BUILD_TYPE} diff --git a/third-party/cmake/BuildLibiconv.cmake b/third-party/cmake/BuildLibiconv.cmake index dc3d8fe4c3..5ff33e0cd3 100644 --- a/third-party/cmake/BuildLibiconv.cmake +++ b/third-party/cmake/BuildLibiconv.cmake @@ -21,6 +21,7 @@ if(MSVC) -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_GENERATOR=${CMAKE_GENERATOR} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${CMAKE_BUILD_TYPE} INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config ${CMAKE_BUILD_TYPE}) diff --git a/third-party/cmake/BuildLibtermkey.cmake b/third-party/cmake/BuildLibtermkey.cmake index 10e98fbab3..d44e09d734 100644 --- a/third-party/cmake/BuildLibtermkey.cmake +++ b/third-party/cmake/BuildLibtermkey.cmake @@ -22,6 +22,7 @@ ExternalProject_Add(libtermkey # Hack to avoid -rdynamic in Mingw -DCMAKE_SHARED_LIBRARY_LINK_C_FLAGS="" -DCMAKE_GENERATOR=${CMAKE_GENERATOR} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} -DUNIBILIUM_INCLUDE_DIRS=${DEPS_INSTALL_DIR}/include -DUNIBILIUM_LIBRARIES=${DEPS_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}unibilium${CMAKE_STATIC_LIBRARY_SUFFIX} BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${CMAKE_BUILD_TYPE} diff --git a/third-party/cmake/BuildLibuv.cmake b/third-party/cmake/BuildLibuv.cmake index dce64c777b..42650308a8 100644 --- a/third-party/cmake/BuildLibuv.cmake +++ b/third-party/cmake/BuildLibuv.cmake @@ -63,10 +63,6 @@ elseif(WIN32) set(BUILD_SHARED ON) elseif(MINGW) set(BUILD_SHARED OFF) - set(LIBUV_PATCH_COMMAND - ${GIT_EXECUTABLE} -C ${DEPS_BUILD_DIR}/src/libuv init - COMMAND ${GIT_EXECUTABLE} -C ${DEPS_BUILD_DIR}/src/libuv apply --ignore-whitespace - ${CMAKE_CURRENT_SOURCE_DIR}/patches/libuv-disable-typedef-MinGW.patch) else() message(FATAL_ERROR "Trying to build libuv in an unsupported system ${CMAKE_SYSTEM_NAME}/${CMAKE_C_COMPILER_ID}") endif() @@ -77,6 +73,7 @@ elseif(WIN32) COMMAND ${CMAKE_COMMAND} ${DEPS_BUILD_DIR}/src/libuv/CMakeLists.txt -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} -DCMAKE_GENERATOR=${CMAKE_GENERATOR} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DBUILD_SHARED_LIBS=${BUILD_SHARED} -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} diff --git a/third-party/cmake/BuildLibvterm.cmake b/third-party/cmake/BuildLibvterm.cmake index 09f2ba7f2c..2c300dda7c 100644 --- a/third-party/cmake/BuildLibvterm.cmake +++ b/third-party/cmake/BuildLibvterm.cmake @@ -47,6 +47,7 @@ if(WIN32) COMMAND ${CMAKE_COMMAND} ${DEPS_BUILD_DIR}/src/libvterm -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} "-DCMAKE_C_FLAGS:STRING=${CMAKE_C_COMPILER_ARG1} -fPIC" -DCMAKE_GENERATOR=${CMAKE_GENERATOR}) set(LIBVTERM_BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${CMAKE_BUILD_TYPE}) diff --git a/third-party/cmake/BuildLuv.cmake b/third-party/cmake/BuildLuv.cmake index 69f3b60ecf..99822249c2 100644 --- a/third-party/cmake/BuildLuv.cmake +++ b/third-party/cmake/BuildLuv.cmake @@ -104,6 +104,7 @@ elseif(MSVC) set(LUV_CONFIGURE_COMMAND ${LUV_CONFIGURE_COMMAND_COMMON} -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} # Same as Unix without fPIC "-DCMAKE_C_FLAGS:STRING=${CMAKE_C_COMPILER_ARG1} ${LUV_INCLUDE_FLAGS}" # Make sure we use the same generator, otherwise we may diff --git a/third-party/cmake/BuildMsgpack.cmake b/third-party/cmake/BuildMsgpack.cmake index 39a8a64d23..ee4f0eb080 100644 --- a/third-party/cmake/BuildMsgpack.cmake +++ b/third-party/cmake/BuildMsgpack.cmake @@ -63,6 +63,7 @@ elseif(MSVC) -DMSGPACK_BUILD_EXAMPLES=OFF -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} "-DCMAKE_C_FLAGS:STRING=${CMAKE_C_COMPILER_ARG1}" -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} # Make sure we use the same generator, otherwise we may diff --git a/third-party/cmake/BuildTreesitter.cmake b/third-party/cmake/BuildTreesitter.cmake index 0aa2706d7d..01fdb837e2 100644 --- a/third-party/cmake/BuildTreesitter.cmake +++ b/third-party/cmake/BuildTreesitter.cmake @@ -42,6 +42,7 @@ if(MSVC) COMMAND ${CMAKE_COMMAND} ${DEPS_BUILD_DIR}/src/tree-sitter/CMakeLists.txt -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} -DCMAKE_GENERATOR=${CMAKE_GENERATOR} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${CMAKE_BUILD_TYPE} diff --git a/third-party/cmake/BuildTreesitterParsers.cmake b/third-party/cmake/BuildTreesitterParsers.cmake index 5284a7fd62..f966d640e6 100644 --- a/third-party/cmake/BuildTreesitterParsers.cmake +++ b/third-party/cmake/BuildTreesitterParsers.cmake @@ -17,6 +17,7 @@ CMAKE_ARGS -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} -DCMAKE_GENERATOR=${CMAKE_GENERATOR} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} # Pass toolchain diff --git a/third-party/cmake/BuildUnibilium.cmake b/third-party/cmake/BuildUnibilium.cmake index 74c1cbddb0..2f940bdfd3 100644 --- a/third-party/cmake/BuildUnibilium.cmake +++ b/third-party/cmake/BuildUnibilium.cmake @@ -18,6 +18,7 @@ if(WIN32) -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} # Pass toolchain -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN} + -DCMAKE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_GENERATOR=${CMAKE_GENERATOR} BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${CMAKE_BUILD_TYPE} diff --git a/third-party/patches/libuv-disable-typedef-MinGW.patch b/third-party/patches/libuv-disable-typedef-MinGW.patch deleted file mode 100644 index a47893cede..0000000000 --- a/third-party/patches/libuv-disable-typedef-MinGW.patch +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/include/uv/win.h b/include/uv/win.h -index f5f1d3a3..64a0dfd9 100644 ---- a/include/uv/win.h -+++ b/include/uv/win.h -@@ -45,7 +45,14 @@ typedef struct pollfd { - #endif - - #include <mswsock.h> -+// Disable the typedef in mstcpip.h of MinGW. -+#define _TCP_INITIAL_RTO_PARAMETERS _TCP_INITIAL_RTO_PARAMETERS__ -+#define TCP_INITIAL_RTO_PARAMETERS TCP_INITIAL_RTO_PARAMETERS__ -+#define PTCP_INITIAL_RTO_PARAMETERS PTCP_INITIAL_RTO_PARAMETERS__ - #include <ws2tcpip.h> -+#undef _TCP_INITIAL_RTO_PARAMETERS -+#undef TCP_INITIAL_RTO_PARAMETERS -+#undef PTCP_INITIAL_RTO_PARAMETERS - #include <windows.h> - - #include <process.h> |