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
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
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.