From 6100d5d0f4693b553c88ccee32d2953033b20d0f Mon Sep 17 00:00:00 2001 From: Dave Parillo Date: Sun, 8 Mar 2026 20:36:25 -0700 Subject: [PATCH 1/2] Fix some issues in upstream. - `Make` does mot match this plugin namespace and conflicts with other plugins. It is also undocumented: #36. - `:make` does not run detault target. - `CMake` is ambiguous. New features or bugs, depending on your outlook: - `CMake` renamed to `CMakeConfigure`. - `CMakeConfigure` stores a file `.vim-cmake` in the project root. CMake looks for this file and uses it if it exists, minimizing the number of times configure needs to be run. - `Make` renamed to `CMakeBuild`. - Updated docs and tests. - Replaced travis & appveyor with github actions. --- .github/workflows/test.yaml | 47 +++++ .gitignore | 1 + .travis.yml | 16 -- README.md | 46 ++++- appveyor.yml | 10 -- autoload/cmake.vim | 295 +++++++++++++++++++++++++++++++ doc/cmake.txt | 212 ++++++++++++++++------ plugin/cmake.vim | 195 +++----------------- test/.vimrc | 14 +- test/cmake.vader | 237 +++++++++++++++++++++---- test/test project/CMakeLists.txt | 2 +- 11 files changed, 785 insertions(+), 290 deletions(-) create mode 100644 .github/workflows/test.yaml delete mode 100644 .travis.yml delete mode 100644 appveyor.yml create mode 100644 autoload/cmake.vim diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..4ff0475 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,47 @@ +name: Test + +on: + push: + branches: [ master, vim-cmake-updates ] + pull_request: + branches: [ master ] + +jobs: + test: + strategy: + matrix: + include: + - os: ubuntu-latest + install: | + sudo apt-get update + sudo apt-get install -y vim cmake + vim_path: "" + run: vim -Es --cmd 'set packpath=' -u test/.vimrc -c 'Vader! test/cmake.vader' + + - os: windows-latest + install: | + winget install --id vim.vim --silent --accept-package-agreements --accept-source-agreements + winget install --id Kitware.CMake --silent --accept-package-agreements --accept-source-agreements + vim_path: | + $vimpath = Get-ChildItem "C:\Program Files\Vim" -Directory | Select-Object -Last 1 + echo "$vimpath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + run: vim -Es --cmd "set packpath=" -u test/.vimrc -c "Vader! test/cmake.vader" + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: ${{ matrix.install }} + + - name: Add Vim to PATH + if: matrix.vim_path != '' + run: ${{ matrix.vim_path }} + + - name: Clone vader.vim + run: git clone https://github.com/junegunn/vader.vim test/vader.vim + + - name: Run tests + run: ${{ matrix.run }} diff --git a/.gitignore b/.gitignore index 50a9abd..c8c26ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +test/vader.vim/ test/test project/build/ doc/tags diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f85420c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: vim - -addons: - apt: - sources: - - ubuntu-toolchain-r-test - - kubuntu-backports - packages: - - g++-4.7 - - cmake - -before_script: | - git clone https://github.com/junegunn/vader.vim.git - -script: | - vim -Nu 'test/.vimrc' -c 'Vader! test/cmake.vader' diff --git a/README.md b/README.md index dd565c4..9637f65 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # vim-cmake -[![Travis (Linux)](https://travis-ci.org/vhdirk/vim-cmake.svg?branch=master)](https://travis-ci.org/vhdirk/vim-cmake) -[![AppVeyor (Windows)](https://ci.appveyor.com/api/projects/status/ns1o9252o2rrmv6g?svg=true)](https://ci.appveyor.com/project/vhdirk/vim-cmake) +[![Test](https://github.com/DaveParillo/vim-cmake/actions/workflows/test.yaml/badge.svg)](https://github.com/DaveParillo/vim-cmake/actions/workflows/test.yaml) vim-cmake is a Vim plugin to make working with CMake a little nicer. @@ -11,11 +10,14 @@ disliked setting makeprg manually each time. This plugin does just that. ### Commands - * `:CMake` searches for the closest directory named build in an upwards search, -and whenever one is found, it runs the cmake command there, assuming the CMakeLists.txt -file is just one directory above. Any arguments given to :CMake will be directly passed + * `:CMakeConfigure` searches for the closest directory named build in an upwards search, +and if found, it runs the `cmake -S 'source_dir' -B 'build_dir'` command there, assuming the CMakeLists.txt +file is just one directory above. Any arguments given to :CMakeConfigure will be directly passed on to the cmake command. It also sets the working directory of the make command, so you can just use quickfix as with a normal Makefile project. + + * `:CMakeBuild` executes the `cmake --build` command in a directory configured + previously by the `:CMakeConfigure` command. If you have the [AsyncRun plugin](https://github.com/skywind3000/asyncrun.vim) installed, it will be used automatically and you will be able to check the result of the cmake command in the quickfix as well. @@ -49,6 +51,40 @@ result of the cmake command in the quickfix as well. ## Installation +With Vim 8.1 and later, no external plugin manager is required. Vim includes a +built-in package system that automatically loads plugins placed in specific +directories under `pack/`. + +Create the `start` directory if it does not already exist: + + mkdir -p ~/.vim/pack/dev/start/ + +Clone the repository into that directory: + + git clone https://github.com/vhdirk/vim-cmake.git \ + ~/.vim/pack/dev/start/vim-cmake + +Plugins placed in `pack/*/start/` are automatically loaded when Vim starts. +No additional configuration is required. + +If you prefer to install the plugin but load it manually, place it in the +`opt` directory instead: + + mkdir -p ~/.vim/pack/dev/opt/ + git clone https://github.com/vhdirk/vim-cmake.git \ + ~/.vim/pack/dev/opt/vim-cmake + +Then load it when needed: + + :packadd vim-cmake + +### Verifying the installation + +Restart Vim and run: + + :CMakeBuild + +If the command is recognized, the plugin is installed correctly. ### Vim-pathogen diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 7afdcd7..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,10 +0,0 @@ -os: Visual Studio 2015 - -install: - - cinst vim - - cinst cmake - - set PATH=%PATH%;C:\Program Files\CMake\bin - - git clone https://github.com/junegunn/vader.vim - -build_script: - - vim -Nu test/.vimrc -c 'Vader! test/cmake.vader' --not-a-term diff --git a/autoload/cmake.vim b/autoload/cmake.vim new file mode 100644 index 0000000..88379f4 --- /dev/null +++ b/autoload/cmake.vim @@ -0,0 +1,295 @@ +" autoload/cmake.vim - Implementation for vim-cmake +" Maintainer: Dirk Van Haerenborgh +" Version: 1.0 + +" Utility function +" Thanks to tpope/vim-fugitive +function! s:fnameescape(file) abort + if exists('*fnameescape') + return fnameescape(a:file) + else + return escape(a:file," \t\n*?[{`$\\%#'\"|!<") + endif +endfunction + +" Returns the path to the dotfile used to persist b:build_dir, +" anchored to the project root (one level above b:build_dir), or +" falling back to the directory of the current file. +function! s:dotfile_path() abort + if exists('b:build_dir') && !empty(b:build_dir) + return fnamemodify(b:build_dir, ':p:h:h') . '/.vim-cmake' + endif + return fnamemodify(expand('%:p:h'), ':p') . '/.vim-cmake' +endfunction + +function! s:find_build_dir() abort + " Do not overwrite already found build_dir, may be set explicitly by user. + if exists("b:build_dir") && b:build_dir != "" + return 1 + endif + " Check dotfile cache first + let l:dotfile = s:dotfile_path() + if filereadable(l:dotfile) + let l:lines = readfile(l:dotfile) + if !empty(l:lines) + let l:candidate = fnamemodify(l:lines[0], ':p') + if isdirectory(l:candidate) + let b:build_dir = l:candidate + echom "vim-cmake: Restored build directory from cache: " . b:build_dir + return 1 + else + call delete(l:dotfile) + endif + endif + endif + " search filesystem + let g:cmake_build_dir = get(g:, 'cmake_build_dir', 'build') + let b:build_dir = finddir(g:cmake_build_dir, ';') + if b:build_dir == "" + " Find build directory in path of current file + let b:build_dir = finddir(g:cmake_build_dir, s:fnameescape(expand("%:p:h")) . ';') + endif + if b:build_dir != "" + " expand() would expand "" to working directory, but we need + " this as an indicator that build was not found + let b:build_dir = fnamemodify(b:build_dir, ':p') + echom "Found cmake build directory: " . s:fnameescape(b:build_dir) + call cmake#SaveBuildDir() + return 1 + else + echom "Unable to find cmake build directory." + return 0 + endif +endfunction + +function! s:find_smp() abort + if executable('nproc') + let l:nproc = system('nproc') + let b:smp = '-j' . substitute(l:nproc, '\n\+$', '', '') + return 1 + endif + return 0 +endfunction + +" Configure the cmake project in the currently set build dir. +" +" This will override any of the following variables if the +" corresponding vim variable is set: +" * CMAKE_INSTALL_PREFIX +" * CMAKE_BUILD_TYPE +" * CMAKE_BUILD_SHARED_LIBS +" If the project is not configured already, the following variables will be set +" whenever the corresponding vim variable for the following is set: +" * CMAKE_CXX_COMPILER +" * CMAKE_C_COMPILER +" * The generator (-G) +function! s:cmake_configure(cmake_vim_command_args) abort + if has('win32') + let l:save_shellslash = &shellslash + set noshellslash + endif + exec 'cd' s:fnameescape(b:build_dir) + let l:argument = [] + " Only change values of variables if project is not configured already, + " otherwise we overwrite existing configuration. + let l:configured = filereadable("CMakeCache.txt") + if !l:configured + if exists("g:cmake_project_generator") + let l:argument += [ "-G \"" . g:cmake_project_generator . "\"" ] + endif + if exists("g:cmake_cxx_compiler") + let l:argument += [ "-DCMAKE_CXX_COMPILER:FILEPATH=" . g:cmake_cxx_compiler ] + endif + if exists("g:cmake_c_compiler") + let l:argument += [ "-DCMAKE_C_COMPILER:FILEPATH=" . g:cmake_c_compiler ] + endif + if exists("g:cmake_usr_args") + let l:argument += [ g:cmake_usr_args ] + endif + endif + if exists("g:cmake_install_prefix") + let l:argument += [ "-DCMAKE_INSTALL_PREFIX:FILEPATH=" . g:cmake_install_prefix ] + endif + if exists("g:cmake_build_type") + let l:argument += [ "-DCMAKE_BUILD_TYPE:STRING=" . g:cmake_build_type ] + endif + if exists("g:cmake_build_shared_libs") + let l:argument += [ "-DBUILD_SHARED_LIBS:BOOL=" . g:cmake_build_shared_libs ] + endif + if exists("g:cmake_toolchain_file") + let l:argument += [ "-DCMAKE_TOOLCHAIN_FILE:FILEPATH=" . g:cmake_toolchain_file ] + endif + if g:cmake_export_compile_commands + let l:argument += [ "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" ] + endif + + let l:argumentstr = join(l:argument, " ") + let l:build_dir = fnamemodify(b:build_dir, ':p') + " Remove trailing slash before :h, otherwise fnamemodify treats + " the empty component after the slash as the last path element + " Remove trailing slash before :h - must handle both / and \ on Windows + let l:build_dir = substitute(l:build_dir, '[/\\]$', '', '') + let l:source_dir = fnamemodify(l:build_dir, ':h') + let l:escaped_build_dir = s:fnameescape(b:build_dir) + let s:cmd = 'cmake -S' . shellescape(l:source_dir) + \ . ' -B' . shellescape(l:build_dir) + \ . ' ' . l:argumentstr + \ . " " . join(a:cmake_vim_command_args) + + echo s:cmd + if exists(":AsyncRun") + execute 'copen' + execute 'AsyncRun ' . s:cmd + execute 'wincmd p' + else + silent let s:res = system(s:cmd) + silent echo s:res + endif + + " Create symbolic link to compilation database for use with YouCompleteMe + if g:cmake_ycm_symlinks && filereadable("compile_commands.json") + if has("win32") + exec "mklink" "../compile_commands.json" "compile_commands.json" + else + silent echo system("ln -s " . s:fnameescape(b:build_dir) . "/compile_commands.json ../compile_commands.json") + endif + echom "Created symlink to compilation database" + endif + exec 'cd -' + if has('win32') + let &shellslash = l:save_shellslash + endif +endfunction + +" ------------------------------------------------------------ +" Public API (cmake# namespace — called from plugin/cmake.vim) +" ------------------------------------------------------------ + +function! cmake#Configure(...) abort + if !s:find_build_dir() + return + endif + call s:cmake_configure(a:000) +endfunction + +function! cmake#CMakeBuild(...) abort + if !s:find_build_dir() + return + endif + echom 'vim-cmake: Using build directory: ' . b:build_dir + if !filereadable(b:build_dir . '/CMakeCache.txt') + echohl WarningMsg + echom 'vim-cmake: Project is not configured. Run :CMakeConfigure first.' + echohl None + return + endif + if g:cmake_use_smp && s:find_smp() + let l:smp = ' ' . shellescape(b:smp) + else + let l:smp = '' + endif + let $CMAKE_BUILD_DIR = b:build_dir + let &makeprg = 'sh -c ''cmake --build "$CMAKE_BUILD_DIR"' . l:smp + \ . ' ${1:+--target "$@"}'' sh' + execute 'make ' . join(a:000) + unlet $CMAKE_BUILD_DIR +endfunction + +function! cmake#FindBuildDir() abort + unlet! b:build_dir + call delete(s:dotfile_path()) + call s:find_build_dir() +endfunction + +function! cmake#SaveBuildDir() abort + if !exists('b:build_dir') || empty(b:build_dir) + return + endif + let l:dotfile = s:dotfile_path() + call writefile([b:build_dir], l:dotfile) +endfunction + +function! cmake#LoadBuildDir() abort + " Don't overwrite an already-set value + if exists('b:build_dir') && !empty(b:build_dir) + return + endif + let l:dotfile = s:dotfile_path() + if !filereadable(l:dotfile) + return + endif + let l:lines = readfile(l:dotfile) + if empty(l:lines) + return + endif + let l:candidate = expand(fnamemodify(l:lines[0], ':p')) + if isdirectory(l:candidate) + let b:build_dir = l:candidate + else + " Dotfile is stale - build dir no longer exists + call delete(l:dotfile) + endif +endfunction + +function! cmake#CleanBuildDir(bang) abort + if !exists('b:build_dir') || empty(b:build_dir) + echohl ErrorMsg + echom 'vim-cmake: No build directory set. Run :CMake or :CMakeFindBuildDir first.' + echohl None + return + endif + + let l:build_dir = fnamemodify(b:build_dir, ':p') + + if !isdirectory(l:build_dir) + echohl ErrorMsg + echom 'vim-cmake: Build directory does not exist: ' . l:build_dir + echohl None + return + endif + + " Guard against cleaning obviously dangerous paths + let l:home = fnamemodify('~', ':p') + let l:cwd = fnamemodify('.', ':p') + let l:root = '/' + if l:build_dir ==# l:home || l:build_dir ==# l:cwd || l:build_dir ==# l:root + echohl ErrorMsg + echom 'vim-cmake: Refusing to clean a root, home, or working directory: ' . l:build_dir + echohl None + return + endif + + " Require CMakeCache.txt as a sanity check + if !filereadable(l:build_dir . '/CMakeCache.txt') + echohl WarningMsg + echom 'vim-cmake: Directory does not look like a CMake build dir (no CMakeCache.txt): ' . l:build_dir + echohl None + if !a:bang + return + endif + echom 'vim-cmake: Proceeding anyway due to !-bang override.' + endif + + " Prompt unless bang + if !a:bang + let l:answer = input('vim-cmake: Clean all files in ' . l:build_dir . '? [y/N] ') + echo ' ' + if l:answer !~? '^y\(es\)\?$' + echom 'vim-cmake: Clean cancelled.' + return + endif + endif + + echom 'vim-cmake: Cleaning ' . l:build_dir . ' ...' + let l:errors = delete(l:build_dir, 'rf') + if l:errors != 0 + echohl ErrorMsg + echom 'vim-cmake: Clean failed (delete returned ' . l:errors . ').' + echohl None + else + echom 'vim-cmake: Done.' + unlet b:build_dir + call delete(s:dotfile_path()) + endif +endfunction + diff --git a/doc/cmake.txt b/doc/cmake.txt index b836e85..7bfa5a1 100644 --- a/doc/cmake.txt +++ b/doc/cmake.txt @@ -8,62 +8,170 @@ License: Same terms as Vim itself (see |license|) INTRODUCTION *cmake* -When working on a CMake project, this plugin provides a single command that -changes the makeprg make command's working directory to the closest 'build' -directory (see g:cmake_build_dir) upwards +When working on a CMake project, this plugin provides commands to configure +and build CMake projects without leaving Vim. The plugin automatically locates +the build directory (see |g:cmake_build_dir|), persists that location across +sessions, and wires up |'makeprg'| so that |:make| builds in the correct +directory. -COMMANDS *cmake-commands* +============================================================================== +COMMANDS *cmake-commands* *:CMakeBuild* -:CMake [args] Runs the cmake command as 'cmake ..', starting in +:CMakeBuild [target] [args] + Runs the cmake command as `'cmake ..'`, starting in first directory called 'build', found in an upwards search. All arguments are directly passed on to CMake. - Also modifies the :make command to build in + Also modifies the |':make'| command to build in that directory. -:CMakeClean deletes all files in the build directory. You can - think of this as a CMake version of make clean. - -:CMakeFindBuildDir resets the build directory path set for the current buffer - and then tries to find a new one. Useful if it previously - found a wrong path to then reset it after a new build folder - has been created for example. - -VARIABLES *cmake-variables* - -g:cmake_install_prefix same as -DCMAKE_INSTALL_PREFIX - -g:cmake_build_type same as -DCMAKE_BUILD_TYPE - -g:cmake_cxx_compiler same as -DCMAKE_CXX_COMPILER, however, this will have - no effect until you run :CMakeClean and :CMake. - -g:cmake_c_compiler same as -DCMAKE_C_COMPILER, however, this will have - no effect until you run :CMakeClean and :CMake. - -g:cmake_build_shared_libs same as -DBUILD_SHARED_LIBS - -g:cmake_toolchain_file same as -DCMAKE_TOOLCHAIN_FILE - -g:cmake_build_dir set the cmake 'build' directory, default: 'build' - -g:cmake_project_generator set project generator, however, this will have - no effect until you run :CMakeClean and :CMake. - -g:cmake_usr_args custom user arguments. Ex: 'let g:cmake_usr_args="-DDEBUG=YES"' - -b:build_dir the path to the cmake build directory for the current buffer. - This variable is set with the first :CMake or :CMakeFindBuildDir call. - Once found, it will not be searched for again unless you call - :CMakeFindBuildDir. If automatic finding is not sufficient you can set - this variable manually to the build dir of your choice. - -OPTIONS *cmake-options* - -g:cmake_export_compile_commands same as -DCMAKE_EXPORT_COMPILE_COMMANDS=ON, useful for - exporting a compilation database to be used with YCM - (https://github.com/Valloric/YouCompleteMe#c-family-semantic-completion) - CMake only supports this flag with Ninja and Makefile generators. - -g:cmake_ycm_symlinks create a symlink to the compile_commands.json file in the - root of the project (build/..) if the file is found. + This command will locate the build directory, configure + the project if it has not been configured yet + (no CMakeCache.txt found), then build [target] using + |:make|. + + The first parameter must be a target name and other + cmake parameters can follow the target in any order. + + Build directory discovery follows this order: + 1. Use |b:build_dir| if already set. + 2. Restore from the session cache (.vim-cmake). + 3. Search upward from the current file for a + directory named |g:cmake_build_dir|. + + Once found, the build directory is saved to the + session cache automatically. + + Configure is skipped when CMakeCache.txt already + exists. Use |:CMakeConfigure| to force a re-run. + + Tab completion is available for [target]: > + :CMake my_ +< + When invoked without arguments, the default build + target is used. + + *:CMakeConfigure* +:CMakeConfigure [args] Run cmake configuration in the build directory, + passing [args] directly to cmake. Use this to + explicitly re-run configuration, for example after + changing a compiler or build type: > + :CMakeConfigure -DCMAKE_BUILD_TYPE=Debug +< + Plugin global variables are translated into + cmake arguments automatically when set. See + |cmake-variables| for the full list. + + *:CMakeClean* +:CMakeClean! Delete all files in the build directory, equivalent + to a cmake clean. Prompts for confirmation before + proceeding. + + Add [!] to skip the confirmation prompt: > + :CMakeClean! +< + The command refuses to delete the build directory if: + - It does not contain a CMakeCache.txt (unless [!] + is given). + - It resolves to the home directory, current working + directory, or filesystem root. + + After a successful clean, |b:build_dir| is unset and + the session cache is cleared, so the next |:CMake| + call will re-run discovery and configuration. + + *:CMakeFindBuildDir* +:CMakeFindBuildDir Clear the current build directory path for the current + buffer and the session cache, then search for a new one. + Useful after creating a new build directory or when + the wrong directory was previously detected. + +============================================================================== +OPTIONS *cmake-options* + + *g:cmake_build_dir* +g:cmake_build_dir The directory name to search for when locating the + build directory. Searched upward from the current + file. Default: 'build'. > + let g:cmake_build_dir = 'build' +< + *b:build_dir* +b:build_dir The resolved path to the cmake build directory for + the current buffer. Set automatically by |:CMake| or + |:CMakeFindBuildDir|. Can be set manually to override + automatic discovery: > + let b:build_dir = '/path/to/my/build' +< + Once set it is not overwritten unless you call + |:CMakeFindBuildDir| or set it explicitly. + + *g:cmake_install_prefix* +g:cmake_install_prefix Passed to cmake as -DCMAKE_INSTALL_PREFIX. + + *g:cmake_build_type* +g:cmake_build_type Passed to cmake as -DCMAKE_BUILD_TYPE. > + let g:cmake_build_type = 'Debug' +< + *g:cmake_cxx_compiler* +g:cmake_cxx_compiler Passed to cmake as -DCMAKE_CXX_COMPILER. Changes + take effect after |:CMakeClean| and |:CMakeConfigure|. + + *g:cmake_c_compiler* +g:cmake_c_compiler Passed to cmake as -DCMAKE_C_COMPILER. Changes + take effect after |:CMakeClean| and |:CMakeConfigure|. + + *g:cmake_build_shared_libs* +g:cmake_build_shared_libs + Passed to cmake as -DBUILD_SHARED_LIBS. + + *g:cmake_toolchain_file* +g:cmake_toolchain_file Passed to cmake as -DCMAKE_TOOLCHAIN_FILE. + + *g:cmake_project_generator* +g:cmake_project_generator + Passed to cmake as -G. Changes take effect after + |:CMakeClean| and |:CMakeConfigure|. + + *g:cmake_export_compile_commands* +g:cmake_export_compile_commands + Passed to cmake as -DCMAKE_EXPORT_COMPILE_COMMANDS. + Default: 0. + + *g:cmake_ycm_symlinks* +g:cmake_ycm_symlinks When set to 1, creates a symlink to the generated + compile_commands.json in the project root for use + with YouCompleteMe or any LSP client that expects it + there. Default: 0. + + *g:cmake_use_smp* +g:cmake_use_smp When set to 1, passes -j{nproc} to the build command + to enable parallel builds. Requires `nproc` to be + available on PATH. Default: 0. + + *g:cmake_usr_args* +g:cmake_usr_args Arbitrary extra arguments passed to cmake during + configuration. Appended after all other arguments: > + let g:cmake_usr_args = '-DSOME_OPTION=ON' +< + +============================================================================== +SESSION CACHE *cmake-cache* + +The plugin persists |b:build_dir| across sessions using a dotfile named +.vim-cmake written to the project root (one level above the build directory). +This file is read automatically on |BufReadPost| and written on |BufWritePost|. + +The cache is cleared automatically by |:CMakeFindBuildDir| and |:CMakeClean|. +It is also invalidated silently if the recorded directory no longer exists. + +The .vim-cmake file is a per-developer local setting and should be added to +the project .gitignore: > + echo '.vim-cmake' >> .gitignore +< +============================================================================== +LICENSE *cmake-license* + +Copyright (c) Dirk Van Haerenborgh, @SteveDeFacto. Distributed under the +same terms as Vim itself. See |license|. + + vim:tw=78:ts=8:ft=help:norl: diff --git a/plugin/cmake.vim b/plugin/cmake.vim index 522d56b..1196adb 100644 --- a/plugin/cmake.vim +++ b/plugin/cmake.vim @@ -1,8 +1,8 @@ " cmake.vim - Vim plugin to make working with CMake a little nicer " Maintainer: Dirk Van Haerenborgh -" Version: 0.2 +" Version: 1.0 -let s:cmake_plugin_version = '0.2' +let s:cmake_plugin_version = '1.0' if exists("loaded_cmake_plugin") finish @@ -29,175 +29,26 @@ if !executable("cmake") finish endif -function! s:find_build_dir() - " Do not overwrite already found build_dir, may be set explicitly - " by user. - if exists("b:build_dir") && b:build_dir != "" - return 1 - endif - - let g:cmake_build_dir = get(g:, 'cmake_build_dir', 'build') - let b:build_dir = finddir(g:cmake_build_dir, ';') - - if b:build_dir == "" - " Find build directory in path of current file - let b:build_dir = finddir(g:cmake_build_dir, s:fnameescape(expand("%:p:h")) . ';') - endif - - if b:build_dir != "" - " expand() would expand "" to working directory, but we need - " this as an indicator that build was not found - let b:build_dir = fnamemodify(b:build_dir, ':p') - echom "Found cmake build directory: " . s:fnameescape(b:build_dir) - return 1 - else - echom "Unable to find cmake build directory." - return 0 - endif - -endfunction - -" Configure the cmake project in the currently set build dir. -" -" This will override any of the following variables if the -" corresponding vim variable is set: -" * CMAKE_INSTALL_PREFIX -" * CMAKE_BUILD_TYPE -" * CMAKE_BUILD_SHARED_LIBS -" If the project is not configured already, the following variables will be set -" whenever the corresponding vim variable for the following is set: -" * CMAKE_CXX_COMPILER -" * CMAKE_C_COMPILER -" * The generator (-G) -function! s:cmake_configure(cmake_vim_command_args) - exec 'cd' s:fnameescape(b:build_dir) - - let l:argument = [] - " Only change values of variables, if project is not configured - " already, otherwise we overwrite existing configuration. - let l:configured = filereadable("CMakeCache.txt") - - if !l:configured - if exists("g:cmake_project_generator") - let l:argument += [ "-G \"" . g:cmake_project_generator . "\"" ] - endif - if exists("g:cmake_cxx_compiler") - let l:argument += [ "-DCMAKE_CXX_COMPILER:FILEPATH=" . g:cmake_cxx_compiler ] - endif - if exists("g:cmake_c_compiler") - let l:argument += [ "-DCMAKE_C_COMPILER:FILEPATH=" . g:cmake_c_compiler ] - endif - - if exists("g:cmake_usr_args") - let l:argument+= [ g:cmake_usr_args ] - endif - endif - - if exists("g:cmake_install_prefix") - let l:argument += [ "-DCMAKE_INSTALL_PREFIX:FILEPATH=" . g:cmake_install_prefix ] - endif - if exists("g:cmake_build_type" ) - let l:argument += [ "-DCMAKE_BUILD_TYPE:STRING=" . g:cmake_build_type ] - endif - if exists("g:cmake_build_shared_libs") - let l:argument += [ "-DBUILD_SHARED_LIBS:BOOL=" . g:cmake_build_shared_libs ] - endif - if exists("g:cmake_toolchain_file") - let l:argument += [ "-DCMAKE_TOOLCHAIN_FILE:FILEPATH=" . g:cmake_toolchain_file ] - endif - if g:cmake_export_compile_commands - let l:argument += [ "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" ] - endif - - let l:argumentstr = join(l:argument, " ") - let l:escaped_build_dir=s:fnameescape(b:build_dir) - let l:home_dir = "-H".l:escaped_build_dir."/.." - let l:build_dir_path = "-B".l:escaped_build_dir - let s:cmd = 'cmake '.l:home_dir.' '.l:build_dir_path.' '.l:argumentstr . " " . join(a:cmake_vim_command_args) - - echo s:cmd - if exists(":AsyncRun") - execute 'copen' - execute 'AsyncRun ' . s:cmd - execute 'wincmd p' - else - silent let s:res = system(s:cmd) - silent echo s:res - endif - - " Create symbolic link to compilation database for use with YouCompleteMe - if g:cmake_ycm_symlinks && filereadable("compile_commands.json") - if has("win32") - exec "mklink" "../compile_commands.json" "compile_commands.json" - else - silent echo system("ln -s " . s:fnameescape(b:build_dir) ."/compile_commands.json ../compile_commands.json") - endif - echom "Created symlink to compilation database" - endif - - exec 'cd -' -endfunction - -" Utility function -" Thanks to tpope/vim-fugitive -function! s:fnameescape(file) abort - if exists('*fnameescape') - return fnameescape(a:file) - else - return escape(a:file," \t\n*?[{`$\\%#'\"|!<") - endif -endfunction - -function! s:find_smp() - if executable('nproc') - let l:nproc = system('nproc') - let b:smp = '-j' . substitute(l:nproc, '\n\+$', '', '') - return 1 - endif - return 0 -endfunction - -command! -complete=customlist,ListTargets -nargs=1 Make :make -function! ListTargets(A, L, C) - if !exists("b:build_dir") - return [] - endif - let all_targets = split(system("cmake --build ". b:build_dir . " --target help | awk ' NR > 1 {print $2}'"), '\n') - let targets = filter(all_targets, "v:val =~ '^" .. a:A .. "'") - return targets -endfunction - " Public Interface: -command! -nargs=? CMake call s:cmake() -command! CMakeClean call s:cmakeclean() -command! CMakeFindBuildDir call s:cmake_find_build_dir() - -function! s:cmake_find_build_dir() - unlet! b:build_dir - call s:find_build_dir() -endfunction - -function! s:cmake(...) - if !s:find_build_dir() - return - endif - - if g:cmake_use_smp && s:find_smp() - let l:smp = ' ' . shellescape(b:smp) - else - let l:smp = '' - endif - - let &makeprg = 'cmake --build ' . shellescape(b:build_dir) . l:smp . ' --target' - call s:cmake_configure(a:000) -endfunction - -function! s:cmakeclean() - if !s:find_build_dir() - return - endif - - silent echo system("rm -r '" . b:build_dir. "'/*") - echom "Build directory has been cleaned." -endfunction +command! -nargs=? -complete=customlist,s:list_targets CMakeBuild call cmake#CMakeBuild() +command! -nargs=? CMakeConfigure call cmake#Configure() +command! -bang CMakeClean call cmake#CleanBuildDir(0) +command! CMakeFindBuildDir call cmake#FindBuildDir() + +function! s:list_targets(A, L, C) abort + if !exists("b:build_dir") + return [] + endif + let l:all_targets = split( + \ system("cmake --build " . b:build_dir . " --target help | awk 'NR > 1 {print $2}'"), + \ '\n') + return filter(l:all_targets, "v:val =~ '^" . a:A . "'") +endfunction + +" Persist and restore b:build_dir across sessions +augroup vim_cmake_persist + autocmd! + autocmd BufReadPost * call cmake#LoadBuildDir() + autocmd BufWritePost * call cmake#SaveBuildDir() +augroup END diff --git a/test/.vimrc b/test/.vimrc index 4113900..5b80db3 100644 --- a/test/.vimrc +++ b/test/.vimrc @@ -1,7 +1,15 @@ filetype off - -set rtp+=vader.vim -set rtp+=. +set nocompatible +set noloadplugins +exec 'set rtp+=' . getcwd() . '/test/vader.vim' +exec 'set rtp+=' . getcwd() +runtime plugin/cmake.vim +runtime plugin/vader.vim filetype plugin indent on syntax enable +function! Normalize(path, ...) abort + let l:modifier = a:0 > 0 ? a:1 : ':p' + let l:fname = fnamemodify(resolve(a:path), l:modifier) + return expand(l:fname) +endfunction diff --git a/test/cmake.vader b/test/cmake.vader index a18ee77..7dc21d8 100644 --- a/test/cmake.vader +++ b/test/cmake.vader @@ -1,65 +1,240 @@ +" cmake.vader - Tests for vim-cmake +" Uses vader.vim: https://github.com/junegunn/vader.vim + +" ============================================================ +" Setup and teardown +" ============================================================ + Before: - " Ensure we are in the test directory - if isdirectory("test") - cd test + if !exists("test_dir") + let test_dir = globpath(&rtp, 'test/cmake.vader') + let test_dir = fnamemodify(test_dir, ':p:h') endif + exec "cd" fnameescape(test_dir) - if !exists("test_dir") - let test_dir = fnamemodify(getcwd(), ':p') + " Clean unit test scratch space + if exists("g:unit_test_dir") + call delete(g:unit_test_dir, 'rf') endif + let g:unit_test_dir = tempname() + call mkdir(g:unit_test_dir . '/build', 'p') + call writefile([''], g:unit_test_dir . '/build/CMakeCache.txt') - Assert !isdirectory("test project/tmp-build"), "TEST ERROR: build directory was not properly deleted" - echo system("mkdir 'test project/tmp-build'") - Assert isdirectory("test project/tmp-build"), "TEST ERROR: build directory was not created" + " Clean integration test build directory + Assert !isdirectory("test project/tmp-build"), + \ "TEST ERROR: build directory was not properly deleted" + call mkdir('test project/tmp-build', 'p') + Assert isdirectory("test project/tmp-build"), + \ "TEST ERROR: build directory was not created" - " Under travis CI the entire project is in a build/ directory + " Under Travis CI the entire project is in a build/ directory " which will make the search from cwd always return a result. " To be able to test searching build dir from current file, the " build dir needs to be named differently as a workaround. let g:cmake_build_dir = "tmp-build" + + " Reset plugin state + unlet! b:build_dir + + " Reset to defaults rather than unlet - autoload code references these directly + let g:cmake_export_compile_commands = 0 + let g:cmake_ycm_symlinks = 0 + let g:cmake_use_smp = 0 + After: + " Restore integration test environment exec "cd" fnameescape(test_dir) - echo system("rm -rf 'test project/tmp-build'") - echo system("rm -f 'test project/compile_commands.json'") + call delete('test project/tmp-build', 'rf') + call delete('test project/compile_commands.json') + call delete('test project/.vim-cmake') -Execute (Find build directory from working dir): - cd test\ project - CMake + " Tear down unit test scratch space + if exists("g:unit_test_dir") + call delete(g:unit_test_dir, 'rf') + unlet g:unit_test_dir + endif + + " Reset plugin state + unlet! b:build_dir + unlet! g:cmake_build_dir + " Reset to defaults rather than unlet - autoload code references these directly + let g:cmake_export_compile_commands = 0 + let g:cmake_ycm_symlinks = 0 + let g:cmake_use_smp = 0 + +" ============================================================ +" Unit tests: build directory discovery +" ============================================================ + +Execute (FindBuildDir locates build/ upward from current file): + let saved_cwd = getcwd() + exec 'cd' g:unit_test_dir + let g:cmake_build_dir = 'build' + call cmake#FindBuildDir() +Then: + AssertEqual Normalize(g:unit_test_dir . '/build/'), b:build_dir + exec 'cd' saved_cwd + +Execute (FindBuildDir resets and rediscovers on second call): + let saved_cwd = getcwd() + exec 'cd' g:unit_test_dir + let g:cmake_build_dir = 'build' + let b:build_dir = '/some/wrong/path' + call cmake#FindBuildDir() +Then: + AssertEqual Normalize(g:unit_test_dir . '/build/'), b:build_dir + exec 'cd' saved_cwd + +" ============================================================ +" Unit tests: session cache +" ============================================================ + +Execute (SaveBuildDir writes dotfile): + let b:build_dir = g:unit_test_dir . '/build/' + call cmake#SaveBuildDir() +Then: + Assert filereadable(g:unit_test_dir . '/.vim-cmake'), + \ 'Expected .vim-cmake to exist after SaveBuildDir' + +Execute (LoadBuildDir restores from dotfile): + execute 'edit ' . g:unit_test_dir . '/CMakeLists.txt' + let b:build_dir = g:unit_test_dir . '/build/' + call cmake#SaveBuildDir() + unlet b:build_dir + call cmake#LoadBuildDir() +Then: + Assert exists('b:build_dir'), 'Expected b:build_dir to be set' + AssertEqual Normalize(g:unit_test_dir . '/build/'), b:build_dir + bwipeout! - Assert filereadable("tmp-build/CMakeCache.txt"), "CMakeCache.txt should be generated" - Assert !filereadable("tmp-build/compile_commands.json"), "Compile commands should not be exported by default" +Execute (LoadBuildDir ignores stale dotfile and deletes it): + execute 'edit ' . g:unit_test_dir . '/CMakeLists.txt' + let b:build_dir = g:unit_test_dir . '/build/' + let g:cache_file = g:unit_test_dir . '/.vim-cmake' + call cmake#SaveBuildDir() + if exists("b:build_dir") + call delete(b:build_dir, 'rf') + unlet b:build_dir + endif + call cmake#LoadBuildDir() +Then: + Assert !exists('b:build_dir'), + \ 'Expected b:build_dir to be unset for stale cache' + Assert !filereadable(g:cache_file), + \ 'Expected stale session cache to be deleted' + bwipeout! + +Execute (LoadBuildDir does not overwrite existing b:build_dir): + let b:build_dir = Normalize(g:unit_test_dir . '/build/') + call writefile(['/some/other/path/'], g:unit_test_dir . '/.vim-cmake') + call cmake#LoadBuildDir() +Then: + AssertEqual Normalize(g:unit_test_dir . '/build/'), b:build_dir, + \ 'Expected existing b:build_dir to be preserved' + +Execute (FindBuildDir clears session cache before rediscovery): + let saved_cwd = getcwd() + execute 'cd' g:unit_test_dir + let g:cmake_build_dir = 'build' + let g:cache_file = Normalize(g:unit_test_dir . '/.vim-cmake', ':p') + call writefile(['/some/wrong/path/'], g:cache_file) + call cmake#FindBuildDir() +Then: + AssertEqual Normalize(g:unit_test_dir . '/build/'), b:build_dir, + \ 'Expected fresh discovery to ignore stale session cache' + execute 'cd' saved_cwd + +" ============================================================ +" Unit tests: CMakeClean safety checks +" ============================================================ + +Execute (CMakeClean refuses when no build dir is set): + unlet! b:build_dir + redir => g:output + call cmake#CleanBuildDir(1) + redir END +Then: + Assert g:output =~# 'No build directory set', + \ 'Expected error about missing build dir' + +Execute (CMakeClean refuses directory without CMakeCache.txt): + let b:build_dir = g:unit_test_dir . '/build/' + call delete(g:unit_test_dir . '/build/CMakeCache.txt') + redir => g:output + call cmake#CleanBuildDir(0) + redir END +Then: + Assert g:output =~# 'does not look like a CMake build dir', + \ 'Expected warning about missing CMakeCache.txt' + Assert isdirectory(g:unit_test_dir . '/build/'), + \ 'Expected build dir to survive refused clean' + +Execute (CMakeClean with bang deletes build dir): + let b:build_dir = g:unit_test_dir . '/build/' + call cmake#CleanBuildDir(1) +Then: + Assert !isdirectory(g:unit_test_dir . '/build/'), + \ 'Expected build dir to be deleted' + Assert !exists('b:build_dir'), + \ 'Expected b:build_dir to be unset after clean' + Assert !filereadable(g:unit_test_dir . '/.vim-cmake'), + \ 'Expected .vim-cmake to be deleted after clean' + +Execute (CMakeClean refuses to delete home directory): + let b:build_dir = fnamemodify('~', ':p') + redir => g:output + call cmake#CleanBuildDir(1) + redir END +Then: + Assert g:output =~# 'Refusing to clean', + \ 'Expected refusal for home directory' + +" ============================================================ +" Integration tests: CMake configure and build +" ============================================================ + +Execute (Find build directory from working dir): + exec "cd" "test project" + CMakeConfigure + Assert filereadable("tmp-build/CMakeCache.txt"), + \ "CMakeCache.txt should be generated" Execute (Find build directory from currently open file): e test\ project/CMakeLists.txt - CMake - Assert filereadable("test project/tmp-build/CMakeCache.txt"), "CMakeCache.txt should be generated" + CMakeConfigure + Assert filereadable("test project/tmp-build/CMakeCache.txt"), + \ "CMakeCache.txt should be generated" + Assert !filereadable("tmp-build/compile_commands.json"), + \ "Compile commands should not be exported by default" Execute (Create symlink to compilation database): let g:cmake_export_compile_commands = 1 let g:cmake_ycm_symlinks = 1 cd test\ project - CMake - + CMakeConfigure " Exporting compile commands does not work with Visual Studio generator if !has("win32") && !has("win32unix") - Assert filereadable("tmp-build/compile_commands.json"), "Compile commands should be exported" - Assert filereadable(resolve("compile_commands.json")), "A symlink should be generated" + Assert filereadable("tmp-build/compile_commands.json"), + \ "Compile commands should be exported" + Assert filereadable(resolve("compile_commands.json")), + \ "A symlink should be generated" + else + Log "Export compile_commands skipped on windows." endif Execute (Open already configured cmake project): cd test\ project/tmp-build silent !cmake .. -DWITH_BYE=ON e ../CMakeLists.txt - CMake - silent make - + CMakeBuild + make enew - if has("win32") || has("win32unix") - read !Debug/hello.exe - else - read !./hello - endif + let exe = executable('Debug/hello.exe') ? 'Debug/hello.exe' : './hello' + let exe = fnamemodify(exe, ':p') + let lines = systemlist(shellescape(exe)) + call map(lines, {_, v -> substitute(v, '\r$', '', '')}) + call setline(1, lines) Expect: - Hello World Bye World + diff --git a/test/test project/CMakeLists.txt b/test/test project/CMakeLists.txt index 82ccdff..b25b37a 100644 --- a/test/test project/CMakeLists.txt +++ b/test/test project/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 2.8.12) +cmake_minimum_required(VERSION 3.15) project(HelloWorld) From c471d03a883a33ce33386a79961478cfb0307bbb Mon Sep 17 00:00:00 2001 From: Dave Parillo Date: Sun, 8 Mar 2026 21:14:33 -0700 Subject: [PATCH 2/2] fix docs --- doc/cmake.txt | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/doc/cmake.txt b/doc/cmake.txt index 7bfa5a1..bd0889d 100644 --- a/doc/cmake.txt +++ b/doc/cmake.txt @@ -18,31 +18,19 @@ directory. COMMANDS *cmake-commands* *:CMakeBuild* :CMakeBuild [target] [args] - Runs the cmake command as `'cmake ..'`, starting in - first directory called 'build', found in an upwards - search. All arguments are directly passed on to CMake. - Also modifies the |':make'| command to build in - that directory. + CMakeBuild searches for the configured build directory + and if found updates the |:makeprg| option and then + builds the [target] using |:make|. - This command will locate the build directory, configure - the project if it has not been configured yet - (no CMakeCache.txt found), then build [target] using - |:make|. + All arguments after the first are directly passed on + to CMake. The first parameter must be a target name and other cmake parameters can follow the target in any order. - Build directory discovery follows this order: - 1. Use |b:build_dir| if already set. - 2. Restore from the session cache (.vim-cmake). - 3. Search upward from the current file for a - directory named |g:cmake_build_dir|. - - Once found, the build directory is saved to the - session cache automatically. - - Configure is skipped when CMakeCache.txt already - exists. Use |:CMakeConfigure| to force a re-run. + Use |:CMakeConfigure| to find a build directory, + generate toolchain files, and update the + *cmake-cache* file. Tab completion is available for [target]: > :CMake my_ @@ -52,7 +40,20 @@ COMMANDS *cmake-commands* *:CMakeBuild* *:CMakeConfigure* :CMakeConfigure [args] Run cmake configuration in the build directory, - passing [args] directly to cmake. Use this to + passing [args] directly to cmake. + CMakeConfigure searches for the build directory + before writing configuration files there + + Build directory discovery follows this order: + 1. Use |b:build_dir| if already set. + 2. Restore from the *cmake-cache* (.vim-cmake). + 3. Search upward from the current file for a + directory named |g:cmake_build_dir|. + + Once found, the build directory is saved to the + *cmake-cache* automatically. + + Use this to explicitly re-run configuration, for example after changing a compiler or build type: > :CMakeConfigure -DCMAKE_BUILD_TYPE=Debug