+++ title = "Sorting VCF files with Vim folds" date = "2019-07-23" author = "Ceda EI" authorTwitter = "" #do not include @ cover = "" tags = ["vim", "neovim"] keywords = ["vim", "neovim", "folds"] description = "A while ago I wanted to sort my contacts which had become a mess. I wanted to use this opportunity to do something nice with Vim too. Enter Vim-folds." showFullContent = false aliases = [ "/2019/07/23/vim-folds.html" ] +++ ## Getting Started I initiated a repository for the plugin. ```sh cd ~/repos/ git init vcf.Vim ``` I use [Vim-plug](https://github.com/junegunn/Vim-plug) for package management in Vim and added that to `Vim-plug` ```Vim Plug '~/repos/vcf.Vim/' ``` Next, I exported all my contacts into a Virtual Contact File. ## Filetype Detection To get started I needed Vim to assign a filetype to VCFs. A small `autocmd` is enough to take care of that. By convention, filetype detection files go in `ftdetect` directory. `ftdetect/vcf.Vim` ```Vim au BufNewFile,BufRead *.vcf set filetype=vcf ``` This sets the filetype of all files with filenames ending with `.vcf` to `vcf` ## Folding Plugins for specific filetypes are stored in the `ftplugin` directory. Vim sources the file if the filetype matches the filename without the `.vim` extension e.g. if the filetype is set to `javascript`, `javascript.vim` in `ftplugin` directory will be sourced. Each contact in a VCF looks like this: ```vcf BEGIN:VCARD VERSION:2.1 N:Lname;Fname;;; FN:Fname Lname TEL;VOICE:+911234567890 END:VCARD ``` I want my folds to look like ```vcf Fname Lname···································· ``` I am going to use `expr` foldmethod. This can simply be done using `set foldmethod=expr`. What this tells vim is to run a function on every line and set the fold level based on that. To set the expression to run, we use `set foldexpr=OurFunction()`. Let's write a function first and set `foldexpr` to it. Define the function by the usual syntax. ```vim function! VCFFold() endfunction set foldmethod=expr set foldexpr=VCFFold() ``` When the function is run, vim sets a special variable `v:num` that tells us which line the function is being run on. To get the current line, we use the `getline` function. ```vim let thisline = getline(v:lnum) ``` We want to start a new level fold at every line which starts with `BEGIN`. For all the other lines, we want to keep the same fold level as previous line since we don't have nested folds in VCF. Vim folds with expr work by running the function on each line and determining the fold level of that line based on the return value of that function. Some of the return values are: + `>n` - This tells vim to start a new `n`th level fold there + `=` - This tells vim that the fold level is same as previous line. + `n` - This tells vim that the fold level is `n` There are more return values. Check `:help fold-expr` We can simply set the fold level to `>1` at every `BEGIN` and set it to `=` on every other line. This way, every contact will end up in a fold starting at the `BEGIN` of every contact. We can simply use `match` function to check if the line begins with `BEGIN` and return `>1` in that case, else we will return `=`. ```vim if match(thisline, '^BEGIN') >= 0 return ">1" endif return "=" ``` Putting it all together, we get. ```vim function! VCFFold() let thisline = getline(v:lnum) if match(thisline, '^BEGIN') >= 0 return ">1" endif return "=" endfunction set foldmethod=expr set foldexpr=VCFFold() ``` ## Fold Text To set the fold text, we have to set the `foldtext` to a function. Let's create a funtion named `VCFFoldText` for this purpose and set `foldtext` to it. ```vim function! VCFFoldText() endfunction set foldtext=VCFFoldText() ``` The name is stored as a line `N:Lname;Fname;;;`. Two special variables set for foldtext function are `v:foldstart` and `v:foldend` which are the line numbers where the current fold starts and ends. We can iterate from `v:foldstart` to `v:foldend` using the range function. While iterating, we can simply look for a line that begins with `N:` and return the name from it. If we don't find any such line, we can return `No Name`. ```vim function! VCFFoldText() for i in range(v:foldstart, v:foldend) let l:thisline = getline(i) if match(l:thisline, '^N:') >= 0 " Return the string here endif endfor return "No Name" endfunction ``` All we need to do is split the parts on semi-colons and join that array back depending on our preferences of whether we want first name first or last name first. In my case I want first name first. ```vim let l:parts = split(l:thisline, ';') return substitute(join(l:parts[1:], " ") . l:parts[0][2:], '\s\+', ' ', 'g') ``` `split` splits the string into an array with the second parameter (`;` in this case) as delimiter. I then join all the elements from first element (skipping the zeroth element which is the last name) and then append the zeroth element without the first two characters (since those are `N:`). Finally, I replace multiple spaces with one space. ## Complete Program ```vim function! VCFFold() let thisline = getline(v:lnum) if match(thisline, '^BEGIN') >= 0 return ">1" endif return "=" endfunction set foldmethod=expr set foldexpr=VCFFold() function! VCFFoldText() for i in range(v:foldstart, v:foldend) let l:thisline = getline(i) if match(l:thisline, '^N:') >= 0 let l:parts = split(l:thisline, ';') return substitute(join(l:parts[1:], " ") . l:parts[0][2:], '\s\+', ' ', 'g') endif endfor return "No Name" endfunction set foldtext=VCFFoldText() ``` ## Installation If you just want the above program, it is available as [vcf.vim](https://gitlab.com/ceda_ei/vcf.vim). You can install it with [Vim-plug](https://github.com/junegunn/Vim-plug) via: ```vim Plug 'https://gitlab.com/ceda_ei/vcf.vim.git' ```