#+TITLE: Programming in Ruby #+AUTHOR: Howard X. Abrams #+DATE: 2022-09-01 #+FILETAGS: :emacs: A literate programming file for configuring Emacs to support the Ruby programming language. #+begin_src emacs-lisp :exports none ;;; ha-programming-ruby --- Ruby configuration. -*- lexical-binding: t; -*- ;; ;; © 2022-2023 Howard X. Abrams ;; Licensed under a Creative Commons Attribution 4.0 International License. ;; See http://creativecommons.org/licenses/by/4.0/ ;; ;; Author: Howard X. Abrams ;; Maintainer: Howard X. Abrams ;; Created: September 1, 2022 ;; ;; While obvious, GNU Emacs does not include this file or project. ;; ;; *NB:* Do not edit this file. Instead, edit the original literate file at: ;; /Users/howard.abrams/other/hamacs/ha-programming-ruby.org ;; And tangle the file to recreate this one. ;; ;;; Code: #+end_src * Getting Started Ruby is probably already installed on the system, and if not, we certainly can download it from [[https://www.ruby-lang.org/en/downloads/][ruby-lang.org]], but since I need to juggle different versions for each project, I use [[https://direnv.net/docs/ruby.html][direnv]] and [[https://www.ruby-lang.org/en/documentation/installation/#ruby-install][ruby-install]]: #+begin_src sh brew install ruby-install #+end_src And then install one or more versions: #+begin_src sh ruby-install -U ruby-install ruby 3 #+end_src ** New Project While we /could/ use a large project templating system, I keep it simple. For each project, create the following directory structure: #+begin_example ├── Gemfile ├── Rakefile ├── lib │   └── hello_world.rb └── test └── hello_world_test.rb #+end_example For instance: #+begin_src sh mkdir -p ~/other/ruby-xp # Change me cd ~/other/ruby-xp mkdir -p lib test #+end_src Now, do the following steps. 1. Create a =.envrc= file with the Ruby you want to use: #+begin_src sh use ruby 2.6.10 #+end_src 2. Next, get Bundler: #+begin_src sh gem install bundle #+end_src 3. Create a minimal =Gemfile=: #+begin_src ruby :tangle ~/other/ruby-xp/Gemfile source 'https://rubygems.org' gem 'rake', group: :development gem 'rubocop', group: :development gem 'solargraph', group: :development #+end_src 4. Grab all the dependencies: #+begin_src sh bundle install #+end_src 5. Create a minimal =Rakefile=: #+begin_src ruby :tangle ~/other/ruby-xp/Rakefile task default: %w[test] task :run do ruby 'lib/hello_world.rb' end task :test do ruby 'test/hello_world_test.rb' end #+end_src 6. Create the first program: #+begin_src ruby :tangle ~/other/ruby-xp/lib/hello_world.rb # 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 #+end_src 7. Create the first test: #+begin_src ruby :tangle ~/other/ruby-xp/test/hello_world_test.rb 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 #+end_src Or something like that. ** Existing Projects For projects from work, I have found we need to isolate the Ruby environment. Once we use [[https://github.com/rbenv/rbenv][rbenv]] or [[https://rvm.io/][rvm]], but now, I just use [[https://direnv.net/docs/ruby.html][direnv]]. Approach one is to use a local directory structure. Assuming I have a =use_ruby= function in [[file:~/.config/direnv/direnvrc][~/.config/direnv/direnvrc]]: #+begin_src conf # From Apple: /usr/bin/ruby use ruby 2.6.10 # Could use 3.2.1 from /usr/local/bin/ruby #+end_src A better solution is to create a container to hold the Ruby environment. Begin with a =Dockerfile=: #+begin_src dockerfile :tangle ~/other/ruby-xp/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/* #+end_src Next, create a =.envrc= in the project’s directory: #+begin_src sh :tangle ~/other/ruby-xp/.envrc CONTAINER_NAME=ruby-xp:latest CONTAINER_WRAPPERS=(bash ruby irb gem bundle rake solargraph rubocop) container_layout #+end_src While that approach works /fairly well/ with [[file:ha-programming.org::*direnv][my direnv configuration]], [[file:ha-programming.org::*Flycheck][Flycheck]] 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: #+begin_src emacs-lisp (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)) #+end_src ** Ruby REPL I am not sure I can learn a new language without a REPL connected to my editor, and for Ruby, this is [[https://github.com/nonsequitur/inf-ruby][inf-ruby]]: #+BEGIN_SRC elisp (use-package inf-ruby :config (ha-local-leader 'ruby-mode-map "R" '("REPL" . inf-ruby))) #+END_SRC ** Electric Ruby The [[https://melpa.org/#/ruby-electric][ruby-electric]] project is a minor mode that aims to add the /extra syntax/ when typing Ruby code. #+begin_src emacs-lisp :tangle no (use-package ruby-electric :hook (ruby-mode . ruby-electric-mode)) #+end_src ** Testing The [[https://github.com/r0man/ruby-test-mode][ruby-test-mode]] project aims a running Ruby test from Emacs seemless: #+begin_src emacs-lisp (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))) #+end_src ** Robe The [[https://github.com/dgutov/robe][Robe project]] can be used instead of [[file:ha-programming.org::*Language Server Protocol (LSP) Integration][LSP]]. #+begin_src emacs-lisp (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))) #+end_src Do we want to load Robe /automatically/? #+begin_src emacs-lisp (use-package robe :hook (ruby-mode . robe-mode)) #+end_src ** Bundler The [[https://github.com/endofunky/bundler.el][Bundler project]] integrates [[https://bundler.io/][bundler]] to install a projects Gems. #+begin_src emacs-lisp (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))) #+end_src ** Rubocop? The lint-like style checker of choice for Ruby is [[https://github.com/bbatsov/rubocop][Rubocop]]. The [[https://github.com/bbatsov/rubocop-emacs][rubocop.el]] mode should work with [[https://github.com/flycheck/flycheck][Flycheck]]. First install it with: #+begin_src sh gem install rubocop #+end_src And then we may or may not need to enable the =rubocop-mode=: #+BEGIN_SRC elisp :tangle no (use-package rubocop :hook (ruby-mode . rubocop-mode)) #+END_SRC * Auxiliary Support ** Cucumber Seems that to understand and edit Cucumber /feature/ definitions, you need [[https://github.com/michaelklishin/cucumber.el][cucumber.el]]: #+begin_src emacs-lisp (use-package feature-mode) #+end_src ** RSpec https://github.com/pezra/rspec-mode * LSP Need to install [[https://github.com/castwide/solargraph][Solargraph]] for the LSP server experience: #+begin_src sh gem install solargraph #+end_src Or add it to your =Gemfile=: #+begin_src ruby gem 'solargraph', group: :development #+end_src * Technical Artifacts :noexport: Let's =provide= a name so we can =require= this file: #+begin_src emacs-lisp :exports none (provide 'ha-programming-ruby) ;;; ha-programming-ruby.el ends here #+end_src #+DESCRIPTION: configuring Emacs to support the Ruby programming language. #+PROPERTY: header-args:sh :tangle no #+PROPERTY: header-args:emacs-lisp :tangle yes #+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes #+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil #+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil #+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js