Vim: Archiving text selections into other file - vim

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>

Related

Move to the latest file in the jumplist

With vim, I like to use C-o and C-i to move through the jumplist and I want to use the same to move to the previous and the next file with <leader>o and <leader>i.
I know I can use the buffer but the list is not always the same.
I tried to use EnhancedJump but I have some bugs and it seems to be obsolete.
Do you have solution?
It would be possible with functions like that:
function! JumpBack()
let l:cfile=expand('%')
let l:nfile=l:cfile
while l:cfile == l:nfile
execute 'normal ' . 1 . "\<c-o>"
let l:nfile=expand('%')
endwhile
endfunction
*Note this could be written cleaner, it is mostly pasted togheter.
However it seems like a sledgehammer method for me, maybe there is a better one.
Update
It was not surprising to hear, that this would lead to an endless loop if the jumplist only contains one file. Here is a better solution:
function! JumpBack()
let l:cfile=expand('%')
let l:jl = split(execute('jumps'), '\n')
let l:jumpcounter = 0
for l:jumpline in reverse(l:jl)
let l:jumpcounter = l:jumpcounter + 1
let l:nfile = split(l:jumpline, '\s')[-1]
if l:cfile != l:nfile
execute 'normal '. l:jumpcounter . "\<c-o>"
return
endif
endfor
endfunction

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.

CamelCase Expansion in Vim like Intellij Idea?

In Intellij Idea, there's a feature. Let's say I have used a variable myCamelCase somewhere in my code. Then if I type mCC and press Ctrl-Enter or some such key combination, it expands to myCamelCase. Is there something similar in Vim?
Okay, forgive me for answering twice, but since my first attempt missed the point, I'll have another go. This is more complicated than I thought, but possibly not as complicated as I have made it (!).
This is now modified to suggest all matching variable names.
First of all, here's a function to generate the 'mCC' abbreviation from the 'myCamelCase' string:
function! Camel_Initials(camel)
let first_char = matchstr(a:camel,"^.")
let other_char = substitute(a:camel,"\\U","","g")
return first_char . other_char
endfunction
Now, here's a function that takes an abbreviation ('mCC') and scans the current buffer (backwards from the current line) for "words" that have this abbreviation. A list of all matches is returned:
function! Expand_Camel_Initials(abbrev)
let winview=winsaveview()
let candidate=a:abbrev
let matches=[]
try
let resline = line(".")
while resline >= 1
let sstr = '\<' . matchstr(a:abbrev,"^.") . '[a-zA-Z]*\>'
keepjumps let resline=search(sstr,"bW")
let candidate=expand("<cword>")
if candidate != a:abbrev && Camel_Initials(candidate) == a:abbrev
call add( matches, candidate )
endif
endwhile
finally
call winrestview(winview)
if len(matches) == 0
echo "No expansion found"
endif
return sort(candidate)
endtry
endfunction
Next, here's a custom-completion function that reads the word under the cursor and suggests the matches returned by the above functions:
function! Camel_Complete( findstart, base )
if a:findstart
let line = getline('.')
let start = col('.') - 1
while start > 0 && line[start - 1] =~ '[A-Za-z_]'
let start -= 1
endwhile
return start
else
return Expand_Camel_Initials( a:base )
endif
endfunction
To make use of this, you must define the "completefunc":
setlocal completefunc=Camel_Complete
To use insert-mode completion, type CTRL-X CTRL-U, but I usually map this to CTRL-L:
inoremap <c-l> <c-x><c-u>
With this code in your vimrc you should find that typing mCC followed by CTRL-L will make the expected replacement. If no matching expansion is found, the abbreviation is unchanged.
The code isn't water-tight, but it works in all the simple cases I tested. Hope it helps. Let me know if anything needs elucidating.
There is a plugin for this in Vim called vim-abolish. Use the map crc to expand to camel case.

What is the easiest way to swap occurrences of two strings in Vim?

What is the easiest way to replace all occurrences of string_a with string_b while at the same time changing anything that was already string_b into string_a? My current method is as follows:
:s/string_a/string_c/g
:s/string_b/string_a/g
:s/string_c/string_b/g
Although this works, it requires extra typing and seems inefficient. Does anybody know of a better way to do this?
I'd do it like this:
:%s/\v(foo|bar)/\={'foo':'bar','bar':'foo'}[submatch(0)]/g
But that's too much typing, so I'd do this:
function! Mirror(dict)
for [key, value] in items(a:dict)
let a:dict[value] = key
endfor
return a:dict
endfunction
function! S(number)
return submatch(a:number)
endfunction
:%s/\v(foo|bar)/\=Mirror({'foo':'bar'})[S(0)]/g
But that still requires typing foo and bar twice, so I'd do something like this:
function! SwapWords(dict, ...)
let words = keys(a:dict) + values(a:dict)
let words = map(words, 'escape(v:val, "|")')
if(a:0 == 1)
let delimiter = a:1
else
let delimiter = '/'
endif
let pattern = '\v(' . join(words, '|') . ')'
exe '%s' . delimiter . pattern . delimiter
\ . '\=' . string(Mirror(a:dict)) . '[S(0)]'
\ . delimiter . 'g'
endfunction
:call SwapWords({'foo':'bar'})
If one of your words contains a /, you have to pass in a delimiter which you know none of your words contains, .e.g
:call SwapWords({'foo/bar':'foo/baz'}, '#')
This also has the benefit of being able to swap multiple pairs of words at once.
:call SwapWords({'foo':'bar', 'baz':'quux'})
You can do this easily with Tim Pope's Abolish plugin
:%S/{transmit,receive}/{receive,transmit}
Here is how I swap two words skip & limit:
%s/skip/xxxxx/g | %s/limit/skip/g | %s/xxxxx/limit/g
Pretty sure someone could turn it into a shorter command which accepts two arguments.
The swapstrings plugin provides a handy command for this:
:SwapStrings string_a string_b
You can do it with a single command as shown in my code below:
:%s/\<\(string_a\|string_b\)\>/\=strpart("string_bstring_a", 8 * ("string_b" == submatch(0)), 8)/g

Resources