Vim Scripting: Count lines that match expression, and fold - vim

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

Related

vim slows down when using fold-expr

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.

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.

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

How to diff two lines in an open file in vim?

I occasionally see very long lines in my code that I need to check if they are the same. Is there a way in vim to select two lines and diff them to show any differences between the two?
For example, given the two lines in vim:
AVeryLongReturnType* MyLongClassName:hasAnEvenLongerFunction(That *is, Overloaded *with, Multiple *different, Parameter *lists);
AVeryLongReturnType* MyLongClassName:hasAnEvenLongerFunction(That *is, Overloaded *with, Multiple *different, Parameter *1ists);
I would like vim to tell me that the two lines are in fact different because each spells "lists" differently. Is this possible, and if so, how do I do it?
A quick and dirty solution is to just select both lines and sort them while removing duplicates:
select lines
":sort u"
if only one line remains, both were equal
if both remain, there most be some difference
An undo recovers everything again.
An alternative to #sehe's approach would not require the use of temp files:
funct! DiffTwoTexts(text1, text2)
new
put =a:text1
normal ggdd
diffthis
new
put =a:text2
normal ggdd
diffthis
endfunct
funct! DiffTwoLines(line1, line2)
let text1 = getline(a:line1)
let text2 = getline(a:line2)
call DiffTwoTexts(text1, text2)
endfunct
comma! DiffWithNext call DiffTwoLines('.', line('.') + 1)
This will still be pretty hard to read, since it keeps everything on a single line, so I came up with this modification:
funct! EvalTextPreprocessor(expr, text)
let text = a:text
return eval(a:expr)
endfunct
comma! -nargs=1 DiffWithNextPre call DiffTwoTexts(
\ EvalTextPreprocessor(<q-args>, getline('.')),
\ EvalTextPreprocessor(<q-args>, getline(line('.') + 1)))
This new command takes a vimscript expression as its argument, wherein the variable text refers to whichever line is being preprocessed. So you can call, e.g.
DiffWithNextPre split(text, '[(,)]\zs')
For your sample data, this gives the two buffers
AVeryLongReturnType* MyLongClassName:hasAnEvenLongerFunction(
That *is,
Overloaded *with,
Multiple *different,
Parameter *lists)
;
and
AVeryLongReturnType* MyLongClassName:hasAnEvenLongerFunction(
That *is,
Overloaded *with,
Multiple *different,
Parameter *1ists)
;
Only the lines that start with Parameter are highlighted.
You can even build up from there, creating a command
comma! DiffTwoCFunctionSigs DiffWithNextPre split(text, '[(,)]\s*\zs')
Notice that I modified the regexp a bit so that it will keep trailing spaces at the end of lines. You could get it to ignore them entirely by moving the \s* to after the \zs. See :help /\zs if you're unfamiliar with what that vim-specific RE atom does.
A nicety would be to make the command take a range (see :help command-range), which you could use by diffing the first line of the range with the last line. So then you just visual-select from the first line to the second and call the command.
I used linediff.vim.
This plugin provides a simple command, ":Linediff", which is used to diff two separate blocks of text.
That is not a feature, however it is easily scripted, e.g. in your vimrc:
function! DiffLineWithNext()
let f1=tempname()
let f2=tempname()
exec ".write " . f1
exec ".+1write " . f2
exec "tabedit " . f1
exec "vert diffsplit " . f2
endfunction
This will open the current and next lines in vertical split in another tab.
Note that this code is a sample
it doesn't check whether next line exists (there are any following lines)
it doesn't cleanup the tempfiles created
a nice improvement would be to take a range, or use the '' mark to select the other line
You can leave off the 'vert' in order to have a horizontal split
Map it to something fancy so you don't have to :call it manually:
:nnoremap <F10> :call DiffLineWithNext()^M
you could also just create a new empty window buffer and copy line, then make command:
:windo diffthis
this should open a new window showing the differences of those 2 lines

Notepad++ like "multi editing" in Vim?

I’m switching from Notepad++ to Vim as my main text editor.
In Notepad++, you can have multiple cursors by holding down Ctrl and clicking anywhere in the text, so that if you type, the text appears in multiple locations.
Is it possible in Vim? Something like insert after selecting multiple rows in Visual mode, but with the possibility to have cursors anywhere in the text.
It’s a feature I rarely use, and it’s also quite easily avoidable; I’m just curious, since it’s the only one I could’t find a replacement for in Vim yet.
There is not a built-in feature of that kind.
Let me suggest a function that repeats command (for example . repeating last
change command) at the positions of given marks. Both marks and command are
specified as string arguments. Marks specified in the way ranges in regular
expressions or scanf-format specifier are defined. For example, za-dx
means marks z, a, b, c, d, x.
function! MarksRepeat(marks, command)
let pos = 0
let len = strlen(a:marks)
let alpha = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
let beta = '1234567899bcdefghijklmnopqrstuvwxyzzBCDEFGHIJKLMNOPQRSTUVWXYZZ'
while pos < len
if a:marks[pos + 1] != '-'
exe 'norm `' . a:marks[pos] . a:command
let pos += 1
elseif a:marks[pos] <= a:marks[pos+2]
let mark = a:marks[pos]
let stop = a:marks[pos+2]
if mark =~ '[0-9a-zA-Z]' && stop =~ '[0-9a-zA-Z]'
while 1
exe 'norm `' . mark . a:command
if mark == stop
break
endif
let mark = tr(mark, alpha, beta)
endwhile
endif
let pos += 3
endif
endwhile
endfunction
In your case, the function could be used as follows.
Mark all places for simultaneous insertions (except one) using Vim
marks (by means of m command).
Actually insert text in the one place that has not been marked.
Run the function:
:call MarksRepeat(‹marks›, '.')
You could insert the text in one place, in a single operation, then use . to repeat that insertion at each other place you want the text.
It's the converse of what you asked for, because you wanted to mark the locations before entering the text, but it gives you the same result in the same number of keystrokes :).
Check multi select vim plugin: http://www.vim.org/scripts/script.php?script_id=953
ib's response and the multi select vim plugin are interesting, but the following is a suggestion that does not require a special function or plugin.
Temporarily set foldmethod=manual, then mark the blocks you want to operate on with zf.
Finally, use the ex command :folddoclosed to do ex commands on the folded blocks.
For example: :folddoclosed norm Iinsert some text at the front
Note, you can use :folddoclosed on any folded groups of lines, so you could use other foldmethods... but usually it makes sense to manually create the folds.
You can also use visual markers, followed by :norm which gives you :'<,'>norm... But visual markers only let you select a continuous range of lines. Using folds and :folddoclosed you can operate on multiple ranges of lines at once.
Another tip... to save time having to type out :folddoclosed, I will type :fo<shifttab><shifttab><shifttab>

Resources