Skip to content

Commit

Permalink
Implement full caching
Browse files Browse the repository at this point in the history
Improves performance by several magnitudes

Fixes #45
  • Loading branch information
nfnty committed Feb 12, 2017
1 parent e6ca31b commit 2dfeb35
Showing 1 changed file with 143 additions and 178 deletions.
321 changes: 143 additions & 178 deletions ftplugin/python/SimpylFold.vim
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ if exists('b:loaded_SimpylFold')
endif
let b:loaded_SimpylFold = 1

let s:blank_regex = '\v^\s*(\#.*)?$'
if &filetype ==# 'pyrex' || &filetype ==# 'cython'
let b:def_regex = '\v^\s*%(%(class|%(async\s+)?def|cdef|cpdef|ctypedef)\s+\w+)|cdef\s*:\s*'
let b:SimpylFold_def_regex = '\v^\s*%(%(class|%(async\s+)?def|cdef|cpdef|ctypedef)\s+\w+)|cdef\s*:'
else
let b:def_regex = '\v^\s*%(class|%(async\s+)?def)\s+\w+|if\s*__name__\s*\=\=\s*%("__main__"|''__main__'')\s*:\s*'
let b:SimpylFold_def_regex = '\v^\s*%(class|%(async\s+)?def)\s+\w+|if\s+__name__\s*\=\=\s*%("__main__"|''__main__'')\s*:'
endif
let s:non_blank_regex = '^\s*[^[:space:]#]'
let s:multiline_def_end_regex = '):$'
let s:multiline_def_end_solo_regex = '^\s*):$'
let s:docstring_start_regex = '^\s*[rR]\?\("""\|''''''\)\%(.*\1\s*$\)\@!'
let s:docstring_end_single_regex = '''''''\s*$'
let s:docstring_end_double_regex = '"""\s*$'
Expand All @@ -18,230 +19,194 @@ let s:import_cont_regex = 'from.*\((\)[^)]*$\|.*\(\\\)$'
let s:import_end_paren_regex = ')\s*$'
let s:import_end_esc_regex = '[^\\]$'

if exists('g:SimpylFold_docstring_level')
let s:docstring_level = g:SimpylFold_docstring_level
else
let s:docstring_level = -1
end

if exists('g:SimpylFold_import_level')
let s:import_level = g:SimpylFold_import_level
else
let s:import_level = -1
end

let s:fold_docstrings = !exists('g:SimpylFold_fold_docstring') || g:SimpylFold_fold_docstring
command! -bang SimpylFoldDocstrings let s:fold_docstrings = <bang>1
command! -bang SimpylFoldDocstrings let s:fold_docstrings = <bang>1 | call SimpylFoldRecache()
let s:fold_imports = !exists('g:SimpylFold_fold_import') || g:SimpylFold_fold_import
command! -bang SimpylFoldImports let s:fold_imports = <bang>1

function! s:GetLine(lnum)
let line = getline(a:lnum)
if line =~# '^\s*):\s*$'
let line = ' ' . line
endif
return line
endfunction
command! -bang SimpylFoldImports let s:fold_imports = <bang>1 | call SimpylFoldRecache()

function! s:GetIndent(lnum)
let ind = indent(a:lnum)
let line = getline(a:lnum)
if line =~# '^\s*):\s*$'
let ind = 4 + ind
function! s:indent(line)
let ind = matchend(a:line, '^ *') / &tabstop
" Fix indent for solo def multiline endings
if a:line =~# s:multiline_def_end_solo_regex
return ind + 1
endif
return ind
endfunction

" Returns the next non-blank line, checking for our definition of blank using
" the s:blank_regex variable described above.
function! s:NextNonBlankOrCommentLine(lnum)
let nnb = a:lnum + 1
while nnb > 0
let nnb = nextnonblank(nnb)
if nnb == 0 || s:GetLine(nnb) !~# s:blank_regex
return nnb
endif

let nnb += 1
endwhile
" this return statement should never be reached, since nextnonblank()
" should never return a negative number. It returns 0 when it reaches EOF.
return -2
endfunction

" Determine the number of containing class or function definitions for the
" given line
function! s:NumContainingDefs(lnum)
" Recall memoized result if it exists in the cache
if has_key(b:cache_NumContainingDefs, a:lnum)
return b:cache_NumContainingDefs[a:lnum]
" given line.
" This function requires that `lnum` is >= previous `lnum`s.
function! s:defs(cache, lines, non_blanks, lnum)
if has_key(a:cache[a:lnum], 'defs')
return a:cache[a:lnum]['defs']
endif

let this_ind = s:GetIndent(a:lnum)

if this_ind == 0
" Indent level
let ind = s:indent(a:lines[a:lnum])
let a:cache[a:lnum]['indent'] = ind " Cache for use in the loop
if ind == 0
let a:cache[a:lnum]['defs'] = 0
return 0
endif

" Walk backwards to the previous non-blank line with a lower indent level
" than this line
let i = a:lnum - 1
while 1
if s:GetLine(i) !~# s:blank_regex
let i_ind = s:GetIndent(i)
if i_ind < this_ind
let ncd = s:NumContainingDefs(i) + (s:GetLine(i) =~# b:def_regex)
break
elseif i_ind == this_ind && has_key(b:cache_NumContainingDefs, i)
let ncd = b:cache_NumContainingDefs[i]
break
endif
endif

let i -= 1

" If we hit the beginning of the buffer before finding a line with a
" lower indent level, there must be no definitions containing this
" line. This explicit check is required to prevent infinite looping in
" the syntactically invalid pathological case in which the first line
" or lines has an indent level greater than 0.
if i <= 1
let ncd = s:GetLine(1) =~# b:def_regex
break
" Walk backwards to find the previous non-blank line with
" a lower indent level than this line
let non_blanks_prev = a:non_blanks[:index(a:non_blanks, a:lnum) - 1]
for lnum_prev in reverse(copy(non_blanks_prev))
if a:cache[lnum_prev]['indent'] < ind
let defs = s:defs(a:cache, a:lines, non_blanks_prev, lnum_prev) +
\ a:cache[lnum_prev]['is_def']
let a:cache[a:lnum]['defs'] = defs
return defs
endif
endfor

endwhile

" Memoize the return value to avoid duplication of effort on subsequent
" lines
let b:cache_NumContainingDefs[a:lnum] = ncd

return ncd

let a:cache[a:lnum]['defs'] = 0
return 0
endfunction

" Construct a foldexpr value
function! s:FoldExpr(lnum, foldlevel)
" If the very next line starts a definition with the same fold level as
" this one, explicitly indicate that a fold ends here
if s:GetLine(a:lnum + 1) =~# b:def_regex && SimpylFold(a:lnum + 1) == a:foldlevel
return '<' . a:foldlevel
" Construct a foldexpr value and cache it
function! s:foldexpr(cache_lnum, foldlevel, is_beginning)
if a:is_beginning
let a:cache_lnum['foldexpr'] = '>' . a:foldlevel
else
return a:foldlevel
let a:cache_lnum['foldexpr'] = a:foldlevel
endif
endfunction

" Compute fold level for Python code
function! SimpylFold(lnum)
" If we are starting a new sweep of the buffer (i.e. the current line
" being folded comes before the previous line that was folded), initialize
" the cache of results of calls to `s:NumContainingDefs`
if !exists('b:last_folded_line') || b:last_folded_line > a:lnum
let b:cache_NumContainingDefs = {}
let b:in_docstring = 0
let b:in_import = 0
endif
let b:last_folded_line = a:lnum
let line = s:GetLine(a:lnum)

" If this line is blank, its fold level is equal to the minimum of its
" neighbors' fold levels, but if the next line begins a definition, then
" this line should fold at one level below the next
if line =~# s:blank_regex
let next_line = s:NextNonBlankOrCommentLine(a:lnum)
if next_line == 0
return 0
elseif s:GetLine(next_line) =~# b:def_regex
return SimpylFold(next_line) - 1
function! s:cache()
let cache = [{}] " With padding for lnum offset
let lines = getbufline(bufnr('%'), 1, '$')
let lnum_last = len(lines)
call insert(lines, '') " Padding for lnum offset

" Cache everything generic that needs to be used later
let non_blanks = []
for lnum in range(1, lnum_last)
let line = lines[lnum]
if line =~# s:non_blank_regex
call add(non_blanks, lnum)
call add(cache, {'blank': 0, 'is_def': line =~# b:SimpylFold_def_regex})
else
return -1
call add(cache, {'blank': 1, 'is_def': 0, 'foldexpr': -1})
endif
endif

if b:in_docstring
if line =~# b:docstring_end_regex
let b:in_docstring = 0
endfor

" Cache non-blanks
let in_docstring = 0
let in_import = 0
for lnum in non_blanks
let line = lines[lnum]

" Docstrings
if s:fold_docstrings
if in_docstring
if line =~# docstring_end_regex
let in_docstring = 0
endif

call s:foldexpr(cache[lnum], s:defs(cache, lines, non_blanks, lnum) + 1, 0)
continue
else
let lnum_prev = lnum - 1
if !cache[lnum_prev]['blank']
let docstring_match = matchlist(line, s:docstring_start_regex)
if !empty(docstring_match) &&
\ (cache[lnum_prev]['is_def'] ||
\ lines[lnum_prev] =~# s:multiline_def_end_regex)
let in_docstring = 1

if docstring_match[1] ==# '"""'
let docstring_end_regex = s:docstring_end_double_regex
else
let docstring_end_regex = s:docstring_end_single_regex
endif

call s:foldexpr(cache[lnum], s:defs(cache, lines, non_blanks, lnum) + 1, 1)
continue
endif
endif
endif
endif

if s:docstring_level == -1
return s:FoldExpr(a:lnum, s:NumContainingDefs(a:lnum) + s:fold_docstrings)
else
return s:FoldExpr(a:lnum, s:docstring_level)
end
endif

let docstring_match = matchlist(line, s:docstring_start_regex)
let prev_line = s:GetLine(a:lnum - 1)
if !empty(docstring_match) && (prev_line =~# b:def_regex || prev_line =~# s:multiline_def_end_regex)
let b:in_docstring = 1

if docstring_match[1] ==# '"""'
let b:docstring_end_regex = s:docstring_end_double_regex
else
let b:docstring_end_regex = s:docstring_end_single_regex
" Imports
if s:fold_imports
if in_import
if line =~# import_end_regex
let in_import = 0
endif

call s:foldexpr(cache[lnum], s:defs(cache, lines, non_blanks, lnum) + 1, 0)
continue
elseif match(line, s:import_start_regex) != -1
let import_cont_match = matchlist(line, s:import_cont_regex)
if !empty(import_cont_match)
if import_cont_match[1] ==# '('
let import_end_regex = s:import_end_paren_regex
let in_import = 1
elseif import_cont_match[2] ==# '\'
let import_end_regex = s:import_end_esc_regex
let in_import = 1
endif
endif

if in_import
call s:foldexpr(cache[lnum], s:defs(cache, lines, non_blanks, lnum) + 1, 1)
continue
endif
endif
endif

if s:docstring_level == -1
return s:FoldExpr(a:lnum, s:NumContainingDefs(a:lnum) + s:fold_docstrings)
else
return s:FoldExpr(a:lnum, s:docstring_level)
end

endif

if b:in_import
if line =~# b:import_end_regex
let b:in_import = 0
endif
" Otherwise, its fold level is equal to its number of containing
" definitions, plus 1, if this line starts a definition of its own
call s:foldexpr(
\ cache[lnum],
\ s:defs(cache, lines, non_blanks, lnum) + cache[lnum]['is_def'],
\ cache[lnum]['is_def'],
\ )
endfor

if s:import_level == -1
return s:FoldExpr(a:lnum, s:NumContainingDefs(a:lnum) + s:fold_imports)
else
return s:FoldExpr(a:lnum, s:import_level)
end
elseif match(line, s:import_start_regex) != -1
let b:in_import = 1

let import_cont_match = matchlist(line, s:import_cont_regex)
if len(import_cont_match) && import_cont_match[1] ==# '('
let b:import_end_regex = s:import_end_paren_regex
elseif len(import_cont_match) && import_cont_match[2] ==# '\'
let b:import_end_regex = s:import_end_esc_regex
else
let b:in_import = 0
end
return cache
endfunction

if s:import_level == -1
return s:FoldExpr(a:lnum, s:NumContainingDefs(a:lnum) + s:fold_imports)
else
return s:FoldExpr(a:lnum, s:import_level)
end
" Compute fold level for Python code
function! SimpylFold(lnum)
if !exists('b:SimpylFold_cache')
let b:SimpylFold_cache = s:cache()
endif
return b:SimpylFold_cache[a:lnum]['foldexpr']
endfunction

" Otherwise, its fold level is equal to its number of containing
" definitions, plus 1, if this line starts a definition of its own
return s:FoldExpr(a:lnum, s:NumContainingDefs(a:lnum) + (line =~# b:def_regex))
" Recache the buffer
function! SimpylFoldRecache()
if exists('b:SimpylFold_cache')
unlet b:SimpylFold_cache
endif
endfunction

" Obtain the first line of the docstring for the folded class or function, if
" any exists, for use in the fold text
function! SimpylFoldText()
let next = nextnonblank(v:foldstart + 1)
let docstring = s:GetLine(next)
let docstring = getline(next)
let ds_prefix = '^\s*\%(\%(["'']\)\{3}\|[''"]\ze[^''"]\)'
if docstring =~# ds_prefix
let quote_char = docstring[match(docstring, '["'']')]
let docstring = substitute(docstring, ds_prefix, '', '')
if docstring =~# s:blank_regex
if docstring !~# s:non_blank_regex
let docstring =
\ substitute(s:GetLine(nextnonblank(next + 1)), '^\s*', '', '')
\ substitute(getline(nextnonblank(next + 1)), '^\s*', '', '')
endif
let docstring = substitute(docstring, quote_char . '\{,3}$', '', '')
return ' ' . docstring
endif
return ''
endfunction

augroup SimpylFold
autocmd TextChanged,InsertLeave <buffer> call SimpylFoldRecache()
augroup END

setlocal foldexpr=SimpylFold(v:lnum)
setlocal foldmethod=expr

Expand Down

0 comments on commit 2dfeb35

Please sign in to comment.