Search code examples
vimvimwiki

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:

  1. I am getting E16 invalid range errors for all 3 lines of ArchiveSelectedLinesToFile. Where from there exactly? And why?
  2. 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?
  3. 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.
  4. 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.


Solution

  • :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>