9

I've been working on modularizing & converting a code in my vimrc into some self-contained and reusable bundles/plugins plugins. I've run into an issue with autoloading & scope that I'm having difficulty understanding. I've read through :h autoload, :h <sid>, :h script-local, but I'm still not quite clear on how this works.

I've been looking at some well-developed plugins to figure out some commonly used patterns, and have structured my plugins as follows:

" ~/.vim/autoload/myplugin.vim

if exists('g:loaded_myplugin')
  finish
endif

let g:loaded_myplugin = 1
let g:myplugin_version = 0.0.1

" Save cpoptions.
let s:cpo_save = &cpo
set cpo&vim

function! myplugin#init() " {{{
  " Default 'init' function. This will run the others with default values,
  " but the intent is that they can be called individually if not all are
  " desired.
  call myplugin#init_thing_one()
  call myplugin#init_thing_two()
endfunction" }}}

function! myplugin#init_thing_one() " {{{
  " init thing one
  call s:set_default('g:myplugin_thing_one_flag', 1)
  " do some things ...
endfunction " }}}

function! myplugin#init_thing_two() " {{{
  " init thing two
  call s:set_default('g:myplugin_thing_two_flag', 1)
  " do some things ...
endfunction " }}}

function! s:set_default(name, default) " {{{
" Helper function for setting default values.
  if !exists(a:name)
    let {a:name} = a:default
  endif
endfunction " }}}

" Restore cpotions.
let &cpo = s:cpo_save
unlet s:cpo_save

At the start of my vimrc, I run the plugin with:

if has('vim_starting')
  if &compatible | set nocompatible | endif
  let g:myplugin_thing_one_flag = 0
  let g:myplugin_thing_two_flag = 2
  call myplugin#init()
endif

This all seems to work correctly and as expected - but each time a function is called, the s:set_default(...) function is called for each flag, which is ineffecient - so I attempted to move them out the functions:

" ~/.vim/autoload/myplugin.vim
" ...
set cpo&vim

" Set all defaults once, the first time this plugin is referenced:
call s:set_default('g:myplugin_thing_one_flag', 1)
call s:set_default('g:myplugin_thing_two_flag', 1)

function! myplugin#init() " {{{
" ...

But this causes errors that I'm unsure how I should resolve:

Error detected while processing /Users/nfarrar/.vim/myplugin.vim
line   40:
E117: Unknown function: <SNR>3_set_default

I still don't solidly understand vim's scoping, but from what I've read - it seems that vim implements a form of name-mangling with scripts to provide 'scope'. It assigns (not sure how exactly this process works) a unique SID for each file that is loaded at runtime - and when you call a function that is prefixed with a script-scope identifier (s:), it transparently replaces that identifier with a mapped SID.

In some cases, I've seen scripts that call functions like this (but it doesn't work in my case, I don't understand why, and hoping someone can explain this):

call <SID>set_default('g:myplugin_thing_one_flag', 1)
call <SNR>set_default('g:myplugin_thing_one_flag', 1)

The following does work, but I'm unsure if it's a good pattern:

" ~/.vim/autoload/myplugin.vim
" ...
set cpo&vim

" Set all defaults once, the first time this plugin is referenced:
call myplugin#set_default('g:myplugin_thing_one_flag', 1)
call myplugin#set_default('g:myplugin_thing_two_flag', 1)

function! myplugin#init() " {{{
" ...

function! myplugin#set_default(name, default) " {{{
    " ...
endfunction " }}}

In script local, it states:

When executing an autocommand or a user command, it will run in the context of
the script it was defined in.  This makes it possible that the command calls a
local function or uses a local mapping.

Otherwise, using "<SID>" outside of a script context is an error.

If you need to get the script number to use in a complicated script, you can
use this function:

    function s:SID()
      return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
    endfun

It sees like this might be the approach I need to take, but I'm not completely sure why, or exactly how to use it. Can anyone provide some insight?

4

In the first case, the error you are getting is that you are trying to call a function prior to its existance. That is, Vim is progressing through your script. When it sees the call line, it hasn't yet processed the function line that creates what you want to call, resulting in the error unknown function.

If you move your call to setup defaults to the end of your script (before you restore cpo but after all of your functions, there won't be an error, because Vim will have processed the script to create the functions first, so once it gets to the call lines, the functions exist. e.g.

"....

function! s:set_default(name, default) " {{{
  " Helper function for setting default values.
  if !exists(a:name)
    let {a:name} = a:default
  endif
endfunction " }}}

" Set all defaults once, the first time this plugin is referenced:
call s:set_default('g:myplugin_thing_one_flag', 1)
call s:set_default('g:myplugin_thing_two_flag', 1)

" Restore cpotions.
let &cpo = s:cpo_save
unlet s:cpo_save

I do not know why the alternative syntax of calling your set_default as an autoload function from within the autoload script works when the function hasn't yet been defined. My guess is this is a side effect of the implementation (where an already read script isn't re-read, or you'd have infinite recursion). I wouldn't count on it always working that way.

  • If I'm understanding correctly, you're saying the entire file isn't sourced & executed prior to the execution of the function call defined in my vimrc? Maybe I'm misunderstanding ... but it seems to me the entire autoload script is sourced & executed first. If I add a the statement echom 'this is the function call' in the function being called from vimrc and another echom 'file was sourced' anywhere else in the file (not in a function), I see the latter first, then the former. – nfarrar Jun 28 '15 at 12:04
  • Sorry, I just realized what you're saying - and that you're correct. Since the function call is happening in the script as it is sourced, it needs to happen after the function is defined. Thank you! – nfarrar Jun 28 '15 at 12:05
12

I recommend this structure:

.
└── myplugin
    ├── LICENSE.txt
    ├── README.md
    ├── autoload
    │   └── myplugin.vim
    ├── doc
    │   └── myplugin.txt
    └── plugin
        └── myplugin.vim

This is compatible to all modern plugin managers, and keeps things clean. Of these:

  • myplugin/doc/myplugin.txt should be the help file
  • myplugin/plugin/myplugin.vim should contain:
    • checks for any prerequisits your plugin needs, such as minimal Vim version and Vim compile-time features
    • key mappings
    • one time initialisation, such as setting defaults
  • myplugin/autoload/myplugin.vim should contain the main code of your plugin.

Scopes are actually pretty simple:

  • functions with names starting with s: may appear anywhere, but are local to the file they are defined in; you can only call them from the file where they are defined;
  • functions in myplugin/autoload/myplugin.vim must have names myplugin#function() and are global; you can call them from everywhere (but beware that calling them causes the file myplugin/autoload/myplugin.vim to be loaded);
  • all other functions must have names starting with an upper case letter, such as Function(), and are also global; you can call them from everywhere.

<SID> and <Plug> are used for mappings, and they are a topic that you should probably avoid until you have a full understanding of how they work and what problem they are supposed to solve.

<SNR> is something that you should never use directly.

  • Why separate between the autoload/ and plugin/ directories? I've always put everything in plugin/ and that seems to work fine? – Martin Tournoij Jun 27 '15 at 17:30
  • 1
    The autoload directory only loads it's contents when needed. This can speed up the start time of vim somewhat. In other words, it works about the same as plugin/ except it is only loaded once it is needed instead of being loaded on startup. – EvergreenTree Jun 27 '15 at 17:33
  • 2
    @Carpetsmoker Pretty much as @EvergreenTree said. Of course, it doesn't really matter for "small" plugins, but it's still good practice. With Vim you have very little control on the garbage collector, and loading things only when and if they are needed can make a difference. On the other hand, there are subtle downsides to moving everything to autoload, f.i. you can't test for the existence of a function if the file it lives in hasn't been loaded. – lcd047 Jun 27 '15 at 18:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.