In the last article, we discussed the evolution of my development setup, focusing on how it became reproducible over time. It was originally written to document the process for my own amusement. Surprisingly, the stats tell a different story — apparently it resonated with enough people sharing the same pain. Over time, revisions were made alongside new discoveries, and I figured the journey was worth a retelling. Read on to find out about an unexpected milestone related to this very blog.
Cute illustration on the topic, by Microsoft Copilot
The Editor’s Blind Spot: Nix vs. VS Code Extensions
Up until the last article, we managed to get a rather consistent setup with home-manager to manage development tools, and subsequently retired tools likepyenv andrbenv with the use ofnix-develop. Little did I know, it wasn’t the end of the story. This time, we are tackling a new issue — the lack of nix-develop support of the Visual Studio Code extensions for development work in Ruby.
- From Dotfile Hacks to Open Source: My Development Environment Evolution
- Beyond rbenv and pyenv: Achieving Reproducible Dev Setups with Nix
Forcing the ruby-lsp extension to source an external Gemfile as detailed was a hackish solution. Although it worked for a bit, most of the time the extension failed to initialize the language server. Observing how inconsistent the behavior is, something must be wrong. On the other hand, setup seems impeccable, tests were passing, and projects were building fine too.
Unfortunately, not much luck with the team’s desired formatter.
It wouldn’t work in the editor.
A type checker extension called sorbet is also common, and that too wouldn’t work at all. Considering the team doesn’t type annotate the code, I quickly dismissed and deactivated the extension. Combining this with the previous problem where ruby-lsp failed to work, the itch grew as time passes on.
Photo by Abu Saeid on Unsplash
Worse still, a formatter that doesn’t work at all times yields errors in the automated checks on every push.
Something was definitely wrong, was it because of VSCode failing to pick up the environment in a nix-develop session? My initial attempts to fix the flake file to include a command to activate the language server properly as shared. Still, none of them seemed to work reliably.
How To: Use vscode-ruby-lsp with flake.nix based dev enviroment · Issue #1851 · Shopify/ruby-lsp
After hitting walls enough times, I decided to post my setup to the discussion board to seek for help.
The Log Line That Clicked Differently
A problem can only be solved if we can clearly define it. We know the setup is failing, but what caused it? Interestingly enough, achieving the problem definition is a series of unlikely coincidences. It almost felt like solving a jigsaw puzzle, where each seemingly unrelated piece ultimately leads to the solution. So what are the missing pieces stalling our progress in understanding what went wrong?
Let’s start with the discovery of previously unused --command argument for nix-develop.
Initially, the gemini-generated flake file for the nix-develop sessions contained some commands to start my favorite fish shell. The snippet on starting a new shell was taken out from the previous discussion as it was somewhat unrelated. It was rather complicated and bound to fail because I was expecting it to work differently in different scenarios. Naturally, I wanted to simplify it somewhat, and eventually I learned about the --command argument.
Instead of relying on a clever (or dumb, depending on how you perceive it) detection logic, I could start a new fish shell session with
nix develop /path/to/flake/dir --command fish
Besides a shell, it could also run any command in the environment defined by the flake file. Discovering this was a crucial step toward fixing the launch of the ruby-lsp language server in the editor.
Remember I posted an SOS message on the discussion board? Luckily, it caught the attention of Vinicius Stock, the project owner. Helpfully enough, he pointed out that an external Gemfile is not necessary, and asked me to check if the language server is launched properly. With that, I checked the log and found this line
2025-07-27 16:56:30.669 [info] (ship_it) Running command: `nix develop /Users/jeffrey04/Projects/home-manager/devenvs/ruby-3.3 && ruby -EUTF-8:UTF-8 '/Users/jeffrey04/.vscode-oss/extensions/shopify.ruby-lsp-0.9.31/activation.rb'` in /Users/jeffrey04/Projects/ship_it using shell: /Users/jeffrey04/.nix-profile/bin/bash
Despite seeing the log multiple times before, now that I know about the --command argument, the same line clicked differently. Does the ruby command after the && operator run in the nix-develop session? Validating in this case is rather easy, just run
nix develop /path/to/flake/dir && ruby --version
Oops, why am I being dropped to a new shell session? Let me exit the session, and sure enough, the ruby command refers to the version offered by the operating system.
❯ nix develop ~/Projects/home-manager/devenvs/ruby-3.3 && ruby --version
warning: updating lock file "/Users/jeffrey04/Projects/home-manager/devenvs/ruby-3.3/flake.lock":
• Added input 'nixpkgs':
'github:NixOS/nixpkgs/544961dfcce86422ba200ed9a0b00dd4b1486ec5?narHash=sha256-EVAqOteLBFmd7pKkb0%2BFIUyzTF61VKi7YmvP1tw4nEw%3D' (2025-10-15)
(nix:ruby-3.3-env) wukong:ib-vpn jeffrey04$
exit
ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.x86_64-darwin24]
Photo by Matias Malka on Unsplash
Clearly, the assumption that the command following the && operator is flawed from the very beginning. With this revelation, we can finally define what the problem is — the language server is not properly launched. In this case, all we need to do is to replace the boolean AND operator into --command.
Easy to say, but implementation is a challenge for someone who has no experience with vscode extension development like myself.
And this is exactly when vibe-coding is useful, with a clear problem definition and goal, the solution was just a prompt or two away with the help of gemini-cli. As expected, the patch worked flawlessly.
Firstly, add a new enum entry to ManagerIdentifier in vscode/src/ruby.ts
export enum ManagerIdentifier {
Asdf = "asdf",
Auto = "auto",
Chruby = "chruby",
Rbenv = "rbenv",
Rvm = "rvm",
Shadowenv = "shadowenv",
Mise = "mise",
RubyInstaller = "rubyInstaller",
NixDevelop = "nix-develop", # add this line
None = "none",
Custom = "custom",
}
Then a new case in the runManagerActivation method
case ManagerIdentifier.NixDevelop:
await this.runActivation(
new NixDevelop(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)),
);
break;
Followed by a new module at vscode/src/ruby/nixDevelop.ts that contains the activation logic:
import * as vscode from "vscode";
import { VersionManager, ActivationResult } from "./versionManager";
export class NixDevelop extends VersionManager {
async activate(): Promise<ActivationResult> {
const customCommand = this.customCommand();
const command = `nix develop ${customCommand} --command ruby`;
const parsedResult = await this.runEnvActivationScript(command);
return {
env: { ...process.env, ...parsedResult.env },
yjit: parsedResult.yjit,
version: parsedResult.version,
gemPath: parsedResult.gemPath,
};
}
private customCommand() {
const configuration = vscode.workspace.getConfiguration("rubyLsp");
const customCommand: string | undefined =
configuration.get("customRubyCommand");
return customCommand || "";
}
}
Lastly, enable the option forrubyVersionManager in vscode/package.json
"rubyLsp.rubyVersionManager": {
"type": "object",
"properties": {
"identifier": {
"description": "The Ruby version manager to use",
"type": "string",
"enum": [
"asdf",
"auto",
"chruby",
"none",
"rbenv",
"rvm",
"shadowenv",
"mise",
"nix-develop", // add this line
"custom"
],
Don’t forget about the actual config (omit the rubyLsp.customRubyCommand if flake.nix is in the workspace)
"rubyLsp.rubyVersionManager": {
"identifier": "nix-develop"
},
"rubyLsp.customRubyCommand": "/Users/jeffrey04/Projects/home-manager/devenvs/ruby-3.3",
The patch is definitely not perfect, and it is reusing the optional customRubyCommand configuration item to define where the directory containing the flake file is. Yet, considering this likely affects only a small group of users, I don’t see a need to introduce a new configuration field for the purpose. Contributions are definitely welcome at the submitted pull request, as it has been stalling for months.
For the sorbet part, I managed to get it working one day out of boredom. Most of the Ruby code I have access for now is not type-annotated, so the effort is purely for fun. A quick and dirty way to do so, is building the language server application directly, and configure vscode as follows
"sorbet.enabled": true,
"sorbet.selectedLspConfigId": "without-bundler",
"sorbet.userLspConfigs": [
{
"id": "without-bundler",
"name": "sorbet",
"description": "Launch locally compiled sorbet",
"command": ["/path/to/sorbet", "typecheck", "--lsp", "--dir=/path/to/project"]
}
]
For some reason I needed to --disable-watchman flag in the command too, but as there’s no real need for now I am not going to dig further.
There you have it, with simplified flake files (removing the detection logic to start fish shell) and invoking fish shell explicitly with --command, and patches and hacks to enable the ruby toolings, we finally have a functional setup for development work in Ruby. Seeing the pull request being stalled for months (and their bot closed my PR recently due to inactivity) does make me upset for a little bit. Moving forward I may need to consider maintaining a fork for it as it does affect my work.
Stalled PR, Sudden Payoff: The Two Timelines
Photo by engin akyurt on Unsplash
Patches to the ruby-lsp were submitted in the late July, and I was hoping to start drafting this article when it received a review. Unfortunately, that didn’t happen. I was caught up with my new job and several major family issues while waiting on the PR. Things finally settled down for a little bit, and I should be back to a relatively more predictable routine. Hopefully this translates to a more consistent publishing schedule for KitFu Coda.
As for the unexpected milestone teased in the beginning, it is about a talk I am about to deliver during the upcoming PyCon Malaysia. My mentor encouraged me to submit for the CFP and it somehow went through. If you are interested in applying AsyncIO in projects, do feel free to drop by and say Hi. For reference, it is based on the articles I published earlier, as linked below.
- Understanding Awaitables: Coroutines, Tasks, and Futures in Python
- AsyncIO Task Management: A Hands-On Scheduler Project
- Concurrency vs. Parallelism: Achieving Scalability with ProcessPoolExecutor
I also discussed briefly with my supervisor in one of our 1 on 1 sessions, mainly to check on any necessary approvals or conflict of interest. He was incredibly supportive about it and offered some tips on presenting ideas to a wider audience. As I am new to Ruby, he also briefly explained how the language approaches asynchronous programming. On a lighter note, he’s even been checking on my preparation progress (zero so far), especially in recent sessions!
With this article checked off in my TODO, I can finally allocate some time on the preparation work. I sometimes feel like I’m running on an energy tank whose maximum capacity depletes as we age. These days, even finding time to game is a challenge. I feel bad for my character in Xenoblade X and I wonder if I’ll ever have time for the new Pokémon Legends Z-A game I’m about to purchase.
Finally, I would like to thank everyone for the support, especially to everyone who reads my work here. Though I have not been publishing much, since starting this technical writing project opens up fascinating new opportunities. Besides scoring a new job, I also started exploring scripted video making, and now a talk. I am thankful and hope to inspire more people through this trying time.
Thanks again for reading, I shall write again soon.
Throughout this journey, Gemini served as a valuable editorial companion, helping refine my prose. However, the voice and code remain entirely my own. I welcome your feedback and recommendations in the comments below, and invite you to subscribe to my Medium for more content on my development adventures!
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We don’t receive any funding, we do this to support the community. ❤️
If you want to show some love, please take a moment to follow me on LinkedIn , TikTok, Instagram. You can also subscribe to our weekly newsletter.
And before you go, don’t forget to clap and follow the writer️!
Top comments (0)