" 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 <F5> <Esc>:EnableFastPHPFolds<Cr>
"       map <F6> <Esc>:EnablePHPFolds<Cr>
"       map <F7> <Esc>:DisablePHPFolds<Cr>
"
"  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 <SID>EnableFastPHPFolds()
command! -nargs=* EnablePHPFolds call <SID>EnablePHPFolds(<f-args>)
command! DisablePHPFolds call <SID>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
