Full Code of zenspider/enhanced-ruby-mode for AI

master 1a3a93a6ba51 cached
17 files
135.0 KB
39.8k tokens
98 symbols
1 requests
Download .txt
Repository: zenspider/enhanced-ruby-mode
Branch: master
Commit: 1a3a93a6ba51
Files: 17
Total size: 135.0 KB

Directory structure:
gitextract_zuffihn_/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── COPYING
├── README.rdoc
├── Rakefile
├── debugging.md
├── enh-ruby-mode.el
├── ruby/
│   ├── erm.rb
│   └── erm_buffer.rb
├── test/
│   ├── enh-ruby-mode-test.el
│   ├── helper.el
│   ├── helper.rb
│   ├── markup.rb
│   └── test_erm_buffer.rb
└── tools/
    ├── debug.rb
    ├── lexer.rb
    └── markup.rb

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/test.yml
================================================
name: test

on:
  push:
    branches:
      - master
      - test
  pull_request:
    branches:
      - master
      - test

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        emacs_version:
          - 28.2
          - 29.1

    steps:
      - name: Set up Emacs
        uses: purcell/setup-emacs@master
        with:
          version: ${{matrix.emacs_version}}

      - name: Check out project
        uses: actions/checkout@v3

      - name: Version Info
        run: |
          ruby -v
          gem -v

      - name: Test
        run: rake test:all


================================================
FILE: .gitignore
================================================
.bzr
.bzrignore
*~
nokoload.gemspec
pkg
tmp
*.elc

================================================
FILE: COPYING
================================================
Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
You can redistribute it and/or modify it under either the terms of the GPL
version 2 (see the file GPL), or the conditions below:

  1. You may make and give away verbatim copies of the source form of the
     software without restriction, provided that you duplicate all of the
     original copyright notices and associated disclaimers.

  2. You may modify your copy of the software in any way, provided that
     you do at least ONE of the following:

       a) place your modifications in the Public Domain or otherwise
          make them Freely Available, such as by posting said
	  modifications to Usenet or an equivalent medium, or by allowing
	  the author to include your modifications in the software.

       b) use the modified software only within your corporation or
          organization.

       c) give non-standard binaries non-standard names, with
          instructions on where to get the original software distribution.

       d) make other distribution arrangements with the author.

  3. You may distribute the software in object code or binary form,
     provided that you do at least ONE of the following:

       a) distribute the binaries and library files of the software,
	  together with instructions (in the manual page or equivalent)
	  on where to get the original distribution.

       b) accompany the distribution with the machine-readable source of
	  the software.

       c) give non-standard binaries non-standard names, with
          instructions on where to get the original software distribution.

       d) make other distribution arrangements with the author.

  4. You may modify and include the part of the software into any other
     software (possibly commercial).  But some files in the distribution
     are not written by the author, so that they are not under these terms.

     For the list of those files and their copying conditions, see the
     file LEGAL.

  5. The scripts and library files supplied as input to or produced as 
     output from the software do not automatically fall under the
     copyright of the software, but belong to whomever generated them, 
     and may be sold commercially, and may be aggregated with this
     software.

  6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
     IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     PURPOSE.


================================================
FILE: README.rdoc
================================================
= Enhanced Ruby Mode

* Git: http://github.com/zenspider/Enhanced-Ruby-Mode
* Author: Geoff Jacobsen / forked by Ryan Davis
* Copyright: 2010 - 2012
* License: RUBY License

== Description

Enhanced Ruby Mode replaces the emacs ruby mode that comes with ruby.

It uses the Ripper class found in ruby 1.9.2 (and later) to parse and indent the source code.  As a consequence only ruby 1.9.2 (or later) syntax is parsed correctly.

Syntax checking is also performed.

== TODO

* Optimisation; currently parses and fontifies whole buffer for most modifications - it still appears to run fast enough on large files.

* Suggestions?


== Synopsis

* Enhanced Ruby Mode is installable via el-get and Melpa, where its package name is +enh-ruby-mode+.

* For manual installation, add the following file to your init file.

   (add-to-list 'load-path "(path-to)/Enhanced-Ruby-Mode") ; must be added after any path containing old ruby-mode
   (autoload 'enh-ruby-mode "enh-ruby-mode" "Major mode for ruby files" t)
   (add-to-list 'auto-mode-alist '("\\.rb\\'" . enh-ruby-mode))
   (add-to-list 'interpreter-mode-alist '("ruby" . enh-ruby-mode))

   ;; optional

   (setq enh-ruby-program "(path-to-ruby1.9)/bin/ruby") ; so that still works if ruby points to ruby1.8

* Enhanced Ruby Mode defines its own specific faces with the hook <tt>erm-define-faces</tt>. If your theme is already defining those faces, to not overwrite them, just remove the hook with:

   (remove-hook 'enh-ruby-mode-hook 'erm-define-faces)

== Existing ruby-mode hooks

You may have existing lines in your emacs config that add minor modes based on ruby mode, like this:
    (add-hook 'ruby-mode-hook 'robe-mode)
    (add-hook 'ruby-mode-hook 'yard-mode)

For these to work with enh-ruby-mode, you need to add hooks to the enh-ruby-mode minor mode:
    (add-hook 'enh-ruby-mode-hook 'robe-mode)
    (add-hook 'enh-ruby-mode-hook 'yard-mode)

== Load enh-ruby-mode for Ruby files

To use enh-ruby-mode for <tt>.rb</tt> add the following to your init file:
    (add-to-list 'auto-mode-alist '("\\.rb\\'" . enh-ruby-mode))

To use enh-ruby-mode for all common Ruby files and the following to your init file:
    (add-to-list 'auto-mode-alist
                 '("\\(?:\\.rb\\|ru\\|rake\\|thor\\|jbuilder\\|gemspec\\|podspec\\|/\\(?:Gem\\|Rake\\|Cap\\|Thor\\|Vagrant\\|Guard\\|Pod\\)file\\)\\'" . enh-ruby-mode))

== Requirements

* ruby 1.9.2 (or later)

== Install

* git clone git@github.com:zenspider/Enhanced-Ruby-Mode.git

== Development

Developing requires minitest 5.x gem.

Testing parser:

    rake test:ruby [N=name or /pattern/]

    rake test:elisp [N=pattern]

    rake test:all

    rake                            # same as test:all

Tests for Emacs Lisp require ERT. It is built-in since Emacs 24.1.

== Credits

Jell (Jean-Louis Giordano) https://github.com/Jell
  Improved UTF-8 support


================================================
FILE: Rakefile
================================================
task :default => %w[clean compile test:all]

el_files = Rake::FileList['**/enh-ruby-mode*.el']

def run cmd
  sh cmd do |good|
    # block prevents ruby backtrace on failure
    exit 1 unless good
  end
end

def emacs args
  emacs_cmd = Dir[
    "/usr/local/bin/emacs",
    "/{My,}Applications/Emacs.app/Contents/MacOS/Emacs" # homebrew
  ].first || "emacs" # trust the path

  run %Q[#{emacs_cmd} -Q -L . #{args}]
end

def emacs_test args
  emacs "-l enh-ruby-mode-test.el #{args}"
end

desc "byte compile the project. Helps drive out warnings, but also faster."
task compile: el_files.ext('.elc')

rule '.elc' => '.el' do |t|
  emacs "--batch -f batch-byte-compile #{t.source}"
end

desc "Clean the project"
task :clean do
  rm_f Dir["**/*~", "**/*.elc"]
end

task :test => %w[ test:ruby test:elisp ]

namespace :test do
  desc "Run tests for Ruby"
  task :ruby do
    n = ENV["N"]

    if n then
      run %Q[ruby -wI. test/test_erm_buffer.rb -n #{n.dump}]
    else
      run %Q[ruby -wI. test/test_erm_buffer.rb]
    end
  end

  desc "Run tests for Emacs Lisp"
  task :elisp do
    n=ENV["N"]

    Dir.chdir "test" do
      if n then
        emacs_test "--batch -eval '(ert-run-tests-batch-and-exit #{n.dump})'"
      else
        emacs_test "--batch -f ert-run-tests-batch-and-exit"
      end
    end
  end

  desc "Run tests for Emacs Lisp interactively"
  task :elispi do
    Dir.chdir "test" do
      emacs_test %q[-eval "(ert-run-tests-interactively 't)"]
    end
  end

  desc "Run test:ruby and test:elisp"
  task :all => [:ruby, :elisp]
end

def docker cmd
  sh %(docker run -v $PWD:/erm --rm -i -t -w /erm/test zenspider/emacs-ruby #{cmd})
end

desc "test in a docker container"
task :docker do
  docker "rake test:all"
end

desc "interactive test in a docker container"
task :dockeri do
  docker "rake test:elispi"
end

desc "run a shell in a docker container"
task :sh do
  docker "/bin/sh"
end

desc "debug a file (F=path)"
task :debug do
  f = ENV["F"]
  system "ruby tools/debug.rb #{f}"
  puts
  system "ruby tools/lexer.rb #{f}"
  puts
  system "ruby tools/markup.rb #{f}"
end


================================================
FILE: debugging.md
================================================
# How to Debug Problems in ERM

These are notes to myself because I don't work on this project much.

## 0. Run `rake docker` or `rake dockeri` to run tests in isolation.

Make sure that everything else is currently good before you go
debugging new issues.

```
rm *.elc; cmacs -Q -l loader.el  wtf.rb
```

## 1. First, get a reproduction in a file named bug###.rb.

This helps you track back to a github issue (create one if necessary).

## 2. Reduce the reproduction to the bare minimum.

Usually, there's little need for "real" code and the submission
contains a lot of sub-expressions that can be removed.

```ruby
renewed_clients = @renewed_clients_ransack
  .result
    .order('due_at desc')
    .page(params[:renewed_clients_page])
```

vs:

```ruby
@b
  .c
    .d
```

There's no need for arguments or the extra calls. Even the assignment
can be removed.

Know what the expected result should be. In the case of the above, the
indentation is off and should be:

```ruby
@b
  .c
  .d
```

## 3. Run `rake debug F=bug###.rb` to get relevant output.

This outputs what it sees, not what it thinks it should be. For the
above, the output looks like (with notes inline):

### 3.1. tools/debug.rb:

```ruby
[:ivar, "@b", 2]
[:sp, "\n", 1]
[:sp, "  ", 2]
[:rem, ".", 1]
[:ident, "c", 1]
[:sp, "\n", 1]
[:sp, "    ", 4]
[:indent, :c, -4]
[:rem, ".", 1]
[:ident, "d", 1]
((15 1 16 c 9)(0 3 16)(3 1 3))
```

This is a raw printing of the tokens as they are lexed in triplets of
token type, token value, and length. It is followed with the data that
actually goes back from the ruby process to emacs. This is what is
used to highlight and/or indent.

TODO: I don't know how to read that sexp yet. But I want to document
all of this output first to see if it knocks something loose.

### 3.2. tools/lexer.rb:

This tool is helpful because it only uses Ripper and knows nothing
about ERM.

This is the raw output from Ripper.lex and is basically the events
that will be triggered in ERM:

```ruby
[[[1, 0], :on_ivar, "@b", EXPR_END],
 [[1, 2], :on_ignored_nl, "\n", EXPR_END],
 [[2, 0], :on_sp, "  ", EXPR_END],
 [[2, 2], :on_period, ".", EXPR_DOT],
 [[2, 3], :on_ident, "c", EXPR_ARG],
 [[2, 4], :on_ignored_nl, "\n", EXPR_ARG],
 [[3, 0], :on_sp, "    ", EXPR_ARG],
 [[3, 4], :on_period, ".", EXPR_DOT],
 [[3, 5], :on_ident, "d", EXPR_ARG],
 [[3, 6], :on_nl, "\n", EXPR_BEG]]
```

This is the raw output from Ripper.sexp_raw:

```ruby
[:program,
 [:stmts_add,
  [:stmts_new],
  [:call,
   [:call, [:var_ref, [:@ivar, "@b", [1, 0]]], :".", [:@ident, "c", [2, 3]]],
   :".",
   [:@ident, "d", [3, 5]]]]]
```

This is the raw output from Ripper.sexp and is basically the same
thing but cleaned up / combined a bit:

```ruby
[:program,
 [[:call,
   [:call, [:var_ref, [:@ivar, "@b", [1, 0]]], :".", [:@ident, "c", [2, 3]]],
   :".",
   [:@ident, "d", [3, 5]]]]]
```

### 3.3. tools/markup.rb

```
((15 1 16 c 9)(0 3 16)(3 1 3))
---
«3»@b«0»
  .c
«@c»    .d
```

The sexp, roughly described is:

```ruby
((code.size, 1, code.size+1, indent_stack.join) result.join)
```

TODO: I'm not sure how to read result yet.

## 4. See if you can find the closest passing version of the repro

In this example, if the receiver is not an ivar, it works fine:

```ruby
b
  .c
  .d
```

## 5. Get both passing and repro into tests

This allows you some freedom. At this point, everything is isolated
and reproducible. You should see that one passes and the other one fails.

```lisp
(ert-deftest enh-ruby-indent-leading-dots-ident ()
  (with-temp-enh-rb-string
   "b\n.c\n.d\n"

   (indent-region (point-min) (point-max))
   (buffer-should-equal "b\n  .c\n  .d\n")))

(ert-deftest enh-ruby-indent-leading-dots-ivar ()
  (with-temp-enh-rb-string
   "@b\n.c\n.d\n"

   (indent-region (point-min) (point-max))
   (buffer-should-equal "@b\n  .c\n  .d\n")))
```

## 6. Try to ferret out the difference.

I was able to do that with:

```
% ruby tools/debug.rb --trace bug128_1.rb | nopwd > 1
% ruby tools/debug.rb --trace bug128_2.rb | nopwd > 2
```

and then using `ediff` to look at the differences in execution paths.

In particular, I could see that a major difference down the line
depended on whether `@ident` was true:

```diff
 #0:./ruby/erm_buffer.rb:19:ErmBuffer::Adder:-:         case sym
-#0:./ruby/erm_buffer.rb:23:ErmBuffer::Adder:-:           @ident = false
+#0:./ruby/erm_buffer.rb:21:ErmBuffer::Adder:-:           @ident = true
 #0:./ruby/erm_buffer.rb:27:ErmBuffer::Adder:-:       @first_token = ft
```

followed by:

```diff
 #0:./ruby/erm_buffer.rb:487:ErmBuffer::Parser:-:       if @ident
+#0:./ruby/erm_buffer.rb:488:ErmBuffer::Parser:-:         line_so_far_str = @line_so_far.map {|a| a[1] }.join
+#0:./ruby/erm_buffer.rb:489:ErmBuffer::Parser:-:         if line_so_far_str.strip == ""
+#0:./ruby/erm_buffer.rb:490:ErmBuffer::Parser:-:           indent :c, (line_so_far_str.length * -1)
+#0:./ruby/erm_buffer.rb:99:ErmBuffer::Parser:>:     def indent type, c = 0
...
```

This was the major difference between the two and made it easy to
reason about.

## 7. Make the test pass

Changing from:

```ruby
  when :ident, :const then
```

to

```ruby
  when :ident, :const, :ivar, :gvar, :cvar then
```

(with 2 extra tests) made the tests pass and things seem happier.

# Profiling

misc dump for now:

https://github.com/zenspider/enhanced-ruby-mode/issues/171

```elisp
(with-current-buffer "big_file.rb"
  (profiler-start 'cpu)
  (--dotimes 10 (self-insert-command 1 ?s))
  (profiler-report)
  (profiler-stop))
```

https://github.com/zenspider/enhanced-ruby-mode/issues/146

profiling electric-indent-mode vs not:

```elisp
(with-current-buffer "ruby25_parser.rb"
  (goto-char (point-max))
  (electric-indent-mode (if electric-indent-mode -1 1))

  (profiler-start 'cpu)
  (--dotimes 10 (call-interactively 'newline))
  (profiler-report)
  (profiler-stop))
```

versus electric-indent-mode under text-mode:

```elisp
(with-current-buffer "ruby25_parser.rb"
  (goto-char (point-max))
  (text-mode)
  (setq start (float-time))
  (electric-indent-mode (if electric-indent-mode -1 1))
  (enh-ruby-mode)
  (erm-wait-for-parse)
  (message "%f" (- (float-time) start)))
```

for testing N large operations across a file/buffer
```elisp
(progn
  (profiler-start 'cpu)
  (--dotimes 20
    (message "attempt %d" it)
    (let ((buf (find-file "lib/ruby27_parser.rb")))
      (with-current-buffer buf
        (enh-ruby-mode)
        ;; (erm-wait-for-parse)

        (goto-char (point-max))

        (--dotimes 10 (call-interactively 'newline))

        (erm-wait-for-parse)

        (set-buffer-modified-p nil)
        (kill-buffer buf))))
  (profiler-report)
  (profiler-stop))
```

for profiling N operations on an open buffer and reverting any changes made:
```elisp
(with-current-buffer "ruby25_parser.rb"
  (goto-char (point-max))

  ;; (electric-indent-mode (if electric-indent-mode -1 1))
  (electric-indent-mode -1)
  ;; (electric-indent-mode 1)

  ;; (erm-wait-for-parse)
  ;; (message "starting")
  (profiler-start 'cpu)

  (--dotimes 10 (call-interactively 'newline))
  ;; (erm-wait-for-parse)

  (profiler-report)
  (profiler-stop)

  (with-current-buffer "ruby25_parser.rb"
    (erm-wait-for-parse)
    (revert-buffer nil t))
  )
```


================================================
FILE: enh-ruby-mode.el
================================================
;;; enh-ruby-mode.el --- Major mode for editing Ruby files  -*- lexical-binding: t; -*-

;; Copyright (C) 2012-2022+ -- Ryan Davis
;; Copyright (C) 2010-2012 Geoff Jacobsen

;; Author: Geoff Jacobsen
;; Maintainer: Ryan Davis
;; URL: https://github.com/zenspider/Enhanced-Ruby-Mode
;; Created: Sep 18 2010
;; Keywords: languages, elisp, ruby
;; Package-Requires: ((emacs "25.1"))
;; Version: 1.2.0

;; This file is not part of GNU Emacs.

;; This file is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 2 of the License, or
;; (at your option) any later version.

;; It is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
;; License for more details.

;; You should have received a copy of the GNU General Public License
;; along with it.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This is a fork of https://github.com/jacott/Enhanced-Ruby-Mode
;; to provide further enhancements and bug fixes.
;;
;; It has been renamed to enh-ruby-mode.el to avoid name conflicts
;; with ruby-mode that ships with Emacs. All symbols that started with
;; 'ruby now start with 'enh-ruby. This also makes it possible to
;; switch back and forth for testing purposes.

;; Provides fontification, indentation, syntax checking, and navigation for Ruby code.
;;
;; If you're installing manually, you should add this to your .emacs
;; file after putting it on your load path:
;;
;;    (add-to-list 'load-path "(path-to)/Enhanced-Ruby-Mode") ; must be added after any path containing old ruby-mode
;;    (setq enh-ruby-program "(path-to-ruby)/bin/ruby") ; so that still works if ruby points to ruby1.8
;;

(require 'cl-lib)                       ; for cdddr, caddr
(require 'files)                        ; for mode-require-final-newline
(require 'paren)                        ; show-paren-data-function
(require 'seq)                          ; seq-remove, seq-difference
(require 'subr-x)                       ; string-trim-right
(require 'cus-edit)                     ; custom-variable-state
(require 'find-func)                    ; find-library-name

;;; Properties & Other Bullshit Codes:

;; 'l - [, (, {, %w/%i open  or | goalpost open
;; 'r - ], ), }, %w/%i close or | goalpost close
;; 'b - begin, def, case, if
;; 'd - do, {, embexpr (interpolation) start
;; 'e - end, embexpr (interpolation) end, close block }
;; 's - statement start on BACKDENT_KW else/when/rescue etc
;; 'c - continue - period followed by return (or other way around?)

;; pc
;; bc
;; nbc
;; npc

;;; Variables:

(defcustom enh-ruby-add-encoding-comment-on-save nil
  "Adds ruby magic encoding comment on save when non-nil."
  :type 'boolean
  :safe #'booleanp
  :group 'enh-ruby)

(defcustom enh-ruby-bounce-deep-indent nil
  "Bounce between normal indentation and deep indentation when non-nil."
  :type 'boolean
  :safe #'booleanp
  :group 'enh-ruby)

(defcustom enh-ruby-check-syntax 'errors-and-warnings
  "Highlight syntax errors and warnings."
  :type '(radio (const :tag "None" nil)
                (const :tag "Errors" errors)
                (const :tag "Errors and warnings" errors-and-warnings))
  :safe #'enh-symbol-or-null-p
  :group 'enh-ruby)

(defcustom enh-ruby-comment-column 32
  "*Indentation column of comments."
  :type 'integer
  :safe #'integerp
  :group 'enh-ruby)

(defcustom enh-ruby-deep-indent-construct t
  "*Deep indent constructs such as if, def, class and module when non-nil."
  :type 'boolean
  :safe #'booleanp
  :group 'enh-ruby)

(defcustom enh-ruby-deep-indent-paren t
  "*Deep indent lists in parenthesis when non-nil."
  ;; FIX: this applies to square brackets as well
  :type 'boolean
  :safe  #'booleanp
  :group 'enh-ruby)

(defcustom enh-ruby-encoding-map
  '((us-ascii       . nil)       ;; Do not put coding: us-ascii
    (utf-8          . nil)       ;; Do not put coding: utf-8
    (shift-jis      . cp932)     ;; Emacs charset name of Shift_JIS
    (shift_jis      . cp932)     ;; MIME charset name of Shift_JIS
    (japanese-cp932 . cp932))    ;; Emacs charset name of CP932
  "Alist to map encoding name from Emacs to ruby."
  :type '(alist :key-type   (symbol :tag "Encoding")
                :value-type (choice (const :tag "Ignore" nil)
                                    (symbol :tag "Charset")))
  :safe (lambda (xs)
          (and (listp xs)
           (cl-every (lambda (x)
                       (and (symbolp (car x))
                            (enh-symbol-or-null-p (cdr x))))
                     xs)))
  :group 'enh-ruby)

(defcustom enh-ruby-extra-keywords nil
  "*A list of idents that will be fontified as keywords.

`erm-reset' will need to be called in order for any global
changes to take effect.

This variable can also be buffer local in which case it will
override the global value for the buffer it is local
to. `ruby-local-enable-extra-keywords' needs to be called after
the value changes."
  :type '(repeat string)
  :safe #'listp
  :group 'enh-ruby)

(defcustom enh-ruby-hanging-brace-deep-indent-level 0
  "*Extra hanging deep indentation for continued ruby curly or square braces."
  :type 'integer
  :safe #'integerp
  :group 'enh-ruby)

(defcustom enh-ruby-hanging-brace-indent-level 2
  "*Extra hanging indentation for continued ruby curly braces."
  :type 'integer
  :safe #'integerp
  :group 'enh-ruby)

(defcustom enh-ruby-hanging-indent-level 2
  "*Extra hanging Indentation for continued ruby statements."
  :type 'integer
  :safe #'integerp
  :group 'enh-ruby)

(defcustom enh-ruby-hanging-paren-deep-indent-level 0
  "*Extra hanging deep indentation for continued ruby parenthesis."
  :type 'integer
  :safe #'integerp
  :group 'enh-ruby)

(defcustom enh-ruby-hanging-paren-indent-level 2
  "*Extra hanging indentation for continued ruby parenthesis."
  :type 'integer
  :safe #'integerp
  :group 'enh-ruby)

(defcustom enh-ruby-indent-level 2
  "*Indentation of ruby statements."
  :type 'integer
  :safe #'integerp
  :group 'enh-ruby)

(defcustom enh-ruby-indent-tabs-mode nil
  "*Indentation can insert tabs in ruby mode if this is non-nil."
  :type 'boolean
  :safe #'booleanp
  :group 'enh-ruby)

(defcustom enh-ruby-preserve-indent-in-heredocs nil
  "Indent heredocs and multiline strings like ‘text-mode’.

Warning: does not play well with command ‘electric-indent-mode’."
  :type 'boolean
  :safe #'booleanp
  :group 'enh-ruby)

(defcustom enh-ruby-program "ruby"
  "The ruby program to parse the source."
  :type 'string
  :safe #'stringp
  :group 'enh-ruby)

(defcustom enh-ruby-use-encoding-map t
  "*Use `enh-ruby-encoding-map' to set encoding magic comment if this is non-nil."
  :type 'boolean
  :safe #'booleanp
  :group 'enh-ruby)

(defvar enh-ruby-use-ruby-mode-show-parens-config nil
  "This flag has no effect anymore as ERM supports command
‘show-paren-mode’ directly.")

(make-obsolete-variable 'enh-ruby-use-ruby-mode-show-parens-config nil "2018-04-03")

;; TODO: renames:
;;
;; enh-ruby-indent-level:                    2
;; enh-ruby-hanging-indent-level:            2
;; enh-ruby-hanging-brace-deep-indent-level: 0
;; enh-ruby-hanging-brace-indent-level:      2
;; enh-ruby-hanging-paren-deep-indent-level: 0
;; enh-ruby-hanging-paren-indent-level:      2
;;
;; Versus:
;;
;; enh-ruby-indent-level:                    2
;; enh-ruby-indent-level-hanging:            2
;; enh-ruby-indent-level-hanging-paren:      2
;; enh-ruby-indent-level-hanging-paren-deep: 0
;; enh-ruby-indent-level-hanging-brace:      2
;; enh-ruby-indent-level-hanging-brace-deep: 0

(defvar need-syntax-check-p)
(defvar erm-buff-num)
(defvar erm-e-w-status)
(defvar erm-full-parse-p)

;;; Constants

(defconst enh-ruby-block-end-re "\\_<end\\_>")

(defconst enh-ruby-symbol-chars "a-zA-Z0-9_=?!")

(defconst enh-ruby-symbol-re (concat "[" enh-ruby-symbol-chars "]"))

(defconst enh-ruby-defun-beg-keywords
  '("class" "module" "def")
  "Keywords at the beginning of definitions.")

(defconst enh-ruby-defun-beg-re
  (regexp-opt enh-ruby-defun-beg-keywords)
  "Regexp to match the beginning of definitions.")

(defconst enh-ruby-defun-and-name-re
  (concat "\\(" enh-ruby-defun-beg-re "\\)[ \t]+\\("
                                         ;; \\. and :: for class method
                                         "\\([A-Za-z_]" enh-ruby-symbol-re "*\\|\\.\\|::" "\\)"
                                         "+\\)")
  "Regexp to match definitions and their name.")

(defconst erm-process-delimiter
  "\n\0\0\0\n")

(define-abbrev-table 'enh-ruby-mode-abbrev-table ()
  "Abbrev table used by enhanced-ruby-mode.")

(define-abbrev enh-ruby-mode-abbrev-table "end" "end"
  #'indent-for-tab-command :system t)

(defvar enh-ruby-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map "{"             #'enh-ruby-electric-brace)
    (define-key map "}"             #'enh-ruby-electric-brace)
    (define-key map (kbd "M-C-a")   #'enh-ruby-beginning-of-defun)
    (define-key map (kbd "M-C-e")   #'enh-ruby-end-of-defun)
    (define-key map (kbd "M-C-b")   #'enh-ruby-backward-sexp)
    (define-key map (kbd "M-C-f")   #'enh-ruby-forward-sexp)
    (define-key map (kbd "M-C-p")   #'enh-ruby-beginning-of-block)
    (define-key map (kbd "M-C-n")   #'enh-ruby-end-of-block)
    (define-key map (kbd "M-C-h")   #'enh-ruby-mark-defun)
    (define-key map (kbd "M-C-q")   #'enh-ruby-indent-exp)
    (define-key map (kbd "C-c C-f") #'enh-ruby-find-file)
    (define-key map (kbd "C-c C-e") #'enh-ruby-find-error)
    (define-key map (kbd "C-c /")   #'enh-ruby-insert-end)
    (define-key map (kbd "C-c {")   #'enh-ruby-toggle-block)
    (define-key map (kbd "M-C-u")   #'enh-ruby-up-sexp)
    (define-key map (kbd "C-j")     #'reindent-then-newline-and-indent)
    map)
  "Syntax table in use in ‘enh-ruby-mode’ buffers.")

(defvar enh-ruby-mode-syntax-table
  (let ((table (make-syntax-table)))
    (modify-syntax-entry ?\' "\"" table)
    (modify-syntax-entry ?\" "\"" table)
    (modify-syntax-entry ?\` "\"" table)
    (modify-syntax-entry ?#  "<"  table)
    (modify-syntax-entry ?\n ">"  table)
    (modify-syntax-entry ?\\ "\\" table)
    (modify-syntax-entry ?$  "'"  table)
    (modify-syntax-entry ??  "_"  table)
    (modify-syntax-entry ?_  "_"  table)
    (modify-syntax-entry ?:  "'"  table)
    (modify-syntax-entry ?<  "."  table)
    (modify-syntax-entry ?>  "."  table)
    (modify-syntax-entry ?&  "."  table)
    (modify-syntax-entry ?|  "."  table)
    (modify-syntax-entry ?%  "."  table)
    (modify-syntax-entry ?=  "."  table)
    (modify-syntax-entry ?/  "."  table)
    (modify-syntax-entry ?+  "."  table)
    (modify-syntax-entry ?*  "."  table)
    (modify-syntax-entry ?-  "."  table)
    (modify-syntax-entry ?\; "."  table)
    (modify-syntax-entry ?\( "()" table)
    (modify-syntax-entry ?\) ")(" table)
    (modify-syntax-entry ?\{ "(}" table)
    (modify-syntax-entry ?\} "){" table)
    (modify-syntax-entry ?\[ "(]" table)
    (modify-syntax-entry ?\] ")[" table)
    (modify-syntax-entry ?@  "'"  table)

    table)
  "Syntax table used by ‘enh-ruby-mode’ buffers.")

(defconst enh-ruby-font-lock-keyword-beg-re "\\(?:^\\|[^.@$:]\\|\\.\\.\\)")

(defconst enh-ruby-font-lock-keywords
  `(;; Core methods that have required arguments.
    (,(concat
       enh-ruby-font-lock-keyword-beg-re
       (regexp-opt
        '( ;; built-in methods on Kernel TODO: add more via reflection?
          "at_exit"
          "autoload"
          "autoload?"
          "callcc"
          "catch"
          "eval"
          "exec"
          "format"
          "lambda"
          "load"
          "loop"
          "open"
          "p"
          "print"
          "printf"
          "proc"
          "putc"
          "puts"
          "require"
          "require_relative"
          "spawn"
          "sprintf"
          "syscall"
          "system"
          "throw"
          "trace_var"
          "trap"
          "untrace_var"
          "warn"
          ;; keyword-like private methods on Module
          "alias_method"
          "attr"
          "attr_accessor"
          "attr_reader"
          "attr_writer"
          "define_method"
          "extend"
          "include"
          "module_function"
          "prepend"
          "private_class_method"
          "private_constant"
          "public_class_method"
          "public_constant"
          "refine"
          "using")
        'symbols))
     (1 (unless (looking-at " *\\(?:[]|,.)}=]\\|$\\)")
          font-lock-builtin-face)))
    ;; Kernel methods that have no required arguments.
    (,(concat
       enh-ruby-font-lock-keyword-beg-re
       (regexp-opt
        '("__callee__"
          "__dir__"
          "__method__"
          "abort"
          "binding"
          "block_given?"
          "caller"
          "exit"
          "exit!"
          "fail"
          "fork"
          "global_variables"
          "local_variables"
          "private"
          "protected"
          "public"
          "raise"
          "rand"
          "readline"
          "readlines"
          "sleep"
          "srand")
        'symbols))
     (1 font-lock-builtin-face)))
  "Additional expressions to highlight in ‘enh-ruby-mode’.")

(defconst enh-ruby-font-names
  '(nil
    font-lock-string-face
    font-lock-type-face
    font-lock-variable-name-face
    font-lock-comment-face
    font-lock-constant-face
    font-lock-string-face
    enh-ruby-string-delimiter-face
    enh-ruby-regexp-delimiter-face
    font-lock-function-name-face
    font-lock-keyword-face
    enh-ruby-heredoc-delimiter-face
    enh-ruby-op-face
    enh-ruby-regexp-face)
  "Font faces used by ‘enh-ruby-mode’.")

;;; Code:

;;;###autoload
(defun enh-symbol-or-null-p (x)
  "Return true if X is either a symbol or null. Used for defcustom safe check."
  (or (symbolp x)
      (null x)))

(define-obsolete-variable-alias 'enh/symbol-or-null-p
                                'enh-symbol-or-null-p "2022-07-07")

;;;###autoload
(define-derived-mode enh-ruby-mode prog-mode "EnhRuby"
  "Enhanced Major mode for editing Ruby code.

\\{enh-ruby-mode-map}"

  (setq-local comment-column               enh-ruby-comment-column)
  (setq-local comment-end                  "")
  (setq-local comment-start                "#")
  (setq-local comment-start-skip           "#+ *")
  (setq-local erm-buff-num                 nil)
  (setq-local erm-e-w-status               nil)
  (setq-local erm-full-parse-p             nil)
  (setq-local indent-line-function         #'enh-ruby-indent-line)
  ;; (setq-local forward-sexp-function        #'enh-ruby-forward-sexp)
  (setq-local need-syntax-check-p          nil)
  (setq-local paragraph-ignore-fill-prefix t)
  (setq-local parse-sexp-ignore-comments   t)
  (setq-local parse-sexp-lookup-properties t)
  (setq-local require-final-newline        mode-require-final-newline)
  (setq-local beginning-of-defun-function  #'enh-ruby-beginning-of-defun)
  (setq-local end-of-defun-function        #'enh-ruby-end-of-defun)
  (setq-local show-paren-data-function     #'erm-show-paren-data-function)
  (setq-local paragraph-start              (concat "$\\|" page-delimiter))
  (setq-local paragraph-separate           paragraph-start)

  (setq-local add-log-current-defun-function
              'enh-ruby-add-log-current-method)

  (setq-local font-lock-keywords    enh-ruby-font-lock-keywords)
  (setq font-lock-defaults          '((enh-ruby-font-lock-keywords) t))
  (setq indent-tabs-mode            enh-ruby-indent-tabs-mode)
  (setq imenu-create-index-function #'enh-ruby-imenu-create-index)

  (if enh-ruby-add-encoding-comment-on-save
    (add-hook 'before-save-hook #'enh-ruby-mode-set-encoding nil t))

  (add-hook 'change-major-mode-hook #'erm-major-mode-changed     nil t)
  (add-hook 'kill-buffer-hook       #'erm-buffer-killed          nil t)

  (abbrev-mode)
  (erm-reset-buffer))

;;; Faces:

(require 'color nil t)

(defun erm-darken-color (name)
  "Return color NAME with foreground 20% darker."
  (color-darken-name (face-attribute name :foreground) 20))

(defun erm-define-faces ()
  "Define faces for ‘enh-ruby-mode’."

 (defface enh-ruby-string-delimiter-face
   `((t :foreground ,(erm-darken-color font-lock-string-face)))
   "Face used to highlight string delimiters like quotes and %Q."
   :group 'enh-ruby)

 (defface enh-ruby-heredoc-delimiter-face
   `((t :foreground ,(erm-darken-color font-lock-string-face)))
   "Face used to highlight string heredoc anchor strings like <<END and END"
   :group 'enh-ruby)

 (defface enh-ruby-regexp-delimiter-face
   `((t :foreground ,(erm-darken-color font-lock-string-face)))
   "Face used to highlight regexp delimiters like / and %r."
   :group 'enh-ruby)

 (defface enh-ruby-regexp-face
   `((t :foreground ,(face-attribute font-lock-string-face :foreground)))
   "Face used to highlight the inside of regular expressions"
   :group 'enh-ruby)

 (defface enh-ruby-op-face
   `((t :foreground ,(erm-darken-color font-lock-keyword-face)))
   "Face used to highlight operators like + and ||"
   :group 'enh-ruby)

 (defface erm-syn-errline
   '((t (:box (:line-width 1 :color "red"))))
   "Face used for marking error lines."
   :group 'enh-ruby)

 (defface erm-syn-warnline
   '((t (:box (:line-width 1 :color "orange"))))
   "Face used for marking warning lines."
   :group 'enh-ruby))

(add-hook 'enh-ruby-mode-hook #'erm-define-faces)

;;; Support Functions:

(defun enh-ruby-mode-set-encoding ()
  "Check encoding on save and create a magic comment if non-standard encoding."
  (save-excursion
    (widen)
    (goto-char (point-min))
    (when (re-search-forward "[^[:ascii:]]" nil t)
      (goto-char (point-min))
      (let ((coding-system
             (or coding-system-for-write
                 buffer-file-coding-system)))
        (if coding-system
            (setq coding-system
                  (or (coding-system-get coding-system 'mime-charset)
                      (coding-system-change-eol-conversion coding-system nil))))
        (setq coding-system
              (if coding-system
                  (symbol-name
                   (or (and enh-ruby-use-encoding-map
                            (cdr (assq coding-system enh-ruby-encoding-map)))
                       coding-system))
                "ascii-8bit"))
        (if (looking-at "^#!") (beginning-of-line 2))
        (cond ((looking-at "\\s *#.*-\*-\\s *\\(en\\)?coding\\s *:\\s *\\([-a-z0-9_]*\\)\\s *\\(;\\|-\*-\\)")
               (unless (string= (match-string 2) coding-system)
                 (goto-char (match-beginning 2))
                 (delete-region (point) (match-end 2))
                 (and (looking-at "-\*-")
                      (let ((n (skip-chars-backward " ")))
                        (cond ((= n 0) (insert "  ") (backward-char))
                              ((= n -1) (insert " "))
                              ((forward-char)))))
                 (insert coding-system)))
              ((looking-at "\\s *#.*coding\\s *[:=]"))
              ((equal "utf-8" coding-system) 'do-nothing) ; hack? should check version?
              (t (insert "# -*- coding: " coding-system " -*-\n")))))))

(defvar erm-ruby-process nil "The current erm process where Emacs is interacting with.")
(defvar erm-response     nil "Private variable.")
(defvar erm-parsing-p    nil "Parsing: t, nil, \\='a (all?), \\='p (partial?).")

(defun erm-ruby-get-process ()
  "Return (or create) the current ruby parser process."
  (when (and erm-ruby-process (not (equal (process-status erm-ruby-process) 'run)))
    (erm-reset)
    (throw 'interrupted t))
  (unless erm-ruby-process
    (let ((process-connection-type nil))
      (setq erm-ruby-process
            (start-process "erm-ruby-process"
                           nil
                           enh-ruby-program (concat (erm-source-dir)
                                                    "ruby/erm.rb")))
      (set-process-coding-system erm-ruby-process 'utf-8 'utf-8)
      (set-process-filter erm-ruby-process #'erm-filter)
      (set-process-query-on-exit-flag erm-ruby-process nil)
      (process-send-string
       erm-ruby-process
       (concat "x0:"
               (mapconcat #'identity (default-value 'enh-ruby-extra-keywords) " ")
               ":"
               erm-process-delimiter))))

  erm-ruby-process)

(defvar erm-no-parse-needed-p nil "Private variable.")
(defvar erm-source-dir        nil "Private variable.")

(defun erm-source-dir ()
  "Return the directory for enh-ruby-mode.el."
  (or erm-source-dir
    (setq erm-source-dir (file-name-directory (find-lisp-object-file-name
                                               'erm-source-dir
                                               (symbol-function 'erm-source-dir))))))


(defvar erm-next-buff-num nil "Private variable.")
(defvar erm-parse-buff nil "Private variable.")
(defvar erm-reparse-list nil "Private variable.")
(defvar erm-syntax-check-list nil "Private variable.")

(defun erm-reset-syntax-buffers (list)
  (let ((buffer (car list)))
    (when buffer
      (when (buffer-live-p buffer)
        (with-current-buffer buffer (setq need-syntax-check-p nil)))
      (erm-reset-syntax-buffers (cdr list)))))

(defun erm-ruby-program-version ()
  "Display the current version of ERM-RUBY-PROGRAM"
  (interactive)
  (let* ((command (format "%s -v" enh-ruby-program))
         (version (shell-command-to-string command))
         (version (cl-second (split-string version))))
   (message "erm-ruby-program -v = %s" version)))

(defun erm-reset ()
  "Reset all ‘enh-ruby-mode’ buffers and restart the ruby parser."
  (interactive)
  (erm-reset-syntax-buffers erm-syntax-check-list)
  (setq erm-reparse-list nil
        erm-syntax-check-list nil
        erm-parsing-p nil
        erm-parse-buff nil
        erm-next-buff-num 1)
  (when erm-ruby-process
    (delete-process erm-ruby-process)
    (setq erm-ruby-process nil))

  (dolist (buf (buffer-list))
    (with-current-buffer buf
      (when (eq 'enh-ruby-mode major-mode)
        (erm-reset-buffer)))))

(defun erm-major-mode-changed ()
  (remove-hook 'kill-buffer-hook #'erm-buffer-killed t)
  (erm-buffer-killed))

(defun erm-proc-string (prefix)
  (concat prefix (number-to-string erm-buff-num) ":" erm-process-delimiter))

(defun erm-buffer-killed ()
  (remove-hook 'kill-buffer-hook #'erm-buffer-killed t)
  (catch 'interrupted
   (process-send-string (erm-ruby-get-process) (erm-proc-string "k"))))

(defun erm-reset-buffer ()
  (setq erm-buff-num erm-next-buff-num)
  (setq erm-next-buff-num (1+ erm-buff-num))
  (add-hook 'after-change-functions #'erm-req-parse nil t)
  (unless
      (enh-ruby-local-enable-extra-keywords)
    (enh-ruby-fontify-buffer)))

(defun enh-ruby-local-enable-extra-keywords ()
  "If the variable `ruby-extra-keywords' is buffer local then
enable the keywords for current buffer."
  (when (local-variable-p 'enh-ruby-extra-keywords)
      (process-send-string (erm-ruby-get-process)
                           (concat "x"
                                   (number-to-string erm-buff-num) ":"
                                   (mapconcat #'identity enh-ruby-extra-keywords " ")
                                   ":" erm-process-delimiter))
      (enh-ruby-fontify-buffer)
      t))

(defun enh-ruby-electric-brace (arg)
  (interactive "P")
  (insert-char last-command-event 1)
  (enh-ruby-indent-line)
  (delete-char -1)
  (self-insert-command (prefix-numeric-value arg)))

(defun enh-ruby-brace-to-do-end (orig end)
  (let (beg-marker end-marker)
    (goto-char end)
    (when (eq (char-before) ?\})
      (delete-char -1)
      (when (save-excursion
              (skip-chars-backward " \t")
              (not (bolp)))
        (insert "\n"))
      (insert "end")
      (setq end-marker (point-marker))
      (when (and (not (eobp)) (eq (char-syntax (char-after)) ?w))
        (insert " "))
      (goto-char orig)
      (delete-char 1)
      (when (eq (char-syntax (char-before)) ?w)
        (insert " "))
      (insert "do")
      (setq beg-marker (point-marker))
      (when (looking-at "\\(\\s \\)*|")
        (unless (match-beginning 1)
          (insert " "))
        (goto-char (1+ (match-end 0)))
        (search-forward "|"))
      (unless (looking-at "\\s *$")
        (insert "\n"))
      (indent-region beg-marker end-marker)
      (goto-char beg-marker))))

(defun enh-ruby-do-end-to-brace (orig end)
  (let (beg-marker end-marker beg-pos end-pos)
    (goto-char (- end 3))
    (when (looking-at enh-ruby-block-end-re)
      (delete-char 3)
      (setq end-marker (point-marker))
      (insert "}")
      (goto-char orig)
      (delete-char 2)
      ;; Maybe this should be customizable, let's see if anyone asks.
      (insert "{ ")
      (setq beg-marker (point-marker))
      (when (looking-at "\\s +|")
        (delete-char (- (match-end 0) (match-beginning 0) 1))
        (forward-char)
        (re-search-forward "|" (line-end-position) t))
      (save-excursion
        (skip-chars-forward " \t\n\r")
        (setq beg-pos (point))
        (goto-char end-marker)
        (skip-chars-backward " \t\n\r")
        (setq end-pos (point)))
      (when (or
             (< end-pos beg-pos)
             (and (= (line-number-at-pos beg-pos) (line-number-at-pos end-pos))
                  (< (+ (current-column) (- end-pos beg-pos) 2) fill-column)))
        (just-one-space -1)
        (goto-char end-marker)
        (just-one-space -1))
      (goto-char beg-marker))))

(defun enh-ruby-toggle-block ()
  "Toggle block type from do-end to braces or back.
The block must begin on the current line or above it and end after the point.
If the result is do-end block, it will always be multiline."
  (interactive)
  (let* ((pos (point))
         (block-start (save-excursion
                        (end-of-line)
                        (while (and (not (bobp))
                                    (not (enh-ruby-point-block-p)))
                          (backward-char))
                        (point)))
         (block-end (save-excursion
                      (goto-char block-start)
                      (enh-ruby-forward-sexp)
                      (point))))
    (if (< pos block-end)
        (if (eq (char-after block-start) ?{)
            (enh-ruby-brace-to-do-end block-start block-end)
          (enh-ruby-do-end-to-brace block-start block-end)))))

(defun enh-ruby-imenu-create-index-in-block (_prefix beg end)
  (let* ((index-alist '())
         (pos beg)
         (prop (get-text-property pos 'indent)))
    (setq end (or end (point-max)))
    (while (and pos (< pos end))
      (goto-char pos)
      (when (and (eq prop 'b) (looking-at enh-ruby-defun-and-name-re))
        (push (cons (concat (match-string 1) " " (match-string 2)) pos) index-alist))

      (setq prop (and (setq pos (enh-ruby-next-indent-change pos))
                      (get-text-property pos 'indent))))

    index-alist))

(defun enh-ruby-imenu-create-index ()
  (nreverse (enh-ruby-imenu-create-index-in-block nil (point-min) nil)))

(defun enh-ruby-add-log-current-method ()
  "Return current method string."
  (condition-case nil
      (save-excursion
        (enh-ruby-beginning-of-defun 1)
        (when (looking-at enh-ruby-defun-and-name-re)
          (let ((def-or-mod (match-string-no-properties 1))
                (def-name   (match-string-no-properties 2)))
            (if (string= "def" def-or-mod)
                (progn
                  (enh-ruby-up-sexp)
                  (when (looking-at enh-ruby-defun-and-name-re)
                    (let ((_mod-or-class (match-string-no-properties 1))
                          (mod-name   (match-string-no-properties 2)))
                      (let* ((meth-name-re (concat
                                            (regexp-opt (list "self" mod-name)
                                                        'words)
                                            "\\.\\(.+\\)"))
                             (cls-meth (and (string-match meth-name-re def-name)
                                            (match-string 2 def-name)))
                             (name (or cls-meth def-name))
                             (sep (if cls-meth "." "#")))
                        (concat mod-name sep name)))))
              nil))))))

;; Stolen shamelessly from James Clark's nxml-mode.
(defmacro erm-with-unmodifying-text-property-changes (&rest body)
  "Evaluate BODY without modifying the buffer's text properties.
Any text properties changes happen as usual but the changes are
not treated as modifications to the buffer."
  (let ((modified (make-symbol "modified")))
    `(let ((,modified (buffer-modified-p))
           (inhibit-read-only t)
           (inhibit-modification-hooks t)
           (buffer-undo-list t)
           (deactivate-mark nil)
           ;; Apparently these avoid file locking problems.
           (buffer-file-name nil)
           (buffer-file-truename nil))
       (unwind-protect
           (progn ,@body)
         (unless ,modified
           (restore-buffer-modified-p nil))))))

(defun enh-ruby-fontify-buffer ()
  "Fontify the current buffer. Useful if faces are out of sync."
  (interactive)
  (if (and erm-parsing-p
           (not (eq erm-parse-buff (current-buffer))))
      (erm-reparse-diff-buf)
    (setq erm-full-parse-p t)
    (condition-case nil
        (erm-req-parse nil nil nil)
      (error nil))))

(defun erm-reparse-diff-buf ()
  (setq erm-reparse-list (cons (current-buffer) erm-reparse-list)))

(defun erm-req-parse (min max len)
  (when (and enh-ruby-check-syntax (not need-syntax-check-p))
    (setq need-syntax-check-p t)
    (setq erm-syntax-check-list (cons (current-buffer) erm-syntax-check-list)))
  (let ((pc (if erm-parsing-p
                (if (eq erm-parse-buff (current-buffer))
                    (setq erm-parsing-p 'a)
                  'dbuf)
              (setq erm-response "")
              (setq erm-parsing-p t)
              (if (not erm-full-parse-p)
                  (if erm-no-parse-needed-p
                      (progn (setq erm-parsing-p nil) 'a)
                    'p)
                (setq min (point-min)
                      max (point-max)
                      len 0
                      erm-full-parse-p nil)
                'r)))
        interrupted-p)
    (setq interrupted-p
          (catch 'interrupted
            (if (eq pc 'dbuf)
                (erm-reparse-diff-buf)
              (setq erm-parse-buff (current-buffer))
              (process-send-string (erm-ruby-get-process)
                                   (format "%s%d:%d:%d:%d:%d:"
                                           pc
                                           erm-buff-num
                                           (point-min)
                                           (point-max)
                                           min
                                           len))
              (process-send-region erm-ruby-process min max)
              (process-send-string erm-ruby-process erm-process-delimiter))
            nil))
    (when interrupted-p
      (setq erm-full-parse-p t))))

(defun erm-wait-for-parse ()
  (while erm-parsing-p
    (accept-process-output (erm-ruby-get-process) 0.5)))

(defun erm-filter (_proc response)
  (setq erm-response (concat erm-response response))
  (when (and (> (length erm-response) 5)
             (string= erm-process-delimiter (substring erm-response -5 nil)))
    (setq response (substring erm-response 0 -5))
    (setq erm-response "")
    (if (buffer-live-p erm-parse-buff)
        (with-current-buffer erm-parse-buff
          (erm-with-unmodifying-text-property-changes
           (erm-parse response)))
      (erm-reset))))

(defun erm-ready ()
  (if erm-full-parse-p
      (enh-ruby-fontify-buffer)
    (setq erm-parsing-p t)
    (process-send-string (erm-ruby-get-process) (erm-proc-string "g"))))

(defun extra-col-% ()
  "Return extra column adjustments in case we are ‘looking-at’ a % construct."
  (or (and (looking-at "%\\([^[:alnum:]]\\|[QqWwIixrs].\\)")
           (1- (length (match-string-no-properties 0))))
      0))

(defun enh-ruby-continue-p (prop)
  "Return whether PROP is a continue property."
  (eq 'c prop))

(defun enh-ruby-block-p (prop)
  "Return whether PROP is a block property."
  (eq 'd prop))

(defun enh-ruby-point-continue-p (point)
  "Return whether property at POINT is a continue property."
  (enh-ruby-continue-p (get-text-property point 'indent)))

(defun enh-ruby-point-block-p (&optional point)
  "Return whether property at POINT is a block property."
  (or point (setq point (point)))
  (enh-ruby-block-p (get-text-property point 'indent)))

(defun enh-ruby-calculate-indent (&optional start-point)
  "Calculate the indentation of the previous line and its level at START-POINT."
  (save-excursion
    (when start-point (goto-char start-point))
    (if (bobp)
        0
      (forward-line 0)
      (skip-syntax-forward " " (line-end-position))
      (let ((pos (line-beginning-position))
            (prop (get-text-property (point) 'indent))
            (face (get-text-property (point) 'font-lock-face)))
        (cond
         ((or (eq 'e prop) (eq 's prop))
          (when (eq 's prop) (forward-char))
          (enh-ruby-backward-sexp)
          (let ((bprop (get-text-property (point) 'indent)))
            (cond ((eq 'd bprop)
                   (setq pos (point))
                   (enh-ruby-skip-non-indentable)
                   (let ((indent (enh-ruby-calculate-indent-1 pos (line-beginning-position)))
                         (chained-stmt-p (save-excursion
                                           (forward-line 0)
                                           (enh-ruby-point-continue-p (point)))))
                     (+ indent
                        (if chained-stmt-p enh-ruby-hanging-indent-level 0))))
                  ((and (not enh-ruby-deep-indent-construct)
                        (eq 'b bprop))
                   (current-indentation))
                  (t
                   (current-column)))))
         ((eq 'r prop)                  ; TODO: make these consistent file-wide
          (let (opening-col opening-is-last-thing-on-line)
            (save-excursion
              (enh-ruby-backward-sexp)
              (setq opening-col (+ (current-column)
                                   (extra-col-%)))
              (forward-char 1)
              (skip-syntax-forward " " (line-end-position))
              (setq opening-is-last-thing-on-line (eolp)))
            (if (and enh-ruby-deep-indent-paren
                     (not enh-ruby-bounce-deep-indent)
                     (not opening-is-last-thing-on-line))
                opening-col             ; deep + !bounce + !hanging = match open
              (forward-line -1)
              (enh-ruby-skip-non-indentable)
              (let* ((opening-char (save-excursion
                                     (enh-ruby-backward-sexp)
                                     (char-after)))
                     (proposed-col (enh-ruby-calculate-indent-1 pos
                                                                (line-beginning-position)))
                     (chained-stmt-p (save-excursion (enh-ruby-backward-sexp)
                                                     (forward-line 0)
                                                     (enh-ruby-point-continue-p (point))))
                     (offset (if (char-equal opening-char ?{)
                                 enh-ruby-hanging-brace-indent-level
                               enh-ruby-hanging-paren-indent-level)))
                (cond ((and chained-stmt-p
                            (not enh-ruby-bounce-deep-indent))
                       (- proposed-col offset))
                      ((< proposed-col opening-col)
                       (- proposed-col offset))
                      (t opening-col))))))

         ((or (memq face '(font-lock-string-face enh-ruby-heredoc-delimiter-face))
              (and (eq 'font-lock-variable-name-face face)
                   (looking-at "#")))
          (when enh-ruby-preserve-indent-in-heredocs
            (forward-line -1)
            (back-to-indentation))
          (current-column))

         (t
          (forward-line -1)

          (enh-ruby-skip-non-indentable)
          (enh-ruby-calculate-indent-1 pos (line-beginning-position))))))))

(defun erm-looking-at-not-indentable ()
  (skip-syntax-forward " " (line-end-position))
  (let ((face (get-text-property (point) 'font-lock-face)))
    (or (= (point) (line-end-position))
        (memq face '(font-lock-string-face font-lock-comment-face enh-ruby-heredoc-delimiter-face))
        (and (eq 'font-lock-variable-name-face face)
             (looking-at "#"))
        (and (memq face '(enh-ruby-regexp-delimiter-face enh-ruby-string-delimiter-face))
             (> (point) (point-min))
             (eq (get-text-property (1- (point)) 'font-lock-face)
                 'font-lock-string-face)))))

(defun enh-ruby-skip-non-indentable ()
  (forward-line 0)
  (while (and (> (point) (point-min))
              (erm-looking-at-not-indentable))
    (skip-chars-backward " \n\t\r\v\f")
    (forward-line 0)))

(defvar enh-ruby-last-bounce-line nil
  "The last line that `erm-bounce-deep-indent-paren` was run against.")

(defvar enh-ruby-last-bounce-deep nil
  "The last result from `erm-bounce-deep-indent-paren`.")

(defun enh-ruby-calculate-indent-1 (limit pos)
  (goto-char pos)

  (let* ((start-pos pos)
         (start-prop (get-text-property pos 'indent))
         (prop start-prop)
         (indent (- (current-indentation)
                    (if (eq 'c prop) enh-ruby-hanging-indent-level 0)))
         (nbc 0)
         (npc 0)
         col max bc pc)

    (setq enh-ruby-last-bounce-deep
          (and (eq enh-ruby-last-bounce-line (line-number-at-pos))
               (not enh-ruby-last-bounce-deep)))
    (setq enh-ruby-last-bounce-line (line-number-at-pos))

    (while (< pos limit)
      (unless prop
        (setq pos (next-single-property-change pos 'indent (current-buffer) limit))
        (when (< pos limit)
          (setq prop (get-text-property pos 'indent))))
      (setq col (- pos start-pos -1))

      (cond
       ;; 'l - [, (, {, %w/%i open  or | goalpost open
       ;; 'r - ], ), }, %w/%i close or | goalpost close
       ;; 'b - begin, def, case, if
       ;; 'd - do, {, embexpr (interpolation) start
       ;; 'e - end, embexpr (interpolation) end, close block }
       ;; 's - statement start on BACKDENT_KW else/when/rescue etc
       ;; 'c - continue - period followed by return (or other way around?)
       ((memq prop '(l))
        (let ((shallow-indent
               (if (char-equal (char-after pos) ?{)
                   (+ enh-ruby-hanging-brace-indent-level indent)
                 (+ enh-ruby-hanging-paren-indent-level indent)))
              (deep-indent
               (cond ((char-equal (char-after pos) ?{)
                      (+ enh-ruby-hanging-brace-deep-indent-level col))
                     ((char-equal (char-after pos) ?%)
                      (+ enh-ruby-hanging-brace-deep-indent-level
                         col
                         (save-excursion
                           (goto-char pos)
                           (extra-col-%))))
                     (t (+ enh-ruby-hanging-paren-deep-indent-level col))))
              (at-eol (save-excursion
                        (goto-char (1+ pos))
                        (skip-syntax-forward " " (line-end-position))
                        (eolp))))
          (if enh-ruby-bounce-deep-indent
              (setq pc (cons (if enh-ruby-last-bounce-deep
                                 shallow-indent
                               deep-indent)
                             pc))
            (setq pc (cons (if (and (not at-eol) enh-ruby-deep-indent-paren)
                               deep-indent
                             (let ((chained-stmt-p (enh-ruby-continue-p start-prop)))
                               (+ shallow-indent (if chained-stmt-p enh-ruby-hanging-paren-indent-level 0))))
                           pc)))))

       ((eq prop 'r)
        (if pc (setq pc (cdr pc)) (setq npc col)))

       ((memq prop '(b d s))
        (and (not enh-ruby-deep-indent-construct)
             (eq prop 'b)
             (setq col
                   (- col (- (save-excursion
                               (goto-char pos)
                               (current-column))
                             (current-indentation)))))
        (setq bc (cons col bc)))

       ((eq prop 'e)
        (if bc
            (setq bc (cdr bc))
          (setq nbc col))))

      (when (< (setq pos (1+ pos)) limit)
        (setq prop (get-text-property pos 'indent))))

    ;;(prin1 (list indent nbc bc npc pc))
    (setq pc (or (car pc) 0))
    (setq bc (or (car bc) 0))
    (setq max (max pc bc nbc npc))

    (+
     (if (eq 'c (get-text-property limit 'indent)) enh-ruby-hanging-indent-level 0)
     (cond
      ((= max 0)
       (if (not (memq (get-text-property start-pos 'font-lock-face)
                      '(enh-ruby-heredoc-delimiter-face font-lock-string-face)))
           indent
         (goto-char (or (enh-ruby-string-start-pos start-pos) limit))
         (current-indentation)))

      ((= max pc) (if (eq 'c (get-text-property limit 'indent))
                      (- pc enh-ruby-hanging-indent-level)
                    pc))

      ((= max bc)
       (if (eq 'd (get-text-property (+ start-pos bc -1) 'indent))
           (let ((chained-stmt-p (enh-ruby-continue-p start-prop)))
             (+ (enh-ruby-calculate-indent-1 (+ start-pos bc -1) start-pos)
                (* (if chained-stmt-p 2 1) enh-ruby-indent-level)))
         (+ bc enh-ruby-indent-level -1)))

      ((= max npc)
       (goto-char (+ start-pos npc))
       (enh-ruby-backward-sexp)
       (enh-ruby-calculate-indent-1 (point) (line-beginning-position)))

      ((= max nbc)
       (goto-char (+ start-pos nbc -1))
       (enh-ruby-backward-sexp)
       (enh-ruby-calculate-indent-1 (point) (line-beginning-position)))

      (t 0)))))

(defun enh-ruby-string-start-pos (pos)
  (when (< 0 (or (setq pos (previous-single-property-change pos 'font-lock-face)) 0))
    (previous-single-property-change pos 'font-lock-face)))

(defun enh-ruby-show-errors-at (pos face)
  (let ((overlays (overlays-at pos))
        overlay
        messages)

    ;; TODO:
    ;; (-map (lambda (o) (overlay-get o 'help-echo))
    ;;       (-filter (lambda (o) (and (overlay-get o 'erm-syn-overlay)
    ;;                                 (eq (overlay-get o 'font-lock-face) face)))))

    (while overlays
      (setq overlay (car overlays))
      (when (and (overlay-get overlay 'erm-syn-overlay)
                 (eq (overlay-get overlay 'font-lock-face) face))
        (setq messages (cons (overlay-get overlay 'help-echo) messages)))
      (setq overlays (cdr overlays)))

    (message "%s" (mapconcat #'identity messages "\n"))
    messages))

(defun enh-ruby-find-error (&optional warnings)
  "Search back, then forward for a syntax error/warning. Display
contents in mini-buffer. Optional WARNINGS will highlight
warnings instead of errors. (I think)."
  (interactive "^P")
  (let (messages
        (face (if warnings 'erm-syn-warnline 'erm-syn-errline))
        (pos (point)))
    (unless (eq last-command #'enh-ruby-find-error)
      (while (and (not messages) (> pos (point-min)))
        (setq messages (enh-ruby-show-errors-at (setq pos (previous-overlay-change pos)) face))))

    (unless messages
      (while (and (not messages) (< pos (point-max)))
        (setq messages (enh-ruby-show-errors-at (setq pos (next-overlay-change pos)) face))))

    (if messages
        (goto-char pos)
      (unless warnings
        (enh-ruby-find-error t)))))

(defun enh-ruby-find-file (filename)
  "Search for and edit FILENAME. Searching is done with `gem
which` but works for standard lib as well as gems."
  (interactive "sgem which ")
  (let* ((command (concat "gem which " filename))
         (output  (shell-command-to-string command))
         (path    (string-trim-right output)))
    (if (file-exists-p path)
        (find-file path)
      (message "%S found nothing" command))))

(defun enh-ruby-up-sexp (&optional arg)
  "Move up one balanced expression (sexp).
With ARG, do it that many times."
  (interactive "^p")
  (unless arg (setq arg 1))
  (while (>= (setq arg (1- arg)) 0)
    (let* ((count 1)
           prop)
      (goto-char
       (save-excursion
         (while (and (not (= (point) (point-min)))
                     (< 0 count))
           (goto-char (enh-ruby-previous-indent-change (point)))
           (setq prop (get-text-property (point) 'indent))
           (setq count (cond
                        ((or (eq prop 'l) (eq prop 'b) (eq prop 'd)) (1- count))
                        ((or (eq prop 'r) (eq prop 'e)) (1+ count))
                        (t count))))
         (point))))))

(defun enh-ruby-beginning-of-defun (&optional arg)
  "Move backward across expression (sexp) looking for a definition beginning.
With ARG, do it that many times."
  (interactive "^p")
  (unless arg (setq arg 1))
  (let (prop)
    (goto-char
     (save-excursion
       (while (>= (setq arg (1- arg)) 0)
         (while (and
                 (> (point) (point-min))
                 (progn
                  (enh-ruby-backward-sexp 1)
                  (setq prop (get-text-property (point) 'indent))
                  (not (and (eq prop 'b) (looking-at enh-ruby-defun-beg-re)))))))
       (point)))))

(defun enh-ruby-mark-defun ()
  "Put mark at end of this Ruby definition, point at beginning."
  (interactive)
  (push-mark (point))
  (enh-ruby-beginning-of-defun 1)
  (enh-ruby-forward-sexp 1)
  (forward-line 1)
  (push-mark (point) nil t)
  (forward-line -1)
  (end-of-line)
  (enh-ruby-backward-sexp 1)
  (forward-line 0))

(defun enh-ruby-indent-exp (&optional _shutup-p)
  "Indent each line in the balanced expression following point syntactically."
  (interactive "*P")
  (erm-wait-for-parse)
  (let ((end-pos (save-excursion (enh-ruby-forward-sexp 1) (point))))
    (indent-region (point) end-pos)))

(set-advertised-calling-convention 'enh-ruby-indent-exp '() "2022-04-26")

(defun enh-ruby-beginning-of-block (&optional arg)
  "Move backward across one expression (sexp) looking for a block beginning.
With ARG, do it that many times."
  (interactive "^p")
  (unless arg (setq arg 1))
  (let (prop
        pos)
    (goto-char
     (save-excursion
       (while (>= (setq arg (1- arg)) 0)
         (while (progn
                  (enh-ruby-backward-sexp 1)
                  (setq pos (point))
                  (setq prop (get-text-property pos 'indent))
                  (and
                   (> pos (point-min))
                   (not (or (eq prop 'b) (eq prop 'd)))))))
       (point)))))

(defun enh-ruby-end-of-defun (&optional arg)
  "Move forwards across one expression (sexp) looking for a definition end.
With ARG, do it that many times."
  (interactive "^p")
  (unless arg (setq arg 1))
  (let (prop)
    (while (>= (setq arg (1- arg)) 0)
         (while (and
                 (< (point) (point-max))
                 (progn
                   (enh-ruby-forward-sexp 1)
                   (setq prop (get-text-property (- (point) 3) 'indent))
                   (not (and (eq prop 'e)
                             (save-excursion
                               (enh-ruby-backward-sexp 1)
                               (looking-at enh-ruby-defun-beg-re))))))))
    (point)))

(defun enh-ruby-end-of-block (&optional arg)
  "Move forwards across one balanced expression (sexp) looking for a block end.
With ARG, do it that many times."
  ;; this is totally broken for {} blocks! see the -3 below
  (interactive "^p")
  (unless arg (setq arg 1))
  (let (prop
        pos)
    (goto-char
     (save-excursion
       (while (>= (setq arg (1- arg)) 0)
         (while (progn
                  (enh-ruby-forward-sexp 1)
                  (setq pos (point))
                  (setq prop (get-text-property (- pos 3) 'indent))
                  (and (< pos (point-max)) (not (or (eq prop 'e) ; closers
                                                    (eq prop 'r))
                                                )))))
       (point)))))

(defun enh-ruby-backward-sexp (&optional arg)
  "Move backward across one balanced expression (sexp).
With ARG, do it that many times."
  (interactive "^p")

  (unless arg (setq arg 1))
  (while (>= (setq arg (1- arg)) 0)
    (let* ((pos (point))
           (prop (get-text-property pos 'indent))
           (count 0))

      (unless (memq prop '(r e))
        (setq prop (and (setq pos (enh-ruby-previous-indent-change pos))
                        (goto-char pos) ;; TODO: remove?
                        (get-text-property pos 'indent))))

      (while (< 0 (setq count
                        (cond
                         ((memq prop '(l b d)) (1- count))
                         ((memq prop '(r e))   (1+ count))
                         ((eq prop 'c)         count)
                         ((eq prop 's)         (if (= 0 count) 1 count))
                         (t                    0))))
        (goto-char pos)
        (setq prop (and (setq pos (enh-ruby-previous-indent-change pos))
                        (get-text-property pos 'indent))))

      (goto-char (if prop pos (point-min))))))

;; 'l - [, (, {, %w/%i open  or | goalpost open
;; 'r - ], ), }, %w/%i close or | goalpost close
;; 'b - begin, def, case, if
;; 'd - do, {, embexpr (interpolation) start
;; 'e - end, embexpr (interpolation) end, close block }
;; 's - statement start on BACKDENT_KW else/when/rescue etc
;; 'c - continue - period followed by return (or other way around?)

;; backwards: l b d s?
;; forwards r e

;; C-M-a           enh-ruby-beginning-of-defun
;; C-M-p           enh-ruby-beginning-of-block
;; C-M-e           enh-ruby-end-of-defun
;; C-M-n           enh-ruby-end-of-block
;; C-M-q           enh-ruby-indent-exp
;; C-M-h           enh-ruby-mark-defun
;; C-M-u           enh-ruby-up-sexp

(defun enh-ruby-forward-sexp (&optional arg)
  "Move backward across one balanced expression (sexp).
With ARG, do it that many times."
  (interactive "^p")
  (unless arg (setq arg 1))
  (let ((i (or arg 1)))
    (cond
     ((< i 0)
      (enh-ruby-backward-sexp (- i)))
     (t
      (skip-syntax-forward " ")
      (while (> i 0)
        (let* ((pos (point))
               (prop (get-text-property pos 'indent))
               (count 0))

          (unless (memq prop '(l b d))
            (setq prop (and (setq pos (enh-ruby-next-indent-change pos))
                            (get-text-property pos 'indent))))

          (while (< 0 (setq count
                            (cond
                             ((memq prop '(l b d)) (1+ count))
                             ((memq prop '(r e))   (1- count))
                             ((memq prop '(c))     count)
                             ((memq prop '(s))     (if (= 0 count) 1 count))
                             (t                    0))))
            (goto-char pos)
            (setq prop (and (setq pos (enh-ruby-next-indent-change pos))
                            (get-text-property pos 'indent))))

          (goto-char (if prop pos (point-max)))

          (cond ((looking-at "end")     ; move past end/}/]/)
                 (forward-word 1))
                ((looking-at "[]})]")
                 (forward-char 1))))

        (setq i (1- i)))))))

(defun enh-ruby-insert-end ()
  (interactive)
  (let ((text (save-excursion
                (forward-line 0)
                (if (looking-at "^[ \t]*$")
                    "end"
                  (if (looking-at ".*{[^}]*$")
                      "\n}"
                    "\nend")))))
    (insert text)
    (enh-ruby-indent-line)))

(defun enh-ruby-previous-indent-change (pos)
  (and pos
       (setq pos (1- pos))
       (>= pos (point-min))
       (or (and (get-text-property pos 'indent)
                pos)
           (and (> pos (point-min))
                (get-text-property (1- pos) 'indent)
                (1- pos))
           (enh-ruby-previous-indent-change (previous-single-property-change pos 'indent))
           (point-min))))

(defun enh-ruby-next-indent-change (pos)
  (and pos (setq pos (1+ pos))
       (<= pos (point-max))
       (or (and (get-text-property pos 'indent) pos)
           (and (< pos (point-max))
                (get-text-property (1+ pos) 'indent)
                (1+ pos))
           (next-single-property-change pos 'indent))))

(defun enh-ruby-indent-line (&optional _ignored)
  "Correct indentation of the current ruby line."
  (erm-wait-for-parse)
  (unwind-protect
      (progn
        (setq erm-no-parse-needed-p t)
        (enh-ruby-indent-to (enh-ruby-calculate-indent)))
    (setq erm-no-parse-needed-p nil)))

(defun enh-ruby-indent-to (indent)
  "Indent the current line until INDENT is reached."
  (unless (= (current-indentation) indent)
    (save-excursion
      (beginning-of-line)
      (let ((pos (point))
            (prop (get-text-property (point) 'indent)))
        (delete-horizontal-space)
        (indent-to indent)
        (if (eq 'c prop) (put-text-property pos (1+ pos) 'indent 'c)))))

  (if (< (current-column) (current-indentation))
      (back-to-indentation)))

(defun enh-ruby-add-faces (list)
  (let* ((ipos     (car   list))
         (buf-size (car   ipos))
         (istart   (cadr  ipos))
         (iend     (cl-caddr ipos))
         (rpos     (cdr   (cadr list))))

    (unless (and (= (buffer-size) buf-size))
      (throw 'interrupted t))

    (if (or (/= (point-min) istart) (/= (point-max) iend))
        (setq erm-full-parse-p t)

      (when (> iend 0)
        (remove-text-properties istart iend '(indent nil))

        (setq ipos (cl-cdddr ipos))

        (while ipos
          (put-text-property (cadr ipos) (1+ (cadr ipos)) 'indent (car ipos))
          (setq ipos (cddr ipos)))

        (while rpos
          (remove-text-properties (car rpos) (cadr rpos) '(font-lock-face nil))
          (setq rpos (cddr rpos))))

      (while (setq list (cdr list))
        (let ((face (nth (caar list) enh-ruby-font-names))
              (pos (cdar list)))
          (while pos
            (put-text-property (car pos) (cadr pos) 'font-lock-face face)
            (setq pos (cddr pos))))))))

(defun erm-syntax-response (response)
  (save-excursion
    (dolist (ol (overlays-in (point-min) (point-max)))
    (when (and (overlayp ol) (overlay-get ol 'erm-syn-overlay))
        (delete-overlay ol)))
    (goto-char (point-min))
    (let ((warn-count 0)
          (error-count 0)
          (e-w erm-e-w-status)
          (last-line 1))
      (while (string-match ":\\([0-9]+\\): *\\(\\(warning\\)?[^\n]+\\)\n" response)
        (let (beg end ov
                  (line-no (string-to-number (match-string 1 response)))
                  (msg (match-string 2 response))
                  (face (if (string= "warning" (match-string 3 response)) 'erm-syn-warnline 'erm-syn-errline)))
          (setq response (substring response (match-end 0)))
          (forward-line (- line-no last-line))

          (when (or (eq face 'erm-syn-errline) (eq enh-ruby-check-syntax 'errors-and-warnings))
            (if (and (not (eq ?: (string-to-char response)))
                     (string-match "\\`[^\n]*\n\\( *\\)\\^\n" response))
                (progn
                  (setq beg (point))
                  (condition-case nil
                      (forward-char  (length (match-string 1 response)))
                    (error (goto-char (point-max))))
                  (setq end (point))

                  (condition-case nil
                      (progn
                        (backward-sexp)
                        (forward-sexp))

                    (error (back-to-indentation)))
                  (setq beg (if (>= (point) end)
                                (1- end)
                              (if (< (point) beg)
                                  (if (>= beg end) (1- end) beg)
                                (point)))))

              (move-end-of-line nil)
              (skip-chars-backward " \n\t\r\v\f")
              (while (and (> (point) (point-min))
                          (eq 'font-lock-comment-face (get-text-property (point) 'font-lock-face)))
                (backward-char))
              (skip-chars-backward " \n\t\r\v\f")
              (setq end (point))
              (back-to-indentation)
              (setq beg (point)))

            (if (eq face 'erm-syn-warnline)
                (setq warn-count (1+ warn-count))
              (setq error-count (1+ error-count)))

            (setq ov (make-overlay beg end nil t t))
            (overlay-put ov 'font-lock-face face)
            (overlay-put ov 'help-echo      msg)
            (overlay-put ov 'erm-syn-overlay  t)
            (overlay-put ov 'priority (if (eq 'erm-syn-warnline face) 99 100)))

          (setq last-line line-no)))
      (if (eq (+ error-count warn-count) 0)
          (setq e-w nil)
        (setq e-w (format ":%d/%d" error-count warn-count)))
      (when (not (string= e-w erm-e-w-status))
        (setq erm-e-w-status e-w)
        (force-mode-line-update)))))

(defun erm-do-syntax-check ()
  (unless erm-parsing-p
    (let ((buffer (car erm-syntax-check-list)))
      (setq erm-syntax-check-list (cdr erm-syntax-check-list))
      (if (buffer-live-p buffer)
          (with-current-buffer buffer
            (when need-syntax-check-p
              (setq need-syntax-check-p nil)
              (setq erm-parsing-p t)
              (process-send-string (erm-ruby-get-process) (erm-proc-string "c"))))
        (if erm-syntax-check-list
            (erm-do-syntax-check))))))

(defun erm-parse (response)
  (let (interrupted-p
        (cmd (aref response 0))
        (send-next-p (eq 'a erm-parsing-p)))
    (setq erm-parsing-p nil)
    (cond
     ((eq ?\( cmd)
          (setq interrupted-p
                (condition-case nil
                    (catch 'interrupted
                      (if send-next-p
                          (erm-ready)
                        (enh-ruby-add-faces (car (read-from-string response))))
                      nil)
                  (error t)))
          (if interrupted-p
              (setq erm-full-parse-p t)
            (if erm-full-parse-p
                (enh-ruby-fontify-buffer)
              (let ((current (car erm-reparse-list)))
                (when (and current (buffer-live-p current))
                    (setq erm-reparse-list (cdr erm-reparse-list))
                    (with-current-buffer current
                      (enh-ruby-fontify-buffer))
                  (erm-do-syntax-check))))))

     ((eq ?c cmd)
      (unless need-syntax-check-p
        (erm-syntax-response (substring response 1)))
      (erm-do-syntax-check))

     (t
      (setq erm-full-parse-p t)
      (error "%s" (substring response 1))))))

(defun erm--end-p ()
  "Is point directly after a block closing \"end\"."
  (let ((end-pos (- (point) 3)))
    (and (>= end-pos (point-min))
         (string= "end" (buffer-substring end-pos (point)))
         (eq (get-text-property end-pos 'indent) 'e))))

(defun erm-show-paren-data-function ()
  ;; First check if we are on opening ('b or 'd). We only care about
  ;; the word openers "if", "do" etc (normal show-paren handles "{")
  (if (and (memq (get-text-property (point) 'indent) '(b d))
           (looking-at "\\w"))
      (save-excursion
        (let ((opener-beg (point))
              (opener-end (save-excursion (forward-word) (point)))
              (closer-end (progn (enh-ruby-forward-sexp 1) (point))))
          (list
           opener-beg
           opener-end
           (save-excursion (skip-syntax-backward ")w") (point))
           closer-end
           (not (erm--end-p)))))
    ;; Now check if we are at a closer ("end")
    (if (erm--end-p)
        (let ((end-pos (point)))
          (save-excursion
            (enh-ruby-backward-sexp 1)
            (list
             (- end-pos 3)
             end-pos
             (point)
             (save-excursion (skip-syntax-forward "(w") (point))
             (or (not (looking-at "\\w"))
                 (not (memq (get-text-property (point) 'indent) '(b d)))))))
      (show-paren--default))))

;;; Debugging / Bug Reporting:

(defun enh-ruby--all-vars-with (pattern)
  "Return all defcustom variables that match PATTERN.
Used for inserting file-local-variables and sending in bug reports."
  (let (mode-vars)
    (mapatoms (lambda (symbol)
                (when (and (string-match-p pattern (symbol-name symbol))
                           (get symbol 'standard-value))
                  (add-to-list 'mode-vars symbol))))
    (sort mode-vars
          (lambda (a b) (string< (symbol-name a) (symbol-name b))))))

(defun enh-ruby--variable-standard-p (sym)
  (and (equal (custom-variable-state sym (symbol-value sym))
              'standard)
       (equal (symbol-value sym)
              (default-value sym))))

(defun enh-ruby--changed-vars-with (pattern)
  "Return all changed defcustom variables that match PATTERN.
Used for inserting file-local-variables and sending in bug reports."
  (seq-remove
   #'enh-ruby--variable-standard-p
   (enh-ruby--all-vars-with pattern)))

(defun enh-ruby--variable-values (vars)
  "Map VARS to a list of (variable value) pairs."
  (mapcar (lambda (symbol) (list symbol (symbol-value symbol)))
          vars))

(defun enh-ruby--uptime-seconds ()
  "Return the number of seconds that Emacs has been running."
  (float-time (time-subtract (current-time) before-init-time)))

(defun enh-ruby-eval-file-local-variables ()
  "Re-evaluate file-local variables and reindent the file.

Really only useful for my debugging sessions when I'm debugging
stuff by changing vars over and over."
  (interactive)
  (hack-local-variables)
  (indent-region (point-min) (point-max)))

(defun enh-ruby--add-fl-variables (pairs)
  (mapc (lambda (kv) (apply 'add-file-local-variable kv))
        (enh-ruby--variable-values pairs)))

(defun enh-ruby-add-file-local-variables ()
  "Insert all currently customized variables for this mode as
file-local variables. This is mainly for providing a complete
example in a bug report."
  (interactive)
  (enh-ruby--add-fl-variables (enh-ruby--changed-vars-with "enh-ruby")))

(defun enh-ruby-add-all-file-local-variables ()
  "Insert all variables for this mode as file-local variables. This
is mainly for providing a complete example in a bug report."
  (interactive)
  (enh-ruby--add-fl-variables (enh-ruby--all-vars-with "enh-ruby")))

(defun enh-ruby-add-indent-file-local-variables ()
  "Insert all indent variables for this mode as file-local
variables. This is mainly for providing a complete example in a
bug report."
  (interactive)
  (enh-ruby--add-fl-variables (enh-ruby--all-vars-with "enh-ruby.*indent")))

(defun enh-ruby-del-file-local-variables ()
  "Delete all file-local-variables that aren't customized"
  (interactive)
  (mapc #'delete-file-local-variable
        (seq-difference (enh-ruby--all-vars-with "enh-ruby")
                        (enh-ruby--changed-vars-with "enh-ruby"))))

(defun enh-ruby-bug-report ()
  "Fill a buffer with data to make a ‘enh-ruby-mode’ bug report."
  (interactive)
  (with-help-window "*enh-ruby-mode bug report*"
    (princ "Please provide the following output in your bug report:\n")
    (princ "\n")
    (let ((print-quoted t))
      (pp (append `((emacs-uptime ,(enh-ruby--uptime-seconds))
                    (mode-path    ,(find-library-name "enh-ruby-mode")))
                  (enh-ruby--variable-values
                   (append '(emacs-version system-type major-mode)
                           (enh-ruby--changed-vars-with "enh-ruby"))))))
    (princ "\n")
    (princ "Also consider using enh-ruby-add-file-local-variables with any code you provide.\n\n")
    (princ "Hit 'q' to close this buffer.")))

(erm-reset)

(provide 'enh-ruby-mode)

;;; enh-ruby-mode.el ends here


================================================
FILE: ruby/erm.rb
================================================
#!/usr/bin/env ruby

require_relative "erm_buffer"

STDIN.set_encoding "UTF-8"
# STDIN.set_encoding "BINARY"

class BufferStore
  def initialize
    @buffers = {}
  end

  def get_buffer buf_num
    @buffers[buf_num] ||= ErmBuffer.new if buf_num > 0
  end

  def rm buf_num
    @buffers.delete buf_num
  end
end

store = BufferStore.new

EOT = "\n\0\0\0\n"

begin
  while c = STDIN.gets(EOT)
    cmd  = c[0].to_sym
    args = c[1..-6].split ":", 6
    bn   = args.shift.to_i
    buf  = store.get_buffer bn

    case cmd
    when :x then
      (buf || ErmBuffer).set_extra_keywords args.first.split " "
    when :c then
      STDERR.print "c"
      STDERR.puts "#{buf.check_syntax}\n\n\0\0\0"
    when :k then
      store.rm bn
    else
      buf.add_content(cmd, *args) unless cmd == :g

      unless cmd == :a
        STDERR.puts buf.parse
        STDERR.puts "\0\0\0"
      end
    end
  end
rescue
  STDERR.puts "e#{$!.class}: #{$!.message}: #{$!.backtrace.join("\n")}#{EOT}"
end


================================================
FILE: ruby/erm_buffer.rb
================================================
require 'ripper'

class ErmBuffer
  FONT_LOCK_NAMES = {
    rem:             0,  # remove/ignore
    sp:              0,
    ident:           0,
    tstring_content: 1,  # font-lock-string-face
    const:           2,  # font-lock-type-face
    ivar:            3,  # font-lock-variable-name-face
    arglist:         3,
    cvar:            3,
    gvar:            3,
    embexpr_beg:     3,
    embexpr_end:     3,
    comment:         4,  # font-lock-comment-face
    embdoc:          4,
    label:           5,  # font-lock-constant-face
    CHAR:            6,  # font-lock-string-face
    backtick:        7,  # ruby-string-delimiter-face
    __end__:         7,
    embdoc_beg:      7,
    embdoc_end:      7,
    tstring_beg:     7,
    tstring_end:     7,
    words_beg:       7,
    regexp_beg:      8,  # ruby-regexp-delimiter-face
    regexp_end:      8,
    tlambda:         9,  # font-lock-function-name-face
    defname:         9,
    kw:              10, # font-lock-keyword-face
    block:           10,
    heredoc_beg:     11,
    heredoc_end:     11,
    op:              12, # ruby-op-face
    regexp_string:   13, # ruby-regexp-face
  }

  module Adder
    attr_accessor :statement_start
    attr_accessor :ident
    attr_accessor :first_token
    attr_accessor :last_add

    def nadd sym, tok, len = tok.size, ft = false, la = nil
      d :nadd => [sym, tok, len, ft, la]
      case sym
      when :sp, :comment then
        case parser.mode
        when :predef, :expdef then
          # do nothing
        when :def
          parser.mode = :postdef
        else
          parser.mode = nil
        end
      else
        self.statement_start = false

        parser.mode = :def if parser.mode == :predef
        self.block = false if block == :b4args

        case sym
        when :ident, :const, :ivar, :gvar, :cvar then
          self.ident = true
        when :rem_rparen, :indent then
          # leave alone?
        else
          d "self.ident = false"
          self.ident = false
        end
      end

      self.first_token = ft
      self.last_add = la

      target = parser.equal?(self) || lineno != parser.lineno ? self : prev
      target.realadd sym, tok, len

      sym
    end
  end # module Adder

  class Heredoc
    include Adder

    attr_accessor :lineno, :lines, :parser, :prev, :tok
    attr_accessor :block

    def initialize parser, prev, tok, lineno
      # TODO: tok?
      self.parser = parser
      self.prev   = prev
      self.lineno = lineno
      self.lines  = []
      self.block  = nil
    end

    def d o
      parser.d o
    end

    def realadd(*args)
      lines << args
    end

    def restore
      lines << [:heredoc_end, nil, nil] if lines.empty?
      if parser.equal? prev then
        for args in lines
          parser.nadd(*args)
        end
        parser.heredoc = nil
      else
        prev.lines += lines
        parser.heredoc = prev
      end
    end
  end # class Heredoc

  class Parser < ::Ripper   #:nodoc: internal use only
    include Adder

    # TODO: add prev_line and copy it when we clear line_so_far
    # TODO: use this to calculate hanging indent for parenless args

    # Indents:
    #
    # l - [, (, {, %w/%i open  or | goalpost open
    # r - ], ), }, %w/%i close or | goalpost close
    # b - begin/def/case/if
    # e - end / embexpr (interpolation) end / close block }
    # d - do / {
    # s - statement start on BACKDENT_KW else/when/rescue etc
    # c - continue - period followed by return (or other way around?)

    INDENT_KW          = [:begin, :def, :case, :module, :class, :do, :for]
    BACKDENT_KW        = [:elsif, :else, :when, :in, :rescue, :ensure]
    BEGINDENT_KW       = [:if, :unless, :while, :until]
    POSTCOND_KW        = [:if, :unless, :or, :and]
    PRE_OPTIONAL_DO_KW = [:in, :while, :until]
    DELIM_MAP          = { "(" => ")", "[" => "]", "{" => "}" }

    ESCAPE_LINE_END    = "\\\n"

    attr_accessor :heredoc, :mode
    attr_accessor :indent_stack, :ident_stack, :brace_stack
    attr_accessor :parser
    attr_accessor :file_encoding
    attr_accessor :ermbuffer
    attr_accessor :point_min
    attr_accessor :point_max
    attr_accessor :src
    attr_accessor :src_size
    attr_accessor :first_count
    attr_accessor :count
    attr_accessor :res
    attr_accessor :block
    attr_accessor :list_count
    attr_accessor :cond_stack
    attr_accessor :plit_stack
    attr_accessor :line_so_far

    def initialize ermbuffer, src, point_min, point_max, first_count
      self.ermbuffer     = ermbuffer
      self.point_min     = point_min
      self.point_max     = point_max
      self.src           = src
      self.src_size      = src.size
      self.file_encoding = src.encoding
      self.first_count   = nil
      self.parser        = self # stupid hack for Adder module above
      super src
    end

    def add(*args)
      (heredoc || self).nadd(*args)
    end

    def indent type, c = 0
      add :indent, type, c
    end

    # Bugs in Ripper:
    # empty here doc fails to fire on_heredoc_end
    def parse
      self.count          = 1
      self.mode           = nil
      self.brace_stack    = []
      self.heredoc        = nil
      self.first_token    = true
      self.last_add       = nil
      self.res            = []
      self.ident          = false
      self.ident_stack    = []
      self.block          = false
      self.statement_start = true
      self.indent_stack   = []
      self.list_count     = 0
      self.cond_stack     = []
      self.plit_stack     = []
      self.line_so_far    = []

      catch :parse_complete do
        super

        realadd :rem_heredoc, '', src_size-count if heredoc
      end

      self.res = res.map.with_index { |v, i|
        "(%d %s)" % [i, v.join(" ")] if v
      }

      "((%s %s %s %s)%s)" % [src_size,
                             point_min,
                             point_max,
                             indent_stack.join(' '),
                             res.join]
    end

    def realadd sym, tok, len
      if sym == :indent
        pos = count + len
        indent_stack << tok << pos if pos.between? point_min, point_max

        return
      end

      start = count
      throw :parse_complete if start > point_max

      len = 2 + src.index("\n", start) - start unless len

      pos = self.count += len
      return if pos < point_min
      start = point_min if start < point_min
      pos = point_max   if pos   > point_max

      sym = :rem if sym =~ /^rem_/
      idx = FONT_LOCK_NAMES[sym]

      if t = res[idx] then
        if t.last == start then
          t[-1] = pos
        else
          t << start << pos
        end
      else
        res[idx] = [start, pos]
      end

      if (sym == :sp && tok == "\n") || (sym == :comment && tok.end_with?("\n"))
        line_so_far.clear
      else
        line_so_far << [sym, tok, len]
      end

      throw :parse_complete if pos == point_max
    end

    def maybe_plit_ending tok
      if tok[-1] == plit_stack.last
        plit_stack.pop

        # Token can sometimes have preceding whitespace, which needs to be added
        # as a separate token to work with indents.
        if tok.length > 1
          add :rem_end_ws, tok[0..-2]
        end
        indent :r
        add :tstring_end, tok[-1]
      end
    end

    ############################################################
    # on_* handlers
    # TODO: I don't like these generated methods. Harder to trace/debug.

    def d o
      ermbuffer.d o
    end

    def debug_on tok
      return unless ermbuffer.debug
      loc  = caller_locations.first.label.to_sym
      rest = line_so_far.map {|a| a[1] }.join
      d "%-10s %p %p %p" % [loc, tok, ident, rest]
    end

    [:CHAR, :__end__, :backtick, :embdoc, :embdoc_beg, :embdoc_end,
     :label, :tlambda, :tstring_beg].each do |event|
      define_method "on_#{event}" do |tok|
        tok.force_encoding file_encoding if tok.encoding != file_encoding
        debug_on tok
        add event, tok
      end
    end

    [:backref, :float, :int].each do |event|
      define_method "on_#{event}" do |tok|
        debug_on tok
        add :"rem_#{event}", tok
      end
    end

    [:cvar, :gvar, :ivar].each do |event|
      define_method "on_#{event}" do |tok|
        if mode == :sym then
          debug_on [tok, :sym]
          add :label, tok
        else
          debug_on [tok, :not_sym]
          add event, tok
        end
      end
    end

    def on_comma tok
      debug_on tok
      self.mode = nil
      r = add :rem_comma, tok, tok.size, false, list_count <= 0
      self.statement_start = true
      r
    end

    def on_comment tok
      debug_on tok
      on_eol :comment, tok
    end

    def on_const tok
      debug_on [tok, mode]
      case mode
      when :sym then
        self.mode = nil
        add :label, tok
      when :def, :predef then
        r = add :const, tok
        self.mode = :predef
        r
      else
        add :const, tok
      end
    end

    def on_embexpr_beg tok
      debug_on tok
      len = tok.size
      if len > 2 then
        add :tstring_content, tok, len - 2
        len = 2
      end

      brace_stack << :embexpr
      cond_stack << false
      plit_stack << false

      indent :d, 1
      add :embexpr_beg, tok, len
    end

    def on_embexpr_end tok
      debug_on tok
      brace_stack.pop
      cond_stack.pop
      plit_stack.pop
      indent :e
      add :embexpr_beg, tok
    end

    def on_embvar tok
      debug_on tok
      len = tok.size
      if len > 1 then
        add :tstring_content, tok, len - 1
        len = 1
      end

      add :ivar, tok, len
    end

    def on_eol sym, tok
      debug_on tok
      indent :c, tok.size if last_add

      r = add sym, tok, tok.size, true

      if heredoc && heredoc.lineno == lineno then
        heredoc.restore
      end

      cond_stack.pop if cond_stack.last
      self.statement_start = true

      r
    end

    def on_heredoc_beg tok
      debug_on tok
      r = add :heredoc_beg, tok
      if !heredoc || heredoc.lineno < lineno then
        self.heredoc = Heredoc.new self, heredoc||self, tok, lineno
      end
      r
    end

    def on_heredoc_end tok
      debug_on tok
      add :heredoc_end, tok
    end

    def on_ident tok
      debug_on [tok, mode]
      case mode
      when :sym then
        add :label, tok
      when :predef, :def then
        add :defname, tok
      when :period then
        add :ident, tok
      else
        if ermbuffer.extra_keywords.include? tok then
          add :kw, tok
        else
          add :ident, tok
        end
      end
    end

    def on_ignored_nl tok
      debug_on tok
      if tok then
        on_nl tok
      end
    end

    def on_kw sym # TODO: break up. 61 lines long
      sym = sym.to_sym
      debug_on [sym, mode]
      case mode
      when :sym then
        add :label, sym
      when :def, :predef then
        if sym != :self then
          add :defname, sym
        else
          r = add :kw, sym
          self.mode = :def
          r
        end
      else
        last_add = nil

        case sym
        when :end then
          indent :e
        when :do then
          if cond_stack.last then
            cond_stack.pop
            r = add :kw, sym
          else
            # `indent` precedes `add` for the compatibility of parsing result.
            #
            # `add` and `indent` must precede `self.block = :b4args`.
            # Otherwise block is overwritten, and indentation is broken
            # in the following code:
            #  each do |a| # <- `|` for argument list is recognized as operator,
            #      # <- which produces an extra indentation.
            #  end

            indent :d
            r = add :kw, sym
            self.block = :b4args
          end

          return r
        when *BEGINDENT_KW then
          if statement_start then
            indent :b
          elsif POSTCOND_KW.include? sym then
            last_add = :cont
          end
        when *POSTCOND_KW then
          last_add = :cont
        when *INDENT_KW then
          indent :b
        when *BACKDENT_KW then
          indent :s if statement_start
        end

        cond_stack << true if PRE_OPTIONAL_DO_KW.include? sym

        r = add :kw, sym, sym.size, false, last_add
        self.mode = :predef if [:def, :alias].include? sym
        r
      end
    end

    def on_lbrace tok
      cond_stack << false
      ident_stack << [ident, mode]

      is_start_of_line = line_so_far.all? {|a| a[0] == :sp }
      if ident && !is_start_of_line then
        brace_stack << :block
        indent :d
        r = add :block, tok
        self.block = :b4args
        r
      else
        brace_stack << :brace
        self.list_count += 1
        indent :l
        add :rem_lbrace, tok
      end
    end

    def on_lparen tok
      debug_on tok
      newmode = case mode
                when :def then
                  self.mode = nil
                  :def
                when :predef then
                  self.mode = :expdef
                  :predef
                else
                  mode
                end

      ident_stack << [ident, newmode]
      cond_stack << false
      indent :l
      self.list_count += 1
      r = add :rem_lparen, tok
      self.statement_start = true
      r
    end

    def on_nl tok
      on_eol :sp, tok
    end

    def on_op tok
      if mode == :sym then
         add :label, tok
      else
        r = if block && tok == '|'
            case block
            when :b4args then
              indent :l
              self.list_count += 1
              self.block = :arglist
            else
              indent :r
              self.list_count -= 1
              self.block = false
            end
            add :arglist, tok, 1
          else
            case mode
            when :def, :predef then
              add :ident, tok
            else
              if mode == :postdef && tok == '='
                indent :e
              end
              add :op, tok, tok.size, false, :cont
            end
          end
        self.statement_start = true
        r
      end
    end

    def on_period tok
      self.mode ||= :period

      debug_on tok

      indent :c, tok.size if tok == "\n"
      d :ident => ident
      d :lsf => line_so_far

      line_so_far_str = line_so_far.map {|a| a[1] }.join
      if line_so_far_str.strip == ""
        indent :c, -line_so_far_str.length
      end

      add :rem_period, tok, tok.size, false, :cont
    end

    def on_rbrace tok
      debug_on tok
      cond_stack.pop
      type = case brace_stack.pop
             when :embexpr then
               indent :e
               if plit_stack.last == false
                 plit_stack.pop
               end
               :embexpr_beg
             when :block then
               indent :e
               :block
             when :brace then
               indent :r
               self.list_count -= 1
               :rem_brace
             else
               :rem_other
             end

      add(type, tok).tap do
        self.ident, self.mode = ident_stack.pop
      end
    end

    def on_regexp_beg tok
      tok.force_encoding file_encoding if tok.encoding != file_encoding
      self.mode = :regexp
      debug_on tok
      add :regexp_beg, tok
    end

    def on_regexp_end tok
      self.mode = nil
      debug_on tok
      add :regexp_end, tok
    end

    def on_rparen tok
      debug_on tok
      indent :r
      r = add :rem_rparen, tok

      self.list_count -= 1
      self.ident, self.mode = ident_stack.pop
      cond_stack.pop

      r
    end

    def on_semicolon tok
      debug_on tok
      r = add :kw, :semicolon, 1, true
      cond_stack.pop if cond_stack.last
      self.statement_start = true
      r
    end

    def on_sp tok
      debug_on tok
      if tok == ESCAPE_LINE_END then
        indent :c, 2
      end
      add :sp, tok, tok.size, first_token, last_add
    end

    def on_symbeg tok
      debug_on tok
      r = add :label, tok
      self.mode = :sym
      r
    end

    def on_tlambeg tok
      debug_on tok
      brace_stack << :block
      indent :d
      add :block, tok
    end

    def on_tstring_content tok
      debug_on tok
      tok.force_encoding file_encoding if tok.encoding != file_encoding
      if mode == :regexp
        add :regexp_string, tok
      elsif plit_stack.last # `tstring_content` is ignored by indent in emacs.
        add :rem_tstring_content, tok # TODO: figure out this context? or collapse?
      else
        add :tstring_content, tok
      end
    end

    def on_tstring_end tok
      debug_on [tok, mode]

      return if maybe_plit_ending(tok)

      if mode == :sym then
        add :label, tok
      else
        add :tstring_end, tok
      end
    end

    def on_label_end tok
      debug_on tok
      add :tstring_beg, tok[0]
      add :label, tok[1]
    end

    def on_words_beg tok
      debug_on tok
      delimiter = tok.strip[-1]  # ie. "%w(\n" => "("
      plit_stack << (DELIM_MAP[delimiter] || delimiter)

      indent :l
      add :words_beg, tok
    end

    def on_words_sep tok
      debug_on tok
      return if maybe_plit_ending(tok)

      add :rem_words_sep, tok
    end

    alias on_lbracket     on_lparen
    alias on_qsymbols_beg on_words_beg
    alias on_qwords_beg   on_words_beg
    alias on_rbracket     on_rparen
    alias on_symbols_beg  on_words_beg
  end # class Parser

  @@extra_keywords = {}

  attr_writer :extra_keywords
  attr_accessor :point_min
  attr_accessor :point_max
  attr_accessor :first_count
  attr_accessor :buffer
  attr_accessor :debug

  def initialize
    self.extra_keywords = nil
    self.first_count    = nil
    self.buffer         = ''
    self.debug          = false
  end

  def d o
    return unless debug
    require "pp"
    o = o.pretty_inspect unless String === o
    puts o.gsub(/^/, "     # ")
  end

  def add_content cmd, point_min, point_max, pbeg, len, content
    self.point_min = point_min.to_i
    self.point_max = point_max.to_i

    pbeg = pbeg.to_i

    self.first_count = pbeg if !first_count || pbeg < first_count

    if cmd == :r || buffer.empty? then
      self.buffer = content
    else
      len = pbeg + len.to_i - 2
      if pbeg == 1 && len < 0 then
        buffer[0..0] = content << buffer[0]
      else
        buffer[pbeg - 1..len] = content
      end
    end
  end

  # verify that this is used in erm.rb. I don't know how to trigger it. & args??
  def check_syntax fname = '', code = buffer
    $VERBOSE = true
    # eval but do not run code
    eval "BEGIN{return}\n#{code}", nil, fname, 0
  rescue SyntaxError
    $!.message
  rescue
    # do nothing
  ensure
    $VERBOSE = nil
  end

  def parse
    parser = ErmBuffer::Parser.new(self, buffer,
                                   point_min, point_max,
                                   first_count||0)
    self.first_count = nil
    parser.parse
  end

  def self.set_extra_keywords keywords
    @@extra_keywords = Hash[keywords.map { |o| [o, true] }]
  end

  def set_extra_keywords keywords
    self.extra_keywords = Hash[keywords.map { |o| [o, true] }]
  end

  def extra_keywords
    @extra_keywords || @@extra_keywords
  end
end # class ErmBuffer


================================================
FILE: test/enh-ruby-mode-test.el
================================================
(eval-and-compile
  (add-to-list 'load-path default-directory)
  (load "./helper" nil t))

(defun erm-run-current-test ()
  (interactive)
  (require 'ert)
  (setq enh-tests nil)
  (ert-delete-all-tests)
  (load-file "../enh-ruby-mode.el")
  (eval-buffer)
  (let ((ert-debug-on-error t))
    (ert-run-tests-interactively (lisp-current-defun-name))))

(defun erm-run-all-tests ()
  (interactive)
  (require 'ert)
  (setq enh-tests nil)
  (ert-delete-all-tests)
  (load-file "../enh-ruby-mode.el")
  (eval-buffer)
  (ert-run-tests-interactively t))

(local-set-key (kbd "C-c C-r") #'erm-run-all-tests)
(local-set-key (kbd "C-c M-r") #'erm-run-current-test)

;; In batch mode, face-attribute returns 'unspecified,
;; and it causes wrong-number-of-arguments errors.
;; This is a workaround for it.
(defun erm-darken-color (name)
  (let ((attr (face-attribute name :foreground)))
    (unless (equal attr 'unspecified)
      (color-darken-name attr 20)
      "#000000")))

(enh-deftest enh-ruby-backward-sexp-test ()
  (with-temp-enh-rb-string
   "def foo\n  xxx\nend\n"

   (goto-char (point-max))
   (enh-ruby-backward-sexp 1)
   (line-should-equal "def foo")))

(enh-deftest enh-ruby-backward-sexp-test--ruby ()
  (with-temp-ruby-string
   "def foo\n  xxx\nend\n"

   (goto-char (point-max))
   (enh-ruby-backward-sexp 1)
   (rest-of-line-should-equal "def foo")))

(enh-deftest enh-ruby-backward-sexp-test-inner--erm ()
  :expected-result :failed
  (with-temp-enh-rb-string
   "def backward_sexp\n  \"string #{expr \"another\"} word\"\nend\n"

   ;; DESIRED:
   ;; def backward_sexp\n  \"string #{expr \"another\"} word\"\nend\n
   ;;                                                      ^ HERE
   ;;                                                   ^ to here
   ;;                                ^ to here
   ;;                       ^ to here
   ;; ^ to here

   (search-forward " word")
   (rest-of-line-should-equal "\"")

   ;; DESIRED:
   (enh-ruby-backward-sexp)
   (rest-of-line-should-equal "word\"")
   ;; (enh-ruby-backward-sexp)
   ;; (rest-of-line-should-equal "#{expr \"another\"} word\"")
   ;; (enh-ruby-backward-sexp)
   ;; (rest-of-line-should-equal "\"string #{expr \"another\"} word\"")
   ;; (enh-ruby-backward-sexp)
   ;; (rest-of-line-should-equal "def backward_sexp")

   ;; CURRENT:
   ;; def backward_sexp\n  \"string #{expr \"another\"} word\"\nend\n
   ;;                                                      ^ HERE
   ;;                                ^ to here
   ;; ^ to here

   ;; CURRENT:

   (enh-ruby-backward-sexp)
   (rest-of-line-should-equal "{expr \"another\"} word\"")

   (enh-ruby-backward-sexp)
   (rest-of-line-should-equal "def backward_sexp")
   ))

(enh-deftest enh-ruby-backward-sexp-test-inner--ruby ()
  :expected-result :failed
  (with-temp-ruby-string
   "def backward_sexp\n  \"string #{expr \"another\"} word\"\nend\n"
   ;;                                                      ^ here
   ;;                     ^ to here
   ;;^ NOT HERE
   (search-forward " word")
   ;; (move-end-of-line nil)

   (rest-of-line-should-equal "\"")

   (enh-ruby-backward-sexp)
   (rest-of-line-should-equal "word\"")

   (enh-ruby-backward-sexp)
   (rest-of-line-should-equal "{expr \"another\"} word\"")

   (enh-ruby-backward-sexp)
   (rest-of-line-should-equal "string #{expr \"another\"} word\"")

   ;; this blows out: (scan-error "Containing expression ends prematurely" 21 21)
   ;; (enh-ruby-backward-sexp)
   ;; (rest-of-line-should-equal "\"string #{expr \"another\"} word\"")
   ))

(enh-deftest enh-ruby-forward-sexp-test ()
  (with-temp-enh-rb-string
   "def foo\n  xxx\n end\n\ndef backward_sexp\n  xxx\nend\n"

   (enh-ruby-forward-sexp 1)
   (forward-char 2)
   (rest-of-line-should-equal "def backward_sexp")))

(enh-deftest enh-ruby-up-sexp-test ()
  (with-temp-enh-rb-string
   "def foo\n  %_bosexp#{sdffd} test1_[1..4].si\nend"

   (search-forward "test1_")
   (enh-ruby-up-sexp)
   (line-should-equal "def foo")))      ; maybe this should be %_bosexp?

(enh-deftest enh-ruby-end-of-defun ()
  (with-temp-enh-rb-string
   "class Class\ndef method\n# blah\nend # method\nend # class"

   (search-forward "blah")
   (enh-ruby-end-of-defun)
   (rest-of-line-should-equal " # method")))

(enh-deftest enh-ruby-end-of-block ()
  (with-temp-enh-rb-string
   "class Class\ndef method\n# blah\nend # method\nend # class"

   (search-forward "blah")
   (enh-ruby-end-of-block)
   (rest-of-line-should-equal " # method")))

;;; indent-region

(enh-deftest enh-ruby-indent-array-of-strings ()
  (with-deep-indent nil
    (string-should-indent "words = [\n'moo'\n]\n"
                          "words = [\n  'moo'\n]\n")))

(enh-deftest enh-ruby-indent-array-of-strings-incl-first ()
  (with-deep-indent nil
    (string-should-indent "words = ['cow',\n'moo'\n]\n"
                          "words = ['cow',\n  'moo'\n]\n")))

(enh-deftest enh-ruby-indent-array-of-strings/deep ()
  (with-deep-indent t
    (string-should-indent "words = ['cow',\n'moo'\n]\n"
                          "words = ['cow',\n         'moo'\n        ]\n")))

(enh-deftest enh-ruby-indent-array-of-strings-incl-first/deep ()
  (with-deep-indent t
    (string-should-indent "words = ['cow',\n'moo'\n]\n"
                          "words = ['cow',\n         'moo'\n        ]\n")))

(enh-deftest enh-ruby-indent-array-of-strings/ruby ()
  (string-should-indent-like-ruby "words = [\n'moo'\n]\n"))

(enh-deftest enh-ruby-indent-array-of-strings-incl-first/ruby ()
  (string-should-indent-like-ruby "words = ['cow',\n'moo'\n]\n"
                                  'deep))

(enh-deftest enh-ruby-indent-not-method ()
  (string-should-indent-like-ruby
   "\nclass Object\ndef !\n100\nend\nend"))

(enh-deftest enh-ruby-indent-hanging-period-after-parens ()
  (string-should-indent-like-ruby
   ":a\n(b)\n.c"))

(enh-deftest enh-ruby-indent-hanging-period ()
  (string-should-indent-like-ruby
   ":a\nb\n.c"))

(enh-deftest enh-ruby-indent-def-endless ()
  (with-deep-indent nil
   (string-should-indent "class Foo\ndef foo = z\ndef bar = y\nend\n"
                         "class Foo\n  def foo = z\n  def bar = y\nend\n")))

(enh-deftest enh-ruby-indent-def-endless/params ()
  (with-deep-indent nil
   (string-should-indent "class Foo\ndef foo(a) = z\ndef bar = y\nend\n"
                         "class Foo\n  def foo(a) = z\n  def bar = y\nend\n")))

(enh-deftest enh-ruby-indent-def-endless/args-forward ()
  (with-deep-indent nil
   (string-should-indent "class Foo\ndef foo(...) = z\ndef bar = y\nend\n"
                         "class Foo\n  def foo(...) = z\n  def bar = y\nend\n")))

(enh-deftest enh-ruby-indent-def-after-private ()
  (with-deep-indent nil
   (string-should-indent "class Foo\nprivate def foo\nx\nend\nend\n"
                         "class Foo\n  private def foo\n    x\n  end\nend\n")))

(enh-deftest enh-ruby-indent-def-after-private/deep ()
  (with-deep-indent t
   (string-should-indent "class Foo\nprivate def foo\nx\nend\nend\n"
                         "class Foo\n  private def foo\n            x\n          end\nend\n")))

(enh-deftest enh-ruby-indent-hash ()
  ;; https://github.com/zenspider/enhanced-ruby-mode/issues/78
  (with-deep-indent nil
    (string-should-indent "c = {\na: a,\nb: b\n}\n"
                          "c = {\n  a: a,\n  b: b\n}\n")))

(defconst input/indent-hash/trail "\nc = {a: a,\nb: b,\n c: c\n}")
(defconst input/indent-hash/hang  "\nc = {\na: a,\nb: b,\n c: c\n}")
(defconst exp/indent-hash/hang    "\nc = {\n  a: a,\n  b: b,\n  c: c\n}")
(defconst exp/indent-hash/trail   "\nc = {a: a,\n     b: b,\n     c: c\n    }")
(defconst exp/indent-hash/trail/8 "\nc = {a: a,\n             b: b,\n             c: c\n    }")

(defmacro with-bounce-and-hang (bounce indent1 indent2 &rest body)
  `(let ((enh-ruby-bounce-deep-indent              ,bounce)
         (enh-ruby-deep-indent-paren               t)
         (enh-ruby-hanging-brace-indent-level      (or ,indent1
                                                       enh-ruby-hanging-brace-indent-level))
         (enh-ruby-hanging-brace-deep-indent-level (or ,indent2
                                                       enh-ruby-hanging-brace-deep-indent-level)))
     ,@body))
(put 'with-bounce-and-hang 'lisp-indent-function 'defun)

(enh-deftest enh-ruby-indent-hash/deep/hang/def ()
  (with-deep-indent t
    (with-bounce-and-hang nil nil nil
      (string-should-indent input/indent-hash/hang
                            exp/indent-hash/hang))))

(enh-deftest enh-ruby-indent-hash/deep/bounce/hang/def ()
  (with-deep-indent t
    (with-bounce-and-hang t nil nil
      (string-should-indent input/indent-hash/hang
                            "\nc = {\n     a: a,\n     b: b,\n     c: c\n    }"))))

;; if bounce off, hanging-brace-deep-indent-level doesn't matter
(enh-deftest enh-ruby-indent-hash/deep/hang/99 ()
  (with-deep-indent t
    (with-bounce-and-hang nil nil 99
      (string-should-indent input/indent-hash/hang
                            exp/indent-hash/hang))))

;; 3 < 4, so close brace is at 1
(enh-deftest enh-ruby-indent-hash/deep/hang/bil-3 ()
  (with-deep-indent t
    (with-bounce-and-hang nil 3 nil
      (string-should-indent input/indent-hash/hang
                            "\nc = {\n   a: a,\n   b: b,\n   c: c\n}"))))

;; 8 > 4, so close brace is at 4
(enh-deftest enh-ruby-indent-hash/deep/hang/bil-8 ()
  (with-deep-indent t
    (with-bounce-and-hang nil 8 nil
      (string-should-indent input/indent-hash/hang
                            "\nc = {\n        a: a,\n        b: b,\n        c: c\n    }"))))

(enh-deftest enh-ruby-indent-hash/deep/trail/def ()
  (with-deep-indent t
    (with-bounce-and-hang nil nil nil
      (string-should-indent input/indent-hash/trail
                            exp/indent-hash/trail))))

(enh-deftest enh-ruby-indent-hash/deep/bounce/trail/def ()
  (with-deep-indent t
    (with-bounce-and-hang t nil nil
      (string-should-indent input/indent-hash/trail
                            exp/indent-hash/trail))))

(enh-deftest enh-ruby-indent-hash/deep/trail/3 ()
  (with-deep-indent t
    (with-bounce-and-hang nil 3 8
      (string-should-indent input/indent-hash/trail
                            exp/indent-hash/trail/8))))

(enh-deftest enh-ruby-indent-hash/deep/bounce/trail/3 ()
  (with-deep-indent t
    (with-bounce-and-hang t 3 8
      (string-should-indent input/indent-hash/trail
                            exp/indent-hash/trail/8))))

(enh-deftest enh-ruby-indent-hash/deep/trail/8 ()
  (with-deep-indent t
    (with-bounce-and-hang nil 8 8
      (string-should-indent input/indent-hash/trail
                            exp/indent-hash/trail/8))))

(enh-deftest enh-ruby-indent-hash/deep/bounce/trail/8 ()
  (with-deep-indent t
    (with-bounce-and-hang t 8 8
      (string-should-indent input/indent-hash/trail
                            exp/indent-hash/trail/8))))

(enh-deftest enh-ruby-indent-bug/90/a ()
  (string-should-indent-like-ruby "aa.bb(:a => 1,\n      :b => 2,\n      :c => 3)\n"
                                  'deep))

(enh-deftest enh-ruby-indent-bug/90/b ()
  (string-should-indent-like-ruby "literal_array = [\n  :a,\n  :b,\n  :c\n]\n"))

(enh-deftest enh-ruby-indent-hash-after-cmd ()
  (with-deep-indent nil
    (string-should-indent "x\n{\na: a,\nb: b\n}"
                          "x\n{\n  a: a,\n  b: b\n}")))

(enh-deftest enh-ruby-indent-hash-after-cmd/deep ()
  (with-deep-indent t
    (string-should-indent "x\n{\na: a,\nb: b\n}"
                          "x\n{\n  a: a,\n  b: b\n}")))

(enh-deftest enh-ruby-indent-hash-after-cmd/ruby ()
  (string-should-indent-like-ruby "x\n{\na: a,\nb: b\n}"))

(enh-deftest enh-ruby-indent-if-in-assignment ()
  (with-deep-indent nil
    (string-should-indent "foo = if bar\nx\nelse\ny\nend\n"
                          "foo = if bar\n  x\nelse\n  y\nend\n")))

(enh-deftest enh-ruby-indent-if-in-assignment/deep ()
  (with-deep-indent t
    (string-should-indent "foo = if bar\nx\nelse\ny\nend\n"
                          "foo = if bar\n        x\n      else\n        y\n      end\n")))

(enh-deftest enh-ruby-indent-leading-dots ()
  (string-should-indent "d.e\n.f\n"
                        "d.e\n  .f\n"))

(enh-deftest enh-ruby-indent-leading-dots-cvar ()
  (string-should-indent "@@b\n.c\n.d\n"
                        "@@b\n  .c\n  .d\n"))

(enh-deftest enh-ruby-indent-leading-dots-cvar/ruby ()
  (string-should-indent-like-ruby "@@b\n.c\n.d\n"))

(enh-deftest enh-ruby-indent-leading-dots-gvar ()
  (string-should-indent "$b\n.c\n.d\n"
                        "$b\n  .c\n  .d\n"))

(enh-deftest enh-ruby-indent-leading-dots-gvar/ruby ()
  (string-should-indent-like-ruby "$b\n.c\n.d\n"))

(enh-deftest enh-ruby-indent-leading-dots-ident ()
  (string-should-indent "b\n.c\n.d\n"
                        "b\n  .c\n  .d\n"))

(enh-deftest enh-ruby-indent-leading-dots-ident/ruby ()
  (string-should-indent-like-ruby "b\n.c\n.d\n"))

(enh-deftest enh-ruby-indent-leading-dots-ivar ()
  (string-should-indent "@b\n.c\n.d\n"
                        "@b\n  .c\n  .d\n"))

(enh-deftest enh-ruby-indent-leading-dots-ivar/ruby ()
  (string-should-indent-like-ruby "@b\n.c\n.d\n"))

(enh-deftest enh-ruby-indent-leading-dots-with-block ()
  (string-should-indent "a\n.b {}\n.c\n"
                        "a\n  .b {}\n  .c\n"))

(enh-deftest enh-ruby-indent-leading-dots-with-block/ruby ()
  (string-should-indent-like-ruby "a\n.b {}\n.c\n"))

(enh-deftest enh-ruby-indent-leading-dots-with-comment ()
  (string-should-indent "a\n.b # comment\n.c\n"
                        "a\n  .b # comment\n  .c\n"))

(enh-deftest enh-ruby-indent-leading-dots-with-comment/ruby ()
  (string-should-indent-like-ruby "a\n.b # comment\n.c\n"))

(enh-deftest enh-ruby-indent-leading-dots/ruby ()
  (string-should-indent-like-ruby "d.e\n.f\n"))

(defconst leading-dot-input  "\na\n.b\n.c(\nd,\ne\n)\n.f\n")
(defconst trailing-dot-input "\na.\nb.\nc(\nd,\ne\n).\nf\n")

(enh-deftest enh-ruby-indent-leading-dots-with-arguments-and-newlines ()
  (string-should-indent leading-dot-input
                        "\na\n  .b\n  .c(\n    d,\n    e\n  )\n  .f\n"))

(enh-deftest enh-ruby-indent-leading-dots-with-arguments-and-newlines/bounce ()
  (with-bounce-and-hang t nil nil
    (string-should-indent leading-dot-input
                          "\na\n  .b\n  .c(\n     d,\n     e\n    )\n  .f\n")))

(enh-deftest enh-ruby-indent-leading-dots-with-arguments-and-newlines/ruby ()
  (string-should-indent-like-ruby leading-dot-input))

(enh-deftest enh-ruby-indent-trailing-dots-with-arguments-and-newlines ()
  (string-should-indent trailing-dot-input
                        "\na.\n  b.\n  c(\n    d,\n    e\n  ).\n  f\n"))

(enh-deftest enh-ruby-indent-trailing-dots-with-arguments-and-newlines/bounce ()
  (with-bounce-and-hang t nil nil
    (string-should-indent trailing-dot-input
                          "\na.\n  b.\n  c(\n    d,\n    e\n   ).\n  f\n")))

(enh-deftest enh-ruby-indent-trailing-dots-with-arguments-and-newlines/ruby ()
  (string-should-indent-like-ruby trailing-dot-input))

(enh-deftest enh-ruby-add-log-current-method/nested-modules ()
  :expected-result :failed
  (with-temp-enh-rb-string
   "module One\nmodule Two\nclass Class\ndef method\n# blah\nend # method\nend # class\nend # One\nend # Two"
   (search-forward "blah")
   (should (equal "One::Two::Class#method" (enh-ruby-add-log-current-method)))))

(enh-deftest enh-ruby-add-log-current-method/compact-modules ()
  (with-temp-enh-rb-string
   "class One::Two::Class\ndef method\n# blah\nend # method\nend # class"
   (search-forward "blah")
   (should (equal "One::Two::Class#method" (enh-ruby-add-log-current-method)))))

(enh-deftest enh-ruby-indent-continued-assignment ()
  (string-should-indent "\na =\nb.map do |c|\nd(c)\nend\n"
                        "\na =\n  b.map do |c|\n    d(c)\n  end\n"))

(enh-deftest enh-ruby-indent-leading-dots-with-block-and-newlines ()
  (string-should-indent "\na\n.b do\nc\nend\n.d\n\ne"
                        "\na\n  .b do\n    c\n  end\n  .d\n\ne"))

(enh-deftest enh-ruby-indent-leading-dots-with-brackets-and-newlines ()
  (string-should-indent "\na\n.b {\nc\n}\n.d\n\ne"
                        "\na\n  .b {\n    c\n  }\n  .d\n\ne"))

(enh-deftest enh-ruby-indent-not-on-eol-opening/deep ()
  (with-deep-indent t
   (string-should-indent "\nfoo(:bar,\n:baz)\nfoo(\n:bar,\n:baz,\n)\n[:foo,\n:bar]\n[\n:foo,\n:bar\n]"
                         "\nfoo(:bar,\n    :baz)\nfoo(\n  :bar,\n  :baz,\n)\n[:foo,\n :bar]\n[\n  :foo,\n  :bar\n]")))

(enh-deftest enh-ruby-indent-pct-w-array ()
  (with-deep-indent nil
    (string-should-indent "words = %w[\na\nb\n]\n"
                          "words = %w[\n  a\n  b\n]\n")))

(enh-deftest enh-ruby-indent-pct-w-array/deep ()
  (with-deep-indent t
    (with-bounce-and-hang nil nil nil
     (string-should-indent "\nwords = %w[a\nb\nc\n]\n"
                           "\nwords = %w[a\n           b\n           c\n          ]\n"))))

;; NO! ruby-mode refuses to indent %w at all
;; words = %w[ a
;; b
;; c
;; ]
;;
;; vs enh-ruby-mode:
;;
;; words = %w[ a
;;   b
;;   c
;; ]
;;
;; and w/ deep-indent-paren t:
;; words = %w[ a
;;             b
;;             c
;;           ]
;;
;; (enh-deftest enh-ruby-indent-pct-w-array/ruby ()
;;   (string-should-indent-like-ruby "words = %w[ a\nb\nc\n]\n"))

(enh-deftest enh-ruby-indent-trailing-dots ()
  (string-should-indent "a.b.\nc\n"
                        "a.b.\n  c\n"))

(enh-deftest enh-ruby-indent-trailing-dots/ruby ()
  (string-should-indent-like-ruby "a.b.\nc\n"))

;;; indent-for-tab-command -- seems different than indent-region in some places

(enh-deftest enh-ruby-beginning-of-block ()
  (with-temp-enh-rb-string
   "RSpec.describe Foo do\n  it 'bar' do\n    HERE\n  end\nend"

   (search-forward "HERE")

   (enh-ruby-beginning-of-block)
   (line-should-equal "  it 'bar' do")

   (enh-ruby-beginning-of-block)
   (line-should-equal "RSpec.describe Foo do")

   (enh-ruby-beginning-of-block)
   (line-should-equal "RSpec.describe Foo do")))

(enh-deftest enh-ruby-indent-for-tab-heredocs/off ()
  (with-temp-enh-rb-string
   "meth <<-DONE\n  a b c\nd e f\nDONE\n"

   (search-forward "d e f")
   (move-beginning-of-line nil)
   (let ((enh-ruby-preserve-indent-in-heredocs nil))
     (indent-for-tab-command)           ; hitting TAB char
     (buffer-should-equal "meth <<-DONE\n  a b c\nd e f\nDONE\n"))))

(enh-deftest enh-ruby-indent-for-tab-heredocs/on ()
  (with-temp-enh-rb-string
   "meth <<-DONE\n  a b c\nd e f\nDONE\n"

   (search-forward "d e f")
   (move-beginning-of-line nil)
   (let ((enh-ruby-preserve-indent-in-heredocs t))
     (indent-for-tab-command)           ; hitting TAB char
     (buffer-should-equal "meth <<-DONE\n  a b c\n  d e f\nDONE\n"))))

(enh-deftest enh-ruby-indent-for-tab-heredocs/unset ()
  (with-temp-enh-rb-string
   "meth <<-DONE\n  a b c\nd e f\nDONE\n"

   (search-forward "d e f")
   (move-beginning-of-line nil)
   (indent-for-tab-command)             ; hitting TAB char
   (buffer-should-equal "meth <<-DONE\n  a b c\nd e f\nDONE\n")))

;;; enh-ruby-toggle-block

(defconst ruby-do-block      "7.times do |i|\n  puts \"number #{i+1}\"\nend\n")
(defconst ruby-brace-block/1 "7.times { |i| puts \"number #{i+1}\" }\n")
(defconst ruby-brace-block/3 "7.times { |i|\n  puts \"number #{i+1}\"\n}\n")

(defun enh-ruby-toggle-block-and-wait ()
  (enh-ruby-toggle-block)
  (erm-wait-for-parse)
  (font-lock-ensure))

(defun toggle-to-do ()
  (enh-ruby-toggle-block-and-wait)
  (buffer-should-equal ruby-do-block))

(defun toggle-to-brace ()
  (enh-ruby-toggle-block-and-wait)
  (buffer-should-equal ruby-brace-block/1))

(enh-deftest enh-ruby-toggle-block/both ()
  (with-temp-enh-rb-string ruby-brace-block/3
    (toggle-to-do)
    (toggle-to-brace)))

(enh-deftest enh-ruby-toggle-block/brace ()
  (with-temp-enh-rb-string ruby-brace-block/3
    (toggle-to-do)))

(enh-deftest enh-ruby-toggle-block/do ()
  (with-temp-enh-rb-string ruby-do-block
    (toggle-to-brace)))

(defconst ruby-brace-block/puts "7.times { |i| puts i }\n")
(defconst ruby-do-block/puts    "7.times do |i|\n  puts i \nend\n")

(enh-deftest enh-ruby-toggle-block/does-not-trigger-when-point-is-beyond-block ()
  (with-temp-enh-rb-string ruby-brace-block/puts
    (search-forward "}")
    (enh-ruby-toggle-block-and-wait)
    (buffer-should-equal ruby-brace-block/puts)))

(enh-deftest enh-ruby-toggle-block/triggers-when-point-is-at-end-of-block ()
  (with-temp-enh-rb-string ruby-brace-block/puts
    (search-forward "}")
    (backward-char)
    (enh-ruby-toggle-block-and-wait)
    (buffer-should-equal ruby-do-block/puts)))

(defconst ruby-puts "puts \"test\"")

(enh-deftest enh-ruby-toggle-block/with-no-block-in-buffer-does-not-fail ()
  (with-temp-enh-rb-string ruby-puts
    (enh-ruby-toggle-block-and-wait)
    (buffer-should-equal ruby-puts)))

(defconst ruby-brace/let "let(:dont_let) { { a: 1, b: 2 } }\n")
(defconst ruby-do/let    "let(:dont_let) do\n  { a: 1, b: 2 } \nend\n")

(enh-deftest enh-ruby-toggle-block/brace-with-inner-hash ()
  (with-temp-enh-rb-string ruby-brace/let
    (enh-ruby-toggle-block-and-wait)
    (buffer-should-equal ruby-do/let)))

(enh-deftest enh-ruby-paren-mode-if/open ()
  (should-show-parens
   "
G|ifG foo
  bar
GendG"))

(enh-deftest enh-ruby-paren-mode-if/close ()
  (should-show-parens
   "
GifG foo
  bar
Gend|G"))

(enh-deftest enh-ruby-paren-mode-if/mismatch ()
  (should-show-parens
   "
R|ifR foo
  bar
R}R"))

(enh-deftest enh-ruby-paren-mode-while-do/open ()
  (should-show-parens
   "
G|whileG foo do
  if bar
    baz
  end
GendG"))

(enh-deftest enh-ruby-paren-mode-while-do/close ()
  (should-show-parens
   "
GwhileG foo do
  if bar
    baz
  end
Gend|G"))

(enh-deftest enh-ruby-paren-mode-while-do/mismatch ()
  (should-show-parens
   "
R|whileR foo do
  if bar
    baz
  end
RR"))

(enh-deftest enh-ruby-paren-mode-begin-end ()
  (should-show-parens
   "
G|beginG
  foo
rescue
GendG"))

(enh-deftest enh-ruby-paren-mode-if-dont-show ()
  "point is not in right spot to highlight pairs so nothing
should be tagged"
  (should-show-parens
   "
i|f foo
  bar
end")
  (should-show-parens
   "
if| foo
  bar
end")
  (should-show-parens
   "
if foo
  bar
en|d")
  (should-show-parens
   "
if foo
  bar
e|nd"))

(enh-deftest enh-ruby-paren-mode-delegate ()
  "delegate braces to show-paren-data-function (i.e. don't
highlight anything)"
  (should-show-parens
   "foo.map G|{G there G}G"))


================================================
FILE: test/helper.el
================================================
(require 'ert)
(require 'ert-x)
(require 'paren)                        ; for show-paren tests & helper
(eval-and-compile
  (add-to-list 'load-path (file-name-directory (directory-file-name default-directory))))
(require 'enh-ruby-mode)

;; I hate this so much... Shuts up "Indenting region..." output
(defun make-progress-reporter (&rest ignored) nil)

(defvar enh-tests '())

;; turns out I had a duplicate test and it was driving me crazy. This is my fix.
(defmacro enh-deftest (name &rest rest)
  (if (memq name enh-tests)
      (error "Duplicate test name! %S" name)
    (setq enh-tests (cons name enh-tests))
    `(ert-deftest ,name ,@rest)))
(put 'enh-deftest 'lisp-indent-function 'defun)

(defmacro with-temp-enh-rb-string (str &rest body)
  `(with-temp-buffer
     (insert ,str)
     (enh-ruby-mode)
     (erm-wait-for-parse)
     (font-lock-ensure)
     (goto-char (point-min))
     (progn ,@body)))
(put 'with-temp-enh-rb-string 'lisp-indent-function 1)

(defmacro with-temp-ruby-string (str &rest body)
  `(with-temp-buffer
     (insert ,str)
     (ruby-mode)
     (font-lock-ensure)
     (goto-char (point-min))
     (progn ,@body)))

(defmacro with-deep-indent (deep? &rest body)
  `(let ((enh-ruby-deep-indent-construct ,deep?) ; def / if
         (enh-ruby-deep-indent-paren ,deep?))    ; arrays / hashes
     ,@body))
(put 'with-deep-indent 'lisp-indent-function 1)

(defun buffer-string-plain ()
  (buffer-substring-no-properties (point-min) (point-max)))

(defun string-plain (s)
  (substring-no-properties s))

(defun string-should-indent (ruby exp)
  (let ((act (with-temp-enh-rb-string ruby (ert-buffer-string-reindented))))
   (should (equal exp (string-plain act)))))

(defun string-should-indent-like-ruby (ruby &optional deep?)
  (with-deep-indent deep?
    (let ((exp (with-temp-ruby-string   ruby (ert-buffer-string-reindented)))
          (act (with-temp-enh-rb-string ruby (ert-buffer-string-reindented))))
      (should (equal (string-plain exp) (string-plain act))))))

(defun buffer-should-equal (exp)
  (should (equal exp (buffer-string-plain))))

(defun rest-of-line-should-equal (exp)
  (should (equal exp (rest-of-line))))

(defun line-should-equal (exp)
  (should (equal exp (all-of-line))))

(defun all-of-line ()
  (save-excursion
    (move-beginning-of-line nil)
    (let ((start (point)))
      (end-of-line)
      (buffer-substring-no-properties start (point)))))

(defun rest-of-line ()
  (save-excursion
   (let ((start (point)))
     (end-of-line)
     (buffer-substring-no-properties start (point)))))

(defun should-show-parens (contents)
  "CONTENTS is a template specifying expected paren highlighting.
GfooG means expect foo be green (matching parens), RfooR means
red (mismatched parens), and | is point. No G/R tags means expect
no erm highlighting (i.e. delegate to normal paren-mode)"
  (with-temp-buffer
    (insert contents)
    (goto-char (point-min))
    (let ((case-fold-search nil) (tags ()) point-pos mismatch)
      (while (re-search-forward "[GR|]" nil t)
        (let ((found-char (char-before)))
          (backward-delete-char 1)
          (cond
           ((char-equal found-char ?G) (push (point) tags))
           ((char-equal found-char ?R) (progn (push (point) tags) (setq mismatch t)))
           ((char-equal found-char ?|) (setq point-pos (point))))))
      (setq tags (nreverse tags))
      (when (and tags (< (abs (- point-pos (nth 3 tags))) (abs (- point-pos (car tags)))))
        (setq tags (list (nth 2 tags) (nth 3 tags) (nth 0 tags) (nth 1 tags))))
      (setq contents (buffer-substring (point-min) (point-max)))
      (with-temp-enh-rb-string
       contents
       (goto-char point-pos)
       (should
        (equal
         (erm-show-paren-data-function)
         (if tags (append tags `(,mismatch)) nil)))))))


================================================
FILE: test/helper.rb
================================================
require_relative './markup'

module ErmTestHelper
  module_function

  # workaround for Minitest::Assertions.assert_parse
  # to remove surrounding `"` from markup diff
  def override_inspect(obj)
    def obj.inspect
      self
    end
    obj
  end
end

module Minitest::Assertions
  module_function

  def assert_parse(markedup_code, buf = ErmBuffer.new, msg = nil)
    markedup_code = markedup_code.gsub(/\A\n|\n\z/, "")

    expected_sexp, code = Markup.parse_markup(markedup_code)
    actual_sexp = Markup.parse_code(code, buf)

    msg = message(msg || code, "") do
      expected_markup = markedup_code
      actual_markup   = Markup.markup(code, Markup.parse_sexp(actual_sexp))

      diff_markup = diff(ErmTestHelper.override_inspect(expected_markup),
                         ErmTestHelper.override_inspect(actual_markup))
      diff_sexp   = diff(expected_sexp, actual_sexp)

      diff_markup + "\n" + diff_sexp
    end

    assert(expected_sexp == actual_sexp, msg)
  end
end


================================================
FILE: test/markup.rb
================================================
# -*- coding: utf-8 -*-
require 'strscan'
require_relative '../ruby/erm_buffer'

module Markup
  module_function

  def parse_code(code, erm_buffer = ErmBuffer.new)
    erm_buffer.add_content(:r, 1, code.size + 1, 0, code.size, code)
    erm_buffer.parse
  end

  # Parses marked-up code and returns [raw_sexp, code]
  # raw_sexp follows the same format as the return value of ErmBuffer::Parser#parse.
  def parse_markup(str)
    code = +""
    indent_stack = []
    result = Hash.new { |h, k| h[k] = [] }
    last_tag = nil

    ss = StringScanner.new(str)
    loop do
      case
      when ss.scan(/«@(.)»/)
        indent_stack << ss[1] << code.size + 1
      when ss.scan(/«(\w+)»/)
        last_tag << code.size + 1 if last_tag && last_tag.size.odd? # close last_tag
        last_tag = (result[ss[1]] << code.size + 1)
      when ss.scan(%r|«/\w*»|)
        last_tag << code.size + 1
      when ss.scan(/[^«]+/), ss.scan(/«[^@\w]/)
        code << ss[0]
      when ss.eos?
        last_tag << code.size + 1 if last_tag && last_tag.size.odd? # close last_tag
        break
      else
        raise "Failed at #{ss.charpos}"
      end
    end

    result = result.sort_by { |k, v| k.to_i }.map { |k, v|
      "(%d %s)" % [k, v.join(" ")] if v
    }

    sexp = "((%s %s %s %s)%s)" % [code.size,
                                  1,
                                  code.size + 1,
                                  indent_stack.join(' '),
                                  result.join]
    [sexp, code]
  end

  # Parses the return value of ErmBuffer::Parser#parse into Array
  def parse_sexp(str)
    paren_stack = []
    result = []

    ss = StringScanner.new(str)
    until ss.eos?
      case
      when ss.scan(/\(/)
        if paren_stack.empty?
          paren_stack.push(result)
        else
          paren_stack.push([])
        end
      when ss.scan(/\)/)
        if paren_stack != [result]
          result.push(paren_stack.pop)
        end
      when ss.scan(/\s+/)
        # skip spaces
      when ss.scan(/([^\s()]+)/)
        paren_stack.last.push(ss[1])
      else
        raise "Failed at #{ss.charpos}."
      end
    end

    result
  end

  def markup(code, parsed_sexp, options = {})
    options = {
      :indent    => true,
      :highlight => true,
      :close_tag  => false,
      :verbose => false,
    }.merge(options)

    indents, highlights = parsed_sexp[0][3..-1], parsed_sexp[1..-1]
    tags = [] # [["«tag»", insert_position], ... ]

    if options[:indent]
      indents.each_slice(2).each do |symbol, index|
        tags << ["«@#{symbol}»", index.to_i]
      end
    end

    if options[:highlight]
      highlights.map do |(id, *ranges)|
        ranges.each_slice(2) do |open, close|
          tags << ["«#{id}»", open.to_i]
          tags << ["«/#{id}»", close.to_i] if options[:close_tag]
        end
      end
    end

    faces = # maps to enh-ruby-font-names
      %w[
         »
         «STRING
         «TYPE
         «VAR
         «COMMENT
         «CONSTANT
         «STRING
         «STRINGQ
         «REGEXPQ
         «FUNCTION
         «KW
         «HEREDOC
         «OP
         «REGEXP
        ]

    tags.map! { |tag, index|
      tag = faces[$1.to_i] if tag =~ /«(\d+)»/
      [tag, index]
    } if options[:verbose]

    indent_tags = {
      "l" => "OPEN",     # - [, (, {, %w/%i open  or | goalpost open
      "r" => "CLOSE",    # - ], ), }, %w/%i close or | goalpost close
      "b" => "BEGIN",    # - begin/def/case/if
      "e" => "END",      # - end / embexpr (interpolation) end / close block }
      "d" => "DO",       # - do / {
      "s" => "STMT",     # - statement start on BACKDENT_KW else/when/rescue etc
      "c" => "CONTINUE", # - continue - period followed by return
    }

    tags.map! { |tag, index|
      tag = "«@#{indent_tags[$1]}»" if tag =~ /«@(.)»/
      [tag, index]
    } if options[:verbose]

    markup = code.dup
    offset = 0
    tags.sort_by { |(tag, index)|
      # Sort tags like «/close»«@indent»«open» for human-readability.
      # tags: 0 = close, 1 = indent, 2 = open
      type = tag.match?(%r%^.@%) ? 0 : tag.match?(%r%^./%) ? 1 : 2

      [index, type]
    }.each do |(tag, index)|
      markup.insert(index - 1 + offset, tag)
      offset += tag.size
    end

    markup
  end
end


================================================
FILE: test/test_erm_buffer.rb
================================================
# -*- coding: utf-8 -*-

gem "minitest"
require "minitest/autorun"

$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..'))

require 'ruby/erm_buffer.rb'
require 'test/helper'

class TestErmBuffer < Minitest::Test
  def parse_text(text,buf=ErmBuffer.new)
    buf.add_content(:r,1,text.size,0,text.size,text)
    buf.parse
  end

  def setup
    super
    ErmBuffer.set_extra_keywords({})
  end

  def test_continations
    assert_parse(%q{
«0»
a,
«@c»b
})
  end

  def test_symbols
    assert_parse('«5»:aaa')
    assert_parse('«5»:@aa')
    assert_parse('«5»:@@a')
    assert_parse('«5»:$aa')
    assert_parse('«5»:<=>')
    assert_parse('«5»:aa')
    assert_parse('«5»:==')
    assert_parse('«5»:a')
    assert_parse('«5»:+')
    assert_parse('«5»:=')
  end

  def test_extra_keywords
    ErmBuffer.set_extra_keywords(%w[require])
    assert_parse(%q{
«10»require«0» «7»'«1»abc«7»'«0»
x.require z
x.
«@c»«10»require
})
  end

  def test_buffer_local_extra_keywords
    ErmBuffer.set_extra_keywords(%w[global])
    local_buf=ErmBuffer.new
    local_buf.set_extra_keywords(%w[local])
    assert_parse(%q{«0»global «10»local}, local_buf)
  end

  def test_reset_mode
   assert_parse(%q{
«0»a«12»=«11»<<END«0»
«3»#«@d»{«0»
  «5»:x«0» «12»==«0» d
«@e»«3»}«1»
«11»END
})
  end

  def test_heredoc_multi
    assert_parse(%q{
«0»a«12»=«11»<<E«0»,«11»<<D«0»
«1»e
«11»E
«1»d
«11»D
})
  end

  def test_heredoc_nesting
    assert_parse(%q{
«0»a«12»=«11»<<E«0»
«1»a
«3»#«@d»{«11»<<EE«0»
«11»EE
«@e»«3»}«1»
«11»E
})
  end

  def test_heredoc_nesting_and_quoting
    assert_parse(%q{
«0»a«12»=«0» «11»<<E«0»,«11»<<-'D'«0»,«5»:b«0»
«1»se«3»#«@d»{«0»
«11»<<E«0»
«1»inner E
«11»E
«@e»«3»}«1»ee
«11»E
«1»sd#{md}ed
«11»   D
«0»
p a
})
  end

  def test_class
    assert_parse(%q{
«@b»«10»class«0» «2»Abc«0»
«@e»«10»end
})
  end

  def test_pct_w
    skip "TODO: broken by #587f48f"
    assert_parse %q{«0» «@l»«7»%w[«0» «1»a«0» «1»b«0» «1»c«0» «@r»«7»]«0» }
  end

  def test_pct_q
    assert_parse %q{«0» «7»%q[«1» a b c «7»]}
    assert_parse %q{«0» «7»%q[«1» a b c «7»]«0» }
  end

  def test_def
    assert_parse(%q{
«@b»«10»def«0» «9»simple«@l»«0»(a,b«@r»)
  «5»:if«0»
«@e»«10»end«0»

«@b»«10»def«0» «2»Const«0».«9»method«0»
  ident
«@e»«10»end«0»

«@b»«10»def«0» «10»self«0».«9»m«@l»«0»(a,
           «12»&«0»block«@r»)
  body
«@e»«10»end«0»

«@b»«10»def«0» «@l»(a «12»+«0» b «@d»«10»{«@l»«3»|«0»a«@r»«3»|«0»
  «@b»«10»def«0» «9»a«0»
  «@e»«10»end«0»
«@e»«10»}«@r»«0»).«9»d«@l»«0»(a«@r»)
  body
«@s»«10»rescue«0» «2»Exception«0» «12»=>«0» e
  raise
«@e»«10»end
})
  end

  def test_endless_def
    assert_parse(%q{
«@b»«10»def«0» «10»self«0».«9»m«0» «@e»«12»=«0» ident

«@b»«10»def«0» «9»m«0» «@e»«12»=«0» ident

«@b»«10»def«0» «9»m«@l»«0»(a«@r») «@e»«12»=«0» ident

«@b»«10»def«0» «9»m«@l»«0»(«12»*«0»a, «12»**«0»k, «12»&«0»b«@r») «@e»«12»=«0» ident

«@b»«10»def«0» «9»m«@l»«0»(«12»...«@r»«0») «@e»«12»=«0» ident

«@b»«10»def«0» «9»m«0» «@e»«12»=«0»
«@c»  ident
})
  end

  def test_heredoc_followed_by_if_arg
    assert_parse(%q{
«0»bob«@l»(«11»<<-END«0», «@b»«10»if«0» a
«1»fdssdfdsf
            dfsdfs"
«11»END
«0»
              fds
            «@s»«10»elsif«0» b
              fds
            «@e»«10»end«0»
«@r»)

a «10»if«0» b
c

a«@l»(sdfdsf«@l»(«@r»),
  «@b»«10»if«0» fds
    dfs
  «@e»«10»end«0»
  «@r»)
})
  end

  def test_if
    assert_parse(%q{
«0»a «10»if«0» dsf
«@b»«10»if«0» a
  b
«@s»«10»else«0»
  c
«@e»«10»end«0»

b«12»=«0»d «10»if«0» sdf

a«10»;«0» «@b»«10»if«0» b
     c
   «@e»«10»end«0»

a«@l»(b,«@b»«10»if«0» c
      d
    «@e»«10»end«0»
  «@r»)

a«@l»(«@b»«10»if«0» c
    d
  «@e»«10»end«0»
  «@r»)

a«12»=«@l»«0»{«5»a:«0» fds,
   «5»:b«0» «12»=>«0» fds
   «@r»}
})
  end

  def test_utf8_here_docs
    assert_parse(%q{
«3»@ü«12»=«11»<<E«0»
«1»á
«11»E
})
  end

  def test_interpolated_string
    assert_parse(%q{«7»"«3»#«@d»{«0»1«@e»«3»}«7»"})

    assert_parse(%q{
«0»puts «7»"«3»#«@d»{«0»
  «@l»[1, 2, 3«@r»].map «@d»«10»do«0» «@l»«3»|«0»i«@r»«3»|«0»
    p «7»"«3»#«@d»{«0»
      i«12»*«0»i
    «@e»«3»}«7»"«0»
  «@e»«10»end«0»
«@e»«3»}«7»"
})
  end

  def test_keyword_do_block
    assert_parse(%q{
«0»each «@d»«10»do«0»
«@e»«10»end
})
  end

  def test_keyword_do_cond
    assert_parse(%q{
«@b»«10»while«0» «10»false«0» «10»do«0»
«@e»«10»end
})

    assert_parse(%q{
«@b»«10»while«0» «@l»(«@b»«10»until«0» «10»false«0» «10»do«0»
       «@e»«10»end«@r»«0») «10»do«0»
«@e»«10»end
})
  end

  def test_percent_literals_w
    skip "TODO: broken by #587f48f"
    assert_parse(%q{
«@l»«0»[
  «7»"«1»one«7»"«0»
«@r»]
«@l»[«7»"«1»one«7»"«@r»«0»]

«@l»«7»%w(«0»
  «1»one«0»
«@r»«7»)«0»
«@l»«7»%W(«0» «1»one«0» «3»#«@d»{«5»:two«@e»«3»}«0» «@r»«7»)«0»

«@b»«10»begin«0»
  a «12»=«0» «@l»[
    «7»"«1»one«7»"«0»
  «@r»]
  a «12»=«0» «@l»«7»%w[«0»
    «1»one«0»
  «@r»«7»]«0»
«@e»«10»end
})
  end

  def test_percent_literals_i
    skip "TODO: broken by #587f48f"
    # %i didn't exist before ruby 2.0
    return if RUBY_VERSION.split(".").first.to_i < 2

    assert_parse(%q{
«@l»«7»%i[«1»one«0» «@r»«7»]«0»

«@l»«7»%I'«1»a«0» «1»\#{a}«@r»«7»'«0»
«@l»«7»%I'«1»a«0» «3»#«@d»{«0»a«@e»«3»}«@r»«7»'
})
  end

  def test_dot_indent
    assert_parse(%q{
«0»a.b

c.
«@c»  d

g
«@c»  .h

i.j
«@c»  .h
})
  end

  def test_dot_indent_with_block
    assert_parse(%q{
«0»a
«@c»  .b «@d»«10»{«0» «@e»«10»}«0»
«@c»  .c
})
  end

  def test_dot_indent_with_comment
    assert_parse(%q{
«0»a
«@c»  .b «4»# comment
«@c»«0»  .c
})
  end

  def test_brace_after_identifer
    assert_parse(%q[
«0»a
«@l»{
«5»:b«0» «12»=>«0» c,
«@r»}
])
  end
end


================================================
FILE: tools/debug.rb
================================================
#!/usr/bin/env ruby -w

require_relative '../ruby/erm_buffer'

trace = ARGV.delete "--trace"
debug = ARGV.delete "-d"

class ErmBuffer::Parser
  alias :old_realadd :realadd
  def realadd(sym,tok,len)
    x = old_realadd(sym, tok, len)
    k = sym =~ /^rem_/ ? :rem : sym
    v = ErmBuffer::FONT_LOCK_NAMES[k] || -1
    puts "%2d %-20p %3d %p" % [v, sym, len, tok]
    x
  end
end

if trace then
  require "tracer"
  Tracer.on
end

ARGV.each do |file|
  buf = ErmBuffer.new
  buf.debug = true if debug
  content = File.read file
  point_min, point_max, pbeg, len = 1, content.size+1, 0, content.size

  buf.add_content :x, point_min, point_max, pbeg, len, content

  puts buf.parse
end


================================================
FILE: tools/lexer.rb
================================================
#!/usr/bin/env ruby -w

require "ripper"
require "pp"

ARGV.each do |p|
  f = File.read p
  puts
  pp Ripper.lex f
  puts
  pp Ripper.sexp_raw f
  puts
  pp Ripper.sexp f
end


================================================
FILE: tools/markup.rb
================================================
#!/usr/bin/env ruby -w

require_relative '../test/markup'

if ARGV.delete("--help")
  puts <<-HERE
Usage: ruby #{__FILE__} <programfile> <options>

Options:
  --help
  --no-indent
  --no-highlight
  -v
    HERE

  exit
end

options = {
  indent: !ARGV.delete("--no-indent"),
  highlight: !ARGV.delete("--no-highlight"),
  verbose: ARGV.delete("-v"),
}

src = ARGF.read
sexp = Markup.parse_code(src)
markup = Markup.markup(src, Markup.parse_sexp(sexp), options)

puts sexp
puts "---"
puts markup
Download .txt
gitextract_zuffihn_/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── COPYING
├── README.rdoc
├── Rakefile
├── debugging.md
├── enh-ruby-mode.el
├── ruby/
│   ├── erm.rb
│   └── erm_buffer.rb
├── test/
│   ├── enh-ruby-mode-test.el
│   ├── helper.el
│   ├── helper.rb
│   ├── markup.rb
│   └── test_erm_buffer.rb
└── tools/
    ├── debug.rb
    ├── lexer.rb
    └── markup.rb
Download .txt
SYMBOL INDEX (98 symbols across 6 files)

FILE: ruby/erm.rb
  class BufferStore (line 8) | class BufferStore
    method initialize (line 9) | def initialize
    method get_buffer (line 13) | def get_buffer buf_num
    method rm (line 17) | def rm buf_num

FILE: ruby/erm_buffer.rb
  class ErmBuffer (line 3) | class ErmBuffer
    type Adder (line 39) | module Adder
      function nadd (line 45) | def nadd sym, tok, len = tok.size, ft = false, la = nil
    class Heredoc (line 84) | class Heredoc
      method initialize (line 90) | def initialize parser, prev, tok, lineno
      method d (line 99) | def d o
      method realadd (line 103) | def realadd(*args)
      method restore (line 107) | def restore
    class Parser (line 121) | class Parser < ::Ripper   #:nodoc: internal use only
      method initialize (line 164) | def initialize ermbuffer, src, point_min, point_max, first_count
      method add (line 176) | def add(*args)
      method indent (line 180) | def indent type, c = 0
      method parse (line 186) | def parse
      method realadd (line 221) | def realadd sym, tok, len
      method maybe_plit_ending (line 261) | def maybe_plit_ending tok
      method d (line 279) | def d o
      method debug_on (line 283) | def debug_on tok
      method on_comma (line 318) | def on_comma tok
      method on_comment (line 326) | def on_comment tok
      method on_const (line 331) | def on_const tok
      method on_embexpr_beg (line 346) | def on_embexpr_beg tok
      method on_embexpr_end (line 362) | def on_embexpr_end tok
      method on_embvar (line 371) | def on_embvar tok
      method on_eol (line 382) | def on_eol sym, tok
      method on_heredoc_beg (line 398) | def on_heredoc_beg tok
      method on_heredoc_end (line 407) | def on_heredoc_end tok
      method on_ident (line 412) | def on_ident tok
      method on_ignored_nl (line 430) | def on_ignored_nl tok
      method on_kw (line 437) | def on_kw sym # TODO: break up. 61 lines long
      method on_lbrace (line 499) | def on_lbrace tok
      method on_lparen (line 518) | def on_lparen tok
      method on_nl (line 540) | def on_nl tok
      method on_op (line 544) | def on_op tok
      method on_period (line 576) | def on_period tok
      method on_rbrace (line 593) | def on_rbrace tok
      method on_regexp_beg (line 619) | def on_regexp_beg tok
      method on_regexp_end (line 626) | def on_regexp_end tok
      method on_rparen (line 632) | def on_rparen tok
      method on_semicolon (line 644) | def on_semicolon tok
      method on_sp (line 652) | def on_sp tok
      method on_symbeg (line 660) | def on_symbeg tok
      method on_tlambeg (line 667) | def on_tlambeg tok
      method on_tstring_content (line 674) | def on_tstring_content tok
      method on_tstring_end (line 686) | def on_tstring_end tok
      method on_label_end (line 698) | def on_label_end tok
      method on_words_beg (line 704) | def on_words_beg tok
      method on_words_sep (line 713) | def on_words_sep tok
    method initialize (line 736) | def initialize
    method d (line 743) | def d o
    method add_content (line 750) | def add_content cmd, point_min, point_max, pbeg, len, content
    method check_syntax (line 771) | def check_syntax fname = '', code = buffer
    method parse (line 783) | def parse
    method set_extra_keywords (line 791) | def self.set_extra_keywords keywords
    method set_extra_keywords (line 795) | def set_extra_keywords keywords
    method extra_keywords (line 799) | def extra_keywords

FILE: test/helper.rb
  type ErmTestHelper (line 3) | module ErmTestHelper
    function override_inspect (line 8) | def override_inspect(obj)
  type Minitest::Assertions (line 16) | module Minitest::Assertions
    function assert_parse (line 19) | def assert_parse(markedup_code, buf = ErmBuffer.new, msg = nil)

FILE: test/markup.rb
  type Markup (line 5) | module Markup
    function parse_code (line 8) | def parse_code(code, erm_buffer = ErmBuffer.new)
    function parse_markup (line 15) | def parse_markup(str)
    function parse_sexp (line 54) | def parse_sexp(str)
    function markup (line 83) | def markup(code, parsed_sexp, options = {})

FILE: test/test_erm_buffer.rb
  class TestErmBuffer (line 11) | class TestErmBuffer < Minitest::Test
    method parse_text (line 12) | def parse_text(text,buf=ErmBuffer.new)
    method setup (line 17) | def setup
    method test_continations (line 22) | def test_continations
    method test_symbols (line 30) | def test_symbols
    method test_extra_keywords (line 43) | def test_extra_keywords
    method test_buffer_local_extra_keywords (line 53) | def test_buffer_local_extra_keywords
    method test_reset_mode (line 60) | def test_reset_mode
    method test_heredoc_multi (line 70) | def test_heredoc_multi
    method test_heredoc_nesting (line 80) | def test_heredoc_nesting
    method test_heredoc_nesting_and_quoting (line 91) | def test_heredoc_nesting_and_quoting
    method test_class (line 107) | def test_class
    method test_pct_w (line 114) | def test_pct_w
    method test_pct_q (line 119) | def test_pct_q
    method test_def (line 124) | def test_def
    method test_endless_def (line 150) | def test_endless_def
    method test_heredoc_followed_by_if_arg (line 167) | def test_heredoc_followed_by_if_arg
    method test_if (line 191) | def test_if
    method test_utf8_here_docs (line 222) | def test_utf8_here_docs
    method test_interpolated_string (line 230) | def test_interpolated_string
    method test_keyword_do_block (line 244) | def test_keyword_do_block
    method test_keyword_do_cond (line 251) | def test_keyword_do_cond
    method test_percent_literals_w (line 264) | def test_percent_literals_w
    method test_percent_literals_i (line 288) | def test_percent_literals_i
    method test_dot_indent (line 301) | def test_dot_indent
    method test_dot_indent_with_block (line 316) | def test_dot_indent_with_block
    method test_dot_indent_with_comment (line 324) | def test_dot_indent_with_comment
    method test_brace_after_identifer (line 332) | def test_brace_after_identifer

FILE: tools/debug.rb
  class ErmBuffer::Parser (line 8) | class ErmBuffer::Parser
    method realadd (line 10) | def realadd(sym,tok,len)
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (147K chars).
[
  {
    "path": ".github/workflows/test.yml",
    "chars": 582,
    "preview": "name: test\n\non:\n  push:\n    branches:\n      - master\n      - test\n  pull_request:\n    branches:\n      - master\n      - t"
  },
  {
    "path": ".gitignore",
    "chars": 49,
    "preview": ".bzr\n.bzrignore\n*~\nnokoload.gemspec\npkg\ntmp\n*.elc"
  },
  {
    "path": "COPYING",
    "chars": 2503,
    "preview": "Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.\nYou can redistribute it and/or modify it under"
  },
  {
    "path": "README.rdoc",
    "chars": 2865,
    "preview": "= Enhanced Ruby Mode\n\n* Git: http://github.com/zenspider/Enhanced-Ruby-Mode\n* Author: Geoff Jacobsen / forked by Ryan Da"
  },
  {
    "path": "Rakefile",
    "chars": 2098,
    "preview": "task :default => %w[clean compile test:all]\n\nel_files = Rake::FileList['**/enh-ruby-mode*.el']\n\ndef run cmd\n  sh cmd do "
  },
  {
    "path": "debugging.md",
    "chars": 7229,
    "preview": "# How to Debug Problems in ERM\n\nThese are notes to myself because I don't work on this project much.\n\n## 0. Run `rake do"
  },
  {
    "path": "enh-ruby-mode.el",
    "chars": 64184,
    "preview": ";;; enh-ruby-mode.el --- Major mode for editing Ruby files  -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2012-2022+ -- "
  },
  {
    "path": "ruby/erm.rb",
    "chars": 983,
    "preview": "#!/usr/bin/env ruby\n\nrequire_relative \"erm_buffer\"\n\nSTDIN.set_encoding \"UTF-8\"\n# STDIN.set_encoding \"BINARY\"\n\nclass Buff"
  },
  {
    "path": "ruby/erm_buffer.rb",
    "chars": 19303,
    "preview": "require 'ripper'\n\nclass ErmBuffer\n  FONT_LOCK_NAMES = {\n    rem:             0,  # remove/ignore\n    sp:              0,"
  },
  {
    "path": "test/enh-ruby-mode-test.el",
    "chars": 22446,
    "preview": "(eval-and-compile\n  (add-to-list 'load-path default-directory)\n  (load \"./helper\" nil t))\n\n(defun erm-run-current-test ("
  },
  {
    "path": "test/helper.el",
    "chars": 3800,
    "preview": "(require 'ert)\n(require 'ert-x)\n(require 'paren)                        ; for show-paren tests & helper\n(eval-and-compil"
  },
  {
    "path": "test/helper.rb",
    "chars": 989,
    "preview": "require_relative './markup'\n\nmodule ErmTestHelper\n  module_function\n\n  # workaround for Minitest::Assertions.assert_pars"
  },
  {
    "path": "test/markup.rb",
    "chars": 4287,
    "preview": "# -*- coding: utf-8 -*-\nrequire 'strscan'\nrequire_relative '../ruby/erm_buffer'\n\nmodule Markup\n  module_function\n\n  def "
  },
  {
    "path": "test/test_erm_buffer.rb",
    "chars": 5539,
    "preview": "# -*- coding: utf-8 -*-\n\ngem \"minitest\"\nrequire \"minitest/autorun\"\n\n$LOAD_PATH.unshift(File.join(File.dirname(__FILE__),"
  },
  {
    "path": "tools/debug.rb",
    "chars": 685,
    "preview": "#!/usr/bin/env ruby -w\n\nrequire_relative '../ruby/erm_buffer'\n\ntrace = ARGV.delete \"--trace\"\ndebug = ARGV.delete \"-d\"\n\nc"
  },
  {
    "path": "tools/lexer.rb",
    "chars": 175,
    "preview": "#!/usr/bin/env ruby -w\n\nrequire \"ripper\"\nrequire \"pp\"\n\nARGV.each do |p|\n  f = File.read p\n  puts\n  pp Ripper.lex f\n  put"
  },
  {
    "path": "tools/markup.rb",
    "chars": 495,
    "preview": "#!/usr/bin/env ruby -w\n\nrequire_relative '../test/markup'\n\nif ARGV.delete(\"--help\")\n  puts <<-HERE\nUsage: ruby #{__FILE_"
  }
]

About this extraction

This page contains the full source code of the zenspider/enhanced-ruby-mode GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (135.0 KB), approximately 39.8k tokens, and a symbol index with 98 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!