ccb49d5a58
Haven't really needed this, as I have a function to load the org file as Emacs Lisp code directly, but this can be a little helpful for debugging.
325 lines
11 KiB
Org Mode
325 lines
11 KiB
Org Mode
#+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 <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:
|
||
;; /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 ".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 ~/.emacs.d/elisp/ha-programming-ruby.el
|
||
#+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
|