├── LICENSE ├── README.md ├── autoload ├── packager.vim └── packager │ ├── job.vim │ ├── plugin.vim │ └── utils.vim ├── lua └── packager.lua └── plugin └── packager.vim /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kristijan Husak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vim packager 2 | 3 | ![preview-gif](https://i.imgur.com/KOTn843.gif) 4 | 5 | This is Yet Another plugin manager for Vim/Neovim. It's written in pure vimscript and utilizes [jobs](https://neovim.io/doc/user/job_control.html) and [pack](https://neovim.io/doc/user/repeat.html#packages) features. 6 | 7 | Tested with: 8 | * Neovim 0.3.2 - Linux, MacOS and Windows 10 9 | * Vim 8.0 - Linux and Windows 10 10 | 11 | ## Why? 12 | There's a lot of plugin managers for vim out there. 13 | 14 | Most popular one is definitely [vim-plug](https://github.com/junegunn/vim-plug). It's a great fully featured plugin manager. 15 | One thing that it does different is managing `runtimepath` manually. In latest Vim (and Neovim), packages can be added to `runtimepath` automatically by vim, just by placing the plugins in the right folder. 16 | This also has one more advantage: You can use (load) plugin manager only when you need it. 17 | 18 | One plugin manager that utilizes the same features as this one is [minpac](https://github.com/k-takata/minpac), 19 | which I used for some time, and which inspired me to write this one (Many thanks to @k-takata). 20 | In minpac, I missed having the window which shows the process and information about 21 | all the plugins while they are being installed/updated/previewed. 22 | I contributed and added a status window, but it still has a bit bad looking install/update process (echoing information to command line). 23 | You can easily loose track what's happening in the process, and echoing causes a lot "Press enter to continue." messages, which blocks the process. 24 | 25 | `Packager` utilizes jobs feature to the maximum, and runs everything that it can in a job, and shows whole process in the separate window, in a very similar way that vim-plug does. 26 | 27 | ## Requirement 28 | * Neovim 0.20+ OR Vim 8.0.0902+ 29 | * Git 30 | * Windows, Linux, macOS 31 | 32 | ## Installation 33 | ### Mac/Linux 34 | #### Vim 35 | ```sh 36 | git clone https://github.com/kristijanhusak/vim-packager ~/.vim/pack/packager/opt/vim-packager 37 | ``` 38 | 39 | #### Neovim 40 | ```sh 41 | git clone https://github.com/kristijanhusak/vim-packager ~/.config/nvim/pack/packager/opt/vim-packager 42 | ``` 43 | 44 | ### Windows 45 | #### Vim 46 | ```sh 47 | git clone https://github.com/kristijanhusak/vim-packager ~/vimfiles/pack/packager/opt/vim-packager 48 | ``` 49 | 50 | #### Neovim 51 | ```sh 52 | git clone https://github.com/kristijanhusak/vim-packager ~/AppData/Local/nvim/pack/packager/opt/vim-packager 53 | ``` 54 | 55 | #### Example .vimrc content 56 | Using `setup` function. 57 | 58 | ```vim 59 | if &compatible 60 | set nocompatible 61 | endif 62 | 63 | function! s:packager_init(packager) abort 64 | call a:packager.add('kristijanhusak/vim-packager', { 'type': 'opt' }) 65 | call a:packager.add('junegunn/fzf', { 'do': './install --all && ln -s $(pwd) ~/.fzf'}) 66 | call a:packager.add('junegunn/fzf.vim') 67 | call a:packager.add('vimwiki/vimwiki', { 'type': 'opt' }) 68 | call a:packager.add('Shougo/deoplete.nvim') 69 | call a:packager.add('autozimu/LanguageClient-neovim', { 'do': 'bash install.sh' }) 70 | call a:packager.add('morhetz/gruvbox') 71 | call a:packager.add('lewis6991/gitsigns.nvim', {'requires': 'nvim-lua/plenary.nvim'}) 72 | call a:packager.add('haorenW1025/completion-nvim', {'requires': [ 73 | \ ['nvim-treesitter/completion-treesitter', {'requires': 'nvim-treesitter/nvim-treesitter'}], 74 | \ {'name': 'steelsojka/completion-buffers', 'opts': {'type': 'opt'}}, 75 | \ 'kristijanhusak/completion-tags', 76 | \ ]}) 77 | call a:packager.add('hrsh7th/vim-vsnip-integ', {'requires': ['hrsh7th/vim-vsnip'] }) 78 | call a:packager.local('~/my_vim_plugins/my_awesome_plugin') 79 | 80 | "Provide full URL; useful if you want to clone from somewhere else than Github. 81 | call a:packager.add('https://my.other.public.git/tpope/vim-fugitive.git') 82 | 83 | "Provide SSH-based URL; useful if you have write access to a repository and wish to push to it 84 | call a:packager.add('git@github.com:mygithubid/myrepo.git') 85 | 86 | "Loaded only for specific filetypes on demand. Requires autocommands below. 87 | call a:packager.add('kristijanhusak/vim-js-file-import', { 'do': 'npm install', 'type': 'opt' }) 88 | call a:packager.add('fatih/vim-go', { 'do': ':GoInstallBinaries', 'type': 'opt' }) 89 | call a:packager.add('neoclide/coc.nvim', { 'do': function('InstallCoc') }) 90 | call a:packager.add('sonph/onehalf', {'rtp': 'vim/'}) 91 | endfunction 92 | 93 | packadd vim-packager 94 | call packager#setup(function('s:packager_init')) 95 | ``` 96 | and run `PackagerInstall` or `PackagerUpdate`. See all available commands [here](#commands) 97 | 98 | Or doing the old way that allows more control. 99 | 100 | ```vim 101 | if &compatible 102 | set nocompatible 103 | endif 104 | 105 | " Load packager only when you need it 106 | function! PackagerInit() abort 107 | packadd vim-packager 108 | call packager#init() 109 | call packager#add('kristijanhusak/vim-packager', { 'type': 'opt' }) 110 | call packager#add('junegunn/fzf', { 'do': './install --all && ln -s $(pwd) ~/.fzf'}) 111 | call packager#add('junegunn/fzf.vim') 112 | call packager#add('vimwiki/vimwiki', { 'type': 'opt' }) 113 | call packager#add('Shougo/deoplete.nvim') 114 | call packager#add('autozimu/LanguageClient-neovim', { 'do': 'bash install.sh' }) 115 | call packager#add('morhetz/gruvbox') 116 | call packager#add('lewis6991/gitsigns.nvim', {'requires': 'nvim-lua/plenary.nvim'}) 117 | call packager#add('haorenW1025/completion-nvim', {'requires': [ 118 | \ ['nvim-treesitter/completion-treesitter', {'requires': 'nvim-treesitter/nvim-treesitter'}], 119 | \ {'name': 'steelsojka/completion-buffers', 'opts': {'type': 'opt'}}, 120 | \ 'kristijanhusak/completion-tags', 121 | \ ]}) 122 | call packager#add('hrsh7th/vim-vsnip-integ', {'requires': ['hrsh7th/vim-vsnip'] }) 123 | call packager#local('~/my_vim_plugins/my_awesome_plugin') 124 | 125 | "Provide full URL; useful if you want to clone from somewhere else than Github. 126 | call packager#add('https://my.other.public.git/tpope/vim-fugitive.git') 127 | 128 | "Provide SSH-based URL; useful if you have write access to a repository and wish to push to it 129 | call packager#add('git@github.com:mygithubid/myrepo.git') 130 | 131 | "Loaded only for specific filetypes on demand. Requires autocommands below. 132 | call packager#add('kristijanhusak/vim-js-file-import', { 'do': 'npm install', 'type': 'opt' }) 133 | call packager#add('fatih/vim-go', { 'do': ':GoInstallBinaries', 'type': 'opt' }) 134 | call packager#add('neoclide/coc.nvim', { 'do': function('InstallCoc') }) 135 | call packager#add('sonph/onehalf', {'rtp': 'vim/'}) 136 | endfunction 137 | 138 | function! InstallCoc(plugin) abort 139 | exe '!cd '.a:plugin.dir.' && yarn install' 140 | call coc#add_extension('coc-eslint', 'coc-tsserver', 'coc-pyls') 141 | endfunction 142 | 143 | " These commands are automatically added when using `packager#setup()` 144 | command! -nargs=* -bar PackagerInstall call PackagerInit() | call packager#install() 145 | command! -nargs=* -bar PackagerUpdate call PackagerInit() | call packager#update() 146 | command! -bar PackagerClean call PackagerInit() | call packager#clean() 147 | command! -bar PackagerStatus call PackagerInit() | call packager#status() 148 | 149 | "Load plugins only for specific filetype 150 | "Note that this should not be done for plugins that handle their loading using ftplugin file. 151 | "More info in :help pack-add 152 | augroup packager_filetype 153 | autocmd! 154 | autocmd FileType javascript packadd vim-js-file-import 155 | autocmd FileType go packadd vim-go 156 | augroup END 157 | 158 | "Lazy load plugins with a mapping 159 | nnoremap ww :unmap wwpackadd vimwikiVimwikiIndex 160 | ``` 161 | 162 | After that, reload vimrc, and run `:PackagerInstall`. It will install all the plugins and run it's hooks. 163 | 164 | If some plugin installation (or it's hook) fail, you will get (as much as possible) descriptive error on the plugin line. 165 | To view more, press `E` on the plugin line to view whole stdout. 166 | 167 | ### Neovim Lua support 168 | There is some basic Lua support for latest Neovim (0.5.0). Here's short example: 169 | ```lua 170 | vim.cmd [[packadd vim-packager]] 171 | require('packager').setup(function(packager) 172 | packager.add('kristijanhusak/vim-packager', { type = 'opt' }) 173 | packager.add('junegunn/fzf', { ['do'] = './install --all && ln -s $(pwd) ~/.fzf'}) 174 | packager.add('junegunn/fzf.vim') 175 | packager.add('vimwiki/vimwiki', { type = 'opt' }) 176 | packager.add('Shougo/deoplete.nvim') 177 | packager.add('autozimu/LanguageClient-neovim', { ['do'] = 'bash install.sh' }) 178 | packager.add('morhetz/gruvbox') 179 | packager.add('lewis6991/gitsigns.nvim', {requires = 'nvim-lua/plenary.nvim'}) 180 | packager.add('haorenW1025/completion-nvim', {requires = { 181 | {'nvim-treesitter/completion-treesitter', {requires = 'nvim-treesitter/nvim-treesitter'}}, 182 | {name = 'steelsojka/completion-buffers', opts = {type = 'opt'}}, 183 | 'kristijanhusak/completion-tags', 184 | }}) 185 | packager.add('hrsh7th/vim-vsnip-integ', {requires = {'hrsh7th/vim-vsnip'} }) 186 | packager['local']('~/my_vim_plugins/my_awesome_plugin') 187 | 188 | --Provide full URL; useful if you want to clone from somewhere else than Github. 189 | packager.add('https://my.other.public.git/tpope/vim-fugitive.git') 190 | 191 | --Provide SSH-based URL; useful if you have write access to a repository and wish to push to it 192 | packager.add('git@github.com:mygithubid/myrepo.git') 193 | 194 | packager.add('kristijanhusak/vim-js-file-import', { ['do'] = 'npm install', type = 'opt' }) 195 | packager.add('fatih/vim-go', { ['do'] = ':GoInstallBinaries', type = 'opt' }) 196 | packager.add('neoclide/coc.nvim', {branch = 'master', ['do'] = function(plugin) 197 | vim.loop.spawn('yarn', { 198 | args = {'install'}, 199 | cwd = plugin.dir, 200 | }) 201 | end}) 202 | packager.add('sonph/onehalf', {rtp = 'vim/'}) 203 | end) 204 | ``` 205 | and run `PackagerInstall` or `PackagerUpdate`. See all available commands [here](#commands) 206 | 207 | ### Functions 208 | 209 | #### packager#setup(callback_function, opts) 210 | This is a small wrapper around functions explained below. It does this: 211 | 1. Adds all necessary commands. `PackagerInstall`, `PackagerUpdate`, `PackagerClean` and `PackagerStatus` 212 | 2. Running any of the command does this: 213 | * calls `packager#init(opts)` 214 | * calls provided `callback_function` with `packager` instance 215 | * calls proper function for the command 216 | 217 | #### packager#init(options) 218 | 219 | Available options: 220 | 221 | * `depth` - `--depth` value to use when cloning. Default: `5` 222 | * `jobs` - Maximum number of jobs that can run at same time. `0` is treated as unlimited. Default: `8` 223 | * `dir` - Directory to use for installation. By default uses `&packpath` value, which is `~/.vim/pack/packager` in Vim, and `~/.config/nvim/pack/packager` in Neovim. 224 | * `window_cmd` - What command to use to open packager window. Default: `vertical topleft new` 225 | * `default_plugin_type` - Default `type` option for plugins where it's not provided. More info below in `packager#add` options. Default: `start` 226 | * `disable_default_mappings` - Disable all default mappings for packager buffer. Default: `0` 227 | 228 | #### packager#add(name, options) 229 | 230 | `name` - Url to the git directory, or only last part of it to use `github`. 231 | 232 | Example: for github repositories, `kristijanhusak/vim-packager` is enough, for something else, like `bitbucket`, use full path `https://bitbucket.org/owner/package` 233 | 234 | Options: 235 | * `name` - Custom name of the plugin. If ommited, last part of url explained above is taken (example: `vim-packager`, in `kristijanhusak/vim-packager`) 236 | * `type` - In which folder to install the plugin. Plugins that are loaded on demand (with `packadd`), goes to `opt` directory, 237 | where plugins that are auto loaded goes to `start` folder. Default: `start` 238 | * `branch` - git branch to use. Default: '' (Uses the default from the repository, usually master) 239 | * `tag` - git tag to use. Default: '' 240 | * `rtp` - Used in case when subdirectory contains vim plugin. Creates a symbolink link from subdirectory to the packager folder. 241 | If `type` of package is `opt` use `packadd {packagename}__{rtp}` to load it (example: `packadd onehalf__vim`) 242 | * `commit` - exact git commit to use. Default: '' (Check below for priority explanation) 243 | * `do` - Hook to run after plugin is installed/updated: Default: ''. Examples below. 244 | * `frozen` - When plugin is frozen, it is not being updated. Default: 0 245 | * `requires` - Dependencies for the plugin. Can be 246 | * *string* (ex. `'kristijanhusak/vim-packager'`) 247 | * *list* (ex. `['kristijanhusak/vim-packager', {'type': 'opt'}]`) 248 | * *dict* (ex. `{'name': 'kristijanhusak/vim-packager', 'opts': {'type': 'opt'} }`). 249 | See example vimrc above. 250 | 251 | `branch`, `tag` and `commit` options go in certain priority: 252 | * `commit` 253 | * `tag` 254 | * `branch` 255 | 256 | Hooks can be defined in 3 ways: 257 | 1. As a string that **doesn't** start with `:`. This runs the command as it is a shell command, in the plugin directory. Example: 258 | ```vimL 259 | call packager#add('junegunn/fzf', { 'do': './install --all'}) 260 | call packager#add('kristijanhusak/vim-js-file-import', { 'do': 'npm install' }) 261 | ``` 262 | 2. As a string that starts with `:`. This executes the hook as a vim command. Example: 263 | ```vimL 264 | call packager#add('fatih/vim-go', { 'do': ':GoInstallBinaries' }) 265 | call packager#add('iamcco/markdown-preview.nvim' , { 'do': ':call mkdp#util#install()' }) 266 | ``` 267 | 268 | 3. As a `funcref` that gets the plugin info as an argument. Example: 269 | ```vimL 270 | call packager#add('iamcco/markdown-preview.nvim' , { 'do': { -> mkdp#util#install() } }) 271 | call packager#add('junegunn/fzf', { 'do': function('InstallFzf') }) 272 | 273 | function! InstallFzf(plugin) abort 274 | exe a:plugin.dir.'/install.sh --all' 275 | endfunction 276 | ``` 277 | 278 | #### packager#local(name, options) 279 | **Note**: This function only creates a symbolic link from provided path to the packager folder 280 | 281 | `name` - Full path to the local folder 282 | Example: `~/my_plugins/my_awesome_plugin` 283 | 284 | Options: 285 | * `name` - Custom name of the plugin. If ommited, last part of path is taken (example: `my_awesome_plugin`, in `~/my_plugins/my_awesome_plugin`) 286 | * `type` - In which folder to install the plugin. Plugins that are loaded on demand (with `packadd`), goes to `opt` directory, 287 | where plugins that are auto loaded goes to `start` folder. Default: `start` 288 | * `do` - Hook to run after plugin is installed/updated: Default: '' 289 | * `frozen` - When plugin is frozen, it is not being updated. Default: 0 290 | 291 | #### packager#install(opts) 292 | 293 | This only installs plugins that are not installed 294 | 295 | Available options: 296 | 297 | * `on_finish` - Run command after installation finishes. For example to quit at the end: `call packager#install({ 'on_finish': 'quitall' })` 298 | * `plugins` - Array of plugin names to install. Example: `call packager#install({'plugins': ['gruvbox', 'gitsigns.nvim']})` 299 | 300 | When installation finishes, there are two mappings that can be used: 301 | 302 | * `D` - Switches view from installation to status. This prints all plugins, and it's status (Installed, Updated, list of commits that were pulled with latest update) 303 | * `E` - View stdout of the plugin on the current line. If something errored (From installation or post hook), it's printed in the preview window. 304 | 305 | #### packager#update(opts) 306 | 307 | This installs plugins that are not installed, and updates existing one to the latest (If it's not marked as frozen) 308 | 309 | Available options: 310 | 311 | * `on_finish` - Run command after update finishes. For example to quit at the end: `call packager#update({ 'on_finish': 'quitall' })` 312 | * `force_hooks` - Force running post hooks for each package even if up to date. Useful when some hooks previously failed. Must be non-empty value: `call packager#update({ 'force_hooks': 1 })` 313 | * `plugins` - Array of plugin names to update. Example: `call packager#update({'plugins': ['gruvbox', 'gitsigns.nvim']})` 314 | 315 | When update finishes, there are two mappings that can be used: 316 | 317 | * `D` - Switches view from installation to status. This prints all plugins, and it's status (Installed, Updated, list of commits that were pulled with latest update) 318 | * `E` - View stdout of the plugin on the current line. If something errored (From installation or post hook), it's printed in the preview window. 319 | 320 | #### packager#status() 321 | 322 | This shows the status for each plugin added from vimrc. 323 | 324 | You can come to this view from Install/Update screens by pressing `D`. 325 | 326 | Each plugin can have several states: 327 | 328 | * `Not installed` - Plugin directory does not exist. If something failed during the clone process, shows the error message that can be previewed with `E` 329 | * `Install/update failed` - Something went wrong during installation/updating of the plugin. Press `E` on the plugin line to view stdout of the process. 330 | * `Post hook failed` - Something went wrong with post hook. Press `E` on the plugin line to view stdout of the process. 331 | * `OK` - Plugin is properly installed and it doesn't have any update information. 332 | * `Updated` - Plugin has some information about the last update. 333 | 334 | #### packager#clean() 335 | 336 | This removes unused plugins. It will ask for confirmation before proceeding. 337 | Confirmation allows selecting option to delete all folders from the list (default action), 338 | or ask for each folder if you want to delete it. 339 | 340 | ### Commands 341 | Commands are added only when using `packager#setup` or Lua `require('packager').setup()` 342 | 343 | * PackagerInstall - same as [packager#install(``)](https://github.com/kristijanhusak/vim-packager#packagerinstallopts). 344 | * PackagerUpdate - same as [packager#update(``)](https://github.com/kristijanhusak/vim-packager#packagerupdateopts). Note that args are passed as they are written. 345 | For example, to force running hooks you would do `:PackagerUpdate {'force_hooks': 1}` 346 | * PackagerClean - same as [packager#clean()](https://github.com/kristijanhusak/vim-packager#packagerclean) 347 | * PackagerStatus - same as [packager#status()](https://github.com/kristijanhusak/vim-packager#packagerstatus) 348 | 349 | ## Configuration 350 | Several buffer mappings are added for packager buffer by default: 351 | 352 | * `q` - Close packager buffer (`(PackagerQuit)`) 353 | * `` - Preview commit under cursor (`(PackagerOpenSha)`) 354 | * `E` - Preview stdout of the installation process of plugin under cursor (`(PackagerOpenStdout)`) 355 | * `` - Jump to next plugin (`(PackagerGotoNextPlugin)`) 356 | * `` - Jump to previous plugin (`(PackagerGotoPrevPlugin)`) 357 | * `D` - Go to status page (`(PackagerStatus)`) 358 | * `O` - Open details of plugin under cursor (`(PackagerPluginDetails)`) 359 | 360 | To use different mapping for any of these, create filetype autocmd with different mapping. 361 | 362 | For example, to use `` instead of `` for jumping to next plugin, add this to vimrc: 363 | 364 | ``` 365 | autocmd FileType packager nmap (PackagerGotoNextPlugin) 366 | ``` 367 | 368 | ## Thanks to: 369 | 370 | * [@k-takata](https://github.com/k-takata) and his [minpac](https://github.com/k-takata/minpac) plugin for inspiration and parts of the code 371 | -------------------------------------------------------------------------------- /autoload/packager.vim: -------------------------------------------------------------------------------- 1 | scriptencoding utf8 2 | let s:packager = {} 3 | let s:slash = exists('+shellslash') && !&shellslash ? '\' : '/' 4 | let s:has_timers = has('timers') 5 | let s:defaults = { 6 | \ 'dir': printf('%s%s%s', substitute(split(&packpath, ',')[0], '\(\\\|\/\)', s:slash, 'g'), s:slash, 'pack'.s:slash.'packager'), 7 | \ 'depth': 5, 8 | \ 'jobs': 8, 9 | \ 'window_cmd': 'vertical topleft new', 10 | \ 'default_plugin_type': 'start', 11 | \ 'disable_default_mappings': 0, 12 | \ } 13 | 14 | function! packager#new(opts) abort 15 | return s:packager.new(a:opts) 16 | endfunction 17 | 18 | function! s:packager.new(opts) abort 19 | call packager#utils#check_support() 20 | let l:instance = extend(copy(self), extend(copy(a:opts), s:defaults, 'keep')) 21 | if has_key(a:opts, 'dir') 22 | let l:instance.dir = substitute(fnamemodify(a:opts.dir, ':p'), '\'.s:slash.'$', '', '') 23 | endif 24 | let l:instance.plugins = {} 25 | let l:instance.processed_plugins = [] 26 | let l:instance.remaining_jobs = 0 27 | let l:instance.running_jobs = 0 28 | let l:instance.install_ran = 0 29 | let l:instance.update_ran = 0 30 | let l:instance.icons_str = join(values(packager#utils#status_icons()), '') 31 | let l:instance.timer = -1 32 | let l:instance.last_render_time = reltime() 33 | let l:instance.git_version = packager#utils#git_version() 34 | silent! call mkdir(printf('%s%s%s', l:instance.dir, s:slash, 'opt'), 'p') 35 | silent! call mkdir(printf('%s%s%s', l:instance.dir, s:slash, 'start'), 'p') 36 | return l:instance 37 | endfunction 38 | 39 | function! s:packager.add(name, ...) abort 40 | let l:plugin = packager#plugin#new(a:name, get(a:, 1, {}), self) 41 | let self.plugins[l:plugin.name] = l:plugin 42 | endfunction 43 | 44 | function! s:packager.add_required(name, required_package) abort 45 | if type(a:required_package) ==? type('') 46 | return self.add(a:required_package, {}) 47 | endif 48 | let l:name = '' 49 | let l:opts = {} 50 | if type(a:required_package) ==? type([]) 51 | let l:name = get(a:required_package, 0, '') 52 | let l:opts = get(a:required_package, 1, {}) 53 | elseif type(a:required_package) ==? type({}) 54 | let l:name = get(a:required_package, 'name', '') 55 | let l:opts = get(a:required_package, 'opts', {}) 56 | endif 57 | if empty(l:name) 58 | throw 'Missing "requires" package name for '.a:name.'.' 59 | endif 60 | let l:plugin = packager#plugin#new(l:name, l:opts, self) 61 | if !has_key(self.plugins, l:plugin.name) 62 | let self.plugins[l:plugin.name] = l:plugin 63 | endif 64 | endfunction 65 | 66 | function! s:packager.local(name, ...) abort 67 | let l:opts = get(a:, 1, {}) 68 | let l:opts.local = 1 69 | let l:plugin = packager#plugin#new(a:name, l:opts, self) 70 | let self.plugins[l:plugin.name] = l:plugin 71 | endfunction 72 | 73 | function! s:packager.install(opts) abort 74 | let self.start_time = reltime() 75 | let self.result = [] 76 | let self.processed_plugins = filter(values(self.plugins), 'v:val.installed ==? 0') 77 | let only_plugins = get(a:opts, 'plugins', []) 78 | if !empty(only_plugins) 79 | let self.processed_plugins = filter(self.processed_plugins, 'index(only_plugins, v:val.name) > -1') 80 | endif 81 | let self.remaining_jobs = len(self.processed_plugins) 82 | 83 | if self.remaining_jobs ==? 0 84 | echo 'Nothing to install.' 85 | return 86 | endif 87 | 88 | let self.install_ran = 1 89 | let self.post_run_opts = a:opts 90 | call self.open_buffer() 91 | if s:has_timers 92 | let self.timer = timer_start(100, {timer->self.render()}, { 'repeat': -1 }) 93 | else 94 | call self.render_if_no_timers() 95 | endif 96 | 97 | for l:plugin in self.processed_plugins 98 | call l:plugin.queue() 99 | call self.start_job(l:plugin.command(self.depth), { 100 | \ 'handler': 's:stdout_handler', 101 | \ 'plugin': l:plugin, 102 | \ 'limit_jobs': v:true 103 | \ }) 104 | endfor 105 | endfunction 106 | 107 | function! s:packager.update(opts) abort 108 | let self.start_time = reltime() 109 | let self.result = [] 110 | let self.processed_plugins = filter(values(self.plugins), 'v:val.frozen ==? 0') 111 | let only_plugins = get(a:opts, 'plugins', []) 112 | if !empty(only_plugins) 113 | let self.processed_plugins = filter(self.processed_plugins, 'index(only_plugins, v:val.name) > -1') 114 | endif 115 | let self.remaining_jobs = len(self.processed_plugins) 116 | 117 | if self.remaining_jobs ==? 0 118 | echo 'Nothing to update.' 119 | return 120 | endif 121 | 122 | let self.update_ran = 1 123 | let self.post_run_opts = a:opts 124 | let self.command_type = 'update' 125 | call self.open_buffer() 126 | if s:has_timers 127 | let self.timer = timer_start(100, {timer->self.render()}, { 'repeat': -1 }) 128 | else 129 | call self.render_if_no_timers() 130 | endif 131 | 132 | for l:plugin in self.processed_plugins 133 | call l:plugin.queue() 134 | call self.start_job(l:plugin.command(self.depth), { 135 | \ 'handler': 's:stdout_handler', 136 | \ 'plugin': l:plugin, 137 | \ 'limit_jobs': v:true 138 | \ }) 139 | endfor 140 | endfunction 141 | 142 | function! s:packager.clean() abort 143 | let l:folders = glob(printf('%s%s*%s*', self.dir, s:slash, s:slash), 0, 1) 144 | let self.processed_plugins = values(self.plugins) 145 | let l:plugins = [] 146 | for l:plugin in self.processed_plugins 147 | call add(l:plugins, l:plugin.dir) 148 | if !empty(l:plugin.rtp_dir) 149 | call add(l:plugins, l:plugin.rtp_dir) 150 | endif 151 | endfor 152 | let l:to_clean = filter(copy(l:folders), {key, val -> index(l:plugins, val) < 0}) 153 | 154 | if len(l:to_clean) <=? 0 155 | echo 'Already clean.' 156 | return 0 157 | endif 158 | 159 | call self.open_buffer() 160 | let l:content = ['Clean up', ''] 161 | let l:lines = {} 162 | 163 | let l:index = 3 164 | for l:item in l:to_clean 165 | call add(l:content, packager#utils#status('waiting', l:item, 'Waiting for confirmation...')) 166 | let l:lines[l:item] = l:index 167 | let l:index += 1 168 | endfor 169 | 170 | call packager#utils#setline(1, l:content) 171 | 172 | let l:option = packager#utils#confirm_with_options('Remove above folder(s)?', "&Yes\n&No\n&Ask for each folder") 173 | if l:option ==? 0 || l:option ==? 2 174 | return self.quit() 175 | endif 176 | 177 | for l:item in l:to_clean 178 | let l:line = l:lines[l:item] 179 | if l:option ==? 3 180 | let l:confirm_delete = packager#utils#confirm(printf('Remove %s ?', l:item)) 181 | if !l:confirm_delete 182 | call packager#utils#setline(l:line, packager#utils#status_ok(l:item, 'Skipped.')) 183 | continue 184 | endif 185 | endif 186 | 187 | if delete(l:item, 'rf') !=? 0 188 | call packager#utils#setline(l:line, packager#utils#status_error(l:item, 'Failed.')) 189 | else 190 | call packager#utils#setline(l:line, packager#utils#status_ok(l:item, 'Removed!')) 191 | endif 192 | endfor 193 | setlocal nomodifiable 194 | endfunction 195 | 196 | function! s:packager.status() abort 197 | if self.is_running() 198 | echo 'Install/Update process still in progress. Please wait until it finishes to view the status.' 199 | return 200 | endif 201 | let l:result = [] 202 | if self.install_ran 203 | let self.processed_plugins = filter(values(self.plugins), 'v:val.installed_now ==? 1') 204 | elseif self.update_ran 205 | let self.processed_plugins = filter(values(self.plugins), 'v:val.updated ==? 1') 206 | else 207 | let self.processed_plugins = values(self.plugins) 208 | endif 209 | let l:has_errors = 0 210 | 211 | for l:plugin in self.processed_plugins 212 | let l:plugin_status = l:plugin.get_content_for_status() 213 | 214 | for l:status_line in l:plugin_status 215 | call add(l:result, l:status_line) 216 | endfor 217 | 218 | if !l:plugin.installed || l:plugin.update_failed || l:plugin.hook_failed 219 | let l:has_errors = 1 220 | endif 221 | endfor 222 | 223 | call self.open_buffer() 224 | let l:content = ['Plugin status.', ''] + l:result + ['', "Press 'Enter' on commit lines to preview the commit."] 225 | call add(l:content, "Press 'O' on plugin to open plugin details.") 226 | if l:has_errors 227 | call add(l:content, "Press 'E' on errored plugins to view stdout.") 228 | endif 229 | call add(l:content, "Press 'q' to quit this buffer.") 230 | 231 | call packager#utils#setline(1, l:content) 232 | setlocal nomodifiable 233 | endfunction 234 | 235 | function! s:packager.quit() abort 236 | if self.is_running() 237 | if !packager#utils#confirm('Installation is in progress. Are you sure you want to quit?') 238 | return 239 | endif 240 | endif 241 | silent! call timer_stop(self.timer) 242 | silent exe ':q!' 243 | endfunction 244 | 245 | function! s:packager.update_running_jobs() abort 246 | let self.remaining_jobs -= 1 247 | let self.remaining_jobs = max([0, self.remaining_jobs]) "Make sure it's not negative 248 | let self.running_jobs -= 1 249 | let self.running_jobs = max([0, self.running_jobs]) "Make sure it's not negative 250 | call self.render_if_no_timers() 251 | endfunction 252 | 253 | function! s:packager.run_post_update_hooks() abort 254 | if has_key(self, 'post_run_hooks_called') 255 | return 256 | endif 257 | 258 | let self.post_run_hooks_called = 1 259 | 260 | call self.update_remote_plugins_and_helptags() 261 | 262 | call self.render_if_no_timers(1) 263 | 264 | if has_key(self, 'post_run_opts') && has_key(self.post_run_opts, 'on_finish') 265 | silent! exe 'redraw' 266 | exe self.post_run_opts.on_finish 267 | endif 268 | endfunction 269 | 270 | function! s:packager.open_buffer() abort 271 | let l:bufnr = bufnr('__packager__') 272 | 273 | if l:bufnr > -1 274 | silent! exe 'b'.l:bufnr 275 | set modifiable 276 | silent 1,$delete _ 277 | else 278 | exe self.window_cmd '__packager__' 279 | endif 280 | 281 | setf packager 282 | setlocal buftype=nofile bufhidden=wipe nobuflisted nolist noswapfile nowrap cursorline nospell 283 | syntax clear 284 | syn match packagerCheck /^✓/ 285 | silent! exe 'syn match packagerPlus /^[+'.packager#utils#status_icons().progress.']/' 286 | silent! exe 'syn match packagerPlusText /\(^[+'.packager#utils#status_icons().progress.']\s\)\@<=[^ —]*/' 287 | syn match packagerX /^✗/ 288 | syn match packagerStar /^\s\s\*/ 289 | silent! exe 'syn match packagerStatus /\(^[+'.packager#utils#status_icons().progress.'].*—\)\@<=\s.*$/' 290 | syn match packagerStatusSuccess /\(^✓.*—\)\@<=\s.*$/ 291 | syn match packagerStatusError /\(^✗.*—\)\@<=\s.*$/ 292 | syn match packagerStatusCommit /\(^\*.*—\)\@<=\s.*$/ 293 | syn match packagerSha /\(\*\s\)\@<=[0-9a-f]\{4,}/ 294 | syn match packagerRelDate /([^)]*)$/ 295 | syn match packagerProgress /\(\[\)\@<=[\=]*/ 296 | 297 | hi def link packagerPlus Special 298 | hi def link packagerPlusText String 299 | hi def link packagerCheck Function 300 | hi def link packagerX WarningMsg 301 | hi def link packagerStar Boolean 302 | hi def link packagerStatus Constant 303 | hi def link packagerStatusCommit Constant 304 | hi def link packagerStatusSuccess Function 305 | hi def link packagerStatusError WarningMsg 306 | hi def link packagerSha Identifier 307 | hi def link packagerRelDate Comment 308 | hi def link packagerProgress Boolean 309 | 310 | call self.add_mappings() 311 | endfunction 312 | 313 | function! s:packager.get_top_status() abort 314 | let l:bar_length = 50 315 | let l:total = len(self.processed_plugins) 316 | let l:installed = l:total - self.remaining_jobs 317 | 318 | let l:bar_installed = float2nr(floor(str2float(l:bar_length) / str2float(l:total) * str2float(l:installed))) 319 | let l:bar_left = l:bar_length - l:bar_installed 320 | let l:bar = printf('[%s%s]', repeat('=', l:bar_installed), repeat('-', l:bar_left)) 321 | 322 | let l:install_text = self.remaining_jobs > 0 ? 'Installing' : 'Installed' 323 | let l:finished = self.remaining_jobs > 0 ? '' : ' - Finished after '.split(reltimestr(reltime(self.start_time)))[0].' sec!' 324 | return [ 325 | \ printf('%s plugins %d / %d%s', l:install_text, l:installed, l:total, l:finished), 326 | \ l:bar, 327 | \ '', 328 | \ ] 329 | endfunction 330 | 331 | function! s:packager.render_if_no_timers(...) abort 332 | if s:has_timers 333 | return 334 | endif 335 | 336 | let l:ms = str2nr(split(split(reltimestr(reltime(self.last_render_time)))[0], '\.')[1]) / 1000 337 | if l:ms < 100 && a:0 ==? 0 338 | return 339 | endif 340 | 341 | return self.render() 342 | endfunction 343 | 344 | function! s:packager.render() abort 345 | let l:content = self.get_top_status() 346 | for l:plugin in self.processed_plugins 347 | if !empty(l:plugin.status) 348 | call add(l:content, packager#utils#status(l:plugin.status, l:plugin.name, l:plugin.status_msg)) 349 | endif 350 | endfor 351 | let l:is_finished = has_key(self, 'post_run_hooks_called') 352 | if l:is_finished 353 | let l:content += [ 354 | \ '', 355 | \ "Press 'D' to view latest updates.", 356 | \ "Press 'O' on plugin to open plugin details.", 357 | \ "Press 'E' on a plugin line to see stdout in preview window.", 358 | \ "Press 'q' to quit this buffer.", 359 | \ ] 360 | endif 361 | let l:bufnr = bufnr('__packager__') 362 | let l:has_setbufline = exists('*setbufline') 363 | if l:has_setbufline 364 | call setbufline(l:bufnr, 1, l:content) 365 | else 366 | if &filetype !=? 'packager' 367 | exe bufwinnr('__packager__').'wincmd w' 368 | endif 369 | call setline(1, l:content) 370 | endif 371 | let self.last_render_time = reltime() 372 | 373 | if l:is_finished 374 | if l:has_setbufline 375 | call setbufvar(l:bufnr, '&modifiable', 0) 376 | else 377 | setlocal nomodifiable 378 | endif 379 | silent! call timer_stop(self.timer) 380 | endif 381 | endfunction 382 | 383 | function! s:packager.open_sha() abort 384 | let l:sha = matchstr(getline('.'), '^\s\s\*\s\zs[0-9a-f]\{7,9}') 385 | if empty(l:sha) 386 | return 387 | endif 388 | 389 | let l:plugin = self.find_plugin_by_sha(l:sha) 390 | 391 | if empty(l:plugin) 392 | return 393 | endif 394 | 395 | silent exe 'pedit' l:sha 396 | wincmd p 397 | setlocal previewwindow filetype=git buftype=nofile nobuflisted modifiable 398 | let l:sha_content = packager#utils#system(['git', '-C', l:plugin.dir, 'show', 399 | \ '--no-color', '--pretty=medium', l:sha 400 | \ ]) 401 | 402 | call setline(1, l:sha_content) 403 | setlocal nomodifiable 404 | call cursor(1, 1) 405 | nnoremap q :q 406 | endfunction 407 | 408 | function! s:packager.open_stdout(...) abort 409 | let l:is_hook = a:0 > 0 410 | let l:plugin_name = packager#utils#trim(matchstr(getline('.'), '^.\s\zs[^—]*\ze')) 411 | if !has_key(self.plugins, l:plugin_name) 412 | return 413 | endif 414 | 415 | let l:content = self.plugins[l:plugin_name].get_stdout_messages() 416 | if empty(l:content) 417 | echo 'No stdout content to show.' 418 | return 419 | endif 420 | 421 | silent exe 'pedit' l:plugin_name 422 | wincmd p 423 | setlocal previewwindow filetype=sh buftype=nofile nobuflisted modifiable 424 | silent 1,$delete _ 425 | call setline(1, l:content) 426 | setlocal nomodifiable 427 | call cursor(1, 1) 428 | nnoremap q :q 429 | endfunction 430 | 431 | function! s:packager.find_plugin_by_sha(sha) abort 432 | for l:plugin in self.processed_plugins 433 | let l:commits = filter(copy(l:plugin.last_update), printf("v:val =~? '^%s'", a:sha)) 434 | if len(l:commits) > 0 435 | return l:plugin 436 | endif 437 | endfor 438 | 439 | return {} 440 | endfunction 441 | 442 | function! s:packager.goto_plugin(dir) abort 443 | let l:flag = a:dir ==? 'previous' ? 'b': '' 444 | return search(printf('^[%s]\s.*$', self.icons_str), l:flag) 445 | endfunction 446 | 447 | function! s:packager.open_plugin_details() abort 448 | let l:plugin_name = packager#utils#trim(matchstr(getline('.'), '^.\s\zs[^—]*\ze')) 449 | if !has_key(self.plugins, l:plugin_name) 450 | return 451 | endif 452 | 453 | let l:plugin = self.plugins[l:plugin_name] 454 | 455 | silent exe 'pedit' l:plugin.name 456 | wincmd p 457 | setlocal previewwindow buftype=nofile nobuflisted modifiable filetype= 458 | silent 1,$delete _ 459 | let l:content = [ 460 | \ 'Plugin details:', 461 | \ '', 462 | \ 'Name: '.l:plugin.name, 463 | \ 'Loading type: '.(l:plugin.type ==? 'start' ? 'Automatically' : 'Manually'), 464 | \ 'Directory: '.l:plugin.dir, 465 | \ 'Url: '.l:plugin.url, 466 | \ ] 467 | 468 | if !empty(l:plugin.branch) 469 | call add(l:content, 'Branch: '.l:plugin.branch) 470 | else 471 | call add(l:content, 'Branch: '.l:plugin.get_main_branch()) 472 | endif 473 | if !empty(l:plugin.tag) 474 | call add(l:content, 'Tag: '.l:plugin.tag) 475 | endif 476 | if !empty(l:plugin.commit) 477 | call add(l:content, 'Commit: '.l:plugin.commit) 478 | endif 479 | if !empty(l:plugin.do) && type(l:plugin.do) ==? type('') 480 | call add(l:content, 'Post install command: '.l:plugin.do) 481 | endif 482 | if l:plugin.frozen 483 | call add(l:plugin, 'Plugin is frozen, no updates are executed for it.') 484 | endif 485 | 486 | call setline(1, l:content) 487 | setlocal nomodifiable 488 | call cursor(1, 1) 489 | nnoremap q :q 490 | endfunction 491 | 492 | function! s:packager.update_remote_plugins_and_helptags() abort 493 | for l:plugin in self.processed_plugins 494 | if l:plugin.updated 495 | silent! exe 'helptags' fnameescape(printf('%s%sdoc', l:plugin.dir, s:slash)) 496 | 497 | if has('nvim') && isdirectory(printf('%s%srplugin', l:plugin.dir, s:slash)) 498 | call packager#utils#add_rtp(l:plugin.dir) 499 | exe 'UpdateRemotePlugins' 500 | endif 501 | endif 502 | endfor 503 | endfunction 504 | 505 | function! s:packager.start_job(cmd, opts) abort 506 | if has_key(a:opts, 'limit_jobs') && self.jobs > 0 507 | if self.running_jobs > self.jobs 508 | while self.running_jobs > self.jobs 509 | silent exe 'redraw' 510 | sleep 100m 511 | endwhile 512 | endif 513 | let self.running_jobs += 1 514 | endif 515 | 516 | let l:opts = { 517 | \ 'on_stdout': function(a:opts.handler, [a:opts.plugin], self), 518 | \ 'on_stderr': function(a:opts.handler, [a:opts.plugin], self), 519 | \ 'on_exit': function(a:opts.handler, [a:opts.plugin], self) 520 | \ } 521 | 522 | if has_key(a:opts, 'cwd') 523 | let l:opts.cwd = a:opts.cwd 524 | endif 525 | 526 | let l:save_shell = packager#utils#set_shell() 527 | let l:job = packager#job#start(a:cmd, l:opts) 528 | call packager#utils#restore_shell(l:save_shell) 529 | 530 | return l:job 531 | endfunction 532 | 533 | function! s:packager.is_running() abort 534 | return self.remaining_jobs > 0 535 | endfunction 536 | 537 | function! s:packager.add_mappings() abort 538 | if self.disable_default_mappings 539 | return 540 | endif 541 | if !hasmapto('(PackagerQuit)') 542 | silent! nmap q (PackagerQuit) 543 | endif 544 | 545 | if !hasmapto('(PackagerOpenSha)') 546 | silent! nmap (PackagerOpenSha) 547 | endif 548 | 549 | if !hasmapto('(PackagerOpenStdout)') 550 | silent! nmap E (PackagerOpenStdout) 551 | endif 552 | 553 | if !hasmapto('(PackagerGotoNextPlugin)') 554 | silent! nmap (PackagerGotoNextPlugin) 555 | endif 556 | 557 | if !hasmapto('(PackagerGotoPrevPlugin)') 558 | silent! nmap (PackagerGotoPrevPlugin) 559 | endif 560 | 561 | if !hasmapto('(PackagerStatus)') 562 | silent! nmap D (PackagerStatus) 563 | endif 564 | 565 | if !hasmapto('(PackagerPluginDetails)') 566 | silent! nmap O (PackagerPluginDetails) 567 | endif 568 | endfunction 569 | 570 | function! s:packager.get_plugins() abort 571 | return map(values(self.plugins), {_, plugin -> plugin.get_info()}) 572 | endfunction 573 | 574 | function! s:packager.get_plugin_names() abort 575 | return keys(self.plugins) 576 | endfunction 577 | 578 | function! s:packager.get_plugin(name) abort 579 | if !has_key(self.plugins, a:name) 580 | throw 'No plugin named '.a:name 581 | endif 582 | return self.plugins[a:name].get_info() 583 | endfunction 584 | 585 | function! s:packager.run_hooks_if_finished() abort 586 | if self.remaining_jobs <=? 0 587 | call self.run_post_update_hooks() 588 | endif 589 | endfunction 590 | 591 | function! s:packager.supports_submodule_progress() abort 592 | if empty(self.git_version) 593 | return 0 594 | endif 595 | 596 | return get(self.git_version, 0) >= 2 && get(self.git_version, 1) >= 11 597 | endfunction 598 | 599 | function! s:stdout_handler(plugin, id, message, event) dict abort 600 | call a:plugin.log_event_messages(a:event, a:message) 601 | call self.render_if_no_timers() 602 | 603 | if a:event !=? 'exit' 604 | return a:plugin.set_status('progress', a:plugin.get_last_progress_message()) 605 | endif 606 | 607 | if a:message !=? 0 608 | call self.update_running_jobs() 609 | let a:plugin.update_failed = 1 610 | let l:err_msg = a:plugin.get_short_error_message() 611 | let l:err_msg = !empty(l:err_msg) ? printf(' - %s', l:err_msg) : '' 612 | call a:plugin.set_status('error', printf('Error (exit status %d)%s', a:message, l:err_msg)) 613 | return self.run_hooks_if_finished() 614 | endif 615 | 616 | let l:status_text = a:plugin.update_install_status() 617 | 618 | if (a:plugin.updated || !empty(get(self.post_run_opts, 'force_hooks', 0))) && !empty(a:plugin.do) 619 | call packager#utils#load_plugin(a:plugin) 620 | call a:plugin.set_status('progress', 'Running post update hooks...') 621 | if type(a:plugin.do) ==? type(function('call')) 622 | try 623 | call call(a:plugin.do, [a:plugin]) 624 | call a:plugin.set_status('ok', 'Finished running post update hook!') 625 | catch 626 | call a:plugin.set_status('error', printf('Error on hook - %s', v:exception)) 627 | endtry 628 | call self.update_running_jobs() 629 | elseif a:plugin.do[0] ==? ':' 630 | try 631 | exe a:plugin.do[1:] 632 | call a:plugin.set_status('ok', 'Finished running post update hook!') 633 | catch 634 | call a:plugin.set_status('error', printf('Error on hook - %s', v:exception)) 635 | endtry 636 | call self.update_running_jobs() 637 | else 638 | call self.start_job(a:plugin.do, { 639 | \ 'handler': 's:hook_stdout_handler', 640 | \ 'plugin': a:plugin, 641 | \ 'cwd': a:plugin.dir 642 | \ }) 643 | endif 644 | else 645 | call a:plugin.set_status('ok', l:status_text) 646 | call self.update_running_jobs() 647 | endif 648 | 649 | return self.run_hooks_if_finished() 650 | endfunction 651 | 652 | function! s:hook_stdout_handler(plugin, id, message, event) dict abort 653 | call self.render_if_no_timers() 654 | call a:plugin.log_event_messages(a:event, a:message, 'hook') 655 | 656 | if a:event !=? 'exit' 657 | return a:plugin.set_status('progress', a:plugin.get_last_progress_message('hook')) 658 | endif 659 | 660 | call self.update_running_jobs() 661 | if a:message !=? 0 662 | let l:err_msg = a:plugin.get_short_error_message('hook') 663 | let l:err_msg = !empty(l:err_msg) ? printf(' - %s', l:err_msg) : '' 664 | let a:plugin.hook_failed = 1 665 | call a:plugin.set_status('error', printf('Error on hook (exit status %d)%s', a:message, l:err_msg)) 666 | else 667 | call a:plugin.set_status('ok', 'Finished running post update hook!') 668 | endif 669 | 670 | return self.run_hooks_if_finished() 671 | endfunction 672 | -------------------------------------------------------------------------------- /autoload/packager/job.vim: -------------------------------------------------------------------------------- 1 | " Author: Prabir Shrestha 2 | " Website: https://github.com/prabirshrestha/async.vim 3 | " License: The MIT License {{{ 4 | " The MIT License (MIT) 5 | " 6 | " Copyright (c) 2016 Prabir Shrestha 7 | " 8 | " Permission is hereby granted, free of charge, to any person obtaining a copy 9 | " of this software and associated documentation files (the "Software"), to deal 10 | " in the Software without restriction, including without limitation the rights 11 | " to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | " copies of the Software, and to permit persons to whom the Software is 13 | " furnished to do so, subject to the following conditions: 14 | " 15 | " The above copyright notice and this permission notice shall be included in all 16 | " copies or substantial portions of the Software. 17 | " 18 | " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | " IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | " FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | " AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | " LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | " OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | " SOFTWARE. 25 | " }}} 26 | 27 | let s:save_cpo = &cpo 28 | set cpo&vim 29 | 30 | let s:jobidseq = 0 31 | let s:jobs = {} " { job, opts, type: 'vimjob|nvimjob'} 32 | let s:job_type_nvimjob = 'nvimjob' 33 | let s:job_type_vimjob = 'vimjob' 34 | let s:job_error_unsupported_job_type = -2 " unsupported job type 35 | 36 | function! s:job_supported_types() abort 37 | let l:supported_types = [] 38 | if has('nvim') 39 | let l:supported_types += [s:job_type_nvimjob] 40 | endif 41 | if !has('nvim') && has('job') && has('channel') && has('lambda') 42 | let l:supported_types += [s:job_type_vimjob] 43 | endif 44 | return l:supported_types 45 | endfunction 46 | 47 | function! s:job_supports_type(type) abort 48 | return index(s:job_supported_types(), a:type) >= 0 49 | endfunction 50 | 51 | function! s:out_cb(jobid, opts, job, data) abort 52 | if has_key(a:opts, 'on_stdout') 53 | call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout') 54 | endif 55 | endfunction 56 | 57 | function! s:err_cb(jobid, opts, job, data) abort 58 | if has_key(a:opts, 'on_stderr') 59 | call a:opts.on_stderr(a:jobid, split(a:data, "\n", 1), 'stderr') 60 | endif 61 | endfunction 62 | 63 | function! s:exit_cb(jobid, opts, job, status) abort 64 | if has_key(a:opts, 'on_exit') 65 | call a:opts.on_exit(a:jobid, a:status, 'exit') 66 | endif 67 | if has_key(s:jobs, a:jobid) 68 | call remove(s:jobs, a:jobid) 69 | endif 70 | endfunction 71 | 72 | function! s:on_stdout(jobid, data, event) abort 73 | if has_key(s:jobs, a:jobid) 74 | let l:jobinfo = s:jobs[a:jobid] 75 | if has_key(l:jobinfo.opts, 'on_stdout') 76 | call l:jobinfo.opts.on_stdout(a:jobid, a:data, a:event) 77 | endif 78 | endif 79 | endfunction 80 | 81 | function! s:on_stderr(jobid, data, event) abort 82 | if has_key(s:jobs, a:jobid) 83 | let l:jobinfo = s:jobs[a:jobid] 84 | if has_key(l:jobinfo.opts, 'on_stderr') 85 | call l:jobinfo.opts.on_stderr(a:jobid, a:data, a:event) 86 | endif 87 | endif 88 | endfunction 89 | 90 | function! s:on_exit(jobid, status, event) abort 91 | if has_key(s:jobs, a:jobid) 92 | let l:jobinfo = s:jobs[a:jobid] 93 | if has_key(l:jobinfo.opts, 'on_exit') 94 | call l:jobinfo.opts.on_exit(a:jobid, a:status, a:event) 95 | endif 96 | endif 97 | endfunction 98 | 99 | function! s:job_start(cmd, opts) abort 100 | let l:jobtypes = s:job_supported_types() 101 | let l:jobtype = '' 102 | 103 | if has_key(a:opts, 'type') 104 | if type(a:opts.type) == type('') 105 | if !s:job_supports_type(a:opts.type) 106 | return s:job_error_unsupported_job_type 107 | endif 108 | let l:jobtype = a:opts.type 109 | else 110 | let l:jobtypes = a:opts.type 111 | endif 112 | endif 113 | 114 | if empty(l:jobtype) 115 | " find the best jobtype 116 | for l:jobtype2 in l:jobtypes 117 | if s:job_supports_type(l:jobtype2) 118 | let l:jobtype = l:jobtype2 119 | endif 120 | endfor 121 | endif 122 | 123 | if l:jobtype ==? '' 124 | return s:job_error_unsupported_job_type 125 | endif 126 | 127 | if l:jobtype == s:job_type_nvimjob 128 | let l:cmd = type(a:cmd) ==? type([]) ? join(' ') : a:cmd 129 | let l:job = jobstart(l:cmd, { 130 | \ 'on_stdout': function('s:on_stdout'), 131 | \ 'on_stderr': function('s:on_stderr'), 132 | \ 'on_exit': function('s:on_exit'), 133 | \ 'cwd': has_key(a:opts, 'cwd') ? a:opts.cwd : getcwd() 134 | \}) 135 | if l:job <= 0 136 | return l:job 137 | endif 138 | let l:jobid = l:job " nvimjobid and internal jobid is same 139 | let s:jobs[l:jobid] = { 140 | \ 'type': s:job_type_nvimjob, 141 | \ 'opts': a:opts, 142 | \ } 143 | let s:jobs[l:jobid].job = l:job 144 | elseif l:jobtype == s:job_type_vimjob 145 | let s:jobidseq = s:jobidseq + 1 146 | let l:jobid = s:jobidseq 147 | 148 | let l:cmd = type(a:cmd) ==? type([]) ? join(a:cmd, ' ') : a:cmd 149 | let l:job = job_start(printf('%s %s "%s"', &shell, &shellcmdflag, l:cmd), { 150 | \ 'out_cb': function('s:out_cb', [l:jobid, a:opts]), 151 | \ 'err_cb': function('s:err_cb', [l:jobid, a:opts]), 152 | \ 'exit_cb': function('s:exit_cb', [l:jobid, a:opts]), 153 | \ 'mode': 'raw', 154 | \ 'cwd': has_key(a:opts, 'cwd') ? a:opts.cwd : getcwd() 155 | \}) 156 | if job_status(l:job) !=? 'run' 157 | return -1 158 | endif 159 | let s:jobs[l:jobid] = { 160 | \ 'type': s:job_type_vimjob, 161 | \ 'opts': a:opts, 162 | \ 'job': l:job, 163 | \ 'channel': job_getchannel(l:job), 164 | \ 'buffer': '' 165 | \ } 166 | else 167 | return s:job_error_unsupported_job_type 168 | endif 169 | 170 | return l:jobid 171 | endfunction 172 | 173 | function! s:job_stop(jobid) abort 174 | if has_key(s:jobs, a:jobid) 175 | let l:jobinfo = s:jobs[a:jobid] 176 | if l:jobinfo.type == s:job_type_nvimjob 177 | call jobstop(a:jobid) 178 | elseif l:jobinfo.type == s:job_type_vimjob 179 | call job_stop(s:jobs[a:jobid].job) 180 | endif 181 | if has_key(s:jobs, a:jobid) 182 | call remove(s:jobs, a:jobid) 183 | endif 184 | endif 185 | endfunction 186 | 187 | function! s:job_send(jobid, data) abort 188 | let l:jobinfo = s:jobs[a:jobid] 189 | if l:jobinfo.type == s:job_type_nvimjob 190 | call jobsend(a:jobid, a:data) 191 | elseif l:jobinfo.type == s:job_type_vimjob 192 | let l:jobinfo.buffer .= a:data 193 | call s:flush_vim_sendraw(a:jobid, v:null) 194 | endif 195 | endfunction 196 | 197 | function! s:flush_vim_sendraw(jobid, timer) abort 198 | " https://github.com/vim/vim/issues/2548 199 | " https://github.com/natebosch/vim-lsc/issues/67#issuecomment-357469091 200 | let l:jobinfo = s:jobs[a:jobid] 201 | if len(l:jobinfo.buffer) <= 1024 202 | call ch_sendraw(l:jobinfo.channel, l:jobinfo.buffer) 203 | let l:jobinfo.buffer = '' 204 | else 205 | let l:to_send = l:jobinfo.buffer[:1023] 206 | let l:jobinfo.buffer = l:jobinfo.buffer[1024:] 207 | call ch_sendraw(l:jobinfo.channel, l:to_send) 208 | call timer_start(0, function('s:flush_vim_sendraw', [a:jobid])) 209 | endif 210 | endfunction 211 | 212 | function! s:job_wait_single(jobid, timeout, start) abort 213 | if !has_key(s:jobs, a:jobid) 214 | return -3 215 | endif 216 | 217 | let l:jobinfo = s:jobs[a:jobid] 218 | if l:jobinfo.type == s:job_type_nvimjob 219 | let l:timeout = a:timeout - reltimefloat(reltime(a:start)) * 1000 220 | return jobwait([a:jobid], float2nr(l:timeout))[0] 221 | elseif l:jobinfo.type == s:job_type_vimjob 222 | let l:timeout = a:timeout / 1000.0 223 | try 224 | while l:timeout < 0 || reltimefloat(reltime(a:start)) < l:timeout 225 | let l:info = job_info(l:jobinfo.job) 226 | if l:info.status ==# 'dead' 227 | return l:info.exitval 228 | elseif l:info.status ==# 'fail' 229 | return -3 230 | endif 231 | sleep 1m 232 | endwhile 233 | catch /^Vim:Interrupt$/ 234 | return -2 235 | endtry 236 | endif 237 | return -1 238 | endfunction 239 | 240 | function! s:job_wait(jobids, timeout) abort 241 | let l:start = reltime() 242 | let l:exitcode = 0 243 | let l:ret = [] 244 | for l:jobid in a:jobids 245 | if l:exitcode != -2 " Not interrupted. 246 | let l:exitcode = s:job_wait_single(l:jobid, a:timeout, l:start) 247 | endif 248 | let l:ret += [l:exitcode] 249 | endfor 250 | return l:ret 251 | endfunction 252 | 253 | " public apis {{{ 254 | function! packager#job#start(cmd, opts) abort 255 | return s:job_start(a:cmd, a:opts) 256 | endfunction 257 | 258 | function! packager#job#stop(jobid) abort 259 | call s:job_stop(a:jobid) 260 | endfunction 261 | 262 | function! packager#job#send(jobid, data) abort 263 | call s:job_send(a:jobid, a:data) 264 | endfunction 265 | 266 | function! packager#job#wait(jobids, ...) abort 267 | let l:timeout = get(a:000, 0, -1) 268 | return s:job_wait(a:jobids, l:timeout) 269 | endfunction 270 | " }}} 271 | -------------------------------------------------------------------------------- /autoload/packager/plugin.vim: -------------------------------------------------------------------------------- 1 | scriptencoding utf8 2 | let s:plugin = {} 3 | let s:is_windows = has('win32') 4 | let s:slash = exists('+shellslash') && !&shellslash ? '\' : '/' 5 | let s:defaults = { 'name': '', 'branch': '', 'commit': '', 'tag': '', 6 | \ 'installed': 0, 'updated': 0, 'rev': '', 'do': '', 'frozen': 0, 'rtp': '', 'requires': '' } 7 | 8 | function! packager#plugin#new(name, opts, packager) abort 9 | return s:plugin.new(a:name, a:opts, a:packager) 10 | endfunction 11 | 12 | function! s:plugin.new(name, opts, packager) abort 13 | let l:instance = extend(copy(self), extend(copy(a:opts), s:defaults, 'keep')) 14 | if !has_key(l:instance, 'type') || empty(l:instance.type) 15 | let l:instance.type = a:packager.default_plugin_type 16 | endif 17 | if index(['opt', 'start'], l:instance.type) <= -1 18 | let l:instance.type = 'start' 19 | endif 20 | let l:instance.packager = a:packager 21 | let l:instance.name = !empty(l:instance.name) ? l:instance.name : split(a:name, '/')[-1] 22 | let l:instance.dir = printf('%s%s%s%s%s', a:packager.dir, s:slash, l:instance.type, s:slash, l:instance.name) 23 | let l:instance.local = get(l:instance, 'local', 0) 24 | let l:instance.rtp_dir = '' 25 | if !empty(l:instance.rtp) 26 | let l:rtp = substitute(l:instance.rtp, '[\\\/]$', '', '') 27 | let l:rtp = substitute(l:rtp, '[\\\/]', '__', 'g') 28 | let l:instance.rtp_dir = printf('%s__%s', l:instance.dir, l:rtp) 29 | endif 30 | let l:instance.url = a:name =~? '^\(http\|git@\).*' 31 | \ ? a:name 32 | \ : l:instance.local ? a:name : printf('https://github.com/%s', a:name) 33 | let l:instance.event_messages = [] 34 | let l:instance.hook_event_messages = [] 35 | let l:instance.update_failed = 0 36 | let l:instance.hook_failed = 0 37 | let l:instance.last_update = [] 38 | let l:instance.head_ref = '' 39 | let l:instance.main_branch = '' 40 | let l:instance.installed_now = 0 41 | let l:instance.status = '' 42 | let l:instance.status_msg = '' 43 | if isdirectory(l:instance.dir) 44 | let l:instance.installed = 1 45 | if s:is_windows 46 | call l:instance.revision('async') 47 | call l:instance.get_head_ref('async') 48 | call l:instance.get_main_branch('async') 49 | endif 50 | endif 51 | if !empty(l:instance.requires) && type(l:instance.requires) !=? type([]) 52 | let l:instance.requires = [l:instance.requires] 53 | endif 54 | 55 | if !empty(l:instance.requires) 56 | for require in l:instance.requires 57 | call l:instance.packager.add_required(l:instance.name, require) 58 | endfor 59 | endif 60 | return l:instance 61 | endfunction 62 | 63 | function! s:plugin.get_info() abort 64 | if !s:is_windows 65 | let self.rev = self.revision() 66 | let self.head_ref = self.get_head_ref() 67 | let self.main_branch = self.get_main_branch() 68 | endif 69 | return { 70 | \ 'type': self.type, 71 | \ 'rev': self.rev, 72 | \ 'head_ref': self.head_ref, 73 | \ 'main_branch': self.main_branch, 74 | \ 'name': self.name, 75 | \ 'dir': self.dir, 76 | \ 'is_local': self.local, 77 | \ 'rtp_dir': self.rtp_dir, 78 | \ 'url': self.url, 79 | \ 'installed': self.installed, 80 | \ } 81 | endfunction 82 | 83 | function! s:plugin.queue() abort 84 | if !s:is_windows 85 | let self.rev = self.revision() 86 | endif 87 | 88 | let l:msg = self.installed ? 'Updating...' : 'Installing...' 89 | return self.set_status('progress', l:msg) 90 | endfunction 91 | 92 | function! s:plugin.set_status(status, status_msg) abort 93 | let self.status = a:status 94 | let self.status_msg = a:status_msg 95 | endfunction 96 | 97 | function! s:plugin.update_git_command() abort 98 | let l:update_cmd = ['cd'] 99 | if s:is_windows 100 | let l:update_cmd += ['/d'] 101 | endif 102 | let l:update_cmd += [self.dir] 103 | let l:has_checkout = v:false 104 | let l:is_on_branch = v:true 105 | 106 | for l:checkout_target in [self.commit, self.tag, self.branch] 107 | if !empty(l:checkout_target) 108 | let l:update_cmd += ['&&', 'git', 'checkout', l:checkout_target] 109 | let l:is_on_branch = l:checkout_target ==? self.branch 110 | let l:has_checkout = v:true 111 | break 112 | endif 113 | endfor 114 | 115 | if !l:has_checkout && self.get_head_ref() ==? 'HEAD' && !empty(self.get_main_branch()) 116 | let l:is_on_branch = v:true 117 | let l:update_cmd += ['&&', 'git', 'checkout', self.main_branch] 118 | endif 119 | 120 | if l:is_on_branch 121 | let l:update_cmd += ['&&', 'git', 'pull', '--ff-only', '--progress', '--rebase=false'] 122 | else 123 | let l:update_cmd += ['&&', 'git', 'fetch', self.url, '--depth', '999999'] 124 | endif 125 | let l:update_cmd += ['&&', 'git', 'submodule', 'update', '--init', '--recursive'] 126 | if self.packager.supports_submodule_progress() 127 | let l:update_cmd += ['--progress'] 128 | endif 129 | 130 | return l:update_cmd 131 | endfunction 132 | 133 | function! s:plugin.install_git_command(depth) abort 134 | let l:depth = !empty(self.commit) ? '999999' : a:depth 135 | let l:clone_cmd = ['git', 'clone', '--progress', self.url, self.dir, '--depth', l:depth, '--no-single-branch'] 136 | 137 | if empty(self.commit) 138 | for l:branch_or_tag in [self.tag, self.branch] 139 | if !empty(l:branch_or_tag) 140 | let l:clone_cmd += ['--branch', l:branch_or_tag] 141 | break 142 | endif 143 | endfor 144 | endif 145 | 146 | let l:clone_cmd += ['&&', 'cd'] 147 | if s:is_windows 148 | let l:clone_cmd += ['/d'] 149 | endif 150 | let l:clone_cmd += [self.dir, '&&', 'git', 'submodule', 'update', '--init', '--recursive'] 151 | if self.packager.supports_submodule_progress() 152 | let l:clone_cmd += ['--progress'] 153 | endif 154 | 155 | if !empty(self.commit) 156 | let l:clone_cmd += ['&&', 'git', 'checkout', self.commit] 157 | endif 158 | 159 | return l:clone_cmd 160 | endfunction 161 | 162 | function! s:plugin.local_command() abort 163 | let l:cmd = packager#utils#symlink(fnamemodify(self.url, ':p'), self.dir) 164 | 165 | if !empty(l:cmd) 166 | return l:cmd 167 | endif 168 | 169 | return ['echo', printf('Cannot install %s locally, linking tool not found.', self.name)] 170 | endfunction 171 | 172 | function! s:plugin.command(depth) abort 173 | if isdirectory(self.dir) && !self.local 174 | return join(self.update_git_command(), ' ') 175 | endif 176 | 177 | if self.local 178 | return join(self.local_command()) 179 | endif 180 | 181 | return join(self.install_git_command(a:depth), ' ') 182 | endfunction 183 | 184 | function! s:plugin.has_updates() abort 185 | return !empty(self.rev) && self.rev !=? self.revision() 186 | endfunction 187 | 188 | function! s:plugin.get_last_update(...) abort 189 | let l:cmd = ['git', '-C', self.dir, 'log', 190 | \ '--color=never', '--pretty=format:"%h %s (%cr)"', '--no-show-signature', 'HEAD@{1}..' 191 | \ ] 192 | 193 | if a:0 > 0 && a:1 ==? 'async' && s:is_windows 194 | return packager#utils#system_async(l:cmd, self, 'last_update', { 195 | \ 'all': v:true, 196 | \ 'formatter': {val -> filter(val, 'v:val !=? "" && v:val !~? "^fatal"')} 197 | \ }) 198 | endif 199 | 200 | let l:commits = packager#utils#system(l:cmd) 201 | let self.last_update = filter(l:commits, 'v:val !=? "" && v:val !~? "^fatal"') 202 | 203 | return self.last_update 204 | endfunction 205 | 206 | function! s:plugin.revision(...) abort 207 | let l:cmd = ['git', '-C', self.dir, 'rev-parse', 'HEAD'] 208 | if a:0 > 0 && a:1 ==? 'async' 209 | return packager#utils#system_async(l:cmd, self, 'rev') 210 | endif 211 | 212 | let l:rev = get(packager#utils#system(l:cmd), 0, '') 213 | if l:rev =~? '^fatal' 214 | return '' 215 | endif 216 | return l:rev 217 | endfunction 218 | 219 | function! s:plugin.get_head_ref(...) abort 220 | if !empty(self.head_ref) 221 | return self.head_ref 222 | endif 223 | 224 | let l:cmd = ['git', '-C', self.dir, 'rev-parse', '--abbrev-ref', 'HEAD'] 225 | 226 | if a:0 > 0 && a:1 ==? 'async' 227 | return packager#utils#system_async(l:cmd, self, 'head_ref') 228 | endif 229 | 230 | let l:head = get(packager#utils#system(l:cmd), 0, '') 231 | let self.head_ref = l:head =~? '^fatal' ? '' : l:head 232 | 233 | return self.head_ref 234 | endfunction 235 | 236 | function! s:plugin.get_main_branch(...) abort 237 | if !empty(self.main_branch) 238 | return self.main_branch 239 | endif 240 | 241 | let l:cmd = ['git', '-C', self.dir, 'symbolic-ref', 'refs/remotes/origin/HEAD'] 242 | 243 | if a:0 > 0 && a:1 ==? 'async' 244 | return packager#utils#system_async(l:cmd, self, 'main_branch', { 245 | \ 'formatter': {val -> get(split(val, '/'), -1, '')} }) 246 | endif 247 | 248 | let l:ref = get(packager#utils#system(l:cmd), 0, '') 249 | let self.main_branch = l:ref =~? '^fatal' ? '' : get(split(l:ref, '/'), -1, '') 250 | 251 | return self.main_branch 252 | endfunction 253 | 254 | function! s:plugin.symlink_rtp() abort 255 | if empty(self.rtp_dir) 256 | return 0 257 | endif 258 | 259 | let l:dir = printf('%s/%s', self.dir, self.rtp) 260 | let l:symlink_cmd = packager#utils#symlink(l:dir, self.rtp_dir) 261 | 262 | if !empty(l:symlink_cmd) 263 | return packager#utils#system(l:symlink_cmd) 264 | endif 265 | 266 | return 0 267 | endfunction 268 | 269 | function! s:plugin.update_install_status() abort 270 | if !self.installed 271 | let self.installed = 1 272 | let self.updated = 1 273 | let self.installed_now = 1 274 | call self.symlink_rtp() 275 | return 'Installed!' 276 | endif 277 | 278 | if self.has_updates() 279 | let self.updated = 1 280 | call self.get_last_update('async') 281 | call self.symlink_rtp() 282 | return 'Updated!' 283 | endif 284 | 285 | return 'Already up to date.' 286 | endfunction 287 | 288 | function! s:plugin.get_message_key(is_hook) abort 289 | if a:is_hook 290 | return 'hook_event_messages' 291 | endif 292 | 293 | return 'event_messages' 294 | endfunction 295 | 296 | function! s:plugin.log_event_messages(event, messages, ...) abort 297 | if a:event ==? 'exit' 298 | return 0 299 | endif 300 | let l:key = self.get_message_key(a:0 > 0) 301 | 302 | if type(a:messages) ==? type([]) 303 | for l:message in a:messages 304 | if !empty(packager#utils#trim(l:message)) && index(self[l:key], l:message) < 0 305 | call add(self[l:key], l:message) 306 | endif 307 | endfor 308 | endif 309 | 310 | if type(a:messages) ==? type('') && index(self[l:key], a:messages) < 0 311 | call add(self[l:key], a:messages) 312 | endif 313 | endfunction 314 | 315 | function! s:plugin.get_last_progress_message(...) abort 316 | let l:key = self.get_message_key(a:0 > 0) 317 | let l:last_msg = get(self[l:key], -1, '') 318 | return get(split(l:last_msg, '\r'), -1, l:last_msg) 319 | endfunction 320 | 321 | function! s:plugin.get_short_error_message(...) abort 322 | let l:is_hook = a:0 > 0 323 | let l:key = self.get_message_key(l:is_hook) 324 | return packager#utils#trim(get(self[l:key], -1, '')) 325 | endfunction 326 | 327 | function! s:plugin.get_stdout_messages() abort 328 | let l:result = [] 329 | let l:key = self.get_message_key(self.hook_failed) 330 | 331 | for l:message in self[l:key] 332 | let l:split_message = split(l:message, '\r') 333 | for l:msg in l:split_message 334 | if !empty(packager#utils#trim(l:msg)) 335 | call add(l:result, l:msg) 336 | endif 337 | endfor 338 | endfor 339 | 340 | return l:result 341 | endfunction 342 | 343 | function! s:plugin.get_content_for_status() abort 344 | if !self.installed 345 | let l:err_msg = self.get_short_error_message() 346 | let l:last_err_line = !empty(l:err_msg) ? ' Last line of error message:' : '' 347 | let l:status = printf('Not installed.%s', l:last_err_line) 348 | let l:result = [packager#utils#status_error(self.name, l:status)] 349 | if !empty(l:err_msg) 350 | call add(l:result, printf(' * %s', l:err_msg)) 351 | endif 352 | return l:result 353 | endif 354 | 355 | if self.update_failed 356 | return [packager#utils#status_error(self.name, 'Install/update failed. Last line of error message:'), 357 | \ printf(' * %s', self.get_short_error_message())] 358 | endif 359 | 360 | if self.hook_failed 361 | return [packager#utils#status_error(self.name, 'Post hook failed. Last line of error message:'), 362 | \ printf(' * %s', self.get_short_error_message('hook'))] 363 | endif 364 | 365 | if self.installed_now 366 | return [packager#utils#status_ok(self.name, 'Installed!')] 367 | endif 368 | 369 | if !self.updated 370 | call self.get_last_update() 371 | endif 372 | 373 | if empty(self.last_update) 374 | return [packager#utils#status_ok(self.name, 'OK.')] 375 | endif 376 | 377 | let l:update_text = self.updated ? 'Updated!' : 'Last update:' 378 | let l:result = [packager#utils#status_ok(self.name, l:update_text)] 379 | for l:update in self.last_update 380 | call add(l:result, printf(' * %s', l:update)) 381 | endfor 382 | return l:result 383 | endfunction 384 | -------------------------------------------------------------------------------- /autoload/packager/utils.vim: -------------------------------------------------------------------------------- 1 | scriptencoding utf8 2 | let s:progress = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] 3 | let s:progress_counter = 0 4 | 5 | function! packager#utils#system(cmds) abort 6 | let l:save_shell = packager#utils#set_shell() 7 | let l:cmd_output = systemlist(join(a:cmds, ' ')) 8 | call packager#utils#restore_shell(l:save_shell) 9 | return l:cmd_output 10 | endfunction 11 | 12 | function! packager#utils#system_async(cmds, self, key, ...) abort 13 | let l:args = a:0 > 0 ? a:1 : {} 14 | let l:save_shell = packager#utils#set_shell() 15 | let l:opts = { 16 | \ 'out': [], 17 | \ 'key': a:key, 18 | \ 'all': !empty(get(l:args, 'all')), 19 | \ 'formatter': get(l:args, 'formatter'), 20 | \ } 21 | let l:job = packager#job#start(join(a:cmds, ' '), { 22 | \ 'on_stdout': function('s:on_stdout', [l:opts], a:self), 23 | \ 'on_stderr': function('s:on_stdout', [l:opts], a:self), 24 | \ 'on_exit': function('s:on_stdout', [l:opts], a:self), 25 | \ }) 26 | 27 | call packager#utils#restore_shell(l:save_shell) 28 | 29 | if l:job <= 0 30 | let a:self[a:key] = l:opts.all ? [] : '' 31 | endif 32 | endfunction 33 | 34 | function! s:on_stdout(opts, id, message, event) dict abort 35 | if a:event ==? 'exit' 36 | if a:opts.all 37 | let self[a:opts.key] = a:opts.out 38 | else 39 | let self[a:opts.key] = get(a:opts.out, 0, '') 40 | endif 41 | 42 | if !empty(a:opts.formatter) 43 | let self[a:opts.key] = a:opts.formatter(self[a:opts.key]) 44 | endif 45 | return a:opts 46 | endif 47 | 48 | let l:msg = type(a:message) ==? type('') ? [a:message] : a:message 49 | for l:msg in a:message 50 | call add(a:opts.out, l:msg) 51 | endfor 52 | endfunction 53 | 54 | function! packager#utils#status_ok(name, status_text) abort 55 | return packager#utils#status('ok', a:name, a:status_text) 56 | endfunction 57 | 58 | function! packager#utils#status_progress(name, status_text) abort 59 | return packager#utils#status('progress', a:name, a:status_text) 60 | endfunction 61 | 62 | function! packager#utils#status_error(name, status_text) abort 63 | return packager#utils#status('error', a:name, a:status_text) 64 | endfunction 65 | 66 | function! packager#utils#status_icons() abort 67 | return { 68 | \ 'ok': '✓', 69 | \ 'error': '✗', 70 | \ 'waiting': '+', 71 | \ 'progress': join(s:progress, ''), 72 | \ } 73 | endfunction 74 | 75 | function! packager#utils#status(icon, name, status_text) abort 76 | let l:icons = packager#utils#status_icons() 77 | let l:icon = l:icons[a:icon] 78 | if a:icon ==? 'progress' 79 | let l:icon = s:progress[s:progress_counter] 80 | let s:progress_counter += 1 81 | if s:progress_counter >= len(s:progress) - 1 82 | let s:progress_counter = 0 83 | endif 84 | endif 85 | return printf('%s %s — %s', l:icon, a:name, a:status_text) 86 | endfunction 87 | 88 | function! packager#utils#confirm(question) abort 89 | let l:confirm = packager#utils#confirm_with_options(a:question, "&Yes\nNo") 90 | return l:confirm ==? 1 91 | endfunction 92 | 93 | function! packager#utils#confirm_with_options(question, options) abort 94 | silent! exe 'redraw' 95 | try 96 | let l:option = confirm(a:question, a:options) 97 | return l:option 98 | catch 99 | return 0 100 | endtry 101 | endfunction 102 | 103 | function! packager#utils#add_rtp(path) abort 104 | if empty(&runtimepath) 105 | let &runtimepath = a:path 106 | else 107 | let &runtimepath .= printf(',%s', a:path) 108 | endif 109 | endfunction 110 | 111 | function! packager#utils#trim(str) abort 112 | return substitute(a:str, '^\s*\(.\{-}\)\s*$', '\1', '') 113 | endfunction 114 | 115 | function! packager#utils#check_support() abort 116 | if !has('packages') 117 | throw '"packages" feature not supported by this version of (Neo)Vim.' 118 | endif 119 | 120 | if !has('nvim') && !has('job') 121 | throw '"jobs" feature not supported by this version of (Neo)Vim.' 122 | endif 123 | endfunction 124 | 125 | function! packager#utils#set_shell() abort 126 | let l:save_shell = [&shell, &shellcmdflag, &shellredir] 127 | 128 | if has('win32') 129 | set shell=cmd.exe shellcmdflag=/c shellredir=>%s\ 2>&1 130 | else 131 | set shell=sh shellredir=>%s\ 2>&1 132 | endif 133 | 134 | return l:save_shell 135 | endfunction 136 | 137 | function! packager#utils#restore_shell(saved_shell) abort 138 | let [&shell, &shellcmdflag, &shellredir] = a:saved_shell 139 | endfunction 140 | 141 | function! packager#utils#load_plugin(plugin) abort 142 | call packager#utils#add_rtp(a:plugin.dir) 143 | for l:path in ['plugin/**/*.vim', 'after/plugin/**/*.vim'] 144 | let l:full_path = printf('%s/%s', a:plugin.dir, l:path) 145 | if !empty(glob(l:full_path)) 146 | silent exe 'source '.l:full_path 147 | endif 148 | endfor 149 | endfunction 150 | 151 | function! packager#utils#setline(line, content) abort 152 | let l:packager_winnr = bufwinnr('__packager__') 153 | 154 | if l:packager_winnr < 0 155 | return 156 | endif 157 | 158 | if winnr() !=? l:packager_winnr 159 | silent! exe l:packager_winnr.'wincmd w' 160 | endif 161 | 162 | call setline(a:line, a:content) 163 | endfunction 164 | 165 | function! packager#utils#symlink(from, to) abort 166 | if executable('ln') 167 | return ['ln', '-sf', a:from, a:to] 168 | endif 169 | 170 | if has('win32') && executable('mklink') 171 | return ['mklink', a:from, a:to] 172 | endif 173 | 174 | return [] 175 | endfunction 176 | 177 | function! packager#utils#git_version() abort 178 | let result = get(packager#utils#system(['git', '--version']), 0, '') 179 | if empty(result) 180 | return [] 181 | endif 182 | 183 | let components = split(result, '\D\+') 184 | if empty(components) 185 | return [] 186 | endif 187 | 188 | return map(components, 'str2nr(v:val)') 189 | endfunction 190 | -------------------------------------------------------------------------------- /lua/packager.lua: -------------------------------------------------------------------------------- 1 | local packager = {} 2 | local callback = nil 3 | local options = nil 4 | 5 | function packager.setup(cb, opts) 6 | callback = cb 7 | options = opts or vim.empty_dict() 8 | 9 | vim.cmd [[command! -nargs=* -bar PackagerInstall call luaeval("require'packager'.run_cmd('install', _A)", ) ]] 10 | vim.cmd [[command! -nargs=* -bar PackagerUpdate call luaeval("require'packager'.run_cmd('update', _A)", ) ]] 11 | vim.cmd [[command! -bar PackagerClean lua require'packager'.run_cmd('clean')]] 12 | vim.cmd [[command! -bar PackagerStatus lua require'packager'.run_cmd('status')]] 13 | end 14 | 15 | function packager.run_cmd(name, args) 16 | vim.fn['packager#init'](options) 17 | callback({ 18 | add = vim.fn['packager#add'], 19 | ['local'] = vim.fn['packager#local'] 20 | }) 21 | 22 | if name == 'install' or name == 'update' then 23 | return vim.fn['packager#'..name](args or vim.empty_dict()) 24 | end 25 | 26 | return vim.fn['packager#'..name]() 27 | end 28 | 29 | return packager 30 | -------------------------------------------------------------------------------- /plugin/packager.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_vim_packager') 2 | finish 3 | endif 4 | let g:loaded_vim_packager = 1 5 | 6 | function packager#init(...) abort 7 | let s:packager = packager#new(a:0 > 0 ? a:1 : {}) 8 | endfunction 9 | 10 | function! packager#add(name, ...) abort 11 | call s:ensure_init() 12 | call s:packager.add(a:name, get(a:, 1, {})) 13 | endfunction 14 | 15 | function! packager#local(name, ...) abort 16 | call s:ensure_init() 17 | call s:packager.local(a:name, get(a:, 1, {})) 18 | endfunction 19 | 20 | function! packager#install(...) abort 21 | call s:ensure_init() 22 | call s:packager.install(a:0 > 0 ? a:1 : {}) 23 | endfunction 24 | 25 | function! packager#update(...) abort 26 | call s:ensure_init() 27 | call s:packager.update(a:0 > 0 ? a:1 : {}) 28 | endfunction 29 | 30 | function! packager#clean() abort 31 | call s:ensure_init() 32 | call s:packager.clean() 33 | endfunction 34 | 35 | function! packager#status() abort 36 | call s:ensure_init() 37 | call s:packager.status() 38 | endfunction 39 | 40 | function! packager#plugins() abort 41 | call s:ensure_init() 42 | return s:packager.get_plugins() 43 | endfunction 44 | 45 | function! packager#plugin_names() abort 46 | call s:ensure_init() 47 | return s:packager.get_plugin_names() 48 | endfunction 49 | 50 | function! packager#plugin(name) abort 51 | call s:ensure_init() 52 | return s:packager.get_plugin(a:name) 53 | endfunction 54 | 55 | function! s:ensure_init() 56 | if !exists('s:packager') 57 | return packager#init() 58 | endif 59 | return s:packager 60 | endfunction 61 | 62 | function! s:call_packager_method(method, ...) abort 63 | if a:0 > 0 64 | return s:packager[a:method](a:1) 65 | endif 66 | 67 | return s:packager[a:method]() 68 | endfunction 69 | 70 | function! packager#setup(callback, ...) abort 71 | let l:opts = get(a:, 1, {}) 72 | if empty(a:callback) 73 | throw 'Provide valid callback to packager setup via string or funcref.' 74 | endif 75 | if type(a:callback) !=? '' && type(a:callback) !=? type(function('tr')) 76 | throw 'Packager callback must be a string or a funcref/function.' 77 | endif 78 | 79 | if type(a:callback) ==? type('') && !exists('*'.a:callback) 80 | throw 'packager function '.a:callback.' does not exist. Try providing a function or funcref.' 81 | endif 82 | 83 | let s:callback = type(a:callback) ==? type('') ? funcref(a:callback) : a:callback 84 | let s:opts = get(a:, 1, {}) 85 | 86 | command! -nargs=* -bar PackagerInstall call s:run_cmd('install', ) 87 | command! -nargs=* -bar PackagerUpdate call s:run_cmd('update', ) 88 | command! -bar PackagerClean call s:run_cmd('clean', ) 89 | command! -bar PackagerStatus call s:run_cmd('status', ) 90 | endfunction 91 | 92 | function! s:run_cmd(name, ...) 93 | call packager#init(s:opts) 94 | call s:callback(s:packager) 95 | let l:args = [] 96 | if a:name ==? 'install' || a:name ==? 'update' 97 | let l:args = [get(a:, 1, {})] 98 | endif 99 | 100 | return call('packager#'.a:name, l:args) 101 | endfunction 102 | 103 | nnoremap (PackagerQuit) :call call_packager_method('quit') 104 | nnoremap (PackagerOpenSha) :call call_packager_method('open_sha') 105 | nnoremap (PackagerOpenStdout) :call call_packager_method('open_stdout') 106 | nnoremap (PackagerGotoNextPlugin) :call call_packager_method('goto_plugin' ,'next') 107 | nnoremap (PackagerGotoPrevPlugin) :call call_packager_method('goto_plugin', 'previous') 108 | nnoremap (PackagerStatus) :call call_packager_method('status') 109 | nnoremap (PackagerPluginDetails) :call call_packager_method('open_plugin_details') 110 | --------------------------------------------------------------------------------