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 ("diagnostics", 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.

Then a couple months ago, Will Leinweber swooped in and implemented a proof-of-concept language server for Standard, and it changed everything.

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.

What the Standard Ruby VS Code extension does

Go install the Standard Ruby extension from the Visual Studio Marketplace, and you’ll see all it does:

  • Tells VS Code which standardrb executable 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 .standard.yml config file

Best of all: it’s fast! Distressingly fast. “How did it even run already?” fast.

Why you don’t want a slow formatter

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:

  1. Save a file to fix a bug
  2. Command-tab to my browser
  3. Load the page
  4. See that bug is still there
  5. Command-tab to my editor
  6. Try a different solution and save the file again
  7. Command-tab to my browser
  8. Reload the page
  9. See my fix from Step 1 represented on the page
  10. 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
  11. Come to grips with the fact that Steps 5-12 were a total waste of time
  12. 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.

Configuring the extension

If you read this far just looking for configuration options for the extension, I’m sorry. They’re actually on the README.

Justin Searls

Person An icon of a human figure Status
Double Agent
Hash An icon of a hash sign Code Name
Agent 002
Location An icon of a map marker Location
Orlando, FL