2

I've written a little function that changes a 'CamelCaseWord' to a space separated 'camel case word':

" Convert a CamelCase word to space separated words.
function! s:converFromCamelCase(arg1)
  let l:len = strlen(a:arg1)
  let l:i = 0
  let l:retVal = ""
  while l:i < l:len
    let l:c = strpart(a:arg1, l:i, 1)
    let l:i += 1
    let l:curKey = l:c
    " Replace all but the first capital letter with space+lowercase of that
    " letter
    if l:curKey =~ '\u'
      if l:i != 1
        let l:retVal .= ' '
      endif
      let l:curKey = tolower(l:c)
    endif
    let l:retVal .= l:curKey
  endwhile

  return l:retVal
endfunction

nnoremap <a-x> substitute(<cword>, <SID>converFromCamelCase(expand("<cword>")))<CR>
"nnoremap <a-x> :echo <SID>converFromCamelCase(expand("<cword>"))<CR>

The echo mapped function works, and shows the 'camel case word'. But, I'm at a loss figuring out how to replace the <cword> with the result of the function.

1

4 Answers 4

5

That's a lot of code. ;) What if I said we could compact that whole thing down to a one-line command (and mapping)? Let's take it a step at a time, though.

First, the substitution...

:s/\u/ \l&/g

...will replace each capital letter in the current line with a space and the lower-cased letter. Same pattern/replacement can be used in substitute().

Let's try leveraging this.

func! ConvertFromCamelCase()
    " get the word to be modified
    let l:tgt = expand("<cword>")

    " Apply the substitution discussed above to the target then
    " throw out the unwanted opening space character.
    let l:repl = substitute(l:tgt, '\u', ' \l&', "g")[1:]

    " Now do substitution on the line's text that just replaces
    " the target word with the modified version. Overwrite the
    " buffer line with this.
    call setline(".", substitute(getline("."), l:tgt, l:repl, ""))
endfunc

If you run that with the cursor on the name of the function up there you'll get:

func! convert from camel case()

There are some corner cases, though, that can trip this up (e.g. when the target word occurs multiple times in the line an occurrence other than the one under the cursor might get modified). Solving these is very doable but would involve several more lines of function calling code.

There's an easier way. We can leverage a Normal mode command instead of function calls. The answer from @statox mentions use of the ciw operation and that's exactly what we'll use here:

func! ConvertFromCamelCase()
    " First two lines are the same as above
    let l:tgt = expand("<cword>")
    let l:repl = substitute(l:tgt, '\u', ' \l&', "g")[1:]

    " Use normal mode commands to insert the updated word
    exe "norm! ciw" . l:repl
endfunc

If you join the functions with each other it doesn't even need to be in a function...

:exe "norm! ciw" . substitute(expand("<cword>"), '\u', ' \l&', "g")[1:]

Now put that in a mapping and you're in business:

:nnoremap <a-x> :exe "norm! ciw" . substitute(expand("<cword>"), '\u', ' \l&', "g")[1:]<cr>

Now, this assumes you are using "CapCase" and not "mixedCase", i.e. the target must start with a capital letter otherwise the first char gets thrown out. If you want this to work with "mixedCase" then you'll have to go back to a function and add a little bit of logic. But shouldn't be any big deal at all.

Update: This should handle CapCase and mixedCase (in the function version)...

let l:repl = tolower(l:tgt[0]) . substitute(l:tgt[1:], '\u', ' \l&', "g")

Obviously, creating a mapping that just calls the function is plenty easy so there's really no disadvantage to using this versus the super compact version.

3
  • 1
    I think that's the correct approach 👍
    – statox
    Jul 5, 2021 at 11:52
  • @statox Cool. I know it works but I hadn't yet convinced myself that it was optimal. :)
    – B Layer
    Jul 5, 2021 at 11:53
  • 1
    @statox BTW, I was completely oblivious to your answer until just now! (I edit in external Vim thanks to Pentadactyl brower plugin). That's probably good as I could have been influenced one way or another by your answer if I read it first...might have come up with something different. Oh, and thanks for the comment in your answer about this.
    – B Layer
    Jul 5, 2021 at 11:57
2

B Layer answer is on point, that is easier to reader and make use of the right built-in functions. I posted this answer as an illustration of what you could have done to avoid changing your function but I think the other answer is a better option.

Your converFromCamelCase takes a string and returns a new one, you can then create a wrapper which takes the current <cword>, gives it to converFromCamelCase and then use :h :execute to build a substitution command like this:

function! Wrapper()
    let word=expand("<cword>")
    let new=ConverFromCamelCase(word)

    execute "s/" . word . "/" . new . "/"
endfunction

nnoremap  <a-x> :call Wrapper()<CR>

Another way is to take inspiration from this plugin which combines ciw and the expression register (see :h i_CTRL-R)

3
  • My preference would be to combine ciw, the expression register, and BLayer’s substitute—that should make sure to hit the right target
    – D. Ben Knoble
    Jul 5, 2021 at 14:16
  • Ah, this is the one I'm going to use. It only affects the word under the cursor, which is exactly what I want it to do. The other solutions, while more elegant, were, to me, write only code. I want to be able to revisit this, years down the road, and be able to quickly understand what is going on. Thanks!
    – renfrow
    Jul 5, 2021 at 18:53
  • Oops, this doesn't quite do what I want, it modifies the first occurence of the camel case word on the line, not the one under the cursor. The file I'm using this function, has three occurences of the of the camel case word, and I don't want to edit the first (as it is part of a file name), but rather the one under the cursor :(.
    – renfrow
    Jul 5, 2021 at 19:15
1

Here's a fully inline solution using register hacks and the expression register to insert the transformed string in place of the old string:

nnoremap <leader>ccc ""ciw<C-r>=substitute(@", '\u', ' \l&', 'g')<cr>

<leader>ccc is an arbitrary choice for ease of use while figuring out the signature - it doesn't have any significance (aside meaning "change camel case"), so feel free to replace.

Here's a semi-ugly command explanation sheet (that reads substantially better in Vim with setlocal cursorcolumn):

""ciw<C-r>=substitute(@", '\u', ' \l&', 'g')<cr>
  ^ change in word, deletes the current word (as defined by Vim) and enters insert mode
    As a side-note, the pre-fixed "" is to make sure everything is added into the " register; see `:h c`.
    Using " specifically is a semi-arbitrary choice, but that's the default register text deleted with 'c'
    is inserted into. See `:h quotequote` for more info on that register specifically.
    You can also use another register if you want to preserve the " register
      ^ <C-r>= lets you execute a command, and in this context, the result is additionally inserted into the buffer.
        Since `substitute()` returns a string, said string is appended to the buffer at the cursor location
           ^ `substitute()` is a function that's equivalent to `s/.../.../...`, with differences that
             I'll leave to the documentation to explain. (`:h substitute()`)
                       ^ Registers can be accessed by using @<register name> in Vimscript. See `:h expr-register`
                           ^ then search for upper-case letters
                                   ^ replace with its lower-case version, and add a space
                                         ^ globally (possibly unnecessary)
                                             ^ and execute the command

Also, the reason "" is explicitly prefixed is to make sure it works regardless of what clipboard is set to. This may be completely redundant, but because I don't entirely comprehend the messy world of registers, I added it in anyway.

Note that it's perfectly possible to use '<C-r>"' instead of @", but this will trigger an error if the word contains a '. While testing this, I generally found ' to be excluded from words and only end up as its own word (i.e. in don't - this is considered to be three different words in my config, for whatever reason), but if you ever end up with ' being a part of the word, <C-r>" can result in an expression similar to ''', which is an invalid string. This is due to the lack of escaping, as what <C-r> yields isn't a Vimscript string. While I'm sure it can be mitigated, @" offers the exact same thing, but pre-packaged in a string that doesn't need this type of escaping.

As a side-effect, this only replaces the word under the cursor, where as line-based substitutions may replace other matches (or force an earlier match). This may or may not be something you want to do. The solution here is definitely more bullet proof in terms of the word's positioning in the line if you need specific replacements.

Finally, you can append <esc> to the map if you don't want to stay in insert mode after the change has been made.

1

Using statox's wrapper function, here's what I wound up with, that works perfectly for the word under the cursor (made it a little more useful also):

function! s:changeWordUnderCursor(new)
    normal "geb"
    let l:lastPos = getpos(".")
    let l:start = getcurpos()[2]
    normal "ee"
    let l:end = getcurpos()[2] + 1

    execute "s/\\%" . l:start . "c.*\\%" . l:end . "c/" . a:new . "/"
    call setpos(".", l:lastPos)
endfunction

nnoremap <a-x> :call <SID>changeWordUnderCursor(<SID>converFromCamelCase(expand("<cword>")))<CR>

Thanks for all the help!

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.