Last active
September 10, 2020 10:54
-
-
Save adscriven/6dcdb723e50e768d267986d0efd75c1f to your computer and use it in GitHub Desktop.
Echo the path to the cursor, in terms of indentation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
" indentpath.vim -- echo the indentation path to the cursor. | |
" https://gist.github.com/adscriven/6dcdb723e50e768d267986d0efd75c1f | |
" Public domain. 2020-09-10 Iau | |
" 2020-09-10 allow echoing lines verbatim with linebreaks | |
" 2020-09-02 change default separator from | to │ | |
" 2020-09-01 s:fit(): remove unnecessary calculations | |
" 2020-09-01 s:truncpc(): handle multibyte | |
" 2020-09-01 s:echolist(): don't add padding to last item | |
" 2020-09-01 s:echolist(): invoke s:fit multiple times | |
" [ ] Echo the path after /...<cr>, not just after n. | |
" [ ] Multibyte is still not handled everywhere. | |
" [ ] Fix s:curindentline() | |
" 1. If a line has indentation, or starts in col 1 AND matches | |
" indentpath_pattern, use that line. | |
" 2. Otherwise the line is blank, or has char in col 1 AND doesn't | |
" match indentpath_pattern. So find lines above and below that | |
" have indentation. Pick the one with maximum indentation. | |
" 3. Give it a better name ffs. | |
" [ ] If <plug>[indentpath-n] generates pattern not found error, this | |
" produces an ugly message that there's an error in key() on line 1. | |
" Trap the error and :echomsg it properly. | |
" REQUIREMENTS {{{1 | |
" Vim 8 | |
" INSTALLATION {{{1 | |
" Chuck this file in ~/.vim/plugin | |
" USAGE {{{1 | |
" | |
" Example vimrc mappings: | |
" | |
" " Display the indentation path on demand. | |
" nmap <BS> <plug>[indentpath-echo] | |
" " Display the indentation path on demand. | |
" nmap \<BS> <plug>[indentpath-echo-verbatim] | |
" " Display the indentation path after searching. | |
" nmap n <plug>[indentpath-n] | |
" nmap N <plug>[indentpath-N] | |
" DESCRIPTION {{{1 | |
" | |
" This plugin echoes the path to the cursor in terms of indentation. | |
" This can be useful when editing long functions to remind you of | |
" your place in the code. It's also useful for indented lists and | |
" other structured text. | |
" So for example, with the text | |
" | |
" fun! s:findline(getaline, iscorrectline) | |
" let n = a:getaline(line('.')) | |
" while n != 0 | |
" | |
" if a:iscorrectline(n) | |
" | |
" return n | |
" # | |
" endif | |
" let n = a:getaline(n) | |
" endwhile | |
" return 0 | |
" endfun | |
" and the cursor indicated by '#', and a screen width of 80, and using | |
" the example mapping above, pressing backspace will display: | |
" | |
" fun! s:findline(getaline, iscorr | while n != | if a:iscorrectlin | |
" | |
" (It may show slightly less if the ruler is displayed in the last line.) | |
" The text is trimmed to fit the screen without triggering a 'Press ENTER ...' | |
" prompt. If the cursor is already at the top level of indentation, | |
" 'TOP' is echoed. | |
" | |
" Again, with the example mappings above, pressing backslash then | |
" backspace will display: | |
" | |
" fun! s:findline(getaline, iscorrectline) | |
" while n != 0 | |
" if a:iscorrectline(n) | |
" Press ENTER or type command to continue | |
" | |
" This is useful if the single-line display loses too much information | |
" and you want the full context. | |
" CONFIGURATION {{{1 | |
" | |
" Configuration is done via the global variables g:indentpath_format, | |
" g:indentpath_pattern, b:indentpath_format, and b:indentpath_pattern. | |
" Thus configuration may be done globally, with buffer-local overrides | |
" for specific filetypes. All variables are optional with fallback | |
" default. | |
" indentpath_format | |
" ----------------- | |
" This controls the path separators, colours, and how the path is | |
" adjusted to fit on the screen. The default value: | |
" let g:indentpath_format = { | |
" \ 'itemhl': 'Normal', The item highlight group. | |
" \ 'sep': '|', Characters separating items. | |
" \ 'sephl': 'SpecialKey', The separator highlight group. | |
" \ 'pad': ' ', Padding either side of a separator. | |
" \ 'padhl': 'Normal', The padding highlight group. | |
" \ 'width': 0, The target width for items. 0 is auto. | |
" \ 'minwidth': 10, The minimum truncated width. | |
" \ 'fit': 1, Adjust the item width to fit the screen. | |
" \ 'trunc': 'r', l, r, or empty. Truncate the left or right | |
" items in the path to fit the screen. | |
" \ 'verbatim': 0 Show lines as they are, with linebreaks. | |
" Overrules all other settings. | |
" You will get a 'Press ENTER ...' prompt. | |
" \ } | |
" | |
" Every property is optional. Some examples: | |
" let g:indentpath_format = { | |
" \ 'width': 0, | |
" \ 'minwidth': 0, | |
" \ 'fit': 0, | |
" \ 'trunc': '' | |
" \ } | |
" Each path item is displayed verbatim (aside from trimming whitespace). | |
" let g:indentpath_format = { | |
" \ 'width': 30, | |
" \ 'minwidth': 0, | |
" \ 'fit': 0, | |
" \ 'trunc': '' | |
" \ } | |
" Each path item is displayed as 30 characters wide. | |
" let g:indentpath_format = { | |
" \ 'width': 30, | |
" \ 'minwidth': 0, | |
" \ 'fit': 0, | |
" \ 'trunc': 'l' | |
" \ } | |
" Each path item is displayed as 30 characters wide, with items | |
" removed from the LHS if the path is too long to fit on the screen | |
" without generating a 'Hit ENTER ...' prompt. | |
" let g:indentpath_format = { | |
" \ 'width': 30, | |
" \ 'minwidth': 10, | |
" \ 'fit': 1, | |
" \ 'trunc': 'r' | |
" \ } | |
" Each path item is displayed as 30 characters wide, unless this | |
" results in a path too long for the screen. 'fit': 1 will cause the | |
" items to be reduced in size until the whole path fits. However no | |
" item will be made shorter than 10 characters. If the path is still | |
" too long to fit, 'truncate': 'r' will cause items to be removed from | |
" the RHS of the path until it is short enough. | |
" let g:indentpath_format = { | |
" \ 'minwidth': 9, | |
" \ 'pad': '' | |
" \ } | |
" Compact. | |
" indentpath_pattern | |
" ------------------ | |
" When looking for parent indentation lines, some lines may need to be | |
" excluded, for example empty lines, or preprocessor lines for C. | |
" Which lines to look for are specified as a regular expression which | |
" is matched against the whole line. | |
" The default is: | |
" '^\%(\s*\)\@>\%(\\\|//\|#\|\*\|/\*\|{\)\@!\&\%(^\h\k\*:\)\@!' | |
" This excludes lines starting with optional whitespace then | |
" '\', '//', '#', '/*', '*', '*\', or '{'. It also excludes | |
" lines with a 'LABEL:' in the first column. It should be a reasonable | |
" effort for VimL, C and perhaps other curly-brace languages. | |
" Alternatively a whitelist of lines to match may work better for | |
" specific filetypes, e.g.: | |
" '^\h\k*(\|^\s*\%(for\|if\|while\|switch\|struct\)\>' | |
" This includes lines starting with 'funcname(', or optional | |
" whitespace followed by 'for', 'if', etc., as a whole word. | |
" Blank lines are always automatically skipped when looking | |
" for the indentation path. | |
" To change the lines matched for a certain filetype, set | |
" b:indentpath_pattern in an ftplugin for that filetype, or use an | |
" autocommand in your vimrc. E.g.: | |
" augroup vimrc_indentpath | |
" autocmd! | |
" autocmd FileType foo let b:indentpath_pattern = | |
" \ '^\s*\%(fun\%[ction]\|if\|while\)\>' | |
" augroup END | |
" The match is case sensitive. Use '\c' in the pattern for | |
" a case-insensitive match. | |
" FIND INDENTATION PATH TO CURSOR {{{1 | |
let s:cpo = &cpo | |
set cpo&vim | |
" Defaults. | |
let s:patvar = 'indentpath_pattern' | |
let s:defpat = '^\%(\s*\)\@>\%(\\\|//\|#\|\*\|/\*\|{\)\@!\&\%(^\h\k\*:\)\@!' | |
let s:getpat = {-> get(b:, s:patvar, get(g:, s:patvar, s:defpat))} | |
" Search for a line. | |
" getaline(m): search from line m, return next line n, 0 when no more lines | |
" iscorrectline(n): return 0 if n == 0 or the check fails, 1 if it succeeds | |
" Returns: n when iscorrectline(n) returns 1. | |
fun! s:findline(getaline, iscorrectline) | |
let n = a:getaline(line('.')) | |
while n != 0 | |
if a:iscorrectline(n) | |
return n | |
endif | |
let n = a:getaline(n) | |
endwhile | |
return 0 | |
endfun | |
let s:prevnonblankline = {n -> prevnonblank(n - 1)} | |
" Return line n if it's non-blank, or the first non-blank | |
" line above or below, whichever has the largest indentation. | |
let s:curindentline = {n -> {nxt, prev -> {in, ip -> | |
\ in > ip ? nxt : prev | |
\ }(indent(nxt), indent(prev))}(nextnonblank(n), prevnonblank(n))} | |
" Make the iscorrectline() predicate for line n. | |
let s:makeisparentline = {n -> {nmax -> {m -> | |
\ indent(m) < indent(nmax) && getline(m) =~# s:getpat() | |
\ }}(s:curindentline(n))} | |
" Return the indentation path to line n as a list. | |
" Finds the parent lines of n, in terms of indentation. Returns a list | |
" of lines, ls, in order of least indentation to most indentation. It | |
" skips blank lines and lines prefixed with: \ // # * /* | |
" You need to supply the initial empty list, ls. | |
let s:indentpath = {n, ls -> n == 0 | |
\ ? ls | |
\ : s:indentpath(s:findline(s:prevnonblankline, s:makeisparentline(n)), | |
\ [getline(n)] + ls)} | |
" DISPLAY INDENTATION PATH {{{1 | |
" Defaults. | |
let s:fmtvar = 'indentpath_format' | |
let s:deffmt = { | |
\ 'itemhl': 'Normal', | |
\ 'sep': '│', | |
\ 'sephl': 'SpecialKey', | |
\ 'pad': ' ', | |
\ 'padhl': 'Normal', | |
\ 'width': 0, | |
\ 'minwidth': 10, | |
\ 'fit': 1, | |
\ 'trunc': 'r', | |
\ 'verbatim': 0 | |
\ } | |
let s:getfmt = {-> extend(extend(copy(s:deffmt), get(g:, s:fmtvar, {})), | |
\ get(b:, s:fmtvar, {}))} | |
" Non-mutating. | |
let s:map = {f, xs -> map(copy(xs), {i, x -> f(x, i)})} | |
" Trim whitespace from the left of string s. | |
let s:triml = {s -> substitute(s, '^\s*', '', '')} | |
" Replace spaces in s with tabs according to &tabstop. | |
" This has become unwieldy in order to deal with multibyte chars. | |
" strpart() because I can't slice: s[0:0] == s[0], and s[0:-1] == s | |
let s:tabstospaces = {s -> {i -> i == -1 | |
\ ? s | |
\ : s:tabstospaces(substitute(s, "\t", repeat(" ", &ts - i % &ts), "")) | |
\ }(stridx(s,"\t")==-1?-1:strdisplaywidth(strpart(s,0,stridx(s,"\t"))))} | |
" Fill width w with string s. Curried for use with s:map. | |
" strchars(s) < w: append spaces to width w | |
" strchars(s) == w: return s | |
" strchars(s) > w: truncate s to w | |
" w == 0: return s. | |
let s:fill = {w -> {s -> {len -> len >= w | |
\ ? s[0:w-1] | |
\ : s . repeat(' ', w - len) | |
\ }(strchars(s))}} | |
" Return screen width available without triggering 'Hit ENTER ...'. | |
" Subtracts space for the ruler from &columns. You also need to hide the | |
" ruler before echoing, and restore it afterwards. This may make the | |
" last path item appear shorter than it is because it's obscured by the | |
" restored ruler. This is necessary to maximise space without generating | |
" a prompt, even if the ruler isn't being displayed in the last line. | |
" XXX: Why this works isn't clear to me; subtract a larger number from | |
" &columns if you do get a prompt. | |
let s:room = {-> &columns - 12} | |
" Truncate string s by percentage pc, with minimum width m. | |
" Round down since integers and ultimately mustn't exceed s:room(). | |
let s:truncpc = {pc, m, s -> {len -> | |
\ substitute(strcharpart(s, 0, max([m, float2nr(round(len * pc - 0.5))])), | |
\ '\s\+$', '', '') | |
\ }(strchars(s))} | |
" Truncate each x in xs to fit total width w, with min. item width m. | |
" XXX: linear scale; exponential would work better so that longer items | |
" get trimmed more aggressively. | |
let s:fit = {w, m, xs -> {len -> len <= w | |
\ ? xs | |
\ : s:map({x -> s:truncpc(1.0 * w / len, m, x)}, xs) | |
\ }(strchars(join(xs, '')))} | |
fun! s:echohl(grp, s) | |
exe 'echohl ' . a:grp | |
echon a:s | |
echohl None | |
endfun | |
" Echo list according to format fmt. | |
fun! s:echolist(list, fmt) | |
let xs = s:map(s:triml, a:list) | |
let xs = s:map(s:tabstospaces, xs) | |
let xs[len(xs) - 1] = substitute(xs[len(xs) - 1], '\s\+$', '', '') | |
" Fill each item to width, or don't change it if width == 0. | |
let xs = s:map(s:fill(a:fmt.width), xs) | |
" Remove unnecessary padding from the last item. | |
let xs[len(xs)-1] = substitute(xs[len(xs)-1], '\s\+$', '', '') | |
let fullsep = a:fmt.pad . a:fmt.sep . a:fmt.pad | |
let Joinlen = {xs -> strchars(join(xs, fullsep))} | |
let seplen = (len(xs) - 1) * strchars(fullsep) | |
let r = s:room() - seplen | |
let len = Joinlen(xs) | |
let lastlen = 0 | |
" Shorten items if necessary. Some items may reach their minimum | |
" width, and then the total may not be shortened enough. But perhaps | |
" there are still some items which can be shortened further. So | |
" invoke s:fit() repeatedly until the items fit on the screen, or | |
" there is nothing left to shorten. | |
while a:fmt.fit && len > s:room() && len != lastlen | |
let xs = s:fit(r, a:fmt.minwidth, xs) | |
let lastlen = len | |
let len = Joinlen(xs) | |
endwhile | |
" Functions for truncating the path as a whole. | |
let trunclist = {} | |
" Remove items from left of xs, replacing with '...', until | |
" the joined xs and separators fit within s:room(). | |
let trunclist.l = {xs -> l:Joinlen(['...'] + xs) <= s:room() | |
\ ? ['...'] + xs | |
\ : len(xs) == 1 | |
\ ? ['...' . xs[0][-s : room() + 3]] | |
\ : l:trunclist.l(xs[1:])} | |
" Similarly, but from the right. | |
let trunclist.r = {xs -> l:Joinlen(xs + ['...']) <= s:room() | |
\ ? xs + ['...'] | |
\ : len(xs) == 1 | |
\ ? [xs[0][0 : s:room() - 4] . '...'] | |
\ : l:trunclist.r(xs[0 : -2])} | |
if a:fmt.trunc =~# '^[lr]$' && Joinlen(xs) > s:room() | |
let xs = trunclist[a:fmt.trunc](xs) | |
endif | |
" Hiding the ruler before echoing allows more room. | |
let ru = &ruler | |
set noruler | |
call s:echohl(a:fmt.itemhl, remove(xs, 0)) | |
for x in xs | |
call s:echohl(a:fmt.padhl, a:fmt.pad) | |
call s:echohl(a:fmt.sephl, a:fmt.sep) | |
call s:echohl(a:fmt.padhl, a:fmt.pad) | |
call s:echohl(a:fmt.itemhl, x) | |
endfor | |
let &ruler = ru | |
endfun | |
fun! s:echoindentpath(verbatim) | |
let xs = s:indentpath(line('.'), []) | |
" Remove the last item -- we don't need the current line. | |
let xs = xs[:-2] | |
let xs = empty(xs) ? ['TOP'] : xs | |
let fmt = s:getfmt() | |
if fmt.verbatim || a:verbatim | |
exe 'echohl ' . fmt.itemhl | |
echo join(xs, "\n") | |
echohl None | |
else | |
call s:echolist(xs, fmt) | |
endif | |
endfun | |
" PUBLIC INTERFACE {{{1 | |
nnoremap <silent> <plug>[indentpath-echo] :<c-u>call <sid>echoindentpath(0)<cr> | |
nnoremap <silent> <plug>[indentpath-echo-verbatim] | |
\ :<c-u>call <sid>echoindentpath(1)<cr> | |
fun! s:key(c) | |
exe 'normal!' . v:count1 . a:c . 'zv' | |
redraw | |
call s:echoindentpath(0) | |
endfun | |
" Automatically echo the indentation path after searching. | |
nnoremap <silent> <plug>[indentpath-n] :<c-u>call <sid>key('n')<cr> | |
nnoremap <silent> <plug>[indentpath-N] :<c-u>call <sid>key('N')<cr> | |
let &cpo = s:cpo | |
" vim: set fdm=marker : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment