Ruby and Emacs Tip: Advanced Pry Integration
Thiago AraΓΊjo Silva Aug 27 Updated on Aug 28, 2018
Pry is one of those tools that you'll despise until you actually learn what it can do for you. It's a window to the Ruby language, a land where things have extreme late-binding and can change at any time.
With Pry, you can easily spelunk anything: edit, view, and navigate through source code; debug, play lines of code, and run shell commands. Doing the same with "puts debugging" or metaprogramming requires repetitive typing, tedious plumbing, third-party gems (method_source, anyone?) and won't give you syntax highlighting. You can run Ruby REPLs in Emacs (including Pry) with inf-ruby, a package co-authored by the great Yukihiro Matsumoto, our dear Matz. But there's so much more power to unlock!
We will rely on Pry's editor integration, so the good news is that you can apply the same principles to your favorite text editor, as long as it supports a client-server architecture. Neovim is one such example. Emacs, however, has a special trick up its sleeve: it's called elisp.
Goals
This post will guide you through integrating Pry into Emacs and ironing out a few annoyances that might arise during the process. Also, it will give you an idea of how to create Pry commands that reach back into Emacs via elisp. Note that it won't teach you how to setup or use Emacs. Among others you will:
- Run Pry from within Emacs,
- Seamlessly integrate Pry into Emacs,
- Edit and reload files without leaving Pry (nor Emacs!),
- Page through Pry's output,
- View a Ruby file in Emacs through Pry,
- Open a gem in Dired through Pry.
Emacs server
With emacsclient
, you can interact with a running Emacs session from the outside. For example:
$ emacsclient my_file.rb
The above command opens my_file.rb
in your current Emacs session. Note that before attempting to use emacsclient
, you'll need to start a server process with M-x server-start
(press M-x
in Emacs, type server-start
, and press return
).
I want things to be as automated as possible, so I created an elisp function to start the server for me when I open Emacs:
(defun run-server ()
"Runs the Emacs server if it is not running"
(require 'server)
(unless (server-running-p)
(server-start)))
(run-server)
The above code can be saved in your ~/.emacs.d/init.el
config file.
Default editor configuration
The next step is to set emacsclient
as your default editor. Save the following lines in your shell config file (.bash_profile
for bash or .zshenv
for zsh):
export EDITOR=emacsclient
export VISUAL=$EDITOR
If you use a Mac computer and a GUI Emacs, Emacs won't inherit your shell environment variables. To overcome this issue, I recommend installing the exec-path-from-shell
package. Be sure to add MELPA to your package archives:
(setq package-archives
'(("melpa" . "http://melpa.milkbox.net/packages/")
("gnu" . "http://elpa.gnu.org/packages/")))
(package-initialize)
Then run:
-
M-x package-refresh-contents
to refresh the packages, -
M-x package-install
,exec-path-from-shell
, and pressreturn
to install the package.
Finally, save this snippet in your init.el
file:
(defun copy-shell-environment-variables ()
(when (memq window-system '(mac ns))
(exec-path-from-shell-initialize)))
(copy-shell-environment-variables)
But there's a gotcha: I prefer nvim
when I'm working on the terminal (which is rare these days because I run most of my shell commands within Emacs). For this reason, I set both editor variables via elisp and leave my shell variables unchanged:
(setenv "VISUAL" "emacsclient")
(setenv "EDITOR" (getenv "VISUAL"))
Trying it out
Run M-x shell
(Emacs' shell), find an existing file, and run the following command:
$ $EDITOR existing-file
Wow, you are still in Emacs, and the file opened right before your eyes. Meanwhile, the Pry buffer is blocked with the message "Waiting for Emacs...". It's waiting for you to edit the file, save it, and press C-x #
(server-edit
) to terminate the connection, which closes the file and unblocks the shell.
Configuring Pry
You need a few settings to make Pry play nice with Emacs. Save these lines in your ~/.pryrc
file:
if ENV['INSIDE_EMACS']
Pry.config.correct_indent = false
Pry.config.pager = false
end
Pry.config.editor = ENV['VISUAL']
-
Pry.config.correct_indent = false
fixes an annoying issue with the command prompt. -
Pry.config.pager = false
tells Pry not to useless
as a pager. Why? Because Pry runs under comint.el, a library that allows an ordinary Emacs buffer to communicate with an inferior shell process - in our case, a Pry REPL. It doesn't support interactive programs such as less, but on the other hand, the buffer itself is a pager. - Finally, we tell Pry to use whatever is set to the
VISUAL
environment variable as its editor.
Running Pry from within Emacs
Nothing special to do. Just ensure you've installed the inf-ruby
package and that your project is configured to use Pry. inf-ruby
provides many commands to spin up a Ruby REPL, like inf-ruby-console-rails
for example. For Rails projects, you can install the pry-rails
gem and call it a day. The projectile-rails
package, which I thoroughly recommend, has a convenient shortcut for this: C-c p r
(projectile-rails-console
).
Editing and reloading code
This is where things start to get fun. Let's suppose you're not getting an expected return value out of a certain gem. The name of the troublesome method is MyGem.do_thing
, which is called by MyApp.do_thing
. Let's edit MyGem.do_thing
:
[0] pry(main)> edit MyGem.do_thing
Waiting for Emacs...
And insert a debug statement:
class MyGem
def self.do_thing(a, b)
binding.pry
# ...
end
end
Now save the file with C-x C-s
and press C-x #
to terminate the emacsclient
connection. Boom! Pry will load your new changes and you'll be taken back to the Pry prompt! Finally, run MyApp.do_thing
:
[1] pry(main)> MyApp.do_thing(c)
From: /Users/thiago/.asdf/installs/ruby/2.4.2/lib/ruby/gems/2.4.0/gems/my_gem-5.1.3/lib/my_gem.rb @ line 48 MyGem#do_thing:
47: def do_thing(a, b)
=> 48: require 'pry'; binding.pry
Awesome, you've hit the breakpoint, so you might want to inspect the state of your application to figure out what's wrong. Now you can debug the world without leaving Emacs! Pry's edit command is extremely handy because you don't need to know where the gem or the method are stored on disk.
Paging through source code
In Pry, you can read the source code of anything with the show-source
command (or $
). Let's take a look at ActiveRecord::Base#establish_connection
:
[6] pry(main)> show-source ActiveRecord::Base.establish_connection
From: /Users/thiago/.asdf/installs/ruby/2.4.2/lib/ruby/gems/2.4.0/gems/activerecord-5.1.3/lib/active_record/connection_handling.rb @ line 47:
Owner: ActiveRecord::ConnectionHandling
Visibility: public
Number of lines: 13
def establish_connection(config = nil)
raise "Anonymous class is not allowed." unless name
config ||= DEFAULT_ENV.call.to_sym
spec_name = self == Base ? "primary" : name
self.connection_specification_name = spec_name
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations)
spec = resolver.resolve(config).symbolize_keys
spec[:name] = spec_name
connection_handler.establish_connection(spec)
end
This snippet fits into a single screen, but what to do when it doesn't? You can press return
(comint-send-input
) to run show-source
and either:
- Scroll back up with
M-v
until reaching the start of the output, thenC-v
to scroll down, or; - Run
C-c C-r
(comint-show-output
) to make the cursor jump to the start of the output, thenC-v
to scroll down.
The second option is a clear winner. However, I've gotten tired of always running comint-show-output
, so I came up with my own automation. I've created the following function and mapped it to <C-return>
:
;; Save this code in init.el
(defun comint-send-input-stay-on-line ()
(interactive)
(call-interactively 'comint-send-input)
(run-with-timer 0.05
nil
(lambda () (call-interactively 'comint-show-output))))
(define-key comint-mode-map (kbd "<C-return>") 'comint-send-input-stay-on-line)
It runs comint-show-output
right after comint-send-input
. There's an interval of 0.05 seconds between the commands to avoid a race condition, time enough for the output to be available before you can actually return to it.
The cool thing is that this shortcut works with any comint prompt, even M-x shell
. I'm not sure if there's an easier solution to this problem, so if you know of any please leave it up in the comments :)
Viewing a file through Pry
show-source
is cool but it's not enough. Often times I want to open the corresponding Ruby file in another buffer, where I have enh-ruby-mode
and a bunch of other tools at my disposal. The default edit
command is a no-go because it blocks the Pry prompt and waits for an edit to be made. Also, the buffer usually gets closed in the end.
Luckily, we can use edit -n
to bypass the blocking behavior. For example:
[7] pry(main)> edit -n Foo.bar
The above command will open the file for the Foo
module with the cursor pointed at the bar
class method.
Open a gem in Dired through Pry
Opening a gem is something I need to do surprisingly often. Sometimes I want to look into the files or just search within the gem's source code. And I'll be happy if I can avoid a trip to GitHub, which also requires going through the trouble of pointing the gem at the specific version that my app is using. To showcase what a custom command looks like, I also don't want to use the bundle open
command.
Here's a supposed Pry command to open a gem in Dired:
# `ggem` is a mnemonic to "go to gem"
[8] pry(main)> ggem graphql-batch
Note that Pry commands have a special syntax. They are not calls to Ruby methods.
Let's apply divide and conquer to make it work. First, we need a way to open a directory in Emacs. Go to a terminal and type in the following command:
$ emacsclient -e '(dired-jump nil "~")'
Flip back to Emacs, and you will see a Dired window with the cursor positioned at your home directory. Awesome! The -e
flag to emacsclient
allows us to run an arbitrary string of elisp code, so we've run the dired-jump
function and passed our home directory (~
) as the second argument.
Now let's create a Ruby method to wrap this call. Save it in ~/.pryrc
:
# The function accepts an arbitrary path
def emacs_open_in_dired(path)
system %{emacsclient -e '(dired-jump nil "#{path}")'}
end
Next, we need to find the gem's directory. In the above example, the gem is graphql-batch
. Here's a method that will return the directory as a Pathname
object:
def gem_dir(gem_name)
gem_dir = Gem::Specification.find_by_name(gem_name).gem_dir
Pathname(gem_dir)
end
Now that we've gathered all the pieces, let's create the Pry command:
# This is a shorthand to create and add a command at the same time
Pry::Commands.create_command 'ggem' do
description 'Open a gem dir in Emacs dired'
banner <<-'BANNER'
Usage: ggem GEM_NAME
Example: ggem propono
BANNER
def process
gem_name = args.join('')
# Opens the gem's lib folder in Dired
emacs_open_in_dired gem_dir(gem_name) / 'lib'
rescue Gem::MissingSpecError
# do nothing
end
end
Great, the command should now be working! You can use it with any gem, as long as it's declared in your Gemfile!
Conclusion
I hope this post was useful to you. There's so much you can do with these tools that I haven't even scratched the surface! Emacs is a great editor, and the combination of Ruby and elisp gives you almost endless possibilities. With inf-ruby
you can also do cool things such as sending Ruby code from a buffer to the REPL process, so I encourage you to explore it in detail!
If Pry isn't already the heart of your Ruby workflow, I recommend you make the leap. If you work with Ruby, it's a life-changing tool. In the next post, I will show you how to run RSpec tests productively.