Last year, I switched to VS Code after a decade using vim in the terminal, and overall the transition has been worthwhile, if not seamless. Its integrated debugger, web editor that syncs your settings, stellar remote pair-programming experience, and GitHub Copilot support offer functionality you can’t really find elsewhere. And that’s just Microsoft’s stuff! It’d be hard to imagine 3rd-party extensions like Tailwind’s class-to-CSS preview implemented in most other editors.
But I don’t write blog posts about the good stuff.
If you’d asked me what I didn’t like about VS Code, I’d have pointed to the
state of Ruby extensions that offer formatting and linting
in VS Code parlance). Most Ruby extensions are pretty slow at this (often
shelling out to a CLI after each save), confusing to configure—especially with
respect to loading gems from Bundler or the
PATH—and often lack many
quality-of-life editor integrations (like providing a status bar item or
populating the Problems view).
And because I help maintain my preferred formatter and linter for Ruby, having a subpar Standard Ruby experience myself made it hard to recommend VS Code to others.
If you’re like me and have been nodding along for the last year as people gushed about “language servers” but had no clue what everyone was talking about, this is a decent overview. They’re not as complicated as you might think.
Go install the Standard Ruby extension from the Visual Studio Marketplace, and you’ll see all it does:
- Tells VS Code which
standardrbexecutable to launch in LSP server mode
- Implements VS Code’s Formatting API, enabling automatic format on save
- Populates the Problems tab with any parse failures, linter warnings, and formatter errors
- Adds a custom status bar item indicating whether the current file has any problems
- Offers a “Format with Automatic Fixes” command that can be run ad hoc or bound to a keyboard shortcut
- Restarts itself whenever anything changes that could impact your Standard Ruby configuration
- Disables itself if it detects you’re working in a project that doesn’t use
Standard or if the current file is ignored by your
Best of all: it’s fast! Distressingly fast. “How did it even run already?” fast.
I don’t care if you use this extension. I do care that whatever you use, it’s fast. If your formatter is slow and runs on every save, turn it off. Run it before each commit or something. No formatter is better than a slow formatter.
This extension is fast because its LSP server is launched once per workspace and it passes each file to an in-memory instance of the RuboCop runner. As a result, both its linting and formatting actions are often imperceptibly fast, even for large files. If you compare this experience to an extension that shells out to a CLI to spawn a new Ruby process and repeatedly loads the same gem dependencies every time a file is saved, it will be orders of magnitude slower. No amount of caching can make up for it.
The reason this is so important is simple: anything that slows down your editor’s ability to write a file to disk will prevent any downstream tool from reading your changes. (Like, say, your Rails development server!)
I finally broke down and decided to build this extension when it started taking multiple seconds to save Ruby files with the extensions I had been using previously. The timing was awful, too—just as a critical delivery deadline was approaching, my productivity ground to a halt. I turned all my Ruby extensions off.
Many of Standard’s rules were chosen because we want Ruby to be easy to write naturally without the aid of an auto-formatter. No fancy alignment rules that look pretty but would be a pain to type by hand. No having to backspace and reformat an earlier line because of something you wrote in a later one. After several years of using Standard, I’ll regularly write entire classes and find they were already formatted correctly.
Here’s what my workflow looked like with a slow auto-formatter:
- Save a file to fix a bug
Command-tabto my browser
- Load the page
- See that bug is still there
Command-tabto my editor
- Try a different solution and save the file again
Command-tabto my browser
- Reload the page
- See my fix from Step 1 represented on the page
- Slowly realize that I had loaded the page in Step 3 before VS Code had written the file to disk, so Rails used the previous version instead
- Come to grips with the fact that Steps 5-12 were a total waste of time
- Go back and revert whatever changes I made in Step 6
All of this applied to test runs, too. I have
Command-R bound to a launch
config that runs the current test. Although the command itself triggers a file
save, it will nevertheless happily start running the test even if those changes
aren’t written to disk yet. Let me tell you: test-driven development is way less
fun when half the time you make a test pass, it fails anyway because your
$3000 computer was too slow to write a 2KB file in under 2 seconds.
My people, this is no way to live.
Once you lose trust that the computer is doing what you’re telling it to do, you can’t help but get inside your own head. Every time I saw an unexpected change in the app, I was no longer engaged in a direct, rapid-fire conversation between myself and the computer. Instead, I was also arguing with myself. Maybe my code actually was correct but didn’t work because I loaded the page too fast? Pretty soon, I found myself not only intentionally slowing down out of caution, I was developing nonsensical habits like running every test twice to make sure my changes “stuck”. I began second-guessing myself and making multiple changes before saving and verifying they worked—greatly increasing the scope of rework I had to do when they didn’t.
If you’ve never crossed the chasm from “fast enough” to “not fast enough”—or if you have, but failed to appreciate its implications—this next part might sound silly.
As these extensions started slowing me down, I stopped having fun building this
app. A week prior, I’d been excitedly jumping out of bed to work on it each
morning. I noticed myself taking more breaks. I became less ambitious about how
much I would attempt to finish in time for my deadline. Instead of
spit-polishing features with nice-to-haves after getting them to work, the
instant my tests passed I’d
git push and walk away.
So, naturally, the first thing I did after hitting my deadline was to drop everything and build this Standard Ruby extension. I never want to experience such a slow a feedback loop when writing Ruby again. I hope others will find it helpful, too.
If you read this far just looking for configuration options for the extension, I’m sorry. They’re actually on the README.