Is there a way to map key range in VIM? - vim

I would like to map any key to a custom function call. The function will determine what to do based on the specific key pressed, or the last key sequence.
Is there a way to map a key range (e.g., "any key" or "a-z0-9")?

You can't do this with a single mapping. The best you can do is a loop and a bunch of mappings. For example to remap all normal mode lower case characters:
for c in range(char2nr('a'), char2nr('z'))
execute 'nnoremap ' . nr2char(c) . ' :echo " Pressed: ' . nr2char(c) . '"<CR>'
endfor
If you want [a-z0-9] you need to use two range() calls to get the right key codes, since the key codes for the numbers and lowercase letters don't line up (See the ASCII table):
for c in range(char2nr('0'), char2nr('9')) + range(char2nr('a'), char2nr('z'))
execute 'nnoremap ' . nr2char(c) . ' :echo " Pressed: ' . nr2char(c) . '"<CR>'
endfor

If you want to act on characters typed individually in INSERT mode, you can use the InsertCharPre event. For example:
autocmd InsertCharPre * call FilterChar()
function! FilterChar()
if (v:char == 'a')
let v:char = 'ouch'
echo v:char
endif
endfunction
This subverts inserting a by replacing it with ouch, on the fly, while also echoing the same text (which means you can do other stuff there, such as calling a function).

Related

Vim: Archiving text selections into other file

I'd like to create a command and bindings for appending a selected piece of text into another file located next to it with a timestamp.
I've put some scraps together from other posts, yet I barely know what I am doing and I am not getting what I expect.
fun! MoveSelectedLinesToFile(filename)
exec "'<,'>w! >>" . a:filename
norm gvd
endfunc
fun! ArchiveSelectedLinesToFile(filename)
call writefile( ["[" . strftime("%Y-%m-%d %H:%M:%S") . "]"], a:filename, "a" )
call MoveSelectedLinesToFile(a:filename)
call writefile( ["",""], a:filename, "a" )
endfunc
vnoremap a :call ArchiveSelectedLinesToFile(expand('%:p') . '.arc.md')<CR>
using this on a sequence of lines 4-6 of this content:
1
2
3
4
5
6
7
8
9
The archive file shows:
[2022-09-19 14:52:10]
[2022-09-19 14:52:10]
while it should show
[2022-09-19 14:52:10]
4
5
6
and the source file was altered to
1
2
3
8
9
which is one line to much as 7 was wrongfully taken.
I am on Windows, if that means anything.
Q:
I am getting E16 invalid range errors for all 3 lines of ArchiveSelectedLinesToFile. Where from there exactly? And why?
Is there a way to maybe just construct a block of text and append that instead of adding the different bits, like the timestamp and the whitespace one by one? Make it one coherent operation? Maybe only bother the piping-mechanism once?
Why is this so inconsistent? Sometimes for one triggering I get 2 timestamps with no payload into the other file and once it even worked fine.
Is there some easier way of doing archiving in this or a similar way?
I am aware that none of this is probably as clean as it could be, yet I do not know better as of now. Improvement suggestions of form are appreciated.
Background:
I recently started using VimWiki for taking notes, including my TODOs, my call-stack if you will. I have one main TODO file, however large tasks warrant their own file.
Now whenever I am done with a TODO I might have put more notes under the heading of that TODO since its inception, holding valuable information for future me.
Without going to the length of creating an extra wiki entry for the topic, traditionally I just deleted the lines, I've come to think it would be neat to archive most of them away instead. I think using Version Control for this is overkill.
By putting those contents into another file with a timestamp this acts as a sort of history. Further I could still choose between archiving and deleting to decide what might be relevant further and what's not.
:help writefile() takes a list as first argument so a better strategy would be to build the list first, with timestamp and all, and then use a single writefile().
For that, you need to start by handling ranges properly. When you call a function defined like this over a range, the function is called for each line:
function! Foo()
echo line('.')
endfunction
:4,6call Foo()
4
5
6
which is totally fine if that's your goal, but this is not the case here. You want your function to be called once, so you need to define it with the range argument:
function! Foo() range
echo line('.')
endfunction
:4,6call Foo()
4
which allows you to handle the range yourself, within the function:
function! Foo() range
echo a:firstline
echo a:lastline
endfunction
:4,6call Foo()
4
6
See :help :func-range.
First, generate a timestamp:
function! ArchiveSelectedLinesToFile() range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
endfunction
Second, store the selected lines in a variable:
function! ArchiveSelectedLinesToFile() range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
let lines = getline(a:firstline, a:lastline)
endfunction
See :help getline().
Third, put them together in a list:
function! ArchiveSelectedLinesToFile() range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
let lines = getline(a:firstline, a:lastline)
let archival_data = [timestamp] + lines
endfunction
See :help list-concatenation.
Fourth, write the list to the given file:
function! ArchiveSelectedLinesToFile(filename) range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
let lines = getline(a:firstline, a:lastline)
let archival_data = [timestamp] + lines
call writefile(archival_data, a:filename, 'a')
endfunction
Fifth, delete the selected lines:
function! ArchiveSelectedLinesToFile(filename) range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
let lines = getline(a:firstline, a:lastline)
let archival_data = [timestamp] + lines
call writefile(archival_data, a:filename, 'a')
execute a:firstline . ',' . a:lastline . 'd _'
endfunction
See :help :range, :help :d and :help "_.
You are not done yet, though, because of all the text objects that start with a. This will cause timing issues so you will have to map your function call to a different key that is not the start of some command, motion, or mapping. Additionally, you might want to restrict your mapping to visual mode. Here is an example with <F5>, YMMV:
xnoremap <F5> :call ArchiveSelectedLinesToFile(expand('%:p') . '.arc.md')<CR>

Placing cursor after lines appended via append / line

I use the following function to insert a break in comments of the following format:
Break:
# Notes -------------------------------------------------------------------
Function:
" Insert RStudio like section break
function! InsertSectionBreak()
let title = input("Section title: ") " Collect title
let title_length = strlen(title) " Number of repetitions
let times = 80 - (title_length + 1)
let char = "-" " Create line break
let sep_line = repeat(char, times)
let final_string = '\n#' . ' ' . title . ' ' . sep_line " Create final title string
call cursor( line('.')+1, 1)
call append(line('.')+1, final_string) " Get current line and insert string
endfunction
" Map function to keyboard shortcut ';s'
nmap <silent> ;s :call InsertSectionBreak()<CR>
Problem
After performing the operation I would like to place the cursor one line below the created section.
Desired behaviour:
# Notes -------------------------------------------------------------------
<cursor should land here>
Current behaviour
The cursors stays on the current line.
<some code I'm typing when I exit insert mode and call ;s - cursor stays here>
# Notes -------------------------------------------------------------------
<cursor lands here>
As a low-level function, append() is not affected by and also does not affect the cursor position. Therefore, you just need to adapt the cursor() arguments. I would also recommend to only change the cursor at the very end, to make the calculation based on line('.') easier:
function! InsertSectionBreak()
let title = input("Section title: ") " Collect title
let title_length = strlen(title) " Number of repetitions
let times = 80 - (title_length + 1)
let char = "-" " Create line break
let sep_line = repeat(char, times)
let final_string = '#' . ' ' . title . ' ' . sep_line " Create final title string
call append(line('.')+1, ['', final_string]) " Get current line and insert string
call cursor(line('.')+4, 1)
endfunction
Additional notes
The '\n#' string includes a literal \n, not a newline character. For that, double quotes would have to be used. However, even that won't work with append() because it always inserts as one text line, rendering the newline as ^#. To include a leading empty line, pass a List of lines instead, and make the first list element an empty string.
You're using mostly low-level functions (cursor(), append()); you could have used higher-level functions (:normal! jj or :execute lnum for cursor positioning, :put =final_string for adding lines) instead. There would be more side effects (like adding to the jumplist, :put depending on and positioning the cursor already, and having the change marks delimit the added text); usually this is good (but it depends on the use case).
Mappings that interactively query stuff from the user are not very Vim-like. I would have rather defined a custom command (e.g. :Break {title}) that takes the title as an argument, and maybe an additional (incomplete command-line) mapping for quick access :nnoremap ;s :Break<Space>. This way, you could easily repeat the last insertion with the same title via #:, for instance.

How to repeat a simple nonrmap in vim?

I am trying to write slightly enhanced version of VIM’s mark function, which uses the combination of m and any capital letter to mark a file.
What I want to implement is, instead of using only a single letter, set a mark allowing the combination of two letters, so that if I have files named, test_views, test_models, test_forms, I could use tv, tm, and tf, then don’t have to bother what to map to which file.
What I come up with is so far is:
nnoremap <some-prefix>tv :let tv = expand("%")<cr>
nnoremap <leader>tv :execute 'edit' tv<cr>
but couldn’t figure out to write this more effectively. (I shouldn’t repeat this 26 * 26 times, should I?).
Creating function should be one way, but even if I could manage to finish writing this, I don’t think the usability would meet what I expect.
nnoremap , :call StoreFile(k_map)
fun! StoreFile(k_map)
let k_map = expand("%”)
endfunl
Any suggestion would be appreciated.
You could create all those mappings with a couple of :help :for loop:
let alphabet = split('abcdefghijklmnopqrstuvwxyz', '\zs')
for first_key in alphabet
for second_key in alphabet
let pair = first_key . second_key
execute "nnoremap <Space>" . pair . " :<C-u>let " . pair . " = expand('%')<CR>"
execute "nnoremap <leader>" . pair . " :execute 'edit '" . pair . "<cr>"
endfor
endfor

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)

Is there a way to make use of two custom, complete functions in Vimscript?

Is there a way to achieve the following in Vim?
command! -nargs=* -complete=customlist,CustomFunc1 -complete=customlist,CustomFunc2 Foo call MyFunction(<f-args>)
The user will be able to tab-complete two arguments when calling the function Foo from Vim command line. The auto-complete will pull from two different lists.
E.g.:
:Foo arg1 good<TAB> whi<TAB>
Pressing Tab completes the words:
:Foo arg1 goodyear white
There is sufficient information passed to completion function through
its arguments. Knowing current cursor position in the command line to
be completed, it is possible to determine the number of the argument
that is currently being edited. Here is the function that returns
that number as the only completion suggestion:
" Custom completion function for the command 'Foo'
function! FooComplete(arg, line, pos)
let l = split(a:line[:a:pos-1], '\%(\%(\%(^\|[^\\]\)\\\)\#<!\s\)\+', 1)
let n = len(l) - index(l, 'Foo') - 1
return [string(n)]
endfunction
Substitute the last line with a call to one of the functions
completing specific argument (assuming they are already written).
For instance:
let funcs = ['FooCompleteFirst', 'FooCompleteSecond']
return call(funcs[n], [a:arg, a:line, a:pos])
Note that it is necessary to ignore whitespace-separated words before
the command name, because those could be the limits of a range, or
a count, if the command has either of them (spaces are allowed in both).
The regular expression used to split command line into arguments takes
into account escaped whitespace which is a part of an argument, and
not a separator. (Of course, completion functions should escape
whitespace in suggested candidates, as usual in case of the command
having more than one possible argument.)
There's no built-in way for vim to do this. What I'd do in this situation is embed the logic into the completion function. When you set -complete=customlist,CompletionFunction, the specified function is invoked with three arguments, in this order:
The current argument
The entire command line up to this point
The cursor position
So, you can analyze these and call another function depending on whether it looks like you're on the second parameter. Here's an example:
command! -nargs=* -complete=customlist,FooComplete Foo call Foo(<f-args>)
function! Foo(...)
" ...
endfunction
function! FooComplete(current_arg, command_line, cursor_position)
" split by whitespace to get the separate components:
let parts = split(a:command_line, '\s\+')
if len(parts) > 2
" then we're definitely finished with the first argument:
return SecondCompletion(a:current_arg)
elseif len(parts) > 1 && a:current_arg =~ '^\s*$'
" then we've entered the first argument, but the current one is still blank:
return SecondCompletion(a:current_arg)
else
" we're still on the first argument:
return FirstCompletion(a:current_arg)
endif
endfunction
function! FirstCompletion(arg)
" ...
endfunction
function! SecondCompletion(arg)
" ...
endfunction
One problem with this example is that it would fail for completions that contain whitespace, so if that's a possibility, you're going to have to make more careful checks.

Resources