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 ^@).