DEV Community

Thiago Araújo Silva
Thiago Araújo Silva

Posted on • Updated on

Ruby and Emacs Tip: Advanced Pry Integration

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 Yukihiro Matsumoto, Ruby's BDFL. 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: 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Then run:

  • M-x package-refresh-contents to refresh the packages,
  • M-x package-install, exec-path-from-shell, and press return 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)
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

Trying it out

Run M-x shell (Emacs' shell), find an existing file, and run the following command:

$ $EDITOR existing-file
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode
  • Pry.config.correct_indent = false fixes an annoying issue with the command prompt.
  • Pry.config.pager = false tells Pry not to use less 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...
Enter fullscreen mode Exit fullscreen mode

And insert a debug statement:

class MyGem
  def self.do_thing(a, b)
    binding.pry
    # ...
  end
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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, then C-v to scroll down, or;
  • Run C-c C-r (comint-show-output) to make the cursor jump to the start of the output, then C-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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 "~")'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)