hamacs/ha-programming-ruby.org
Howard Abrams 6d92980311 Migration from ~/other to ~/src
Why was it any other way?
2024-10-19 13:34:01 -07:00

347 lines
11 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#+title: Programming in Ruby
#+author: Howard X. Abrams
#+date: 2022-09-01
#+tags: emacs ruby programming
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:
;; ~/src/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 ~/src/ruby-xp # Change me
cd ~/src/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 ~/src/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 ~/src/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 ~/src/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 ~/src/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 /opt/homebrew/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 ~/src/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 projects directory:
#+begin_src sh :tangle ~/src/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, well 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
* 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
Careful observers will note that
* 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: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