Resize vim split based on number of visible lines - vim

I am trying to write a function in my vimrc which lets me create a small split which looks as though it is inside the current file but is, in fact, a different file.
To do this cleanly, I want this split to appear immediately below the cursor which means I need to resize my splits appropriately. Unfortunately, I cannot find a way to measure the number of visible lines between my cursor and the top of the window. In files where I have folded code, the approach of using line(".") - line("w0") ("." gives line number of cursor; "w0" gives line number of topmost visible line) does not work since this calculation includes the hidden lines inside the folds. Does anybody know how this could be achieved?
Images for reference before and after inserting the split:

line(".") - line("w0")
gives you the number of physical lines between the top of the window and the current line. As you figured out, this method doesn't account for folds.
You can count the number of folds within a range with something like this:
function! CountFolds(top_line, bottom_line)
let folded_lines = []
for line_nr in range(a:top_line, a:bottom_line)
if foldlevel(line_nr) == 0
call add(folded_lines, 0)
else
call add(folded_lines, 1)
endif
endfor
return count(uniq(folded_lines), 1)
endfunction
and then remove it from your initial line count:
let top_line = line("w0")
let bottom_line = line(".")
let physical_lines = bottom_line - top_line
let screen_lines = physical_lines - CountFolds(top_line, bottom_line)
Reference:
:help range()
:help foldlevel()
:help add()
:help uniq()
:help count()
Note that you may need your script to account for soft-wrapped lines as well, a topic that is well worth another question.

Related

Assign variable to spelllang in vim script

im trying to create a vim function to cycle through different spelllang.
let g:SpellLanglist= ["en_us", "es"]
let s:lang_index=0
function! SpellLangCycle()
let l:lang=get(g:SpellLanglist, eval(g:lang_index))
set spelllang = l:lang
let g:lang_index = s:lang_index + 1
endfunction
nnoremap <leader>s :call SpellLangCycle()<CR>
I dont know how to assign lang variable to spelllang. set spelllang = l:lang is not working.
Thank you in advance
You should use:
let &spelllang = l:lang
You can set and read contents of an option (such as spellang) by using the & prefix. (The set command doesn't work with dynamic contents or local variables, unless you use execute to evaluate a string as a command, but that's much inferior to using & to refer to it.)
See :help :let-option for more details on using let with an option.
Your script has other issues as well. If you're going to track which language index was last set, you should probably do so in a per-buffer variable, since 'spelllang' is a per-buffer option.
You can use get() to read that variable but use a default variable if it's unset, which might be really helpful for the first time this code is run in a specific buffer.
You can access an item from the list with a simple [...] containing the index.
You also need to "cycle" the variable back to zero once it reaches the last element of the list. You can do so using the % operator with the length of the list.
Putting it all together:
let g:SpellLanglist= ["en_us", "es"]
function! SpellLangCycle()
let l:lang_index = get(b:, 'lang_index', -1)
let l:lang_index = (l:lang_index + 1) % len(g:SpellLanglist)
let &spelllang = g:SpellLanglist[l:lang_index]
let b:lang_index = l:lang_index
endfunction
nnoremap <leader>s :call SpellLangCycle()<CR>
But I think you can do even better. You don't need an external variable to store the index into the list, you can just look at the current &spelllang and find it in the list to find the currently set index.
You can do that with the index() function. It also returns -1 if the item is not found in the list, which works for us since after we increment it we'll get to the first item on the list.
function! SpellLangCycle()
let l:lang_index = index(g:SpellLanglist, &spelllang)
let l:lang_index = (l:lang_index + 1) % len(g:SpellLanglist)
let &spelllang = g:SpellLanglist[l:lang_index]
endfunction
One advantage of this approach is that this will always cycle to the next language in the list, even if you set 'spelllang' outside of this function... You can still use it to pick up from the point you set it to, or it will restart from the first one if what you set it to is not on the list.

In vim, how to split a word and flip the halves? FooBar => BarFoo

I sometimes write a multi-word identifier in one order, then decide the other order makes more sense. Sometimes there is a separator character, sometimes there is case boundary, and sometimes the separation is positional. For example:
$foobar becomes $barfoo
$FooBar becomes $BarFoo
$foo_bar becomes $bar_foo
How would I accomplish this in vim? I want to put my cursor on the word, hit a key combo that cuts the first half, then appends it to the end of the current word. Something like cw, but also yanking into the cut buffer and then appending to the current word (eg ea).
Nothing general and obvious comes to mind. This is more a novelty question than one of daily practical use, but preference is given to shortest answer with fewest plugins. (Hmm, like code golf for vim.)
You can use this function, it swaps any word of the form FooBar, foo_bar, or fooBar:
function! SwapWord()
" Swap the word under the cursor, ex:
" 'foo_bar' --> 'bar_foo',
" 'FooBar' --> 'BarFoo',
" 'fooBar' --> 'barFoo' (keeps case style)
let save_cursor = getcurpos()
let word = expand("<cword>")
let match_ = match(word, '_')
if match_ != -1
let repl = strpart(word, match_ + 1) . '_' . strpart(word, 0, match_)
else
let matchU = match(word, '\u', 1)
if matchU != -1
let was_lower = (match(word, '^\l') != -1)
if was_lower
let word = substitute(word, '^.', '\U\0', '')
endif
let repl = strpart(word, matchU) . strpart(word, 0, matchU)
if was_lower
let repl = substitute(repl, '^.', '\L\0', '')
endif
else
return
endif
endif
silent exe "normal ciw\<c-r>=repl\<cr>"
call setpos('.', save_cursor)
endf
Mapping example:
noremap <silent> gs :call SwapWord()<cr>
Are you talking about a single instance, globally across a file, or generically?
I would tend to just do a global search and replace, e.g.:
:1,$:s/$foobar/$barfoo/g
(for all lines, change $foobar to $barfoo, every instance on each line)
EDIT (single occurrence with cursor on the 'f'):
3xep
3xep (had some ~ in there before the re-edit of the question)
4xea_[ESC]px
Best I got for now. :)
nnoremap <Leader>s dwbP
Using Leader, s should now work.
dw : cut until the end of the word from cursor position
b : move cursor at the beginning of the word
P : paste the previously cut part at the front
It won't work for you last example though, you have to add another mapping to deal with _ .
(If you don't know what Leader is, see :help mapleader)

Jump to the end of a long list of repeated pattern

I have a big file with a lot of lines that share the same pattern, something like this:
dbn.py:206 ... (some other text) <-- I am here
dbn.py:206 ... (some other text)
...
(something I don't know) <-- I want to jump here
Is there a quick way in Vim to jump to the place where the succession of dbp.py:206 ends?
/^\(dbn.py\)\#!
Matches first line which does not start with the text inside the escaped parentheses.
If you want quick access to this you could add a vmap which yanks the visually selected text and inserts it in the right spot (but first escaping it with escape(var, '/').
Try this vmap: vmap <leader>n "hy<Esc>/^\(<C-R>=escape(#h,'/')<CR>\)\#!<CR>
Press n when visually selecting the text you wish to skip and you should be placed on the next first line which does not begin with the selection.
I just write a function to select identical lines:
nnoremap vii :call SelectIdenticalLines()<CR>
fun! SelectIdenticalLines()
let s = getline('.')
let n = line('.')
let i = n
let j = n
while getline(i)==s && i>0
let i-=1
endwhile
while getline(j)==s && j<=line('$')
let j+=1
endwhile
call cursor(i+1, 0)
norm V
call cursor(j-1, 0)
endfun
type vii to select identical lines (feel free to change the key-binding)
type zf to fold them.
type za to toggle folding
It's handy when you want to squeeze several empty line.
It acts like C-x C-o in emacs.
One option is to go to the bottom of the file and search backwards for the last line you want, then go down one:
G ?^dbn\.py:206?+1

Context sensitivity in commentstring

When setting folds manually, it would be handy if it were possible to set the commentstring in a context sensitive manner. Consider the case in which the language uses BCPL-style comment markers (i.e., comments begin with // and are terminated by a newline), the first line of the visual block contains a comment, and the last line does not. Currently, if commentstring is set to //%s, redundant // characters will be appended to the first line when zf is used to create a fold.
Is it possible to set commentstring so that the // characters are added only if they do not already appear on the line?
According to :help fold-create-marker, automatic folding marker insertion
does not work properly when:
The line already contains a marker with a level number. Vim then doesn't
know what to do.
Folds nearby use a level number in their marker which gets in the way.
The line is inside a comment, commentstring isn't empty and nested
comments don't work. For example with C: adding /* {{{ */ inside
a comment will truncate the existing comment.
Thus, it is not possible to change the default zf behavior by modifying the
commentstring setting.
However, it is possible to create a custom version of the zf command that
would take into consideration that starting or ending (or both) lines of
a fold could have comments. For example, consider the following mappings, one
for marking a fold by visual selection, another for using with a motion
command.
nnoremap <silent> <leader>zf :set opfunc=CreateMarkerFold<cr>g#
vnoremap <silent> <leader>zf :<c-u>call CreateMarkerFold(visualmode(), 1)<cr>
function! CreateMarkerFold(vt, ...)
let range = map(['[<', ']>'], 'line("''".v:val[a:0])')
let mark = split(&foldmarker, ',')
let pat = escape(&commentstring, '\')
let pat = '\V' . substitute(pat, '\\\#<!%s', '\\zs\\ze\\.\\{-}', '')
for i in [1, 0]
let line = getline(range[i])
if line =~ pat
let line = substitute(line, pat, escape(mark[i], '\'), '')
else
let line .= printf(&commentstring, mark[i])
endif
call setline(range[i], line)
endfor
endfunction
Both of the mappings follow the same routine. Before adding starting and
ending folding markers, it checks separately whether the first and the last
lines of the block to fold match the commentstring pattern. For each of the
two that do match, it inserts the corresponding marker inside the first found
comment, at the very beginning of its text. Otherwise, the marker is
decorated according to the commentstring template and added at the end of
the line.
If in the latter case it is preferable to separate the marker on its own line,
one can change the for loop as shown below.
for i in [1, 0]
let line = getline(range[i])
if line =~ pat
let line = substitute(line, pat, escape(mark[i], '\'), '')
call setline(range[i], line)
else
call append(range[i] - !i, printf(&commentstring, mark[i]))
endif
endfor
Unlike the previous version of the loop, the order of processing the two
lines is important: The ending marker line should be added first, if
necessary, because inserting a line for the beginning marker would shift the
following lines changing their numbers.

How to repeatedly add text on both sides of a word in vim?

I have a bunch of local variable references in a Python script that I want to pull from a dictionary instead. So, I need to essentially change foo, bar, and others into env['foo'], env['bar'] and so on. Do I need to write a regular expression and match each variable name to transform, or is there a more direct approach that I could just repeat with the . command?
You can use a macro: type these commands in one go (with spacing just to insert comments)
" first move to start of the relevant word (ie via search)
qa " record macro into the a register.
ienv['<esc> " insert relevant piece
ea'] " move to end of word and insert relevant piece
q " stop recording
then, when you're on the next word, just hit #a to replay the macro (or even ## to repeat the last replay after that).
There's an easier way - you can use a regex search and replace. Go into cmdline mode by typing a colon and then run this command:
%s/\\(foo\|bar\|baz\\)/env['\1']/
Replacing foo, bar, and baz with whatever your actual variable names are. You can add as many additional variables as you'd like, just be sure to escape your OR pipes with a backslash. Hope that helps.
you could write a function that would do this pretty well, add this to your .vimrc file:
function! s:surround()
let word = expand("<cword>")
let command = "%s/".word."/env[\'".word."\']/g"
execute command
endfunction
map cx :call <SID>surround()<CR>
This will surround every occurance of the word currently under the cursor.
If you wanted to specify what went before and after each instance you could use this:
function! s:surround()
let word = expand("<cword>")
let before = input("what should go before? ")
let after = input("what should go after? ")
let command = "%s/".word."/".before.word.after."/g"
execute command
endfunction
map cx :call <SID>surround()<CR>
If you only want to confirm each instance of the variable you could use this:
function! s:surround()
let word = expand("<cword>")
let before = input("what should go before? ")
let after = input("what should go after? ")
let command = "%s/".word."/".before.word.after."/c"
execute command
endfunction
map cx :call <SID>surround()<CR>
I figured out one way to do what I need. Use q{0-9a-zA-Z"} to record key strokes into a buffer. Position the cursor at the begging of the variable name, then cw and type env['']. Next move the cursor back one space to the last quote and paste the buffer filled from the cw command with P. Finally, reuse the recording with #{0-9a-z".=*} for each variable.

Resources