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 . 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 erm-define-faces. 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 .rb 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 . ;;; 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 "\\_") (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 < (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»<«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»<«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__} 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