Vim getline() equivalent for current display/screen line? - vim

Are there any vim functions, similar to getline(), that can access the current display line of a wrapped line of text in a buffer? Alternatively, are there any functions similar to col(), getpos() or getcurpos() that I could use to return the start/end column of the current display/screen line, from within a script?
I realize I could move the cursor around with g0 and g$, and then get positions after such moves. However, I'm specifically looking for a built-in or custom function that does NOT move the cursor. This is being used in a complex operator-pending mapping, so it's best for the cursor to stay put.

After some tinkering, this seemed to be the best available method for finding the bounds of the current screen line, in the absence of other answers. Alas, it does move the cursor, but restores its original position. The function returns a list of the beginning-of-line and end-of-line column indices, like [1, 25].
function! DispLineCols()
"display line columns: [bol, eol] columns for current screen line
let r = []
let p = getcurpos()
normal! g0
call add(r, col('.'))
normal! g$
call add(r, col('.'))
call setpos('.', p)
return r
endfunction
I wrote the following function to return a list of lists (in the same [bol, eol] format indicated above) for all screen lines that the current wrapped line comprises. For example, for a wrapped line that comprises three screen lines, this might return: [[1, 79], [80, 159], [160, 180]]
function! DispLinesCols()
"display lines columns: [[bol1, eol1], [bol2, eol2], ...] columns for
" all screen line wraps of current line
let r = []
let p = getcurpos()
normal! 0
call add(r, DispLineCols())
normal! g$l
while col('.') != r[-1][1]
call add(r, DispLineCols())
normal! g$l
endwhile
call setpos('.', p)
return r
endfunction
Not a perfect (or even elegant) solution, but gets the bounds of the display/screen line well enough. Still looking for better solutions...
EDIT: For completeness, here are two functions that will turn the column [bol, eol] output from the functions above into the actual text of the lines.
function! DispLineText()
"display line text: the text on the current screen line
let [beg, end] = DispLineCols()
return getline('.')[beg-1 : end-1]
endfunction
function! DispLinesText()
"display lines text: list of screen lines comprising the current line
let r = []
let l = getline('.')
for [beg, end] in DispLinesCols()
call add(r, l[beg-1 : end-1])
endfor
return r
endfunction

Related

Resize vim split based on number of visible lines

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.

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

Can you do interactive macros or recordings in vim?

I would like to define a vim macro that breaks for user input at certain times, is this possible?
EDIT: Turns out I ment recordings (q), not macros
It is possible to use the input command in a recording, but it's more trouble than it's worth.
I first mapped insert input escape to a key
:map <F2> a<C-R>=input('input: ')<CR>
then I made this recording in the q register
name:
and pasted it into a new tab
iname: ^[
And after the final escape I pressed <C-V><F2> making the line:
iname ^[^[OQ
That I yanked back to the q buffer then used the macro, letting me use the input function.
It works, but terribly.
Yes. See the function input({prompt}, [, {text} [, {completion}] ]). There is even
inputdialog({prompt} [, {text} [, {cancelreturn}]]), for a dialog popup.
If you use input() inside a mapping or macro, the remaining characters will be taken as input, which is not what you want. Vim offers the inputsave() and inputrestore() functions to temporarily suspend reading from the mapping character stream.
Based on mogelbrod's answer, this doesn't work; the itest is read in as input:
oBEFORE ^R=input('prompt> ')^Mitest
But this does:
function! Input()
call inputsave()
let text = input('prompt> ')
call inputrestore()
return text
endfunction
oBEFORE ^R=Input()^Mitest
Unfortunately, because <C-R> takes an expression, we cannot put the commands inline, but have to define a separate Input() function.
Unfortunately it doesn't seem to be possible. You can trigger input() inside a macro, but continuing on afterwards doesn't seem to be possible as any additional input recorded is inserted into the input prompt.
Yank the line into a named register ("qY) and run it (#q) to try it out.
Note: replace ^R and ^M with Ctrl-V Ctrl-R/M (see :help i_CTRL-V).
oBEFORE ^R=input('prompt> ') - works
oBEFORE ^R=input('prompt> ')^Mitest - works, but inserts itest into the prompt
oBEFORE ^R=input('prompt> ')<CR>test - fails
I have collected information from this and other threads and written this script:
function! MacroInterrupt()
"call inputsave()
if strlen(reg_recording()) == 0
if mode() == 'n'
call inputsave()
let tmp_col = col('.')
let tmp_line = line('.')
let text = input('input:')
let line = getline('.')
call setline('.', strpart(line, 0, col('.') - 1) . text . strpart(line, col('.') - 1))
call cursor(tmp_line, tmp_col + strlen(text))
call inputrestore()
return text
else
call inputsave()
let text = input('input:')
call inputrestore()
return text
endif
else
echo "Interrupt added to macro"
call setreg(reg_recording(), getreg(reg_recording()) . "\<F2>")
"echo getreg("q")
endif
"call inputrestore()
endfunction
map <F2> :call MacroInterrupt() <CR>
inoremap <buffer><expr> <F2> MacroInterrupt()
I hope this can help especially people attempting the same.

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.

Resources