Background
When setting up, my development environment usually looks like this
$ROOT
|- foreman
|- foreman-tasks
|-----B<-----MORE PLUGINS-----B<-----
`- foreman_remote_execution
I would dare to say this is pretty standard layout - both forklift-based and foremanctl-based development environments do this.
When using VSCode (or derivatives, $EDITOR in rest of the post), I usually open foreman and then add the other plugin directories to the same workspace.
ruby-lsp is a ruby language server from folks over at Shopify. The problem when trying to use ruby-lsp together with $EDITOR and the official extension (Shopify.ruby-lsp) is that the extension is rather opinionated about how a project looks like, which doesn’t really play well with our plugin architecture.
If you set up the layout as I do, you’ll end up with $EDITOR firing up several instances of the language server - one for each folder in the workspace. Without any configuration and vanilla foreman, most likely all of them will crash and you’ll get a bunch of errors in $EDITOR. There are three possible workarounds (install ruby-lsp as a gem globally, add it to foreman’s Gemfile/bundler.d or configuring the workspace’s rubyLsp.bundleGemfile to point at a minimalistic gemfile that would pull in the gem - personally I had best results with the last one, but ymmv). These should allow the language servers to start. Sadly, even with those running, we’re not quite there yet.
The fact that there are multiple instances of ruby-lsp is somewhat problematic for us. There seems to be an assumption that all the folders in the workspace are completely isolated, so for example trying to “Go to definition” of ::Api::V2::BaseController (which is defined in foreman itself - beyond the folder boundary) while editing a controller in foreman-tasks will just do nothing. These things should work relatively well within the scope of the folder, but crossing that boundary seems to be a no-go.
There is a workaround for that too, even though I wouldn’t dare to call it a solution. We can slightly change how to manage things in $EDITOR to conform with the expectations of the language server.
The actual howto
The workaround is realtively simple. Treat $ROOT as the real project root and provide a Gemfile that would pull in ruby-lsp there. ruby-lsp will then be spawned in that root and scan all the projects cloned within.
Suggested directory structure
$ROOT
|- foreman.code-workspace
|- foreman
|- foreman-tasks
|-----B<-----MORE PLUGINS-----B<-----
|- foreman_remote_execution
`- Gemfile
Root gemfile
# $ROOT/Gemfile
source 'https://rubygems.org'
gem 'ruby-lsp'
gem 'ruby-lsp-rails'
By changing this layout to nest foreman and the plugins within the same “project” from $EDITOR’s point of view, we can trick $EDITOR into spawning just a single instance of ruby-lsp and effectively removing the folder boundary for it.
This gives us single ruby-lsp instance for foreman and all the enabled plugins, allowing things like “Go to definition” to work. It comes at the cost of $EDITOR not treating plugins as individual projects.
This can be resolved with yet another layer of workarounds - configuring the workspace to treat the directories as workspace folders.
# $ROOT/foreman.code-workspace
{
"folders": [
{
"path": ".",
"name": "root"
},
{
"path": "./foreman",
"name": "foreman"
},
{
"path": "./foreman-tasks",
"name": "foreman-tasks"
}
],
"settings": {
"files.exclude": {
"**/node_modules": true,
"foreman/": true,
"foreman-tasks/": true,
},
"rubyLsp.bundleGemfile": "/home/aruzicka/foreman-workspace/Gemfile"
}
}
Note, using absolute path for rubyLsp.bundleGemfile makes it be used for all the ruby-lsps spawned for individual “workspace folders”. Using relative path would make them use a Gemfile relative to their own project roots.
With a setup like this, there will still be multiple instances of ruby-lsp (I haven’t found a way how to prevent that from happening), but navigation across boundaries seems to still work as at least one of them has the whole picture.
When using helix editor, it can be run with functioning LSP from $ROOT with bundle exec hx.
That’s it, thank you for sticking with me till the end, I’m pretty sure I stopped seeing the forest for the trees at some point, so the whole post might be somewhat overcomplicated.
Open questions
If this whole post is reasonable enough:
- Should we capture this in our developer docs?
- Should we guide people towards this by modifying forklift and foremanctl to spawn the supplementary Gemfile in
$ROOT?- Should we go one step further and also generate
$EDITORworkspace config?
- Should we go one step further and also generate