" Plugin for automatic folding of PHP functions (also folds related PHPdoc) " " Maintainer: Ray Burgemeestre " Last Change: 2013 Aug 26 " " USAGE " If you enabled the script in your after/ftplugin directory (see install) " then it will be executed after you open a .php file. " " After e.g. adding/moving functions, you can re-execute the script by using " the following key mappings: " " F5 - To fold functions, classes, and other stuff with PHPdoc (depending " on your configuration). " F6 - To do the same with more extensive bracket checking (might work " better if your folds are messed up due to misinterpreted brackets). " F7 - To remove all folds. " " INSTALL " 1. Put phpfolding.vim in your plugin directory (~/.vim/ftplugin/php/) " " make sure you have "filetype plugin on" in your .vimrc! " " 2. You might want to add the following keyboard mappings to your .vimrc: " " map :EnableFastPHPFolds " map :EnablePHPFolds " map :DisablePHPFolds " " 3. You can disable auto folding in your .vimrc with: " " let g:DisableAutoPHPFolding = 1 " " By default EnableFastPHPFolds is called. Do these mess up your folds, " you can try to replace EnableFastPHPFolds by EnablePHPFolds. You can " change this in function s:CheckAutocmdEnablePHPFold. " " NOTE " It may be that you need to load the plugin from your .vimrc manually, in " case it doesn't work: " " let php_folding=0 " (if you can't use the after directory in step 3) " source ~/path/to/phpfolding.vim " (if you're not using the default plugin directory) " " MORE INFORMATION " - In PHPCustomFolds() you can i.e. comment the PHPFoldPureBlock('class', ...) " call to have the script not fold classes. You can also change the second " parameter passed to that function call, to have it or not have it fold " PhpDoc comments. All other folding you can turn on/off in this function. " - You can tweak the foldtext to your liking in the function PHPFoldText(). " - You can set some preferences and default settings a few lines below " at the "Script configuration" part. " " This script is tested with Vim version >= 6.3 on windows and linux. let s:save_cpo = &cpo set cpo&vim " Avoid reloading {{{1 if exists('loaded_phpfolding') " ftplugin section if !get(g:, 'DisableAutoPHPFolding', 0) call s:EnableFastPHPFolds() endif let &cpo = s:save_cpo unlet s:save_cpo finish endif let loaded_phpfolding = 1 " }}} " .vimrc variable to disable autofolding for php files {{{1 if !exists("g:DisableAutoPHPFolding") let g:DisableAutoPHPFolding = 0 endif let g:DisablePHPFoldingClass = get(g:, 'DisablePHPFoldingClass', 1) " }}} command! EnableFastPHPFolds call EnableFastPHPFolds() command! -nargs=* EnablePHPFolds call EnablePHPFolds() command! DisablePHPFolds call DisablePHPFolds() " {{{ Script configuration " Display the following after the foldtext if a fold contains phpdoc let g:phpDocIncludedPostfix = '**' let g:phpDocBlockIncludedPostfix = '**#@+' " Default values " .. search this # of empty lines for PhpDoc comments let g:searchPhpDocLineCount = 1 " .. search this # of empty lines that 'trail' the foldmatch let g:searchEmptyLinesPostfixing = 1 " }}} " {{{ Script constants let s:synIDattr_exists = exists('*synIDattr') let s:TRUE = 1 let s:FALSE = 0 let s:MODE_CREATE_FOLDS = 1 let s:MODE_REMEMBER_FOLD_SETTINGS = 2 let s:FOLD_WITH_PHPDOC = 1 let s:FOLD_WITHOUT_PHPDOC = 2 let s:SEARCH_PAIR_START_FIRST = 1 let s:SEARCH_PAIR_IMMEDIATELY = 2 " }}} function! s:EnableFastPHPFolds() " {{{ call s:EnablePHPFolds(s:FALSE) endfunction " }}} function! s:EnablePHPFolds(...) " {{{ let s:extensiveBracketChecking = s:TRUE " Check function arguments if a:0 == 1 let s:extensiveBracketChecking = a:1 endif " Remember cursor information if possible let s:savedCursor = line(".") " Initialize variables setlocal foldmethod=manual setlocal foldtext=PHPFoldText() let s:openFoldListItems = 0 let s:fileLineCount = line('$') let s:searchPhpDocLineCount = g:searchPhpDocLineCount let s:searchEmptyLinesPostfixing = g:searchEmptyLinesPostfixing " Move to end of file exec s:fileLineCount " First pass: Look for Folds, remember opened folds let s:foldingMode = s:MODE_REMEMBER_FOLD_SETTINGS call s:PHPCustomFolds() " Second pass: Recreate Folds, restore previously opened let s:foldingMode = s:MODE_CREATE_FOLDS " .. Remove all folds first normal! zE let s:foldsCreated = 0 call s:PHPCustomFolds() " .. Fold all normal! zM " Restore previously opened folds let currentItem = 0 while currentItem < s:openFoldListItems exec s:foldsOpenedList{currentItem} normal! zo let currentItem = currentItem + 1 endwhile :redraw echo s:foldsCreated . " fold(s) created" " Restore cursor exec s:savedCursor endfunction " }}} function! s:DisablePHPFolds() " {{{ "setlocal foldmethod=manual setlocal foldtext= normal! zE echo "php fold(s) deleted" endfunction " }}} function! s:PHPCustomFolds() " {{{ " NOTE: The two last parameters for functions PHPFoldProperties() and " PHPFoldPureBlock() overwrite: 'g:searchPhpDocLineCount' and " 'g:searchEmptyLinesPostfixing'.. " Fold function with PhpDoc (function foo() {}) call s:PHPFoldPureBlock('function', s:FOLD_WITH_PHPDOC) " Fold class properties with PhpDoc (var $foo = NULL;) call s:PHPFoldProperties('^\s*\(\(private\)\|\(public\)\|\(protected\)\|\(var\)\)\s\(static\s\)*\$', ";", s:FOLD_WITH_PHPDOC, 1, 1) if !g:DisablePHPFoldingClass " Fold class without PhpDoc (class foo {}) call s:PHPFoldPureBlock('^\s*\(abstract\s*\)\?class', s:FOLD_WITH_PHPDOC) endif " Fold define()'s with their PhpDoc call s:PHPFoldProperties('^\s*define\s*(', ";", s:FOLD_WITH_PHPDOC) " Fold includes with their PhpDoc call s:PHPFoldProperties('^\s*require\s*', ";", s:FOLD_WITH_PHPDOC) call s:PHPFoldProperties('^\s*include\s*', ";", s:FOLD_WITH_PHPDOC) " Fold GLOBAL Arrays with their PhpDoc (some PEAR packages use these) call s:PHPFoldProperties('^\s*\$GLOBALS.*array\s*(', ";", s:FOLD_WITH_PHPDOC) " Fold marker style comments ({{{ foo }}}) call s:PHPFoldMarkers('{{{', '}}}') " Fold PhpDoc "DocBlock" templates (#@+ foo #@-) call s:PHPFoldMarkers('#@+', '#@-') endfunction " }}} function! s:PHPFoldPureBlock(startPattern, ...) " {{{ let s:searchPhpDocLineCount = g:searchPhpDocLineCount let s:searchEmptyLinesPostfixing = g:searchEmptyLinesPostfixing let s:currentPhpDocMode = s:FOLD_WITH_PHPDOC if a:0 >= 1 " Do we also put the PHP doc part in the fold? let s:currentPhpDocMode = a:1 endif if a:0 >= 2 " How far do we want to look for PhpDoc comments? let s:searchPhpDocLineCount = a:2 endif if a:0 == 3 " How greedy are we on postfixing empty lines? let s:searchEmptyLinesPostfixing = a:3 endif " Move to file end exec s:fileLineCount " Loop through file, searching for folds while 1 let s:lineStart = s:FindPureBlockStart(a:startPattern) if s:lineStart != 0 let s:lineStart = s:FindOptionalPHPDocComment() let s:lineStop = s:FindPureBlockEnd('{', '}', s:SEARCH_PAIR_START_FIRST) " Stop on Error if s:lineStop == 0 break endif " Do something with the potential fold based on the Mode we're in call s:HandleFold() else break endif " Goto fold start (remember we're searching upwards) exec s:lineStart endwhile if s:foldingMode != s:MODE_REMEMBER_FOLD_SETTINGS " Remove created folds normal! zR endif endfunction " }}} function! s:PHPFoldMarkers(startPattern, endPattern, ...) " {{{ let s:currentPhpDocMode = s:FOLD_WITHOUT_PHPDOC " Move to file end exec s:fileLineCount " Loop through file, searching for folds while 1 let s:lineStart = s:FindPatternStart(a:startPattern) if s:lineStart != 0 let s:lineStart = s:FindOptionalPHPDocComment() " The fourth parameter is for disabling the search for trailing " empty lines.. let s:lineStop = s:FindPureBlockEnd(a:startPattern, a:endPattern, \ s:SEARCH_PAIR_IMMEDIATELY, s:FALSE) " Stop on Error if s:lineStop == 0 break endif " Do something with the potential fold based on the Mode we're in call s:HandleFold() else break endif " Goto fold start (remember we're searching upwards) exec s:lineStart endwhile if s:foldingMode != s:MODE_REMEMBER_FOLD_SETTINGS " Remove created folds normal! zR endif endfunction " }}} function! s:PHPFoldProperties(startPattern, endPattern, ...) " {{{ let s:searchPhpDocLineCount = g:searchPhpDocLineCount let s:searchEmptyLinesPostfixing = g:searchEmptyLinesPostfixing let s:currentPhpDocMode = s:FOLD_WITH_PHPDOC if a:0 >= 1 " Do we also put the PHP doc part in the fold? let s:currentPhpDocMode = a:1 endif if a:0 >= 2 " How far do we want to look for PhpDoc comments? let s:searchPhpDocLineCount = a:2 endif if a:0 == 3 " How greedy are we on postfixing empty lines? let s:searchEmptyLinesPostfixing = a:3 endif " Move to end of file exec s:fileLineCount " Loop through file, searching for folds while 1 let s:lineStart = s:FindPatternStart(a:startPattern) if s:lineStart != 0 let s:lineStart = s:FindOptionalPHPDocComment() let s:lineStop = s:FindPatternEnd(a:endPattern) " Stop on Error if s:lineStop == 0 break endif " Do something with the potential fold based on the Mode we're in call s:HandleFold() else break endif " Goto fold start (remember we're searching upwards) exec s:lineStart endwhile if s:foldingMode != s:MODE_REMEMBER_FOLD_SETTINGS " Remove created folds normal! zR endif endfunction " }}} function! s:HandleFold() " {{{ if s:foldingMode == s:MODE_REMEMBER_FOLD_SETTINGS " If we are in an actual fold.., if foldlevel(s:lineStart) != 0 " .. and it is not closed.., if foldclosed(s:lineStart) == -1 " .. and it is more then one lines " (it has to be or it will be open by default) if s:lineStop - s:lineStart >= 1 " Remember it as an open fold let s:foldsOpenedList{s:openFoldListItems} = s:lineStart let s:openFoldListItems = s:openFoldListItems + 1 endif endif endif " If the cursor is inside the fold, it needs to be opened if s:lineStart <= s:savedCursor && s:lineStop >= s:savedCursor " Remember it as an open fold let s:foldsOpenedList{s:openFoldListItems} = s:lineStart let s:openFoldListItems = s:openFoldListItems + 1 endif elseif s:foldingMode == s:MODE_CREATE_FOLDS " Correct lineStop if needed (the script might have mistaken lines " beyond the file's scope for trailing empty lines) if s:lineStop > s:fileLineCount let s:lineStop = s:fileLineCount endif " Create the actual fold! exec s:lineStart . "," . s:lineStop . "fold" let s:foldsCreated = s:foldsCreated + 1 endif endfunction " }}} function! s:FindPureBlockStart(startPattern) " {{{ " When the startPattern is 'function', this following search will match: " " function foo($bar) { function foo($bar) " { " " function foo($bar) function foo($bar1, " .. { $bar2) " { " "return search(a:startPattern . '.*\%[\n].*{', 'W') "return search(a:startPattern . '.*\%[\n].*\%[\n].*{', 'bW') " This function can match the line its on *again* if the cursor was " restored.. hence we search twice if needed.. let currentLine = line('.') let line = search(a:startPattern . '.*\(\%[\n].*\)\{,10\}{', 'bW') if currentLine == line let line = search(a:startPattern . '.*\(\%[\n].*\)\{,10\}{', 'bW') endif return line endfunction " }}} function! s:FindPatternStart(startPattern) " {{{ " This function can match the line its on *again* if the cursor was " restored.. hence we search twice if needed.. let currentLine = line('.') let line = search(a:startPattern, 'bW') if currentLine == line let line = search(a:startPattern, 'bW') endif return line endfunction " }}} function! s:FindOptionalPHPDocComment() " {{{ " Is searching for PHPDoc disabled? if s:currentPhpDocMode == s:FOLD_WITHOUT_PHPDOC " .. Return the original Fold's start return s:lineStart endif " Skipover 'empty' lines in search for PhpDoc let s:counter = 0 let s:currentLine = s:lineStart - 1 while s:counter < s:searchPhpDocLineCount let line = getline(s:currentLine) if (matchstr(line, '^\s*$') == line) let s:currentLine = s:currentLine - 1 endif let s:counter = s:counter + 1 endwhile " Is there a closing C style */ on the above line? let checkLine = s:currentLine if strridx(getline(checkLine), "\*\/") != -1 " Then search for the matching C style /* opener while 1 if strridx(getline(checkLine), "\/\*") != -1 " Only continue adjusting the Fold's start if it really is PHPdoc.. " (which is characterized by a double asterisk, like /**) if strridx(getline(checkLine), "\/\*\*") != -1 " Also only continue adjusting if the PHPdoc opener does " not contain a '/**#@+'. Those type of comments are " supposed to match with a #@- .. if strridx(getline(checkLine), '#@+') == -1 " .. Return this as the Fold's start return checkLine else break endif else break endif endif let checkLine = checkLine - 1 endwhile endif " .. Return the original Fold's start return s:lineStart endfunction " }}} function! s:FindPureBlockEnd(startPair, endPair, searchStartPairFirst, ...) " {{{ " Place Cursor on the opening pair/brace? if a:searchStartPairFirst == s:SEARCH_PAIR_START_FIRST let line = search(a:startPair, 'W') endif " Search for the entangled closing brace " call cursor(line, 1) " set the cursor to the start of the lnum line if s:extensiveBracketChecking == s:TRUE let line = searchpair(a:startPair, a:startPair, a:endPair, 'W', 'SkipMatch()') else let line = searchpair(a:startPair, a:startPair, a:endPair, 'W') endif if line == 0 let line = search(a:endPair, 'W') endif if line == 0 " Return error return 0 endif " If the fold exceeds more than one line, and searching for empty lines is " not disabled.. let foldExceedsOneLine = line - s:lineStart >= 1 if a:0 == 1 let emptyLinesNotDisabled = a:1 else let emptyLinesNotDisabled = s:TRUE endif if foldExceedsOneLine && emptyLinesNotDisabled " Then be greedy with extra 'trailing' empty line(s) let s:counter = 0 while s:counter < s:searchEmptyLinesPostfixing let linestr = getline(line + 1) if (matchstr(linestr, '^\s*$') == linestr) let line = line + 1 endif let s:counter = s:counter + 1 endwhile endif return line endfunction " }}} function! s:FindPatternEnd(endPattern) " {{{ let line = search(a:endPattern, 'W') " If the fold exceeds more than one line if line - s:lineStart >= 1 " Then be greedy with extra 'trailing' empty line(s) let s:counter = 0 while s:counter < s:searchEmptyLinesPostfixing let linestr = getline(line + 1) if (matchstr(linestr, '^\s*$') == linestr) let line = line + 1 endif let s:counter = s:counter + 1 endwhile endif return line endfunction " }}} function! PHPFoldText() " {{{ let currentLine = v:foldstart let lines = (v:foldend - v:foldstart + 1) let lineString = getline(currentLine) " See if we folded a marker if strridx(lineString, "{{{") != -1 " }}} " Is there text after the fold opener? if (matchstr(lineString, '^.*{{{..*$') == lineString) " }}} " Then only show that text let lineString = substitute(lineString, '^.*{{{', '', 'g') " }}} " There is text before the fold opener else " Try to strip away the remainder let lineString = substitute(lineString, '\s*{{{.*$', '', 'g') " }}} endif " See if we folded a DocBlock elseif strridx(lineString, '#@+') != -1 " Is there text after the #@+ piece? if (matchstr(lineString, '^.*#@+..*$') == lineString) " Then show that text let lineString = substitute(lineString, '^.*#@+', '', 'g') . ' ' . g:phpDocBlockIncludedPostfix " There is nothing? else " Use the next line.. let lineString = getline(currentLine + 1) . ' ' . g:phpDocBlockIncludedPostfix endif " See if we folded an API comment block elseif strridx(lineString, "\/\*\*") != -1 " (I can't get search() or searchpair() to work.., therefore the " following loop) let s:state = 0 while currentLine < v:foldend let line = getline(currentLine) if s:state == 0 && strridx(line, "\*\/") != -1 " Found the end, now we need to find the first not-empty line let s:state = 1 elseif s:state == 1 && (matchstr(line, '^\s*$') != line) " Found the line to display in fold! break endif let currentLine = currentLine + 1 endwhile let lineString = getline(currentLine) endif " Some common replaces... " if currentLine != v:foldend let lineString = substitute(lineString, '/\*\|\*/\d\=', '', 'g') let lineString = substitute(lineString, '^\s*\*\?\s*', '', 'g') let lineString = substitute(lineString, '{$', '', 'g') let lineString = substitute(lineString, '($', '(..);', 'g') " endif " Emulates printf("%3d", lines).. if lines < 10 let lines = " " . lines elseif lines < 100 let lines = " " . lines endif " Append an (a) if there is PhpDoc in the fold (a for API) if currentLine != v:foldstart let lineString = lineString . " " . g:phpDocIncludedPostfix . " " endif " Return the foldtext return "+--".lines." lines: " . lineString endfunction " }}} function! SkipMatch() " {{{ " This function is modified from a PHP indent file by John Wellesz " found here: http://www.vim.org/scripts/script.php?script_id=1120 if (!s:synIDattr_exists) return 0 endif let synname = synIDattr(synID(line("."), col("."), 0), "name") if synname == "phpParent" || synname == "javaScriptBraces" || synname == "phpComment" return 0 else return 1 endif endfun " }}} " " Check filetype == php before automatically creating (fast) folds {{{1 " function! s:CheckAutocmdEnablePHPFold() " if &filetype == "php" && ! g:DisableAutoPHPFolding " call s:EnableFastPHPFolds() " endif " endfunction " " }}} " " Call CheckAutocmdEnablePHPFold on BufReadPost {{{1 " augroup SetPhpFolds " au! " au BufReadPost * call s:CheckAutocmdEnablePHPFold() " augroup END " " }}} " ftplugin section if !get(g:, 'DisableAutoPHPFolding', 0) call s:EnableFastPHPFolds() endif let &cpo = s:save_cpo unlet s:save_cpo " vim:ft=vim:foldmethod=marker:nowrap:tabstop=4:shiftwidth=4