Table of contents with Fold in Vim - vim

How can I unfold only the folds containing a fold, to get an outline of my document?
If everything is folded and I press zr a few times I get something close to what I want, except that if parts have different depths I'm either not seeing some folds or seeing some content.
In this example:
# Title {{{1
# Subtitle {{{2
some code here
# Another Title {{{1
code here directly under the level 1 title
I would like to see this when folded:
# Title {{{1
# Subtitle {{{2
# Another Title {{{1

That's not trivial; I've solved this with a recursive function that determines the level of nesting, and then closes the innermost folds.
" [count]zy Unfold all folds containing a fold / containing at least
" [count] levels of folds. Like |zr|, but counting from
" the inside-out. Useful to obtain an outline of the Vim
" buffer that shows the overall structure while hiding the
" details.
function! s:FoldOutlineRecurse( count, startLnum, endLnum )
silent! keepjumps normal! zozj
if line('.') > a:endLnum
" We've moved out of the current parent fold.
" Thus, there are no contained folds, and this one should be closed.
execute a:startLnum . 'foldclose'
return [0, 1]
elseif line('.') == a:startLnum && foldclosed('.') == -1
" We've arrived at the last fold in the buffer.
execute a:startLnum . 'foldclose'
return [1, 1]
else
let l:nestLevelMax = 0
let l:isDone = 0
while ! l:isDone && line('.') <= a:endLnum
let l:endOfFold = foldclosedend('.')
let l:endOfFold = (l:endOfFold == -1 ? line('$') : l:endOfFold)
let [l:isDone, l:nestLevel] = s:FoldOutlineRecurse(a:count, line('.'), l:endOfFold)
if l:nestLevel > l:nestLevelMax
let l:nestLevelMax = l:nestLevel
endif
endwhile
if l:nestLevelMax < a:count
execute a:startLnum . 'foldclose'
endif
return [l:isDone, l:nestLevelMax + 1]
endif
endfunction
function! s:FoldOutline( count )
let l:save_view = winsaveview()
try
call cursor(1, 0)
keepjumps normal! zM
call s:FoldOutlineRecurse(a:count, 1, line('$'))
catch /^Vim\%((\a\+)\)\=:E490:/ " E490: No fold found
" Ignore, like zr, zm, ...
finally
call winrestview(l:save_view)
endtry
endfunction
nnoremap <silent> zy :<C-u>call <SID>FoldOutline(v:count1)<CR>

Related

Search for word under cursor and invoke function on key press VimScript

Hi I am currently trying to reformat python 2 code that was written using camelCase style variables and I need to convert them to snake_case.
I've written two small vimscript functions to aid in doing so. I would like to this one step further. I would like to on a single keystroke to search for the word under my cursor, and invoke my function FindAndReplace which will take the current search term and do the processing as opposed to me having to manually type in the search term using input.
function! SplitDelim(expr, pat)
let result = []
let expr = a:expr
while 1
let [w, s, e] = matchstrpos(expr, a:pat)
if s == -1
break
endif
call add(result, s ? expr[:s-1] : '')
call add(result, join(['', tolower(w)], '_'))
let expr = expr[e:]
endwhile
call add(result, expr)
return join(result, '')
endfunction
function! FindAndReplace()
" get current cursor position to keep screen constant
let cur_cursor_pos = getpos('.')
call inputsave()
let g:search_term = input("Enter search term: ")
call inputrestore()
execute '%s' . '/' . g:search_term . '/' . SplitDelim(g:search_term, '[A-Z]') . '/'
" set cursor back to where it was at start of invocation from execing s/
call setpos('.', cur_cursor_pos)
endfunction
Examples
The contents of some file
fooBarBaz
invoking the function
call FindAndReplace()
pass search term through input
fooBarBaz
resultant file now reads
foo_bar_baz.
What I want
Open some file whose content is
fooBarBaz (place cursor over word)
press ctrl-q and the contents of the file becomes
foo_bar_baz
I've determined the solution to my question.
expand('<cword>') will return the current word under your cursor. The keybinding was a simple nnoremap.
Full solution
function! SplitDelim(expr, pat)
let result = []
let expr = a:expr
while 1
let [w, s, e] = matchstrpos(expr, a:pat)
if s == -1
break
endif
call add(result, s ? expr[:s-1] : '')
call add(result, join(['', tolower(w)], '_'))
let expr = expr[e:]
endwhile
call add(result, expr)
return join(result, '')
endfunction
function! FindAndReplace()
" get current cursor position to keep screen constant
let cur_cursor_pos = getpos('.')
let search_term = expand('<cword>')
execute '%s' . '/' . search_term . '/' . SplitDelim(search_term, '[A-Z]') . '/'
" set cursor back to where it was at start of invocation from execing s/
call setpos('.', cur_cursor_pos)
endfunction
nnoremap <C-Q> :call FindAndReplace()<CR>

Efficient way to delete line containing certain text in vim with prompt

At present i can search for text
/text
and then delete line using dd and if i don't want to delete i can go for next match with n.
But is there any more fast way to do that!
This command below deletes all the lines containing text, but the problem is that it deletes all lines at once, sometimes that text is in some line that is exception.
:g/text/d
But i want something simple like like
:%s/text/some_other_text/gc
because this gives the option to substitute or not to.
You don't need a global command for this. The substitute command in by itself will suffice by
adding a wildcard
and adding an end-of-line.
example
%s/.*text.*\n//gc
You can mix :help global and :help substitute:
:g/text/s/.*\n//c
This will ask for confirmation before deleting every line containing text:
I've tried to found a way to use global and :substitute, and that correctly handles matches on consecutive lines, and matches on the first line, but alas, I'm not inspired.
So, I'm back to my basics: I've implemented what I think is missing: :confirm global.
The result has been pushed in my library plugin.
How it works:
I prepare a stateful variable that remembers the previous user choice when it matters (always, or quit, or last).
I execute global on the pattern, and for each match I check what the user wishes to do.
I either use the don't-ask-again states
or I ask using the StatusLineNC highlight group with echo "\rmessage" + :redraw. This is a very old trick we used to do even before Vim 6 IIRC.
The related code is the following:
" Function: lh#ui#ask(message) {{{3
function! lh#ui#ask(message) abort
redraw! " clear the msg line
echohl StatusLineNC
echo "\r".a:message
echohl None
let key = nr2char(getchar())
return key
endfunction
" Function: lh#ui#confirm_command(command) {{{3
" states:
" - ask
" - ignore
" - always
function! s:check() dict abort
if self.state == 'ignore'
return
elseif self.state == 'always'
let shall_execute_command = 1
elseif self.state == 'ask'
try
let cleanup = lh#on#exit()
\.restore('&cursorline')
\.restore_highlight('CursorLine')
set cursorline
hi CursorLine cterm=NONE ctermbg=black ctermfg=white guibg=black guifg=white
let choice = lh#ui#ask(self.message)
if choice == 'q'
let self.state = 'ignore'
let shall_execute_command = 0
" TODO: find how not to blink
redraw! " clear the msg line
elseif choice == 'a'
let self.state = 'always'
let shall_execute_command = 1
" TODO: find how not to blink
redraw! " clear the msg line
elseif choice == 'y'
" leave state as 'ask'
let shall_execute_command = 1
elseif choice == 'n'
" leave state as 'ask'
let shall_execute_command = 0
elseif choice == 'l'
let shall_execute_command = 1
let self.state = 'ignore'
endif
finally
call cleanup.finalize()
endtry
endif
if shall_execute_command
execute self.command
endif
endfunction
function! s:getSID() abort
return eval(matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_getSID$'))
endfunction
let s:k_script_name = s:getSID()
function! lh#ui#make_confirm_command(command, message) abort
let res = lh#object#make_top_type(
\ { 'state': 'ask'
\ , 'command': a:command
\ , 'message': a:message . ' (y/n/a/q/l/^E/^Y)'
\ })
call lh#object#inject_methods(res, s:k_script_name, 'check')
return res
endfunction
" Function: lh#ui#global_confirm_command(pattern, command, message [, sep='/']) {{{3
" Exemple: to remove lines that match a pattern:
" > call lh#ui#global_confirm_command(pattern, 'd', 'delete line?')
function! lh#ui#global_confirm_command(pattern, command, message, ...) abort
let cmd = lh#ui#make_confirm_command(a:command, a:message)
let sep = get(a:, 1, '/')
exe 'g'.sep.a:pattern.sep.'call cmd.check()'
endfunction
" Function: lh#ui#_confirm_global(param) {{{3
function! lh#ui#_confirm_global(param) abort
let sep = a:param[0]
let parts = split(a:param, sep)
if len(parts) < 2
throw "Not enough arguments to `ConfirmGlobal`!"
endif
let cmd = join(parts[1:])
call lh#ui#global_confirm_command(parts[0], cmd, cmd . ' on line?', sep)
endfunction
command! -nargs=1 ConfirmGlobal call lh#ui#_confirm_global('<args>')
From here you could either type:
:call lh#ui#global_confirm_command(pattern, 'd', 'delete line?')
or :ConfirmGlobal/pattern/d which generates a less instructive prompt
The most efficient way is to combine :glboal and :norm
:g/test/norm dd

Vim function toggle replace character under cursor

I'm trying to change "X" to " " and vice versa to mark a checkbox in a markdown file in normal mode:
- [X] Zucchini
- [ ] Nutmeg
Here's what I've tried:
First
function! ToggleComplete()
if getline('.')[col('.')-1] == 'X'
return ' '
else
return 'X'
endif
endfunction
nnoremap <C-x> :call ToggleComplete()<CR>
Second
function! ToggleComplete()
if getline('.')[col('.')-1] == 'X'
return '\r\<Space>'
else
return '\rX'
endif
endfunction
nnoremap <C-x> :call ToggleComplete()<CR>
It really can't work like this; the main reason is how you use the return statement : your function returns a space or an X char, but the returned value is never used, and is lost when you use call ToggleComplete(). Actually there's nothing in your code that changes the content of your buffer.
A secondary point: your if test is very restrictive; it requires your cursor to be exactly on the right char in the line, in order to work (because of [col('.')-1]). Maybe it's what you want, but you may also add some flexibility by using a test which works without depending on the cursor column.
The following is one possibility of doing what you want:
function! ToggleComplete()
" Get current line:
let l:line = getline('.')
" Get the char to test with the help of a pattern, ' ' or 'X':
" \zs and \ze lets you retrieve only the part between themselves:
let l:char = matchstr(l:line, '\[\zs.\ze]')
" Invert the value:
if l:char == 'X'
let l:char = ' '
else
let l:char = 'X'
endif
" Replace the current line with a new one, with the right
" char substituted:
call setline(line('.'), substitute(l:line, '\[\zs.\ze]', l:char, ''))
" Please note that this last line is doing the desired job. There is
" no need to return anything
endfunction

Fold up to the fold start, instead of the indent start

I use foldmethod=indent and when I fold code like this:
def cake():
#cake!
print( "cake" )
print( "for" )
print( "you" )
I see
def cake():
#cake!
print( "cake" ) +++ 3 lines folded
but I want to see
def cake(): +++ 5 lines folded
Is there a way to do fold up to the first line (def cake():) like this?
Chapters 48 and 49 of Learn Vimscript the Hard Way talk about how to do that, using foldmethod=expr instead of indent. Basically you need to make a custom ftplugin and put a folding script in it; the script contains functions used to determine what fold level different lines should have.
As luck would have it, the example code given in those two chapters is for the Potion language which, like Python, is whitespace-sensitive, so it should be pretty easy to adapt it to Python. Since Vim already comes with a Python ftplugin, I think you can put the folding script described on the site into .vim/after/ftplugin/python instead of .vim/ftplugin/potion.
I solved this using this tutorial.
This is the finished bunch of functions:
fu! Indent_level(lnum)
return indent(a:lnum) / &shiftwidth
endfunction
fu! Next_non_blank_line(lnum)
let numlines = line('$')
let current = a:lnum + 1
while current <= numlines
if getline(current) =~? '\v\S'
return current
endif
let current += 1
endwhile
return -2
endfunction
fu! Custom_fold_expr(lnum)
if getline(a:lnum) =~? '\v^\s*$'
return '-1'
endif
let this_indent = Indent_level(a:lnum)
let next_indent = Indent_level(Next_non_blank_line(a:lnum))
if next_indent == this_indent
return this_indent
elseif next_indent < this_indent
return this_indent
elseif next_indent > this_indent
return '>' . next_indent
endif
endf
set foldexpr=Custom_fold_expr(v:lnum)
foldmethod=expr
Please don't edit the indentation of the "end" markers on this post, it looks gorgeous after you put this in your vimrc.

How to resize a window to fit, taking into account only logical lines?

I'm looking to write a function that I can call from a map. The idea is to resize a window to fit the buffer contents. This isn't too difficult:
fu! ResizeWindow(vert) "{{{
if a:vert
let longest = max(map(range(1, line('$')), "virtcol([v:val, '$'])"))
exec "vertical resize " . (longest+4)
else
exec 'resize ' . line('$')
1
endif
endfu "}}}
I would, however, like the function to take logical lines into account when calculating the height (I'm not overly worried about width).
For example, a line that has wrapped (due to :set wrap) would count as two or more lines. A block of 37 lines that are folded would only count as one.
Does anyone know of a convenient way of getting this 'logical line count' without having to try and calculate it manually? If I do need to do this manually are there any other cases I'm missing that would cause a line to be represented with a potentially different number of lines?
For anyone interested, I gave up trying to find a simple solution to this. Below is the code I ended up with. It takes into account a couple of obvious edge cases; I'm sure others remain. Suggestions for improvement are very welcome.
fu! Sum(vals) "{{{
let acc = 0
for val in a:vals
let acc += val
endfor
return acc
endfu "}}}
fu! LogicalLineCounts() "{{{
if &wrap
let width = winwidth(0)
let line_counts = map(range(1, line('$')), "foldclosed(v:val)==v:val?1:(virtcol([v:val, '$'])/width)+1")
else
let line_counts = [line('$')]
endif
return line_counts
endfu "}}}
fu! LinesHiddenByFoldsCount() "{{{
let lines = range(1, line('$'))
call filter(lines, "foldclosed(v:val) > 0 && foldclosed(v:val) != v:val")
return len(lines)
endfu "}}}
fu! AutoResizeWindow(vert) "{{{
if a:vert
let longest = max(map(range(1, line('$')), "virtcol([v:val, '$'])"))
exec "vertical resize " . (longest+4)
else
let line_counts = LogicalLineCounts()
let folded_lines = LinesHiddenByFoldsCount()
let lines = Sum(line_counts) - folded_lines
exec 'resize ' . lines
1
endif
endfu "}}}

Resources