How to permanently delete a specific entry from Vim's command-line history without losing the changelists of the recently edited files?

Viewed 1751

When we execute a Vim Ex command, it gets saved in the command-line history, which we can access by opening the command-line window with q:.

However, sometimes, I want to be able to remove some of them permanently. For example, if I execute the command :a, all subsequent commands in the command-line window are recognized as elements of the syntax group vimInsert, which is linked to the highlight group String, which is colored in cyan in my colorscheme.

To fix the syntax highlighting, I could try to delete the :a command from the command-line window hitting dd, but the entry is removed only while the window is opened. As soon as I close it, and reopen it later, the :a command is back. To make the removal permanent, I tried to add an autocmd in my vimrc:

augroup my_cmdline_window
    au!
    au CmdWinLeave * call s:update_history()
augroup END

fu! s:update_history() abort
    let new_hist = getline(1, '$')
    call histdel(':')
    for i in new_hist
        call histadd(':', i)
    endfor
endfu

It calls a function, which updates the command history by reading it from the current window, before closing it. It works while the current Vim session is running, but as soon as I quit it then start a new one, all the deleted entries are back.

I think the reason why the removal doesn't persist across sessions, is because when I quit a session Vim merges the current command-line history with the one stored in ~/.viminfo.

What is saved inside this file is controlled by the 'viminfo' option. The value of my 'viminfo' is:

viminfo='100,<50,s10,h

It doesn't contain the : parameter, which sets the maximum number of items in the command-line history, which are to be saved. It means that Vim should use the 'history' option. The default value of my 'history' is 1000.

So, to force Vim to overwrite the command-line history stored inside ~/.viminfo with the one of the current session, I tried to use the :wviminfo command inside my autocmd:

augroup my_cmdline_window
    au!
    au CmdWinLeave * call s:update_history()
augroup END

fu! s:update_history() abort
    let new_hist = getline(1, '$')
    call histdel(':')
    for i in new_hist
        call histadd(':', i)
    endfor
    wviminfo!
endfu

I added a bang to :wviminfo, which according to :h :wviminfo:

When [!] is used, the old information is not read first,
only the internal info is written.

It seems to work, because with this autocmd, the removal of an entry inside the command-line window persists across sessions. However, it also seems to erase all the changelists of the recently edited files, because after starting a new Vim session, all their changelists are empty.

And since :h 'viminfo explains that when you include the ' item in 'viminfo', the changelist and the jumplist are also stored in ~/.viminfo, I suspect that the jumplist and the marks are also lost after using :wviminfo!.

Is there a way to avoid the loss of the changelists/jumplists/marks, while still removing an arbitrary entry from the command-line history?


Edit:

I came up with this:

augroup my_cmdline_window
    au!
    au CmdWinEnter * let s:old_cmdline_hist = getline(1, line('$')-1)
    au CmdWinLeave * call s:update_history()
augroup END

fu! s:update_history() abort
    let hist = filter(getline(1, '$'), 'v:val !~# "^\\s*$"')

    call histdel(':')
    for i in hist
        call histadd(':', i)
    endfor

    let viminfo = expand('~/.viminfo')
    if !filereadable(viminfo)
        return
    endif

    let info = readfile(viminfo)
    let deleted_entries = filter(copy(s:old_cmdline_hist), 'index(hist, v:val) == -1')

    call map(deleted_entries, 'index(info, ":".v:val)')
    call sort(filter(deleted_entries, 'v:val >= 0'))
    if empty(deleted_entries)
        return
    endif

    for entry in reverse(deleted_entries)
        call remove(info, entry, entry + 1)
    endfor
    call writefile(info, viminfo, 'b')
endfu

Here's what the code does:

When you enter the command-line window, the 1st autocmd captures the command-line history inside s:old_cmdline_hist.

When you leave it, the 2nd autocmd updates the history of the current session, and tries to remove the deleted entries from ~/.viminfo.

let hist = filter(getline(1, '$'), 'v:val !~# "^\\s*$"')
call histdel(':')
for i in hist
    call histadd(':', i)
endfor

This block updates only the history of the current session.

let viminfo = expand('~/.viminfo')
if !filereadable(viminfo)
    return
endif

This one checks whether ~/.viminfo is readable. If it's not, the function stops.

let info = readfile(viminfo)
let deleted_entries = filter(copy(s:old_cmdline_hist), 'index(hist, v:val) == -1')

This block gets the contents of ~/.viminfo inside the list info (1 item = 1 line). It also gets the entries which were deleted by invoking filter(). The latter removes all the entries which don't satisfy the condition index(hist, v:val) == -1. This expression is true if an entry of the old history, on which filter() operates, is not inside the new history.

call map(deleted_entries, 'index(info, ":".v:val)')

This line converts the deleted text entries into their line addresses inside ~/.viminfo.

call sort(filter(deleted_entries, 'v:val >= 0'))
if empty(deleted_entries)
    return
endif

This block sorts the line addresses, and removes the ones which are not positive, in case some of them weren't found in the previous step (index() returned -1). It also checks whether there are still line addresses at this point, and if there aren't, again, the function stops.

for entry in reverse(deleted_entries)
    call remove(info, entry, entry + 1)
endfor

This block removes the relevant lines from info. It deletes 2 lines (entry and entry + 1), because for every command-line saved in ~/.viminfo, there's also a 2nd line which seems to be a timestamp. Also, it deletes in reverse order, to not have to update the addresses of the lines to delete (every time you delete a line, the addresses of the next ones are decreased by one).

call writefile(info, viminfo, 'b')

This line overwrites ~/.viminfo with the new contents, which should be the same as before, without the lines deleted in the command-line window.

I don't post it as an answer, as I'm not sure it's reliable and there aren't any side-effects. I did a backup of ~/.viminfo before testing the code.

Edit2:

I don't think it's able to delete a line which contains a literal carriage return. There may be other special characters causing a similar issue (like a null character ^@).

1 Answers

History is stored in .viminfo file in plain text. So you could just open your .viminfo and delete that buggy line.

Alternatively, You can clear the history via the histdel() function, e.g. for searches:

:call histdel('/')

You can delete certain history entries by number or regex. See :help histdel.

Related