Vimscript function to select block then sort - vim

I'm learning Vimscript and I'm trying to select a block of text and sort it via a function. Here's what I got right now:
function! SortFirstBlock()
call setpos(".", [0, 1, 1, 0])
execute "normal! vip:sort<cr>"
endfunction
How I read this is to go to the first position in the buffer via setpos, then with execute, run vip which visually selects a block of text, then :sort to sort that block.
My cursor goes to the first position, but doesn't highlight the block and doesn't sort. What is the proper way to do this?

Almost there, all that's missing is a backslash before <CR>:
function! SortFirstBlock() abort
call cursor(1, 1)
execute "normal! vip:sort\<CR>"
endfunction
<> tokens must be inside a double-quoted string and escaped. The documentation for all such special tokens is at :h expr-string.
Two unrelated suggestions, code review style:
You can use cursor() as a shorthand for setpos(".", ...).
Always use abort to make a function stop as soon as it encounters an error. If you don't use abort Vim will try to continue executing even after an error and you'll get a long and confusing stacktrace.

What's wrong with your function:
setpos() is too verbose, cursor() is simpler,
execute is not necessary because you don't concatenate anything,
you should separate macros and ex commands:
normal! vip
sort
normal! <Esc>
but visual mode is totally unnecessary here anyway.
You should use :sort over the desired range:
function! SortFirstBlock()
1,/^$/-sort
endfunction
Well, I would simply use :1,/^$/-sort<CR> and don't bother with a function, actually… but your example may be part of a larger whole.

I’ve upvoted glts’ answer but I thought I’d offer an alternative solution.
Instead of selecting before sorting, you can simply sort from the position you just moved to (the start of the file) to the line just before the end of the current paragraph:
function! SortFirstBlock()
call setpos(".", [0, 1, 1, 0])
,'}-1sort
endfunction
Nothing before the comma means the start of the range is the current line.
The '} is a predefined mark which indicates the end of the paragraph. The
-1 moves the end-point of the range to be sorted back by one line so that
the blank line at the end of the paragraph isn’t included in the sort. This
range covers the same lines that would be included with a vip in Normal
mode.

Related

How to use ":g/regex/d_" without losing current cursor position

In Vim, if you run
:g/some_word/d_
all lines containing "some_word" are removed and the cursor automatically jumps to the place of last deletion. I want the cursor to stay where it was before the operation. How do I accomplish this?
You can't make the cursor stay but you can make it go back to where it was:
:g/some_word/d_|norm ''
The answer by romainl is a good one, and it should do the trick without any additional fuss.
If this is a common problem, and you'd rather not add the additional norm! '' at the end (or type the key sequence when you're done), you could encapsulate it in a command:
command! -nargs=* G call s:G(<q-args>)
function! s:G(args)
let saved_position = winsaveview()
exe 'g'.a:args
call winrestview(saved_position)
endfunction
The :G command will invoke the s:G function with all of its arguments, which saves the position, runs the normal g with these exact arguments, and then restores it. So you'd do :G/some_word/d_ and it would run the command and restore the cursor in the line/column where it started.
Obviously, this only makes sense if you use it often enough and you don't often work on bare Vims (like on remote servers and such). Otherwise, it might be a better idea to try romainl's suggestion, or get used to typing in ''. Your choice.
Also, if the current position happens to be after some of these lines, the cursor might end up in an unexpected place. For example, if your cursor is on line 7 and the command deletes line 3, then you'll be back at line 7, but all the text will have shifted up one line, so you'll be in the "wrong" place. You could probably play around with this function and compensate for the change, but it'll get pretty complicated pretty fast, so I wouldn't recommend it :)
The anwolib plugin provides a handy :KeepView command; you can apply this to any Ex command, so it's even more generic than the :G command suggested by #AndrewRadev:
:KeepView g/some_word/d_

How to select first paragraph of file without moving cursor in Vimscript?

With the answers from a previous question, I've put toegther a little function to help me insert language pragmas into a source code file:
function! HaskellInsertLanguagePragma() abort
let here = getpos(".")
let prag = input("GHC pragma: ")
call append(0, "{-# LANGUAGE " . prag . " #-}")
call setpos(".", [0, 1, 1, 0])
,'}-sort
,'}-Tabularize /#-}$/
call setpos(".", here)
endfunction
Basically, it asks you for a keyword, then inserts it, along with the boilerplate into the first line of the file, sorts, it, and lines up the closing delimiter using the Tabular plugin. Awesome.
The problem with the function is that, because a line is inserted, the setpos at the end puts the cursor at the line previous to when it started now. The obvious solution is to setpos with the line number incremented by one, but I wonder whether either of the following two solutions might be better:
Set a mark at the beginning of the function and go to that mark at the end. If so, how to set and move to marks within Vimscript?
Don't move the cursor at all, and do the ,'}-sort and ,'}-Tabular using explicit ranges. But how do I specify the range "first paragrpah of the file"?
To me it seems that being able to select the first paragraph of the file is the better approach. Appreciate any help in figuring out how to do this.
In Vimscript, I normally set mark before, and move back to after.
function! MyFunction()
" Save our location.
normal! mz
...
" Move back to our original location.
normal! `z
endfunction
#Andy Ray answered your first question very nicely. About the second one, unfortunately, even using explicit ranges, it seems that sort function using a range changes the position of the cursor. How to specify a paragraph with a range? You can use
1;'}-sort
1;'}-Tabularize /#-}$/
When separated with ; the cursor position will be set to that line
before interpreting the next line specifier, so you do not need to move cursor position to the first line (check :h :,). Or you can use a pattern
1;/^$/-sort
1;/^$/-Tabularize /#-}$/
^$ means blank line, which usually ends paragraph. The / after pattern is required for separation.
:h [range]
:h sort

Returning to Normal Mode from Insert Mode after "execute normal"

You can perform normal mode commands programmatically in Ex mode, via execute normal, e.g.
:execute "normal" "iNEWTEXT\<Esc>0"
This switches to insert mode (i), writes "NEWTEXT", escapes to normal mode (\< Esc>), then moves to the start of the line (0).
However, using a non-constant string, either a register or variable, the behavior is different. For example, suppose you have the same command above saved on a line in any file (not necessarily a vimscript file):
iNEWTEXT\<Esc>0
You can then copy the text into any register (here, z) via "zy$ and execute the register via #z. This time, though, the output is different:
NEWTEXT\<Esc>0
After entering insert mode, the Escape is no longer treated as a special character, and is instead taken literally. Alternative forms like \e don't work either. Is there a way around this?
EDIT: Using Ingo's answer, I created the the following function. Basically, the use is for having a set of normal/insert commands embedded within the text of the file, and being able to execute them. More commonly, something similar is used for running Ex commands from a line of text, but I couldn't find anything that did this exact thing for normal and insert mode.
So, you'd have text like the following in your file:
jy10j10jpO\<Esc>jEll
When on that line, you could call the function or a remap, and the commands would execute (in this example, copying and pasting 10 lines, and moving 2 columns past the first word). Ingo's alternatives are better for serious usage, namely sourcing commands from another file, having the command in the .vimrc, or a file-type specific option. Macros saved by a session would work just as well, and are more practical than having commands scattered throughout a file. In my case, I was syncing across multiple devices, and didn't want to have another file or clutter my vimrc with this very specific command, but didn't mind cluttering this specific file itself. Think of this like a portable macro.
" Execute current line as Vim normal mode commands.
nnoremap <A-y> :call EvaluateLineAsNormalModeCmd()<CR>
function! EvaluateLineAsNormalModeCmd()
let g:getCurrentLine = getline(".")
"have to :execute twice: once to get the contents of the
"register inserted into a double-quoted string, and then once for
"the :normal to evaluate the string.
execute 'execute "normal" "' . g:getCurrentLine . '"'
endfunction
EDIT2/3: Here are two functions using Christian Brabandt's answer. They work about the same but can put the user in insert mode at the end (whereas, based on my minimal information, 'i' in the other context is considered an incomplete command and not executed, and :startinsert can't be used in that situation). PS: Please don't ask me what all those single and double quotes are doing, as I can't wrap my head around it O_o
function! EvaluateLineAsNormalModeCmd()
normal! 0y$
execute ':call feedkeys("'.#".'", "t")'
endfunction
function! EvaluateLineAsNormalModeCmd()
let g:getCurrentLine = getline(".")
execute ':call feedkeys("'.g:getCurrentLine.'", "t")'
endfunction
If you really need this (the use case is dubious), you have to :execute twice: once to get the contents of the register inserted into a double-quoted string, and then once for the :normal to evaluate the string.
:execute 'execute "normal" "' . #z . '"'
PS: Please give more background; what is your final goal? When a question is only about a small technical step, it's difficult to provide a good answer. If you don't tell us why you want this, it's easy to succumb to the XY problem.
I would rather use the feedkeys() function. E.g. for your sample, this should work:
exe ':call feedkeys("'.#".'", "t")'
(If you yanked your line into the unnamed register, else adjust the register name accordingly). Note, quoting could get ugly.
To understand what is going on, this is what is done:
exe ':call feedkeys(' - First part of the feedkeys() function call
" - Start of Quote for the first argument
. - String concatenation
#" - content of the unnamed register
. - String concatenation
' - Start of second part of the feedkeys function call
" - End of Quote for the first argument
, "t")' - Second argument of feedkeys() function call
You could also do it in 2 steps like this:
exe ':let a="'. #". '"' - Also needs to quote #" correctly.
call feedkeys(a, 't')
which should be easier to understand. The exe call is only to translate the normalized key notation into literal keys.

vim script exec pastes unformated text

In all honesty the title is bad. Consider the following 5 lines:
function Example()
let ## = "-_-"
execute "normal! ]P"
call cursor(line('.'), col('.')-1)
endfunction
When this function is called, I expect to get -_- as output and the cursor should be moved to the left, meaning that it is at the third character, so if I press a key, like I for example I will get -_i-
What happens in reality is quite different (and to some degree interesting)
The output the first time this is called is - _- and after that it's _--
I assume that "cursor" shifts the position of the word under the cursor.
Basically: Why is it happening? How can I get the desired effect ?
Very important edit:
Apperantly the problem isn't in the plugins. When I go for:
call Example()
It works flawlessly. Thing is it is supposed to be triggered by a key. I have currently bound it like so:
inoremap ' <C-O>: call Example()<CR>
So now I am thinking that something in the mapping is broken...
I cannot reproduce your strange behavior. I get ----_-_-_-_- on repeated invocations, as expected. I again suspect there are plugins at work. Try with vim -N -u NONE. As this is a paste, there's little that could influence this function, though. You could try to workaround via :noautocmd call Example(), but I'd rather try to find the root cause of this disconcerting strangeness.
The "-_-" is not a full line, so the ]P (paste with adapted indent) has no effect here. You could just as well have used P.
To move the cursor one left, rather use :normal! h. The subtraction from col('.') again only works for single-byte ASCII characters.

Comment / uncomment multiple fixed lines in vim

In my code I have multiple scattered lines which help me to debug my program and show me what is going on during execution. Is there an easy and fast way to comment and uncomment (toggle) these fixed lines in vim? I thought about marking these lines with a special sign (e.g. //) like this in python:
print "Debug!" # //
and everytime a sepcific shortcut is pressed all lines which end with a "# 'some optional descriptive text' //" are commented or uncommented, respectively.
I've looked at NERD Commenter, but from what I read the lines to be commented / uncommented have to be selected each time?
First, find a pattern that selects the right lines. If you have :set hls, it will help spot the matches. I think something like /#.*\/\/$ is what you want.
Next, comment out the selected lines with
:g/<pattern>/s/^/# /
if # will comment out the line, and un-comment them with
:g/<pattern>/s/^# //
Now, you want a single command to toggle. You can either use a variable to keep track of the toggle state, or you can try to figure out the current state by examining the lines that match. Using a variable seems simpler.
The variable could be global, local to the buffer, or local to the script. I like using script-local variables in order to avoid cluttering the namespace. In this case, using a script-local variable might mean that vim will get confused when you switch buffers, so let's use a buffer-local variable, say b:commentToggle.
The first time the function is called, it notices that the variable is not set, so use search() to look for a line that starts with # (There is a space there!) and ends with the pattern we already have. The n flag means not to move the cursor, and w means to wrap around the end of the file (like searching with 'wrapscan' set). The search() function returns the line number (1-based!) if the pattern is found, 0 if not. See :help search().
This seems to work in a small test:
fun! CommentToggle()
if !exists('b:commentToggle')
let b:commentToggle = !search('^# .*#.*\/\/$', 'nw')
endif
if b:commentToggle == 1
g/#.*\/\/$/s/^/# /
else
g/#.*\/\/$/s/^# //e
endif
let b:commentToggle = !b:commentToggle
endfun
nnoremap <F4> :call CommentToggle()<CR>
If you want to put # in front of the first non-blank, then use ^\s*# in the search() command; s/\ze\S/# / or s/\S/\1# / in the first :g line; and s/^\s*\zs# // in the second :g line. See :help /\zs, :help /\ze, and :help sub-replace-special.

Resources