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

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.

Related

How to do mark-like mapping in vim

The m normal command accepts a letter just after it to set a "letter" mark.
I would like to create a similar command that works across tabs... But my problem is with the binding : Is there a simple way to bind for example M<letter> to a function or a command or should I manually repeat all the possibilities ?
As romainl has already said, no.
Covering this for good measure (and in case someone else comes along later), you can only practically map upper-case letters. As is outlined in the documentation, lower-case marks are only valid within a single file. Upper-case ones, that the Vim docs calls "file marks", are valid from anywhere. Unless you have some dark magic function to resolve ambiguous file marks, you probably only need a single for loop mapping the upper-case letters, if you're going with the brute-force option.
That said, there are a couple alternatives here as well.
As far as I know, the only "dynamic" bit of a command is a count (or a range, but unless you want to map characters to a number (and handle ranges and other fun stuff:tm:), I don't recommend this approach:
" The <C-U> is required for all uses. If you want a function,
" you'd need :<C-U>call YourFunction()<cr>
nnoremap M :<C-U>echom v:count<cr>
See also :h v:count, which states:
Note: the <C-U> is required to remove the line range that you get when typing ':' after a count.
You can then run 26M, decode v:count as Z, and then do whatever fancy lookup from there.
The second alternative, and the one proposed by romainl and by far the most used one in cases like this (source: experience and lots of code browsing), is using a for loop to brute-force map letters:
for i in range(char2nr('A'), char2nr('Z'))
exec 'nnoremap M' . nr2char(i) ':echo "This is where the appropriate function call goes"<cr>'
endfor
Which produces all 26 mappings.
And the final approach is abusing getchar(). This means a single mapping, but at the expense of doing additional processing:
func! Func()
let c = getchar()
echo c
" Character processing (the `echo` isn't required) here
endfunc
nnoremap M :call Func()<cr>
You can decide which works for you, but I strongly suggest preferring option 2, as this gives you map timeouts and clear definitions, and more obvious conflict detection.

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.

Sorting Vim Folds

I have a file that looks something like this:
dog{{{
blah blah blah
}}}
cat{{{
blah blah
}}}
aardvark{{{
blah blah blah blah
}}}
In my vimrc I have set foldmethod=marker, so that the contents of the file are all folded at the curly braces. My question is, how can I sort the file based on the fold header? I want the aarvaark section to come first, then cat, then dog.
Some folded sections have other folds within them. I do not want to sort those lower fold levels. The file should be sorted by level 1 folds only.
With all folds closed (zM or :set foldlevel=0), you can use delete dd and paste p to manually shift the folded blocks around. This is okay for small amounts of text.
For huge texts, you'd have to write a sort function yourself, as Vim does not offer such functionality. The following algorithm can be used:
Join all folded lines together with a delimiter that does not exist in the text (e.g. <Nul>).
Execute the :sort command.
Un-join the range on the special delimiter.
With error handling and corner cases, it's actually quite a bit of implementation effort. I've made an attempt here: https://gist.github.com/4145501
One approach would be as follows:
Search and replace the line endings so that the level-one folds are each on one line (i.e. replacing the line ending with a unique string that doesn't appear elsewhere in the file).
Use :sort to sort the lines in the file, as usual.
Reverse the search and replace from Step 1 to reintroduce the line endings.
This could all be recorded as a Vim macro.
Steps 1 and 3 would be straightforward if there weren't nested folds, or if there was an easy way of distinguishing top-level folds from nested ones (e.g. if the level-one {{{ and }}} lines are flush left and the others have leading space). If this isn't the case, it's probably still possible but much more difficult.
I would use power of Vim macros.
Suppose that you do not have any other staff in your file except the records and first record starts on the first line. And that you do not have start of level 1 and end on the same line. And all your {} match well.
Go to the first entry and record a macro to register a (qa - start recording q - stop)
/{^MV%k:s/\n/EEEEEE/^Mj^
Copy mine or better record it yourself: (^M - stands for literal enter)
/{^M find next {
V%k select line-vise till the end of level 1 but not the closing line with }}}
:s/\n/EEEEEE/^M change symbol 'end of line' to some unique string inside selection
j^ go one line down and to the begging of the line in case the previous record had leading spaces.
Run the macro starting from the second line (suppose that the first is processed already when you recorded the macro.)
In normal mode 100000#a - use whatever number greater than number of your records. Macro will stop itself when all lines are processed if the first record is in the first line of file. As a side effect it will visually select the first line - unselect it.
Sort the file
:sort
Restore the end of lines:
:%s/EEEEEE/^M/g
Note ^M should be literal enter. To type it use Ctrl-V Enter
It works with nested levels because % jumps to the matching closing }. That is why it is important that you have all the {} matched well.
Macro will stop when could not go up with k that is why you must have the first record on the first line. Empty lines or anything that is not a record between records will be ignored by the macro.
The method above should work well for your file. If you want a more general approach with folds then use the following commands in the macro instead of /{ and %
Firstly open all folds
zj - go to the start of the next open fold
]z - go to the end of the current fold.
The rest of the macro is the same.
This method is more general but I found it less reliable since it depend on all folds open, folding enable and so on.
There's a vim-sort-folds plugin that looks specially made for this.

How to unwrap text in Vim?

I usually have the tw=80 option set when I edit files, especially LaTeX sources. However, say, I want to compose an email in Vim with the tw=80 option, and then copy and paste it to a web browser. Before I copy and paste, I want to unwrap the text so that there isn't a line break every 80 characters or so. I have tried tw=0 and then gq, but that just wraps the text to the default width of 80 characters. My question is: How do I unwrap text, so that each paragraph of my email appears as a single line? Is there an easy command for that?
Go to the beginning of you paragraph and enter:
v
i
p
J
(The J is a capital letter in case that's not clear)
For whole document combine it with norm:
:%norm vipJ
This command will only unwrap paragraphs. I guess this is the behaviour you want.
Since joining paragraph lines using Normal mode commands is already
covered by another answer, let us consider solving the same issue by
means of line-oriented Ex commands.
Suppose that the cursor is located at the first line of a paragraph.
Then, to unwrap it, one can simply join the following lines up until
the last line of that paragraph. A convenient way of doing that is to
run the :join command designed exactly for the purpose. To define
the line range for the command to operate on, besides the obvious
starting line which is the current one, it is necessary to specify
the ending line. It can be found using the pattern matching the very
end of a paragraph, that is, two newline characters in a row or,
equivalently, a newline character followed by an empty line. Thus,
translating the said definition to Ex-command syntax, we obtain:
:,-/\n$/j
For all paragraphs to be unwrapped, run this command on the first line
of every paragraph. A useful tool to jump through them, repeating
a given sequence of actions, is the :global command (or :g for
short). As :global scans lines from top to bottom, the first line
of the next paragraph is just the first non-empty line among those
remaining unprocessed. This observation gives us the command
:g/./,-/\n$/j
which is more efficient than its straightforward Normal-mode
counterparts.
The problem with :%norm vipJ is that if you have consecutive lines shorter than 80 characters it will also join them, even if they're separated by a blank line. For instance the following example:
# Title 1
## Title 2
Will become:
# Title 1 ## Title 2
With ib's answer, the problem is with lists:
- item1
- item2
Becomes:
- item1 - item2
Thanks to this forum post I discovered another method of achieving this which I wrapped in a function that works much better for me since it doesn't do any of that:
function! SoftWrap()
let s:old_fo = &formatoptions
let s:old_tw = &textwidth
set fo=
set tw=999999 " works for paragraphs up to 12k lines
normal gggqG
let &fo = s:old_fo
let &tw = s:old_tw
endfunction
Edit: Updated the method because I realized it wasn't working on a Linux setup. Remove the lines containing fo if this newer version doesn't work with MacVim (I have no way to test).

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

Resources