a3e92b4f7f
The Tree Sitter just isn't changing anything for me, so I'm removing this code for the time being. I'll reinvestigate later.
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 "." (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
|