1

Context

Some time ago, I added the following line to ~/.vim/after/ftplugin/sh.vim to make working with parameter expansions easier:

setlocal iskeyword+=$

This is nice to be able to e.g. ysiw" via surroud.vim to surround a parameter expansion in quotes.

Problem

However, I've noticed that when doing keyword-local completion (i_<C-n>), typing $partial_var_name will not complete to full_var_name unless $full_var_name is also present in the file already. This I sort of expect.

I would like to fix it.

Attempted Solution

I tried the following (there is no CompleteStart event, unfortunately):

 " Fix keywords
 setlocal iskeyword+=$

augroup sh_ftplugin
  au!
  au MenuPopup setlocal iskeyword-=$
  au CompleteDone setlocal iskeyword+=$
augroup END

It did not work.

TL;DR

My question is thus, how can I make insert-mode completion work the way I would like?

My thoughts

  • by ignoring iskeyword in completion,
  • modifying it during completion
  • defining a text object for parameter expansions—neat plugin idea, by the way
  • changing how I use iskeyword as a last resort (that is, removing $ permanently)
1

I wrote my own solution by creating a parameter-expansion text object, something I'd been meaning to do for a while anyway.

I include the full code at the very end, which I place in ~/.vim/autoload/sh.vim.

Then, in my ~/.vim/after/ftplugin/sh.vim, I remove the setlocal iskeyword+=$ line. This is the key step, since it allows completion to work again.

I add

xnoremap <buffer> <silent> iv :<C-u>call sh#in_parameter_expansion()<CR>
onoremap <buffer> <silent> iv :<C-u>call sh#in_parameter_expansion()<CR>

This allows me to do things like viv and div to modify parameter expansions. iv stands for "in variable" (ip and ap are taken for paragraphs)—one can easily use whatever mappings they wish.


Bonus

I also created the following mapping to make use of surround.vim as mentioned in the question:

" Relies on surround.vim and custom text object above
nmap <buffer> <silent> <LocalLeader>qv ysiv"

We need nmap here because we want recursive mapping expansion. But now a simple <LocalLeader>qv (read: quote variable) will quote a parameter expansion (this is a common refactor for me).

In the most up-to-date version, I include a text-object for subshells ($(...)). It handles nested expansions and arithmetic as well. See the link under Code.


Code

Well commented, with some patterns generalized.

You can find the most up-to-date version in my dotfiles on github.

" ~/.vim/autoload/sh.vim
" Based on https://vimways.org/2018/transactions-pending/

" Text object for shell parameter expansions
" Definitions:
"   Parameter: An entity that stores values. It can be a name, a number, or one
"   of the special characters listed below. A variable is a parameter denoted by
"   a name.
"   Name: A word consisting solely of letters, numbers, and underscores, and
"   beginning with a letter or underscore. Names are used as shell variable and
"   function names. Also referred to as an identifier.
"   Positional Parameters: Any parameter denoted by one or more digits. Those
"   with multiple digits require braces.
"   Special Parameters: The following list:
"     - *
"     - @
"     - #
"     - ?
"     - -
"     - $
"     - !
"     - 0
"     - _
"  Expansion: Any usage of a parameter's value in place of its name: the common
"  forms include $var and ${var}. The more 'esoteric' options are documented
"  under 'Parameter Expansion' in bash(1).
"
"  In general, one wants to select the entire expansion construct in order to
"  e.g. surround in quotes (a common idiom). This means:
"    - $
"    - a parameter (name or special); or,
"    - a brace construction:
"      - {
"      - !, optionally                               -+
"      - parameter (name or special)                  |-basically 'anything'
"      - an expansion option (see above), optionally -+
"      - }
"
"  In the case of the braces, we can simply select the ${...}. Sans braces, we
"  have to match exactly the definition of a parameter. We construct a regex for
"  this purpose.

function! s:branchify_atoms(atoms)
  return join(a:atoms, '\|')
endfunction

function! s:group_atoms(atoms)
  return map(copy(a:atoms), 'printf("\\(%s\\)", v:val)')
endfunction

" matches a name (no braces)
let s:name_pattern = '\m' " magic mode
let s:name_pattern .= '\(\a\|_\)' " begins with alpha or underscore
let s:name_pattern .= '\(\a\|\d\|_\)*' " letters, numbers, underscores

" matches a positional parameter (no braces)
let s:positional_pattern = '\m' " magic
let s:positional_pattern .= '[1-9]' " a single digit

" matches any of the special parameters (no braces)
let s:special_pattern = '\m' " magic
let s:special_pattern .= s:branchify_atoms(
      \ [
      \ '\*',
      \ '@',
      \ '#',
      \ '?',
      \ '-',
      \ '\$',
      \ '!',
      \ '0',
      \ '_'
      \ ]
      \ ) " special params as branches

let s:parameter_pattern = '\m' " magic
let s:parameter_pattern .= s:branchify_atoms(
      \ s:group_atoms(
      \   [s:name_pattern, s:positional_pattern, s:special_pattern],
      \   )
      \ )

" matches anything in braces
let s:brace_pattern = '\m' " magic
let s:brace_pattern .= '{' " opening brace
let s:brace_pattern .= '.\{-}' " anything, but non-greedy: there may be two braces in one line
let s:brace_pattern .= '}' " closing brace

" matches a $ followed by a parameter or a brace construction
let s:expansion_pattern = '\m' " magic
let s:expansion_pattern .= '\$' " literal '$'
let s:expansion_pattern .= s:group_atoms(
      \ [s:branchify_atoms(
      \   s:group_atoms(
      \     [s:parameter_pattern, s:brace_pattern],
      \     )
      \   )]
      \ )[0] " extract the one and only element

function! sh#in_parameter_expansion() abort
  let l:line = line('.')
  if !search(s:expansion_pattern, 'ce', l:line)
    return
  endif
  normal! v
  call search(s:expansion_pattern, 'cb', l:line)
endfunction

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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