Skip to content

Instantly share code, notes, and snippets.

@BlueDrink9
Last active February 29, 2024 16:27
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BlueDrink9/474b150c44d41b80934990c0acfb00be to your computer and use it in GitHub Desktop.
Save BlueDrink9/474b150c44d41b80934990c0acfb00be to your computer and use it in GitHub Desktop.
Share your vim-plug plugins with your lazy.nvim neovim setup, with minimal performance loss

lazy.nvim adapter for vim-plug

This is a drop-in adapter layer for vim-plug and lazy.nvim, allowing you to have a core set of vim plugins, configured with Plug, which will also be loaded using lazy.nvim instead whenever you use neovim (alongside any additional set of lazy.nvim plugins you have configured.)

It works by adding a new command Plugin as a drop-in Plug replacement. That calls a function that sets up lazy.nvim specs for the equivalent Plug args, and stores them in a variable that you can then add to your lazy spec.

It also:

  • Installs plug if you are in vim, lazy.nvim if you are in neovim.
  • Allows these extra lazy load arguments compared to Plug: event, afterLoad (if using nvim, will convert to lazy.nvim's config, i.e. runs after plugin is lazyloaded).
  • Adds the 'dependencies' option to Plug, for simple delcarations of additional plugins.

This script prioritises neovim in terms of performance - where converting or extra code could be added for only one or the other, vim / vim-plug is always given the extra load.

Usage:

First, find and replace all instances of "Plug '" in your config with 'Plugin'.

Ensure these lines exist in your lazy config:

require("lazy").setup({
  root = vim.g.pluginInstallPath,  -- share plugin folder with Plug
  spec = {
      ...
      LazyPlugSpecs,
      }
})

Then add this to your .vimrc, around where you have your vim-plug setup.

let g:pluginInstallPath = "~/.vim/plugins"
" First source this gist:
source "plug_lazy_adapter.vim"
if !has('nvim')
    call plug#begin(g:pluginInstallPath)
endif

" uses default config function name
Plugin 'machakann/vim-sandwich', {'keys': ['ys', 'ds', 'cs'],
            \ 'afterLoad': v:true}
function! Plug_after_vim_sandwich()
    runtime macros/sandwich/keymap/surround.vim
endf
" Custom afterLoad function
Plugin 'https://github.com/benknoble/vim-auto-origami', {
            \ 'on': 'AutoOrigamiFoldColumn',
            \ 'event': ["CursorHold","BufWinEnter","WinEnter"],
            \ 'afterLoad': 'PluginAfterAutoOrigami'}
function! PluginAfterAutoOrigami()
    au myPlugins CursorHold,BufWinEnter,WinEnter * AutoOrigamiFoldColumn
endf
" Keys will do nothing in vim, but will create lazyLoad keys for lazy.nvim.
Plugin 'wellle/targets.vim', {
            \ 'keys': MakeLazyKeys({
            \ 'n': ['[', ']'],
            \ 'ov': ['i,', 'a', 'I', 'A'],
            \ })}

let g:vim_shell_plug_args = {'dependencies': ['xolox/vim-misc']}
if has('nvim')
    let g:vim_shell_plug_args['event'] = 'VeryLazy'
endif
Plugin 'https://github.com/xolox/vim-shell', g:vim_shell_plug_args

if !has('nvim')
    call plug#end()
else
    lua require("config.lazy")
endif

Finally, copy in this adapter script to your config:

plug_lazy_adapter.vim

Note: The latest version of this script, if any updates haven't propagated to the gist, resides in my dotfiles (permalink)

" This command is your main interface to the plugin:
" args: anything vim-plug supports in its extra options, plus:
" * afterLoad: the name of a vimscript function to run after the plugin
" lazy-loads.
" * event: list of autocmd events that the plugin should lazy-load on
" * Any other lazy.nvim spec config options that don't have a Plug alternative
" (Note, for `keys`, modes are only supported via the special MakeLazyKeys
" function.)
command! -bang -nargs=+ Plugin call <sid>PluginAdapter(<args>)

function! GetPluginName(pluginUrl)
    " Get plugin name out of a plugin spec name/url. Fairly simplistic, so
    " don't include .git at the end of your urls.
    return split(a:pluginUrl, '/')[-1]
endfunction

" Define function to check if a plugin is added to the manager.
" Accepts only the last part of the plugin name (See GetPluginName()).
" Usage: IsPluginUsed('nvim-treesitter')
if has('nvim')
    lua IsPluginUsed = function(name) return require("lazy.core.config").plugins[name] ~= nil end
endif

function! IsPluginUsed(name)
    if has('nvim')
        return has_key(s:plugs, a:name)
    else
        return has_key(g:plugs, a:name)
    endif
endfunction

" To remove a Plugged repo using UnPlug 'pluginName'
function! s:deregister(name)
    " name: See GetPluginName()
    try
        if has('nvim')
            call remove(s:plugs, a:name)
            return
        else
            call remove(g:plugs, a:name)
            call remove(g:plugs_order, index(g:plugs_order, a:name))
        endif
        " strip anything after period because not legitimate variable.
        let l:varname = substitute(a:name, '\..*', '', '')
        let l:varname = substitute(l:varname, 'vim-', '', '')
        exec 'let g:loaded_' . l:varname . ' = 1'
    catch /^Vim\%((\a\+)\)\=:E716:/
        echom 'Unplug failed for ' . a:name
    endtry
endfunction
command! -nargs=1 -bar UnPlug call s:deregister(<args>)

if has('nvim')
    lua << EOF
    -- Allow making a keys table with modes for Lazy.vim in vimscript
    -- Expects a dictionary where keys are a string of modes and values are
    -- a list of keys.
    -- usage: add plugin opt:
    -- Plugin 'abc/def', {'keys': MakeLazyKeys({
    --             " \ 'n': ['[', ']'],
    --             " \ 'ov': ['i,', 'a', 'I', 'A'],
    --             " \ })}
    -- Returns a lua function for setting up a lazy keys spec. Need to return a
    -- function because can't return a mixed list/dict table in vimscript.
    MakeLazyKeys = function(args)
        return function()
            local ret = {}
            for modes, keys in pairs(args) do
                for _, key in ipairs(keys) do
                    modesT = {}
                    for i = 1, #modes do
                        modesT[i] = modes:sub(i, i)
                    end
                    table.insert(ret, { key, mode = modesT})
                end
            end
            return ret
        end
    end
EOF

    function! MakeLazyKeys(args)
        return luaeval('MakeLazyKeys(_A[1])', [a:args])
    endfunction
else
    function! MakeLazyKeys(args)
        return []
    endfunction
endif

" Passes plugin to a lua function that creates a lazy.nvim spec, or to a vimscript
" function to adapt the args for vim-plug.
function! s:PluginAdapter(...)
    let l:plugin = a:1
    let l:args = {}
    if a:0 == 2
        let l:args = a:2
    endif
    if has('nvim')
        " Has to be global so lua sees it
        let g:__plugin_args = l:args
        exec 'lua PlugToLazy("' . l:plugin  . '", vim.g.__plugin_args)'
        let s:plugs[GetPluginName(l:plugin)] = 1
    else
        call PlugPlusLazyArgs(l:plugin, l:args)
    endif
endfunction

if has('nvim')
let s:plugs = {}
lua << EOF
LazyPlugSpecs = {}
-- Compatibility function to convert vim-plug's Plug command to lazy.nvim spec
function PlugToLazy(plugin, opts)
    -- Build lazy plugin spec, converting any vim-plug options.
    local lazySpec = {}
    if opts then
        lazySpec = opts
        lazySpec.ft = opts["for"]
        -- lazySpec.for = nil
        lazySpec.name = opts["as"]
        lazySpec.as = nil
        lazySpec.cmd = opts["on"]
        lazySpec.on = nil
        lazySpec.version = opts["tag"]
        if opts['afterLoad'] then
            lazySpec['config'] = function()
                -- Either call the default afterLoad function...
                if opts['afterLoad'] == true then
                    vim.fn[vim.fn.PluginNameToFunc(
                        vim.fn.GetPluginName(plugin)
                    )]()
                else
                    -- ...or call the specified name.
                    vim.fn[opts['afterLoad']]()
                end
            end
        end
        if lazySpec.cmd then
            -- Ensure it is a list/table
            if type(lazySpec.cmd) == "string" then
                lazySpec.cmd = {lazySpec.cmd}
            end
            -- <plug> mappings are commands ('on') for Plug, but keys for Lazy
            for k, cmd in pairs(lazySpec.cmd) do
                if string.find(string.lower(cmd), "<plug>", 1, 6) then
                    lazySpec.keys = lazySpec.keys or {}
                    -- Convert plug mappings for all modes, not just default of 'n'
                    table.insert(lazySpec.keys, {cmd, mode={'n','v','o','l'}})
                    lazySpec.cmd[k] = nil
                end
            end
            -- Remove any empty cmd table to prevent lazyload (may be empty if
            -- used to force a lazy load in Plug)
            if not lazySpec.cmd then lazySpec.cmd = nil end
        end
    end
    lazySpec[1] = plugin
    table.insert(LazyPlugSpecs, lazySpec)
end
EOF
endif

function! PluginNameToFunc(name)
    " Convert a plugins name to default function name to use for afterLoad
    " functions.
    " Has to be a global function so lua can access it.
    return 'Plug_after_' . substitute(a:name, '[\\.-]', '_', 'g')
endfunction

function! s:installPluginManager()
    if has('nvim')
        let g:loaded_plug = 1
        lua << EOF
        local lazypath = vim.g.pluginInstallPath .. "/lazy.nvim"
        if not vim.loop.fs_stat(lazypath) then
            -- bootstrap lazy.nvim
            -- stylua: ignore
            vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", "--branch=stable", lazypath })
        end
        vim.opt.rtp:prepend(vim.env.LAZY or lazypath)
EOF

    else
        if has('win32') || has ('win64')
            let l:vimhome = $HOME."/vimfiles"
        else
            let l:vimhome = $HOME."/.vim"
        endif
        let l:plugin_manager_dir = expand(l:vimhome . '/autoload')
        let l:plugin_manager_file = l:plugin_manager_dir . '/plug.vim'
        let s:plugin_manager_url = "https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim"

        if !filereadable(l:plugin_manager_file)
            exec "silent !mkdir -p " . l:plugin_manager_dir
            if Executable("curl")
                let s:downloader = "curl -fLo "
            elseif Executable("wget")
                let s:downloader = "wget --no-check-certificate -O "
            else
                echoerr "You have to install curl or wget, or install plugin manager yourself!"
                echoerr "Plugin manager not installed. No plugins will be loaded."
                finish
            endif
            " Continue installing...
            echom "Installing plugin manager..."
            echo ""
            call system(printf('%s %s %s', s:downloader, l:plugin_manager_file, s:plugin_manager_url))
            if !filereadable(l:plugin_manager_file)
                echoerr "Plugin manager not installed. No plugins will be loaded."
                finish
            endif
            autocmd myPlugins VimEnter * PlugInstall
        endif

    endif
endfunction
call s:installPluginManager()

" Rest is entirely plug-specific
if has('nvim')
    finish
endif

function! PlugPlusLazyArgs(plugin, args)
    let l:plugin_name = GetPluginName(a:plugin)
    let l:args = a:args
    " convert lazy args we want to keep when using plug
    for dep in get(l:args, 'dependencies', [])
        Plug dep
    endfor
    " Handle hook for after load
    let l:func = get(l:args, 'afterLoad', v:false)
    " If 'afterLoad' is v:true, call function based off a default name
    " convention (the plugin name, with _ replacing . and -). Otherwise
    " call the function name passed in. Only will be called for
    " lazy-loaded plugins, so don't use without an 'on' or 'for' mapping.
    " ('keys' gets ignored).
    if l:func == v:true
        exec 'au User ' . l:plugin_name . ' call ' . PluginNameToFunc(l:plugin_name) . '()'
    elseif l:func != v:false
        exec 'au User ' . l:plugin_name . ' call ' . l:func . '()'
    endif

    for event in get(l:args, 'event', [])
        " Removes unsupported events, e.g. VeryLazy.
        if !exists('##' . event)
            continue
        endif
        call s:loadPluginOnEvent(l:plugin_name, event)
        " Add empty 'on' argument to enable lazyloading.
        if !has_key(l:args, 'on')
            let l:args['on'] = []
        endif
    endfor

    call s:removeUnsupportedArgs(l:args)
    Plug a:plugin, l:args
endfunction

function! s:removeUnsupportedArgs(args)
    " Remove args unsupported by Plug
    let l:PlugOpts = [
                \ 'branch',
                \ 'tag',
                \ 'commit',
                \ 'rtp',
                \ 'dir',
                \ 'as',
                \ 'do',
                \ 'on',
                \ 'for',
                \ 'frozen',
                \ ]
    for opt in keys(a:args)
        if index(l:PlugOpts, opt) < 0  " If item not in the list.
            silent! call remove(a:args, opt)
        endif
    endfor
endfunction

" Add support for loading Plug plugins on specific event.
function! s:loadPluginOnEvent(name, event)
    " Plug-loads function on autocmd event.
    " Plug options should include 'on': [] to prevent load before event.
    " name: the last part of the plugin url (See GetPluginName()).
    " event: name of autocmd event
    " Example:
    " Plug 'ycm-core/YouCompleteMe', {'on': []}
    " call LoadPluginOnEvent('YouCompleteMe', 'InsertEnter')
    let l:plugLoad = 'autocmd ' . a:event . ' * call plug#load("'
    let l:plugLoadEnd = '")'
    let l:augroupName = a:name . '_' . a:event
    let l:undoAutocmd = 'autocmd! ' . l:augroupName
    exec 'augroup ' . l:augroupName
        autocmd!
        exec  l:plugLoad . a:name . l:plugLoadEnd . ' | ' . l:undoAutocmd
    exec 'augroup END'
endfunction
@baco
Copy link

baco commented Jan 14, 2024

Couldn't all this come as a Vim/Neovim plugin itself? I mean, is a lot of code to keep up to date by hand when updates come.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment