Programming in Ruby
Table of Contents
A literate programming file for configuring Emacs to support the Ruby programming language.
Getting Started
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
New Project
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.
Existing Projects
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 , seems to want the checkers to be installed globally.
Configuration
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))
Ruby REPL
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)))
Electric 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))
Testing
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)))
Robe
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))
Bundler
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)))
Rubocop?
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))
Auxiliary Support
Cucumber
Seems that to understand and edit Cucumber feature definitions, you need cucumber.el:
(use-package feature-mode)
LSP
Need to install Solargraph for the LSP server experience:
gem install solargraph
Or add it to your Gemfile
:
gem 'solargraph', group: :development
XRef Interface with GNU Global
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