Files
dotfiles/.config/vim/vimrc.d/40-keys.vim
Julian Prein 0a5b6d0767 vim:keys: Only map v_* & v_# if they don't exist
I learned just now that neovim added mappings for these by default in
v0.8.0. But instead of checking for neovim, check for the existence of
mappings in case vim adds these in the future too.
2025-09-18 01:07:34 +02:00

556 lines
18 KiB
VimL

" vim: set foldmethod=marker:
" Keybindings """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Clear search result highlights when pressing Escape in normal mode
nnoremap <silent> <Esc> :nohlsearch<CR><Esc>
" Indentation jump
" https://vim.fandom.com/wiki/Move_to_next/previous_line_with_same_indentation
" noremap <silent> <C-k> :call search('^'. matchstr(getline('.'), '\(^\s*\)') .'\%<' . line('.') . 'l\S', 'be')<CR>
" noremap <silent> <C-j> :call search('^'. matchstr(getline('.'), '\(^\s*\)') .'\%>' . line('.') . 'l\S', 'e')<CR>
" Split view navigation
" Create new panes
nnoremap <C-w>N <Cmd>vsplit<CR>
nnoremap <C-w>n <Cmd>split<CR>
" My brain expects <C-w>{gf,gF} to open in a split, not a tab.
" Swap the mappings for tab and split.
nnoremap <C-w>gf <C-w>f
nnoremap <C-w>gF <C-w>F
nnoremap <C-w>f <C-w>gf
nnoremap <C-w><C-f> <C-w>gf
nnoremap <C-w>F <C-w>gF
" Analogous mapping to tmux's <prefix>! to move the current window to a new tab
nnoremap <C-w>! <C-w>T
" Substitute command
if (exists('+inccommand') && &inccommand != '')
nnoremap S :%s/
vnoremap S :s/\%V
else
" This does not work with live previewing commands (inccommand) since the
" replace pattern is already defined and thus everything matching the search
" pattern is just deleted.
nnoremap S :%s//gc<Left><Left><Left>
vnoremap S :s/\%V/gc<Left><Left><Left>
endif
" Interact with the system clipboard
if (has('clipboard'))
map <leader>y "+y
map <leader>Y "+Y
map <leader>p "+p
map <leader>P "+P
endif
" Do not move the cursor to the start of the selection after a yank
" https://stackoverflow.com/a/3806664/20927629
vmap y ygv<Esc>
" Ctrl-Backspace should delete words in insert mode and on command-line.
noremap! <C-BS> <C-W>
map! <C-H> <C-BS>
" Correct word with best/first suggestion.
noremap <leader>c 1z=
" Correct next or last misspelled word (and their non-rare/region versions)
" without moving
" TODO: see :keepjumps
" Problem: with keepjumps the <C-O> is not possible anymore
noremap <leader>]s ]s1z=<C-O>
noremap <leader>[s [s1z=<C-O>
noremap <leader>]S ]S1z=<C-O>
noremap <leader>[S [S1z=<C-O>
" Toggle spell language between German and English
function! CycleSpellLang()
if (&spelllang == 'en')
setl spelllang=de
elseif (&spelllang == 'de')
setl spelllang=en
endif
endfunction
" Toggle spell, cycle and set spelllang
map <leader>st <Cmd>set spell!<CR>
map <leader>sc <Cmd>call CycleSpellLang()<CR>
map <leader>ss :set spelllang=
" Jump through jump table but center while still respecting 'foldopen'
noremap <expr> <C-I> '<C-I>' . (match(&fdo, 'mark') > -1 ? 'zv' : '') . 'zz'
noremap <expr> <C-O> '<C-O>' . (match(&fdo, 'mark') > -1 ? 'zv' : '') . 'zz'
nmap <Tab> <C-I>
nmap <S-Tab> <C-O>
" Terminal
if (has('nvim'))
tnoremap <leader><Esc> <C-\><C-n>
nmap <leader><CR> :split +terminal<CR>i
nmap <leader>v<CR> :vsplit +terminal<CR>i
elseif (has('terminal'))
nmap <leader><CR> <Cmd>terminal<CR>
endif
if (get(g:, 'loaded_fzf'))
nmap <leader>ff <Cmd>Files<CR>
nmap <leader>fj <Cmd>Lines<CR>
nmap <leader>f/ <Cmd>Lines<CR>
nmap <leader>fh <Cmd>Helptags<CR>
nmap <leader>ft <Cmd>Tags<CR>
nmap <leader>fbt <Cmd>BTags<CR>
" git files that `git status` lists
nmap <leader>fgf <Cmd>GFiles?<CR>
" 'git log (log?)' and 'git log buffer '
map <leader>fgll <Cmd>Commits<CR>
map <leader>fglb <Cmd>BCommits<CR>
" TODO: <leader>fglb should restrict the log to all staged files when called
" in .git/COMMIT_EDITMSG, maybe with an ftplugin?
endif
" Search for selected text.
" Modified from https://vim.fandom.com/wiki/Search_for_visually_selected_text
function! GetVisualSelection(escape = "", byteescape = 'n')
let l:old_reg = getreg('"')
let l:old_regtype = getregtype('"')
norm gvy
let l:sel = getreg('"')
call setreg('"', l:old_reg, l:old_regtype)
let l:sel = l:sel->escape(a:escape)
for l:char in a:byteescape
let l:sel = l:sel->substitute('\'..l:char, '\\'..l:char, 'g')
endfor
return l:sel
endfunction
" In case these do not exist already (At the time of writing only Neovim adds
" mappings for these)
if maparg('*', 'v', 0, 1) == {}
vmap * /\V<C-R>=GetVisualSelection('/\')<CR><CR>
vmap # ?\V<C-R>=GetVisualSelection('?\')<CR><CR>
endif
" Extended `*`. Starts vim search (without jump) and ripgrep
nmap <leader>* :let @/ = '\<' . expand('<cword>') . '\>' <bar>
\ set hlsearch <bar>
\ Rg \b<C-R>=expand('<cword>')<CR>\b<CR>
" TODO: pass --multiline to rg when multiple lines selected
" TODO: Use ^ and $ anchors in visual-line mode
vmap <leader>* :<C-U>let @/ = "\\V<C-R>=escape(GetVisualSelection('\'), '"\')<CR>" <bar>
\ set hlsearch <bar>
\ Rg <C-R>=GetVisualSelection('.\[]<bar>*+?{}^$()', 'nt')<CR><CR>
nmap <leader>g* :let @/ = expand('<cword>') <bar>
\ set hlsearch <bar>
\ Rg <C-R>=expand('<cword>')<CR><CR>
vmap <leader>g* <leader>*
" Search inside visual selection
noremap <leader>v/ /\%V
vmap <leader>/ <Esc><leader>v/
" Select last pasted text in same visual mode as it was selected (v, V, or ^V)
" Taken from: https://vim.fandom.com/wiki/Selecting_your_pasted_text
" TODO: I want the gp default back - find new mapping
" nnoremap <expr> gp '`[' . strpart(getregtype(), 0, 1) . '`]'
" Git bindings
" Reference the commit that the cursor is currently on with the 'reference'
" format (Mnemonic: "git reference commit").
" NOTE: I can't use --pretty=reference since it would insert the abbreviated
" hash additionally to the existing hash.
" NOTE: This uses `system` and not `:r!` to insert the text directly at the
" cursor. `subject[:-1]` cuts off the trailing newline.
" TODO: print error message but insert nothing on git error
nmap <leader>grc :let subject=system('git show -s --date=short --pretty="format:(%s, %ad)" <C-R><C-W>')<CR>viw<Esc>a <C-R>=subject[:-1]<CR><Esc>
" Insert a Signed-off-by trailer
nmap <leader>gso :r!git config --get user.name<CR>:r!git config --get user.email<CR>I<<ESC>A><ESC>kJISigned-off-by: <ESC>
" Add, stash or checkout the current file
nmap <leader>gfa <Cmd>!git add -- %<CR>
nmap <leader>gfs <Cmd>!git stash -- %<CR>
nmap <leader>gfu <Cmd>!git checkout -- %<CR>
if exists('g:loaded_fugitive')
" Interactive `git status`
nmap <leader>gg <Cmd>G<CR>
" Start a commit and open the message in a split
nmap <leader>gcc <Cmd>G commit<CR>
" Amend the current commit and open the message in a split
nmap <leader>gca <Cmd>G commit --amend<CR>
" Commit with the last commit message as template
nmap <leader>gclm <Cmd>G commit-last-msg<CR>
" Move to root of directory
nmap <leader>gcd <Cmd>Gcd<CR>
" git blame in scroll bound vertical split (only the commit hashes, see
" :help :Git_blame)
nmap <leader>gb :G blame<CR>C
else
" Move to root of repository
" NOTE: only works if a file is already opened
nnoremap <leader>gcd <Cmd>cd %:h <Bar> cd `git rev-parse --show-toplevel`<CR>
endif
if exists('g:loaded_gitgutter')
" Mnemonic: "git <add|undo|preview>"
nmap <leader>ga <Plug>(GitGutterStageHunk)
xmap <leader>ga <Plug>(GitGutterStageHunk)
" TODO: nmap <leader>gs <Plug>(GitGutterStashHunk)
nmap <leader>gu <Plug>(GitGutterUndoHunk)
nmap <leader>gp <Plug>(GitGutterPreviewHunk)
" Add hunk/h version to textobject bindings that use `c` (for `change I
" presume?) (e.g. ic -> ih)
omap ih <Plug>(GitGutterTextObjectInnerPending)
omap ah <Plug>(GitGutterTextObjectOuterPending)
xmap ih <Plug>(GitGutterTextObjectInnerVisual)
xmap ah <Plug>(GitGutterTextObjectOuterVisual)
" Same for hunk navigation bindings + center line
" TODO: <leader>[h to jump between **all** hunks across all modified
" files. (similar to `add -p`)
nmap [h <Plug>(GitGutterPrevHunk)zz
nmap ]h <Plug>(GitGutterNextHunk)zz
endif
" Y should behave like D & C does
nnoremap Y y$
" Clear line (`cc` but stay in normal mode)
nmap <leader>dd 0D
vmap <leader>d <Cmd>keepp '<,'>s/^.*$//<CR>
" Fix & command to also use last flags
nnoremap & <Cmd>&&<CR>
xnoremap & <Cmd>&&<CR>
" see :help i_ctrl-g_u
" Do not break undo sequence when using the arrow keys or home and end in insert
" mode
inoremap <Left> <C-G>U<Left>
inoremap <Right> <C-G>U<Right>
inoremap <expr> <Home> col('.') == match(getline('.'), '\S') + 1 ?
\ repeat('<C-G>U<Left>', col('.') - 1) :
\ (col('.') < match(getline('.'), '\S') ?
\ repeat('<C-G>U<Right>', match(getline('.'), '\S') + 0) :
\ repeat('<C-G>U<Left>', col('.') - 1 - match(getline('.'), '\S')))
inoremap <expr> <End> repeat('<C-G>U<Right>', col('$') - col('.'))
" break undo sequence with every space and newline, making insert mode changes
" revertible in smaller chunks
inoremap <Space> <C-G>u<Space>
" Open the manpage in the WORD under cursor
nnoremap gm :Man <C-r><C-a><CR>
xnoremap gm :Man <C-r><C-a><CR>
" Format the current paragraph, while keeping the cursor position
nmap Q gwap
imap <C-q> <C-o>Q
" Swap movement mappings that act on display lines with the normal ones, making
" it easier to navigate long wrapped lines.
function! MapWrapMovement()
let l:mappings = {
\ 'j': 'gj',
\ 'k': 'gk',
\ '0': 'g0',
\ '^': 'g^',
\ '$': 'g$',
\ 'gj': 'j',
\ 'gk': 'k',
\ 'g0': '0',
\ 'g^': '^',
\ 'g$': '$',
\ }
if &wrap
for [l:from, l:to] in items(l:mappings)
execute 'noremap ' .. l:from .. ' ' .. l:to
endfor
else
for l:key in keys(l:mappings)
execute 'silent! unmap ' .. l:key
endfor
endif
endfunction
augroup WrapMovementMappings
au!
au OptionSet wrap call MapWrapMovement()
augroup END
" Convert Unix timestamp to human readable
" Mnemonic: "Unix timestamp convert" with pun to UTC
nnoremap <leader>utc ciw<C-r>=strftime("%F %T", @")<CR><Esc>
vnoremap <leader>utc <Cmd>keepp '<,'>s/\v(^\|[^0-9])\zs[0-9]{10}\ze([^0-9]\|$)/\=strftime("%F %T",submatch(0))/g<CR>
" Convert decimal numbers to hex
" https://stackoverflow.com/a/1118642
nnoremap <leader>hex ciw<C-r>=printf("0x%X", @")<CR><Esc>
vnoremap <leader>hex <Cmd>keepp '<,'>s/\v<\d+>/\=printf("0x%X", submatch(0))/g<CR>
" TODO: <leader>sec that uses the `duration` alias from zsh
" Relax mappings that jump to opening braces on first column: Just make sure
" they are on an unindented line. This is useful for files that use a different
" coding style guide than the kernel and similar.
" TODO: support [count]
" TODO: sections? (see :h [[ and :h section)
" TODO: exclusive and exclusive-linewise?
noremap <silent> [[ <Cmd>call search('\v^(\S.*)?\{', 'besW')<CR>
noremap <silent> ]] <Cmd>call search('\v^(\S.*)?\{', 'esW')<CR>
" Make `dest` do the same thing as `src` currently does. `src` can be remapped
" afterwards without `dest`'s behaviour changing. Call with `expand("<sflnum>")`
" as `lnum`.
function! s:mapcpy(to, from, lnum, mode = "")
let l:curr_map = maparg(a:from, a:mode, 0, 1)
if empty(l:curr_map)
" Simply do a `*noremap <to> <from>` (but with correct lnum)
let l:curr_map = {
\ "rhs": a:from, "noremap": 1, "mode": a:mode,
\ "sid": expand("<SID>"),
\ "script": 0, "expr": 0, "buffer": 0, "silent": 0,
\ "nowait": 0, "abbr": 0, "scriptversion": 1,
\ }
endif
" Overwrite lhs of current mapping. Also change lnum to calling line.
let l:curr_map["lnum"] = a:lnum
let l:curr_map["lhs"] = a:to
if has("nvim")
" TODO: is `1, 1, 1` correct?
let l:curr_map["lhsraw"] = nvim_replace_termcodes(a:to, 1, 1, 1)
else
" TODO: convert to bytes (find inverse of keytrans())
let l:curr_map["lhsraw"] = a:to
endif
if has_key(l:curr_map, "lhsrawalt")
call remove(l:curr_map, "lhsrawalt")
endif
call mapset(l:curr_map)
endfunction
" Swap ]] and ][, so that ]] jumps forward to the next '}' and ][ to '{'. I find
" this more intuitive, as now the first bracket indicates the jump direction
" (i.e. ] -> forward, [ -> backward) and the second bracket the orientation of
" the target brace (i.e. [ -> {, ] -> }) .
"
" While doing this, keep the functionality from above that the opening brace
" does not need to be in the first column. I could have simply mapped ][ above
" instead of ]], but prefer to have it modular. In the case that ]] is not
" mapped yet (e.g. because I disabled the mapping above), this simply does a
" `noremap ][ ]]`.
call s:mapcpy("][", "]]", expand("<sflnum>"))
noremap ]] ][
" Strip trailing whitespace
nnoremap <leader><space> <Cmd>keepp silent! %s/\v\s+$//<CR>
vnoremap <leader><space> <Cmd>keepp silent! '<,'>s/\v\s+$//<CR>
" Convert double quotes to single. Convert only pairs to lower the false
" positive rate.
nnoremap <leader>" <Cmd>keepp silent! %s/\v"([^"]*)"/'\1'/g<CR>
vnoremap <leader>" <Cmd>keepp silent! '<,'>s/\v"([^"]*)"/'\1'/g<CR>
" Better™ >> and <<. When using tabs for indentation and spaces for alignment,
" vim's behaviour is pretty disappointing since it will convert the indentation
" to a series of tabs followed by spaces. 'preserveindent' changes that, but
" adds the tabs at the end of the indentation, which does not make sense for
" aligned text as it will then have indentation consisting of tabs, spaces and
" again tabs.
"
" This tries to improve this by overriding >> and << to simply add or remove a
" tab (or spaces if 'expandtab' is set) at the beginning of the line. It also
" keeps the cursor on the same character and uses the normal-mode count like the
" visual one instead of targeting multiple lines since I always use visual mode
" for that (and it makes the function usable from normal and visual mode).
function! s:indent(count, left, visual)
" NOTE: the \t should be unescaped/in double quotes for strdisplaywidth
let l:indentation = &expandtab ? repeat(' ', shiftwidth()) : "\t"
" Count changes the level not the number of lines
let l:indentation = repeat(l:indentation, a:count + 1)
" save current cursor position
let l:line = line('.')
let l:col = virtcol('.')
let l:line_len = len(getline(l:line))
let l:off = strdisplaywidth(l:indentation)
if !a:left
let l:col += l:off
let l:substitute = 's/^/' .. l:indentation .. '/'
else
let l:col -= l:off
let l:substitute = 's/^' .. l:indentation .. '//'
endif
let l:substitute = (a:visual ? "'<,'>" : '') .. l:substitute
" Remove or add indentation at the beginning of the line
execute l:substitute
" Reset cursor position
if a:visual
" the cursor jumps to the last line that changed - reset it
execute ':' .. l:line
endif
if l:line_len != len(getline(l:line))
" Only change column if the current line changed
execute 'normal' l:col .. '|'
endif
endfunction
nmap >> <Cmd>call <SID>indent(v:count, v:false, v:false)<CR>
nmap << <Cmd>call <SID>indent(v:count, v:true, v:false)<CR>
" Also keep(s) selection after changing the indentation in visual mode
vmap > <Cmd>call <SID>indent(v:count, v:false, v:true)<CR>
vmap < <Cmd>call <SID>indent(v:count, v:true, v:true)<CR>
vnoremap = =gv
" Center search results while still respecting 'foldopen'
function! s:CenterNext(count, command)
let l:foldopen = match(&foldopen, 'search') > -1 ? 'zv' : ''
" Search count (i.e. [5/10]) will not display with 'lazyredraw'
let l:lazyredraw_bkp = &lazyredraw
set nolazyredraw
execute 'normal! ' .. a:count .. a:command .. l:foldopen .. 'zz'
let &lazyredraw = l:lazyredraw_bkp
endfunction
" NOTE: v:hlsearch's value is restored when returning from a function and thus
" needs to be set here (see :h function-search-undo)
map n <Cmd>call <SID>CenterNext(v:count1, 'n') <Bar> let v:hlsearch = 1<CR>
map N <Cmd>call <SID>CenterNext(v:count1, 'N') <Bar> let v:hlsearch = 1<CR>
cnoremap <expr> <CR> "<CR>" .
\ (getcmdtype() == '/' \|\| getcmdtype() == '?'
\ ? (match(&fdo, 'search') > -1 ? 'zv' : '') . "zz"
\ : "")
" Switch to lower/upper case
nnoremap <leader><C-U> gUl
vnoremap <leader><C-U> gU
nnoremap <leader><C-L> gul
vnoremap <leader><C-L> gu
" Expand visual selection over all directly following lines in the given
" direction that contain the current selection at the same position.
"
" Example:
" ```
" - TODO: ...
" - TODO: ...
" - TODO: ...
" ```
"
" In visual block one can select `TODO: ` on the first line and then call
" `ExpandVisualSelection(1)` which results in a block selection that spans over
" all other TODOs as well.
function! ExpandVisualSelection(direction)
let l:sel = GetVisualSelection('\')
normal gv
" Move the cursor onto the side of the selection that points in the
" direction of the expansion.
let l:swap_ends = 0
if (
\ (getpos('.') == getpos("'>") && a:direction < 0) ||
\ (getpos('.') == getpos("'<") && a:direction > 0)
\)
normal o
let l:swap_ends = 1
endif
if (a:direction < 0)
" Because of the greedy nature of search(), we cannot use the same
" regex/approach as when searching forwards, as it will only ever match
" the preceding line.
while (
\ (line('.') - 1) &&
\ match(getline(line('.') - 1), '\%'.col("'<").'c'.l:sel) != -1
\)
call cursor(line('.') - 1, col("'<"))
endwhile
elseif (a:direction > 0)
let l:pat = '\v%#(.*\n.*%' . col("'<") . 'c\V' . l:sel . '\)\*'
call search(l:pat, 'e')
else
" TODO: expand in both directions
endif
" Reset cursor column
if (l:swap_ends && mode() == "\<C-V>")
normal O
endif
endfunction
vmap <silent> <leader>j <Cmd>call ExpandVisualSelection(1)<CR>
vmap <silent> <leader>k <Cmd>call ExpandVisualSelection(-1)<CR>
" TODO: Also map h and l that expand the visual selection over a range of lines
" as far as all the lines are identical
"
" In the above example with a visual block selection including only the
" dashes: <leader>l would now expand the selection to also include the
" `TODO: ` after (or more if `...` continues to be the same on all lines)
let g:macro_type_mappings = {
\ '<Space>': '_',
\ }
function! s:macro_type()
if !exists('s:macro_type')
let s:macro_type = 0
endif
if !s:macro_type
let s:macro_type = 1
" Disable on InsertLeave
au! macro_type InsertLeave * call s:macro_type()
for [l:from, l:to] in items(g:macro_type_mappings)
execute 'imap ' .. l:from .. ' ' .. l:to
endfor
for l:key in "abcdefghijklmnopqrstuvwxyz"
execute 'imap ' .. l:key .. ' ' .. toupper(l:key)
endfor
else
let s:macro_type = 0
au! macro_type
for l:key in keys(g:macro_type_mappings)
execute 'iunmap ' .. l:key
endfor
for l:key in "abcdefghijklmnopqrstuvwxyz"
execute 'iunmap ' .. l:key
endfor
endif
endfunction
" Type everything uppercase and underscores instead of spaces
noremap <leader>mac <Cmd>call <sid>macro_type()<CR>i
augroup macro_type
" NOTE: group used in macro_type()
au!
augroup END
" Escape underscores (useful when writing LaTeX)
vmap <leader>\_ <Cmd>keepp '<,'>s/\v(^<Bar>[^\\])\zs\ze_/\\/g<CR>
" Center line when opening line from quickfix window
augroup qf_centered_enter
au!
au FileType qf noremap <buffer> <CR> <CR>zz
augroup END
" TODO: make `gf` open absolute paths relative to PWD if possible
" TODO: operator pending mode: [ia]$ in shell scripts to select the current
" parameter expansion or command substitution
"