211 lines
5.5 KiB
Markdown
211 lines
5.5 KiB
Markdown
|
---
|
||
|
title: Sorting VCF files with Vim folds
|
||
|
tags: vim neovim
|
||
|
---
|
||
|
|
||
|
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.
|
||
|
|
||
|
<!--more-->
|
||
|
|
||
|
## 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:
|
||
|
|
||
|
```
|
||
|
BEGIN:VCARD
|
||
|
VERSION:2.1
|
||
|
N:Lname;Fname;;;
|
||
|
FN:Fname Lname
|
||
|
TEL;VOICE:+911234567890
|
||
|
END:VCARD
|
||
|
```
|
||
|
|
||
|
I want my folds to look like
|
||
|
|
||
|
```
|
||
|
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'
|
||
|
```
|