" vim: set foldmethod=marker: " Keybindings """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" " Clear search result highlights when pressing Escape in normal mode nnoremap :nohlsearch " Indentation jump " https://vim.fandom.com/wiki/Move_to_next/previous_line_with_same_indentation " noremap :call search('^'. matchstr(getline('.'), '\(^\s*\)') .'\%<' . line('.') . 'l\S', 'be') " noremap :call search('^'. matchstr(getline('.'), '\(^\s*\)') .'\%>' . line('.') . 'l\S', 'e') " Split view navigation " Create new panes nnoremap N vsplit nnoremap n split " My brain expects {gf,gF} to open in a split, not a tab. " Swap the mappings for tab and split. nnoremap gf f nnoremap gF F nnoremap f gf nnoremap gf nnoremap F gF " Analogous mapping to tmux's ! to move the current window to a new tab nnoremap ! 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 vnoremap S :s/\%V/gc endif " Interact with the system clipboard if (has('clipboard')) map y "+y map Y "+Y map p "+p map 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 " Ctrl-Backspace should delete words in insert mode and on command-line. noremap! map! " Correct word with best/first suggestion. noremap c 1z= " Correct next or last misspelled word (and their non-rare/region versions) " without moving " TODO: see :keepjumps " Problem: with keepjumps the is not possible anymore noremap ]s ]s1z= noremap [s [s1z= noremap ]S ]S1z= noremap [S [S1z= " 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 st set spell! map sc call CycleSpellLang() map ss :set spelllang= " Jump through jump table but center while still respecting 'foldopen' noremap '' . (match(&fdo, 'mark') > -1 ? 'zv' : '') . 'zz' noremap '' . (match(&fdo, 'mark') > -1 ? 'zv' : '') . 'zz' nmap nmap " Terminal if (has('nvim')) tnoremap nmap :split +terminali nmap v :vsplit +terminali elseif (has('terminal')) nmap terminal endif " Plugin specific bindings if (get(g:, 'loaded_fzf')) nmap f Files nmap j Lines nmap / Lines nmap h Helptags " TODO: fix this? if (get(g:, 'loaded_gutentags') || 1) nmap t Tags nmap bt BTags endif endif " Search for selected text. " Modified from https://vim.fandom.com/wiki/Search_for_visually_selected_text function! GetVisualSelection() let l:old_reg = getreg('"') let l:old_regtype = getregtype('"') norm gvy let l:sel = getreg('"') call setreg('"', l:old_reg, l:old_regtype) return l:sel endfunction vmap * /\V=escape(GetVisualSelection(),'/\') vmap # ?\V=escape(GetVisualSelection(),'?\') " Extended `*`. Starts vim search (without jump) and ripgrep nmap * :let @/ = '\<' . expand('') . '\>' \ set hlsearch \ Rg \b=expand('')\b vmap * :let @/ = "\\V=escape(escape(GetVisualSelection(), '\'), '"\')" \ set hlsearch \ Rg =escape(GetVisualSelection(), '.\[]*+?{}^$()') nmap g* :let @/ = expand('') \ set hlsearch \ Rg =expand('') vmap g* * " Search inside visual selection noremap v/ /\%V vmap / 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 nnoremap gp '`[' . strpart(getregtype(), 0, 1) . '`]' " Git bindings " Insert a commit's subject behind the SHA1 that the cursor is currently on. " Mnemonic: "git reference commit" " NOTE: This uses `system` and not `:r!` to insert the text directly at the " cursor. `subject[:-2]` cuts off the trailing newline. " TODO: print error message but insert nothing on git error nmap grc :let subject=system('git show -s --format="(\"%s\")" ')viwa =subject[:-2] " Insert a Signed-off-by trailer nmap gso :r!git config --get user.name:r!git config --get user.emailI<A>kJISigned-off-by: " Add, stash or checkout the current file nmap ga !git add -- % nmap gs !git stash -- % nmap gu !git checkout -- % if exists('g:loaded_fugitive') " Interactive `git status` nmap gg G " Start a commit and open the message in a split nmap gcc G commit " Amend the current commit and open the message in a split nmap gca G commit --amend " Move to root of directory nmap gcd Gcd " git blame in scroll bound vertical split (only the commit hashes, see " :help :Git_blame) nmap gb :G blameC else " Move to root of repository " NOTE: only works if a file is already opened nnoremap gcd cd %:h cd `git rev-parse --show-toplevel` endif if exists('g:loaded_gitgutter') " Add `g` prefix to hunk bindings " Mnemonic: "git hunk " nmap gha (GitGutterStageHunk) " TODO: nmap ghs (GitGutterStashHunk) nmap ghu (GitGutterUndoHunk) nmap ghp (GitGutterPreviewHunk) " StageHunk can be used for single lines. Mnemonic w/o `h`unk xmap ga (GitGutterStageHunk) " Add hunk/h version to textobject bindings that use `c` (for `change I " presume?) (e.g. ic -> ih) omap ih (GitGutterTextObjectInnerPending) omap ah (GitGutterTextObjectOuterPending) xmap ih (GitGutterTextObjectInnerVisual) xmap ah (GitGutterTextObjectOuterVisual) " Same for hunk navigation bindings + center line nmap [h (GitGutterPrevHunk)zz nmap ]h (GitGutterNextHunk)zz endif if (get(g:, 'loaded_fzf')) " git files that `git status` lists nmap gf GFiles? " 'git log (log?)' and 'git log buffer ' map gll Commits map glb BCommits " TODO: glb should restrict the log to all staged files when called " in .git/COMMIT_EDITMSG, maybe with an ftplugin? endif " Y should behave like D & C does nnoremap Y y$ " Clear line (`cc` but stay in normal mode) nmap dd 0D vmap d keepp '<,'>s/^.*$// " Fix & command to also use last flags nnoremap & && xnoremap & && " see :help i_ctrl-g_u " Do not break undo sequence when using the arrow keys or home and end in insert " mode inoremap U inoremap U inoremap col('.') == match(getline('.'), '\S') + 1 ? \ repeat('U', col('.') - 1) : \ (col('.') < match(getline('.'), '\S') ? \ repeat('U', match(getline('.'), '\S') + 0) : \ repeat('U', col('.') - 1 - match(getline('.'), '\S'))) inoremap repeat('U', col('$') - col('.')) " break undo sequence with every space and newline, making insert mode changes " revertible in smaller chunks inoremap u " Open the manpage in the WORD under cursor nnoremap gm :Man xnoremap gm :Man " Format the current paragraph, while keeping the cursor position nmap Q gwap imap 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 utc ciw=strftime("%F %T", @") vnoremap utc keepp '<,'>s/\v(^\|[^0-9])\zs[0-9]{10}\ze([^0-9]\|$)/\=strftime("%F %T",submatch(0))/g " Convert decimal numbers to hex " https://stackoverflow.com/a/1118642 nnoremap hex ciw=printf("0x%X", @") vnoremap hex keepp '<,'>s/\v<\d+>/\=printf("0x%X", submatch(0))/g " TODO: 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 [[ call search('\v^(\S.*)?\{', 'besW') noremap ]] call search('\v^(\S.*)?\{', 'esW') " Make `dest` do the same thing as `src` currently does. `src` can be remapped " afterwards without `dest`'s behaviour changing. Call with `expand("")` " 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 ` (but with correct lnum) let l:curr_map = { \ "rhs": a:from, "noremap": 1, "mode": a:mode, \ "sid": expand(""), \ "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("")) noremap ]] ][ " Strip trailing whitespace nnoremap keepp silent! %s/\v\s+$// vnoremap keepp silent! '<,'>s/\v\s+$// " Convert double quotes to single. Convert only pairs to lower the false " positive rate. nnoremap " keepp silent! %s/\v"([^"]*)"/'\1'/g vnoremap " keepp silent! '<,'>s/\v"([^"]*)"/'\1'/g " 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 >> call indent(v:count, v:false, v:false) nmap << call indent(v:count, v:true, v:false) " Also keep(s) selection after changing the indentation in visual mode vmap > call indent(v:count, v:false, v:true) vmap < call indent(v:count, v:true, v:true) 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 call CenterNext(v:count1, 'n') let v:hlsearch = 1 map N call CenterNext(v:count1, 'N') let v:hlsearch = 1 cnoremap "" . \ (getcmdtype() == '/' \|\| getcmdtype() == '?' \ ? (match(&fdo, 'search') > -1 ? 'zv' : '') . "zz" \ : "") " Switch to lower/upper case nnoremap gUl vnoremap gU nnoremap gul vnoremap 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 = escape(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() == "\") normal O endif endfunction vmap j call ExpandVisualSelection(1) vmap k call ExpandVisualSelection(-1) " 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: 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 = { \ '': '_', \ } 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 mac call macro_type()i augroup macro_type " NOTE: group used in macro_type() au! augroup END " Escape underscores (useful when writing LaTeX) vmap \_ keepp '<,'>s/\v(^[^\\])\zs\ze_/\\/g " Center line when opening line from quickfix window augroup qf_centered_enter au! au FileType qf noremap zz augroup END " TODO: make `gf` open absolute paths relative to PWD if possible "