Configuring Vim for Ada projects

Configuring Vim settings

Many default settings in Vim only work when programming in C which means you can’t use gf on a package to go to its file definition in Ada. The following settings will fix these shortcomings

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