autoload/unobtrusive_fold.vim
214888c9
 "" Guard
 if exists('g:autoloaded_unobtrusive_folds') || &compatible
   finish
 endif
 let g:autoloaded_unobtrusive_folds = 1
 
 "" Configuration
 
 ""
 " @section Configuration, config
 " Since this method of folding is more involved than the built-in ones it can
 " be slow for very big files. Options are provided to mitigate this.
 
 """ g:unobtrusive_fold_max_lines
 if !exists('g:unobtrusive_fold_max_lines')
   ""
   " Set to non-zero value to make the commands below do nothing if the number
   " of lines in the current buffer is higher than the given value. Defaults to
   " 0.
   let g:unobtrusive_fold_max_lines = 0
 endif
 
 """ g:unobtrusive_fold_max_prevlines
 if !exists('g:unobtrusive_fold_max_prevlines')
   ""
   " Set to non-zero value to look at at most this many previous lines when
   " calculating folds. Defaults to 0.
   let g:unobtrusive_fold_max_prevlines = 0
 endif
 
 "" Constants
 
 let s:pattern_prefix = '\V\c\^\s\*'
 
 "" Functions
 
 """ #text()
 
 ""
 " @public
 " Set 'foldtext' to this function to get an improved version of
 " `getline(v:foldstart)`. Leading tabs are expanded to 'tabstop' spaces. If
 " 'fillchars' `fold:` is not space, the number of lines included in the fold is
 " printed, padded with `fold:` so that it lines up with one column short of the
 " right edge of the window, or 'textwidth' (if not 0), whichever is smaller.
 " The empty column at the far right is nice for people who set 'colorcolumn' to
 " `+1`. The rest of the line is cleared (i.e. not filled automatically by Vim
 " with 'fillchars' `fold:`). A good value for `fold:` might be the box drawing
 " character `─`.
 function! unobtrusive_fold#text() abort
   let line = getline(v:foldstart)
   let line = substitute(line, '\t', repeat(' ', &l:tabstop), 'g')
   let fillchar = matchstr(&fillchars, '.*\(^\|,\)fold:\zs.\ze')
   if empty(fillchar)
     let fillchar = '-'
   endif
   if fillchar !=# ' '
     let foldlines = v:foldend - v:foldstart + 1
     let signs = len(split(execute('sign place buffer=' . bufnr()), '\n')) - 1
     let numbers = &l:number ? line('$') : &l:relativenumber ? winheight(0) : 0
     let winwidth = winwidth(0)
     \ - &l:foldcolumn
     \ - (signs && &l:signcolumn ==# 'auto' || &l:signcolumn ==# 'yes' ? 2 : 0)
     \ - (numbers ? max([&l:numberwidth, strlen(numbers) + 1]) : 0)
     let width = winwidth - 1
     if &l:textwidth
       let width = min([width, &l:textwidth])
     endif
     let fill = repeat(fillchar, width - strlen(line) - strlen(foldlines) - 2)
     let pad = repeat(' ', winwidth - width)
     let line = line . ' ' . fill . ' ' . foldlines . pad
   endif
   return line
 endfunction
 
 """ #comment()
 
 function! unobtrusive_fold#comment(bang) abort
   if empty(&l:commentstring)
     return
   endif
   let commentstart = escape(split(&l:commentstring, '\s*%s')[0], '\')
   let commentlast  = escape(commentstart[-1:],                   '\')
   call unobtrusive_fold#set(
   \   a:bang,
   \   commentstart . '\zs' . commentlast . '\+\ze',
   \   commentstart,
   \   []
   \ )
 endfunction
 
 """ #char()
 
 function! unobtrusive_fold#char(bang, char) abort
   let char = escape(a:char, '\')
   call unobtrusive_fold#set(
   \   a:bang,
   \   '\zs' . char . '\+\ze',
   \   '',
   \   ['Comment', 'Code', 'Snippet']
   \ )
 endfunction
 
 """ #set()
 
 function! unobtrusive_fold#set(bang, pattern, commentstart, ignore) abort
   if g:unobtrusive_fold_max_lines && line('$') > g:unobtrusive_fold_max_lines
     return
   endif
   let w:unobtrusive_fold_indent       = !empty(a:bang)
   let w:unobtrusive_fold_pattern      = a:pattern . '\s\*\S'
   let w:unobtrusive_fold_ignore       = a:ignore
   let w:unobtrusive_fold_commentstart = a:commentstart
   let &l:foldexpr   = 'unobtrusive_fold#expr()'
   let &l:foldmethod = 'expr'
 endfunction
 
 """ s:foldlevel()
 
 function! s:foldlevel(lnum) abort
   let foldlevel = strlen(matchstr(
   \   getline(a:lnum),
   \   s:pattern_prefix . w:unobtrusive_fold_pattern
   \ ))
   if foldlevel && !empty(w:unobtrusive_fold_ignore)
     for syntax in map(synstack(a:lnum, 1), 'synIDattr(v:val, "name")')
       for ignore in w:unobtrusive_fold_ignore
         if syntax =~# '\v' . '(^|[a-z])' . ignore . '([A-Z]|$)'
           return 0
         endif
       endfor
     endfor
   endif
   return foldlevel
 endfunction
 
 """ s:indent()
 
 function! s:indent(lnum, ...) abort
   if (a:lnum < 1 && line('$') < a:lnum) || (a:0 && s:foldlevel(a:lnum))
     return 0
   endif
   return !empty(getline(a:lnum)) + indent(a:lnum) / shiftwidth()
 endfunction
 
 """ s:prevfoldlevelbase()
 
 function! s:prevfoldlevelbase(lnum) abort
   let stopline = g:unobtrusive_fold_max_prevlines
   \ ? max(1, a:lnum - g:unobtrusive_fold_max_prevlines)
   \ : 0
   let pattern_column = '\%<' . (indent(a:lnum) + 2) . 'v'
   let pos = getpos('.')
   call cursor(a:lnum, 1)
   let prev = search(
   \   s:pattern_prefix . pattern_column . w:unobtrusive_fold_pattern,
   \   'bW',
   \   stopline
   \ )
   call setpos('.', pos)
   if !prev
     return [0, 0]
   endif
   let foldlevel = s:foldlevel(prev)
   return [foldlevel, foldlevel - indent(prev) / shiftwidth()]
 endfunction
 
 """ s:nextnonblank()
 
 function! s:nextnonblank(lnum)
   if empty(w:unobtrusive_fold_commentstart)
     return nextnonblank(a:lnum)
   endif
   let pos = getpos('.')
   call cursor(a:lnum, 1)
   let next = search(
   \   '\V\^\(' . w:unobtrusive_fold_commentstart . '\[[:punct:]]\*\$\)\@!\.',
   \   'Wc'
   \ )
   call setpos('.', pos)
   return next
 endfunction
 
 """ #expr()
 
 function! unobtrusive_fold#expr() abort
   """" Empty line
   if empty(getline(v:lnum))
     return '='
   endif
   """" Marker...
   """" ...start
   let foldlevel = s:foldlevel(v:lnum)
   if foldlevel
     return '>' . foldlevel
   endif
   """" ...end...
   let next = s:nextnonblank(v:lnum + 1)
   """" ...foldlevel
   let nextfoldlevel = s:foldlevel(next)
   if nextfoldlevel
     let prevfoldlevel = s:prevfoldlevelbase(v:lnum)[0]
     return '<' . (min([nextfoldlevel, prevfoldlevel]) + 1)
   endif
   """" ...indent
   if indent(next) < indent(v:lnum)
     let prevfoldlevel = s:prevfoldlevelbase(v:lnum)[0]
     let nextfoldlevel = s:prevfoldlevelbase(next)[0]
     if nextfoldlevel < prevfoldlevel
       return '<' . (nextfoldlevel + 1)
     endif
   endif
   """" Indent...
   if w:unobtrusive_fold_indent
     let indent      = s:indent(v:lnum)
     let nextindent  = s:indent(next)
     let aboveindent = s:indent(v:lnum - 1, 1)
     let belowindent = s:indent(v:lnum + 1, 1)
     """" ...start
     if aboveindent < indent && (belowindent >= indent || nextindent > indent)
       let prevbase = s:prevfoldlevelbase(v:lnum)[1]
       return '>' . (prevbase + indent)
     endif
     """" ...end
     if belowindent < indent && (aboveindent || belowindent)
       let prevbase = s:prevfoldlevelbase(v:lnum)[1]
       return '<' . (prevbase + min([indent, nextindent + !!belowindent]))
     endif
   endif
   """" No change
   return '='
 endfunction
 
 """ #debug()
 
 function! unobtrusive_fold#debug() abort
   let lines  = range(1, line('$'))
   let exprs  = map(copy(lines), 'execute("let v:lnum=v:val|echon ".&foldexpr)')
   for l in lines
     let e = exprs[l-1]
     if len(e) == 1
       let exprs[l-1] = e =~# '^[0-9]$' ? ' '.e : e.' '
     endif
   endfor
   let levels = map(copy(lines), 'foldlevel(v:val)')
   let debugs = map(copy(lines), '"[".exprs[v:val-1]."|".levels[v:val-1]."]"')
 
   let pos = getpos('.')
   call setreg('"', join(debugs, "\n"), 'b')
   normal! gg0""P
   call setpos('.', pos)
 endfunction