Language servers (implemented via LSPs) need access to the correct respective language binaries and libraries in order to do their jobs.
Nix flakes provide our target environment for developers
When your team uses Nix flakes for package management, including local dev
shells, like we do, you might run into the situation we did with language
servers. Namely, that by default they use system-included binaries and libraries
for things like ruby or python3.
That won’t do in a world controlled by Nix, because you have to get Nix to
provide the correct binaries and environment by loading the devShell for your
project, with all of its dependencies. We do this with the wonderful
Direnv tool: every time a developer changes (cd) into an OMC project
directory, there is a .envrc file that tells Direnv to use flake (among
other things).
When language servers run in an editor, they also need Nix
Okay, so if a developer is running a terminal on their local project, direnv
being loaded into their shell config handles the Nix dev shell setup and
correctly resolves dependencies. But if you are trying to run a language server,
it is running inside of an editor or IDE. Chances are that your editor knows
nothing about Direnv or Nix.
At OMC, we use Zed across the team, mostly because it’s incredibly fast, has great support for editing code across many languages, and has good agent support (we use Claude Code).
So we just had to quickly teach Zed how to use direnv when starting up the
language servers we care about. In this case, we’re looking at one of our Ruby
projects, so the language servers are from the Ruby world.
The Zed project settings file
We all share the following config inside of the project repo:
// .zed/settings.json
{
"languages": {
"Ruby": {
// Use solargraph and rubocop; disable ruby-lsp
"language_servers": ["solargraph", "rubocop", "!ruby-lsp", "..."]
}
},
"lsp": {
// Wrap with direnv so language servers see the Nix flake environment (Ruby 3.3.x + project gems).
// Without this, Zed uses the system Ruby 3.4.x and bundler finds stub specs compiled
// for the wrong Ruby, causing "missing extensions" warnings and wasm trap errors.
"solargraph": {
"binary": {
// Uses bin/solargraph-lsp to fix a solargraph-rails plugin loading issue.
"path": "direnv",
"arguments": ["exec", ".", "solargraph-lsp"]
}
},
"rubocop": {
"binary": {
"path": "direnv",
"arguments": ["exec", ".", "rubocop", "--lsp"]
}
}
}
}
None of this is ground-breaking, but it shows a clean pattern for replacing
two language server binaries with explicit calls to direnv and using its
exec subcommand to set the project context (. is the project’s top-level
directory) and then invoke each language server.
The only wrinkle I hit in this one was that solargraph-rails, for some
reason, relies on the Object#present? method from ActiveSupport. So
Claude helped write a small wrapper script in our ./bin directory that just
wraps the language server with a quick require:
$ cat bin/solargraph-lsp
#!/usr/bin/env sh
# Wrapper for solargraph stdio that pre-loads active_support/core_ext/object/blank
# so the solargraph-rails plugin can call Object#present? without crashing.
exec env RUBYOPT="-ractive_support/core_ext/object/blank" solargraph stdio "$@"
And now our language servers load correctly from Nix via Direnv in Zed!
Ready to take a closer look at Bonsai?
Bonsai manages your search clusters and helps you achieve better search results for your users and your business. Find out if Bonsai is a good fit for you in just 15 minutes.
Learn how a managed service works and why it’s valuable to dev teams
You won’t be pressured or used in any manipulative sales tactics
We’ll get a deep understanding of your current tech stack and needs
Get the information you need to decide whether to go with Bonsai