Persistent history (MRU files, :cmd's)?

I’ve looked at the plugin ecosystem, but I can’t find something like vim’s oldfiles, or the MRU (most recently used files) plugin. Ditto for viminfo (saving and restoring command history, foremost).

As far as implementing — for MRU files, I suppose hooks on BufEnter (or whatever it’s called) can update a MRU log. For history, I suppose the history register can be saved, but not sure about restoring.

@Screwtapello’s kakoune-state-save is invaluable for me: It can save the command histories in : | and search history in /. It also saves your selections and restores it on opening the buffer in a new Kakoune session, so you can continue where you left off.

However I haven’t run across a plugin that would replicate the MRU functionality; I seem to remember some discussion about it but I can’t find anything on it right now. I’d wager it wouldn’t be very difficult technically, it might even be a good fit for kakoune-state-save since it already handles persisting things to disk.

2 Likes

This doesn’t persist across reboots but I use my daemon manager to launch a named kakoune server and have alias k='kak -c global -e "cd $PWD"'. Can combine this with the save state plugin if you want.

@prion is that kak -d -s IAMSOALONE for “starting a server”? It’s not clearly specified.

If I start a normal kak (*) I only see a single ps process, not a server and a client. When that kak exits, no kak's are left around. Is that single process in a special “server+client” state or how does it know it should exit with the client?


(*) after I get over how it ruins my vertical qterminal tabs with a nice huge title

Yeah, just make sure the process isn’t attached to your terminal.

I don’t know how a normal kak instance works. But I do know if you connect to a running kak, e.g. with :new, and then :quit the original kak it will tell you it’s been forked to the background.

I hadn’t considered MRU functionality for kakoune-state-save; for me, I normally edit projects rather than individual files, so I use tab-completion in the shell to find the project I was working on last, and then a command like :find (or even just regular :edit) to pull up the files I want.

Now that I think of it, because kakoune-state-save will save and restore the command history, and because Kakoune lets you cycle through command history (with <up>), that’s kinda-sorta an MRU feature: type :e <up> and you’ll get the most recent file you opened. It doesn’t record the absolute path, so Kakoune would have to be started in the same directory; the entire history is capped at 100 entries, so you wouldn’t get a predictable number of MRU files; but that’s probably the closest you’ll get to an MRU feature today with already-existing plugins.

I’m not sure an MRU feature would fit with the rest of kakoune-state-save’s features, since all the other things it does are “invisible” — that is, there’s no extra prompts or commands or mappings or user-modes or scratch-buffers, it just silently makes a new Kakoune instance behave more like the previous instance. An MRU feature could quite happily exist on its own, though - you’d just need a str-list option to store used files, a hook to add/move a filename to the front of the list when it’s used, some kind of edit command that used that list as the list of completions, and code to persist that list to disk between sessions.

You are right, I guess conceptually they are not really a good fit. Nevertheless, it seems it is pretty easy to implement like you said, especially if we rely on kakoune-state-save features for persistence. I gave it a shot as such, clobbering the b register temporarily:

declare-option -hidden str-list mru_buffers

hook global BufCreate [^*].* %{
    # prevent duplicate entries
    set-option -remove global mru_buffers %val{hook_param}
    set-option -add global mru_buffers %val{hook_param}
}

# persist buffer list across sessions using kakoune-state-save, using register b
hook global KakBegin .* %{
    state-save-reg-load b
    set-option global mru_buffers %reg{b}
}
hook global KakEnd .* %{
    set-register b %opt{mru_buffers}
    state-save-reg-save b
}

define-command mru -docstring "Display most recently used files, press return to edit" %{
    # create and populate the MRU buffer
    try %{ delete-buffer *mru* }
    edit -scratch *mru*
    evaluate-commands -save-regs b %{
        set-register b %opt{mru_buffers}
        execute-keys '"b<a-P><a-space>a<ret><esc>gj'
    }  # maybe we need to save more registers to prevent side effects?
    # press enter to open
    map buffer normal <ret> "<a-x>_: edit <c-r>.<ret>"

    # delete buffer once we switch off of it?
    # hook -once global WinDisplay (?<![*]mru[*]).* %{ try %{ delete-buffer *mru* } }
}

Edit: set-option -remove requires the dev version of Kakoune since it was implemented last September.

2 Likes

@bravekarma doesn’t this ruin register b for other purposes (such as the selection-backup mechanism I’ve been recently looking at?) If we go to the trouble of declaring a separate option, why not save it somewhere under $XDG_DATA_HOME/kak/ (i.e. ~/.local/share ...)? I guess just to reuse state-save code?

Bravekarma’s snippet clobbers register "b when a Kakoune session begins (but it would have been blank anyway) and just before the session ends (but it was just about to be discarded anyway). The :mru command uses -save-regs b around the code that manipulates "b, so the previous value of the register is restored by the time you see the *mru* buffer on-screen.

Ah, I understand a little better, but not completely.

Doesn’t kakoune-state-save aim to save all registers, like vim does in viminfo? For example in vim I would expect whatever I save in a named register to persist between sessions (not that I really care, that’s just what default behavior is).

Also, other extensions (if they used the same idea) would need to pick a different register to persist their data in — correct?

kakoune-state-save doesn’t save or restore any registers by default, for privacy and security reasons.

For cursor positions, the only privacy concern is recording the names of files on your hard-drive to people who have access to your local hard-drive; the only security concern is that somebody could overwrite a cursor-position file and make you lose your place. Either way, this doesn’t seem like a big deal so cursor-positions are saved and restored by default.

For registers, they may contain document fragments (alphabetic registers) or commands or searches (history registers) for things you tried then reverted. Making a permanent record of such things on disk could be a privacy concern. Meanwhile, the only way to restore such things is with Kakoune’s :evaluate-commands command, which can be pretty easily tricked into executing arbitrary commands. While an attacker would already need write access for such an exploit, I imagine there’s people who would be uncomfortable with that happening automatically. For people who do want to save and restore registers by default, it’s really easy to set up and there’s a config snippet in the README you can copy and paste, so it’s off by default.

The other potential issue with saving and restoring registers is that in Kakoune registers are also marks, but unlike Vim, Kakoune won’t automatically open the file a mark points to when you try to jump to the mark. I could update the register loading/saving code to say something like “if this register contents looks like a mark, make sure the filename is an absolute path before saving it” and “if this saved register looks like a mark, open the named file in addition to restoring the register value”, but until somebody makes that change, saving and restoring registers is not as useful as people might expect.

In the config snippet, the bits that clobber the "b register proceed to immediately use the value and don’t care about it after that. If many plugins used "b as scratch space at startup, it would be fine - they’d each take turns and not step on each other’s toes.

The only concern would be if somebody used kakoune-state-save to persist user-provided information in the "b register. If the “restore the user’s "b value” hook executed last, everything would be fine, but if some other plugin’s hook happened to execute last, the user’s "b value would be lost.

That’d be easy to fix, though - just wrap the contents of the KakBegin hook in the snippet in evaluate-commands -save-regs b %{ } like the :mru command does.

2 Likes

I have this in my kakrc which appends the filename to a ~/.mru file. I use the entries in this file mostly to prepopulate fzf with interesting files:

hook global BufCreate [^*].* %{
    nop %sh{
        echo "$kak_buffile" >> ~/.mru
    }
}

@danr nice; then

hook global BufCreate [^*].* %{
  nop %sh{
    mru="$kak_config"/mru
    realpath -- "$kak_buffile" | cat - "$mru" |
      awk '!seen[$0]++' | head -100 >"$mru".new
    mv "$mru".new "$mru"
  }
}

… but obviously this needs a buf-local map to actually recall files easily, plus if you open the file kak will complain about it being modified externally etc.

1 Like

I occasionally run prune-mru | sponge ~/.mru where prune-mru is this little script:

$ cat bin/prune-mru 
#!/usr/bin/python
import os, sys
with open(os.path.expanduser('~/.mru')) as f:
    lines = list(f)
nub = set()
for line in lines:
    if line in nub:
        continue
    nub.add(line)
    line = line.strip()
    if os.path.isfile(line):
        print(line)

I quite often open files in kak that are modified externally. I just press Y in the modal and it autoreloads automatically, very convenient.

1 Like

I’m using a similar hook which handles everything in one command:

hook global BufCreate [^*].* %{
    nop %sh{
        mru=~/.cache/kak-mru
        echo "$kak_buffile" | awk '!seen[$0]++' - "$mru" | sponge "$mru"
    }
}

I then execute this script from kakoune to get a list of files in fzf (uses kakoune.cr):

~ > cat .local/bin/kcr-fzf-mru
#!/bin/sh
# Jump to recent file in Kakoune.

kcr edit "$(fzf < ~/.cache/kak-mru)"
1 Like

I’ve solved the MRU files problem by writing the mru-files.kak plugin, which I’m now satisfied with, as I’ve managed to make it save on every file visit with (I think) minimal cost (WinDisplay hook that sets a NormalIdle hook, history file in /run until KakEnd)

For persisting command history, I’ve come up with this snippet based on @Screwtapello’s kakoune-state-save:

def cmdhist-save -override %{
  echo -to-file %opt{cmdhist_file} -quoting kakoune -- %reg{colon}
}
def cmdhist-load -override %{
  nop %sh{ mkdir -p "${kak_opt_cmdhist_file%/*}"; >> "$kak_opt_cmdhist_file" }
  eval reg colon %sh{ cat "$kak_opt_cmdhist_file" }
}
hook  global KakEnd   .* %{ cmdhist-save }
hook  global KakBegin .* %{ cmdhist-load }

However this has some downsides, as the cmdhist.txt file contains a sequence of quoted kak commands on a single line, and I can’t easily filter them (probably don’t want to save :quit) or truncate the list.

I’d prefer to have one command per line in the file. I think this can be achieved by using --quoting shell when saving, and the kakquote helpers (now in k9s0ke-shlib) when reading back the values. With one line per command it would be easy to filter the list through user-supplied code. Thoughts?

EDIT this is really nasty with multiple sessions, as histories clobber each other. So, yeah, you probably need timestamps to merge histories properly, which means hooks upon hooks, which I was trying to avoid.

2 Likes

I’ve been using this for quite a long time:

hook global BufOpenFile .* recentf-add-file
hook global BufWritePost .* recentf-add-file

declare-option -docstring "Path to the file where to store recent file list." \
str recentf_file "%val{config}/.recentf"
declare-option -docstring "Maximum amount of entries in the recent file list." \
int max_recentf_files 45

define-command -hidden recentf-add-file %{ nop %sh{
    if grep -q "${kak_buffile:?}" "${kak_opt_recentf_file:?}"; then
        # store full recentf list without current entry
        old=$(grep -v "${kak_buffile}" "${kak_opt_recentf_file}")
        # move current entry to the beginning of the list
        printf "%s\n%s\n" "${kak_buffile}" "${old}" > "${kak_opt_recentf_file}"
    else
        # put current entry to the beginning of the list
        printf "%s\n%s\n" "${kak_buffile}" "$(cat "$kak_opt_recentf_file")" > "$kak_opt_recentf_file"
        # remove everyting past the `max_recentf_files' count
        printf "%s\n" "$(head -"${kak_opt_max_recentf_files:?}" "$kak_opt_recentf_file")" > "${kak_opt_recentf_file}"
    fi
}}

define-command recentf -params 1 -shell-script-completion %{ cat "${kak_opt_recentf_file}" } %{ edit -existing %arg{1} }
alias global & recentf

This simply writes last N file paths into a file in your config dir, and you can browse this list with the default completion mechanism. If file exists in the .recentf file list, it is moved to top when visited. If you only visit new files, the head command will trim everything past the specified number.

2 Likes

Thank you @andreyorst that explains how to implement :mru-files <TAB>. I’ve added that to my plugin (mru-files now completes from MRU list, or displays *mru* if no arg selected). I think you meant -shell-script-candidates?

[ Of course, that’s how it all started, with custom shell scripts (which work just fine). The exercise was how to run %sh with as few commands as possible (I have sh functions implementing head & grep equivalents), on an idle hook, and not write to disk (history file in /tmp or run, synchronized to ~/.config/kak on only KakEnd) ]

No, unfortunately -shell-script-candidates does sorting to arguments thus list of recently edited files becomes sorted and recent files are no longer on the top. -shell-script-completion doesn’t have this problem but it doesn’t allow fuzzy matching through completion items

Well in my case if the user wants MRU (chronological), they can press <ret> and get the full *mru* buffer. But I could try -shell-script-completion with non-fuzzy matching, equivalent to grep -F "$tok". I’m not convinced that’s better.

Great implementation with as little code as possible, though, your recentf thing.

1 Like