I want to programatically close a fold in vim based on whether or not it matches a regular expression. I've defined a function in my vimrc to do so:
" Support python 2 and 3
if has('python')
command! -nargs=1 Python2or3 python <args>
elseif has('python3')
command! -nargs=1 Python2or3 python3 <args>
else
echo "Error: Requires Vim compiled with +python or +python3"
finish
endif
" Define function
func! FoldCopyrightHeader()
Python2or3 << EOF
import re
import vim
# Look at the first 30 lines
header = '\n'.join(vim.current.buffer[0:30])
pattern = 'Copyright .* by .* THE POSSIBILITY OF SUCH DAMAGE'
match = re.search(pattern, header, flags=re.MULTILINE | re.DOTALL)
if match:
# Find the line number of the block to fold
lineno = header[:match.start()].count('\n')
# Remember the current position
row, col = vim.current.window.cursor
# move cursor to the fold block
vim.command('cal cursor({},{})'.format(lineno, 0))
# close the fold
vim.command('call feedkeys("zc")')
# move back to the original position
vim.command('cal cursor({},{})'.format(row, col))
EOF
endfunc
The idea is to search for the pattern, if it exists, then move to where the pattern is, enter the key commands zc to close the fold, and then move back to your original position.
However, this doesn't quite work. If I call this function via :call FoldCopyrightHeader(), then it closes whatever fold the cursor is currently on, and does nothing else.
My guess is that the feedkeys command is asynchronous vim.command('call feedkeys("zc")') and is happening before/after the move cursor commands get executed.
Is there anything I can do to prevent this?
And I solved it as I was typing the question.
Using vim.command(':foldclose') instead of vim.command('call feedkeys("zc")') seems to do the trick.
Related
I'm trying to make my own snippets in pure vimscript using autocommands. I want to make it so that after I type a specific character sequence, it is replaced by another character sequence. I made one like this, that replaces "hello" with "bye" when you type it.
function F()
if (strpart (getline('.'), 0, col('.')-1) =~ 'hello$')
execute "normal 5i\<Backspace>"
normal! abye
normal! l
endif
endfunction
autocmd TextChangedI *.tex call F()
And it works fine if there are characters after the cursor. However, if I am writing at the end of the line, after the change the cursor is between 'y' and 'e', because the autocommand calls the function, the cursor is at the end of the line, and then it enters insert mode which starts inserting before the last character.
How can I make it so that the cursor is always left after 'bye'? I don't want to use iabbrev for a couple of reasons, one being that it doesn't expand as soon as I type.
I can do it with the option
set ve+=onemore
but I don't like it's effects on normal writing.
How about the following, which uses setline and cursor functions rather than normal keymaps:
function! F() abort
let [l:line, l:col] = [getline('.'), col('.')]
if strpart(l:line, 0, l:col-1) =~ 'hello$'
let l:left = strpart(l:line, 0, l:col-6)
let l:right = strpart(l:line, l:col-1)
call setline('.', l:left . 'bye' . l:right)
call cursor('.', l:col-2) " 2 = length diff between hello and bye
endif
endfunction
This seems working for me (on Neovim 0.6).
I have implemented a command in vim which pastes the result of a calculation into your file, i.e. you type
:CalP 34 * 89
and it should paste the result after your cursor.
The code is as follows:
command! -nargs=+ CalP :call Calculator(<q-args>) | normal! p
py from math import *
fun Calculator(arg)
redir #"
execute "py print " a:arg
redir END
let #" = strpart(#", 1)
endfun
This works but is messier than I would like for a simple operation, mainly because:
I don't know a better way to redirect the output of py print ... to the " register
I have to write execute "py print " a:arg because just py print a:arg doesn't work
The let #" = strpart(#", 1) removes the stray newline at the front of the register which py print creates, ideally this should be removed
I think this should be do-able in one line but I don't know enough vimscript.
No scripting is needed for this. In insert mode, you can use <Ctrl-R>=34*89<CR> to insert the result of that calculation.
:help i_CTRL-R
:help expression
I'll second #Amadan's suggestion. If you prefer Python over Vimscript, you can use the pyeval() function, e.g. directly from insert mode:
<C-R>=pyeval('34 * 89')<CR>
If you would like to keep your custom command, that's possible, too:
command! -nargs=+ CalP execute 'normal! a' . pyeval(<q-args>) . "\<Esc>"
Often times when reviewing log files in vim, I'll highlight interesting lines using marks. At some point, I'd like to be able to copy all of the interesting lines (either all marked lines, or a list of marks) to either a register or another file (it doesn't really matter which; the goal is to facilitate writing a summary). I haven't been able to find any built in way to do this; is it possible in vim?
I suppose it's probably a fairly straightforward function; probably looking something like this, but my vimscript abilities are very weak:
for cur_mark in list_of_marks
goto mark
yank current line and append to register
Has anyone ever written anything similar that they can point me to?
Thanks
EDIT: I posted the accepted solution at https://github.com/mikeage/vim-yankmarks
As always, there are few things that are more motivating than asking for help. Here's what I came up with; feedback welcome.
function! Yankmark()
let save_cursor = getpos(".")
let n = 0
" I should really make this a parameter...
let marks_to_yank="abcdefghijklmnopqrstuvwxyz"
let nummarks = strlen(marks_to_yank)
" Clear the a register
let #a=''
while n < nummarks
let c = strpart(marks_to_yank, n, 1)
" Is the mark defined
if getpos("'".c)[2] != 0
" using g' instead of ' doesn't mess with the jumplist
exec "normal g'".c
normal "Ayy
endif
let n = n + 1
endwhile
call setpos('.', save_cursor)
endfunction
Mikeage had a great idea; here's a more refined version of his function turned into a command:
":YankMarks [{marks}] [{register}]
" Yank all marked (with [a-z] / {marks} marks) lines into
" the default register / {register} (in the order of the
" marks).
function! s:YankMarks( ... )
let l:marks = 'abcdefghijklmnopqrstuvwxyz'
let l:register = '"'
if a:0 > 2
echohl ErrorMsg
echomsg 'Too many arguments'
echohl None
return
elseif a:0 == 2
let l:marks = a:1
let l:register = a:2
elseif a:0 == 1
if len(a:1) == 1
let l:register = a:1
else
let l:marks = a:1
endif
endif
let l:lines = ''
let l:yankedMarks = ''
for l:mark in split(l:marks, '\zs')
let l:lnum = line("'" . l:mark)
if l:lnum > 0
let l:yankedMarks .= l:mark
let l:lines .= getline(l:lnum) . "\n"
endif
endfor
call setreg(l:register, l:lines, 'V')
echomsg printf('Yanked %d line%s from mark%s %s',
\ len(l:yankedMarks),
\ len(l:yankedMarks) == 1 ? '' : 's',
\ len(l:yankedMarks) == 1 ? '' : 's',
\ l:yankedMarks
\) . (l:register ==# '"' ? '' : ' into register ' . l:register)
endfunction
command! -bar -nargs=* YankMarks call <SID>YankMarks(<f-args>)
A different way of accomplishing this might be using the :global command. The global command takes the form :g/{pattern}/{cmd}. The command, {cmd}, will be executed on all lines matching {pattern}.
Append lines matching a pattern to a register:
:g/pattern/yank A
Append matching line to a log file:
:g/pattern/w >> file.log
Of course if you want to find line matching a mark you can match it in your pattern. The following pattern matches a line with mark m.
:g/\%'m/w >> file.log
To do something like this. (Note: I am using \v to turn on very magic)
:g/\v(%'a|%'b|%'m)/yank A
Of course if a pattern won't work you can do this by hand. Instead of marking the lines just build up the lines as you go. Just yank a line to an uppercase register to append.
"Ayy
Or do a write append with a range of a single line
:.w >> file.log
For more help see
:h :g
:h :w_a
:h /\%'m
:h /\v
You can do something like:
:redir #a
:silent marks XYZN
:redir END
"ap
That way the output of the :marks command will be redirected to the a register. Note, that it will only lists (in the above case) the X, Y, Z and N marks (as the arguments), and if there was an a register, it will be deleted/overwritten.
Also note, that it might not give the desired output, but gives you a starting point...
I like the solution from Mikeage, though I would probably solve this with the multiselect - Create multiple selections and operate plugin. This also has the benefit that you don't run out of marks.
With the plugin, you can select lines with <Leader>msa or :MSAdd. Finally, yank all lines with:
:let #a=''
:MSExecCmd yank A
If you use an upper-case register name when yanking into a specific register, Vim will append the yanked content instead of overwriting the register's value.
So, for example:
"ayy - yank current line to register a, overwriting
[move]
"Ayy - append this line to register a
[move]
"ap - paste all yanked material
See :help quotea for more details.
Typically Vim's global command :g// works on per line basis. Is it possible to make it work on per occurrence basis as there could be more than one occurrence on a line.
Not a direct answer, but you could use something like :rubydo, which will run some ruby scriptlet per line of code. Combining that with gsub in ruby should get you the ability to do just about anything per occurrence of a match. Of course, you will need to do it with ruby code, which may not give you access to everything that you might need without hassle (register appending would be annoying for instance)
:[range]rubyd[o] {cmd} Evaluate Ruby command {cmd} for each line in the
[range], with $_ being set to the text of each line in
turn, without a trailing <EOL>. Setting $_ will change
the text, but note that it is not possible to add or
delete lines using this command.
The default for [range] is the whole file: "1,$".
You can try:
command! -bang -nargs=1 -range OGlobal
\ <line1>,<line2>call s:Global("<bang>", <f-args>)
function! s:Global(bang, param) range
let inverse = a:bang == '!'
" obtain the separator character
let sep = a:param[0]
" obtain all fields in the initial command
let fields = split(a:param, sep)
" todo: handle inverse
let l = a:firstline
while 1
let l = search(fields[0], 'W')
if l == -1 || l > a:lastline
break
endif
exe fields[1]
endwhile
endfunction
Which you can use as :global, except that the command name is different (and that the bang option as not been implemented yet)
I have a log file with backspace characters in it (^H). I'm looking through the file in Vim and it can be quite hard to see what's going on.
Ideally I'd like to be able to "apply" all the ^H on a given line/range so that I can see the final result.
I'd much rather do this within Vim on a line-by-line basis, but a solution which converts the whole file is better than nothing.
Turn on the 'paste' option (using :set paste), and then press dd i <CTRL-R> 1 <ESC> on each line that you want to apply the backspaces to. This also works if you delete multiple lines, or even the whole file.
The key here is that you are using <CTRL-R> 1 in insert mode to 'type out' the contents of register 1 (where your deleted lines just got put), and 'paste' option prevents Vim from using any mappings or abbreviations.
I googled this while trying to remember the command I had used before to `apply' backspaces, and then I remembered it: col -b - here is the manpage. (It does a little more and comes from BSD or more exactly AT&T UNIX as the manpage says, so if you are on Linux you may need to install an additional package, on debian its in bsdmainutils.)
Simplistic answer:
:%s/[^^H]^H//g
where ^^H is:
Literal ^ character
Ctrl-V Ctrl-H
and repeat it couple of times (until vim will tell you that no substitutions have been made
If you want without repetition, and you don't mind using %!perl:
%!perl -0pe 's{([^\x08]+)(\x08+)}{substr$1,0,-length$2}eg'
All characters are literal - i.e. you don't have to do ctrl-v ... anywhere in above line.
Should work in most cases.
All right, here is a bare-metal solution.
Copy this code into a file named crush.c:
#include <stdio.h>
// crush out x^H sequences
// there was a program that did this, once
// cja, 16 nov 09
main()
{
int c, lc = 0;
while ((c = getchar()) != EOF) {
if (c == '\x08')
lc = '\0';
else {
if (lc)
putchar(lc);
lc = c;
}
}
if (lc)
putchar(lc);
}
Compile this code with your favorite compiler:
gcc crush.c -o crush
Then use it like this to crush out those bothersome sequences:
./crush <infilename >outfilename
Or use it in a pipeline ("say" is a speech-to-text app on the Mac)
man date | ./crush | say
You can copy crush to your favorite executable directory (/usr/local/bin, or some such) and then reference it as follows
man date | crush | say
Just delete all occurrences of .^H (where . is the regex interpretation of .):
:s/.^H//g
(insert ^H literally by entering Ctrl-V Ctrl-H)
That will apply to the current line. Use whatever range you want if you want to apply it to other lines.
Once you done one :s... command, you can repeat on another line by just typing :sg (you need to g on the end to re-apply to all occurrences on the current line).
How about the following function? I've used \%x08 instead of ^H as it's easier to copy and paste the resulting code. You could type it in and use Ctrl-V Ctrl-H if you prefer, but I thought \%x08 might be easier. This also attempts to handle backspaces at the start of the line (it just deletes them).
" Define a command to make it easier to use (default range is whole file)
command! -range=% ApplyBackspaces <line1>,<line2>call ApplyBackspaces()
" Function that does the work
function! ApplyBackspaces() range
" For each line in the selected lines
for index in range(a:firstline, a:lastline)
" Get the line as a string
let thisline = getline(index)
" Remove backspaces at the start of the line
let thisline = substitute(thisline, '^\%x08*', '', '')
" Repeatedly apply backspaces until there are none left
while thisline =~ '.\%x08'
" Substitute any character followed by backspace with nothing
let thisline = substitute(thisline, '.\%x08', '', 'g')
endwhile
" Remove any backspaces left at the start of the line
let thisline = substitute(thisline, '^\%x08*', '', '')
" Write the line back
call setline(index, thisline)
endfor
endfunction
Use with:
" Whole file:
:ApplyBackspaces
" Whole file (explicitly requested):
:%ApplyBackspaces
" Visual range:
:'<,'>ApplyBackspaces
For more information, see:
:help command
:help command-range
:help function
:help function-range-example
:help substitute()
:help =~
:help \%x
Edit
Note that if you want to work on a single line, you could do something like this:
" Define the command to default to the current line rather than the whole file
command! -range ApplyBackspaces <line1>,<line2>call ApplyBackspaces()
" Create a mapping so that pressing ,b in normal mode deals with the current line
nmap ,b :ApplyBackspaces<CR>
or you could just do:
nmap ,b :.ApplyBackspaces<CR>
Here's a much faster Awk filter that does the same:
#!/usr/bin/awk -f
function crushify(data) {
while (data ~ /[^^H]^H/) {
gsub(/[^^H]^H/, "", data)
}
print data
}
crushify($0)
Note that where ^^H appears, the first caret in ^^H is a caret (shift-6) and the second caret with H is entered (into vim) by typing CTRL-v CTRL-H
Here's a Bash-based filter you can use to process the whole file:
#!/bin/bash
while read LINE; do
while [[ "$LINE" =~ '^H' ]]; do
LINE="${LINE/[^^H]^H/}"
done
echo "$LINE"
done
Note that where ^H appears, it is entered into vim using CTRL-v CTRL-h, and the ^^H is entered as SHIFT-6 CTRL-v CTRL-h.