LSP: project-specific settings

I’ve had the need to have project specific settings for my LSP servers for a while now. I’ve been asked to create a discourse post about this to explore my use case and possible solutions.

The reason I need project-specific settings is that I’m working on Ada programs and using the Ada Language Server. The Ada programming language has a build tool named GPRBuild, which you can think of as somewhere in-between Cmake and Cargo. GPRBuild is driven by GPRFiles (files whose extension is .gpr, but with not other standard naming scheme) and in order to be able to correctly resolve dependencies, the Ada Language Server needs to be able to read GPRFiles. It’s not uncommon for projects to have multiple GPRFiles, and for the user to choose one or the other depending on what they’re trying to do. You can configure what GPRFile the Ada Language Server should use by specifying settings, e.g.

require("lspconfig").als.setup{ settings = { ada = { projectFile = "gnat.gpr" } } }

However, I need something more flexible - I’m only going to use the gnat.gpr file when working on Gnat, other projects will use other gpr files.

The officially-recomended approach is to use an exrc: add a file to your project directory that will be automatically executed by neovim on startup and use that to configure the LSP.

I have three problems with that approach though:

  1. I never start neovim in my project’s repository, so exrc files aren’t read.
  2. I don’t like litering my project directories with editor-specific files.
  3. I don’t like the security implications.

On matrix, I’ve been advised to look into the on_init hook and the workspace/didChangeConfiguration notification. It required reading Neovim’s source code, but I eventually managed to figure out how to use them in order to do what I need:

local function als_on_init(client)
  client.config.settings.ada = { projectFile = "gnat.gpr" }
  client.notify("workspace/didChangeConfiguration")
  return true
end
require("lspconfig").als.setup{ on_init = als_on_init }

This piece of code lets me programmatically choose what settings to send to the ALS no sooner than when the neovim client is connecting to it, and without having to source any external files! Many thanks to @clason, @mfussenegger and @mjlbach for their advice :).

3 Likes

Thanks for sharing ! I prefer to use the project-specific approach but your concerns are valid and it’s nice that you’'ve shared your solution.

1 Like

Loading Vimscript (or Lua) automatically on the fly is insecure. However, loading JSON should be safe since it cannot contain executable code. I have several rules set up for Lua so I can load the correct settings for different projects (because of how Lua is supposed to be embedded there is no default Lua use case). One of the rules looks like this:

function M.has_local_settings_file()
	return filereadable('lua-lsp.json')
end

Then I can do something along these lines:

local config = {...}  -- Default configuration
if (has_local_settings_file()) then
    config.settings = function ()
        vim.fn.json_decode(vim.fn.readfile('lua-lsp.json'))
    end,
end

I have a few more rules and the setup is a bit more compilcated, but that’s the gist of it. You still have to create an extra file, but it works for language servers which do not have a configuration mechanism built in.