A literate programming file for configuring Emacs to support the Ruby programming language.
Ruby is probably already installed on the system, and if not, we certainly can download it from ruby-lang.org, but since I need to juggle different versions for each project, I use direnv and ruby-install:
brew install ruby-installAnd then install one or more versions:
ruby-install -U
ruby-install ruby 3While we could use a large project templating system, I keep it simple. For each project, create the following directory structure:
├── Gemfile
├── Rakefile
├── lib
│ └── hello_world.rb
└── test
└── hello_world_test.rb
For instance:
mkdir -p ~/src/ruby-xp # Change me
cd ~/src/ruby-xp
mkdir -p lib testNow, do the following steps.
- Create a
.envrcfile with the Ruby you want to use:use ruby 2.6.10 - Next, get Bundler:
gem install bundle - Create a minimal
Gemfile:source 'https://rubygems.org' gem 'rake', group: :development gem 'rubocop', group: :development gem 'solargraph', group: :development
- Grab all the dependencies:
bundle install - Create a minimal
Rakefile:task default: %w[test] task :run do ruby 'lib/hello_world.rb' end task :test do ruby 'test/hello_world_test.rb' end
- Create the first program:
# frozen_string_literal: true # The basic greeting class class HelloWorld attr_reader :name def initialize(name = nil) @name = name end def greeting if @name "Hello there, #{@name}" else 'Hello World!' end end end puts HelloWorld.new(ARGV.first).greeting
- Create the first test:
require 'test/unit' require_relative '../lib/hello_world' class TestHelloWorld < Test::Unit::TestCase def test_default assert_equal 'Hello World!', HelloWorld.new.greeting end def test_name assert_equal 'Hello there, Bob', HelloWorld.new('Bob').greeting end end
Or something like that.
For projects from work, I have found we need to isolate the Ruby environment. Once we use rbenv or rvm, but now, I just use direnv. Approach one is to use a local directory structure. Assuming I have a use_ruby function in ~/.config/direnv/direnvrc:
# From Apple: /usr/bin/ruby
use ruby 2.6.10
# Could use 3.2.1 from /opt/homebrew/bin/rubyA better solution is to create a container to hold the Ruby environment. Begin with a Dockerfile:
## -*- dockerfile-image-name: "ruby-xp" -*-
FROM alpine:3.13
ENV NOKOGIRI_USE_SYSTEM_LIBRARIES=1
ADD Gemfile /
RUN apk update \
&& apk add ruby \
ruby-etc \
ruby-bigdecimal \
ruby-io-console \
ruby-irb \
ca-certificates \
libressl \
bash \
&& apk add --virtual .build-dependencies \
build-base \
ruby-dev \
libressl-dev \
&& gem install bundler || apk add ruby-bundler \
&& bundle config build.nokogiri --use-system-libraries \
&& bundle config git.allow_insecure true \
&& gem install json \
&& bundle install \
&& gem cleanup \
&& apk del .build-dependencies \
&& rm -rf /usr/lib/ruby/gems/*/cache/* \
/var/cache/apk/* \
/tmp/* \
/var/tmp/*Next, create a .envrc in the project’s directory:
CONTAINER_NAME=ruby-xp:latest
CONTAINER_WRAPPERS=(bash ruby irb gem bundle rake solargraph rubocop)
container_layoutWhile that approach works fairly well with my direnv configuration, Flycheck seems to want the checkers to be installed globally.
While Emacs supplies a Ruby editing environment, we’ll still use use-package to grab the latest:
(use-package ruby-mode
:mode (rx "." (optional "e") "rb" eos)
:mode (rx "Rakefile" eos)
:mode (rx "Gemfile" eos)
:mode (rx "Berksfile" eos)
:mode (rx "Vagrantfile" eos)
:interpreter "ruby"
:init
(setq ruby-indent-level 2
ruby-indent-tabs-mode nil)
:hook (ruby-mode . superword-mode))I am not sure I can learn a new language without a REPL connected to my editor, and for Ruby, this is inf-ruby:
(use-package inf-ruby
:config
(ha-local-leader 'ruby-mode-map
"R" '("REPL" . inf-ruby)))The ruby-electric project is a minor mode that aims to add the extra syntax when typing Ruby code.
(use-package ruby-electric
:hook (ruby-mode . ruby-electric-mode))The ruby-test-mode project aims a running Ruby test from Emacs seemless:
(use-package ruby-test-mode
:hook (ruby-mode . ruby-test-mode)
:config
(ha-local-leader 'ruby-mode-map
"t" '(:ignore t :which-key "test")
"t t" '("test one" . ruby-test-run-at-point)
"t g" '("toggle code/test" . ruby-test-toggle-implementation-and-specification)
"t A" '("test all" . ruby-test-run)
"t a" '("retest" . ruby-test-rerun)))The Bundler project integrates bundler to install a projects Gems.
(use-package bundler
:config
(ha-local-leader 'ruby-mode-map
"g" '(:ignore t :which-key "bundler")
"g o" '("open" . bundle-open)
"g g" '("console" . bundle-console)
"g c" '("check" . bundle-check)
"g i" '("install" . bundle-install)
"g u" '("update" . bundle-update)))The lint-like style checker of choice for Ruby is Rubocop. The rubocop.el mode should work with Flycheck. First install it with:
gem install rubocopAnd then we may or may not need to enable the rubocop-mode:
(use-package rubocop
:hook (ruby-mode . rubocop-mode))Seems that to understand and edit Cucumber feature definitions, you need cucumber.el:
(use-package feature-mode)https://github.com/pezra/rspec-mode
The built-in Eglot package natively supports Solargraph via eglot-server-programs (no extra package needed):
(use-package eglot
:straight nil
:after ruby-mode
:defer t
:hook (ruby-mode . eglot-ensure))Keybindings for the eglot-based LSP sub-menu, wired into the ruby-mode local leader:
(use-package eglot
:straight nil
:after ruby-mode
:config
(ha-local-leader 'ruby-mode-map
"w" '(:ignore t :which-key "eglot")
"wD" '("disconnect" . eglot-shutdown)
"wR" '("reconnect" . eglot-reconnect)
"w?" '("describe" . eglot-show-workspace-configuration)
"wa" '("code actions" . eglot-code-actions)
"wo" '("organize imports" . eglot-code-action-organize-imports)
"wr" '("rename" . eglot-rename)
"w=" '("format buffer" . eglot-format-buffer)))Need to install Solargraph for the LSP server experience:
gem install solargraphOr add it to your Gemfile:
gem 'solargraph', group: :developmentOnce installed, eglot finds and talks to solargraph automatically; no further Emacs configuration is needed.
Let’s provide a name so we can require this file: