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. autoload/folds/python.vim 0 → 100644 +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() 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.
autoload/folds/python.vim 0 → 100644 +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()