Configuring Neovim for Ada projects

This document covers Ada support for the latest Neovim plugins. It’s going to be a living document for several reasons. The reason plugins might be desired is that the native Vim experience is missing some functionality.

Tools to look at

There are several external tools that can be hooked into Vim/Neovim that can provide first-class experiences to languages outside of your typical C style languages.

Language Servers

Language Servers describes a standard protocol for communication between the editor and a language server which performs the gruntwork of an IDE (linting, navigation, definition hints, etc).

Tree-sitter

A quick note about Tree-sitter is that it does not support Ada at the time of writing. There were performance issues when attempting to implement it. Nonetheless, it is worth keeping an eye on this because it allows some really cool functionality in Neovim.

Tree-sitter incrementally parses source code to provide syntax trees. This allows for robust syntax highlighting that does not rely on regex. Further, the code does not have to be syntactically correct. Tree-sitter can work even with syntax errors. There are other interesting Neovim plugins which leverage tree-sitter’s parsing.

Configuring Vim settings

Prior to language servers, we would set the suffixesadd and the path to allow searching for files when using gf. While this still works, it has a shortcoming. That is it requires a file name convention for it to work. Using the Ada language server makes this so much easier.

setlocal suffixesadd=.ads

let projprefix = '/path/to/project'

" Change casing of file to lowercase and replace subpackage '.' with '-'
" Note, the file naming scheme must follow what the GNAT compiler expects for
" this to work.
setlocal includeexpr=tolower(substitute(v:fname,'\\.','-','g'))

" Set the path variable to include the directories 'src', 'test', and all their
" subdirectories
let &l:path .= join([projprefix . '/src/**', projprefix . '/test/**'], ',')

It would also be nice to be able to search for files in Vim. Setting the path option will enable that built-in functionality. First, define the root repository directory. Then use globs to find every source directory in that root directory.

let projprefix = '/path/to/project/root'

" Set path for system includes
let sysincludes = '/path/to/gcc/adainclude'
let &l:path = sysincludes

" The following creates a list of all the files in the 'src' and 'test'
" directories.
let &l:path .= ',' . join([projprefix . '/src/**', projprefix . '/test/**'], ',')

Configuring the built-in Ada plug-in

Check out :h ft_ada for the complete summary of options. First, let’s set g:gnat.Make_Command to use gprbuild instead of gnatmake. We want the gprbuild command to compile the project file we are using.

call g:gnat.Set_Project_File("project.gpr")
let g:gnat.Make_Command = '"gprbuild -P " . self.Project_File'

The latest GNAT compiler has a new error format that we need to add to. Below, I set the default error formats and append the new error format to the end. This is to prevent the error format string from growing each time my Vim configuration script is sourced.

let g:gnat.Error_Format = '%f:%l:%c: %trror: %m,'
let g:gnat.Error_Format .= '%f:%l:%c: %tarning: %m,'
let g:gnat.Error_Format .= '%f:%l:%c: (%ttyle) %m,'
" The following is the new error format
let g:gnat.Error_Format .= '%f:%l:%c: %m'

Configuring the ALE plug-in for Ada

As of the time of writing, ALE only supports GCC for linting Ada code. I am working on adding support for GPRBuild. In addition I want to add a fixer and language server.

Configuring GCC

First, let’s enable the ALE linter for Ada.

let b:ale_linters = ['gcc']

Let’s also enable some generic fixers that ALE provides. These fix common whitespace issues.

let b:ale_fixers = ['remove_trailing_lines', 'trim_whitespace']

When using ALE to lint Ada files, you’ll find that it won’t search for include directories outside of the current directory. Search for the ALE documentation in Vim and you’ll find ale-ada-gcc which lists multiple variables which can be modified. The variable we’re interested in is b:ale_ada_gcc_options which we will append our include search paths to.

It would be nice to either 1) determine your include directories from your gpr file, or 2) recursively add all source directories in your project to the search path. While I couldn’t figure out the former, I could leverage what I did above to define the path option.

" Join the include directories prefixing each directory with the '-I' flag
let incdirs = ' -I ''' . join(srcdirs, ''' -I ''')

" Add third party include libraries to search path where the text in angle
brackets are placeholders for the actual library paths.
let b:ale_ada_gcc_options = '-I /<inc_prefix>/<lib1> -I /<inc_prefix>/<lib2> '

" Add project source directores to search path
let b:ale_ada_gcc_options .= incdirs

Configuring GPRBuild

I added support for GPRBuild because more complex builds at work required many GCC flags to correctly check the syntax and semantics of source files. Instead of determining the correct flags to pass to GCC, it would be easier to use GPRBuild to lint code. I currently use this in place of GCC for linting. The following variables need to be configured:

let b:ale_linters = ['gprbuild']
let b:ale_ada_gprbuild_project = '/path/to/project.gpr'

Wrapping it all up

We do not want to load project specific settings for every Ada file we open. Let’s only setup project specific settings when we’re editing Ada source files in the project we’re interested in. The entire Vimscript is below.

let g:gnat.Make_Command = '"gprbuild -P " . self.Project_File'

let g:gnat.Error_Format = '%f:%l:%c: %trror: %m,'
let g:gnat.Error_Format .= '%f:%l:%c: %tarning: %m,'
let g:gnat.Error_Format .= '%f:%l:%c: (%ttyle) %m,'
" The following is the new error format
let g:gnat.Error_Format .= '%f:%l:%c: %m'

let b:ale_linters = ['gcc']

setlocal suffixesadd=.ads
setlocal include=^\s*with

function! GetAdaInclude(fname, prefix)
    " Change casing of file to lowercase and replace subpackage '.' to '-'
    " Note, this depends on the naming scheme being used. This works with
    " GNAT's recommended naming scheme.
    let file = tolower(substitute(a:fname,'\\.','-','g'))

    " Search for the file in all subdirectories of the project, appending
    " ".ads" to the search string
    return findfile(file, a:prefix . '/**/*')
endfunction

" Project specific settings go in here
if match(expand('%:p'), '/path/to/project/root') != -1
    let projprefix = '/path/to/project/root'

    let &l:includeexpr = GetAdaInclude(v:fname, projprefix)

    " Find all files in the src and test subdirectories
    let srcdirs = glob(projprefix . '/src/**', 1, 1) + glob(projprefix . '/test/**', 1, 1)

    " Remove file names from paths. Sort the list of paths and remove
    " duplicates.
    call uniq(sort(map(srcdirs, 'fnamemodify(v:val, ":h")')))

    " Set the path variable
    let &l:path = join(srcdirs, ',')

    " Set the GPR file
    call g:gnat.Set_Project_File("project.gpr")

    " Join the include directories prefixing each directory with the '-I' flag
    let incdirs = ' -I ''' . join(srcdirs, ''' -I ''')

    " Add third party include libraries to search path
    let b:ale_ada_gcc_options = '-I /<inc_prefix>/<lib1> -I /<inc_prefix>/<lib2> '

    " Add project source directores to search path
    let b:ale_ada_gcc_options .= incdirs

    " Add generic fixers
    let b:ale_fixers = ['remove_trailing_lines', 'trim_whitespace']
endif