From d113fe0a30302a450db0fe04e775025d39145e8f Mon Sep 17 00:00:00 2001 From: Jacob Niehus Date: Mon, 17 Nov 2014 14:01:49 -0700 Subject: [PATCH 1/6] Handle folding of nested defs correctly --- autoload/pymode/folding.vim | 15 +++++++++++++++ autoload/pymode/motion.vim | 10 +++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/autoload/pymode/folding.vim b/autoload/pymode/folding.vim index 78cf8486..c7d7f769 100644 --- a/autoload/pymode/folding.vim +++ b/autoload/pymode/folding.vim @@ -57,6 +57,21 @@ fun! pymode#folding#expr(lnum) "{{{ return "<".(indent / &shiftwidth + 1) endif + " Handle nested defs + let last_def = pymode#motion#BlockStart(a:lnum, s:def_regex) + if getline(last_def) =~ s:def_regex + let last_def_end = pymode#motion#BlockEnd(last_def) + if last_def_end < line('$') + let nested = getline(pymode#motion#BlockStart(last_def - 1)) =~ s:def_regex + if nested && getline(nextnonblank(a:lnum)) !~ s:def_regex + let fold_end = min([prevnonblank(last_def_end - 1) + 2, last_def_end]) + if a:lnum == fold_end + return 's1' + endif + endif + endif + endif + if line =~ s:blank_regex if prev_line =~ s:blank_regex if indent(a:lnum + 1) == 0 && getline(a:lnum + 1) !~ s:blank_regex diff --git a/autoload/pymode/motion.vim b/autoload/pymode/motion.vim index 67e99e6b..1ddfa491 100644 --- a/autoload/pymode/motion.vim +++ b/autoload/pymode/motion.vim @@ -32,15 +32,15 @@ endfunction "}}} fun! pymode#motion#select(pattern, inner) "{{{ let cnt = v:count1 - 1 let orig = getpos('.')[1:2] - let snum = s:BlockStart(orig[0], a:pattern) + let snum = pymode#motion#BlockStart(orig[0], a:pattern) if getline(snum) !~ a:pattern return 0 endif - let enum = s:BlockEnd(snum, indent(snum)) + let enum = pymode#motion#BlockEnd(snum, indent(snum)) while cnt let lnum = search(a:pattern, 'nW') if lnum - let enum = s:BlockEnd(lnum, indent(lnum)) + let enum = pymode#motion#BlockEnd(lnum, indent(lnum)) call cursor(enum, 1) endif let cnt = cnt - 1 @@ -58,7 +58,7 @@ fun! pymode#motion#select(pattern, inner) "{{{ endfunction "}}} -fun! s:BlockStart(lnum, ...) "{{{ +fun! pymode#motion#BlockStart(lnum, ...) "{{{ let pattern = a:0 ? a:1 : '^\s*\(@\|class\s.*:\|def\s\)' let lnum = a:lnum + 1 let indent = 100 @@ -82,7 +82,7 @@ fun! s:BlockStart(lnum, ...) "{{{ endfunction "}}} -fun! s:BlockEnd(lnum, ...) "{{{ +fun! pymode#motion#BlockEnd(lnum, ...) "{{{ let indent = a:0 ? a:1 : indent(a:lnum) let lnum = a:lnum while lnum From 906eff560e0ab1ce037b6f7088ffb23aa9a4959a Mon Sep 17 00:00:00 2001 From: Jacob Niehus Date: Mon, 17 Nov 2014 18:16:17 -0700 Subject: [PATCH 2/6] Make folding nested defs much faster --- autoload/pymode/folding.vim | 57 ++++++++++++++++++++++++++++++------- autoload/pymode/motion.vim | 10 +++---- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/autoload/pymode/folding.vim b/autoload/pymode/folding.vim index c7d7f769..03fbe695 100644 --- a/autoload/pymode/folding.vim +++ b/autoload/pymode/folding.vim @@ -58,18 +58,43 @@ fun! pymode#folding#expr(lnum) "{{{ endif " Handle nested defs - let last_def = pymode#motion#BlockStart(a:lnum, s:def_regex) - if getline(last_def) =~ s:def_regex - let last_def_end = pymode#motion#BlockEnd(last_def) - if last_def_end < line('$') - let nested = getline(pymode#motion#BlockStart(last_def - 1)) =~ s:def_regex - if nested && getline(nextnonblank(a:lnum)) !~ s:def_regex - let fold_end = min([prevnonblank(last_def_end - 1) + 2, last_def_end]) - if a:lnum == fold_end - return 's1' + if indent(prevnonblank(a:lnum)) + let curpos = getcurpos() + try + let last_block = s:BlockStart(a:lnum) + let last_block_indent = indent(last_block) + + " Check if last class/def is not indented and therefore can't be + " nested and make sure it is a class/def block instead of a zero + " indented regular statement + if last_block_indent && getline(last_block) =~ s:def_regex + " Note: This relies on the cursor position being set by s:BlockStart + let next_def = searchpos('^\s*def \w', 'nW')[0] + let next_def_indent = next_def ? indent(next_def) : -1 + let last_block_end = s:BlockEnd(last_block) + + " If the next def has the same or greater indent than the + " previous def, it is either nested at the same level or + " nested one level deeper, and in either case will have its + " own fold. If the class/def containing the current line is on + " the first line it can't be nested, and if the this block + " ends on the last line, it contains no trailing code that + " should not be folded. Otherwise, we know the current line + " is at the end of a nested def. + if next_def_indent < last_block_indent && last_block > 1 && last_block_end < line('$') + + " Include up to one blank line in the fold + let fold_end = min([prevnonblank(last_block_end - 1) + 1, last_block_end]) + if a:lnum == fold_end + return 's1' + else + return '=' + endif endif endif - endif + finally + call setpos('.', curpos) + endtry endif if line =~ s:blank_regex @@ -91,5 +116,17 @@ fun! pymode#folding#expr(lnum) "{{{ endfunction "}}} +fun! s:BlockStart(lnum) "{{{ + " Note: Make sure to reset cursor position after using this function. + call cursor(a:lnum, 0) + let max_indent = max([indent(prevnonblank(a:lnum)) - &shiftwidth, 0]) + return searchpos('\v^(\s{,'.max_indent.'}(def |class |\@)\w|[^ \t#])', 'bcnW')[0] +endfunction "}}} + +fun! s:BlockEnd(lnum) "{{{ + " Note: Make sure to reset cursor position after using this function. + call cursor(a:lnum, 0) + return searchpos('\v^\s{,'.indent('.').'}\S', 'nW')[0] - 1 +endfunction "}}} " vim: fdm=marker:fdl=0 diff --git a/autoload/pymode/motion.vim b/autoload/pymode/motion.vim index 1ddfa491..67e99e6b 100644 --- a/autoload/pymode/motion.vim +++ b/autoload/pymode/motion.vim @@ -32,15 +32,15 @@ endfunction "}}} fun! pymode#motion#select(pattern, inner) "{{{ let cnt = v:count1 - 1 let orig = getpos('.')[1:2] - let snum = pymode#motion#BlockStart(orig[0], a:pattern) + let snum = s:BlockStart(orig[0], a:pattern) if getline(snum) !~ a:pattern return 0 endif - let enum = pymode#motion#BlockEnd(snum, indent(snum)) + let enum = s:BlockEnd(snum, indent(snum)) while cnt let lnum = search(a:pattern, 'nW') if lnum - let enum = pymode#motion#BlockEnd(lnum, indent(lnum)) + let enum = s:BlockEnd(lnum, indent(lnum)) call cursor(enum, 1) endif let cnt = cnt - 1 @@ -58,7 +58,7 @@ fun! pymode#motion#select(pattern, inner) "{{{ endfunction "}}} -fun! pymode#motion#BlockStart(lnum, ...) "{{{ +fun! s:BlockStart(lnum, ...) "{{{ let pattern = a:0 ? a:1 : '^\s*\(@\|class\s.*:\|def\s\)' let lnum = a:lnum + 1 let indent = 100 @@ -82,7 +82,7 @@ fun! pymode#motion#BlockStart(lnum, ...) "{{{ endfunction "}}} -fun! pymode#motion#BlockEnd(lnum, ...) "{{{ +fun! s:BlockEnd(lnum, ...) "{{{ let indent = a:0 ? a:1 : indent(a:lnum) let lnum = a:lnum while lnum From 391ece061adea41215ed662e6954b9e1c04722a6 Mon Sep 17 00:00:00 2001 From: Jacob Niehus Date: Mon, 17 Nov 2014 20:14:27 -0700 Subject: [PATCH 3/6] Fix edge case bug at end of top-level fold --- autoload/pymode/folding.vim | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/autoload/pymode/folding.vim b/autoload/pymode/folding.vim index 03fbe695..9a232960 100644 --- a/autoload/pymode/folding.vim +++ b/autoload/pymode/folding.vim @@ -79,14 +79,18 @@ fun! pymode#folding#expr(lnum) "{{{ " own fold. If the class/def containing the current line is on " the first line it can't be nested, and if the this block " ends on the last line, it contains no trailing code that - " should not be folded. Otherwise, we know the current line - " is at the end of a nested def. + " should not be folded. Finally, if the next non-blank line + " after the end of the previous def is less indented than the + " previous def, it is not part of the same fold as that def. + " Otherwise, we know the current line is at the end of a + " nested def. if next_def_indent < last_block_indent && last_block > 1 && last_block_end < line('$') + \ && indent(nextnonblank(last_block_end)) >= last_block_indent " Include up to one blank line in the fold let fold_end = min([prevnonblank(last_block_end - 1) + 1, last_block_end]) if a:lnum == fold_end - return 's1' + return next_def ? 's1' : 0 else return '=' endif From 5304abef6bf96e4cf107ac48f3bfaead006f8b52 Mon Sep 17 00:00:00 2001 From: Jacob Niehus Date: Tue, 18 Nov 2014 11:30:53 -0700 Subject: [PATCH 4/6] Fix more corner cases - Fix folding of code between two defs nested under the same def - Don't search for lines other than def or class in BlockStart because only def and class can contain nested folds - Fix pattern used to find next_def - Fix case where last_block_end is not followed by a blank line --- autoload/pymode/folding.vim | 39 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/autoload/pymode/folding.vim b/autoload/pymode/folding.vim index 9a232960..8434ef90 100644 --- a/autoload/pymode/folding.vim +++ b/autoload/pymode/folding.vim @@ -65,32 +65,33 @@ fun! pymode#folding#expr(lnum) "{{{ let last_block_indent = indent(last_block) " Check if last class/def is not indented and therefore can't be - " nested and make sure it is a class/def block instead of a zero - " indented regular statement - if last_block_indent && getline(last_block) =~ s:def_regex + " nested. + if last_block_indent " Note: This relies on the cursor position being set by s:BlockStart - let next_def = searchpos('^\s*def \w', 'nW')[0] + let next_def = searchpos(s:def_regex, 'nW')[0] let next_def_indent = next_def ? indent(next_def) : -1 let last_block_end = s:BlockEnd(last_block) - " If the next def has the same or greater indent than the - " previous def, it is either nested at the same level or - " nested one level deeper, and in either case will have its - " own fold. If the class/def containing the current line is on - " the first line it can't be nested, and if the this block - " ends on the last line, it contains no trailing code that - " should not be folded. Finally, if the next non-blank line - " after the end of the previous def is less indented than the - " previous def, it is not part of the same fold as that def. - " Otherwise, we know the current line is at the end of a - " nested def. - if next_def_indent < last_block_indent && last_block > 1 && last_block_end < line('$') + " If the next def has greater indent than the previous def, it + " is nested one level deeper and will have its own fold. If + " the class/def containing the current line is on the first + " line it can't be nested, and if this block ends on the last + " line, it contains no trailing code that should not be + " folded. Finally, if the next non-blank line after the end of + " the previous def is less indented than the previous def, it + " is not part of the same fold as that def. Otherwise, we know + " the current line is at the end of a nested def. + if next_def_indent <= last_block_indent && last_block > 1 && last_block_end < line('$') \ && indent(nextnonblank(last_block_end)) >= last_block_indent " Include up to one blank line in the fold - let fold_end = min([prevnonblank(last_block_end - 1) + 1, last_block_end]) + if getline(last_block_end) =~ s:blank_regex + let fold_end = min([prevnonblank(last_block_end - 1), last_block_end]) + 1 + else + let fold_end = last_block_end + endif if a:lnum == fold_end - return next_def ? 's1' : 0 + return 's1' else return '=' endif @@ -124,7 +125,7 @@ fun! s:BlockStart(lnum) "{{{ " Note: Make sure to reset cursor position after using this function. call cursor(a:lnum, 0) let max_indent = max([indent(prevnonblank(a:lnum)) - &shiftwidth, 0]) - return searchpos('\v^(\s{,'.max_indent.'}(def |class |\@)\w|[^ \t#])', 'bcnW')[0] + return searchpos('\v^\s{,'.max_indent.'}(def |class )\w', 'bcnW')[0] endfunction "}}} fun! s:BlockEnd(lnum) "{{{ From 09dc5efccff21189518a6ba72bd18cd5e8a70963 Mon Sep 17 00:00:00 2001 From: Jacob Niehus Date: Tue, 18 Nov 2014 18:35:50 -0700 Subject: [PATCH 5/6] Add file length limit for checking for nested folds --- autoload/pymode/folding.vim | 5 +++-- plugin/pymode.vim | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/autoload/pymode/folding.vim b/autoload/pymode/folding.vim index 8434ef90..83ff887e 100644 --- a/autoload/pymode/folding.vim +++ b/autoload/pymode/folding.vim @@ -57,8 +57,9 @@ fun! pymode#folding#expr(lnum) "{{{ return "<".(indent / &shiftwidth + 1) endif - " Handle nested defs - if indent(prevnonblank(a:lnum)) + " Handle nested defs but only for files shorter than + " g:pymode_folding_nest_limit lines due to performance concerns + if line('$') < g:pymode_folding_nest_limit && indent(prevnonblank(a:lnum)) let curpos = getcurpos() try let last_block = s:BlockStart(a:lnum) diff --git a/plugin/pymode.vim b/plugin/pymode.vim index d63fa59b..9bd4d95c 100644 --- a/plugin/pymode.vim +++ b/plugin/pymode.vim @@ -36,6 +36,8 @@ call pymode#default("g:pymode_indent", 1) " Enable/disable pymode folding for pyfiles. call pymode#default("g:pymode_folding", 1) +" Maximum file length to check for nested class/def statements +call pymode#default("g:pymode_folding_nest_limit", 1000) " Change for folding customization (by example enable fold for 'if', 'for') call pymode#default("g:pymode_folding_regex", '^\s*\%(class\|def\) \w\+') From f2a03ec7e9c328eb7789082e49c8b07fcd527117 Mon Sep 17 00:00:00 2001 From: Jacob Niehus Date: Wed, 19 Nov 2014 12:21:27 -0700 Subject: [PATCH 6/6] Fix BlockStart when previous line is indented --- autoload/pymode/folding.vim | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/autoload/pymode/folding.vim b/autoload/pymode/folding.vim index 83ff887e..e5ea4082 100644 --- a/autoload/pymode/folding.vim +++ b/autoload/pymode/folding.vim @@ -68,7 +68,7 @@ fun! pymode#folding#expr(lnum) "{{{ " Check if last class/def is not indented and therefore can't be " nested. if last_block_indent - " Note: This relies on the cursor position being set by s:BlockStart + call cursor(a:lnum, 0) let next_def = searchpos(s:def_regex, 'nW')[0] let next_def_indent = next_def ? indent(next_def) : -1 let last_block_end = s:BlockEnd(last_block) @@ -125,7 +125,26 @@ endfunction "}}} fun! s:BlockStart(lnum) "{{{ " Note: Make sure to reset cursor position after using this function. call cursor(a:lnum, 0) - let max_indent = max([indent(prevnonblank(a:lnum)) - &shiftwidth, 0]) + + " In case the end of the block is indented to a higher level than the def + " statement plus one shiftwidth, we need to find the indent level at the + " bottom of that if/for/try/while/etc. block. + let last_def = searchpos(s:def_regex, 'bcnW')[0] + if last_def + let last_def_indent = indent(last_def) + call cursor(last_def, 0) + let next_stmt_at_def_indent = searchpos('\v^\s{'.last_def_indent.'}[^[:space:]#]', 'nW')[0] + else + let next_stmt_at_def_indent = -1 + endif + + " Now find the class/def one shiftwidth lower than the start of the + " aforementioned indent block. + if next_stmt_at_def_indent && next_stmt_at_def_indent < a:lnum + let max_indent = max([indent(next_stmt_at_def_indent) - &shiftwidth, 0]) + else + let max_indent = max([indent(prevnonblank(a:lnum)) - &shiftwidth, 0]) + endif return searchpos('\v^\s{,'.max_indent.'}(def |class )\w', 'bcnW')[0] endfunction "}}}