vim slows down when using fold-expr - vim

I'm trying to create a simple, fast folding method for large markdown files. I'm using the fold-expr method in vim. For example, if I wanted to start folds on H1 and H2 markdown entries, my vimscript code is:
function! MarkdownLevel()
if getline(v:lnum) =~ '^# '
" begin a fold of level one here
return ">1"
elseif getline(v:lnum) =~ '^## '
" begin a fold of level two
return ">2"
else
return "="
endif
endfunction
This works perfectly, and I get nested folds. However, when I have a large markdown file, vim slows down considerably. This is unsurprising and is, in fact, indicated in the fold-expr help in vim. It's because the = sign tells vim to scan backwards in the file until the first line with an explicitly defined foldlevel can be found; this could be thousands of lines away.
I tried to replace the last line with
else
" set foldlevel to foldlevel of previous line
return foldlevel(v:lnum-1)
endif
But this doesn't work as expected. Does anyone know how to fix this? It's clear I'm not understanding how the foldlevel function works, or how the folding algorithm in vim is implemented.

I figured out how to fix the slowdown and learned somethings about how fold-expr works in vim. I tested the performance issues on a 3000 line md file.
I was relying on the following automatic folding functionality that fold-expr is supposed to have: it starts a fold if foldlevel of the current line is smaller than the foldlevel of the next. It ends a fold if the foldlevel of the current line is larger than the foldlevel of the next. Turns out that this does not work as intended, as far as I can tell.
What worked was to explicitly tell vim that a fold starts here using return ">1", where 1 is replaced by the appropriate number.
After learning how to profile vim scripts from #PeterRinker, I figured out that the return "=" statement was being evaluated many, many times when I was editing line (for example) 3000.
This was my fix: if the foldlevel of the current line does not fall into any of the heading types and the foldlevel of the previous line has already been defined, the current line should just inherit the foldlevel of the previous line. This is an obvious solution, but it does not work if I used return "1" instead of return ">1" above. It needs the return "=" statement on the first pass to figure out the foldlevel.
So my startup times are a little big (about 1 second) for a 3000 line file, but now editing is very smooth. The following is the finished, simplistic code. Other more elaborate markdown projects do not have this useful simplification.
function! MarkdownLevel()
let theline = getline(v:lnum)
let nextline = getline(v:lnum+1)
if theline =~ '^# '
" begin a fold of level one here
return ">1"
elseif theline =~ '^## '
" begin a fold of level two here
return ">2"
elseif theline =~ '^### '
" begin a fold of level three here
return ">3"
elseif nextline =~ '^===*'
" elseif the next line starts with at least two ==
return ">1"
elseif nextline =~ '^---*'
" elseif the line ends with at least two --
return ">2"
elseif foldlevel(v:lnum-1) != "-1"
return foldlevel(v:lnum-1)
else
return "="
endif
end

Have you thought about using Drew Nelstrom's vim-markdown-folding plugin?
You may also want to look that the Vimcast episode: Profiling Vimscript performance. This episode actually talks about folding markdown.
Cautionary thoughts
I can not be for certain because I have not profiled your code (and you should really profile your code), but as the fold expression gets called on every line every time things get redrawn it can be very taxing on Vim. Some guesses:
Using relative fold expressions like = means we need to compute the previous line so as you can imagine this can become problematic. Try and use exact depths without computing other lines if you can.
You are using getline() twice in your function needlessly
Some files are just going to cause problems accept this fact and disable folding via zi

That is to be expected, since Vim has to compute your expression a lot for every line. This is also mentioned in the help below :h fold-expr
Note: Since the expression has to be evaluated for every line,
this fold method can be very slow!
Try to avoid the "=", "a" and "s" return values, since Vim often
has to search backwards for a line for which the fold level is
defined. This can be slow.

Related

vim folds for asciidoc: include the line _previous_ to the section-title line

Summary. How (and if?) can I get vim folds to automatically associate the asciidoc [id=''] lines found immediately prior to a section-title line with the fold created for said section?
Details. asciidoc content enables section-title identification (that, among other things, maps to rendered-HTML-anchor-tag names and also enables intra-document cross-reference), otherwise known (I think?) as a block identifier. However, the [id=''] line blurb must be place prior to the section header line, even though it's part of the section. eg:
[id='under_construction', reftext='Under Construction']
## DISCLAIMER: This Document is Under Construction
This makes vim-folding of an asciidoc file much harder to manage, as folded-section moves "lose" the previous line (and all the section id's get shuffled), because in the eyes of the vim fold, the previous line belongs to the previous section.
I'm not sure how vim foldings asciidoc, but I assume that new section starts with ## (or ==, judging from a brief look at the link you provided), and [id=...] provides additional information on the section.
So you could have a look at Steve Losh's markdown folding and :h fold-expr.
So here is the modified code for markdown folding:
function! Fold_askiidoc(lnum)
let l1 = getline(a:lnum)
if l1 =~ '^\s*$'
" assume sections are separated by blank lines
return '0'
endif
" check next line
let l2 = getline(a:lnum+1)
if l2 =~ '^#'
" next line starts with hashes (or '=', or any symbol)
" number of hashes specifies indent level
return '>'.matchend(l2, '^#\+')
else
" otherwise keep previous foldlevel
return '='
endif
endfunction
setlocal foldexpr=Fold_test(v:lnum)
setlocal foldmethod=expr
It checks each line, if it's blank, suppose it's the end of a section. If the next line starts with #, it means the fold starts on the current line. The amount of #'s specify folding level.
It folds any non-blank line before a section title, if you want it only for specific lines, like id=[''], you would have to add additional string comparison.
You can save it to $HOME/.vim/after/ftplugin/asciidoc.vim. I'm not sure about file type, if it exists or you have to create it separately. From there it should be loaded automatically each time you open specific file. Or you can just put function in your vimrc and specify
setlocal foldexpr=Fold_test(v:lnum)
setlocal foldmethod=expr
as auto-comand for this file type.

How to write a foldexpr for Vim that would use the comments structure?

Is it possible to construct a foldexpr that would detect the following matches \n\n"\s[A-Z][A-Z].*\n as a start for first-level folds, and these matches \n"\s[A-Z][a-z].*\n as a start for second-level folds? Naturally, the nearest possible \n\n\n would mark an end of a first-level fold, and then \n\n would close the second-level fold.
Or am I missing something?
(I surely know about {{{ markers, it just doesn't seem right to my adding additional markers to a file...)
You won't be able to do this (at least, not easily or clearly) in a one-liner. You'll want to write a function.
Your function will not be able to use a single getline() plus regex compare, because getline() only returns a single line and you want to include multiple lines in your start/end strings. You can however, use multiple getline() calls and compare each line separately.
To enable starting new folds at the same level of a currently existing fold, you'll need to return strings ">1" or ">2". For ending folds, it is probably easiest to just set an explicit level (using strings "<2", etc. sometimes acts unexpectedly for me). See :help fold-expr for possible return values. It may be useful to know the last line's foldlevel in your function. For that, use the function foldlevel().
Here is an example which I think does what you ask for, though you may need to clean it up if it's not actually what you want. Load the script, source it, and it can use itself as test data:
fun! FoldSomething(lnum)
let line1=getline(a:lnum)
let line2=getline(a:lnum+1)
if line1=~'^$'
if line2=~#'^"\s[A-Z][A-Z]'
return ">1"
elseif line2=~'^$'
return 0
elseif foldlevel(a:lnum-1)==2
return 1
endif
elseif line1=~#'^"\s[A-Z][a-z]'
return ">2"
endif
return "="
endfun
set foldexpr=FoldSomething(v:lnum)
set foldmethod=expr
set foldcolumn=3
finish
" AA level 1 fold
" This is a level 2 fold
Here is some stuff under the fold.
It should be part of the level 2.
This isn't in the level 2.
I guess that makes it just part of the level 1.
" This is another
" level 2 fold.
" Watch out!
" This is 2 level 2 folds.
" BB another level 1 fold
" starts here.
"
This line shouldn't be folded at all.
That's because there were so many empty lines before.

Comment / uncomment multiple fixed lines in vim

In my code I have multiple scattered lines which help me to debug my program and show me what is going on during execution. Is there an easy and fast way to comment and uncomment (toggle) these fixed lines in vim? I thought about marking these lines with a special sign (e.g. //) like this in python:
print "Debug!" # //
and everytime a sepcific shortcut is pressed all lines which end with a "# 'some optional descriptive text' //" are commented or uncommented, respectively.
I've looked at NERD Commenter, but from what I read the lines to be commented / uncommented have to be selected each time?
First, find a pattern that selects the right lines. If you have :set hls, it will help spot the matches. I think something like /#.*\/\/$ is what you want.
Next, comment out the selected lines with
:g/<pattern>/s/^/# /
if # will comment out the line, and un-comment them with
:g/<pattern>/s/^# //
Now, you want a single command to toggle. You can either use a variable to keep track of the toggle state, or you can try to figure out the current state by examining the lines that match. Using a variable seems simpler.
The variable could be global, local to the buffer, or local to the script. I like using script-local variables in order to avoid cluttering the namespace. In this case, using a script-local variable might mean that vim will get confused when you switch buffers, so let's use a buffer-local variable, say b:commentToggle.
The first time the function is called, it notices that the variable is not set, so use search() to look for a line that starts with # (There is a space there!) and ends with the pattern we already have. The n flag means not to move the cursor, and w means to wrap around the end of the file (like searching with 'wrapscan' set). The search() function returns the line number (1-based!) if the pattern is found, 0 if not. See :help search().
This seems to work in a small test:
fun! CommentToggle()
if !exists('b:commentToggle')
let b:commentToggle = !search('^# .*#.*\/\/$', 'nw')
endif
if b:commentToggle == 1
g/#.*\/\/$/s/^/# /
else
g/#.*\/\/$/s/^# //e
endif
let b:commentToggle = !b:commentToggle
endfun
nnoremap <F4> :call CommentToggle()<CR>
If you want to put # in front of the first non-blank, then use ^\s*# in the search() command; s/\ze\S/# / or s/\S/\1# / in the first :g line; and s/^\s*\zs# // in the second :g line. See :help /\zs, :help /\ze, and :help sub-replace-special.

Vim Scripting: Count lines that match expression, and fold

I am currently developing a plugin for Vim for managing checklists.
I am currently using ":setlocal foldmethod=indent" in a syntax file to handle all of the folding within each checklist document. However, I'd like to create a function for folding that is more flexible, and will not rely on the indentation of the line to determine whether or not it is folded.
Here is an example checklist:
+ Parent
* Child
* Child
* Child
When a user presses <leader>vv on the "+ Parent" line, it fold the lines underneath it because they are indented one level. The problem with this is that the foldmethod sticks around for other buffers and folds lines that do not need to be folded.
Here is how I've thought of handling it so far:
function! FoldLines()
let l:line = getline(line(".") + 1)
" If next line is a child
if match(l:line, '^\s*\*') >= 0
" Loop until blank line is found, and store line numbers in a list
endif
" Select lines from list and fold
endfunction
I don't know how to handle the loop and folding. Any suggestions?
UPDATE
Well, karategeek6's solution works to some degree, but I failed to mention that my plugin also toggles checklist items.
Example:
+ Parent
× Child - Toggled
* Child - Standard
I may be wrong, but I don't think that foldexpr will be able to handle lines with both * and × at the beginning.
I've tried:
set foldexpr=strlen(substitute(substitute(getline(v:lnum),'\\s','',\"g\"),'[^[*|×]].*','',''))
but that doesn't seem to work, either.
It sounds like what you want it to fold by expression. According to the user manual, in fold by expression, you use an expression to set the fold level of every line. The user manual gives an example which I think can be adapted to your needs.
:set foldmethod=expr
:set foldexpr=strlen(substitute(substitute(getline(v:lnum),'\\s','',\"g\"),'[^*].*','',''))
You can read more on the example at the user manual, but in a nutshell, it will set the fold level equal to that of the number of leading '*', irrespective of whitespace.
Hopefully this is either exactly what you were looking for, or can point you in the right direction. The key aspect to remember in folding by expression is that you are deciding the fold level of each line, rather than which lines to fold.
Well, it seems like I found a solution. I ended up using this:
setlocal foldlevel=0
setlocal foldmethod=expr
setlocal foldexpr=FoldLevel(v:lnum)
function! FoldLevel(linenum)
let linetext = getline(a:linenum)
let level = indent(a:linenum) / 4
if linetext =~ '^\s*[\*|×]'
let level = 20
endif
return level
endfunction

Using Vim, isn't there a more efficient way to format LaTeX paragraphs according to this best practice?

The best practice mentioned in the title is the one suggested by Uri:
When writing paragraphs, start each
sentence at the beginning of a line,
and if it spills over, each subsequent
line is tabbed.
I use gVim with Vim-LaTeX, which comes with an indent/tex.vim file, to edit LaTeX files. The way I currently implement the practice mentioned above is as follows:
I :set textwidth=79 to automatically break lines before they become too long.
I manually hit Enter after I finish inserting each sentence.
If I'm done with revising and editing a sentence, I manually shift any spillovers using >>, prefixing it with a count if necessary.
Occasionally, that last step will make one or more spillovers go over the maximum line width. In this case, I
gqq the faulty line.
J my way through to the end of the sentence.
repeat steps 1 and 2 as necessary.
As you can imagine, this can become tedious. Isn't there a more efficient way to achieve the same result? Ultimately, I want to be able to write the sentences without worrying about their format, and then use gqap, or gqip, to automatically produce the result that I currently produce manually.
To do that, I suspect that I will need to write a formatexpr of my own, but I'm not sure how to proceed. I have found a number of plugins, Latex Text Formatter and Text (Especially LaTeX) Formatter, and a tip, but none of them seem to suit my needs, and I'm not sure how to modify them to do so.
I may well be oversimplifying the problem, but does this mapping do what you want?
nnoremap \z (j>>gq)
So pressing \z in normal mode will do the following: From the cursor position, jump to the start of the sentence. Then go to the next line and indent it. Then reformat from this line to the end of the sentence. Reformatting sentence-wise is the way to go, rather than reformatting each line individually, as your method seems to do.
Of course you can use an insert-mode mapping if you prefer, or even try redefining the behaviour of the Enter key to do this automatically (although I don't know if this will have unintended consequences...).
One way to do this is not by actually breaking the lines in the file but instead doing the following:
set wrap linebreak
let &showbreak='===> '
The wrap option makes long lines wrap instead of extending off the screen and linebreak makes the line breaks happen only at characters specified in the breakat option.
You can set showbreak to anything that is pleasing to your eye. My favorite when I'm using vim where unicode characters work right is:
let &showbreak="\u21aa "
This puts a ↪ symbol at the beginning of each wrapped line.
I also like to turn on line numbers (set number) to give another indicator of what the actual lines in the file are.
To make navigating the file easier you might want to use
noremap j gj
noremap k gk
noremap gj j
noremap gk k
This makes k and j move up and down by displayed lines not file lines. To affect the cursor keys as well replace k with <Up> and j with <Down>.
One option that takes different tack than tabbing subsequent lines would be to set the w flag in formatoptions. When you do that it changes the way Vim identifies new paragraphs, and lines ending in a space are understood to continue on a new line as part of same paragraph. See :h fo-table.
If you set the w flag and enter your text so that continued sentence lines are the only ones ending in a space (and abandon completely practice of entering tabs at beginning of any text lines) then I think you should be able to use gqap to format text paragraphs as you want. To get visual cues to logical structure you can then set listchars to display the eol (i.e., <cr>) character and set different highlightings for <space><cr> and for <non-space><cr> so that sentence/paragraph ends are easily spotted.
Another benefit of this method is that you can just type your text naturally and let line breaks be entered automatically by textwidth setting. (Just make sure that LaTeX formatting lines don't break automatically in textwidth area; you want them to have non-space char as last char in line.)
That tip also caught my eye. Here's how I solved the problem (a diff of the changed lines in tex.vim):
*** tex.vim.old 2011-08-16 08:26:56.845046457 +0200
--- tex.vim 2011-08-16 08:59:14.736306930 +0200
***************
*** 90,95 ****
--- 90,96 ----
" LH modification : \begin does not always start a line
if line =~ '\\begin{\(.*\)}' && line !~ 'verbatim'
\ && line !~ 'document'
+ \ || line =~ '^\s*[A-Z].*[a-zA-Z0-9,]\s*$\C'
let ind = ind + &sw
***************
*** 105,110 ****
--- 106,112 ----
" Subtract a 'shiftwidth' when an environment ends
if cline =~ '^\s*\\end' && cline !~ 'verbatim'
\&& cline !~ 'document'
+ \|| line =~ '\.\s*$'
if g:tex_indent_items == 1
" Remove another sw for item-environments
Basically it indents new lines when the previous line starts with a capital letter and ends with a letter, digit, or comma, and "unindents" new lines with the previous line ends with a period.
There is definitely room for improvement (better criteria) but for me it works all right so far.
I find the suggestion from #kev (and the people commented) at this post to be the most satisfying.
There, it is explained that by setting
:set fo+=n
followed by either
:let &flp='^\s*\\(item\|end\|begin)*\s*'
or
:let &l:flp='^\s*\\\(item\|end\|begin\)\s*'
lets you type gggqG to reformat the entire file.
I use the vim-textobj-usr plugin to define a "LaTeXPar" text-object. Then I can use gwal to format.
There is already a vim-textobj-latex plugin, but the biggest text-object it defines is "environment". This is not what I (and OP) want.
A "LaTeXPar" is delimited by
an empty line
a line begin with \[, \], \begin, \end, }
a line end with {
this is adapted to my writing habit: I always have an empty line after \section, always use \[ \] on a single line, and so on. You can easily write one for yourself.
Here is the relative part in my ~/.vim/ftplugin/tex.vim.
call textobj#user#plugin('latexpar', {
\ 'par': {
\ 'select-a-function': 'LaTeXPar',
\ 'select-a': 'al',
\ },
\ })
function! LaTeXPar()
let pattern='\v^$|^\s*(\\\[|\\\]|\\begin|\\end|\})|\{$'
if search(pattern,"bW")
normal! j
else
normal! gg
endif
let head_pos = getpos('.')
if search(pattern,"W")
normal! k
else
normal! G
endif
let tail_pos = getpos('.')
" echo head_pos[2]
" echo tail_pos[2]
return ["V", head_pos, tail_pos]
endfunction

Resources