5

I would like to create a global (configuration) variable that ideally would work as a comma separated list but that we could also append to it. The idea is to have the same behavior as when using += when setting &runtimepath.

So, it would be possible to do something like:

let g:myvar+="newitem"

And the variable contents would become:

"item1","item2","newitem"

Two questions:

  1. Can such a variable be implemented in Vim (and how)?
  2. Would it be possible to allow and define a list for its tab completion?
6

You could have your variable be a list and add values with :let g:var += ['item']. However, it won't be possible to prevent the same item to be added several times.

That's why I've come up with lh#list#push_if_new()

function! lh#list#push_if_new(list, value) abort
  let matching = filter(copy(a:list), 'v:val == a:value')
  if empty(matching)
    call add (a:list, a:value)
  endif
  return a:list
endfunction

It's quite cumbersome to use. Having a command that calls the function would help though. It would look like this.

" some plugin
command! -nargs=+ -complete=customlist,lh#let#_push_options_complete
      \ PushOptions call lh#let#_push_options(<f-args>)

And can be used this way regarding completion

:let g:foobar = {'titi': []}
:let g:acceptable_options_for['g:foobar.titi'] = ['s', 'foo', 'toto', 'titi' ]

:PushOptions foo<tab>
" -> g:foobar.
" <tab> again -> g:foobar.titi
" <space><tab> -> list of possible choices: s, foo, toto, titi

Completion properties:

  • It can complete variables names, and even search for list/dict fields in dictionaries
  • It can complete values from a list of possible values, which is filtered out of the values already set

And here are the functions required for the completion:

" # PushOptions {{{2
"
" Function: lh#let#_push_options(variable, ...) {{{3
function! lh#let#_push_options(variable, ...) abort
  let var = lh#let#if_undef(a:variable, '[]')
  for val in a:000
    call lh#list#push_if_new(var, val)
  endfor
  return var
endfunction

" Function: lh#let#_list_all_list_variables_in_scope(scope) {{{3
function! lh#let#_list_all_list_variables_in_scope(scope) abort
  let vars = map(keys({a:scope}), 'a:scope.v:val')
  " Keep only lists and dictionaries
  call filter(vars, 'type({v:val}) == type([]) || type({v:val}) == type({})')
  return vars
endfunction

" Function: lh#let#_list_variables(lead) {{{3
function! lh#let#_list_variables(lead) abort
  if empty(a:lead)
    " No variable spécified yet
    let vars 
          \ = lh#let#_list_all_list_variables_in_scope('g:')
          \ + lh#let#_list_all_list_variables_in_scope('b:')
          \ + lh#let#_list_all_list_variables_in_scope('w:')
          \ + lh#let#_list_all_list_variables_in_scope('t:')
  elseif stridx(a:lead, '.') >= 0
    " Dictionary
    let [all, dict, key ; trail] = matchlist(a:lead, '\v(.*)\.(.*)')
    let vars = keys({dict})
    call filter(vars, 'type({dict}[v:val]) == type([]) || type({dict}[v:val]) == type({})')
    call map(vars, 'v:val. (type({dict}[v:val])==type({})?".":"")')
    call map(vars, 'dict.".".v:val')
    return vars
  else
    " Simple variables
    if         (len(a:lead) == 1 && a:lead    =~ '[gbwt]')
          \ || (len(a:lead) > 1  && a:lead[1] == ':')
      let scope = a:lead[0]
      let filter_scope = ''
    else
      let scope = 'g'
      let filter_scope = 'g:'
    endif
    let vars = lh#let#_list_all_list_variables_in_scope(scope.':')
    call filter(vars, 'v:val =~ "^".filter_scope.a:lead')
  endif
  " Add dot to identified dictionaries
  call map(vars, 'v:val. (type({v:val})==type({})?".":"")')
  return vars
endfunction

" Function: lh#let#_push_options_complete(ArgLead, CmdLine, CursorPos) {{{3
call lh#let#if_undef('g:acceptable_options_for', '{}')

function! lh#let#_push_options_complete(ArgLead, CmdLine, CursorPos) abort
  let tmp = substitute(a:CmdLine, '\s*\S*', 'Z', 'g')
  let pos = strlen(tmp)

  call s:Verbose('complete(lead="%1", cmdline="%2", cursorpos=%3)', a:ArgLead, a:CmdLine, a:CursorPos)

  if     2 == pos
    " First argument: a variable name
    return lh#let#_list_variables(a:ArgLead)
  elseif pos >= 3
    " Doesn't handle 'foo\ bar', but we don't need this to fetch a variable
    " name
    let args = split(a:CmdLine, '\s\+')
    let varname = args[1]
    call s:Verbose('complete: varname=%1', varname)
    " Other arguments: acceptable values
    let acceptable_values = get(g:acceptable_options_for, varname, [])
    let crt_val = '\v^'.join(exists(varname)? eval(varname) : [], '|').'$'
    let acceptable_values = filter(copy(acceptable_values), 'v:val !~ crt_val')
    return acceptable_values
  endif
endfunction
3

If you want g:myvar to be a string, you could define the following commands :AddMyvar and :RemoveMyvar:

command! -nargs=1 -complete=customlist,s:CompleteMyvar AddMyvar
         \ if !exists('g:myvar') |
         \     let g:myvar = '<args>' |
         \ elseif !count(split(g:myvar, ','), '<args>') |
         \     let g:myvar .= ',<args>' |
         \ endif

command! -nargs=1 -complete=customlist,s:CompleteMyvar RemoveMyvar
    \ if exists('g:myvar') |
    \     let g:myvar = join(filter(split(g:myvar, ','), "v:val !=# '<args>'"), ',') |
    \ endif

function! s:CompleteMyvar(A, L, P) abort
    let mycandidates = ['foo', 'bar', 'baz']
    return filter(mycandidates, 'v:val =~ "^" . a:A')
endfunction

If you want g:myvar to be a list:

command! -nargs=1 -complete=customlist,s:CompleteMyvar AddMyvar
         \ if !exists('g:myvar') |
         \     let g:myvar = ['<args>'] |
         \ elseif !count(g:myvar, '<args>') |
         \     call add(g:myvar, '<args>') |
         \ endif

command! -nargs=1 -complete=customlist,s:CompleteMyvar RemoveMyvar
         \ if exists('g:myvar') |
         \     call filter(g:myvar, "v:val !=# '<args>'") |
         \ endif

function! s:CompleteMyvar(A, L, P) abort
    let mycandidates = ['foo', 'bar', 'baz']
    return filter(mycandidates, 'v:val =~ "^" . a:A')
endfunction

To use the commands, you would type:

  • :AddMyvar newitem to add newitem to g:myvar
  • :RemoveMyvar olditem to remove olditem from g:myvar

The tab completion is managed by the s:CompleteMyvar() function, which returns the list of suggestions ['foo', 'bar', 'baz'].
If you type something after one of the commands, it is compared with the beginning of each item in the list, and only the items which match it will be returned.
You would have to edit the list mycandidates so that the function returns the suggestions you really want.


To explain how :AddMyvar works a little:

command! -nargs=1 -complete=customlist,s:CompleteMyvar AddMyvar

define the :AddMyvar command which waits for one argument (-nargs=1) and will make suggestions when you hit Tab based on what the s:CompleteMyvar() function returns,

     \ if !exists('g:myvar') |

It tests if g:myvar does not exist,

     \     let g:myvar = ['<args>'] |

if it doesn't, g:myvar is defined as a single-item list containing the argument you provided on the command line. So if g:myvar does not exist and you type :AddMyvar foo then g:myvar will be ['foo'],

     \ elseif !count(g:myvar, '<args>') |

otherwise, it checks if the argument you provided is not already present inside g:myvar to avoid adding the same item by mistake.
count() is a function which returns the number of times an item is present inside a list.
In a test, a non-zero value is considered as true and a zero value as false.
By prefixing the function with a bang you invert the test. non-zero will be false and zero will be true.

     \     call add(g:myvar, '<args>') |

Finally, if the argument you provided was absent from g:myvar, it's added.

If you don't like the command spreading on several lines, you could wrap the code inside a function and make the command call the latter.


How :RemoveMyvar works :

command! -nargs=1 -complete=customlist,s:CompleteMyvar RemoveMyvar

define a similar :RemoveMyvar command,

     \ if exists('g:myvar') |

which tests if g:myvar exists,

     \     call filter(g:myvar, "v:val !=# '<args>'") |

and if it does, removes any item from g:myvar which is not different from the argument you provided.
filter() is a function which can take each item of a list, here g:myvar, and test it against an arbitrary condition, here v:val !=# '<args>'.
Inside the condition, you refer to an item from the list with the internal variable v:val.
If the test fails, filter() removes the item from the list.


For more information, see:

:help :command-completion-customlist
:help exists()
:help count()
:help split()
:help filter()
:help join()

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.