VIM: Restore position after undo of a function - vim

For my LaTeX editing in vim, I recorded a handful of useful macros and wrapped them into functions/command. I have one, that change the type of a Latex environment e.g., when I have:
\begin{itemize}
\item First
\item Second
\end{itemize}
I just enter :ChangeEnv enumerate with the cursor somewhere in the environment to change from itemize to enumerate.
The code in my ftplugin/tex.vim looks like this:
function! ChangeEnv(newenv)
let l:save = #e
let #e = a:newenv
let l:line = getline('.')
" Fake change to restore undo
normal ix
normal x
if match(l:line, '\\begin{') != -1
normal mz_%f{lci}e'zf{l.`z:delma z
else
normal my?\\begin{^M_mz%f{lci}^Re^['zf{l.`y:delma yz
endif
let #e = l:save
endfunction
command -nargs=1 ChangeEnv :silent call ChangeEnv(<f-args>)
The first part (after if match(...) intended if the cursor is on the \begin{...} part of the environment works perfectly so far, I can make the change and undo it, cursor stays where it should.
The second part, intended for inside the environment, also works great, but when the change is undone, the cursor jumps to the first charachter of the \begin line.
The normal ix and normal x part is intended to ensure the cursor position is restored after the und (I have this from here: Restor Cursor Position)
My question is, why doesn't it work for the second macro? Is there some error?
To spare you deconstructing the macro, this are the steps:
my - set y mark at current position
?\\begin{^M - Search backward for the beginning of the environment and jump there
_mz - go to first character of that line and set the z mark
% - Jump to the matching \end{... of the environment (This is part of the matchit vim plugin, delivered with vim but not active per default).
f{l- Jump forward to { and one character right
ci} - Change inner {...}
^Re^[ - Insert the content of the e register, where the new environment name is stored and return to normal mode
'z' - Jump to the line beginning of the z mark (The \begin{...)
f{l. - Forward to {, one step right and repeat the last change
`y - Jump to the y mark, the initial position
:delma yz - Delete the y and z mark
The undo behavior is not a deal breaker, nevertheless, I'd at least like to know why it behaves that way.
Thank you in advance.

Usually, when you make multiple changes, each one is undone separately. But inside a function, everything is lumped together as one single change.
Your intention of doing this dummy change at the beginning (when the cursor hasn't yet been moved) is that the cursor returns to that point after undo.
However, I think as Vim treats the whole set of changes done within ChangeEnv() as one big modification, it returns to the beginning of the range of changed lines (mark '[), as it does when undoing built-in commands. Which change command was executed "first" doesn't matter, all that matters is the range of lines that got changed. As your second branches searches backwards to the \begin and makes a change there, that's where the cursor will be after undo. I don't think there's a way around this; undo cannot be influenced by scripts, as this could potentially introduce inconsistencies.
What you could do is setting a mark after doing the changes (at the end of your function: :normal! m'), so that you can quickly jump back there (via `` or <C-o>).

Related

How to prevent vim substitute from moving the cursor

The company I work for uses special headers for source files, which contain the date of last modification.
I wrote a vim script that updates this date automatically on each buffer write.
I am using the search/substitue feature to do the trick.
Now the problem is that the replace does move the cursor at the beginning of the file, which is very annoying because at each buffer write, the user has to jump back manually to the previous editing position.
Does anyone know a way to prevent vim from jumping when updating the date, or at least to make it jump back to the previous position?
Interactively, you can use <C-O> or the change mark `` to move back to the original position, as shown at vim replace all without cursor moving.
In a Vimscript, especially when this runs on every buffer write, I would wrap the code:
let l:save_view = winsaveview()
%substitute///
call winrestview(l:save_view)
The former move commands might still affect the window view (i.e. which exact lines and columns are shown in the viewport), whereas this solution restores everything as it was.
Bonus stuff
Also, be aware that the {pattern} used in the :substitute is added to the search history. To avoid that, append
call histdel('search', -1)
(The search pattern itself isn't affected when you have this in a :function.)
or use the :keeppatterns command introduced in Vim 8:
keeppatterns %substitute///
Related plugins
I've implemented similar functionality in my AutoAdapt plugin. You'll see all these tricks used there, too.
I think using the function substitute() will keep your changelist and jumplist. In neovim I am using a function to change the date of file modification this way:
M.changeheader = function()
-- We only can run this function if the file is modifiable
local bufnr = vim.api.nvim_get_current_buf()
if not vim.api.nvim_buf_get_option(bufnr, "modifiable") then
require("notify")("Current file not modifiable!")
return
end
-- if not vim.api.nvim_buf_get_option(bufnr, "modified") then
-- require("notify")("Current file has not changed!")
-- return
-- end
if vim.fn.line("$") >= 7 then
os.setlocale("en_US.UTF-8") -- show Sun instead of dom (portuguese)
local time = os.date("%a, %d %b %Y %H:%M:%S")
local l = 1
while l <= 7 do
vim.fn.setline(l, vim.fn.substitute(vim.fn.getline(l), 'last (change|modified): \\zs.*', time , 'gc'))
l = l + 1
end
require("notify")("Changed file header!")
end
end

Can vim markers work with the repeat key?

I regularly use the . key to repeat my last vim command. I'm wondering if there's a way to use it with commands performed using markers. Here's a simple example:
Mark a line of text using m'a
Move down a few lines, e.g. 5j
Indent the lines using >'a -- indents 6 lines
Press . to repeat above command on the same 6 lines -- will only affect 1st line
Is there a way to get vim to apply the same marker range of the previous command when using the . command?
ps. I know the above example would have been easier with 6>>, which works with the ., I'm just using it as an example. I often use markers for more complex/longer commands.
The problem is that the command you're repeating is >'a, but because of the rule "After applying the operator the cursor is mostly left at the start of the text that was operated upon" (from :help operator), >'a leaves the cursor at mark a if that was above your starting position. Then when you repeat >'a you end up with a linewise motion from that line to itself, which only re-indents one line.
Since you're left at the beginning of the block of text, to affect the same block of text again you can use the '] (linewise) or `] (charwise) motions, which move to the end of the block of text just affected. So you can use >'] to indent the same block again, and since your cursor starts and ends at the same position this time, you can repeat it additional times with ..
A simpler choice, though, would be to just use V5j> instead of ma5j>'a. V starts visual mode (linewise) at your current position, 5j moves down 5 lines, and then > indents. And when you repeat using ., the same visual selection still pertains, so you get the desired result whether you moved up or down.

How to delete line(s) below current line in vim?

Is there a command to delete a line (or several lines) that is immediately below current line?
Currently I'm doing it as:
jdd and then . to repeat as needed.
Is there a command that would combine all these?
UPDATE: The reason I would like to have such command is that I don't like to move away from current position, yet be able to delete lines below.
The delete ex command will work nicely.
:+,$d
This will delete all the lines from current +1 till the end ($)
To delete the next 2 lines the follow range would work, +1,+2 or shorthand +,+2
:+,+2d
As #ib mentioned the :delete or :d command will move the cursor to the start of the line next to the deleted text. (Even with nostartofline set). To overcome this we can issue the `` normal mode command. `` will jump back to the exact position before the last jump, in this case the :d command. Our command is now
:+,+2denter``
Or as one ex command
:+,+2d|norm! ``
To make this easier we wrap this all up in a command:
command! -count=1 -register D :+,+<count>d <reg><bar>norm! ``
Now to delete the next following 3 lines:
:3D
This command can also take a {reg} like :delete and :yank do. So deleting the next 4 lines into register a would be:
:4D a
For more information
:h :d
:h :command
:h :command-register
:h :command-count
:h ``
dG should work.
This means delete all rows until end of file from current cursor.
This will delete ALL lines below the current one:
jdG
Unfortunately that will move the cursor to the beginning of current line after the deletion is made.
well, to do it simply you could use the xxdd command. Most of the time I know (at least have an idea) the size of the script I am editing. So, the command as below is usually more than enough :
99dd
999dd to remove 999lines starting at the cursor position.
9999dd
99999dd for very long script ;)
The other solutions are informative, but I feel it'd be simpler to use a macro for this:
qq (begins recording)
jddk (go down, delete the line, and go back up - i.e. the thing you want to do)
q (end recording)
Now you can do #q to perform this action, maintaining the cursor at the current position. You could also do something like 5#q to delete 5 lines below the cursor.
And finally, if you're repeating the action more than once, you could just type ## after the first time you run #q (this repeats the last used macro - in this case q)
This is a job for marks!
Try maj20dd`a
ma sets the file-specific mark 'a', j20dd does the deletion you want (20 lines in this case), and `a restores you to the mark's position (line and column).
Obviously this pattern can be extended to do anything you want before returning to the mark. If you use mA (or any other capital letter) the mark will actually be unique across files, so you can even edit elsewhere before returning. If you have a very frequent usage you could make it a macro as suggested above.
You could enter the number of lines to delete: j 20 dd k.
Just for the fun of it, you can define a little function that does
exactly what you described: deletes the next n lines below the
current line and restores the initial cursor position.
function! DeleteNextLines(n, reg)
let l = line('.')
let m = min([a:n, line('$')-l])
if m > 0
let c = col('.')
exe '+,+'.m 'd' a:reg
call cursor(l, c)
endif
endfunction
Also, you can define a command that accepts the number of lines
to delete (one, if omitted) and the register name to use as an
optional argument (just like the :delete command).
:command! -range=1 -register -bar D call DeleteNextLines(<count>, <q-reg>)
Additionally, you can define a mapping for triggering the above
:D command, if it is necessary.

Fast switching between two lines in file in vim

sometimes I have to switch between 2 lines. To do this, I have to pass line number. Are there shortcuts to moving between 2 last 'visited' list. Something similar to '. (it moves to last edited line)
You can use bookmarks, something like ma to mark a bookmark named a, and then 'a to go to it. Naming boomarks a and b for example, you can switch between them with 'a and 'b.
Double-backtick or double single-quote will do exactly that
There is a command that will bounce to the last location you jumped away from. If repeated, it will then return to the jump destination you were just on. If repeated further it will bounce you back and forth between the two locations.
The command has two forms:
``
or
''
ctrl-o and ctrl-i will move backwards and forwards through your history of jump points. A jump point is pretty much any command that moves the cursor more than one line. It will also go back to previous files that were loaded into the current buffer.
See :h jump-motions for more information.

Prepending a character followed by the line number to every line

I'm hand-editing CNC Gcode text files and need a way to reference locations in the file and on the toolpath.
I want to modify every line in the text file so that it begins with the the upper case letter N followed by the line number, incremented in tens for each successive line, then a whitespace followed by the original text on that line. How can I do this in Vim?
I'm not sure about vi, but (since you're using the vim tag) Vim allows you to accomplish your task as follows:
Adjust the first line by hand (insert a N10 at the beginning of the line), then put the cursor at the beginning of the next line.
Press qb to start recording a macro (the b names the register used to store the macro; feel free to use a different letter -- and definitely do use a different letter if you've got something useful stashed away in b).
Move the cursor upward to the beginning of the previous line (which you have adjusted by hand). Press v to start visual selection mode, then f to move the cursor to the next space on the line (if you use a single space as your whitespace separator, that is; adjust this step if you're using a tab or multiple spaces).
Press y to yank the selected text. This will also remove the visual selection.
Move the cursor to the beginning of the next line. Press P to insert the previously yanked text before the cursor, that is, on the very beginning of the line.
Move the cursor to the numeric part of the line header. Press 10 C-a (1, 0, control + A) to increment that number by 10.
Move the cursor to the beginning of the next line. Press q to stop recording the macro.
Press 10000000 #b to execute the macro 10000000 times or until it hits the end of the file. This should be enough to take care of all the lines in your file, unless it is really huge, in which case use a bigger number.
...or use Vim to write a simple script to do the job in whichever language you like best, then run it from a terminal (or from withing Vim with something like :!./your-script-name). ;-)
The following command will prepend ‘N<line number * 10>’ to every line:
:g/^/exe 'normal! 0iN' . (line('.')*10) . ' '
You can do it easily in Vim with this:
:%s/^/\=line(".")*10 . " "/
This replaces the start of every line with the result of an expression that gives the line number times ten, followed by a space.
I have not timed it, but I suspect it might be noticeably faster than the other Vim solutions.
Cheating answer:
:%!awk '{print "N" NR "0", $0}'
There are two ways to implement that without resorting to external
tools: via a macro or by using Vimscript. In my opinion, the first way
is a little cumbersome (and probably not as effective as the solution
listed below).
The second way can be implemented like this (put the code into your
.vimrc or source it some other way):
function! NumberLines(format) range
let lfmt = (empty(a:format) ? 'N%04d' : a:format[0]) . ' %s'
for lnum in range(a:firstline, a:lastline)
call setline(lnum, printf(lfmt, lnum, getline(lnum)))
endfor
endfunction
The NumberLines function enumerates all lines of the file in a given
range and prepends to each line its number according to the provided
printf-format (N%04d, by default).
To simplify the usage of this function, it is convenient to create
a command that accepting a range of lines to process (the whole file,
by default) and a optional argument for the line number format:
command! -range=% -nargs=? NumberLines <line1>,<line2>call NumberLines([<f-args>])

Resources