7

Edit: I turned this into a little plugin. This is my first attempt at writing one and I have no idea what I'm doing, so any help is much appreciated. :)

Here it is: https://github.com/danielbmarques/vim-dialect

----------

I want to keep using zg and zw to add words to the global vim spellfiles, but I want to use zG and zW to add words to a spellfile specific to the file I'm editing. In other words, I want zG and zW to be persistent for each file.

Setting the spellfile changes the zg and zw commands, while what I want is to change only the zG and zW commands.

So I just did this:

:au BufNewFile,BufRead * let &l:spellfile = expand('%:p:h') . '/.' .
    \ substitute(expand('%:t'), '\(.*\)\..*', '\1', '') . '.utf-8.add'
nnoremap zG :call LocalSpell("zG")<cr>
nnoremap zW :call LocalSpell("zW")<cr>
nnoremap zuG :call LocalSpell("zuG")<cr>
nnoremap zuW :call LocalSpell("zuW")<cr>
nnoremap zg :call GlobalSpell("zg")<cr>
nnoremap zw :call GlobalSpell("zw")<cr>
nnoremap zug :call GlobalSpell("zug")<cr>
nnoremap zuw :call GlobalSpell("zuw")<cr>
vnoremap zG :call LocalSpell("gvzG")<cr>
vnoremap zW :call LocalSpell("gvzW")<cr>
vnoremap zuG :call LocalSpell("gvzuG")<cr>
vnoremap zuW :call LocalSpell("gvzuW")<cr>
vnoremap zg :call GlobalSpell("gvzg")<cr>
vnoremap zw :call GlobalSpell("gvzw")<cr>
vnoremap zug :call GlobalSpell("gvzug")<cr>
vnoremap zuw :call GlobalSpell("gvzuw")<cr>

function! LocalSpell(cmd)
    if &l:spellfile == ""
        execute "normal! " . a:cmd
    else
        execute "normal! " . tolower(a:cmd)
    endif
endfunction

function! GlobalSpell(cmd)
    let b:spellfile = &l:spellfile
    setlocal spellfile=
    execute "normal! " . a:cmd
    let &l:spellfile = b:spellfile
endfunction

And it works… but it doesn't look like a nice solution to me (edit: it's getting better already :)). So I thought I'd post it here in case anyone has any better ideas or improvements to suggest. What do you think?

  • Where did you include the code above? A filetype plugin? How it is triggered/read? – mMontu Jul 15 '16 at 12:08
  • @mMontu Well, for now I just added it to my vimrc. I was thinking of writing a tiny plugin just to take it out of there. I don't see why make this filetype specific, I never want the words I add to be disposed, and the spellfile is only created when the first zG is ran anyway. – dbmrq Jul 15 '16 at 16:24
  • As you mentioned a spellfile specific to the file I'm editing I thought you were using multiple spellfiles for zG; as the spellfilename is derived from the file name (let &l:spellfile = expand('%:p:h')...), you could achieve this for an specific filetype by defining the filename and mappings when opening that file (you would need to include <buffer> to the mappings). I also assumed that you wanted this change in zG only for some specific files. – mMontu Jul 15 '16 at 19:20
  • @mMontu Oh, no, I didn't mean a spellfile for specific filetypes, but one spellfile for each file. So I can add words and when I open that file again they'll still be added, but just for that specific file and not any others. That's an interesting idea too, though. Unfortunately it seems that isn't a way to source more than one spellfile, otherwise I could do both and have a command to add words for the current file and another to add words for the current filetype. – dbmrq Jul 15 '16 at 19:46
  • I was talking exactly about one spellfile for each file. Maybe I missed something, but if you added the code to your .vimrc you couldn't have more than one spellfile, unless you explicitly :source it again to re-evaluate the %. – mMontu Jul 15 '16 at 20:00
2

Cool idea!

You don't mention what you think is wrong with your code. It looks pretty good to me!

I'd probably do it the other way around: it's only the local behaviour you want to change, so ideally, we'd remove the GlobalSpell function and all the Global mappings, and just alter the local spelling functionality.

Unfortunately this approach only works if 'spellfile' isn't empty before the autocommand runs. (Because with an empty 'spellfile', Vim figures out a location in the 'runtimepath' at the time you add a spelling, but you have no way of accessing that path.) It also won't work if 'spellfile' is already a list: to be more robust it should really count the entries and alter the zg count accordingly.

Therefore, the following approach is okay for personal use in a .vimrc, but you can't really use it in your plugin.

set spell
set spellfile=~/global.utf-8.add

augroup persistent_spelling
  autocmd!
  autocmd BufNewFile,BufRead * let &l:spellfile .= ',' . expand('%:p:h') . '/.' .
    \ substitute(expand('%:t'), '\(.*\)\..*', '\1', '') . '.utf-8.add'
augroup END

function! LocalSpell(cmd)
  if &l:spellfile !~ ","
    execute "normal! " . a:cmd
  else
    execute "normal! 2" . tolower(a:cmd)
  endif
endfunction

nnoremap zG :call LocalSpell("zG")<cr>
nnoremap zW :call LocalSpell("zW")<cr>
nnoremap zuG :call LocalSpell("zuG")<cr>
nnoremap zuW :call LocalSpell("zuW")<cr>
vnoremap zG :call LocalSpell("gvzG")<cr>
vnoremap zW :call LocalSpell("gvzW")<cr>
vnoremap zuG :call LocalSpell("gvzuG")<cr>
vnoremap zuW :call LocalSpell("gvzuW")<cr>

In the above I also surrounded your autocommand in an augroup, which is always a good idea.

You could also consider using expression mappings. You would then remove the if block from the function, and instead write mappings like:

nnoremap <expr> zG if &l:spellfile !~ ',' ? 'zG' : ':call LocalSpell("zG")<CR>'

I'm not sure if it's actually worth it in this case, though: you're just moving complexity from the function to the mapping.

The only other thing that sticks out is the long series of very similar map commands. You could trim this down a bit with a loop (and unlike the above you can use this approach in your plugin):

for lhs in ['zG', 'zW', 'zuG', 'zuW']
  execute 'nnoremap' lhs ':call LocalSpell("'.lhs.'")<cr>'
  execute 'vnoremap' lhs ':call LocalSpell("gv'.lhs.'")<cr>'
  let lhs = tolower(lhs)
  execute 'nnoremap' lhs ':call GlobalSpell("'.lhs.'")<cr>'
  execute 'vnoremap' lhs ':call GlobalSpell("gv'.lhs.'")<cr>'
endfor

If you're feeling super DRY then you could create all the mappings with a single :execute line, but I think this is actually a bad idea: you'd end up with much more complicated code that's almost as long as the original! I tried it anyway, for fun:

for [map_type, gv_type] in items({'n': '', 'v': 'gv'})
  for scope_type in ['Local', 'Global']
    for undo_type in ['', 'u']
      for spell_type in ['G', 'W']
        if scope_type == 'Global'
          let spell_type = tolower(spell_type)
        endif
        let lhs = 'z'.undo_type.spell_type
        execute map_type.'noremap' lhs
          \ ':call' scope_type.'Spell("'.gv_type.lhs.'")<cr>'
      endfor
    endfor
  endfor
endfor

Finally I note that none of the above works if the file you're editing has an underscore in its name, because you cannot use underscores in spell file names! You'll need to alter your autocommand to strip these out/transform them.

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.