2023-12-03 18:57:36 +00:00
#+title : Programming in Ruby
#+author : Howard X. Abrams
#+date : 2022-09-01
#+tags : emacs ruby programming
2022-09-02 23:14:10 +00:00
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; -* -
;;
2023-02-23 17:35:36 +00:00
;; © 2022-2023 Howard X. Abrams
2022-09-02 23:14:10 +00:00
;; Licensed under a Creative Commons Attribution 4.0 International License.
;; See http://creativecommons.org/licenses/by/4.0/
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams >
;; 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:
2024-10-19 20:34:01 +00:00
;; ~/src/hamacs/ha-programming-ruby.org
2022-09-02 23:14:10 +00:00
;; 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
2023-04-01 23:29:40 +00:00
** New Project
2022-09-02 23:14:10 +00:00
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
2024-10-19 20:34:01 +00:00
mkdir -p ~/src/ruby-xp # Change me
cd ~/src/ruby-xp
2023-04-01 23:29:40 +00:00
mkdir -p lib test
2022-09-02 23:14:10 +00:00
#+end_src
Now, do the following steps.
1. Create a =.envrc= file with the Ruby you want to use:
2023-04-01 23:29:40 +00:00
#+begin_src sh
use ruby 2.6.10
2022-09-02 23:14:10 +00:00
#+end_src
2. Next, get Bundler:
#+begin_src sh
2023-04-01 23:29:40 +00:00
gem install bundle
2022-09-02 23:14:10 +00:00
#+end_src
3. Create a minimal =Gemfile= :
2024-10-19 20:34:01 +00:00
#+begin_src ruby :tangle ~/src/ruby-xp/Gemfile
2022-09-02 23:14:10 +00:00
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= :
2024-10-19 20:34:01 +00:00
#+begin_src ruby :tangle ~/src/ruby-xp/Rakefile
2022-09-02 23:14:10 +00:00
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:
2024-10-19 20:34:01 +00:00
#+begin_src ruby :tangle ~/src/ruby-xp/lib/hello_world.rb
2023-04-01 23:29:40 +00:00
# frozen_string_literal: true
# The basic greeting class
2022-09-02 23:14:10 +00:00
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
2023-04-01 23:29:40 +00:00
puts HelloWorld.new(ARGV.first).greeting
2022-09-02 23:14:10 +00:00
#+end_src
7. Create the first test:
2024-10-19 20:34:01 +00:00
#+begin_src ruby :tangle ~/src/ruby-xp/test/hello_world_test.rb
2022-09-02 23:14:10 +00:00
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.
2023-04-01 23:29:40 +00:00
** 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
2023-11-06 17:36:06 +00:00
# Could use 3.2.1 from /opt/homebrew/bin/ruby
2023-04-01 23:29:40 +00:00
#+end_src
A better solution is to create a container to hold the Ruby environment. Begin with a =Dockerfile= :
2024-10-19 20:34:01 +00:00
#+begin_src dockerfile :tangle ~/src/ruby-xp/Dockerfile
2023-04-01 23:29:40 +00:00
## -*- 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:
2024-10-19 20:34:01 +00:00
#+begin_src sh :tangle ~/src/ruby-xp/ .envrc
2023-04-01 23:29:40 +00:00
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.
2022-09-02 23:14:10 +00:00
* 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
2023-09-08 17:58:17 +00:00
:mode (rx "." (optional "e") "rb" eos)
2022-09-02 23:14:10 +00:00
: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
2023-05-01 18:49:33 +00:00
(ha-local-leader 'ruby-mode-map
2022-09-02 23:14:10 +00:00
"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.
2023-04-01 23:29:40 +00:00
#+begin_src emacs-lisp :tangle no
2022-09-02 23:14:10 +00:00
(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
2023-05-01 18:49:33 +00:00
(ha-local-leader 'ruby-mode-map
2022-09-02 23:14:10 +00:00
"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
2023-04-01 23:29:40 +00:00
** 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
2023-05-01 18:49:33 +00:00
(ha-local-leader 'ruby-mode-map
2023-04-01 23:29:40 +00:00
"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
2023-05-01 18:49:33 +00:00
(use-package bundler
:config
(ha-local-leader 'ruby-mode-map
2023-04-01 23:29:40 +00:00
"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
2022-09-02 23:14:10 +00:00
** 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
2023-02-22 01:26:25 +00:00
* 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
2023-04-01 23:29:40 +00:00
** RSpec
https://github.com/pezra/rspec-mode
2022-09-02 23:14:10 +00:00
* 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
2024-07-26 04:20:48 +00:00
* XRef Interface with GNU Global
The [[http://www.gnu.org/software/global/ ][GNU Global ]] has the ability to generate a tags file for large, multi-project Ruby code bases.
First, issue these two:
#+begin_src sh :dir ~/work/gourmet
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
#+end_src
And now we need the GNU Global for Emacs, we are using the most up-to-date version of [[https://github.com/leoliu/ggtags ][ggtags ]].
#+begin_src emacs-lisp
(use-package ggtags
:hook ((ruby-mode . #'ggtags-mode)))
#+end_src
2022-09-02 23:14:10 +00:00
2024-07-26 04:20:48 +00:00
Careful observers will note that
2022-09-02 23:14:10 +00:00
* 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
2024-03-07 04:02:25 +00:00
#+description : configuring Emacs to support the Ruby programming language.
2022-09-02 23:14:10 +00:00
2024-03-07 04:02:25 +00:00
#+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
2022-09-02 23:14:10 +00:00
2024-03-07 04:02:25 +00:00
#+options : num:nil toc:t todo:nil tasks:nil tags:nil date:nil
#+options : skip:nil author:nil email:nil creator:nil timestamp:nil
#+infojs_opt : view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js