Tip 28Setting Buffer-Local Configuration Per Project

Autocommands and ftplugins allow you to apply settings to all files of a particular filetype. But what if you want to apply settings to files within a directory? Vim doesn’t have a built-in mechanism for this, but you can achieve this effect using the Projectionist plugin.

Preparation

In this tip, you’ll use two plugins: Projectionist by Tim Pope,[57] and ALE by Andrew Wray.[58] You can install these to your bundle package like this:

=> $ cd $VIMCONFIG/pack/bundle/start
=> $ git clone https://github.com/tpope/vim-projectionist.git
=> $ git clone https://github.com/w0rp/ale.git

Run :helptags ALL to index the documentation for these plugins.

The Projectionist plugin is the main subject here, while the ALE plugin is merely used for illustrative purposes. Check out Tip 12, Linting the Current File for details on how the ALE plugin works.

Setting Up the Demo Projects

You’ll use two different projects as you work through this tip. Both are simple JavaScript projects, but each one is configured to use a different linting tool. The linting project uses eslint, whereas the hinting project uses jshint. You’ll need to install the dependencies for each project.

First, change to the linting directory and install its dependencies. Then run eslint to check that it works:

=> $ cd code/linting
=> $ npm install
<= [email protected] /Users/drew/modvim/code/linting
 └── [email protected]
 
=> $ ./node_modules/.bin/eslint date-in.js
<=  1:33 error Strings must use doublequote quotes
  7:5 error 'offset' is constant no-const-assign
  9:5 error 'offset' is constant no-const-assign
 13:47 error Missing semicolon semi
 
 ✖ 4 problems (4 errors, 0 warnings)

Next, do the same for the hinting project:

=> $ cd code/hinting
=> $ npm install
<= [email protected] /Users/drew/modvim/code/hinting
 └── [email protected]
=> $ ./node_modules/.bin/jshint date-in.js
<= date-in.js: line 15, col 47, Missing semicolon.
 
 1 error

Add the following lines to your vimrc:

 let​ g:ale_linters = {
 ​ ​'javascript'​: [​'eslint'​],
 ​ }

This makes ALE use eslint for JavaScript files.

Configuring ALE Globally and Locally

The ALE plugin automatically lints the current buffer. For JavaScript files, ALE supports many linting tools, including eslint and jshint. In preparation for this tip, you set the g:ale_linters variable, making ALE use eslint for all JavaScript files. That’s exactly the behavior we want for the linting project. But we’re going to have to find a way to override this preference for the hinting project.

In both linting and hinting projects, you’ll find a file called date-in.js. Open both files using one tab page for each project:

=> $ cd code
=> $ vim -p linting/date-in.js hinting/date-in.js

Use :tabfirst to activate the linting project and :tablast to activate the hinting project.

You can get details about how ALE is configured for the current file using the :ALEInfo command. At present, running this command in either project will print the same information:

=> :ALEInfo
<= Current Filetype: javascript
 Available Linters: ['eslint', 'flow', 'jscs', 'jshint', 'standard', 'xo']
  Enabled Linters: ['eslint']
 ...

In both cases, the current filetype is JavaScript and the only enabled linter is eslint.

Activate the hinting project, then set the b:ale_linters variable as follows:

=> :tablast
=> :let b:ale_linters = {'javascript': ['jshint']}
=> :ALEInfo
<= Current Filetype: javascript
 Available Linters: ['eslint', 'flow', 'jscs', 'jshint', 'standard', 'xo']
  Enabled Linters: ['jshint']
 ...

For the hinting/date-in.js file, ALE now uses jshint instead of eslint. That’s because ALE gives higher precedence to the buffer-local b:ale_linters variable than the global g:ale_linters equivalent. The linting/date-in.js file does not have b:ale_linters set, so it still uses the global setting.

Now, we have the behavior we want: ALE defaults to using eslint for JavaScript files, but it uses jshint where specified. Next, let’s find a way to automate this so that we don’t have to set b:ale_linters by hand.

Setting Local Variables for Files in a Project

Vim provides a couple of mechanisms that allow you to apply settings by filetype: either by creating a filetype plugin or by using autocmds (as discussed in Tip 26, Using Autocommands to Respond to Events). Those methods won’t help in this case, because we want to apply different settings to two different files with the same filetype.

Another way to approach this problem is to say that we want to apply different settings for each project. That’s where the Projectionist plugin can help. By placing a .projections.json file in the root directory of your project, you can make Projectionist apply customizations that only affect the files contained in that directory. The JSON format allows you to specify a filepath or glob, then attach metadata to any buffers that match that pattern.

In the hinting directory, create a .projections.json file with the following contents:

 {
 "*.js"​: {
 "linters"​: [​"jshint"​]
  }
 }

If you open any file in the hinting directory, the metadata specified in the .projections.json file will be attached to the buffer by means of a variable called b:projectionist.

When you started your current Vim session, the .projections.json file didn’t exist, so the b:projectionist variable isn’t currently set on any of your buffers. To fix this, you can either restart Vim or use the :edit! command to reload your existing buffers. (If you choose to restart Vim you might want to record a session first. See Tip 23, Saving and Restoring Sessions to find out how.) Next, inspect the b:projectionist variable on the hinting/date-in.js file:

=> :bufdo edit!
=> :edit hinting/date-in.js
=> :echo b:projectionist
<= {
  '/Users/drew/modvim/code/hinting': [
  {'*.js': {'linters': ['jshint']}}
  ]
 }

You can use the projectionst#query(key) function to retrieve metadata for a specified key. This function takes the filepath of the active buffer into account. When the active buffer is hinting/package.json or linting/date-in.js, a query for the ’linters’ key returns an empty list:

=> :edit hinting/package.json
=> :echo projectionist#query('linters')
<= []
=> :edit linting/date-in.js
=> :echo projectionist#query('linters')
<= []

In the case of hinting/package.json, the b:projectionist metadata is available, but the filepath doesn’t match the *.js pattern. Whereas the linting/date-in.js file is in a different directory entirely, so your projectionst configuration doesn’t even apply there.

When the hinting/date-in.js buffer is active and you query for the ’linters’ key, you get a result (here and in later examples, I’ve pretty-printed the output from :echo to make it fit the page):

=> :edit hinting/date-in.js
=> :echo projectionist#query('linters')
<= [
  ['/Users/drew/modvim/code/hinting', ['jshint']]
 ]

The list contains one result: itself a list of the form [root, value]. The root is the directory where the .projections.json file is defined (we will explore the significance of this in the next section). The value of [’jshint’] is the part that we are interested in.

Remember what we’re trying to achieve here: we want to define a local b:ale_linters variable so that all JavaScript files in the hinting directory use jshint. What we’ve done so far is define a projection that matches js files, and attaches metadata to those buffers with the ’linters’ key. Next, we want to define a b:ale_linters variable for any buffers that have the ’linters’ metadata attached.

Copy this snippet of Vim script into your vimrc file and reload it:

 augroup configure_projects
  autocmd!
  autocmd User ProjectionistActivate ​call​ s:linters()
 augroup END
 
 function​! s:linters() abort
 let​ ​l​:linters = projectionist#query(​'linters'​)
 if​ len(​l​:linters) > 0
 let​ b:ale_linters = {&​filetype​: ​l​:linters[0][1]}
 endif
 endfunction

The ProjectionistActivate autocommand lets you run code after Projectionist has attached metadata to a buffer. Here, the autocommand triggers the s:linters() function, which queries for the ’linters’ key. If the active buffer has a value for that key, then b:ale_linters is set to a dictionary, associating the current filetype with the specified linters.

Use :qa! to quit Vim, then open the two date-in.js files again in two separate tabs:

=> $ vim -p linting/date-in.js hinting/date-in.js

When the linting/date-in.js is active, ALE uses eslint. But when the hinting/date-in.js is active, ALE uses jshint:

=> :tabfirst
=> :ALEInfo
<= Enabled Linters: ['eslint']
=> :tablast
=> :ALEInfo
<= Enabled Linters: ['jshint']

That’s just what we wanted!

Projectionist Specificity

Metadata that you specify in a projections.json file will be attached to all files beneath that directory. You can place a .projections.json configuration file in more than one directory, which means it’s possible for a file to have metadata attached to it from more than one set of projections.

Let’s explore this idea. Switch to the code/hardwrap directory:

=> $ cd code/hardwrap

This directory contains a couple of plaintext files and a couple of Markdown files, each containing pseudo-Latin placeholder text:

 hardwrap
 ├── .projections.json
 ├── lorem-ipsum.md
 ├── pellentesque.txt
 └── subdir
  ├── .projections.json
  ├── maecenas.txt
  └── vestibulum.md

In the project root, you’ll find a .projections.json file containing the following:

 {
 "*"​: { ​"hardwrap"​: ​"78"​ }
 }

The "*" pattern matches all files within the hardwrap directory, as well as all files in any subdirectories. Open the lorem-ipsum.md file and query Projectionist for the ’hardwrap’ key:

=> :edit lorem-ipsum.md
=> :echo projectionist#query('hardwrap')
<= [
  ['/Users/drew/modvim/code/hardwrap', '78']
 ]

If you run the same query in the pellentesque.txt and subdir/vestibulum.md files, you’ll see the same output.

In the subdir directory, you’ll find another .projections.json file containing the following:

 {
 "*.txt"​: { ​"hardwrap"​: ​"42"​ }
 }

This pattern only matches files with the txt extension. Open the subdir/maecenas.txt file and query Projectionist for the ’hardwrap’ key:

=> :edit subdir/maecenas.txt
=> :echo projectionist#query('hardwrap')
<= [
  ['/Users/drew/modvim/code/hardwrap/subdir', '42'],
  ['/Users/drew/modvim/code/hardwrap', '78']
 ]

This time the list of matches contains two results: one from each .projections.json file. The proximity between the current buffer and the Projections config file determines the order of the results.

Suppose you want to use the value of ’hardwrap’ for Vim’s ‘textwidth’ setting. You can either use the value 42 or 78, but not both! It would make sense here to use the value from the .projections.json file closest to the current buffer, which is the first result from the query.

This snippet of Vim script would do the trick:

 augroup configure_projects
  autocmd!
  autocmd User ProjectionistActivate ​call​ s:hardwrap()
 augroup END
 
 function​! s:hardwrap() abort
 for​ [root, value] ​in​ projectionist#query(​'hardwrap'​)
 let​ &​l​:textwidth = value
 break
 endfor
 endfunction

The ProjectionistActivate autocommand triggers the s:hardwrap() function. This queries Projectionist for the ’hardwrap’ key, then loops through the results and uses the matching value to set the buffer-local ‘textwidth’ option. The break statement causes the loop to exit after its first execution. The outcome is that the first result from the query is used, while any subsequent results are discarded. (Taking out the break statement would make the last item in the list stick.)

Designing Generalized Projections

In the first example, you used the key ’linters’ to attach data that would be used to set the b:ale_linters variable. In the second example, you used the key ’hardwrap’ to set the ‘textwidth’ option. You might wonder: Why not rename the ’linters’ key to ’ale_linters’, and the ’hardwrap’ key to ’textwidth’? That would give a more accurate description of how the metadata is being used.

You can do that if you like, but you might regret it later when you switch from ALE to another linting plugin. In that scenario, you could adapt the s:linters() function in your vimrc, so that instead of setting the b:ale_linters variable, it would do something equivalent for your preferred linting plugin. Using a generic key such as ’linters’ makes it easy to imagine other ways the metadata could be used.

Projections are defined in a json file. That means you can safely check them into source control, just like an .editorconfig file. By contrast, a Vim script file can be executed, making it a potential security risk. See What About Local vimrc Files?.

The .projections.json file format is editor-agnostic. In theory, you could use it to configure any text editor or tooling. I can’t name any other text editors that use projections today, but it’s a useful format and it might spread beyond the Vim ecosystem. When defining projections, I prefer to use keys that are abstract, rather than choosing keys that are tightly coupled to Vim’s options or to a particular plugin.

Built-in Projections

Some keys have special meaning built in to the Projectionist plugin. The following table picks out some of the highlights:

KeyPurpose

path

Adds a list of directories to Vim’s ‘path’ setting. This affects the behavior of the gf and :find commands.

make

Sets the ‘makeprg’ option. Additionally, it will attempt to set the ‘errorformat’ if a suitable compiler plugin can be found.

dispatch

Sets the b:dispatch variable. This specifies how the :Dispatch command works.

start

Sets the b:start variable. This specifies how the :Start command works.

type

Used for defining a navigation command.

alternate

Sets up an alternate file, which is used by the :A command.

The ’make’, ’dispatch’, and ’start’ keys all support the Dispatch plugin, which I covered in Tip 10, Running a Build and Navigating Failures and Tip 11, Switching Compilers.

The ’type’ key is used heavily in Tip 8, Finding Files Semantically, while the ’alternate’ key is used in Tip 9, Jumping to an Alternate File.

It’s hard to give an elevator pitch on what makes the Projectionist plugin so useful. Hopefully you can see now that it provides the glue that binds together lots of important bits of functionality.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.27.232