eigenclass logo
MAIN  Index  Search  Changes  PageRank  Login

SimpleFold 0.4.0 and visual comparison of (vim) folding methods

/hiki/update.png 0.4.0b: quick bugfix release fixing issues with non-Ruby files, and defaulting to closing folds after <Leader>f. Also a few words about how to use the :Fold command as a sort of "internal grep" (a tribute to the vimtip this was originally based on).

I've been reworking my vim folding script for Ruby. It stays within the usability subspace I like (optimized vertical space, sensible foldtext, fold on classes, methods, etc.), but now it can also fold optionally on if/while and friends, and also supports folds set with markers. It is now available as a plugin, so installation is as easy as copying it into ~/.vim/plugin/; the default mapping is <Leader>f (i.e. \f if you didn't change mapleader).

Here's a few animations comparing it to the 'syntax' and 'marker' methods.

Syntax vs. SimpleFold

Both images correspond to the same files; I've timed them carefully to show equivalent views (top-level, class-level, method-level) at a time (you might have to reload the page to make sure they play synchronously). The first one is fdm=syntax, the second is SimpleFold's /hiki/syntax.gif /hiki/simplefold1.gif

  • syntax doesn't fold comments, so the window covers but a small part of the buffer even when all folds are closed
  • syntax uses nested folds for nested classes/modules/methods; SimpleFold makes them all top-level so there's no need to open class/module folds to see their methods (fdm=syntax with foldnestmax is not enough)
  • syntax folds on lots of things, which is good or bad depending on your preferences

Marker vs. Simplefold

/hiki/marker.gif /hiki/simplefold2.gif

  • markers are robust and portable, but need some work and pollute the code
  • SimpleFold is almost a superset of fdm=marker because you can specify nestable folds with
# open a fold {{{{ 
 .... stuff
 # and now close it }}}} (the markers can be anywhere in the line)

(yes, FOUR braces, so that legacy markers are ignored) and top-level ones with

 #{{{   # this line must match ^\s*\{\{\{
   code
  #}}}   # and this one  ^\s*\{\{\{
 # again, to ignore legacy markers 
  • SimpleFold optimizes vertical space usage: no markers (by default) cluttering the code. This, and the "sticky comments" (which get folded along with the next method/class/etc) plus the use of foldtext help achieve over 66% vertical space savings in the "class-level" view.

Download

Just copy it to the plugin directory (e.g. ~/.vim/plugin), and use <Leader>f to fold.

/hiki/update.png You can also use

 :Fold UNQUOTED-REGEXP

to create folds showing the lines matching the given regexp, so that everything else is hidden; e.g.

 :Fold \vfoo(bar|baz)

will create folds starting at each line matching foo(bar|baz), so that these are the only lines you see in your buffer (it's effectively a sort of internal grep).

This vimscript is fairly tricky; I consider it BETA until I get some feedback, so please report any problems (preferably along with the code that triggers the error).

SimpleFold.vim

" Copyright (C) 2006    Mauricio Fernandez <mfp@acm.org>
" Plugin for simple search-based folding
" Designed for use with Ruby, but can be tailored to other filetypes.
"     Version:    0.4.0b 2006-05-12
"      Author:    Mauricio Fernandez <mfp@acm.org>
"  Maintainer:    Mauricio Fernandez <mfp@acm.org> http://eigenclass.org
"     License:    Ruby's license (dual GPL/"Ruby artistic license")
" 
" Mappings and commands
" ---------------------
" Defines the :Fold command; use it as 
"   :Fold \v^function
" You can try it with this very file to see what happens.
" The default mapping to fold the current file using the default fold
" expression (more on this below) is
"    map <unique> <silent> <Leader>f <Plug>SimpleFold_Foldsearch
"                          =========
" i.e. \f unless you changed mapleader. You can copy the above mapping to your
" .vimrc and modify it as desired.
"
" Options
" -------
" By default, secondary, nestable subfolds will be created for the supported
" filetypes (read below to see how this is controlled by the associated fold
" expressions). You can turn that off with:
"   let g:SimpleFold_use_subfolds = 0
"
" Fold expressions
" ----------------
" The default fold expression for most filetypes is
"   let b:simplefold_expr = '\v^\s*[#%"0-9]{0,4}\s*\{(\{\{|!!)'
" The expressions for the extra marker-based folding phase are:
"   let b:simplefold_marker_start = '\v\{\{\{\{'
"   let b:simplefold_marker_end = '\v\}\}\}\}'
"
" You can tailor the fold expressions to other filetypes, taking the
" expressions for Ruby as an example:
"
"    au Filetype ruby let b:simplefold_expr = 
"	    \'\v(^\s*(def|class|module|attr_reader|attr_accessor|alias_method|' .
"                 \   'attr|module_function' . ')\s' . 
"           \ '\v^\s*(public|private|protected)>' .
"	    \ '|^\s*\w+attr_(reader|accessor)\s|^\s*[#%"0-9]{0,4}\s*\{\{\{[^{])' .
"	    \ '|^\s*[A-Z]\w+\s*\=[^=]'
"    au Filetype ruby let b:simplefold_nestable_start_expr = 
"		\ '\v^\s*(def>|if>|unless>|while>.*(<do>)?|' . 
"                \         'until>.*(<do>)?|case>|for>|begin>)' .
"                \ '|^[^#]*.*<do>\s*(\|.*\|)?'
"    au Filetype ruby let b:simplefold_nestable_end_expr = 
"		\ '\v^\s*end'
"
" Here's the (simpler) setup for Java:
" Java support
"    au Filetype java let b:simplefold_expr = 
"			 \ '\(^\s*\(\(private\|public\|protected\|class\)\>\)\)'

if exists("loaded_simplefold")
    finish
endif
let loaded_simplefold = 1

let s:save_cpo = &cpo
set cpo&vim

"{{{ set s:sid

map <SID>xx <SID>xx
let s:sid = maparg("<SID>xx")
unmap <SID>xx
let s:sid = substitute(s:sid, 'xx', '', '')

"{{{ FoldText
function! s:Num2S(num, len)
    let filler = "                                                            "
    let text = '' . a:num
    return strpart(filler, 1, a:len - strlen(text)) . text
endfunction

function! s:SimpleFold_FoldText()
    let linenum = v:foldstart
    if match(getline(linenum), b:simplefold_marker_start) != -1
  let line = getline(linenum)
    else
  while linenum <= v:foldend
      let line = getline(linenum)
      if !exists("b:simplefold_prefix") || match(line, b:simplefold_prefix) == -1
    break
      else
    let linenum = linenum + 1
      endif
  endwhile
  if exists("b:simplefold_prefix") && match(line, b:simplefold_prefix) != -1
      " all lines matched the prefix regexp
      let line = getline(v:foldstart)
  endif
    endif
    let sub = substitute(line, '/\*\|\*/\|{{{\d\=', '', 'g')
    let diff = v:foldend - v:foldstart + 1
    return  '+' . v:folddashes . '[' . s:Num2S(diff,3) . ']' . sub
endfunction

"{{{~ Foldsearch adapted from t77: Fold on search result
function! s:Foldsearch(search)
    call s:SimpleFold_SetupBuffer()
    " set manual
    setlocal fdm=manual
    let origlineno = line(".")
    normal zE
    normal G$
    " set the foldtext
    execute 'setlocal foldtext=' .  s:sid . 'SimpleFold_FoldText()'
    let folded = 0     "flag to set when a fold is found
    let line1 =  0     "set marker for beginning of fold
    let flags = "w"    "allow wrapping
    let first_code_line = 0
    if a:search == ""
  if exists("b:simplefold_expr")
      let searchre = b:simplefold_expr
  else
      let searchre = '\v^\s*[#%"0-9]{0,4}\s*\{(\{\{|!!)'
  endif
    else
  let searchre = a:search
    endif
    while search(searchre, flags) > 0 
  let  line2 = line(".")
  while line2 - 1 >= line1 && line2 - 1 > 0 "sanity check
      let prevline = getline(line2 - 1)
      if exists("b:simplefold_prefix") && (match(prevline, b:simplefold_prefix) != -1)
    let line2 = line2 - 1
      else
    break
      endif
  endwhile
  if (line2 - 1 >= line1)
      execute ":" . line1 . "," . (line2-1) . "fold"
      "echo "fold " . line1 . " - " . (line2 - 1)
      if g:SimpleFold_use_subfolds
    call s:FoldNestableBlocks(first_code_line + 1, line2 - 2, "", "")
      endif
      let folded = 1       "at least one fold has been found
  endif
  let line1 = line2     "update marker
  let first_code_line = line2 + 1
  let flags = "W"       "turn off wrapping
    endwhile
    let line2 = line("$")
    if (line2  >= line1 && folded == 1)
  execute ":". line1 . "," . line2 . "fold"
  execute "normal " . line1 . "G"
  " try to find the last top-level fold so that we get the correct range
  " for nested subblocks
  if search(searchre, "W") > 0  
      let line1 = line(".")    
  endif
  let line1 = line1 + 1
  "echo "last call: " line1 . " - " . line2
  if g:SimpleFold_use_subfolds
      call s:FoldNestableBlocks(line1, line2, "", "")
  endif
    endif
    call s:FoldNestableBlocks(1, line("$"), b:simplefold_marker_start, 
  \			 b:simplefold_marker_end)
    normal zM
    execute "normal " . origlineno . "G"
endfunction

function! s:FoldNestableBlocks(start, end, start_expr, end_expr)
    call s:SimpleFold_SetupBuffer()
    if a:end - a:start < 1
  return 0
    endif

    if a:start_expr == ""
  if exists("b:simplefold_nestable_start_expr")
      let start_expr = b:simplefold_nestable_start_expr
  else
      return
  endif
    else
  let start_expr = a:start_expr
    endif
    if a:end_expr == ""
  if exists("b:simplefold_nestable_end_expr")
      let end_expr = b:simplefold_nestable_end_expr
  else
      return
  endif
    else
        let end_expr = a:end_expr
    endif
    "echo "nested " . a:start . " <-> " . a:end
    let origlineno = line(".")
    execute "normal " . (a:start - 1). "G" 
    normal $
    normal zR
    " allow wrapping if a:start was 1 (i.e. we moved to line("$"))
    let flags = (a:start == 1) ? "w" : "W"
    let done_up_to = a:start
    "echo "searching for " . start_expr . " from " . line(".")
    while search(start_expr, flags) > 0
  let flags = "W"
  let first_line = line(".")
  "echo "MATCH " . start_expr . " " . first_line
  if first_line >= a:end || first_line < done_up_to
      break
  endif
  if searchpair(start_expr, "", end_expr, "W") > 0
      let last_line = line(".")
      let done_up_to = last_line
      if last_line <= a:end
    "echo "nested fold (" . a:start . " - " .  a:end . ") " .
    "     \ first_line . " - " . last_line
    execute ":" . first_line . "," . last_line . "fold"
    if last_line - first_line >= 2 && last_line <= a:end
        call s:FoldNestableBlocks(first_line + 1, last_line - 1,
        \                         start_expr, end_expr)
    endif
      endif
  endif
    endwhile
    execute "normal " . origlineno . "G"
    "echo "RET " . a:start . " - " . a:end " -> " . origlineno
endfunction

function! s:SimpleFold_SetupBuffer()
    if !exists("b:simplefold_expr")
  let b:simplefold_expr = '\v^\s*[#%"0-9]{0,4}\s*\{(\{\{|!!)'
    endif
    if !exists("b:simplefold_marker_start")
  let b:simplefold_marker_start = '\v\{\{\{\{'
    endif
    if !exists("b:simplefold_marker_end")
  let b:simplefold_marker_end = '\v\}\}\}\}'
    endif
endfunction

"{{{~fold commands

if !exists(":Fold")
    command -nargs=1 Fold :call s:Foldsearch(<q-args>)
endif

"{{{ mappings and default options
if !hasmapto("<Plug>SimpleFold_Foldsearch")
    map <unique> <silent> <Leader>f <Plug>SimpleFold_Foldsearch
endif
noremap <unique> <script> <Plug>SimpleFold_Foldsearch <SID>FoldSearch
noremap <SID>FoldSearch :call <SID>Foldsearch("")<cr>

let g:SimpleFold_use_subfolds = 1

"{{{ Fold expressions for different filetypes

" default expression
aug SimpleFold
    au!
    au BufEnter * call s:SimpleFold_SetupBuffer()
    " Ruby support
    au Filetype ruby let b:simplefold_expr = 
      \'\v(^\s*(def|class|module|attr_reader|attr_accessor|alias_method|' .
                 \   'attr|module_function' . ')\s' . 
            \ '|\v^\s*(public|private|protected)>' .
      \ '|^\s*\w+attr_(reader|accessor)\s|^\s*[#%"0-9]{0,4}\s*\{\{\{[^{])' .
      \ '|^\s*[A-Z]\w+\s*\=[^=]|^__END__$'
    au Filetype ruby let b:simplefold_nestable_start_expr = 
    \ '\v^\s*(def>|if>|unless>|while>.*(<do>)?|' . 
                \         'until>.*(<do>)?|case>|for>|begin>)' .
                \ '|^[^#]*.*<do>\s*(\|.*\|)?'
    au Filetype ruby let b:simplefold_nestable_end_expr = 
    \ '\v^\s*end'
    
    au Filetype ruby let b:simplefold_prefix='\v^\s*(#.*)?$'

    " Java support
    au Filetype java let b:simplefold_expr = 
       \ '\(^\s*\(\(private\|public\|protected\|class\)\s\)\)'
aug END

let &cpo = s:save_cpo


Bug in call to s:FoldNestableBlocks - Scott Guelich (2006-05-26 (Fri) 20:30:18)

Right now if b:simplefold_expr doesn't match anything in the file, then none of the nestable folds happen either and nothing gets folded. It looks like it's due to a bug in the call to s:FoldNestableBlocks that happens at the end of s:Foldsearch (line 162).

The call currently reads:

 call s:FoldNestableBlocks(1, line("$"), b:simplefold_marker_start, 
 \    b:simplefold_marker_end)

when it should probably be:

 call s:FoldNestableBlocks(1, line("$"), b:simplefold_nestable_start_expr, 
 \    b:simplefold_nestable_end_expr)

Scott 2006-05-26 (Fri) 20:34:27

Oops, too quick on the tab key... meant to also add a big *THANKS*! It's great that this solution is so customizable... and it definitely makes folding more useful with Ruby.

mfp 2006-05-29 (Mon) 05:50:22

Thanks for the bug report! Actually, you could call it a misfeature: I thought that nestable folds should only happen when there are top-level folds, but what you describe makes probably more sense. I've spent most of my hacktime on rcov lately, but I also made some changes to SimpleFold (simplified filetype-based settings and cleaned it up a little). I have an idea to make SimpleFold faster and more "interactive" (so that folds update automatically and you don't need to <Leader>f all the time), but it'll take a while.


Support for PHP - Dan (2006-05-13 (Sat) 03:05:52)

After a little experimentation, here's what I've got so far for PHP.

au Filetype php let b:simplefold_expr =
   \ '\v^\s*(class|function|const|public|private|define)>' .
   \ '|^\s*[#%"0-9]{0,4}\s*\{\{\{[^{]'
au Filetype php let b:simplefold_nestable_start_expr =
   \ '\v^\s*(if|for(each)?|while|switch)\s*\(.*\)\_s*\{'
au Filetype php let b:simplefold_nestable_end_expr =
   \ '\v^\s*\}'
au Filetype php let b:simplefold_prefix =
   \ '\v^\s*((#|//).*)?$' .
   \ '|\v^\s*(/\*\_.*\*/)?$' .
   \ '|^\s*$'

It works alright for files that contain classes, but doesn't work on files that just contain PHP and HTML. Adding

  '\v^\s*(if|for(each)?|while|switch)\s*\(.*\)\_s*\{'

to the alternatives in b:simplefold_expr makes it fold those files too, but since the top-level folding doesn't use any type of end_expr to find the closing curly braces, the folds are all off. I'd be interested to see what improvements others can make to these rules.

Dan 2006-05-13 (Sat) 03:15:26

Oops, that last line in b:simplefold_prefix is redundant. It could be better defined like this:

au Filetype php let b:simplefold_prefix =
   \ '\v^\s*((/\*\_.*\*/)|((#|//).*))?$'

mfp 2006-05-13 (Sat) 13:37:21

Thank you! This is getting into the next release.

mfp 2006-05-13 (Sat) 13:41:40

Almost forgot... could I have your full name to credit you properly? Dan(iel Berger)?

Dan 2006-05-13 (Sat) 23:30:44

Ah, sure. And nope, I'm not that Dan :) I'm Dan McCormack.

Note that I only tested these PHP rules on my own code (and thus, on my own coding style). They may very well miss some types of syntax that are allowed by the parser but I don't happen to use in my code. But it's a start, and anyone else can feel free to expand on them as necessary.

No Title - Dan (2006-05-13 (Sat) 00:21:00)

Maybe this is more of a general vim issue, but do we really have to type \f each time we want to 'detect' new folds, such as after defining a new method? I like your folding approach a lot, but having to type \f all the time does a lot to outweigh the advantages. I've been using fdm=marker for a year and was very happy with the way it immediately reflects changes in the file. Is there some way to configure vim to do the same with SimpleFold?

mfp 2006-05-13 (Sat) 13:36:14

I have an idea... stay tunned :)


Folding all borked on vim 64 - kaspar (2006-05-12 (Fri) 07:25:53)

I just tried out that script on vim64 and all I got was vim complaining that b:simplefold_marker_start didn't exist. Should that work on 6.4 ?

mfp 2006-05-12 (Fri) 08:15:13

OK, just tested it on 6.2 and 6.4, and reproduced the bug. It happens when ((filetype is off OR you're editing a file whose filetype is not ruby nor java) AND you didn't specify the file in the cmdline); e.g.

 vim foo.txt

works (so it'll fold on markers when you do <Leader>f --- there's no other expression for non-{Ruby-Java) files) but

 vim
 and then...   :e foo.txt

won't.

For the time being, the workarounds are:

  • use <Leader>f only with ruby files :)
  • open the file from the cmdline: vim whatever

I'm fixing this in a few hours when I get home.

mfp 2006-05-12 (Fri) 14:14:36

Should be fixed by now in 0.4.0b.

Justin Dossey 2006-05-16 (Tue) 12:37:48

I'm using this heavily, and there are indeed some minor bugs. 1. it's slow :) 2. When you move a folded method with a key sequence like dd10kp, the method is unfolded under the cursor; 3. When you start a new method above an older one, the older one's end is highlighted, and then when you type in the end for the new method, the older one is expanded.

I really appreciate the work you've done with ruby vim folding. Thank you!

Bruno Michel 2006-05-30 (Mardi) 17:09:14

Thanks for this very usefull plugin.

I have a minor bug report: line 251, the let definition can overwrite a previous declaration of g:SimpleFold_use_subfolds (in .vimrc for example). The patch: if !exists("g:SimpleFold_use_subfolds")

   let g:SimpleFold_use_subfolds = 1

endif