Using macOS, you may have had experiences of handling plist files. For example, ~/Library/Preferences/.GlobalPreferences.plist
file holds some configurations of macOS. When you type defaults write -g ApplePressAndHoldEnabled -bool false
on terminal, the following lines are added to .GlobalPreferences.plist
:
<key>ApplePressAndHoldEnabled</key>
<false/>
So when you dig down the preferences or resources of macOS system, you’ll meet plist files.
darfink’s vim-plist plugin handles *.plist files quite well. A plist file is in one of three formats; json, binary, xml. macOS is bundled with the plutil
command that can convert a plist file from one format to another. The plugin also uses plutil
to handle read and write of plist files.
The plugin registers autocmd for BufReadCmd
and FileReadCmd
to read *.plist files, BufWriteCmd
and FileWriteCmd
to write *.plist files. BufRead
and BufWrite
events are triggered after reading the file into the buffer, but BufReadCmd
and BufWriteCmd
events are triggered before reading the file, and that autocmd should handle actual read and write operation of that file. These differences make handling plist files more complex.
Overall, the plugin is quite useful and seamless. But recently, I found some *.strings plist files under /System/Library
, almost of them in binary formats. The plugin registers autocmds only for *.plist files, so there is no chance to convert them to readable formats.
At first, I’ve considered to register an autocmd for *.strings file, but I’m not sure about that .strings
extension is only used for plist file, and also there can be other extensions with plist contents (for example, *.nib files are plist, too).
darfink’s vim-plist checks g:plist_save_format
and b:plist_save_format
before writing to plist files. The buffer-local variable is set when the plugin reads the file and detect the format. The global one is set by user, and overrides buffer-local one.
I don’t want the plugin to override the content of plist files with different format, so I haven’t set g:plist_save_format
. Then the problem raised. Open a new plist file, like vim test.plist
, and save it after editing. Then the plugin didn’t set b:plist_save_format
because it’s a new file, and I also didn’t set g:plist_save_format
, so the plugin don’t know the format to use for saving.
I think this problem can be solved by patching the plugin, but its last commit is pushed in 2014, which makes me use the faster way.
vim-plist always uses plutil -convert
command when opening plist files. But plutil
checks whether the given file is valid or not. This leads to a problem when we have incomplete plist files. Say a plist file is tracked by git, and when there is a merge conflict, that plist file will contain SCM conflict markers.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<<<<<<< HEAD
<key>other_content</key>
<integer>4</integer>
=======
<key>content</key>
<integer>5</integer>
>>>>>>> master
<key>test</key>
<integer>3</integer>
</dict>
</plist>
With vim-plist, we will never be able to edit this file because vim-plist will refuse to load it into the buffer.
I decided to check if the file is binary plist file or not, and load with functions of vim-plist plugin. The checking process is easy, because the file starts with bplist
. At first, I think it’s okay to register an autocmd for BufReadCmd
because calling functions of the plugin should be easy. So my first try was:
function! s:DetectBinaryPlist()
let l:filename = expand('<afile>')
if filereadable(l:filename)
let l:content = readfile(l:filename, 'b', 1)
if len(content) > 0 && content[0] =~# '^bplist'
return 1
endif
endif
return 0
endfunction
autocmd BufReadCmd *
\ if s:DetectBinaryPlist() |
\ call plist#Read(1) |
\ call plist#ReadPost() |
\ endif
Can you see the problem? This makes Vim returns an empty buffer when it reads a file that’s not a binary plist file. As I said above, BufReadCmd
should handle actual read and write operation of the file. If it’s not a binary plist file, Vim won’t read anything according to this code.
So I changed BufReadCmd
to BufRead
. This event happens after reading the file into the buffer, so I have to empty the buffer.
function! s:ConvertBinaryPlist()
silent! execute '%d'
call plist#Read(1)
call plist#ReadPost()
endfunction
autocmd BufRead *
\ if getline(1) =~# '^bplist' |
\ call s:ConvertBinaryPlist() |
\ endif
The getline(1)
reads the first line of the buffer, and we can call it because it’s after reading the file. It’s working quite well, so at that time, I wanted to bring the writing functionality also.
function! s:ConvertBinaryPlist()
silent! execute '%d'
call plist#Read(1)
call plist#ReadPost()
autocmd! BufWriteCmd,FileWriteCmd <buffer>
autocmd BufWriteCmd,FileWriteCmd <buffer>
\ call plist#Write()
endfunction
Note that the autocmd!
line means deleting every other BufWriteCmd
and FileWriteCmd
autocmds for that buffer, and the second line registers BufWriteCmd
and FileWriteCmd
for that buffer.
But when I saved after editing a *.strings file, I saw this error message:
<stdin>: Property List error: Unable to convert string to correct encoding / JSON error: JSON text did not start with array or object and option to allow fragments not set.
After poking around, I found that the fileencoding
is set to latin1
. The original file is binary and we just replaced the contents of the buffer, the fileencoding
was not properly set. So I just set it to UTF-8.
function! s:ConvertBinaryPlist()
silent! execute '%d'
call plist#Read(1)
call plist#ReadPost()
set fileencoding=utf-8
autocmd! BufWriteCmd,FileWriteCmd <buffer>
autocmd BufWriteCmd,FileWriteCmd <buffer>
\ call plist#Write()
endfunction
It’s simple:
autocmd BufNewFile *.plist
\ if !get(b:, 'plist_original_format') |
\ let b:plist_original_format = 'xml' |
\ endif
We don’t write binary file by our own hands, so a new plist file would be in xml format.
autocmd
s of vim-plist (Update: 2019-06-18)I decided to remove default autocmd
s of vim-plist. The plist files I open or edit are either in XML format or binary format. When the file is in binary format, it’s handled by above vimrc. When the file is in XML format, it doesn’t need to be converted as I’ll save them in the same format.
let g:loaded_plist = 1
let g:plist_display_format = 'xml'
let g:plist_save_format = ''
let g:plist_json_filetype = 'json'
When g:loaded_plist
exists, vim-plist will do nothing. In plugin/plist.vim, it registers autocmd
s and set default values to global variables. So here we set global variables of vim-plist. The default value of g:plist_json_filetype
is 'javascript'
, but I set it to 'json'
as Vim can handle that filetype.
As we disabled all autocmd
s of vim-plist, above “Saving a new file” section also doesn’t work. Instead of setting the b:plist_original_format
variable, setting filetype
to xml would be enough.
autocmd BufNewFile *.plist set filetype=xml
Also, there was an update to vim-plist, supporting plistutil and removing the g:plist_json_filetype
variable.
In this pull request, plist#BufReadCmd()
, plist#FileReadCmd()
, plist#BufWriteCmd()
, and plist#FileWriteCmd()
are added instead of plist#Read()
and plist#Write()
functions. So we apply that changes to the patch.
So this is the complete part of my .vimrc for binary plist files:
function! s:ConvertBinaryPlist()
silent! execute '%d'
call plist#BufReadCmd()
set fileencoding=utf-8
augroup BinaryPlistWrite
autocmd! BufWriteCmd,FileWriteCmd <buffer>
autocmd BufWriteCmd <buffer> call plist#BufWriteCmd()
autocmd FileWriteCmd <buffer> call plist#FileWriteCmd()
augroup END
endfunction
augroup BinaryPlistRead
autocmd!
autocmd BufRead *
\ if getline(1) =~# '^bplist' |
\ call s:ConvertBinaryPlist() |
\ endif
autocmd BufNewFile *.plist set filetype=xml
augroup END
" Disable default autocmds
let g:loaded_plist = 1
let g:plist_display_format = 'xml'
let g:plist_save_format = ''
It’s also available on GitHub, you can visit my dotfiles repository!