Check if a selection exists in vimscript - vim

I want to write a function in vimscript that echoes the selected text or, if no text is selected, the entire buffer.
How can I distinguish between these two cases?

Define two mappings, an :nmap using the entire buffer, and a :vmap for the selected text. Both can invoke the same function, passing an isVisual boolean flag or a mode argument.
Anything else (custom commands, direct function :call) would require an explicit hint, because in order to invoke them, visual mode as already been left (for command-line mode). You also cannot use the '<,'> marks for the detection, because they will keep the last selection even after it has been removed.

I write a function to get visually selected text.
I hope it can help you.
function! GetSelected()
" save reg
let reg = '"'
let reg_save = getreg(reg)
let reg_type = getregtype(reg)
" yank visually selected text
silent exe 'norm! gv"'.reg.'y'
let value = getreg(reg)
" restore reg
call setreg(reg,reg_save,reg_type)
return value
endfun
" viusal map
vnoremap gs :<C-U>echo GetSelected()<CR>
" normal map
nnoremap gs :<C-U>echo join(getline(1, '$'), "\n")<CR>

Related

Vim copy and paste line with a search and replace

Say I've written code that references the x dimension. What is the best way to get vim to duplicate a line of code replacing all references to x to y and to z (best being the most clear method).
Input:
length_x = X_vec.dot(X_vec)**.5
Desired Output:
length_x = X_vec.dot(X_vec)**.5
length_y = Y_vec.dot(Y_vec)**.5
length_z = Z_vec.dot(Z_vec)**.5
Here's my best so far.
function SwitchXtoYZ()
:normal yy
:normal p
:normal! V
:s/X/Y/ge
:normal! V
:s/x/y/ge
:normal p
:normal! V
:s/X/Z/ge
:normal! V
:s/x/z/ge
endfunction
command XtoYZ exec SwitchXtoYZ() | :normal `.
It works, but I feel this is not very vim-y. Bonus points if the cursor returns to where it was before the command XtoYZ was issued (it currently goes the beginning of the second inserted line).
You don't need a function to do that, a macro would be fine for your requirement. Also you can define a macro in your vimrc too, if you like, so that you can have it everytime you open vim.
here is the macro:
qqv<Esc>Y2p:s/x/y/gi<Enter>n:s//z/gi<Enter>`<q
so it was recorded and saved in register q, you can #q to replay it.
explain it a little:
qq " start recording into q
v<esc> " enter visual mode and exit. to let `< work
Y2p " yank current line and paste twice below
:s/x/y/gi<Enter> " x->y sub, case insensitive
n " go to next x (here we could use j too)
:s//z/gi<Enter> " do another sub x->z
`< " back to the old cursor position
q " end recording
if you want to X->Y and x->y, just remove the i flag and add two more :s
The : at the beginning of each line is optional, as are the :normal! V lines.
You are leveraging the Normal commands that you know, which is a good way to start, but IMHO you get cleaner code if you use more Command-mode (ex) commands and functions. I would do something like this:
function! SwitchXtoYZ()
let save_cursor = getpos(".")
copy .
s/X/Y/ge
s/x/y/ge
-copy .
s/X/Z/ge
s/x/z/ge
call setpos('.', save_cursor)
endfun
command! XtoYZ call SwitchXtoYZ()
:help function-list
:help getpos()
:help :call
:help :exec

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.

How to ignore space after comments when calculating indent level in Vim

Consider writing a JavaDoc-style comment which includes an indented list (when expandtab is set and softtabstop=2):
/**
* First line:
* - Indented text
*/
Currently, after typing First line: and hitting return, Vim will correctly insert *<space>. However, when I hit tab to indent the second line, only one space will be inserted instead of two.
Is it possible to fix this, so the space after * will be ignored during indent calculations?
I am still a beginner at VimScript, but I cooked this up for you. Give it a try and let me know what you think.
function AdjustSoftTabStop()
" Only act if we are in a /* */ comment region
if match(getline('.'), '\s*\*') == 0
" Compensate for switching out of insert mode sometimes removing lone
" final space
if match(getline('.'), '\*$') != -1
" Put back in the space that was removed
substitute/\*$/\* /
" Adjust position of the cursor accordingly
normal l
endif
" Temporary new value for softtabstop; use the currect column like a
" base to extend off of the normal distance
let &softtabstop+=col('.')
endif
endfunction
function ResetSoftTabStop()
" Note that you will want to change this if you do not like your tabstop
" and softtabstop equal.
let &softtabstop=&tabstop
endfunction
" Create mapping to call the function when <TAB> is pressed. Note that because
" this is mapped with inoremap (mapping in insert mode disallowing remapping of
" character on the RHS), it does not result in infinite recursion.
inoremap <TAB> <ESC>:call AdjustSoftTabStop()<CR>a<TAB><ESC>:call ResetSoftTabStop()<CR>a

vim - set auto indent to fill the leading space with space or tabstop

It seems if we enable 'ai', vim will fill the the leading space with tabstop.
I can make it fill with just space with 'et'. I don't like a C file mixed with space and tabstop.
My vimrc:
set ts=4 et
set ai
set hlsearch
syntax on
filetype plugin indent on
autocmd FileType make setlocal noexpandtab
However, in some condition I do need to input tabstop when I hit the 'TAB' on keyboard, for example, in makefile and some others.
The 'autocmd FileType' command is not good: I can't add every file type in vimrc.
What I want is simple:
autoindent to fill leading area with
space;
when hit 'TAB' on keyboard, tabstop
input, not space (so no 'et')
How to do it?
inoremap <expr> <tab> ((getline('.')[:col('.')-2]=~'\S')?("\<C-v>\t"):(repeat(' ', &ts-((virtcol('.')-1)%&ts))))
It does the same as #Lynch answer if I read it correctly.
You can also use <C-v><Tab>: this will insert <Tab> without invoking any mappings and ignores expandtab unless you remapped <C-v> or <C-v><Tab> for some reason.
If you want to just insert tab do
inoremap <Tab> <C-v><Tab>
It will ignore expandtab setting.
I did it using a function. I tested it, but maybe in some particular case you will have to fix some bugs. Try adding this to your vimrc:
set et
function! Inserttab()
let insert = ""
let line = getline('.')
let pos = getpos('.')[2]
let before = ""
let after = line
if pos != 1
let before = line[ 0: pos - 1]
let after = line[pos : strlen(line) ]
endif
if pos != 1 && substitute(before, "[ \t]", "", "g") != ""
let insert = "\t"
else
let insert = " "
endif
let line = before . insert . after
call setline('.', line)
call cursor(line('.'), strlen(before . insert))
endfunction
inoremap <tab> <esc>:call Inserttab()<CR>a
Basicaly it does remap your key in visual mode to the function Inserttab(). Also note that if you change ts for something other than 4 it will still output 4 spaces instead of two because the value is hard coded.
Also im not very familiar with vim scripts, but I think all the variables used will be global which is a bad thing.
I forgot to mention that to "see" white spaces you can use set list. You disable this with set nolist. Also in normal mode you can use ga to see information about the character your cursor is on.
Edit
I realise that you may want to insert tab at the beginin of the line. My script insert space at the begining and tab anywhere else.
If you really want a tab every time you hit tab key you could simply use this:
set et
function! Inserttab()
let insert = ""
let line = getline('.')
let pos = getpos('.')[2]
let before = ""
let after = line
if pos != 1
let before = line[ 0: pos - 1]
let after = line[pos : strlen(line) ]
endif
let insert = "\t"
let line = before . insert . after
call setline('.', line)
call cursor(line('.'), strlen(before . insert))
endfunction
inoremap <tab> <esc>:call Inserttab()<CR>a
But I dont see the point, with this version you will never be able to indent manually from insert mode.
One way to do it is
:set sw=4 (or whatever you want)
:set ts=46 (or some large number)
Then autoindent will not insert tabs unless you reach 46 spaces, in which case you can put in a higher number.
Only drag about this is if someone else is using tabs, then you have to reset ts to agree with the file you are editing. On the other hand, it will make the tabs immediately obvious, which can be desirable as well.

Get the current line in visual mode from a function

I have a simple vim script that takes a visual block of text and stores it as a list. The problem with the function VtoList() is that it executes after the cursor returns to the start of the visual block, not before it. Because of this, I have no way of getting the line where the visual block ends.
nn <F2> :call VtoList()<CR>
func! VtoList()
firstline = line('v') " Gets the line where the visual block begins
lastline = line('.') " Gets the current line, but not the one I want.
mylist = getline(firstline, lastline)
echo mylist
endfunc
The problem is with line('.'). It should return the current line of the cursor, but before the function is called, the cursor has already returned to the start of the visual block. Thus, I'm only getting a single line instead of a range of lines.
I put together a solution that sets a mark everytime the user hits V and sets another mark before the function is called.
nnoremap V mV
nnoremap <F2> mZ:call VtoList()<CR>
The function works fine if I substitute line('v') and line('.') with line("'V") and line("'Z"), but I want to avoid this solution if I can because it could conflict with a user's mappings.
Is there a way I can get current line of a visual block within a function before the cursor has returned to the start of the visual block?
Don't use :, use <expr>:
function! s:NumSort(a, b)
return a:a>a:b ? 1 : a:a==a:b ? 0 : -1
endfunction
func! VtoList()
let [firstline, lastline]=sort([line('v'), line('.')], 's:NumSort')
let mylist = getline(firstline, lastline)
echo mylist
return ""
endfunc
vnoremap <expr> <F2> VtoList()
Note other changes: let (you forgot it), sort (line where selection starts may be after the line where selection ends), vnoremap (line("v") works only in visual mode), return (expr mappings return value is executed, but you don't need it, you need only side effects). You can replace the second line with
if mode()=~#"^[vV\<C-v>]"
let [firstline, lastline]=sort([line('v'), line('.')], 's:NumSort')
else
let [firstline, lastline]=sort([line("'<"), line("'>")], 's:NumSort')
endif
The reason why your solution is not working is that when : occurs in the mapping, you immediately exit visual mode and enter command mode. line("v") works only in visual mode.
Other note: vnoremap {lhs} : will produce command line already filled with '<,'>. You may have added range to the function definition and use let [firstline, lastline]=sort([a:firstline, a:lastline], 's:NumSort'). But you nevertheless will exit visual mode with :.

Resources