10

I like the behavior of the 'smartcase' option, which lets searches I type in by hand be case-insensitive by default, case-sensitive when there are any capital letters. I also like the behavior of the 'noignorecase' option, which lets searches generated by the editor (as when hitting * or #) be case-sensitive by default.

Unfortunately, 'smartcase' only does its magic when 'ignorecase' is on, so now I have a conflict: I can either get the nice behavior for hand-generated searches or the nice behavior for machine-generated searches, but not both.

How I can I get both nice behaviors?

Daniel Wagner
  • 225
  • 1
  • 6

2 Answers2

13

You can fix that for the * command by putting the following mapping in your ~/.vimrc.

nnoremap * <silent> *N:let @/.='\C'<CR>n

When you type *, the mapping first executes the normal * command, then jumps back to the original word (N), then appends \C to the search expression (:let @/.='\C'<CR>), which makes it case-sensitive, and finally jumps forward again (n).

A complete compendium of bindings which doesn't clobber jumps (using Vitor's trick from the comments) and supports both forward and backwards search looks like this:

nnoremap <silent>  * :let @/='\C\<' . expand('<cword>') . '\>'<CR>:let v:searchforward=1<CR>n
nnoremap <silent>  # :let @/='\C\<' . expand('<cword>') . '\>'<CR>:let v:searchforward=0<CR>n
nnoremap <silent> g* :let @/='\C'   . expand('<cword>')       <CR>:let v:searchforward=1<CR>n
nnoremap <silent> g# :let @/='\C'   . expand('<cword>')       <CR>:let v:searchforward=0<CR>n
Daniel Wagner
  • 225
  • 1
  • 6
garyjohn
  • 6,309
  • 20
  • 21
  • 1
    KISS: :nnoremap <silent> * :let @/='\C\<' . expand('<cword>') . '\>'<CR>n (the last S stands for "solution";) – Vitor Jul 29 '15 at 13:17
  • @Vitor I like the idea of your solution because it doesn't clobber the jump registers, but it doesn't seem to work: after executing that nnoremap, the * key doesn't seem to do anything. – Daniel Wagner Jul 29 '15 at 21:27
  • @Vitor Oops, I put the <silent> in the wrong place -- as did garyjohn! Your code works well, thanks. – Daniel Wagner Jul 29 '15 at 21:35
  • How could this solution be implemented in Lua / Neovim. – 71GA Dec 19 '22 at 09:51
  • @71GA I migrated this to my init.vim when I switched, and it has continued to work just fine. – Daniel Wagner Dec 22 '22 at 17:54
  • @DanielWagner Huh, this did not work for me. This is why I am asking about the Lua way. :) Maybe I was not clear enough. – 71GA Dec 22 '22 at 20:44
3

Here's a version of garyjohn/Vitor's solution that is a closer match to *'s behaviour in a few ways.

Making it an expr map fixes two things:

  • 'hlsearch' -- turns on highlight immediately instead of after next n
  • counts -- 3* skips the first two results

And explicitly handle some others:

  • # when in the middle of a word -- jump to previous match instead of start of current match
  • empty strings -- error
  • nonwords -- * on ) at end of line doesn't do wholeword
function! s:SearchCword(wholeword, direction) abort
    let query = expand('<cword>')
    if empty(query)
        echohl ErrorMsg
        echomsg "E348: No string under cursor"
        echohl None
        return ""
    endif
    " Doing * on a nonword character at end of line produces no word
    " characters so wholeword is invalid.
    if a:wholeword && query =~# '\w'
        let query = '\<'.. query ..'\>'
    endif
    let @/ = query ..'\C'
    let searchforward = a:direction ==# 'n'
    let suffix = ''
    if !searchforward
        " v:searchforward is reset after functions so it must be part of our
        " returned command instead.
        let suffix = ":let v:searchforward=0\<CR>"
    endif
    if searchforward || s:IsCursorAtStartOfWord(expand("<cword>"))
        return a:direction .. suffix
    endif
    return a:direction..a:direction .. suffix
endfunction
function! s:IsCursorAtStartOfWord(query) abort
    let index_in_word = match(getline('.'), '\%' . col('.') ..'c'.. a:query)
    return index_in_word > 0
endfunction
nnoremap <expr>  * <SID>SearchCword(1, "n")
nnoremap <expr>  # <SID>SearchCword(1, "N")
nnoremap <expr> g* <SID>SearchCword(0, "n")
nnoremap <expr> g# <SID>SearchCword(0, "N")

I've added SearchCword to vim-searchsavvy as searchsavvy#SearchCword(). It supports a few more options (like making * follow 'smartcase' rules instead of always case sensitive).

3N4N
  • 5,684
  • 18
  • 45
idbrii
  • 641
  • 4
  • 14
  • Cool. I wonder if the echoerr can be replaced by throw? Although honestly I’m pretty sure I have both smartcase and ignorecase set and don’t have this issue with *, but I could be misremembering. – D. Ben Knoble Sep 21 '21 at 21:20
  • 1
    throw causes "Error detected while processing..." but we want an error message like the default behaviour. (You got my hopes up for a version of echoerr that's good for producing pretty error messages. : ) – idbrii Sep 21 '21 at 21:53
  • 1
    With set smartcase, * on "FizzBuzz" will match "Fizzbuzz". The smart logic only applies to user typed searches. The help suggests doing /<Up> – idbrii Sep 21 '21 at 21:54