Project based config in vim
Tags: vim, shell
Published: 2021-04-01 17:41:14

---
title: Project based config in vim 
published: true
description: we can have nice things in vim
tags: #vim #shell
//cover_image: https://direct_url_to_image.jpg
---

Every once in a while I find myself wanting to have a specific configuration in a project. Is not that I needed it, but it would be nice to have. There are ways to achieve this, some "better" than others. Today I'm going to share with you a couple ways we can do it and my current solution for this.

## editorconfig

- Plugin: [editorconfig-vim](https://github.com/editorconfig/editorconfig-vim)

If all you want is to keep the aesthetic of your code consistent, this is the way to go.

With this plugin vim can read the popular [.editorconfig](https://editorconfig.org/) format, which allows you to set options like the indent style, tab size, maximum length of line... that kind of stuff.

The limited set of options offered by this plugin make it the safest choice. Because arbitrary code execution is cool until is not.

## local vimrc

- Plugin: [vim addon local vimrc](https://github.com/MarcWeber/vim-addon-local-vimrc)

Speaking of arbitrary code execution... what if we want that? That plugin is **one** option.

This one will scan your project searching for a `.vimrc` file and source it. Now you have unlimited power, in your "local" `.vimrc` you can have anything you want. Sounds good, right? Also kinda dangerous, that's why this plugin also offers a "hash protection." Basically, it checks the integrity of the file before executing it. If it turns out it changed since the last time you used it, it will prompt you and ask you if you're sure you want to source the `.vimrc`.

For me, it still feels wrong. Maybe the hash protection is enough, maybe it isn't. It just feels wrong.


## nvim project

- Plugin: [nvim project config](https://github.com/windwp/nvim-projectconfig)

This one is similar to local vimrc in that it will look for a `vim` file or `lua` file (is a neovim plugin) and execute it. The difference is the "local config" is not meant to live in your project folder, it will live on the same place your `init.vim` is (it will create a "projects" folder). What it will do is check if your current project has a config file in the folder this plugin manages, and of course it will execute it if it does exists. 

Cool thing about it is that you don't have to worry about some sneaky `.vimrc` file lurking around looking to cause you harm. On the other hand, this plugin doesn't check the integrity of the files it executes. It shouldn't be such a big deal because you won't be downloading random files in the "projects" folder, you should know for sure what's in there.

My problem with this plugin is that is written in lua, and it's somewhat opinionated in the way it handles your local config.

## Do It Yourself

The current solution I'm exploring is very much inspired by `nvim-project`. Is like a "low tech" version of that. The goal is to have more control over the config files and at same time keep it simple.

Ultimately want I want to do is map a project folder to a config file. That's pretty much it. vim can do that.

I did some experiments and it turns out this works.

```vim
let s:project_config = {
  \ "/tmp/test": "IT WORKS"
  \ }

echo s:project_config[getcwd()]
```

If I open vim in `/tmp/test` folder and run `:source /path/to/test.vim` it will show me the message "IT WORKS." That is half of what I need. Let's look for the other half.

```diff
 let s:project_config = {
-  \ "/tmp/test": "IT WORKS"
+  \ "/tmp/test": "/tmp/other/localrc.vim"
   \ }
-
- echo s:project_config[getcwd()]
+ echo get(s:project_config, getcwd(), '')
```

Now `/tmp/test` will point to another file in some random folder. And instead of accessing the property on `project_config` directly we use the `get` function which will provide a nice default value if we don't get what we want.

The next step is to check the local config exists and can be read.

```vim
let s:project_config = {
  \ "/tmp/test": "/tmp/other/localrc.vim"
  \ }

let s:file = get(s:project_config, getcwd(), '')

if filereadable(s:file)
  execute 'source' s:file
endif
```

That is the core feature I want. Now lets make it nicer.

I want to be able to go to the "local config" from my project folder. Additionaly, I want to be able to create a local config if there isn't one.

```diff
  if filereadable(s:file)
    execute 'source' s:file
+   execute 'nnoremap <C-x>c :edit' s:file '<cr>'
  else
+   execute 'nnoremap <C-x>c :edit /location/of/projects-rc/'
  endif
```

I know `<C-x>c` is a horrible shortcut. Look, I'm running out combination on my vimrc.

So, if the local config exists `<C-x>c` will take me to that file, if not it will prompt me to create one with the `:edit` command.

What happens when I want to add the file to `s:project_config`? I can solve that, by adding this on the last line.

```vim
execute 'nnoremap <C-x>C :edit' expand('<sfile>:p') '<cr>'
```

There you go. All set.

The last thing **I** want to take care of is avoid writing long paths. So... have you ever heard about currying? Not the food. Doesn't matter, lets add a litte helper function.

```vim
function! s:prefix(path) abort
  return {file -> expand(a:path . file)}
endfunction
```

This is a function that returns another function. Why? Because I can. Anyway, what we are doing here is creating a "path" that the `expand` function will use. We need `expand` if want to use special characters like `~`, or make use of some special tokens vim provides (like `%` or `:p`). Now you could do something like this.

```vim
let s:my_code = s:prefix('~/Projects/')
let s:work_code = s:prefix('~/some/path/to/your/work/folder/')
let s:localrc = s:prefix('~/.vim/project-rc/')

let s:project_config = {
  \ s:my_code('cool-site'): s:localrc('cool-site.vim'),
  \ s:work_code('legacy-site'): s:localrc('legacy-site.vim')
  \ }

let s:file = get(s:project_config, getcwd(), '')

if filereadable(s:file)
  execute 'source' s:file
  execute 'nnoremap <C-x>c :edit' s:file '<cr>'
else
  execute 'nnoremap <C-x>c :edit' s:localrc('')
endif

execute 'nnoremap <C-x>C :edit' expand('<sfile>:p') '<cr>'
```

And this will work just fine.

What about the integrity of files? I haven't dealt with that just yet. But I figure you could manage `~/.vim/project-rc/` with `git`. Next, find a way to do a `git status -s <file>` before executing the file, so if `git status` gives you something then don't run the file (it should be clean). And just commit every change when you modify anything in `~/.vim/projects-rc`.

That's it for today. Let me know what you think about this.

Update (august 2021):

This solution of mine, I recommend using it as an "after plugin". Basically, save the script in this path `~/.vim/after/plugin/localrc.vim`.

That should make vim source that script automatically after your "regular" plugins.

Now for the neovim fans out there, if for some reason you would want this but written in lua, here it is:

```lua
local fn = vim.fn
local map = function(lhs, rhs)
  vim.api.nvim_set_keymap('n', lhs, rhs, {noremap = true})
end

local prefix = function(path)
  return function(file) return fn.expand(path .. file) end
end

local my_code = prefix('~/Projects/')
local work_code = prefix('~/some/path/to/your/work/folder/')
local localrc = prefix('~/.config/nvim/project-rc/')

local project_config = {
  [my_code('cool-site')] = localrc('cool-site.lua'),
  [work_code('legacy-site')] = localrc('legacy-site.lua')
}

local file = project_config[fn.getcwd()] or ''

if fn.filereadable(file) == 1 then
  dofile(file)
  map('<C-x>c', ':edit ' .. file)
else
  map('<C-x>c', ':edit ' .. localrc(''))
end

map('<C-x>C', ':edit ' .. fn.expand('<sfile>:p'))
```

Save this in `~/.config/nvim/after/plugin/localrc.lua` and it should work like a charm.

---

If you find this article useful and want to support my efforts, [buy me a coffee ☕](https://www.buymeacoffee.com/vonheikemen).

[![buy me a coffee](https://res.cloudinary.com/vonheikemen/image/upload/v1618466522/buy-me-coffee_ah0uzh.png)](https://www.buymeacoffee.com/vonheikemen)