12

I mapped zz to 1z=, which is great most of the time, but every now and then the first suggestion isn't the right one.

So I'd like to keep repeating zz (or .) to cycle through the other suggestions.

A second zz on the same word, then, would work like u2z=, a third zz would work like u3z= and so on.

Any ideas on how to do that?


Edit:

Based on @nobe4's awesome answer I managed to do what I want, but I'll leave it here for a while in case anyone has any improvements or suggestions:

let s:spell_position = []
let s:spell_count = 0
let s:spell_word = ""

function! LoopSpell()

    if s:spell_position != getpos('.') ||
            \ (s:spell_count > 0 && s:spell_word !~ expand("<cword>"))
        let s:spell_count = 0
        let s:spell_position = getpos('.')
    endif

    if s:spell_count > 0
        silent execute "normal! u"
    endif

    let s:current_word = expand("<cword>")
    if len(s:current_word) <= 0
        return
    endif

    let s:spell_suggestions = spellsuggest(expand(s:current_word))
    if len(s:spell_suggestions) <= 0
        return
    endif

    if s:spell_count >= len(s:spell_suggestions)
        let s:spell_word = s:current_word
        let s:spell_count = 0
    else
        let s:spell_word = s:spell_suggestions[s:spell_count]
        let s:spell_count += 1
    endif
    silent execute "normal! ciw" . s:spell_word
    let s:spell_position = getpos('.')

endfunction

nnoremap <c-m> :call LoopSpell()<CR>

(I changed the mapping to <c-m> because of @Vitor's comment. Also this allows me to hold those keys down and sort of scroll through the suggestions really fast. I'm thinking of it as <c-mistake>.)

5
  • 2
    I would suggest you to check this plugin which was made by a user of this site. It really improves the spell checking workflow: to start correcting you use the :Correct command: you'll be able to navigate trough the words to correct with n and N, a split window opens with all the correction suggestions you can simply navigate through them with j and k and <CR> will apply the correction.
    – statox
    Jul 9, 2016 at 11:16
  • @statox Thanks for the suggestion. I'll check it out, but I still want my zz command to fix specific things quickly.
    – dbmrq
    Jul 9, 2016 at 17:47
  • 3
    Hope you are aware that originally zz centers the window around the current line. It's probably one of the shortcuts I use more often. You should also checkout zb and zt.
    – Vitor
    Jul 9, 2016 at 23:57
  • @Vitor Interesting, I didn't know that! I usually keep my scrolloff pretty high, but that still seems useful, I'll consider another mapping. Thanks!
    – dbmrq
    Jul 10, 2016 at 5:34
  • This vim script does word completion/spell correction/synonyms (using aspell, thesaurus, dictionary) stackoverflow.com/a/46645434/476175
    – mosh
    May 8, 2018 at 0:16

4 Answers 4

6

Here's what I came up with:

Spell Rotate

spell rotate

Features

  • The '[ and '] marks are used to keep track of the text being worked on. Making a change elsewhere will effectively "accept" the suggested change.
  • Accepts a count.
  • Goes backwards using zp
  • Repeatable using vim-repeat.
  • Undo once to restore the original word regardless of how many suggestions have been cycled.
  • Works in visual mode to get suggestions for split words (e.g. "head line" -> "headline")
    • Uses '< and '> marks to keep track of the text.
    • Note: Doesn't seem to be repeatable with vim-repeat.
  • The original word being changed is kept in the unnamed register.
  • The original, previous, current, and next suggestions are displayed in the command line.
  • Naive command :SpellRotateSubAll to replace all text matching the original with the current suggestion.

Plugin: spellrotate.vim

function! s:spell_rotate(dir, visual) abort
  if a:visual
    " Restore selection.  This line is seen throughout the function if the
    " selection is cleared right before a potential return.
    normal! gv
    if getline("'<") != getline("'>")
      echo 'Spell Rotate: can''t give suggestions for multiple lines'
      return
    endif
  endif

  if !&spell
    echo 'Spell Rotate: spell not enabled.'
    return
  endif

  " Keep the view to restore after a possible jump using the change marks.
  let view = winsaveview()
  let on_spell_word = 0

  if exists('b:_spell') && getline("'[") == getline("']")
    let bounds = b:_spell.bounds
    " Confirm that the cursor is between the bounds being tracked.
    let on_spell_word = bounds[0][0] == bounds[1][0]
          \ && view.lnum == bounds[0][0]
          \ && view.col >= bounds[0][1]
          \ && view.col <= bounds[1][1]
  endif

  " Make sure the correct register is used
  let register = &clipboard == 'unnamed'
        \ ? '*' : &clipboard == 'unnamedplus'
        \ ? '+' : '"'

  " Store the text in the unnamed register.  Note that yanking will clear
  " the visual selection.
  if on_spell_word
    if a:visual
      keepjumps normal! y
    else
      keepjumps normal! `[v`]y
    endif
    call winrestview(view)
  elseif a:visual
    keepjumps normal! y
  else
    keepjumps normal! viwy
  endif

  let cword = getreg(register)

  if !on_spell_word || b:_spell.alts[b:_spell.index] != cword
    " Start a new list of suggestions.  The word being replaced will
    " always be at index 0.
    let spell_list = [cword] + spellsuggest(cword)
    let b:_spell = {
          \ 'index': 0,
          \ 'bounds': [[0, 0], [0, 0]],
          \ 'cword': cword,
          \ 'alts': spell_list,
          \ 'n_alts': len(spell_list),
          \ }

    if len(b:_spell.alts) > 1
      " Do something to change the buffer and force a new undo point to be
      " created.  This is because `undojoin` is used below and it won't
      " work if we're not at the last point of the undo history.
      if a:visual
        normal! xP
      else
        normal! ix
        normal! x
      endif
    endif
  endif

  if a:visual
    normal! gv
  endif

  if len(b:_spell.alts) < 2
    echo 'Spell Rotate: No suggestions'
    return
  endif

  " Force the next changes to be part of the last undo point
  undojoin

  " Setup vim-repeat if it exists.
  silent! call repeat#set(printf("\<Plug>(SpellRotate%s%s)",
        \ a:dir < 0 ? 'Backward' : 'Forward', a:visual ? 'V' : ''))

  " Get the suggested, previous, and next text
  let i = (b:_spell.index + (a:dir * v:count1)) % b:_spell.n_alts
  if i < 0
    let i += b:_spell.n_alts
  endif

  let next = (i + 1) % b:_spell.n_alts
  let prev = (i - 1) % b:_spell.n_alts
  if prev < 0
    let prev += b:_spell.n_alts
  endif

  let next_word = b:_spell.alts[next]
  let prev_word = b:_spell.alts[prev]

  let b:_spell.index = i
  call setreg(register, b:_spell.alts[i])

  if a:visual
    normal! p`[v`]
  else
    keepjumps normal! gvp
  endif

  " Keep the original word in the unnamed register
  call setreg(register, b:_spell.cword)

  let b:_spell.bounds = [
        \ getpos(a:visual ? "'<" : "'[")[1:2],
        \ getpos(a:visual ? "'>" : "']")[1:2],
        \ ]

  echon printf('Suggestion %*s of %s for "', strlen(b:_spell.n_alts - 1), b:_spell.index, b:_spell.n_alts - 1)
  echohl Title
  echon b:_spell.cword
  echohl None
  echon '":  '

  if a:dir < 0
    echohl String
  else
    echohl Comment
  endif
  echon prev_word
  echohl None

  echon ' < '

  echohl Keyword
  echon b:_spell.alts[i]
  echohl None

  echon ' > '

  if a:dir > 0
    echohl String
  else
    echohl Comment
  endif
  echon next_word
  echohl None

  redraw
endfunction


function! s:spell_rotate_suball() abort
  if !exists('b:_spell') || len(b:_spell.alts) < 2
    return
  endif
  execute '%s/'.b:_spell.cword.'/'.b:_spell.alts[b:_spell.index].'/g'
endfunction


command! SpellRotateSubAll call s:spell_rotate_suball()

nnoremap <silent> <Plug>(SpellRotateForward) :<c-u>call <sid>spell_rotate(v:count1, 0)<cr>
nnoremap <silent> <Plug>(SpellRotateBackward) :<c-u>call <sid>spell_rotate(-v:count1, 0)<cr>
vnoremap <silent> <Plug>(SpellRotateForwardV) :<c-u>call <sid>spell_rotate(v:count1, 1)<cr>
vnoremap <silent> <Plug>(SpellRotateBackwardV) :<c-u>call <sid>spell_rotate(-v:count1, 1)<cr>

nmap <silent> zz <Plug>(SpellRotateForward)
nmap <silent> zp <Plug>(SpellRotateBackward)
vmap <silent> zz <Plug>(SpellRotateForwardV)
vmap <silent> zp <Plug>(SpellRotateBackwardV)
4
  • 1
    Wow, now we're talking! You should turn this into a standalone plugin so we could keep future changes and improvements all at the same place. Or I can try to do it if you're not interested.
    – dbmrq
    Jul 13, 2016 at 3:44
  • @danielbmarques Easy enough, here you go: github.com/tweekmonster/spellrotate.vim
    – Tommy A
    Jul 13, 2016 at 4:41
  • Fantastic, thank you! I'll accept your answer as the right one since it's exactly what I wanted and more, and I'll give the bounty to @nobe4 for all his effort and help.
    – dbmrq
    Jul 13, 2016 at 5:12
  • @danielbmarques No problem. I'm in it for the interesting questions and solutions 😄
    – Tommy A
    Jul 13, 2016 at 5:16
5

As @statox suggested, you can use the plugin I wrote: vimcorrect.

I'll explain basically how it works, so if you want to reuse some part of it, you can.

To focus on the next misspelled word I use directly ]s and [s as they jump to the next/previous match. I defined a custom match function to highlight the current word:

enter image description here

matchadd('error', '\%'.line('.').'l'.'\%'.col('.').'c'.s:current_word)

Which add to the match group error the current word at the current line/column (to prevent multiple matching on the same line).


The spellbadword() function return a list of possible correction for the word under the cursor.

I simply display this list in a buffer, and I map <CR> to replace the misspelled word by the current line (i.e. a possible corrected word).


I also map n and N to ]s and [s, as I am used to press them to search.

q is mapped to quit the plugin, close the split and remove the highlight.

Note: it's still highly unstable, but I plan on making some change soon. If you feel like you can/want to improve this plugin, feel free to fork/open a pull request.

2
  • Thanks for the explanation. Your plugin looks great, I'll definitely use it. I still want my zz command, though, so I can fix things quickly without entering a special mode. Maybe we can add that to vimcorrect if I ever figure it out. :)
    – dbmrq
    Jul 9, 2016 at 17:46
  • Well, I definitely need to add more customization. So defining custom mapping may be an improvement you can add if you want :) (if you start developing in vimscript it may be a good way to learn)
    – nobe4
    Jul 9, 2016 at 17:49
3

Aside from the other answers, there is actually a way built right in to Vim: <C-x>s. This will use Vim's insert mode completion menu.

Pressing <C-x>s from insert mode should correct the word under the cursor to the first suggestion and show the completion menu with further suggestions (if any). You can use the 'completeopt' setting to customise some settings for the completion menu.

It's a bit annoying that this only works from insert mode and using using <C-x><C-s> can be problematic (see the note below), so you can define your own mapping for this:

inoremap <expr> <C-@>  pumvisible() ?  "\<C-n>" : "\<C-x>s"
nnoremap <expr> <C-@> pumvisible() ?  "i\<C-n>" : "i\<C-x>s"

<C-@> is Control + Space.

Also see :help ins-completion :help i_CTRL-X_s


I personally use a more advanced version which will "guess" if we want to either spell check the work, or use regular auto-completion for code:

fun! GuessType()
    " Use omnicomplete for Go
    if &filetype == 'go'
        let l:def = "\<C-x>\<C-o>"
    " Keyword complete for anything else
    else
        let l:def = "\<C-x>\<C-n>"
    endif

    " If we have spell suggestions for the current word, use that. Otherwise use
    " whatever we figured out above.
    try
        if spellbadword()[1] != ''
            return "\<C-x>s"
        else
            return l:def
        endif
    catch
        return l:def
    endtry
endfun

inoremap <expr> <C-@>  pumvisible() ?  "\<C-n>" : GuessType()
inoremap <expr> <Down> pumvisible() ? "\<C-n>" : "\<Down>"
inoremap <expr> <Up> pumvisible() ? "\<C-p>" : "\<Up>"
nnoremap <expr> <C-@> pumvisible() ?  "i\<C-n>" : 'i' . GuessType()

I believe there are also some plugins which do roughly similar things (like SuperTab, which is fairly popular), but I could never get them to behave as I want to.


Caveat: If you are using Vim from a terminal, then <C-s> will mean "stop output". This is why both <C-x><C-s> and <C-x>s are mapped by default. Use <C-q> to continue output if you press <C-s> by accident. You can also disable <C-s> if you don't use it (see this question). If you are using GVim you can ignore this.

2
+50

Here is a function that should work:

let s:last_spell_changedtick = {}

function! LoopSpell()
  " Save current line and column
  let l:line = line('.')
  let l:col = col('.')

  " check if the current line/column is already in the last_spell_changedtick
  if has_key(s:last_spell_changedtick, l:line) == 0
    let s:last_spell_changedtick[l:line] = {}
  endif

  if has_key(s:last_spell_changedtick[l:line], l:col) == 0
    let s:last_spell_changedtick[l:line][l:col] = 0
  endif

  " If the value already exists, undo the change
  if s:last_spell_changedtick[l:line][l:col] != 0
    normal u
  endif

  " Get the current word
  let l:current_word = spellbadword()
  if len(l:current_word) == 0
    call <SID>Quit()
  endif

  " Get suggestions for the current word
  let s:current_word = l:current_word[0]
  let l:suggestions = spellsuggest(expand(s:current_word))

  " If the current word present no spelling suggestions, pass
  if len(suggestions) <= 0
    return
  endif

  " Replace the word with suggestion
  silent execute "normal! ce" . l:suggestions[s:last_spell_changedtick[l:line][l:col]]
  normal! b

  " Increment the count
  let s:last_spell_changedtick[l:line][l:col] = s:last_spell_changedtick[l:line][l:col] + 1

endfunction

function! LoopConfirm()
  let s:last_spell_changedtick = {}
endfunction

nnoremap zz :call LoopSpell()<CR>
nnoremap z= :call LoopConfirm()<CR>

The basic idea is to map every changed word to a line/column pair (so that it will not work only for one element) and check whether the element had already been modified.

To do the replace, it's pretty much what my plugin does:

  • fetch the current misspelled word
  • check if corrections exists
  • replace word with corrected suggestion

When using this, if you want to go back to the misspelled word you can simply press u.

The LoopConfirm function reset the dictionary, so if you change your text, you can call it to prevent collisions.

Let me know if you run into any issue/if you have questions.

6
  • Uuh, that's looking good. It still has many problems, though. Take a phrase like "teh qick borwn foz jums ofer teh lazi dor" and try to correct each word that way. I can never get "teh" to "the", although it's number 4 on the list. "qick" works, but "borwn" changes to something else, even if "brown" is first on the list, and then it skips straight to "foz". I never got past that. Also I don't like the extra z= part, but I probably could find a way to get around that myself if the rest worked. This is getting very close to what I want, though. I'll keep trying to fix it. Thanks!
    – dbmrq
    Jul 12, 2016 at 17:36
  • See my update, I add a increment too soon :) Yeah I'm not happy with the z= either. But with this method you need to keep a reference of where you are. But if you don't need to keep all references at the same time I can simplify that :)
    – nobe4
    Jul 12, 2016 at 17:46
  • I'm not sure what you mean by "keep all references at the same time"… but couldn't we just reset the dictionary whenever the cursor moves? The function would check if the cursor is at the same place as it was last time it was called, and if it isn't it resets.
    – dbmrq
    Jul 12, 2016 at 18:00
  • Also the way it is it doesn't seems to work properly when the cursor isn't at the start of the word. Try to correct every mistake in that sentence placing the cursor at the middle of each word. I skips to the next right away.
    – dbmrq
    Jul 12, 2016 at 18:07
  • 1
    Ok, I think I got it! Check my last edit. This seems to work pretty much perfectly. I'll leave the question open a little longer to see if anyone else has something to add, but your answer was is great, thank you. :)
    – dbmrq
    Jul 12, 2016 at 20:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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