Commit dbef0037 authored by Dom Sekotill's avatar Dom Sekotill
Browse files

Initial commit

parents
Loading
Loading
Loading
Loading

README.md

0 → 100644
+56 −0
Original line number Diff line number Diff line
Python-Fold
===========

Better Vim folding for python files.


Installing
----------

Use your preferred vim plugin manager:

* [dein](https://github.com/Shougo/dein.vim)
* [vim-plug](https://github.com/junegunn/vim-plug)
* [vundle](https://github.com/VundleVim/Vundle.vim)
* [pathogen](https://github.com/tpope/vim-pathogen)


Usage
-----

The plugin creates folds starting after 'class' and 'def' (including 'async 
def') lines until the last non-blank line before the indent drops to the 
same or a lower level than the 'class' or 'def' line itself.  The fold level 
depends on the nesting of the classes and functions.

Multiline docstrings are also folded.  Their level is ALWAYS below the 
deepest nested class or function (subject to restriction by the 
'foldnestmax' option).  This allows all multiline docstrings to be folded 
without folding anything else.  The easiest way to quickly find the right 
level for this is to type (in normal mode) `zRzm`; this (1) sets the level 
to the highest value in the buffer then (2) drops it down by one.

If classes or functions contain a docstring the first non-blank line of the 
docstring is shown as the fold text, otherwise "[No Docstring]" is shown. 
The fold text for multiline docstrings is always the first non-blank line.


Troubleshooting
---------------

To re-initialise the plugin for a buffer (if fold settings were messed with) 
run the following command while in the buffer:

```
call folds#python#Enable()
```


Known Problems
--------------

1. The entire buffer is scanned when changes are made, which is slow.
2. On starting vim, fold text is not shown.
3. Code (or what looks like code) in multiline strings will be folded.
4. Class/function recognition is simplistic; for instance a line containing 
   just `def:` will cause the subsequent lines to be treated as a function.
+330 −0
Original line number Diff line number Diff line
let s:root = 0
let s:expect = 0
let s:max_depth = 0


function! folds#python#Enable()
	setl foldexpr=folds#python#FoldExpr()
	setl foldtext=folds#python#FoldText()
	setl foldmethod=expr
	au InsertLeave <buffer> :if &l:foldenable | normal zxzv
	au BufWinEnter <buffer> :if &l:foldenable | normal zxzRzm
endfunction

function! folds#python#FoldExpr()
	if v:lnum != s:expect | call s:parsebuffer() | endif
	let s:expect = v:lnum + 1
	let fold = s:root.get_at(v:lnum)
	if fold.type == 'doc'
		" multi-line doc-strings go on their own below the deepest level
		return (fold.start != fold.end) ? s:max_depth + 1 : fold.parent.depth
	else
		return fold.depth
	endif
endfunction

function! folds#python#FoldText()
	if s:root is 0 | call s:parsebuffer() | endif
	let cur = s:root.get_at(v:foldstart)
	let indent = indent(cur.foldstart)
	let padlen = (&l:textwidth ? (&l:textwidth) : 80)
	let padlen -= (len(cur.msg) + indent)
	return repeat(' ', indent) . cur.msg . repeat(' ', padlen)
endfunction

function! s:newFold(type, indent, ...)
	let a:msg = get(a:, 1, '')

	let fold = {}
	let fold.type = a:type
	let fold.indent = a:indent
	let fold.msg = a:msg

	let fold.parent = 0
	let fold.children = []
	let fold.depth = 0

	let fold.start = 0
	let fold.end = line('$')
	let fold.foldstart = 0

	function fold.startline(start, ...)
		" Set start line and parent, return self for chaining
		"
		" When run in a 'foldexpr' context no arguments should be passed, 
		" otherwise a:start is required.

		if self.parent
			throw "RuntimeError: Fold.startline: " .
			    \ "already called on a fold"
		endif

		let a:parent = get(a:, 1, 0)
		if a:parent is 0
			let a:parent = s:root.get_at(a:start)
		endif

		let self.start = a:start
		let self.foldstart = a:start
		let self.end = a:parent.end
		if exists('a:parent.insert_child')
			call a:parent.insert_child(self)
		endif

		return self
	endfunction

	function fold.endline(end)
		" Set end line, check validity of siblings and return parent
		"
		" When run in a 'foldexp' context no arguments should be passed,
		" otherwise a:end is required.
		"
		" When run outside of a 'foldexpr' context some of the folds in 
		" `self.parent.children` (ie. self's siblings) may be reassigned as 
		" children of self.

		if self.parent is 0
			throw "ValueError: Fold.endline: " .
			    \ "Fold.startline has not been called"
		elseif self.end < a:end
			throw "ValueError: Fold.endline: " .
			    \ "cannot expand a fold"
		elseif self.start > a:end
			throw "ValueError: Fold.endline: " .
			    \ "end line is before the start value"
		elseif exists('self.children[-1]') && self.children[-1].end > a:end
			throw "ValueError: Fold.endline: " .
			    \ "end line is before the end of the last child"
		elseif self.parent.end < a:end
			throw "ValueError: Fold.endline: " .
			    \ "end line is after parent's end"
		endif

		let self.end = a:end
		for child in self.parent.get_between(self.start, self.end)
			if child is self | continue | endif
			call self.parent.remove_child(child)
			call self.insert_child(child)
		endfor

		return self.parent
	endfunction

	function fold.insert_child(child)
		if a:child.start < self.foldstart
			throw "ValueError: Fold.insert_child: " .
			    \ "child starts before the start of parent's fold"
		elseif a:child.end > self.end
			throw "ValueError: Fold.insert_child: " .
			    \ "child ends after the end of parent"
		endif

		let a:child.depth = self.depth + 1

		let idx = 0
		for sibling in self.children
			if sibling.start < a:child.start && sibling.end >= a:child.end
				return sibling.insert_child(a:child)
			elseif a:child.start < sibling.start && a:child.end >= sibling.end
				call remove(self.children, idx)
				call insert(self.children, a:child, idx)
				let sibling.parent = 0
				let a:child.parent = self
				return a:child.insert_child(sibling)
			elseif sibling.start > a:child.end
				call insert(self.children, a:child, idx)
				let a:child.parent = self
				return
			elseif sibling.end < a:child.start
				let idx += 1
			else
				throw "ValueError: Fold.insert_child: " .
				    \ "child overlaps with a child already joined to parent"
			endif
		endfor
		call add(self.children, a:child)
		let a:child.parent = self
	endfunction

	function fold.remove_child(child, ...)
		let a:remove_tree = get(a:, 1, 0)
		let idx = 0
		for child in self.children
			if child is a:child
				let child.parent = 0
				let child.depth = 0
				call remove(self.children, idx)
				if !a:remove_tree
					for grandchild in child.children
						self.insert_child(grandchild)
					endfor
					let child.children = []
				endif
				return child
			endif
			let idx += 1
		endfor
		throw "NotFoundError: Fold.remove_child: " .
		    \ "child was not found in parent"
	endfunction

	function fold.abort()
		let parent = self.parent
		call parent.remove_child(self)
		return parent
	endfunction

	function fold.get_at(line)
		if  self.start > a:line || self.end < a:line
			throw "NotFoundError: Fold.get_at: " .
			    \ "line number is outside of fold's boundaries"
		endif
		if a:line < self.foldstart
			return self.parent
		endif
		for child in self.children
			if child.foldstart <= a:line && a:line <= child.end
				return child.get_at(a:line)
			endif
		endfor
		return self
	endfunction

	function fold.get_between(start, end)
		let found = []
		for child in self.children
			if child.start >= a:start && child.end <= a:end
				call add(found, child)
			elseif child.start >= a:end
				break
			endif
		endfor
		return found
	endfunction

	return (fold)
endfunction

function! s:parsebuffer()
	let s:root = s:newFold('file', 0)
	let s:root.has_docstring = 0
	let cur_fold = s:root
	let s:max_depth = 0
	let last_lnum = 0
	for lnum in range(1, line('$'))
		let line = getline(lnum)
		if empty(line) | continue | endif
		let cur_fold = s:parseline(lnum, line, cur_fold, last_lnum)
		let last_lnum = lnum
		let s:max_depth = max([s:max_depth, cur_fold.depth])
	endfor
endfunction

function! s:parseline(lnum, line, cur_fold, last_lnum)
	let cur_fold = a:cur_fold

	if cur_fold.type == 'doc'
		if a:line =~ cur_fold.qtype . '\s*$'
			let cur_fold = cur_fold.endline(a:lnum)
		endif
		return cur_fold
	endif

	let match = matchlist(a:line, '\v^(\s*)(\@|%(async\s+)?def>|class>)?(\_.*)')
	let indlvl = len(match[1])

	while !(cur_fold.parent is 0) && indlvl <= cur_fold.indent
		if s:is_incomplete_def(cur_fold)
			" Something that looked like a class/def is not
			let cur_fold = cur_fold.abort()
			continue
		endif
		let cur_fold = cur_fold.endline(a:last_lnum)
	endwhile

	if !empty(match[2])
		if cur_fold.type == '@'
			let cur_fold.type = match[2]
			let cur_fold.msg = match[3]
		else
			let cur_fold = s:newFold(match[2], indlvl, '[No Docstring]').startline(a:lnum)
			let cur_fold.has_docstring = 0
		endif

	elseif !get(cur_fold, 'has_docstring', 1) && match[3] =~ '\v^(["''`])\1{2}'
		if s:is_incomplete_def(cur_fold)
			" something funny here, abort
			return cur_fold.abort()
		endif
		let cur_fold.has_docstring = 1
		let docmatch = matchlist(match[3], '\v^((["''`])\2{2})(.{-})(\1?)\s*$')
		if !empty(docmatch[3])
			let msg = docmatch[3]
		else
			let msg = getline(nextnonblank(a:lnum + 1))
			let msg = substitute(msg, '^\s*', '', '')
		endif
		let cur_fold = s:newFold('doc', indlvl, msg).startline(a:lnum, cur_fold)
		let cur_fold.qtype = docmatch[1]
		if !empty(docmatch[4])
			let cur_fold = cur_fold.endline(a:lnum)
		endif

	endif

	if s:is_incomplete_def(cur_fold) && match[3] =~ ':\s*$'
		let cur_fold.foldstart = a:lnum + 1
	endif

	return cur_fold
endfunction

function! s:is_incomplete_def(fold)
	return ( (a:fold.foldstart == a:fold.start && a:fold.type =~# 'class\|def')
		\ || (a:fold.type == '@') )
endfunction

function! folds#python#Debug()
	if exists('b:target_buf')
		let target_buf = b:target_buf
		let win = bufwinnr(b:target_buf)
		if win == -1
			exec "noau bottomright vsplit " . b:target_buf
		else
			exec win . "wincmd w"
		endif
	else
		let target_buf = bufnr('%')
	endif
		
	if !exists('b:debug_buf') || !bufexists(b:debug_buf)
		noau leftabove vnew
		setl buftype=nofile
		setl nobuflisted
		let b:target_buf = target_buf
		let debug_buf = bufnr('%')
	else
		let debug_buf = b:debug_buf
		let win = bufwinnr(b:debug_buf)
		if win == -1
			exec "noau leftabove vsplit " . b:debug_buf
		else
			exec win . "wincmd w"
		endif
	endif

	1,$d

	exec "buf " . target_buf
	let target_len = line('$')
	let b:debug_buf = debug_buf
	setl foldexpr=folds#python#FoldExpr()
	setl foldenable foldmethod=expr

	exec "buf " . debug_buf
	for lnum in range(1, target_len)
		let cur = s:root.get_at(lnum)
		call append(lnum - 1, "[" . cur.type . "/" . cur.start . "/" . cur.foldstart . "/" . cur.end . "] " . cur.depth . " " . cur.msg)
	endfor
endfunction

plugin/python-fold.vim

0 → 100644
+1 −0
Original line number Diff line number Diff line
au FileType python call folds#python#Enable()