I'm trying to write a opfunc which surrounds selected motion with two stars (bold in Markdown):

nnoremap <localleader>b :set opfunc=BoldIt<CR>g@
function! BoldIt(type, ...)
    execute "normal! `[i**"
    execute "normal! `]a**"

According to :map-operator info:

  • [ mark denotes start of text
  • ] mark denotes end of text

In my function just one of the marks works. I mean, if I comment either of the execute lines, the other one does the job correctly, as supposed to.

However, when run as shown, stars end in the same position. It's just like if one of the mark get lost. What I'm missing?

2 Answers 2


What I'm missing?

:h '] "To the last character of the previously changed or yanked text."

You must keep marks intact until you've done with all of them. Maybe :lockmarks or setline() or whatever.

  • You're right. Insert/append commands were changing the mark. :lockmarks neither work. I checked the doc and it says visual area won't be adjusted, say nothing about opfunc marks though.
    – mzcarlos
    Aug 13, 2021 at 18:44

@Matt is correct. You have effectively changed the last change marks which is why your approach can not work.

I would suggest you use c and <c-r> to paste/put the last deleted/yanked text to do your star-surrounding. e.g. c**<c-r>"**

I would also recommend following the technique/example laid out by Vim's help (See :h g@)

nnoremap <expr> <localleader>b BoldIt()
xnoremap <expr> <localleader>b BoldIt()
function! BoldIt(...)
    if !a:0
      set opfunc=BoldIt
      return 'g@'
    let type = a:1

    let sel_save = &selection
    let reg_save = @@
    let reg_type = getregtype('"')
    let cb_save = &clipboard
    let visual_marks_save = [getpos("'<"), getpos("'>")]

    let commands = {}
    let commands['line'] = "'[v']g_c**\<c-r>\"**\<esc>"
    let commands['char'] = "`[v`]c**\<c-r>\"**\<esc>"
    let commands['block'] = "`[\<c-v>`]c**\<c-r>\"**\<esc>"

      set clipboard= selection=inclusive
      let cmd = 'noautocmd keepjumps normal! '
      let cmd .= get(commands, type, '')
      silent exe cmd
      call setreg('"', reg_save, reg_type)
      call setpos("'<", visual_marks_save[0])
      call setpos("'>", visual_marks_save[1])
      let &clipboard = cb_save
      let &selection = sel_save

Note: This may require a newer version of Vim (tested on Vim 8.2.2681)

  • 1
    Thank you! This approach is certainly easier. Finally, I used yank and <c-r> as you suggest (and restore the registers too, which is indeed a good idea). In vim 8.1 your example fails with "E475: Invalid argument: type='')".
    – mzcarlos
    Aug 13, 2021 at 18:50
  • 1
    I am using Vim 8.2.2681. I have changed it to use ... for parameters since (type='') requires a newer version Aug 13, 2021 at 21:26
  • 1
    Three more changes needed to work in Vim ver 8.1.1401: 1) function getreginfo() doesn't exist, so I replaced that call with @@; 2) modify literal-dict syntax ( #{}), which is not supported; 3) in the silent-exe line, there's an extra dot. Now it works. I appreciate your suggestion, it's cleaner and more readable code IMO, and I like the a:0 trick as well as putting commands into a dict.
    – mzcarlos
    Aug 14, 2021 at 4:05
  • 1
    @D.BenKnoble, Thank you. I have fixed it up Aug 16, 2021 at 19:01

Your Answer

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

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