diff --git a/_posts/2019-07-23-vim-folds.md b/_posts/2019-07-23-vim-folds.md new file mode 100644 index 0000000..a30b0c5 --- /dev/null +++ b/_posts/2019-07-23-vim-folds.md @@ -0,0 +1,210 @@ +--- +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. + + + +## 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' +```