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-install
And then install one or more versions:
ruby-install -U
ruby-install ruby 3
While 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 test
Now, do the following steps.
- Create a
.envrc
file 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/ruby
A 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_layout
While 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 Robe project can be used instead of LSP.
(use-package robe
:config
(ha-local-leader 'ruby-mode-map
"w" '(:ignore t :which-key "robe")
"ws" '("start" . robe-start))
;; The following leader-like keys, are only available when I have
;; started LSP, and is an alternate to Command-m:
:general
(:states 'normal :keymaps 'robe-mode-map
", w r" '("restart" . lsp-reconnect)
", w b" '("events" . lsp-events-buffer)
", w e" '("errors" . lsp-stderr-buffer)
", w q" '("quit" . lsp-shutdown)
", w l" '("load file" . ruby-load-file)
", l r" '("rename" . lsp-rename)
", l f" '("format" . lsp-format)
", l a" '("actions" . lsp-code-actions)
", l i" '("imports" . lsp-code-action-organize-imports)
", l d" '("doc" . lsp-lookup-documentation)))
Do we want to load Robe automatically?
(use-package robe :hook (ruby-mode . robe-mode))
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 rubocop
And 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
Need to install Solargraph for the LSP server experience:
gem install solargraph
Or add it to your Gemfile
:
gem 'solargraph', group: :development
The GNU Global has the ability to generate a tags file for large, multi-project Ruby code bases.
First, issue these two:
find . -name .git | while read DOTGIT
do
REPO=$(dirname $DOTGIT)
(cd $REPO && git pull origin master)
done
find . -name "*.rb" > gtags.files
gtags --gtagslabel=new-ctags --file gtags.files
And now we need the GNU Global for Emacs, we are using the most up-to-date version of ggtags.
(use-package ggtags
:hook ((ruby-mode . #'ggtags-mode)))
Careful observers will note that
Let’s provide
a name so we can require
this file: