├── .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 /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - test 8 | pull_request: 9 | branches: 10 | - master 11 | - test 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | emacs_version: 19 | - 28.2 20 | - 29.1 21 | 22 | steps: 23 | - name: Set up Emacs 24 | uses: purcell/setup-emacs@master 25 | with: 26 | version: ${{matrix.emacs_version}} 27 | 28 | - name: Check out project 29 | uses: actions/checkout@v3 30 | 31 | - name: Version Info 32 | run: | 33 | ruby -v 34 | gem -v 35 | 36 | - name: Test 37 | run: rake test:all 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bzr 2 | .bzrignore 3 | *~ 4 | nokoload.gemspec 5 | pkg 6 | tmp 7 | *.elc -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the GPL 3 | version 2 (see the file GPL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a) place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b) use the modified software only within your corporation or 18 | organization. 19 | 20 | c) give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d) make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a) distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b) accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c) give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d) make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Enhanced Ruby Mode 2 | 3 | * Git: http://github.com/zenspider/Enhanced-Ruby-Mode 4 | * Author: Geoff Jacobsen / forked by Ryan Davis 5 | * Copyright: 2010 - 2012 6 | * License: RUBY License 7 | 8 | == Description 9 | 10 | Enhanced Ruby Mode replaces the emacs ruby mode that comes with ruby. 11 | 12 | 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. 13 | 14 | Syntax checking is also performed. 15 | 16 | == TODO 17 | 18 | * Optimisation; currently parses and fontifies whole buffer for most modifications - it still appears to run fast enough on large files. 19 | 20 | * Suggestions? 21 | 22 | 23 | == Synopsis 24 | 25 | * Enhanced Ruby Mode is installable via el-get and Melpa, where its package name is +enh-ruby-mode+. 26 | 27 | * For manual installation, add the following file to your init file. 28 | 29 | (add-to-list 'load-path "(path-to)/Enhanced-Ruby-Mode") ; must be added after any path containing old ruby-mode 30 | (autoload 'enh-ruby-mode "enh-ruby-mode" "Major mode for ruby files" t) 31 | (add-to-list 'auto-mode-alist '("\\.rb\\'" . enh-ruby-mode)) 32 | (add-to-list 'interpreter-mode-alist '("ruby" . enh-ruby-mode)) 33 | 34 | ;; optional 35 | 36 | (setq enh-ruby-program "(path-to-ruby1.9)/bin/ruby") ; so that still works if ruby points to ruby1.8 37 | 38 | * 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: 39 | 40 | (remove-hook 'enh-ruby-mode-hook 'erm-define-faces) 41 | 42 | == Existing ruby-mode hooks 43 | 44 | You may have existing lines in your emacs config that add minor modes based on ruby mode, like this: 45 | (add-hook 'ruby-mode-hook 'robe-mode) 46 | (add-hook 'ruby-mode-hook 'yard-mode) 47 | 48 | For these to work with enh-ruby-mode, you need to add hooks to the enh-ruby-mode minor mode: 49 | (add-hook 'enh-ruby-mode-hook 'robe-mode) 50 | (add-hook 'enh-ruby-mode-hook 'yard-mode) 51 | 52 | == Load enh-ruby-mode for Ruby files 53 | 54 | To use enh-ruby-mode for .rb add the following to your init file: 55 | (add-to-list 'auto-mode-alist '("\\.rb\\'" . enh-ruby-mode)) 56 | 57 | To use enh-ruby-mode for all common Ruby files and the following to your init file: 58 | (add-to-list 'auto-mode-alist 59 | '("\\(?:\\.rb\\|ru\\|rake\\|thor\\|jbuilder\\|gemspec\\|podspec\\|/\\(?:Gem\\|Rake\\|Cap\\|Thor\\|Vagrant\\|Guard\\|Pod\\)file\\)\\'" . enh-ruby-mode)) 60 | 61 | == Requirements 62 | 63 | * ruby 1.9.2 (or later) 64 | 65 | == Install 66 | 67 | * git clone git@github.com:zenspider/Enhanced-Ruby-Mode.git 68 | 69 | == Development 70 | 71 | Developing requires minitest 5.x gem. 72 | 73 | Testing parser: 74 | 75 | rake test:ruby [N=name or /pattern/] 76 | 77 | rake test:elisp [N=pattern] 78 | 79 | rake test:all 80 | 81 | rake # same as test:all 82 | 83 | Tests for Emacs Lisp require ERT. It is built-in since Emacs 24.1. 84 | 85 | == Credits 86 | 87 | Jell (Jean-Louis Giordano) https://github.com/Jell 88 | Improved UTF-8 support 89 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => %w[clean compile test:all] 2 | 3 | el_files = Rake::FileList['**/enh-ruby-mode*.el'] 4 | 5 | def run cmd 6 | sh cmd do |good| 7 | # block prevents ruby backtrace on failure 8 | exit 1 unless good 9 | end 10 | end 11 | 12 | def emacs args 13 | emacs_cmd = Dir[ 14 | "/usr/local/bin/emacs", 15 | "/{My,}Applications/Emacs.app/Contents/MacOS/Emacs" # homebrew 16 | ].first || "emacs" # trust the path 17 | 18 | run %Q[#{emacs_cmd} -Q -L . #{args}] 19 | end 20 | 21 | def emacs_test args 22 | emacs "-l enh-ruby-mode-test.el #{args}" 23 | end 24 | 25 | desc "byte compile the project. Helps drive out warnings, but also faster." 26 | task compile: el_files.ext('.elc') 27 | 28 | rule '.elc' => '.el' do |t| 29 | emacs "--batch -f batch-byte-compile #{t.source}" 30 | end 31 | 32 | desc "Clean the project" 33 | task :clean do 34 | rm_f Dir["**/*~", "**/*.elc"] 35 | end 36 | 37 | task :test => %w[ test:ruby test:elisp ] 38 | 39 | namespace :test do 40 | desc "Run tests for Ruby" 41 | task :ruby do 42 | n = ENV["N"] 43 | 44 | if n then 45 | run %Q[ruby -wI. test/test_erm_buffer.rb -n #{n.dump}] 46 | else 47 | run %Q[ruby -wI. test/test_erm_buffer.rb] 48 | end 49 | end 50 | 51 | desc "Run tests for Emacs Lisp" 52 | task :elisp do 53 | n=ENV["N"] 54 | 55 | Dir.chdir "test" do 56 | if n then 57 | emacs_test "--batch -eval '(ert-run-tests-batch-and-exit #{n.dump})'" 58 | else 59 | emacs_test "--batch -f ert-run-tests-batch-and-exit" 60 | end 61 | end 62 | end 63 | 64 | desc "Run tests for Emacs Lisp interactively" 65 | task :elispi do 66 | Dir.chdir "test" do 67 | emacs_test %q[-eval "(ert-run-tests-interactively 't)"] 68 | end 69 | end 70 | 71 | desc "Run test:ruby and test:elisp" 72 | task :all => [:ruby, :elisp] 73 | end 74 | 75 | def docker cmd 76 | sh %(docker run -v $PWD:/erm --rm -i -t -w /erm/test zenspider/emacs-ruby #{cmd}) 77 | end 78 | 79 | desc "test in a docker container" 80 | task :docker do 81 | docker "rake test:all" 82 | end 83 | 84 | desc "interactive test in a docker container" 85 | task :dockeri do 86 | docker "rake test:elispi" 87 | end 88 | 89 | desc "run a shell in a docker container" 90 | task :sh do 91 | docker "/bin/sh" 92 | end 93 | 94 | desc "debug a file (F=path)" 95 | task :debug do 96 | f = ENV["F"] 97 | system "ruby tools/debug.rb #{f}" 98 | puts 99 | system "ruby tools/lexer.rb #{f}" 100 | puts 101 | system "ruby tools/markup.rb #{f}" 102 | end 103 | -------------------------------------------------------------------------------- /debugging.md: -------------------------------------------------------------------------------- 1 | # How to Debug Problems in ERM 2 | 3 | These are notes to myself because I don't work on this project much. 4 | 5 | ## 0. Run `rake docker` or `rake dockeri` to run tests in isolation. 6 | 7 | Make sure that everything else is currently good before you go 8 | debugging new issues. 9 | 10 | ``` 11 | rm *.elc; cmacs -Q -l loader.el wtf.rb 12 | ``` 13 | 14 | ## 1. First, get a reproduction in a file named bug###.rb. 15 | 16 | This helps you track back to a github issue (create one if necessary). 17 | 18 | ## 2. Reduce the reproduction to the bare minimum. 19 | 20 | Usually, there's little need for "real" code and the submission 21 | contains a lot of sub-expressions that can be removed. 22 | 23 | ```ruby 24 | renewed_clients = @renewed_clients_ransack 25 | .result 26 | .order('due_at desc') 27 | .page(params[:renewed_clients_page]) 28 | ``` 29 | 30 | vs: 31 | 32 | ```ruby 33 | @b 34 | .c 35 | .d 36 | ``` 37 | 38 | There's no need for arguments or the extra calls. Even the assignment 39 | can be removed. 40 | 41 | Know what the expected result should be. In the case of the above, the 42 | indentation is off and should be: 43 | 44 | ```ruby 45 | @b 46 | .c 47 | .d 48 | ``` 49 | 50 | ## 3. Run `rake debug F=bug###.rb` to get relevant output. 51 | 52 | This outputs what it sees, not what it thinks it should be. For the 53 | above, the output looks like (with notes inline): 54 | 55 | ### 3.1. tools/debug.rb: 56 | 57 | ```ruby 58 | [:ivar, "@b", 2] 59 | [:sp, "\n", 1] 60 | [:sp, " ", 2] 61 | [:rem, ".", 1] 62 | [:ident, "c", 1] 63 | [:sp, "\n", 1] 64 | [:sp, " ", 4] 65 | [:indent, :c, -4] 66 | [:rem, ".", 1] 67 | [:ident, "d", 1] 68 | ((15 1 16 c 9)(0 3 16)(3 1 3)) 69 | ``` 70 | 71 | This is a raw printing of the tokens as they are lexed in triplets of 72 | token type, token value, and length. It is followed with the data that 73 | actually goes back from the ruby process to emacs. This is what is 74 | used to highlight and/or indent. 75 | 76 | TODO: I don't know how to read that sexp yet. But I want to document 77 | all of this output first to see if it knocks something loose. 78 | 79 | ### 3.2. tools/lexer.rb: 80 | 81 | This tool is helpful because it only uses Ripper and knows nothing 82 | about ERM. 83 | 84 | This is the raw output from Ripper.lex and is basically the events 85 | that will be triggered in ERM: 86 | 87 | ```ruby 88 | [[[1, 0], :on_ivar, "@b", EXPR_END], 89 | [[1, 2], :on_ignored_nl, "\n", EXPR_END], 90 | [[2, 0], :on_sp, " ", EXPR_END], 91 | [[2, 2], :on_period, ".", EXPR_DOT], 92 | [[2, 3], :on_ident, "c", EXPR_ARG], 93 | [[2, 4], :on_ignored_nl, "\n", EXPR_ARG], 94 | [[3, 0], :on_sp, " ", EXPR_ARG], 95 | [[3, 4], :on_period, ".", EXPR_DOT], 96 | [[3, 5], :on_ident, "d", EXPR_ARG], 97 | [[3, 6], :on_nl, "\n", EXPR_BEG]] 98 | ``` 99 | 100 | This is the raw output from Ripper.sexp_raw: 101 | 102 | ```ruby 103 | [:program, 104 | [:stmts_add, 105 | [:stmts_new], 106 | [:call, 107 | [:call, [:var_ref, [:@ivar, "@b", [1, 0]]], :".", [:@ident, "c", [2, 3]]], 108 | :".", 109 | [:@ident, "d", [3, 5]]]]] 110 | ``` 111 | 112 | This is the raw output from Ripper.sexp and is basically the same 113 | thing but cleaned up / combined a bit: 114 | 115 | ```ruby 116 | [:program, 117 | [[:call, 118 | [:call, [:var_ref, [:@ivar, "@b", [1, 0]]], :".", [:@ident, "c", [2, 3]]], 119 | :".", 120 | [:@ident, "d", [3, 5]]]]] 121 | ``` 122 | 123 | ### 3.3. tools/markup.rb 124 | 125 | ``` 126 | ((15 1 16 c 9)(0 3 16)(3 1 3)) 127 | --- 128 | «3»@b«0» 129 | .c 130 | «@c» .d 131 | ``` 132 | 133 | The sexp, roughly described is: 134 | 135 | ```ruby 136 | ((code.size, 1, code.size+1, indent_stack.join) result.join) 137 | ``` 138 | 139 | TODO: I'm not sure how to read result yet. 140 | 141 | ## 4. See if you can find the closest passing version of the repro 142 | 143 | In this example, if the receiver is not an ivar, it works fine: 144 | 145 | ```ruby 146 | b 147 | .c 148 | .d 149 | ``` 150 | 151 | ## 5. Get both passing and repro into tests 152 | 153 | This allows you some freedom. At this point, everything is isolated 154 | and reproducible. You should see that one passes and the other one fails. 155 | 156 | ```lisp 157 | (ert-deftest enh-ruby-indent-leading-dots-ident () 158 | (with-temp-enh-rb-string 159 | "b\n.c\n.d\n" 160 | 161 | (indent-region (point-min) (point-max)) 162 | (buffer-should-equal "b\n .c\n .d\n"))) 163 | 164 | (ert-deftest enh-ruby-indent-leading-dots-ivar () 165 | (with-temp-enh-rb-string 166 | "@b\n.c\n.d\n" 167 | 168 | (indent-region (point-min) (point-max)) 169 | (buffer-should-equal "@b\n .c\n .d\n"))) 170 | ``` 171 | 172 | ## 6. Try to ferret out the difference. 173 | 174 | I was able to do that with: 175 | 176 | ``` 177 | % ruby tools/debug.rb --trace bug128_1.rb | nopwd > 1 178 | % ruby tools/debug.rb --trace bug128_2.rb | nopwd > 2 179 | ``` 180 | 181 | and then using `ediff` to look at the differences in execution paths. 182 | 183 | In particular, I could see that a major difference down the line 184 | depended on whether `@ident` was true: 185 | 186 | ```diff 187 | #0:./ruby/erm_buffer.rb:19:ErmBuffer::Adder:-: case sym 188 | -#0:./ruby/erm_buffer.rb:23:ErmBuffer::Adder:-: @ident = false 189 | +#0:./ruby/erm_buffer.rb:21:ErmBuffer::Adder:-: @ident = true 190 | #0:./ruby/erm_buffer.rb:27:ErmBuffer::Adder:-: @first_token = ft 191 | ``` 192 | 193 | followed by: 194 | 195 | ```diff 196 | #0:./ruby/erm_buffer.rb:487:ErmBuffer::Parser:-: if @ident 197 | +#0:./ruby/erm_buffer.rb:488:ErmBuffer::Parser:-: line_so_far_str = @line_so_far.map {|a| a[1] }.join 198 | +#0:./ruby/erm_buffer.rb:489:ErmBuffer::Parser:-: if line_so_far_str.strip == "" 199 | +#0:./ruby/erm_buffer.rb:490:ErmBuffer::Parser:-: indent :c, (line_so_far_str.length * -1) 200 | +#0:./ruby/erm_buffer.rb:99:ErmBuffer::Parser:>: def indent type, c = 0 201 | ... 202 | ``` 203 | 204 | This was the major difference between the two and made it easy to 205 | reason about. 206 | 207 | ## 7. Make the test pass 208 | 209 | Changing from: 210 | 211 | ```ruby 212 | when :ident, :const then 213 | ``` 214 | 215 | to 216 | 217 | ```ruby 218 | when :ident, :const, :ivar, :gvar, :cvar then 219 | ``` 220 | 221 | (with 2 extra tests) made the tests pass and things seem happier. 222 | 223 | # Profiling 224 | 225 | misc dump for now: 226 | 227 | https://github.com/zenspider/enhanced-ruby-mode/issues/171 228 | 229 | ```elisp 230 | (with-current-buffer "big_file.rb" 231 | (profiler-start 'cpu) 232 | (--dotimes 10 (self-insert-command 1 ?s)) 233 | (profiler-report) 234 | (profiler-stop)) 235 | ``` 236 | 237 | https://github.com/zenspider/enhanced-ruby-mode/issues/146 238 | 239 | profiling electric-indent-mode vs not: 240 | 241 | ```elisp 242 | (with-current-buffer "ruby25_parser.rb" 243 | (goto-char (point-max)) 244 | (electric-indent-mode (if electric-indent-mode -1 1)) 245 | 246 | (profiler-start 'cpu) 247 | (--dotimes 10 (call-interactively 'newline)) 248 | (profiler-report) 249 | (profiler-stop)) 250 | ``` 251 | 252 | versus electric-indent-mode under text-mode: 253 | 254 | ```elisp 255 | (with-current-buffer "ruby25_parser.rb" 256 | (goto-char (point-max)) 257 | (text-mode) 258 | (setq start (float-time)) 259 | (electric-indent-mode (if electric-indent-mode -1 1)) 260 | (enh-ruby-mode) 261 | (erm-wait-for-parse) 262 | (message "%f" (- (float-time) start))) 263 | ``` 264 | 265 | for testing N large operations across a file/buffer 266 | ```elisp 267 | (progn 268 | (profiler-start 'cpu) 269 | (--dotimes 20 270 | (message "attempt %d" it) 271 | (let ((buf (find-file "lib/ruby27_parser.rb"))) 272 | (with-current-buffer buf 273 | (enh-ruby-mode) 274 | ;; (erm-wait-for-parse) 275 | 276 | (goto-char (point-max)) 277 | 278 | (--dotimes 10 (call-interactively 'newline)) 279 | 280 | (erm-wait-for-parse) 281 | 282 | (set-buffer-modified-p nil) 283 | (kill-buffer buf)))) 284 | (profiler-report) 285 | (profiler-stop)) 286 | ``` 287 | 288 | for profiling N operations on an open buffer and reverting any changes made: 289 | ```elisp 290 | (with-current-buffer "ruby25_parser.rb" 291 | (goto-char (point-max)) 292 | 293 | ;; (electric-indent-mode (if electric-indent-mode -1 1)) 294 | (electric-indent-mode -1) 295 | ;; (electric-indent-mode 1) 296 | 297 | ;; (erm-wait-for-parse) 298 | ;; (message "starting") 299 | (profiler-start 'cpu) 300 | 301 | (--dotimes 10 (call-interactively 'newline)) 302 | ;; (erm-wait-for-parse) 303 | 304 | (profiler-report) 305 | (profiler-stop) 306 | 307 | (with-current-buffer "ruby25_parser.rb" 308 | (erm-wait-for-parse) 309 | (revert-buffer nil t)) 310 | ) 311 | ``` 312 | -------------------------------------------------------------------------------- /enh-ruby-mode.el: -------------------------------------------------------------------------------- 1 | ;;; enh-ruby-mode.el --- Major mode for editing Ruby files -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2012-2022+ -- Ryan Davis 4 | ;; Copyright (C) 2010-2012 Geoff Jacobsen 5 | 6 | ;; Author: Geoff Jacobsen 7 | ;; Maintainer: Ryan Davis 8 | ;; URL: https://github.com/zenspider/Enhanced-Ruby-Mode 9 | ;; Created: Sep 18 2010 10 | ;; Keywords: languages, elisp, ruby 11 | ;; Package-Requires: ((emacs "25.1")) 12 | ;; Version: 1.2.0 13 | 14 | ;; This file is not part of GNU Emacs. 15 | 16 | ;; This file is free software: you can redistribute it and/or modify 17 | ;; it under the terms of the GNU General Public License as published by 18 | ;; the Free Software Foundation, either version 2 of the License, or 19 | ;; (at your option) any later version. 20 | 21 | ;; It is distributed in the hope that it will be useful, but WITHOUT 22 | ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 23 | ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 24 | ;; License for more details. 25 | 26 | ;; You should have received a copy of the GNU General Public License 27 | ;; along with it. If not, see . 28 | 29 | ;;; Commentary: 30 | 31 | ;; This is a fork of https://github.com/jacott/Enhanced-Ruby-Mode 32 | ;; to provide further enhancements and bug fixes. 33 | ;; 34 | ;; It has been renamed to enh-ruby-mode.el to avoid name conflicts 35 | ;; with ruby-mode that ships with Emacs. All symbols that started with 36 | ;; 'ruby now start with 'enh-ruby. This also makes it possible to 37 | ;; switch back and forth for testing purposes. 38 | 39 | ;; Provides fontification, indentation, syntax checking, and navigation for Ruby code. 40 | ;; 41 | ;; If you're installing manually, you should add this to your .emacs 42 | ;; file after putting it on your load path: 43 | ;; 44 | ;; (add-to-list 'load-path "(path-to)/Enhanced-Ruby-Mode") ; must be added after any path containing old ruby-mode 45 | ;; (setq enh-ruby-program "(path-to-ruby)/bin/ruby") ; so that still works if ruby points to ruby1.8 46 | ;; 47 | 48 | (require 'cl-lib) ; for cdddr, caddr 49 | (require 'files) ; for mode-require-final-newline 50 | (require 'paren) ; show-paren-data-function 51 | (require 'seq) ; seq-remove, seq-difference 52 | (require 'subr-x) ; string-trim-right 53 | (require 'cus-edit) ; custom-variable-state 54 | (require 'find-func) ; find-library-name 55 | 56 | ;;; Properties & Other Bullshit Codes: 57 | 58 | ;; 'l - [, (, {, %w/%i open or | goalpost open 59 | ;; 'r - ], ), }, %w/%i close or | goalpost close 60 | ;; 'b - begin, def, case, if 61 | ;; 'd - do, {, embexpr (interpolation) start 62 | ;; 'e - end, embexpr (interpolation) end, close block } 63 | ;; 's - statement start on BACKDENT_KW else/when/rescue etc 64 | ;; 'c - continue - period followed by return (or other way around?) 65 | 66 | ;; pc 67 | ;; bc 68 | ;; nbc 69 | ;; npc 70 | 71 | ;;; Variables: 72 | 73 | (defcustom enh-ruby-add-encoding-comment-on-save nil 74 | "Adds ruby magic encoding comment on save when non-nil." 75 | :type 'boolean 76 | :safe #'booleanp 77 | :group 'enh-ruby) 78 | 79 | (defcustom enh-ruby-bounce-deep-indent nil 80 | "Bounce between normal indentation and deep indentation when non-nil." 81 | :type 'boolean 82 | :safe #'booleanp 83 | :group 'enh-ruby) 84 | 85 | (defcustom enh-ruby-check-syntax 'errors-and-warnings 86 | "Highlight syntax errors and warnings." 87 | :type '(radio (const :tag "None" nil) 88 | (const :tag "Errors" errors) 89 | (const :tag "Errors and warnings" errors-and-warnings)) 90 | :safe #'enh-symbol-or-null-p 91 | :group 'enh-ruby) 92 | 93 | (defcustom enh-ruby-comment-column 32 94 | "*Indentation column of comments." 95 | :type 'integer 96 | :safe #'integerp 97 | :group 'enh-ruby) 98 | 99 | (defcustom enh-ruby-deep-indent-construct t 100 | "*Deep indent constructs such as if, def, class and module when non-nil." 101 | :type 'boolean 102 | :safe #'booleanp 103 | :group 'enh-ruby) 104 | 105 | (defcustom enh-ruby-deep-indent-paren t 106 | "*Deep indent lists in parenthesis when non-nil." 107 | ;; FIX: this applies to square brackets as well 108 | :type 'boolean 109 | :safe #'booleanp 110 | :group 'enh-ruby) 111 | 112 | (defcustom enh-ruby-encoding-map 113 | '((us-ascii . nil) ;; Do not put coding: us-ascii 114 | (utf-8 . nil) ;; Do not put coding: utf-8 115 | (shift-jis . cp932) ;; Emacs charset name of Shift_JIS 116 | (shift_jis . cp932) ;; MIME charset name of Shift_JIS 117 | (japanese-cp932 . cp932)) ;; Emacs charset name of CP932 118 | "Alist to map encoding name from Emacs to ruby." 119 | :type '(alist :key-type (symbol :tag "Encoding") 120 | :value-type (choice (const :tag "Ignore" nil) 121 | (symbol :tag "Charset"))) 122 | :safe (lambda (xs) 123 | (and (listp xs) 124 | (cl-every (lambda (x) 125 | (and (symbolp (car x)) 126 | (enh-symbol-or-null-p (cdr x)))) 127 | xs))) 128 | :group 'enh-ruby) 129 | 130 | (defcustom enh-ruby-extra-keywords nil 131 | "*A list of idents that will be fontified as keywords. 132 | 133 | `erm-reset' will need to be called in order for any global 134 | changes to take effect. 135 | 136 | This variable can also be buffer local in which case it will 137 | override the global value for the buffer it is local 138 | to. `ruby-local-enable-extra-keywords' needs to be called after 139 | the value changes." 140 | :type '(repeat string) 141 | :safe #'listp 142 | :group 'enh-ruby) 143 | 144 | (defcustom enh-ruby-hanging-brace-deep-indent-level 0 145 | "*Extra hanging deep indentation for continued ruby curly or square braces." 146 | :type 'integer 147 | :safe #'integerp 148 | :group 'enh-ruby) 149 | 150 | (defcustom enh-ruby-hanging-brace-indent-level 2 151 | "*Extra hanging indentation for continued ruby curly braces." 152 | :type 'integer 153 | :safe #'integerp 154 | :group 'enh-ruby) 155 | 156 | (defcustom enh-ruby-hanging-indent-level 2 157 | "*Extra hanging Indentation for continued ruby statements." 158 | :type 'integer 159 | :safe #'integerp 160 | :group 'enh-ruby) 161 | 162 | (defcustom enh-ruby-hanging-paren-deep-indent-level 0 163 | "*Extra hanging deep indentation for continued ruby parenthesis." 164 | :type 'integer 165 | :safe #'integerp 166 | :group 'enh-ruby) 167 | 168 | (defcustom enh-ruby-hanging-paren-indent-level 2 169 | "*Extra hanging indentation for continued ruby parenthesis." 170 | :type 'integer 171 | :safe #'integerp 172 | :group 'enh-ruby) 173 | 174 | (defcustom enh-ruby-indent-level 2 175 | "*Indentation of ruby statements." 176 | :type 'integer 177 | :safe #'integerp 178 | :group 'enh-ruby) 179 | 180 | (defcustom enh-ruby-indent-tabs-mode nil 181 | "*Indentation can insert tabs in ruby mode if this is non-nil." 182 | :type 'boolean 183 | :safe #'booleanp 184 | :group 'enh-ruby) 185 | 186 | (defcustom enh-ruby-preserve-indent-in-heredocs nil 187 | "Indent heredocs and multiline strings like ‘text-mode’. 188 | 189 | Warning: does not play well with command ‘electric-indent-mode’." 190 | :type 'boolean 191 | :safe #'booleanp 192 | :group 'enh-ruby) 193 | 194 | (defcustom enh-ruby-program "ruby" 195 | "The ruby program to parse the source." 196 | :type 'string 197 | :safe #'stringp 198 | :group 'enh-ruby) 199 | 200 | (defcustom enh-ruby-use-encoding-map t 201 | "*Use `enh-ruby-encoding-map' to set encoding magic comment if this is non-nil." 202 | :type 'boolean 203 | :safe #'booleanp 204 | :group 'enh-ruby) 205 | 206 | (defvar enh-ruby-use-ruby-mode-show-parens-config nil 207 | "This flag has no effect anymore as ERM supports command 208 | ‘show-paren-mode’ directly.") 209 | 210 | (make-obsolete-variable 'enh-ruby-use-ruby-mode-show-parens-config nil "2018-04-03") 211 | 212 | ;; TODO: renames: 213 | ;; 214 | ;; enh-ruby-indent-level: 2 215 | ;; enh-ruby-hanging-indent-level: 2 216 | ;; enh-ruby-hanging-brace-deep-indent-level: 0 217 | ;; enh-ruby-hanging-brace-indent-level: 2 218 | ;; enh-ruby-hanging-paren-deep-indent-level: 0 219 | ;; enh-ruby-hanging-paren-indent-level: 2 220 | ;; 221 | ;; Versus: 222 | ;; 223 | ;; enh-ruby-indent-level: 2 224 | ;; enh-ruby-indent-level-hanging: 2 225 | ;; enh-ruby-indent-level-hanging-paren: 2 226 | ;; enh-ruby-indent-level-hanging-paren-deep: 0 227 | ;; enh-ruby-indent-level-hanging-brace: 2 228 | ;; enh-ruby-indent-level-hanging-brace-deep: 0 229 | 230 | (defvar need-syntax-check-p) 231 | (defvar erm-buff-num) 232 | (defvar erm-e-w-status) 233 | (defvar erm-full-parse-p) 234 | 235 | ;;; Constants 236 | 237 | (defconst enh-ruby-block-end-re "\\_") 238 | 239 | (defconst enh-ruby-symbol-chars "a-zA-Z0-9_=?!") 240 | 241 | (defconst enh-ruby-symbol-re (concat "[" enh-ruby-symbol-chars "]")) 242 | 243 | (defconst enh-ruby-defun-beg-keywords 244 | '("class" "module" "def") 245 | "Keywords at the beginning of definitions.") 246 | 247 | (defconst enh-ruby-defun-beg-re 248 | (regexp-opt enh-ruby-defun-beg-keywords) 249 | "Regexp to match the beginning of definitions.") 250 | 251 | (defconst enh-ruby-defun-and-name-re 252 | (concat "\\(" enh-ruby-defun-beg-re "\\)[ \t]+\\(" 253 | ;; \\. and :: for class method 254 | "\\([A-Za-z_]" enh-ruby-symbol-re "*\\|\\.\\|::" "\\)" 255 | "+\\)") 256 | "Regexp to match definitions and their name.") 257 | 258 | (defconst erm-process-delimiter 259 | "\n\0\0\0\n") 260 | 261 | (define-abbrev-table 'enh-ruby-mode-abbrev-table () 262 | "Abbrev table used by enhanced-ruby-mode.") 263 | 264 | (define-abbrev enh-ruby-mode-abbrev-table "end" "end" 265 | #'indent-for-tab-command :system t) 266 | 267 | (defvar enh-ruby-mode-map 268 | (let ((map (make-sparse-keymap))) 269 | (define-key map "{" #'enh-ruby-electric-brace) 270 | (define-key map "}" #'enh-ruby-electric-brace) 271 | (define-key map (kbd "M-C-a") #'enh-ruby-beginning-of-defun) 272 | (define-key map (kbd "M-C-e") #'enh-ruby-end-of-defun) 273 | (define-key map (kbd "M-C-b") #'enh-ruby-backward-sexp) 274 | (define-key map (kbd "M-C-f") #'enh-ruby-forward-sexp) 275 | (define-key map (kbd "M-C-p") #'enh-ruby-beginning-of-block) 276 | (define-key map (kbd "M-C-n") #'enh-ruby-end-of-block) 277 | (define-key map (kbd "M-C-h") #'enh-ruby-mark-defun) 278 | (define-key map (kbd "M-C-q") #'enh-ruby-indent-exp) 279 | (define-key map (kbd "C-c C-f") #'enh-ruby-find-file) 280 | (define-key map (kbd "C-c C-e") #'enh-ruby-find-error) 281 | (define-key map (kbd "C-c /") #'enh-ruby-insert-end) 282 | (define-key map (kbd "C-c {") #'enh-ruby-toggle-block) 283 | (define-key map (kbd "M-C-u") #'enh-ruby-up-sexp) 284 | (define-key map (kbd "C-j") #'reindent-then-newline-and-indent) 285 | map) 286 | "Syntax table in use in ‘enh-ruby-mode’ buffers.") 287 | 288 | (defvar enh-ruby-mode-syntax-table 289 | (let ((table (make-syntax-table))) 290 | (modify-syntax-entry ?\' "\"" table) 291 | (modify-syntax-entry ?\" "\"" table) 292 | (modify-syntax-entry ?\` "\"" table) 293 | (modify-syntax-entry ?# "<" table) 294 | (modify-syntax-entry ?\n ">" table) 295 | (modify-syntax-entry ?\\ "\\" table) 296 | (modify-syntax-entry ?$ "'" table) 297 | (modify-syntax-entry ?? "_" table) 298 | (modify-syntax-entry ?_ "_" table) 299 | (modify-syntax-entry ?: "'" table) 300 | (modify-syntax-entry ?< "." table) 301 | (modify-syntax-entry ?> "." table) 302 | (modify-syntax-entry ?& "." table) 303 | (modify-syntax-entry ?| "." table) 304 | (modify-syntax-entry ?% "." table) 305 | (modify-syntax-entry ?= "." table) 306 | (modify-syntax-entry ?/ "." table) 307 | (modify-syntax-entry ?+ "." table) 308 | (modify-syntax-entry ?* "." table) 309 | (modify-syntax-entry ?- "." table) 310 | (modify-syntax-entry ?\; "." table) 311 | (modify-syntax-entry ?\( "()" table) 312 | (modify-syntax-entry ?\) ")(" table) 313 | (modify-syntax-entry ?\{ "(}" table) 314 | (modify-syntax-entry ?\} "){" table) 315 | (modify-syntax-entry ?\[ "(]" table) 316 | (modify-syntax-entry ?\] ")[" table) 317 | (modify-syntax-entry ?@ "'" table) 318 | 319 | table) 320 | "Syntax table used by ‘enh-ruby-mode’ buffers.") 321 | 322 | (defconst enh-ruby-font-lock-keyword-beg-re "\\(?:^\\|[^.@$:]\\|\\.\\.\\)") 323 | 324 | (defconst enh-ruby-font-lock-keywords 325 | `(;; Core methods that have required arguments. 326 | (,(concat 327 | enh-ruby-font-lock-keyword-beg-re 328 | (regexp-opt 329 | '( ;; built-in methods on Kernel TODO: add more via reflection? 330 | "at_exit" 331 | "autoload" 332 | "autoload?" 333 | "callcc" 334 | "catch" 335 | "eval" 336 | "exec" 337 | "format" 338 | "lambda" 339 | "load" 340 | "loop" 341 | "open" 342 | "p" 343 | "print" 344 | "printf" 345 | "proc" 346 | "putc" 347 | "puts" 348 | "require" 349 | "require_relative" 350 | "spawn" 351 | "sprintf" 352 | "syscall" 353 | "system" 354 | "throw" 355 | "trace_var" 356 | "trap" 357 | "untrace_var" 358 | "warn" 359 | ;; keyword-like private methods on Module 360 | "alias_method" 361 | "attr" 362 | "attr_accessor" 363 | "attr_reader" 364 | "attr_writer" 365 | "define_method" 366 | "extend" 367 | "include" 368 | "module_function" 369 | "prepend" 370 | "private_class_method" 371 | "private_constant" 372 | "public_class_method" 373 | "public_constant" 374 | "refine" 375 | "using") 376 | 'symbols)) 377 | (1 (unless (looking-at " *\\(?:[]|,.)}=]\\|$\\)") 378 | font-lock-builtin-face))) 379 | ;; Kernel methods that have no required arguments. 380 | (,(concat 381 | enh-ruby-font-lock-keyword-beg-re 382 | (regexp-opt 383 | '("__callee__" 384 | "__dir__" 385 | "__method__" 386 | "abort" 387 | "binding" 388 | "block_given?" 389 | "caller" 390 | "exit" 391 | "exit!" 392 | "fail" 393 | "fork" 394 | "global_variables" 395 | "local_variables" 396 | "private" 397 | "protected" 398 | "public" 399 | "raise" 400 | "rand" 401 | "readline" 402 | "readlines" 403 | "sleep" 404 | "srand") 405 | 'symbols)) 406 | (1 font-lock-builtin-face))) 407 | "Additional expressions to highlight in ‘enh-ruby-mode’.") 408 | 409 | (defconst enh-ruby-font-names 410 | '(nil 411 | font-lock-string-face 412 | font-lock-type-face 413 | font-lock-variable-name-face 414 | font-lock-comment-face 415 | font-lock-constant-face 416 | font-lock-string-face 417 | enh-ruby-string-delimiter-face 418 | enh-ruby-regexp-delimiter-face 419 | font-lock-function-name-face 420 | font-lock-keyword-face 421 | enh-ruby-heredoc-delimiter-face 422 | enh-ruby-op-face 423 | enh-ruby-regexp-face) 424 | "Font faces used by ‘enh-ruby-mode’.") 425 | 426 | ;;; Code: 427 | 428 | ;;;###autoload 429 | (defun enh-symbol-or-null-p (x) 430 | "Return true if X is either a symbol or null. Used for defcustom safe check." 431 | (or (symbolp x) 432 | (null x))) 433 | 434 | (define-obsolete-variable-alias 'enh/symbol-or-null-p 435 | 'enh-symbol-or-null-p "2022-07-07") 436 | 437 | ;;;###autoload 438 | (define-derived-mode enh-ruby-mode prog-mode "EnhRuby" 439 | "Enhanced Major mode for editing Ruby code. 440 | 441 | \\{enh-ruby-mode-map}" 442 | 443 | (setq-local comment-column enh-ruby-comment-column) 444 | (setq-local comment-end "") 445 | (setq-local comment-start "#") 446 | (setq-local comment-start-skip "#+ *") 447 | (setq-local erm-buff-num nil) 448 | (setq-local erm-e-w-status nil) 449 | (setq-local erm-full-parse-p nil) 450 | (setq-local indent-line-function #'enh-ruby-indent-line) 451 | ;; (setq-local forward-sexp-function #'enh-ruby-forward-sexp) 452 | (setq-local need-syntax-check-p nil) 453 | (setq-local paragraph-ignore-fill-prefix t) 454 | (setq-local parse-sexp-ignore-comments t) 455 | (setq-local parse-sexp-lookup-properties t) 456 | (setq-local require-final-newline mode-require-final-newline) 457 | (setq-local beginning-of-defun-function #'enh-ruby-beginning-of-defun) 458 | (setq-local end-of-defun-function #'enh-ruby-end-of-defun) 459 | (setq-local show-paren-data-function #'erm-show-paren-data-function) 460 | (setq-local paragraph-start (concat "$\\|" page-delimiter)) 461 | (setq-local paragraph-separate paragraph-start) 462 | 463 | (setq-local add-log-current-defun-function 464 | 'enh-ruby-add-log-current-method) 465 | 466 | (setq-local font-lock-keywords enh-ruby-font-lock-keywords) 467 | (setq font-lock-defaults '((enh-ruby-font-lock-keywords) t)) 468 | (setq indent-tabs-mode enh-ruby-indent-tabs-mode) 469 | (setq imenu-create-index-function #'enh-ruby-imenu-create-index) 470 | 471 | (if enh-ruby-add-encoding-comment-on-save 472 | (add-hook 'before-save-hook #'enh-ruby-mode-set-encoding nil t)) 473 | 474 | (add-hook 'change-major-mode-hook #'erm-major-mode-changed nil t) 475 | (add-hook 'kill-buffer-hook #'erm-buffer-killed nil t) 476 | 477 | (abbrev-mode) 478 | (erm-reset-buffer)) 479 | 480 | ;;; Faces: 481 | 482 | (require 'color nil t) 483 | 484 | (defun erm-darken-color (name) 485 | "Return color NAME with foreground 20% darker." 486 | (color-darken-name (face-attribute name :foreground) 20)) 487 | 488 | (defun erm-define-faces () 489 | "Define faces for ‘enh-ruby-mode’." 490 | 491 | (defface enh-ruby-string-delimiter-face 492 | `((t :foreground ,(erm-darken-color font-lock-string-face))) 493 | "Face used to highlight string delimiters like quotes and %Q." 494 | :group 'enh-ruby) 495 | 496 | (defface enh-ruby-heredoc-delimiter-face 497 | `((t :foreground ,(erm-darken-color font-lock-string-face))) 498 | "Face used to highlight string heredoc anchor strings like < (length erm-response) 5) 885 | (string= erm-process-delimiter (substring erm-response -5 nil))) 886 | (setq response (substring erm-response 0 -5)) 887 | (setq erm-response "") 888 | (unless (buffer-live-p erm-parse-buff) 889 | (erm-reset)) 890 | (when (buffer-live-p erm-parse-buff) 891 | (with-current-buffer erm-parse-buff 892 | (erm-with-unmodifying-text-property-changes 893 | (erm-parse response)))))) 894 | 895 | (defun erm-ready () 896 | (if erm-full-parse-p 897 | (enh-ruby-fontify-buffer) 898 | (setq erm-parsing-p t) 899 | (process-send-string (erm-ruby-get-process) (erm-proc-string "g")))) 900 | 901 | (defun extra-col-% () 902 | "Return extra column adjustments in case we are ‘looking-at’ a % construct." 903 | (or (and (looking-at "%\\([^[:alnum:]]\\|[QqWwIixrs].\\)") 904 | (1- (length (match-string-no-properties 0)))) 905 | 0)) 906 | 907 | (defun enh-ruby-continue-p (prop) 908 | "Return whether PROP is a continue property." 909 | (eq 'c prop)) 910 | 911 | (defun enh-ruby-block-p (prop) 912 | "Return whether PROP is a block property." 913 | (eq 'd prop)) 914 | 915 | (defun enh-ruby-point-continue-p (point) 916 | "Return whether property at POINT is a continue property." 917 | (enh-ruby-continue-p (get-text-property point 'indent))) 918 | 919 | (defun enh-ruby-point-block-p (&optional point) 920 | "Return whether property at POINT is a block property." 921 | (or point (setq point (point))) 922 | (enh-ruby-block-p (get-text-property point 'indent))) 923 | 924 | (defun enh-ruby-calculate-indent (&optional start-point) 925 | "Calculate the indentation of the previous line and its level at START-POINT." 926 | (save-excursion 927 | (when start-point (goto-char start-point)) 928 | (if (bobp) 929 | 0 930 | (forward-line 0) 931 | (skip-syntax-forward " " (line-end-position)) 932 | (let ((pos (line-beginning-position)) 933 | (prop (get-text-property (point) 'indent)) 934 | (face (get-text-property (point) 'font-lock-face))) 935 | (cond 936 | ((or (eq 'e prop) (eq 's prop)) 937 | (when (eq 's prop) (forward-char)) 938 | (enh-ruby-backward-sexp) 939 | (let ((bprop (get-text-property (point) 'indent))) 940 | (cond ((eq 'd bprop) 941 | (setq pos (point)) 942 | (enh-ruby-skip-non-indentable) 943 | (let ((indent (enh-ruby-calculate-indent-1 pos (line-beginning-position))) 944 | (chained-stmt-p (save-excursion 945 | (forward-line 0) 946 | (enh-ruby-point-continue-p (point))))) 947 | (+ indent 948 | (if chained-stmt-p enh-ruby-hanging-indent-level 0)))) 949 | ((and (not enh-ruby-deep-indent-construct) 950 | (eq 'b bprop)) 951 | (current-indentation)) 952 | (t 953 | (current-column))))) 954 | ((eq 'r prop) ; TODO: make these consistent file-wide 955 | (let (opening-col opening-is-last-thing-on-line) 956 | (save-excursion 957 | (enh-ruby-backward-sexp) 958 | (setq opening-col (+ (current-column) 959 | (extra-col-%))) 960 | (forward-char 1) 961 | (skip-syntax-forward " " (line-end-position)) 962 | (setq opening-is-last-thing-on-line (eolp))) 963 | (if (and enh-ruby-deep-indent-paren 964 | (not enh-ruby-bounce-deep-indent) 965 | (not opening-is-last-thing-on-line)) 966 | opening-col ; deep + !bounce + !hanging = match open 967 | (forward-line -1) 968 | (enh-ruby-skip-non-indentable) 969 | (let* ((opening-char (save-excursion 970 | (enh-ruby-backward-sexp) 971 | (char-after))) 972 | (proposed-col (enh-ruby-calculate-indent-1 pos 973 | (line-beginning-position))) 974 | (chained-stmt-p (save-excursion (enh-ruby-backward-sexp) 975 | (forward-line 0) 976 | (enh-ruby-point-continue-p (point)))) 977 | (offset (if (char-equal opening-char ?{) 978 | enh-ruby-hanging-brace-indent-level 979 | enh-ruby-hanging-paren-indent-level))) 980 | (cond ((and chained-stmt-p 981 | (not enh-ruby-bounce-deep-indent)) 982 | (- proposed-col offset)) 983 | ((< proposed-col opening-col) 984 | (- proposed-col offset)) 985 | (t opening-col)))))) 986 | 987 | ((or (memq face '(font-lock-string-face enh-ruby-heredoc-delimiter-face)) 988 | (and (eq 'font-lock-variable-name-face face) 989 | (looking-at "#"))) 990 | (when enh-ruby-preserve-indent-in-heredocs 991 | (forward-line -1) 992 | (back-to-indentation)) 993 | (current-column)) 994 | 995 | (t 996 | (forward-line -1) 997 | 998 | (enh-ruby-skip-non-indentable) 999 | (enh-ruby-calculate-indent-1 pos (line-beginning-position)))))))) 1000 | 1001 | (defun erm-looking-at-not-indentable () 1002 | (skip-syntax-forward " " (line-end-position)) 1003 | (let ((face (get-text-property (point) 'font-lock-face))) 1004 | (or (= (point) (line-end-position)) 1005 | (memq face '(font-lock-string-face font-lock-comment-face enh-ruby-heredoc-delimiter-face)) 1006 | (and (eq 'font-lock-variable-name-face face) 1007 | (looking-at "#")) 1008 | (and (memq face '(enh-ruby-regexp-delimiter-face enh-ruby-string-delimiter-face)) 1009 | (> (point) (point-min)) 1010 | (eq (get-text-property (1- (point)) 'font-lock-face) 1011 | 'font-lock-string-face))))) 1012 | 1013 | (defun enh-ruby-skip-non-indentable () 1014 | (forward-line 0) 1015 | (while (and (> (point) (point-min)) 1016 | (erm-looking-at-not-indentable)) 1017 | (skip-chars-backward " \n\t\r\v\f") 1018 | (forward-line 0))) 1019 | 1020 | (defvar enh-ruby-last-bounce-line nil 1021 | "The last line that `erm-bounce-deep-indent-paren` was run against.") 1022 | 1023 | (defvar enh-ruby-last-bounce-deep nil 1024 | "The last result from `erm-bounce-deep-indent-paren`.") 1025 | 1026 | (defun enh-ruby-calculate-indent-1 (limit pos) 1027 | (goto-char pos) 1028 | 1029 | (let* ((start-pos pos) 1030 | (start-prop (get-text-property pos 'indent)) 1031 | (prop start-prop) 1032 | (indent (- (current-indentation) 1033 | (if (eq 'c prop) enh-ruby-hanging-indent-level 0))) 1034 | (nbc 0) 1035 | (npc 0) 1036 | col max bc pc) 1037 | 1038 | (setq enh-ruby-last-bounce-deep 1039 | (and (eq enh-ruby-last-bounce-line (line-number-at-pos)) 1040 | (not enh-ruby-last-bounce-deep))) 1041 | (setq enh-ruby-last-bounce-line (line-number-at-pos)) 1042 | 1043 | (while (< pos limit) 1044 | (unless prop 1045 | (setq pos (next-single-property-change pos 'indent (current-buffer) limit)) 1046 | (when (< pos limit) 1047 | (setq prop (get-text-property pos 'indent)))) 1048 | (setq col (- pos start-pos -1)) 1049 | 1050 | (cond 1051 | ;; 'l - [, (, {, %w/%i open or | goalpost open 1052 | ;; 'r - ], ), }, %w/%i close or | goalpost close 1053 | ;; 'b - begin, def, case, if 1054 | ;; 'd - do, {, embexpr (interpolation) start 1055 | ;; 'e - end, embexpr (interpolation) end, close block } 1056 | ;; 's - statement start on BACKDENT_KW else/when/rescue etc 1057 | ;; 'c - continue - period followed by return (or other way around?) 1058 | ((memq prop '(l)) 1059 | (let ((shallow-indent 1060 | (if (char-equal (char-after pos) ?{) 1061 | (+ enh-ruby-hanging-brace-indent-level indent) 1062 | (+ enh-ruby-hanging-paren-indent-level indent))) 1063 | (deep-indent 1064 | (cond ((char-equal (char-after pos) ?{) 1065 | (+ enh-ruby-hanging-brace-deep-indent-level col)) 1066 | ((char-equal (char-after pos) ?%) 1067 | (+ enh-ruby-hanging-brace-deep-indent-level 1068 | col 1069 | (save-excursion 1070 | (goto-char pos) 1071 | (extra-col-%)))) 1072 | (t (+ enh-ruby-hanging-paren-deep-indent-level col)))) 1073 | (at-eol (save-excursion 1074 | (goto-char (1+ pos)) 1075 | (skip-syntax-forward " " (line-end-position)) 1076 | (eolp)))) 1077 | (if enh-ruby-bounce-deep-indent 1078 | (setq pc (cons (if enh-ruby-last-bounce-deep 1079 | shallow-indent 1080 | deep-indent) 1081 | pc)) 1082 | (setq pc (cons (if (and (not at-eol) enh-ruby-deep-indent-paren) 1083 | deep-indent 1084 | (let ((chained-stmt-p (enh-ruby-continue-p start-prop))) 1085 | (+ shallow-indent (if chained-stmt-p enh-ruby-hanging-paren-indent-level 0)))) 1086 | pc))))) 1087 | 1088 | ((eq prop 'r) 1089 | (if pc (setq pc (cdr pc)) (setq npc col))) 1090 | 1091 | ((memq prop '(b d s)) 1092 | (and (not enh-ruby-deep-indent-construct) 1093 | (eq prop 'b) 1094 | (setq col 1095 | (- col (- (save-excursion 1096 | (goto-char pos) 1097 | (current-column)) 1098 | (current-indentation))))) 1099 | (setq bc (cons col bc))) 1100 | 1101 | ((eq prop 'e) 1102 | (if bc 1103 | (setq bc (cdr bc)) 1104 | (setq nbc col)))) 1105 | 1106 | (when (< (setq pos (1+ pos)) limit) 1107 | (setq prop (get-text-property pos 'indent)))) 1108 | 1109 | ;;(prin1 (list indent nbc bc npc pc)) 1110 | (setq pc (or (car pc) 0)) 1111 | (setq bc (or (car bc) 0)) 1112 | (setq max (max pc bc nbc npc)) 1113 | 1114 | (+ 1115 | (if (eq 'c (get-text-property limit 'indent)) enh-ruby-hanging-indent-level 0) 1116 | (cond 1117 | ((= max 0) 1118 | (if (not (memq (get-text-property start-pos 'font-lock-face) 1119 | '(enh-ruby-heredoc-delimiter-face font-lock-string-face))) 1120 | indent 1121 | (goto-char (or (enh-ruby-string-start-pos start-pos) limit)) 1122 | (current-indentation))) 1123 | 1124 | ((= max pc) (if (eq 'c (get-text-property limit 'indent)) 1125 | (- pc enh-ruby-hanging-indent-level) 1126 | pc)) 1127 | 1128 | ((= max bc) 1129 | (if (eq 'd (get-text-property (+ start-pos bc -1) 'indent)) 1130 | (let ((chained-stmt-p (enh-ruby-continue-p start-prop))) 1131 | (+ (enh-ruby-calculate-indent-1 (+ start-pos bc -1) start-pos) 1132 | (* (if chained-stmt-p 2 1) enh-ruby-indent-level))) 1133 | (+ bc enh-ruby-indent-level -1))) 1134 | 1135 | ((= max npc) 1136 | (goto-char (+ start-pos npc)) 1137 | (enh-ruby-backward-sexp) 1138 | (enh-ruby-calculate-indent-1 (point) (line-beginning-position))) 1139 | 1140 | ((= max nbc) 1141 | (goto-char (+ start-pos nbc -1)) 1142 | (enh-ruby-backward-sexp) 1143 | (enh-ruby-calculate-indent-1 (point) (line-beginning-position))) 1144 | 1145 | (t 0))))) 1146 | 1147 | (defun enh-ruby-string-start-pos (pos) 1148 | (when (< 0 (or (setq pos (previous-single-property-change pos 'font-lock-face)) 0)) 1149 | (previous-single-property-change pos 'font-lock-face))) 1150 | 1151 | (defun enh-ruby-show-errors-at (pos face) 1152 | (let ((overlays (overlays-at pos)) 1153 | overlay 1154 | messages) 1155 | 1156 | ;; TODO: 1157 | ;; (-map (lambda (o) (overlay-get o 'help-echo)) 1158 | ;; (-filter (lambda (o) (and (overlay-get o 'erm-syn-overlay) 1159 | ;; (eq (overlay-get o 'font-lock-face) face))))) 1160 | 1161 | (while overlays 1162 | (setq overlay (car overlays)) 1163 | (when (and (overlay-get overlay 'erm-syn-overlay) 1164 | (eq (overlay-get overlay 'font-lock-face) face)) 1165 | (setq messages (cons (overlay-get overlay 'help-echo) messages))) 1166 | (setq overlays (cdr overlays))) 1167 | 1168 | (message "%s" (mapconcat #'identity messages "\n")) 1169 | messages)) 1170 | 1171 | (defun enh-ruby-find-error (&optional warnings) 1172 | "Search back, then forward for a syntax error/warning. Display 1173 | contents in mini-buffer. Optional WARNINGS will highlight 1174 | warnings instead of errors. (I think)." 1175 | (interactive "^P") 1176 | (let (messages 1177 | (face (if warnings 'erm-syn-warnline 'erm-syn-errline)) 1178 | (pos (point))) 1179 | (unless (eq last-command #'enh-ruby-find-error) 1180 | (while (and (not messages) (> pos (point-min))) 1181 | (setq messages (enh-ruby-show-errors-at (setq pos (previous-overlay-change pos)) face)))) 1182 | 1183 | (unless messages 1184 | (while (and (not messages) (< pos (point-max))) 1185 | (setq messages (enh-ruby-show-errors-at (setq pos (next-overlay-change pos)) face)))) 1186 | 1187 | (if messages 1188 | (goto-char pos) 1189 | (unless warnings 1190 | (enh-ruby-find-error t))))) 1191 | 1192 | (defun enh-ruby-find-file (filename) 1193 | "Search for and edit FILENAME. Searching is done with `gem 1194 | which` but works for standard lib as well as gems." 1195 | (interactive "sgem which ") 1196 | (let* ((command (concat "gem which " filename)) 1197 | (output (shell-command-to-string command)) 1198 | (path (string-trim-right output))) 1199 | (if (file-exists-p path) 1200 | (find-file path) 1201 | (message "%S found nothing" command)))) 1202 | 1203 | (defun enh-ruby-up-sexp (&optional arg) 1204 | "Move up one balanced expression (sexp). 1205 | With ARG, do it that many times." 1206 | (interactive "^p") 1207 | (unless arg (setq arg 1)) 1208 | (while (>= (setq arg (1- arg)) 0) 1209 | (let* ((count 1) 1210 | prop) 1211 | (goto-char 1212 | (save-excursion 1213 | (while (and (not (= (point) (point-min))) 1214 | (< 0 count)) 1215 | (goto-char (enh-ruby-previous-indent-change (point))) 1216 | (setq prop (get-text-property (point) 'indent)) 1217 | (setq count (cond 1218 | ((or (eq prop 'l) (eq prop 'b) (eq prop 'd)) (1- count)) 1219 | ((or (eq prop 'r) (eq prop 'e)) (1+ count)) 1220 | (t count)))) 1221 | (point)))))) 1222 | 1223 | (defun enh-ruby-beginning-of-defun (&optional arg) 1224 | "Move backward across expression (sexp) looking for a definition beginning. 1225 | With ARG, do it that many times." 1226 | (interactive "^p") 1227 | (unless arg (setq arg 1)) 1228 | (let (prop) 1229 | (goto-char 1230 | (save-excursion 1231 | (while (>= (setq arg (1- arg)) 0) 1232 | (while (and 1233 | (> (point) (point-min)) 1234 | (progn 1235 | (enh-ruby-backward-sexp 1) 1236 | (setq prop (get-text-property (point) 'indent)) 1237 | (not (and (eq prop 'b) (looking-at enh-ruby-defun-beg-re))))))) 1238 | (point))))) 1239 | 1240 | (defun enh-ruby-mark-defun () 1241 | "Put mark at end of this Ruby definition, point at beginning." 1242 | (interactive) 1243 | (push-mark (point)) 1244 | (enh-ruby-beginning-of-defun 1) 1245 | (enh-ruby-forward-sexp 1) 1246 | (forward-line 1) 1247 | (push-mark (point) nil t) 1248 | (forward-line -1) 1249 | (end-of-line) 1250 | (enh-ruby-backward-sexp 1) 1251 | (forward-line 0)) 1252 | 1253 | (defun enh-ruby-indent-exp (&optional _shutup-p) 1254 | "Indent each line in the balanced expression following point syntactically." 1255 | (interactive "*P") 1256 | (erm-wait-for-parse) 1257 | (let ((end-pos (save-excursion (enh-ruby-forward-sexp 1) (point)))) 1258 | (indent-region (point) end-pos))) 1259 | 1260 | (set-advertised-calling-convention 'enh-ruby-indent-exp '() "2022-04-26") 1261 | 1262 | (defun enh-ruby-beginning-of-block (&optional arg) 1263 | "Move backward across one expression (sexp) looking for a block beginning. 1264 | With ARG, do it that many times." 1265 | (interactive "^p") 1266 | (unless arg (setq arg 1)) 1267 | (let (prop 1268 | pos) 1269 | (goto-char 1270 | (save-excursion 1271 | (while (>= (setq arg (1- arg)) 0) 1272 | (while (progn 1273 | (enh-ruby-backward-sexp 1) 1274 | (setq pos (point)) 1275 | (setq prop (get-text-property pos 'indent)) 1276 | (and 1277 | (> pos (point-min)) 1278 | (not (or (eq prop 'b) (eq prop 'd))))))) 1279 | (point))))) 1280 | 1281 | (defun enh-ruby-end-of-defun (&optional arg) 1282 | "Move forwards across one expression (sexp) looking for a definition end. 1283 | With ARG, do it that many times." 1284 | (interactive "^p") 1285 | (unless arg (setq arg 1)) 1286 | (let (prop) 1287 | (while (>= (setq arg (1- arg)) 0) 1288 | (while (and 1289 | (< (point) (point-max)) 1290 | (progn 1291 | (enh-ruby-forward-sexp 1) 1292 | (setq prop (get-text-property (- (point) 3) 'indent)) 1293 | (not (and (eq prop 'e) 1294 | (save-excursion 1295 | (enh-ruby-backward-sexp 1) 1296 | (looking-at enh-ruby-defun-beg-re)))))))) 1297 | (point))) 1298 | 1299 | (defun enh-ruby-end-of-block (&optional arg) 1300 | "Move forwards across one balanced expression (sexp) looking for a block end. 1301 | With ARG, do it that many times." 1302 | ;; this is totally broken for {} blocks! see the -3 below 1303 | (interactive "^p") 1304 | (unless arg (setq arg 1)) 1305 | (let (prop 1306 | pos) 1307 | (goto-char 1308 | (save-excursion 1309 | (while (>= (setq arg (1- arg)) 0) 1310 | (while (progn 1311 | (enh-ruby-forward-sexp 1) 1312 | (setq pos (point)) 1313 | (setq prop (get-text-property (- pos 3) 'indent)) 1314 | (and (< pos (point-max)) (not (or (eq prop 'e) ; closers 1315 | (eq prop 'r)) 1316 | ))))) 1317 | (point))))) 1318 | 1319 | (defun enh-ruby-backward-sexp (&optional arg) 1320 | "Move backward across one balanced expression (sexp). 1321 | With ARG, do it that many times." 1322 | (interactive "^p") 1323 | 1324 | (unless arg (setq arg 1)) 1325 | (while (>= (setq arg (1- arg)) 0) 1326 | (let* ((pos (point)) 1327 | (prop (get-text-property pos 'indent)) 1328 | (count 0)) 1329 | 1330 | (unless (memq prop '(r e)) 1331 | (setq prop (and (setq pos (enh-ruby-previous-indent-change pos)) 1332 | (goto-char pos) ;; TODO: remove? 1333 | (get-text-property pos 'indent)))) 1334 | 1335 | (while (< 0 (setq count 1336 | (cond 1337 | ((memq prop '(l b d)) (1- count)) 1338 | ((memq prop '(r e)) (1+ count)) 1339 | ((eq prop 'c) count) 1340 | ((eq prop 's) (if (= 0 count) 1 count)) 1341 | (t 0)))) 1342 | (goto-char pos) 1343 | (setq prop (and (setq pos (enh-ruby-previous-indent-change pos)) 1344 | (get-text-property pos 'indent)))) 1345 | 1346 | (goto-char (if prop pos (point-min)))))) 1347 | 1348 | ;; 'l - [, (, {, %w/%i open or | goalpost open 1349 | ;; 'r - ], ), }, %w/%i close or | goalpost close 1350 | ;; 'b - begin, def, case, if 1351 | ;; 'd - do, {, embexpr (interpolation) start 1352 | ;; 'e - end, embexpr (interpolation) end, close block } 1353 | ;; 's - statement start on BACKDENT_KW else/when/rescue etc 1354 | ;; 'c - continue - period followed by return (or other way around?) 1355 | 1356 | ;; backwards: l b d s? 1357 | ;; forwards r e 1358 | 1359 | ;; C-M-a enh-ruby-beginning-of-defun 1360 | ;; C-M-p enh-ruby-beginning-of-block 1361 | ;; C-M-e enh-ruby-end-of-defun 1362 | ;; C-M-n enh-ruby-end-of-block 1363 | ;; C-M-q enh-ruby-indent-exp 1364 | ;; C-M-h enh-ruby-mark-defun 1365 | ;; C-M-u enh-ruby-up-sexp 1366 | 1367 | (defun enh-ruby-forward-sexp (&optional arg) 1368 | "Move backward across one balanced expression (sexp). 1369 | With ARG, do it that many times." 1370 | (interactive "^p") 1371 | (unless arg (setq arg 1)) 1372 | (let ((i (or arg 1))) 1373 | (cond 1374 | ((< i 0) 1375 | (enh-ruby-backward-sexp (- i))) 1376 | (t 1377 | (skip-syntax-forward " ") 1378 | (while (> i 0) 1379 | (let* ((pos (point)) 1380 | (prop (get-text-property pos 'indent)) 1381 | (count 0)) 1382 | 1383 | (unless (memq prop '(l b d)) 1384 | (setq prop (and (setq pos (enh-ruby-next-indent-change pos)) 1385 | (get-text-property pos 'indent)))) 1386 | 1387 | (while (< 0 (setq count 1388 | (cond 1389 | ((memq prop '(l b d)) (1+ count)) 1390 | ((memq prop '(r e)) (1- count)) 1391 | ((memq prop '(c)) count) 1392 | ((memq prop '(s)) (if (= 0 count) 1 count)) 1393 | (t 0)))) 1394 | (goto-char pos) 1395 | (setq prop (and (setq pos (enh-ruby-next-indent-change pos)) 1396 | (get-text-property pos 'indent)))) 1397 | 1398 | (goto-char (if prop pos (point-max))) 1399 | 1400 | (cond ((looking-at "end") ; move past end/}/]/) 1401 | (forward-word 1)) 1402 | ((looking-at "[]})]") 1403 | (forward-char 1)))) 1404 | 1405 | (setq i (1- i))))))) 1406 | 1407 | (defun enh-ruby-insert-end () 1408 | (interactive) 1409 | (let ((text (save-excursion 1410 | (forward-line 0) 1411 | (if (looking-at "^[ \t]*$") 1412 | "end" 1413 | (if (looking-at ".*{[^}]*$") 1414 | "\n}" 1415 | "\nend"))))) 1416 | (insert text) 1417 | (enh-ruby-indent-line))) 1418 | 1419 | (defun enh-ruby-previous-indent-change (pos) 1420 | (and pos 1421 | (setq pos (1- pos)) 1422 | (>= pos (point-min)) 1423 | (or (and (get-text-property pos 'indent) 1424 | pos) 1425 | (and (> pos (point-min)) 1426 | (get-text-property (1- pos) 'indent) 1427 | (1- pos)) 1428 | (enh-ruby-previous-indent-change (previous-single-property-change pos 'indent)) 1429 | (point-min)))) 1430 | 1431 | (defun enh-ruby-next-indent-change (pos) 1432 | (and pos (setq pos (1+ pos)) 1433 | (<= pos (point-max)) 1434 | (or (and (get-text-property pos 'indent) pos) 1435 | (and (< pos (point-max)) 1436 | (get-text-property (1+ pos) 'indent) 1437 | (1+ pos)) 1438 | (next-single-property-change pos 'indent)))) 1439 | 1440 | (defun enh-ruby-indent-line (&optional _ignored) 1441 | "Correct indentation of the current ruby line." 1442 | (erm-wait-for-parse) 1443 | (unwind-protect 1444 | (progn 1445 | (setq erm-no-parse-needed-p t) 1446 | (enh-ruby-indent-to (enh-ruby-calculate-indent))) 1447 | (setq erm-no-parse-needed-p nil))) 1448 | 1449 | (defun enh-ruby-indent-to (indent) 1450 | "Indent the current line until INDENT is reached." 1451 | (unless (= (current-indentation) indent) 1452 | (save-excursion 1453 | (beginning-of-line) 1454 | (let ((pos (point)) 1455 | (prop (get-text-property (point) 'indent))) 1456 | (delete-horizontal-space) 1457 | (indent-to indent) 1458 | (if (eq 'c prop) (put-text-property pos (1+ pos) 'indent 'c))))) 1459 | 1460 | (if (< (current-column) (current-indentation)) 1461 | (back-to-indentation))) 1462 | 1463 | (defun enh-ruby-add-faces (list) 1464 | (let* ((ipos (car list)) 1465 | (buf-size (car ipos)) 1466 | (istart (cadr ipos)) 1467 | (iend (cl-caddr ipos)) 1468 | (rpos (cdr (cadr list)))) 1469 | 1470 | (unless (and (= (buffer-size) buf-size)) 1471 | (throw 'interrupted t)) 1472 | 1473 | (if (or (/= (point-min) istart) (/= (point-max) iend)) 1474 | (setq erm-full-parse-p t) 1475 | 1476 | (when (> iend 0) 1477 | (remove-text-properties istart iend '(indent nil)) 1478 | 1479 | (setq ipos (cl-cdddr ipos)) 1480 | 1481 | (while ipos 1482 | (put-text-property (cadr ipos) (1+ (cadr ipos)) 'indent (car ipos)) 1483 | (setq ipos (cddr ipos))) 1484 | 1485 | (while rpos 1486 | (remove-text-properties (car rpos) (cadr rpos) '(font-lock-face nil)) 1487 | (setq rpos (cddr rpos)))) 1488 | 1489 | (while (setq list (cdr list)) 1490 | (let ((face (nth (caar list) enh-ruby-font-names)) 1491 | (pos (cdar list))) 1492 | (while pos 1493 | (put-text-property (car pos) (cadr pos) 'font-lock-face face) 1494 | (setq pos (cddr pos)))))))) 1495 | 1496 | (defun erm-syntax-response (response) 1497 | (save-excursion 1498 | (dolist (ol (overlays-in (point-min) (point-max))) 1499 | (when (and (overlayp ol) (overlay-get ol 'erm-syn-overlay)) 1500 | (delete-overlay ol))) 1501 | (goto-char (point-min)) 1502 | (let ((warn-count 0) 1503 | (error-count 0) 1504 | (e-w erm-e-w-status) 1505 | (last-line 1)) 1506 | (while (string-match ":\\([0-9]+\\): *\\(\\(warning\\)?[^\n]+\\)\n" response) 1507 | (let (beg end ov 1508 | (line-no (string-to-number (match-string 1 response))) 1509 | (msg (match-string 2 response)) 1510 | (face (if (string= "warning" (match-string 3 response)) 'erm-syn-warnline 'erm-syn-errline))) 1511 | (setq response (substring response (match-end 0))) 1512 | (forward-line (- line-no last-line)) 1513 | 1514 | (when (or (eq face 'erm-syn-errline) (eq enh-ruby-check-syntax 'errors-and-warnings)) 1515 | (if (and (not (eq ?: (string-to-char response))) 1516 | (string-match "\\`[^\n]*\n\\( *\\)\\^\n" response)) 1517 | (progn 1518 | (setq beg (point)) 1519 | (condition-case nil 1520 | (forward-char (length (match-string 1 response))) 1521 | (error (goto-char (point-max)))) 1522 | (setq end (point)) 1523 | 1524 | (condition-case nil 1525 | (progn 1526 | (backward-sexp) 1527 | (forward-sexp)) 1528 | 1529 | (error (back-to-indentation))) 1530 | (setq beg (if (>= (point) end) 1531 | (1- end) 1532 | (if (< (point) beg) 1533 | (if (>= beg end) (1- end) beg) 1534 | (point))))) 1535 | 1536 | (move-end-of-line nil) 1537 | (skip-chars-backward " \n\t\r\v\f") 1538 | (while (and (> (point) (point-min)) 1539 | (eq 'font-lock-comment-face (get-text-property (point) 'font-lock-face))) 1540 | (backward-char)) 1541 | (skip-chars-backward " \n\t\r\v\f") 1542 | (setq end (point)) 1543 | (back-to-indentation) 1544 | (setq beg (point))) 1545 | 1546 | (if (eq face 'erm-syn-warnline) 1547 | (setq warn-count (1+ warn-count)) 1548 | (setq error-count (1+ error-count))) 1549 | 1550 | (setq ov (make-overlay beg end nil t t)) 1551 | (overlay-put ov 'font-lock-face face) 1552 | (overlay-put ov 'help-echo msg) 1553 | (overlay-put ov 'erm-syn-overlay t) 1554 | (overlay-put ov 'priority (if (eq 'erm-syn-warnline face) 99 100))) 1555 | 1556 | (setq last-line line-no))) 1557 | (if (eq (+ error-count warn-count) 0) 1558 | (setq e-w nil) 1559 | (setq e-w (format ":%d/%d" error-count warn-count))) 1560 | (when (not (string= e-w erm-e-w-status)) 1561 | (setq erm-e-w-status e-w) 1562 | (force-mode-line-update))))) 1563 | 1564 | (defun erm-do-syntax-check () 1565 | (unless erm-parsing-p 1566 | (let ((buffer (car erm-syntax-check-list))) 1567 | (setq erm-syntax-check-list (cdr erm-syntax-check-list)) 1568 | (if (buffer-live-p buffer) 1569 | (with-current-buffer buffer 1570 | (when need-syntax-check-p 1571 | (setq need-syntax-check-p nil) 1572 | (setq erm-parsing-p t) 1573 | (process-send-string (erm-ruby-get-process) (erm-proc-string "c")))) 1574 | (if erm-syntax-check-list 1575 | (erm-do-syntax-check)))))) 1576 | 1577 | (defun erm-parse (response) 1578 | (let (interrupted-p 1579 | (cmd (aref response 0)) 1580 | (send-next-p (eq 'a erm-parsing-p))) 1581 | (setq erm-parsing-p nil) 1582 | (cond 1583 | ((eq ?\( cmd) 1584 | (setq interrupted-p 1585 | (condition-case nil 1586 | (catch 'interrupted 1587 | (if send-next-p 1588 | (erm-ready) 1589 | (enh-ruby-add-faces (car (read-from-string response)))) 1590 | nil) 1591 | (error t))) 1592 | (if interrupted-p 1593 | (setq erm-full-parse-p t) 1594 | 1595 | (if erm-full-parse-p 1596 | (enh-ruby-fontify-buffer) 1597 | (if (car erm-reparse-list) 1598 | (with-current-buffer (car erm-reparse-list) 1599 | (setq erm-reparse-list (cdr erm-reparse-list)) 1600 | (enh-ruby-fontify-buffer)) 1601 | (erm-do-syntax-check))))) 1602 | 1603 | ((eq ?c cmd) 1604 | (unless need-syntax-check-p 1605 | (erm-syntax-response (substring response 1))) 1606 | (erm-do-syntax-check)) 1607 | 1608 | (t 1609 | (setq erm-full-parse-p t) 1610 | (error "%s" (substring response 1)))))) 1611 | 1612 | (defun erm--end-p () 1613 | "Is point directly after a block closing \"end\"." 1614 | (let ((end-pos (- (point) 3))) 1615 | (and (>= end-pos (point-min)) 1616 | (string= "end" (buffer-substring end-pos (point))) 1617 | (eq (get-text-property end-pos 'indent) 'e)))) 1618 | 1619 | (defun erm-show-paren-data-function () 1620 | ;; First check if we are on opening ('b or 'd). We only care about 1621 | ;; the word openers "if", "do" etc (normal show-paren handles "{") 1622 | (if (and (memq (get-text-property (point) 'indent) '(b d)) 1623 | (looking-at "\\w")) 1624 | (save-excursion 1625 | (let ((opener-beg (point)) 1626 | (opener-end (save-excursion (forward-word) (point))) 1627 | (closer-end (progn (enh-ruby-forward-sexp 1) (point)))) 1628 | (list 1629 | opener-beg 1630 | opener-end 1631 | (save-excursion (skip-syntax-backward ")w") (point)) 1632 | closer-end 1633 | (not (erm--end-p))))) 1634 | ;; Now check if we are at a closer ("end") 1635 | (if (erm--end-p) 1636 | (let ((end-pos (point))) 1637 | (save-excursion 1638 | (enh-ruby-backward-sexp 1) 1639 | (list 1640 | (- end-pos 3) 1641 | end-pos 1642 | (point) 1643 | (save-excursion (skip-syntax-forward "(w") (point)) 1644 | (or (not (looking-at "\\w")) 1645 | (not (memq (get-text-property (point) 'indent) '(b d))))))) 1646 | (show-paren--default)))) 1647 | 1648 | ;;; Debugging / Bug Reporting: 1649 | 1650 | (defun enh-ruby--all-vars-with (pattern) 1651 | "Return all defcustom variables that match PATTERN. 1652 | Used for inserting file-local-variables and sending in bug reports." 1653 | (let (mode-vars) 1654 | (mapatoms (lambda (symbol) 1655 | (when (and (string-match-p pattern (symbol-name symbol)) 1656 | (get symbol 'standard-value)) 1657 | (add-to-list 'mode-vars symbol)))) 1658 | (sort mode-vars 1659 | (lambda (a b) (string< (symbol-name a) (symbol-name b)))))) 1660 | 1661 | (defun enh-ruby--variable-standard-p (sym) 1662 | (and (equal (custom-variable-state sym (symbol-value sym)) 1663 | 'standard) 1664 | (equal (symbol-value sym) 1665 | (default-value sym)))) 1666 | 1667 | (defun enh-ruby--changed-vars-with (pattern) 1668 | "Return all changed defcustom variables that match PATTERN. 1669 | Used for inserting file-local-variables and sending in bug reports." 1670 | (seq-remove 1671 | #'enh-ruby--variable-standard-p 1672 | (enh-ruby--all-vars-with pattern))) 1673 | 1674 | (defun enh-ruby--variable-values (vars) 1675 | "Map VARS to a list of (variable value) pairs." 1676 | (mapcar (lambda (symbol) (list symbol (symbol-value symbol))) 1677 | vars)) 1678 | 1679 | (defun enh-ruby--uptime-seconds () 1680 | "Return the number of seconds that Emacs has been running." 1681 | (float-time (time-subtract (current-time) before-init-time))) 1682 | 1683 | (defun enh-ruby-eval-file-local-variables () 1684 | "Re-evaluate file-local variables and reindent the file. 1685 | 1686 | Really only useful for my debugging sessions when I'm debugging 1687 | stuff by changing vars over and over." 1688 | (interactive) 1689 | (hack-local-variables) 1690 | (indent-region (point-min) (point-max))) 1691 | 1692 | (defun enh-ruby--add-fl-variables (pairs) 1693 | (mapc (lambda (kv) (apply 'add-file-local-variable kv)) 1694 | (enh-ruby--variable-values pairs))) 1695 | 1696 | (defun enh-ruby-add-file-local-variables () 1697 | "Insert all currently customized variables for this mode as 1698 | file-local variables. This is mainly for providing a complete 1699 | example in a bug report." 1700 | (interactive) 1701 | (enh-ruby--add-fl-variables (enh-ruby--changed-vars-with "enh-ruby"))) 1702 | 1703 | (defun enh-ruby-add-all-file-local-variables () 1704 | "Insert all variables for this mode as file-local variables. This 1705 | is mainly for providing a complete example in a bug report." 1706 | (interactive) 1707 | (enh-ruby--add-fl-variables (enh-ruby--all-vars-with "enh-ruby"))) 1708 | 1709 | (defun enh-ruby-add-indent-file-local-variables () 1710 | "Insert all indent variables for this mode as file-local 1711 | variables. This is mainly for providing a complete example in a 1712 | bug report." 1713 | (interactive) 1714 | (enh-ruby--add-fl-variables (enh-ruby--all-vars-with "enh-ruby.*indent"))) 1715 | 1716 | (defun enh-ruby-del-file-local-variables () 1717 | "Delete all file-local-variables that aren't customized" 1718 | (interactive) 1719 | (mapc #'delete-file-local-variable 1720 | (seq-difference (enh-ruby--all-vars-with "enh-ruby") 1721 | (enh-ruby--changed-vars-with "enh-ruby")))) 1722 | 1723 | (defun enh-ruby-bug-report () 1724 | "Fill a buffer with data to make a ‘enh-ruby-mode’ bug report." 1725 | (interactive) 1726 | (with-help-window "*enh-ruby-mode bug report*" 1727 | (princ "Please provide the following output in your bug report:\n") 1728 | (princ "\n") 1729 | (let ((print-quoted t)) 1730 | (pp (append `((emacs-uptime ,(enh-ruby--uptime-seconds)) 1731 | (mode-path ,(find-library-name "enh-ruby-mode"))) 1732 | (enh-ruby--variable-values 1733 | (append '(emacs-version system-type major-mode) 1734 | (enh-ruby--changed-vars-with "enh-ruby")))))) 1735 | (princ "\n") 1736 | (princ "Also consider using enh-ruby-add-file-local-variables with any code you provide.\n\n") 1737 | (princ "Hit 'q' to close this buffer."))) 1738 | 1739 | (erm-reset) 1740 | 1741 | (provide 'enh-ruby-mode) 1742 | 1743 | ;;; enh-ruby-mode.el ends here 1744 | -------------------------------------------------------------------------------- /ruby/erm.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "erm_buffer" 4 | 5 | STDIN.set_encoding "UTF-8" 6 | # STDIN.set_encoding "BINARY" 7 | 8 | class BufferStore 9 | def initialize 10 | @buffers = {} 11 | end 12 | 13 | def get_buffer buf_num 14 | @buffers[buf_num] ||= ErmBuffer.new if buf_num > 0 15 | end 16 | 17 | def rm buf_num 18 | @buffers.delete buf_num 19 | end 20 | end 21 | 22 | store = BufferStore.new 23 | 24 | EOT = "\n\0\0\0\n" 25 | 26 | begin 27 | while c = STDIN.gets(EOT) 28 | cmd = c[0].to_sym 29 | args = c[1..-6].split ":", 6 30 | bn = args.shift.to_i 31 | buf = store.get_buffer bn 32 | 33 | case cmd 34 | when :x then 35 | (buf || ErmBuffer).set_extra_keywords args.first.split " " 36 | when :c then 37 | STDERR.print "c" 38 | STDERR.puts "#{buf.check_syntax}\n\n\0\0\0" 39 | when :k then 40 | store.rm bn 41 | else 42 | buf.add_content(cmd, *args) unless cmd == :g 43 | 44 | unless cmd == :a 45 | STDERR.puts buf.parse 46 | STDERR.puts "\0\0\0" 47 | end 48 | end 49 | end 50 | rescue 51 | STDERR.puts "e#{$!.class}: #{$!.message}: #{$!.backtrace.join("\n")}#{EOT}" 52 | end 53 | -------------------------------------------------------------------------------- /ruby/erm_buffer.rb: -------------------------------------------------------------------------------- 1 | require 'ripper' 2 | 3 | class ErmBuffer 4 | FONT_LOCK_NAMES = { 5 | rem: 0, # remove/ignore 6 | sp: 0, 7 | ident: 0, 8 | tstring_content: 1, # font-lock-string-face 9 | const: 2, # font-lock-type-face 10 | ivar: 3, # font-lock-variable-name-face 11 | arglist: 3, 12 | cvar: 3, 13 | gvar: 3, 14 | embexpr_beg: 3, 15 | embexpr_end: 3, 16 | comment: 4, # font-lock-comment-face 17 | embdoc: 4, 18 | label: 5, # font-lock-constant-face 19 | CHAR: 6, # font-lock-string-face 20 | backtick: 7, # ruby-string-delimiter-face 21 | __end__: 7, 22 | embdoc_beg: 7, 23 | embdoc_end: 7, 24 | tstring_beg: 7, 25 | tstring_end: 7, 26 | words_beg: 7, 27 | regexp_beg: 8, # ruby-regexp-delimiter-face 28 | regexp_end: 8, 29 | tlambda: 9, # font-lock-function-name-face 30 | defname: 9, 31 | kw: 10, # font-lock-keyword-face 32 | block: 10, 33 | heredoc_beg: 11, 34 | heredoc_end: 11, 35 | op: 12, # ruby-op-face 36 | regexp_string: 13, # ruby-regexp-face 37 | } 38 | 39 | module Adder 40 | attr_accessor :statement_start 41 | attr_accessor :ident 42 | attr_accessor :first_token 43 | attr_accessor :last_add 44 | 45 | def nadd sym, tok, len = tok.size, ft = false, la = nil 46 | d :nadd => [sym, tok, len, ft, la] 47 | case sym 48 | when :sp, :comment then 49 | case parser.mode 50 | when :predef, :expdef then 51 | # do nothing 52 | when :def 53 | parser.mode = :postdef 54 | else 55 | parser.mode = nil 56 | end 57 | else 58 | self.statement_start = false 59 | 60 | parser.mode = :def if parser.mode == :predef 61 | self.block = false if block == :b4args 62 | 63 | case sym 64 | when :ident, :const, :ivar, :gvar, :cvar then 65 | self.ident = true 66 | when :rem_rparen, :indent then 67 | # leave alone? 68 | else 69 | d "self.ident = false" 70 | self.ident = false 71 | end 72 | end 73 | 74 | self.first_token = ft 75 | self.last_add = la 76 | 77 | target = parser.equal?(self) || lineno != parser.lineno ? self : prev 78 | target.realadd sym, tok, len 79 | 80 | sym 81 | end 82 | end # module Adder 83 | 84 | class Heredoc 85 | include Adder 86 | 87 | attr_accessor :lineno, :lines, :parser, :prev, :tok 88 | attr_accessor :block 89 | 90 | def initialize parser, prev, tok, lineno 91 | # TODO: tok? 92 | self.parser = parser 93 | self.prev = prev 94 | self.lineno = lineno 95 | self.lines = [] 96 | self.block = nil 97 | end 98 | 99 | def d o 100 | parser.d o 101 | end 102 | 103 | def realadd(*args) 104 | lines << args 105 | end 106 | 107 | def restore 108 | lines << [:heredoc_end, nil, nil] if lines.empty? 109 | if parser.equal? prev then 110 | for args in lines 111 | parser.nadd(*args) 112 | end 113 | parser.heredoc = nil 114 | else 115 | prev.lines += lines 116 | parser.heredoc = prev 117 | end 118 | end 119 | end # class Heredoc 120 | 121 | class Parser < ::Ripper #:nodoc: internal use only 122 | include Adder 123 | 124 | # TODO: add prev_line and copy it when we clear line_so_far 125 | # TODO: use this to calculate hanging indent for parenless args 126 | 127 | # Indents: 128 | # 129 | # l - [, (, {, %w/%i open or | goalpost open 130 | # r - ], ), }, %w/%i close or | goalpost close 131 | # b - begin/def/case/if 132 | # e - end / embexpr (interpolation) end / close block } 133 | # d - do / { 134 | # s - statement start on BACKDENT_KW else/when/rescue etc 135 | # c - continue - period followed by return (or other way around?) 136 | 137 | INDENT_KW = [:begin, :def, :case, :module, :class, :do, :for] 138 | BACKDENT_KW = [:elsif, :else, :when, :in, :rescue, :ensure] 139 | BEGINDENT_KW = [:if, :unless, :while, :until] 140 | POSTCOND_KW = [:if, :unless, :or, :and] 141 | PRE_OPTIONAL_DO_KW = [:in, :while, :until] 142 | DELIM_MAP = { "(" => ")", "[" => "]", "{" => "}" } 143 | 144 | ESCAPE_LINE_END = "\\\n" 145 | 146 | attr_accessor :heredoc, :mode 147 | attr_accessor :indent_stack, :ident_stack, :brace_stack 148 | attr_accessor :parser 149 | attr_accessor :file_encoding 150 | attr_accessor :ermbuffer 151 | attr_accessor :point_min 152 | attr_accessor :point_max 153 | attr_accessor :src 154 | attr_accessor :src_size 155 | attr_accessor :first_count 156 | attr_accessor :count 157 | attr_accessor :res 158 | attr_accessor :block 159 | attr_accessor :list_count 160 | attr_accessor :cond_stack 161 | attr_accessor :plit_stack 162 | attr_accessor :line_so_far 163 | 164 | def initialize ermbuffer, src, point_min, point_max, first_count 165 | self.ermbuffer = ermbuffer 166 | self.point_min = point_min 167 | self.point_max = point_max 168 | self.src = src 169 | self.src_size = src.size 170 | self.file_encoding = src.encoding 171 | self.first_count = nil 172 | self.parser = self # stupid hack for Adder module above 173 | super src 174 | end 175 | 176 | def add(*args) 177 | (heredoc || self).nadd(*args) 178 | end 179 | 180 | def indent type, c = 0 181 | add :indent, type, c 182 | end 183 | 184 | # Bugs in Ripper: 185 | # empty here doc fails to fire on_heredoc_end 186 | def parse 187 | self.count = 1 188 | self.mode = nil 189 | self.brace_stack = [] 190 | self.heredoc = nil 191 | self.first_token = true 192 | self.last_add = nil 193 | self.res = [] 194 | self.ident = false 195 | self.ident_stack = [] 196 | self.block = false 197 | self.statement_start = true 198 | self.indent_stack = [] 199 | self.list_count = 0 200 | self.cond_stack = [] 201 | self.plit_stack = [] 202 | self.line_so_far = [] 203 | 204 | catch :parse_complete do 205 | super 206 | 207 | realadd :rem_heredoc, '', src_size-count if heredoc 208 | end 209 | 210 | self.res = res.map.with_index { |v, i| 211 | "(%d %s)" % [i, v.join(" ")] if v 212 | } 213 | 214 | "((%s %s %s %s)%s)" % [src_size, 215 | point_min, 216 | point_max, 217 | indent_stack.join(' '), 218 | res.join] 219 | end 220 | 221 | def realadd sym, tok, len 222 | if sym == :indent 223 | pos = count + len 224 | indent_stack << tok << pos if pos.between? point_min, point_max 225 | 226 | return 227 | end 228 | 229 | start = count 230 | throw :parse_complete if start > point_max 231 | 232 | len = 2 + src.index("\n", start) - start unless len 233 | 234 | pos = self.count += len 235 | return if pos < point_min 236 | start = point_min if start < point_min 237 | pos = point_max if pos > point_max 238 | 239 | sym = :rem if sym =~ /^rem_/ 240 | idx = FONT_LOCK_NAMES[sym] 241 | 242 | if t = res[idx] then 243 | if t.last == start then 244 | t[-1] = pos 245 | else 246 | t << start << pos 247 | end 248 | else 249 | res[idx] = [start, pos] 250 | end 251 | 252 | if (sym == :sp && tok == "\n") || (sym == :comment && tok.end_with?("\n")) 253 | line_so_far.clear 254 | else 255 | line_so_far << [sym, tok, len] 256 | end 257 | 258 | throw :parse_complete if pos == point_max 259 | end 260 | 261 | def maybe_plit_ending tok 262 | if tok[-1] == plit_stack.last 263 | plit_stack.pop 264 | 265 | # Token can sometimes have preceding whitespace, which needs to be added 266 | # as a separate token to work with indents. 267 | if tok.length > 1 268 | add :rem_end_ws, tok[0..-2] 269 | end 270 | indent :r 271 | add :tstring_end, tok[-1] 272 | end 273 | end 274 | 275 | ############################################################ 276 | # on_* handlers 277 | # TODO: I don't like these generated methods. Harder to trace/debug. 278 | 279 | def d o 280 | ermbuffer.d o 281 | end 282 | 283 | def debug_on tok 284 | return unless ermbuffer.debug 285 | loc = caller_locations.first.label.to_sym 286 | rest = line_so_far.map {|a| a[1] }.join 287 | d "%-10s %p %p %p" % [loc, tok, ident, rest] 288 | end 289 | 290 | [:CHAR, :__end__, :backtick, :embdoc, :embdoc_beg, :embdoc_end, 291 | :label, :tlambda, :tstring_beg].each do |event| 292 | define_method "on_#{event}" do |tok| 293 | tok.force_encoding file_encoding if tok.encoding != file_encoding 294 | debug_on tok 295 | add event, tok 296 | end 297 | end 298 | 299 | [:backref, :float, :int].each do |event| 300 | define_method "on_#{event}" do |tok| 301 | debug_on tok 302 | add :"rem_#{event}", tok 303 | end 304 | end 305 | 306 | [:cvar, :gvar, :ivar].each do |event| 307 | define_method "on_#{event}" do |tok| 308 | if mode == :sym then 309 | debug_on [tok, :sym] 310 | add :label, tok 311 | else 312 | debug_on [tok, :not_sym] 313 | add event, tok 314 | end 315 | end 316 | end 317 | 318 | def on_comma tok 319 | debug_on tok 320 | self.mode = nil 321 | r = add :rem_comma, tok, tok.size, false, list_count <= 0 322 | self.statement_start = true 323 | r 324 | end 325 | 326 | def on_comment tok 327 | debug_on tok 328 | on_eol :comment, tok 329 | end 330 | 331 | def on_const tok 332 | debug_on [tok, mode] 333 | case mode 334 | when :sym then 335 | self.mode = nil 336 | add :label, tok 337 | when :def, :predef then 338 | r = add :const, tok 339 | self.mode = :predef 340 | r 341 | else 342 | add :const, tok 343 | end 344 | end 345 | 346 | def on_embexpr_beg tok 347 | debug_on tok 348 | len = tok.size 349 | if len > 2 then 350 | add :tstring_content, tok, len - 2 351 | len = 2 352 | end 353 | 354 | brace_stack << :embexpr 355 | cond_stack << false 356 | plit_stack << false 357 | 358 | indent :d, 1 359 | add :embexpr_beg, tok, len 360 | end 361 | 362 | def on_embexpr_end tok 363 | debug_on tok 364 | brace_stack.pop 365 | cond_stack.pop 366 | plit_stack.pop 367 | indent :e 368 | add :embexpr_beg, tok 369 | end 370 | 371 | def on_embvar tok 372 | debug_on tok 373 | len = tok.size 374 | if len > 1 then 375 | add :tstring_content, tok, len - 1 376 | len = 1 377 | end 378 | 379 | add :ivar, tok, len 380 | end 381 | 382 | def on_eol sym, tok 383 | debug_on tok 384 | indent :c, tok.size if last_add 385 | 386 | r = add sym, tok, tok.size, true 387 | 388 | if heredoc && heredoc.lineno == lineno then 389 | heredoc.restore 390 | end 391 | 392 | cond_stack.pop if cond_stack.last 393 | self.statement_start = true 394 | 395 | r 396 | end 397 | 398 | def on_heredoc_beg tok 399 | debug_on tok 400 | r = add :heredoc_beg, tok 401 | if !heredoc || heredoc.lineno < lineno then 402 | self.heredoc = Heredoc.new self, heredoc||self, tok, lineno 403 | end 404 | r 405 | end 406 | 407 | def on_heredoc_end tok 408 | debug_on tok 409 | add :heredoc_end, tok 410 | end 411 | 412 | def on_ident tok 413 | debug_on [tok, mode] 414 | case mode 415 | when :sym then 416 | add :label, tok 417 | when :predef, :def then 418 | add :defname, tok 419 | when :period then 420 | add :ident, tok 421 | else 422 | if ermbuffer.extra_keywords.include? tok then 423 | add :kw, tok 424 | else 425 | add :ident, tok 426 | end 427 | end 428 | end 429 | 430 | def on_ignored_nl tok 431 | debug_on tok 432 | if tok then 433 | on_nl tok 434 | end 435 | end 436 | 437 | def on_kw sym # TODO: break up. 61 lines long 438 | sym = sym.to_sym 439 | debug_on [sym, mode] 440 | case mode 441 | when :sym then 442 | add :label, sym 443 | when :def, :predef then 444 | if sym != :self then 445 | add :defname, sym 446 | else 447 | r = add :kw, sym 448 | self.mode = :def 449 | r 450 | end 451 | else 452 | last_add = nil 453 | 454 | case sym 455 | when :end then 456 | indent :e 457 | when :do then 458 | if cond_stack.last then 459 | cond_stack.pop 460 | r = add :kw, sym 461 | else 462 | # `indent` precedes `add` for the compatibility of parsing result. 463 | # 464 | # `add` and `indent` must precede `self.block = :b4args`. 465 | # Otherwise block is overwritten, and indentation is broken 466 | # in the following code: 467 | # each do |a| # <- `|` for argument list is recognized as operator, 468 | # # <- which produces an extra indentation. 469 | # end 470 | 471 | indent :d 472 | r = add :kw, sym 473 | self.block = :b4args 474 | end 475 | 476 | return r 477 | when *BEGINDENT_KW then 478 | if statement_start then 479 | indent :b 480 | elsif POSTCOND_KW.include? sym then 481 | last_add = :cont 482 | end 483 | when *POSTCOND_KW then 484 | last_add = :cont 485 | when *INDENT_KW then 486 | indent :b 487 | when *BACKDENT_KW then 488 | indent :s if statement_start 489 | end 490 | 491 | cond_stack << true if PRE_OPTIONAL_DO_KW.include? sym 492 | 493 | r = add :kw, sym, sym.size, false, last_add 494 | self.mode = :predef if [:def, :alias].include? sym 495 | r 496 | end 497 | end 498 | 499 | def on_lbrace tok 500 | cond_stack << false 501 | ident_stack << [ident, mode] 502 | 503 | is_start_of_line = line_so_far.all? {|a| a[0] == :sp } 504 | if ident && !is_start_of_line then 505 | brace_stack << :block 506 | indent :d 507 | r = add :block, tok 508 | self.block = :b4args 509 | r 510 | else 511 | brace_stack << :brace 512 | self.list_count += 1 513 | indent :l 514 | add :rem_lbrace, tok 515 | end 516 | end 517 | 518 | def on_lparen tok 519 | debug_on tok 520 | newmode = case mode 521 | when :def then 522 | self.mode = nil 523 | :def 524 | when :predef then 525 | self.mode = :expdef 526 | :predef 527 | else 528 | mode 529 | end 530 | 531 | ident_stack << [ident, newmode] 532 | cond_stack << false 533 | indent :l 534 | self.list_count += 1 535 | r = add :rem_lparen, tok 536 | self.statement_start = true 537 | r 538 | end 539 | 540 | def on_nl tok 541 | on_eol :sp, tok 542 | end 543 | 544 | def on_op tok 545 | if mode == :sym then 546 | add :label, tok 547 | else 548 | r = if block && tok == '|' 549 | case block 550 | when :b4args then 551 | indent :l 552 | self.list_count += 1 553 | self.block = :arglist 554 | else 555 | indent :r 556 | self.list_count -= 1 557 | self.block = false 558 | end 559 | add :arglist, tok, 1 560 | else 561 | case mode 562 | when :def, :predef then 563 | add :ident, tok 564 | else 565 | if mode == :postdef && tok == '=' 566 | indent :e 567 | end 568 | add :op, tok, tok.size, false, :cont 569 | end 570 | end 571 | self.statement_start = true 572 | r 573 | end 574 | end 575 | 576 | def on_period tok 577 | self.mode ||= :period 578 | 579 | debug_on tok 580 | 581 | indent :c, tok.size if tok == "\n" 582 | d :ident => ident 583 | d :lsf => line_so_far 584 | 585 | line_so_far_str = line_so_far.map {|a| a[1] }.join 586 | if line_so_far_str.strip == "" 587 | indent :c, -line_so_far_str.length 588 | end 589 | 590 | add :rem_period, tok, tok.size, false, :cont 591 | end 592 | 593 | def on_rbrace tok 594 | debug_on tok 595 | cond_stack.pop 596 | type = case brace_stack.pop 597 | when :embexpr then 598 | indent :e 599 | if plit_stack.last == false 600 | plit_stack.pop 601 | end 602 | :embexpr_beg 603 | when :block then 604 | indent :e 605 | :block 606 | when :brace then 607 | indent :r 608 | self.list_count -= 1 609 | :rem_brace 610 | else 611 | :rem_other 612 | end 613 | 614 | add(type, tok).tap do 615 | self.ident, self.mode = ident_stack.pop 616 | end 617 | end 618 | 619 | def on_regexp_beg tok 620 | tok.force_encoding file_encoding if tok.encoding != file_encoding 621 | self.mode = :regexp 622 | debug_on tok 623 | add :regexp_beg, tok 624 | end 625 | 626 | def on_regexp_end tok 627 | self.mode = nil 628 | debug_on tok 629 | add :regexp_end, tok 630 | end 631 | 632 | def on_rparen tok 633 | debug_on tok 634 | indent :r 635 | r = add :rem_rparen, tok 636 | 637 | self.list_count -= 1 638 | self.ident, self.mode = ident_stack.pop 639 | cond_stack.pop 640 | 641 | r 642 | end 643 | 644 | def on_semicolon tok 645 | debug_on tok 646 | r = add :kw, :semicolon, 1, true 647 | cond_stack.pop if cond_stack.last 648 | self.statement_start = true 649 | r 650 | end 651 | 652 | def on_sp tok 653 | debug_on tok 654 | if tok == ESCAPE_LINE_END then 655 | indent :c, 2 656 | end 657 | add :sp, tok, tok.size, first_token, last_add 658 | end 659 | 660 | def on_symbeg tok 661 | debug_on tok 662 | r = add :label, tok 663 | self.mode = :sym 664 | r 665 | end 666 | 667 | def on_tlambeg tok 668 | debug_on tok 669 | brace_stack << :block 670 | indent :d 671 | add :block, tok 672 | end 673 | 674 | def on_tstring_content tok 675 | debug_on tok 676 | tok.force_encoding file_encoding if tok.encoding != file_encoding 677 | if mode == :regexp 678 | add :regexp_string, tok 679 | elsif plit_stack.last # `tstring_content` is ignored by indent in emacs. 680 | add :rem_tstring_content, tok # TODO: figure out this context? or collapse? 681 | else 682 | add :tstring_content, tok 683 | end 684 | end 685 | 686 | def on_tstring_end tok 687 | debug_on [tok, mode] 688 | 689 | return if maybe_plit_ending(tok) 690 | 691 | if mode == :sym then 692 | add :label, tok 693 | else 694 | add :tstring_end, tok 695 | end 696 | end 697 | 698 | def on_label_end tok 699 | debug_on tok 700 | add :tstring_beg, tok[0] 701 | add :label, tok[1] 702 | end 703 | 704 | def on_words_beg tok 705 | debug_on tok 706 | delimiter = tok.strip[-1] # ie. "%w(\n" => "(" 707 | plit_stack << (DELIM_MAP[delimiter] || delimiter) 708 | 709 | indent :l 710 | add :words_beg, tok 711 | end 712 | 713 | def on_words_sep tok 714 | debug_on tok 715 | return if maybe_plit_ending(tok) 716 | 717 | add :rem_words_sep, tok 718 | end 719 | 720 | alias on_lbracket on_lparen 721 | alias on_qsymbols_beg on_words_beg 722 | alias on_qwords_beg on_words_beg 723 | alias on_rbracket on_rparen 724 | alias on_symbols_beg on_words_beg 725 | end # class Parser 726 | 727 | @@extra_keywords = {} 728 | 729 | attr_writer :extra_keywords 730 | attr_accessor :point_min 731 | attr_accessor :point_max 732 | attr_accessor :first_count 733 | attr_accessor :buffer 734 | attr_accessor :debug 735 | 736 | def initialize 737 | self.extra_keywords = nil 738 | self.first_count = nil 739 | self.buffer = '' 740 | self.debug = false 741 | end 742 | 743 | def d o 744 | return unless debug 745 | require "pp" 746 | o = o.pretty_inspect unless String === o 747 | puts o.gsub(/^/, " # ") 748 | end 749 | 750 | def add_content cmd, point_min, point_max, pbeg, len, content 751 | self.point_min = point_min.to_i 752 | self.point_max = point_max.to_i 753 | 754 | pbeg = pbeg.to_i 755 | 756 | self.first_count = pbeg if !first_count || pbeg < first_count 757 | 758 | if cmd == :r || buffer.empty? then 759 | self.buffer = content 760 | else 761 | len = pbeg + len.to_i - 2 762 | if pbeg == 1 && len < 0 then 763 | buffer[0..0] = content << buffer[0] 764 | else 765 | buffer[pbeg - 1..len] = content 766 | end 767 | end 768 | end 769 | 770 | # verify that this is used in erm.rb. I don't know how to trigger it. & args?? 771 | def check_syntax fname = '', code = buffer 772 | $VERBOSE = true 773 | # eval but do not run code 774 | eval "BEGIN{return}\n#{code}", nil, fname, 0 775 | rescue SyntaxError 776 | $!.message 777 | rescue 778 | # do nothing 779 | ensure 780 | $VERBOSE = nil 781 | end 782 | 783 | def parse 784 | parser = ErmBuffer::Parser.new(self, buffer, 785 | point_min, point_max, 786 | first_count||0) 787 | self.first_count = nil 788 | parser.parse 789 | end 790 | 791 | def self.set_extra_keywords keywords 792 | @@extra_keywords = Hash[keywords.map { |o| [o, true] }] 793 | end 794 | 795 | def set_extra_keywords keywords 796 | self.extra_keywords = Hash[keywords.map { |o| [o, true] }] 797 | end 798 | 799 | def extra_keywords 800 | @extra_keywords || @@extra_keywords 801 | end 802 | end # class ErmBuffer 803 | -------------------------------------------------------------------------------- /test/enh-ruby-mode-test.el: -------------------------------------------------------------------------------- 1 | (eval-and-compile 2 | (add-to-list 'load-path default-directory) 3 | (load "./helper" nil t)) 4 | 5 | (defun erm-run-current-test () 6 | (interactive) 7 | (require 'ert) 8 | (setq enh-tests nil) 9 | (ert-delete-all-tests) 10 | (load-file "../enh-ruby-mode.el") 11 | (eval-buffer) 12 | (let ((ert-debug-on-error t)) 13 | (ert-run-tests-interactively (lisp-current-defun-name)))) 14 | 15 | (defun erm-run-all-tests () 16 | (interactive) 17 | (require 'ert) 18 | (setq enh-tests nil) 19 | (ert-delete-all-tests) 20 | (load-file "../enh-ruby-mode.el") 21 | (eval-buffer) 22 | (ert-run-tests-interactively t)) 23 | 24 | (local-set-key (kbd "C-c C-r") #'erm-run-all-tests) 25 | (local-set-key (kbd "C-c M-r") #'erm-run-current-test) 26 | 27 | ;; In batch mode, face-attribute returns 'unspecified, 28 | ;; and it causes wrong-number-of-arguments errors. 29 | ;; This is a workaround for it. 30 | (defun erm-darken-color (name) 31 | (let ((attr (face-attribute name :foreground))) 32 | (unless (equal attr 'unspecified) 33 | (color-darken-name attr 20) 34 | "#000000"))) 35 | 36 | (enh-deftest enh-ruby-backward-sexp-test () 37 | (with-temp-enh-rb-string 38 | "def foo\n xxx\nend\n" 39 | 40 | (goto-char (point-max)) 41 | (enh-ruby-backward-sexp 1) 42 | (line-should-equal "def foo"))) 43 | 44 | (enh-deftest enh-ruby-backward-sexp-test--ruby () 45 | (with-temp-ruby-string 46 | "def foo\n xxx\nend\n" 47 | 48 | (goto-char (point-max)) 49 | (enh-ruby-backward-sexp 1) 50 | (rest-of-line-should-equal "def foo"))) 51 | 52 | (enh-deftest enh-ruby-backward-sexp-test-inner--erm () 53 | :expected-result :failed 54 | (with-temp-enh-rb-string 55 | "def backward_sexp\n \"string #{expr \"another\"} word\"\nend\n" 56 | 57 | ;; DESIRED: 58 | ;; def backward_sexp\n \"string #{expr \"another\"} word\"\nend\n 59 | ;; ^ HERE 60 | ;; ^ to here 61 | ;; ^ to here 62 | ;; ^ to here 63 | ;; ^ to here 64 | 65 | (search-forward " word") 66 | (rest-of-line-should-equal "\"") 67 | 68 | ;; DESIRED: 69 | (enh-ruby-backward-sexp) 70 | (rest-of-line-should-equal "word\"") 71 | ;; (enh-ruby-backward-sexp) 72 | ;; (rest-of-line-should-equal "#{expr \"another\"} word\"") 73 | ;; (enh-ruby-backward-sexp) 74 | ;; (rest-of-line-should-equal "\"string #{expr \"another\"} word\"") 75 | ;; (enh-ruby-backward-sexp) 76 | ;; (rest-of-line-should-equal "def backward_sexp") 77 | 78 | ;; CURRENT: 79 | ;; def backward_sexp\n \"string #{expr \"another\"} word\"\nend\n 80 | ;; ^ HERE 81 | ;; ^ to here 82 | ;; ^ to here 83 | 84 | ;; CURRENT: 85 | 86 | (enh-ruby-backward-sexp) 87 | (rest-of-line-should-equal "{expr \"another\"} word\"") 88 | 89 | (enh-ruby-backward-sexp) 90 | (rest-of-line-should-equal "def backward_sexp") 91 | )) 92 | 93 | (enh-deftest enh-ruby-backward-sexp-test-inner--ruby () 94 | :expected-result :failed 95 | (with-temp-ruby-string 96 | "def backward_sexp\n \"string #{expr \"another\"} word\"\nend\n" 97 | ;; ^ here 98 | ;; ^ to here 99 | ;;^ NOT HERE 100 | (search-forward " word") 101 | ;; (move-end-of-line nil) 102 | 103 | (rest-of-line-should-equal "\"") 104 | 105 | (enh-ruby-backward-sexp) 106 | (rest-of-line-should-equal "word\"") 107 | 108 | (enh-ruby-backward-sexp) 109 | (rest-of-line-should-equal "{expr \"another\"} word\"") 110 | 111 | (enh-ruby-backward-sexp) 112 | (rest-of-line-should-equal "string #{expr \"another\"} word\"") 113 | 114 | ;; this blows out: (scan-error "Containing expression ends prematurely" 21 21) 115 | ;; (enh-ruby-backward-sexp) 116 | ;; (rest-of-line-should-equal "\"string #{expr \"another\"} word\"") 117 | )) 118 | 119 | (enh-deftest enh-ruby-forward-sexp-test () 120 | (with-temp-enh-rb-string 121 | "def foo\n xxx\n end\n\ndef backward_sexp\n xxx\nend\n" 122 | 123 | (enh-ruby-forward-sexp 1) 124 | (forward-char 2) 125 | (rest-of-line-should-equal "def backward_sexp"))) 126 | 127 | (enh-deftest enh-ruby-up-sexp-test () 128 | (with-temp-enh-rb-string 129 | "def foo\n %_bosexp#{sdffd} test1_[1..4].si\nend" 130 | 131 | (search-forward "test1_") 132 | (enh-ruby-up-sexp) 133 | (line-should-equal "def foo"))) ; maybe this should be %_bosexp? 134 | 135 | (enh-deftest enh-ruby-end-of-defun () 136 | (with-temp-enh-rb-string 137 | "class Class\ndef method\n# blah\nend # method\nend # class" 138 | 139 | (search-forward "blah") 140 | (enh-ruby-end-of-defun) 141 | (rest-of-line-should-equal " # method"))) 142 | 143 | (enh-deftest enh-ruby-end-of-block () 144 | (with-temp-enh-rb-string 145 | "class Class\ndef method\n# blah\nend # method\nend # class" 146 | 147 | (search-forward "blah") 148 | (enh-ruby-end-of-block) 149 | (rest-of-line-should-equal " # method"))) 150 | 151 | ;;; indent-region 152 | 153 | (enh-deftest enh-ruby-indent-array-of-strings () 154 | (with-deep-indent nil 155 | (string-should-indent "words = [\n'moo'\n]\n" 156 | "words = [\n 'moo'\n]\n"))) 157 | 158 | (enh-deftest enh-ruby-indent-array-of-strings-incl-first () 159 | (with-deep-indent nil 160 | (string-should-indent "words = ['cow',\n'moo'\n]\n" 161 | "words = ['cow',\n 'moo'\n]\n"))) 162 | 163 | (enh-deftest enh-ruby-indent-array-of-strings/deep () 164 | (with-deep-indent t 165 | (string-should-indent "words = ['cow',\n'moo'\n]\n" 166 | "words = ['cow',\n 'moo'\n ]\n"))) 167 | 168 | (enh-deftest enh-ruby-indent-array-of-strings-incl-first/deep () 169 | (with-deep-indent t 170 | (string-should-indent "words = ['cow',\n'moo'\n]\n" 171 | "words = ['cow',\n 'moo'\n ]\n"))) 172 | 173 | (enh-deftest enh-ruby-indent-array-of-strings/ruby () 174 | (string-should-indent-like-ruby "words = [\n'moo'\n]\n")) 175 | 176 | (enh-deftest enh-ruby-indent-array-of-strings-incl-first/ruby () 177 | (string-should-indent-like-ruby "words = ['cow',\n'moo'\n]\n" 178 | 'deep)) 179 | 180 | (enh-deftest enh-ruby-indent-not-method () 181 | (string-should-indent-like-ruby 182 | "\nclass Object\ndef !\n100\nend\nend")) 183 | 184 | (enh-deftest enh-ruby-indent-hanging-period-after-parens () 185 | (string-should-indent-like-ruby 186 | ":a\n(b)\n.c")) 187 | 188 | (enh-deftest enh-ruby-indent-hanging-period () 189 | (string-should-indent-like-ruby 190 | ":a\nb\n.c")) 191 | 192 | (enh-deftest enh-ruby-indent-def-endless () 193 | (with-deep-indent nil 194 | (string-should-indent "class Foo\ndef foo = z\ndef bar = y\nend\n" 195 | "class Foo\n def foo = z\n def bar = y\nend\n"))) 196 | 197 | (enh-deftest enh-ruby-indent-def-endless/params () 198 | (with-deep-indent nil 199 | (string-should-indent "class Foo\ndef foo(a) = z\ndef bar = y\nend\n" 200 | "class Foo\n def foo(a) = z\n def bar = y\nend\n"))) 201 | 202 | (enh-deftest enh-ruby-indent-def-endless/args-forward () 203 | (with-deep-indent nil 204 | (string-should-indent "class Foo\ndef foo(...) = z\ndef bar = y\nend\n" 205 | "class Foo\n def foo(...) = z\n def bar = y\nend\n"))) 206 | 207 | (enh-deftest enh-ruby-indent-def-after-private () 208 | (with-deep-indent nil 209 | (string-should-indent "class Foo\nprivate def foo\nx\nend\nend\n" 210 | "class Foo\n private def foo\n x\n end\nend\n"))) 211 | 212 | (enh-deftest enh-ruby-indent-def-after-private/deep () 213 | (with-deep-indent t 214 | (string-should-indent "class Foo\nprivate def foo\nx\nend\nend\n" 215 | "class Foo\n private def foo\n x\n end\nend\n"))) 216 | 217 | (enh-deftest enh-ruby-indent-hash () 218 | ;; https://github.com/zenspider/enhanced-ruby-mode/issues/78 219 | (with-deep-indent nil 220 | (string-should-indent "c = {\na: a,\nb: b\n}\n" 221 | "c = {\n a: a,\n b: b\n}\n"))) 222 | 223 | (defconst input/indent-hash/trail "\nc = {a: a,\nb: b,\n c: c\n}") 224 | (defconst input/indent-hash/hang "\nc = {\na: a,\nb: b,\n c: c\n}") 225 | (defconst exp/indent-hash/hang "\nc = {\n a: a,\n b: b,\n c: c\n}") 226 | (defconst exp/indent-hash/trail "\nc = {a: a,\n b: b,\n c: c\n }") 227 | (defconst exp/indent-hash/trail/8 "\nc = {a: a,\n b: b,\n c: c\n }") 228 | 229 | (defmacro with-bounce-and-hang (bounce indent1 indent2 &rest body) 230 | `(let ((enh-ruby-bounce-deep-indent ,bounce) 231 | (enh-ruby-deep-indent-paren t) 232 | (enh-ruby-hanging-brace-indent-level (or ,indent1 233 | enh-ruby-hanging-brace-indent-level)) 234 | (enh-ruby-hanging-brace-deep-indent-level (or ,indent2 235 | enh-ruby-hanging-brace-deep-indent-level))) 236 | ,@body)) 237 | (put 'with-bounce-and-hang 'lisp-indent-function 'defun) 238 | 239 | (enh-deftest enh-ruby-indent-hash/deep/hang/def () 240 | (with-deep-indent t 241 | (with-bounce-and-hang nil nil nil 242 | (string-should-indent input/indent-hash/hang 243 | exp/indent-hash/hang)))) 244 | 245 | (enh-deftest enh-ruby-indent-hash/deep/bounce/hang/def () 246 | (with-deep-indent t 247 | (with-bounce-and-hang t nil nil 248 | (string-should-indent input/indent-hash/hang 249 | "\nc = {\n a: a,\n b: b,\n c: c\n }")))) 250 | 251 | ;; if bounce off, hanging-brace-deep-indent-level doesn't matter 252 | (enh-deftest enh-ruby-indent-hash/deep/hang/99 () 253 | (with-deep-indent t 254 | (with-bounce-and-hang nil nil 99 255 | (string-should-indent input/indent-hash/hang 256 | exp/indent-hash/hang)))) 257 | 258 | ;; 3 < 4, so close brace is at 1 259 | (enh-deftest enh-ruby-indent-hash/deep/hang/bil-3 () 260 | (with-deep-indent t 261 | (with-bounce-and-hang nil 3 nil 262 | (string-should-indent input/indent-hash/hang 263 | "\nc = {\n a: a,\n b: b,\n c: c\n}")))) 264 | 265 | ;; 8 > 4, so close brace is at 4 266 | (enh-deftest enh-ruby-indent-hash/deep/hang/bil-8 () 267 | (with-deep-indent t 268 | (with-bounce-and-hang nil 8 nil 269 | (string-should-indent input/indent-hash/hang 270 | "\nc = {\n a: a,\n b: b,\n c: c\n }")))) 271 | 272 | (enh-deftest enh-ruby-indent-hash/deep/trail/def () 273 | (with-deep-indent t 274 | (with-bounce-and-hang nil nil nil 275 | (string-should-indent input/indent-hash/trail 276 | exp/indent-hash/trail)))) 277 | 278 | (enh-deftest enh-ruby-indent-hash/deep/bounce/trail/def () 279 | (with-deep-indent t 280 | (with-bounce-and-hang t nil nil 281 | (string-should-indent input/indent-hash/trail 282 | exp/indent-hash/trail)))) 283 | 284 | (enh-deftest enh-ruby-indent-hash/deep/trail/3 () 285 | (with-deep-indent t 286 | (with-bounce-and-hang nil 3 8 287 | (string-should-indent input/indent-hash/trail 288 | exp/indent-hash/trail/8)))) 289 | 290 | (enh-deftest enh-ruby-indent-hash/deep/bounce/trail/3 () 291 | (with-deep-indent t 292 | (with-bounce-and-hang t 3 8 293 | (string-should-indent input/indent-hash/trail 294 | exp/indent-hash/trail/8)))) 295 | 296 | (enh-deftest enh-ruby-indent-hash/deep/trail/8 () 297 | (with-deep-indent t 298 | (with-bounce-and-hang nil 8 8 299 | (string-should-indent input/indent-hash/trail 300 | exp/indent-hash/trail/8)))) 301 | 302 | (enh-deftest enh-ruby-indent-hash/deep/bounce/trail/8 () 303 | (with-deep-indent t 304 | (with-bounce-and-hang t 8 8 305 | (string-should-indent input/indent-hash/trail 306 | exp/indent-hash/trail/8)))) 307 | 308 | (enh-deftest enh-ruby-indent-bug/90/a () 309 | (string-should-indent-like-ruby "aa.bb(:a => 1,\n :b => 2,\n :c => 3)\n" 310 | 'deep)) 311 | 312 | (enh-deftest enh-ruby-indent-bug/90/b () 313 | (string-should-indent-like-ruby "literal_array = [\n :a,\n :b,\n :c\n]\n")) 314 | 315 | (enh-deftest enh-ruby-indent-hash-after-cmd () 316 | (with-deep-indent nil 317 | (string-should-indent "x\n{\na: a,\nb: b\n}" 318 | "x\n{\n a: a,\n b: b\n}"))) 319 | 320 | (enh-deftest enh-ruby-indent-hash-after-cmd/deep () 321 | (with-deep-indent t 322 | (string-should-indent "x\n{\na: a,\nb: b\n}" 323 | "x\n{\n a: a,\n b: b\n}"))) 324 | 325 | (enh-deftest enh-ruby-indent-hash-after-cmd/ruby () 326 | (string-should-indent-like-ruby "x\n{\na: a,\nb: b\n}")) 327 | 328 | (enh-deftest enh-ruby-indent-if-in-assignment () 329 | (with-deep-indent nil 330 | (string-should-indent "foo = if bar\nx\nelse\ny\nend\n" 331 | "foo = if bar\n x\nelse\n y\nend\n"))) 332 | 333 | (enh-deftest enh-ruby-indent-if-in-assignment/deep () 334 | (with-deep-indent t 335 | (string-should-indent "foo = if bar\nx\nelse\ny\nend\n" 336 | "foo = if bar\n x\n else\n y\n end\n"))) 337 | 338 | (enh-deftest enh-ruby-indent-leading-dots () 339 | (string-should-indent "d.e\n.f\n" 340 | "d.e\n .f\n")) 341 | 342 | (enh-deftest enh-ruby-indent-leading-dots-cvar () 343 | (string-should-indent "@@b\n.c\n.d\n" 344 | "@@b\n .c\n .d\n")) 345 | 346 | (enh-deftest enh-ruby-indent-leading-dots-cvar/ruby () 347 | (string-should-indent-like-ruby "@@b\n.c\n.d\n")) 348 | 349 | (enh-deftest enh-ruby-indent-leading-dots-gvar () 350 | (string-should-indent "$b\n.c\n.d\n" 351 | "$b\n .c\n .d\n")) 352 | 353 | (enh-deftest enh-ruby-indent-leading-dots-gvar/ruby () 354 | (string-should-indent-like-ruby "$b\n.c\n.d\n")) 355 | 356 | (enh-deftest enh-ruby-indent-leading-dots-ident () 357 | (string-should-indent "b\n.c\n.d\n" 358 | "b\n .c\n .d\n")) 359 | 360 | (enh-deftest enh-ruby-indent-leading-dots-ident/ruby () 361 | (string-should-indent-like-ruby "b\n.c\n.d\n")) 362 | 363 | (enh-deftest enh-ruby-indent-leading-dots-ivar () 364 | (string-should-indent "@b\n.c\n.d\n" 365 | "@b\n .c\n .d\n")) 366 | 367 | (enh-deftest enh-ruby-indent-leading-dots-ivar/ruby () 368 | (string-should-indent-like-ruby "@b\n.c\n.d\n")) 369 | 370 | (enh-deftest enh-ruby-indent-leading-dots-with-block () 371 | (string-should-indent "a\n.b {}\n.c\n" 372 | "a\n .b {}\n .c\n")) 373 | 374 | (enh-deftest enh-ruby-indent-leading-dots-with-block/ruby () 375 | (string-should-indent-like-ruby "a\n.b {}\n.c\n")) 376 | 377 | (enh-deftest enh-ruby-indent-leading-dots-with-comment () 378 | (string-should-indent "a\n.b # comment\n.c\n" 379 | "a\n .b # comment\n .c\n")) 380 | 381 | (enh-deftest enh-ruby-indent-leading-dots-with-comment/ruby () 382 | (string-should-indent-like-ruby "a\n.b # comment\n.c\n")) 383 | 384 | (enh-deftest enh-ruby-indent-leading-dots/ruby () 385 | (string-should-indent-like-ruby "d.e\n.f\n")) 386 | 387 | (defconst leading-dot-input "\na\n.b\n.c(\nd,\ne\n)\n.f\n") 388 | (defconst trailing-dot-input "\na.\nb.\nc(\nd,\ne\n).\nf\n") 389 | 390 | (enh-deftest enh-ruby-indent-leading-dots-with-arguments-and-newlines () 391 | (string-should-indent leading-dot-input 392 | "\na\n .b\n .c(\n d,\n e\n )\n .f\n")) 393 | 394 | (enh-deftest enh-ruby-indent-leading-dots-with-arguments-and-newlines/bounce () 395 | (with-bounce-and-hang t nil nil 396 | (string-should-indent leading-dot-input 397 | "\na\n .b\n .c(\n d,\n e\n )\n .f\n"))) 398 | 399 | (enh-deftest enh-ruby-indent-leading-dots-with-arguments-and-newlines/ruby () 400 | (string-should-indent-like-ruby leading-dot-input)) 401 | 402 | (enh-deftest enh-ruby-indent-trailing-dots-with-arguments-and-newlines () 403 | (string-should-indent trailing-dot-input 404 | "\na.\n b.\n c(\n d,\n e\n ).\n f\n")) 405 | 406 | (enh-deftest enh-ruby-indent-trailing-dots-with-arguments-and-newlines/bounce () 407 | (with-bounce-and-hang t nil nil 408 | (string-should-indent trailing-dot-input 409 | "\na.\n b.\n c(\n d,\n e\n ).\n f\n"))) 410 | 411 | (enh-deftest enh-ruby-indent-trailing-dots-with-arguments-and-newlines/ruby () 412 | (string-should-indent-like-ruby trailing-dot-input)) 413 | 414 | (enh-deftest enh-ruby-add-log-current-method/nested-modules () 415 | :expected-result :failed 416 | (with-temp-enh-rb-string 417 | "module One\nmodule Two\nclass Class\ndef method\n# blah\nend # method\nend # class\nend # One\nend # Two" 418 | (search-forward "blah") 419 | (should (equal "One::Two::Class#method" (enh-ruby-add-log-current-method))))) 420 | 421 | (enh-deftest enh-ruby-add-log-current-method/compact-modules () 422 | (with-temp-enh-rb-string 423 | "class One::Two::Class\ndef method\n# blah\nend # method\nend # class" 424 | (search-forward "blah") 425 | (should (equal "One::Two::Class#method" (enh-ruby-add-log-current-method))))) 426 | 427 | (enh-deftest enh-ruby-indent-continued-assignment () 428 | (string-should-indent "\na =\nb.map do |c|\nd(c)\nend\n" 429 | "\na =\n b.map do |c|\n d(c)\n end\n")) 430 | 431 | (enh-deftest enh-ruby-indent-leading-dots-with-block-and-newlines () 432 | (string-should-indent "\na\n.b do\nc\nend\n.d\n\ne" 433 | "\na\n .b do\n c\n end\n .d\n\ne")) 434 | 435 | (enh-deftest enh-ruby-indent-leading-dots-with-brackets-and-newlines () 436 | (string-should-indent "\na\n.b {\nc\n}\n.d\n\ne" 437 | "\na\n .b {\n c\n }\n .d\n\ne")) 438 | 439 | (enh-deftest enh-ruby-indent-not-on-eol-opening/deep () 440 | (with-deep-indent t 441 | (string-should-indent "\nfoo(:bar,\n:baz)\nfoo(\n:bar,\n:baz,\n)\n[:foo,\n:bar]\n[\n:foo,\n:bar\n]" 442 | "\nfoo(:bar,\n :baz)\nfoo(\n :bar,\n :baz,\n)\n[:foo,\n :bar]\n[\n :foo,\n :bar\n]"))) 443 | 444 | (enh-deftest enh-ruby-indent-pct-w-array () 445 | (with-deep-indent nil 446 | (string-should-indent "words = %w[\na\nb\n]\n" 447 | "words = %w[\n a\n b\n]\n"))) 448 | 449 | (enh-deftest enh-ruby-indent-pct-w-array/deep () 450 | (with-deep-indent t 451 | (with-bounce-and-hang nil nil nil 452 | (string-should-indent "\nwords = %w[a\nb\nc\n]\n" 453 | "\nwords = %w[a\n b\n c\n ]\n")))) 454 | 455 | ;; NO! ruby-mode refuses to indent %w at all 456 | ;; words = %w[ a 457 | ;; b 458 | ;; c 459 | ;; ] 460 | ;; 461 | ;; vs enh-ruby-mode: 462 | ;; 463 | ;; words = %w[ a 464 | ;; b 465 | ;; c 466 | ;; ] 467 | ;; 468 | ;; and w/ deep-indent-paren t: 469 | ;; words = %w[ a 470 | ;; b 471 | ;; c 472 | ;; ] 473 | ;; 474 | ;; (enh-deftest enh-ruby-indent-pct-w-array/ruby () 475 | ;; (string-should-indent-like-ruby "words = %w[ a\nb\nc\n]\n")) 476 | 477 | (enh-deftest enh-ruby-indent-trailing-dots () 478 | (string-should-indent "a.b.\nc\n" 479 | "a.b.\n c\n")) 480 | 481 | (enh-deftest enh-ruby-indent-trailing-dots/ruby () 482 | (string-should-indent-like-ruby "a.b.\nc\n")) 483 | 484 | ;;; indent-for-tab-command -- seems different than indent-region in some places 485 | 486 | (enh-deftest enh-ruby-beginning-of-block () 487 | (with-temp-enh-rb-string 488 | "RSpec.describe Foo do\n it 'bar' do\n HERE\n end\nend" 489 | 490 | (search-forward "HERE") 491 | 492 | (enh-ruby-beginning-of-block) 493 | (line-should-equal " it 'bar' do") 494 | 495 | (enh-ruby-beginning-of-block) 496 | (line-should-equal "RSpec.describe Foo do") 497 | 498 | (enh-ruby-beginning-of-block) 499 | (line-should-equal "RSpec.describe Foo do"))) 500 | 501 | (enh-deftest enh-ruby-indent-for-tab-heredocs/off () 502 | (with-temp-enh-rb-string 503 | "meth <<-DONE\n a b c\nd e f\nDONE\n" 504 | 505 | (search-forward "d e f") 506 | (move-beginning-of-line nil) 507 | (let ((enh-ruby-preserve-indent-in-heredocs nil)) 508 | (indent-for-tab-command) ; hitting TAB char 509 | (buffer-should-equal "meth <<-DONE\n a b c\nd e f\nDONE\n")))) 510 | 511 | (enh-deftest enh-ruby-indent-for-tab-heredocs/on () 512 | (with-temp-enh-rb-string 513 | "meth <<-DONE\n a b c\nd e f\nDONE\n" 514 | 515 | (search-forward "d e f") 516 | (move-beginning-of-line nil) 517 | (let ((enh-ruby-preserve-indent-in-heredocs t)) 518 | (indent-for-tab-command) ; hitting TAB char 519 | (buffer-should-equal "meth <<-DONE\n a b c\n d e f\nDONE\n")))) 520 | 521 | (enh-deftest enh-ruby-indent-for-tab-heredocs/unset () 522 | (with-temp-enh-rb-string 523 | "meth <<-DONE\n a b c\nd e f\nDONE\n" 524 | 525 | (search-forward "d e f") 526 | (move-beginning-of-line nil) 527 | (indent-for-tab-command) ; hitting TAB char 528 | (buffer-should-equal "meth <<-DONE\n a b c\nd e f\nDONE\n"))) 529 | 530 | ;;; enh-ruby-toggle-block 531 | 532 | (defconst ruby-do-block "7.times do |i|\n puts \"number #{i+1}\"\nend\n") 533 | (defconst ruby-brace-block/1 "7.times { |i| puts \"number #{i+1}\" }\n") 534 | (defconst ruby-brace-block/3 "7.times { |i|\n puts \"number #{i+1}\"\n}\n") 535 | 536 | (defun enh-ruby-toggle-block-and-wait () 537 | (enh-ruby-toggle-block) 538 | (erm-wait-for-parse) 539 | (font-lock-ensure)) 540 | 541 | (defun toggle-to-do () 542 | (enh-ruby-toggle-block-and-wait) 543 | (buffer-should-equal ruby-do-block)) 544 | 545 | (defun toggle-to-brace () 546 | (enh-ruby-toggle-block-and-wait) 547 | (buffer-should-equal ruby-brace-block/1)) 548 | 549 | (enh-deftest enh-ruby-toggle-block/both () 550 | (with-temp-enh-rb-string ruby-brace-block/3 551 | (toggle-to-do) 552 | (toggle-to-brace))) 553 | 554 | (enh-deftest enh-ruby-toggle-block/brace () 555 | (with-temp-enh-rb-string ruby-brace-block/3 556 | (toggle-to-do))) 557 | 558 | (enh-deftest enh-ruby-toggle-block/do () 559 | (with-temp-enh-rb-string ruby-do-block 560 | (toggle-to-brace))) 561 | 562 | (defconst ruby-brace-block/puts "7.times { |i| puts i }\n") 563 | (defconst ruby-do-block/puts "7.times do |i|\n puts i \nend\n") 564 | 565 | (enh-deftest enh-ruby-toggle-block/does-not-trigger-when-point-is-beyond-block () 566 | (with-temp-enh-rb-string ruby-brace-block/puts 567 | (search-forward "}") 568 | (enh-ruby-toggle-block-and-wait) 569 | (buffer-should-equal ruby-brace-block/puts))) 570 | 571 | (enh-deftest enh-ruby-toggle-block/triggers-when-point-is-at-end-of-block () 572 | (with-temp-enh-rb-string ruby-brace-block/puts 573 | (search-forward "}") 574 | (backward-char) 575 | (enh-ruby-toggle-block-and-wait) 576 | (buffer-should-equal ruby-do-block/puts))) 577 | 578 | (defconst ruby-puts "puts \"test\"") 579 | 580 | (enh-deftest enh-ruby-toggle-block/with-no-block-in-buffer-does-not-fail () 581 | (with-temp-enh-rb-string ruby-puts 582 | (enh-ruby-toggle-block-and-wait) 583 | (buffer-should-equal ruby-puts))) 584 | 585 | (defconst ruby-brace/let "let(:dont_let) { { a: 1, b: 2 } }\n") 586 | (defconst ruby-do/let "let(:dont_let) do\n { a: 1, b: 2 } \nend\n") 587 | 588 | (enh-deftest enh-ruby-toggle-block/brace-with-inner-hash () 589 | (with-temp-enh-rb-string ruby-brace/let 590 | (enh-ruby-toggle-block-and-wait) 591 | (buffer-should-equal ruby-do/let))) 592 | 593 | (enh-deftest enh-ruby-paren-mode-if/open () 594 | (should-show-parens 595 | " 596 | G|ifG foo 597 | bar 598 | GendG")) 599 | 600 | (enh-deftest enh-ruby-paren-mode-if/close () 601 | (should-show-parens 602 | " 603 | GifG foo 604 | bar 605 | Gend|G")) 606 | 607 | (enh-deftest enh-ruby-paren-mode-if/mismatch () 608 | (should-show-parens 609 | " 610 | R|ifR foo 611 | bar 612 | R}R")) 613 | 614 | (enh-deftest enh-ruby-paren-mode-while-do/open () 615 | (should-show-parens 616 | " 617 | G|whileG foo do 618 | if bar 619 | baz 620 | end 621 | GendG")) 622 | 623 | (enh-deftest enh-ruby-paren-mode-while-do/close () 624 | (should-show-parens 625 | " 626 | GwhileG foo do 627 | if bar 628 | baz 629 | end 630 | Gend|G")) 631 | 632 | (enh-deftest enh-ruby-paren-mode-while-do/mismatch () 633 | (should-show-parens 634 | " 635 | R|whileR foo do 636 | if bar 637 | baz 638 | end 639 | RR")) 640 | 641 | (enh-deftest enh-ruby-paren-mode-begin-end () 642 | (should-show-parens 643 | " 644 | G|beginG 645 | foo 646 | rescue 647 | GendG")) 648 | 649 | (enh-deftest enh-ruby-paren-mode-if-dont-show () 650 | "point is not in right spot to highlight pairs so nothing 651 | should be tagged" 652 | (should-show-parens 653 | " 654 | i|f foo 655 | bar 656 | end") 657 | (should-show-parens 658 | " 659 | if| foo 660 | bar 661 | end") 662 | (should-show-parens 663 | " 664 | if foo 665 | bar 666 | en|d") 667 | (should-show-parens 668 | " 669 | if foo 670 | bar 671 | e|nd")) 672 | 673 | (enh-deftest enh-ruby-paren-mode-delegate () 674 | "delegate braces to show-paren-data-function (i.e. don't 675 | highlight anything)" 676 | (should-show-parens 677 | "foo.map G|{G there G}G")) 678 | -------------------------------------------------------------------------------- /test/helper.el: -------------------------------------------------------------------------------- 1 | (require 'ert) 2 | (require 'ert-x) 3 | (require 'paren) ; for show-paren tests & helper 4 | (eval-and-compile 5 | (add-to-list 'load-path (file-name-directory (directory-file-name default-directory)))) 6 | (require 'enh-ruby-mode) 7 | 8 | ;; I hate this so much... Shuts up "Indenting region..." output 9 | (defun make-progress-reporter (&rest ignored) nil) 10 | 11 | (defvar enh-tests '()) 12 | 13 | ;; turns out I had a duplicate test and it was driving me crazy. This is my fix. 14 | (defmacro enh-deftest (name &rest rest) 15 | (if (memq name enh-tests) 16 | (error "Duplicate test name! %S" name) 17 | (setq enh-tests (cons name enh-tests)) 18 | `(ert-deftest ,name ,@rest))) 19 | (put 'enh-deftest 'lisp-indent-function 'defun) 20 | 21 | (defmacro with-temp-enh-rb-string (str &rest body) 22 | `(with-temp-buffer 23 | (insert ,str) 24 | (enh-ruby-mode) 25 | (erm-wait-for-parse) 26 | (font-lock-ensure) 27 | (goto-char (point-min)) 28 | (progn ,@body))) 29 | (put 'with-temp-enh-rb-string 'lisp-indent-function 1) 30 | 31 | (defmacro with-temp-ruby-string (str &rest body) 32 | `(with-temp-buffer 33 | (insert ,str) 34 | (ruby-mode) 35 | (font-lock-ensure) 36 | (goto-char (point-min)) 37 | (progn ,@body))) 38 | 39 | (defmacro with-deep-indent (deep? &rest body) 40 | `(let ((enh-ruby-deep-indent-construct ,deep?) ; def / if 41 | (enh-ruby-deep-indent-paren ,deep?)) ; arrays / hashes 42 | ,@body)) 43 | (put 'with-deep-indent 'lisp-indent-function 1) 44 | 45 | (defun buffer-string-plain () 46 | (buffer-substring-no-properties (point-min) (point-max))) 47 | 48 | (defun string-plain (s) 49 | (substring-no-properties s)) 50 | 51 | (defun string-should-indent (ruby exp) 52 | (let ((act (with-temp-enh-rb-string ruby (ert-buffer-string-reindented)))) 53 | (should (equal exp (string-plain act))))) 54 | 55 | (defun string-should-indent-like-ruby (ruby &optional deep?) 56 | (with-deep-indent deep? 57 | (let ((exp (with-temp-ruby-string ruby (ert-buffer-string-reindented))) 58 | (act (with-temp-enh-rb-string ruby (ert-buffer-string-reindented)))) 59 | (should (equal (string-plain exp) (string-plain act)))))) 60 | 61 | (defun buffer-should-equal (exp) 62 | (should (equal exp (buffer-string-plain)))) 63 | 64 | (defun rest-of-line-should-equal (exp) 65 | (should (equal exp (rest-of-line)))) 66 | 67 | (defun line-should-equal (exp) 68 | (should (equal exp (all-of-line)))) 69 | 70 | (defun all-of-line () 71 | (save-excursion 72 | (move-beginning-of-line nil) 73 | (let ((start (point))) 74 | (end-of-line) 75 | (buffer-substring-no-properties start (point))))) 76 | 77 | (defun rest-of-line () 78 | (save-excursion 79 | (let ((start (point))) 80 | (end-of-line) 81 | (buffer-substring-no-properties start (point))))) 82 | 83 | (defun should-show-parens (contents) 84 | "CONTENTS is a template specifying expected paren highlighting. 85 | GfooG means expect foo be green (matching parens), RfooR means 86 | red (mismatched parens), and | is point. No G/R tags means expect 87 | no erm highlighting (i.e. delegate to normal paren-mode)" 88 | (with-temp-buffer 89 | (insert contents) 90 | (goto-char (point-min)) 91 | (let ((case-fold-search nil) (tags ()) point-pos mismatch) 92 | (while (re-search-forward "[GR|]" nil t) 93 | (let ((found-char (char-before))) 94 | (backward-delete-char 1) 95 | (cond 96 | ((char-equal found-char ?G) (push (point) tags)) 97 | ((char-equal found-char ?R) (progn (push (point) tags) (setq mismatch t))) 98 | ((char-equal found-char ?|) (setq point-pos (point)))))) 99 | (setq tags (nreverse tags)) 100 | (when (and tags (< (abs (- point-pos (nth 3 tags))) (abs (- point-pos (car tags))))) 101 | (setq tags (list (nth 2 tags) (nth 3 tags) (nth 0 tags) (nth 1 tags)))) 102 | (setq contents (buffer-substring (point-min) (point-max))) 103 | (with-temp-enh-rb-string 104 | contents 105 | (goto-char point-pos) 106 | (should 107 | (equal 108 | (erm-show-paren-data-function) 109 | (if tags (append tags `(,mismatch)) nil))))))) 110 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require_relative './markup' 2 | 3 | module ErmTestHelper 4 | module_function 5 | 6 | # workaround for Minitest::Assertions.assert_parse 7 | # to remove surrounding `"` from markup diff 8 | def override_inspect(obj) 9 | def obj.inspect 10 | self 11 | end 12 | obj 13 | end 14 | end 15 | 16 | module Minitest::Assertions 17 | module_function 18 | 19 | def assert_parse(markedup_code, buf = ErmBuffer.new, msg = nil) 20 | markedup_code = markedup_code.gsub(/\A\n|\n\z/, "") 21 | 22 | expected_sexp, code = Markup.parse_markup(markedup_code) 23 | actual_sexp = Markup.parse_code(code, buf) 24 | 25 | msg = message(msg || code, "") do 26 | expected_markup = markedup_code 27 | actual_markup = Markup.markup(code, Markup.parse_sexp(actual_sexp)) 28 | 29 | diff_markup = diff(ErmTestHelper.override_inspect(expected_markup), 30 | ErmTestHelper.override_inspect(actual_markup)) 31 | diff_sexp = diff(expected_sexp, actual_sexp) 32 | 33 | diff_markup + "\n" + diff_sexp 34 | end 35 | 36 | assert(expected_sexp == actual_sexp, msg) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/markup.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'strscan' 3 | require_relative '../ruby/erm_buffer' 4 | 5 | module Markup 6 | module_function 7 | 8 | def parse_code(code, erm_buffer = ErmBuffer.new) 9 | erm_buffer.add_content(:r, 1, code.size + 1, 0, code.size, code) 10 | erm_buffer.parse 11 | end 12 | 13 | # Parses marked-up code and returns [raw_sexp, code] 14 | # raw_sexp follows the same format as the return value of ErmBuffer::Parser#parse. 15 | def parse_markup(str) 16 | code = +"" 17 | indent_stack = [] 18 | result = Hash.new { |h, k| h[k] = [] } 19 | last_tag = nil 20 | 21 | ss = StringScanner.new(str) 22 | loop do 23 | case 24 | when ss.scan(/«@(.)»/) 25 | indent_stack << ss[1] << code.size + 1 26 | when ss.scan(/«(\w+)»/) 27 | last_tag << code.size + 1 if last_tag && last_tag.size.odd? # close last_tag 28 | last_tag = (result[ss[1]] << code.size + 1) 29 | when ss.scan(%r|«/\w*»|) 30 | last_tag << code.size + 1 31 | when ss.scan(/[^«]+/), ss.scan(/«[^@\w]/) 32 | code << ss[0] 33 | when ss.eos? 34 | last_tag << code.size + 1 if last_tag && last_tag.size.odd? # close last_tag 35 | break 36 | else 37 | raise "Failed at #{ss.charpos}" 38 | end 39 | end 40 | 41 | result = result.sort_by { |k, v| k.to_i }.map { |k, v| 42 | "(%d %s)" % [k, v.join(" ")] if v 43 | } 44 | 45 | sexp = "((%s %s %s %s)%s)" % [code.size, 46 | 1, 47 | code.size + 1, 48 | indent_stack.join(' '), 49 | result.join] 50 | [sexp, code] 51 | end 52 | 53 | # Parses the return value of ErmBuffer::Parser#parse into Array 54 | def parse_sexp(str) 55 | paren_stack = [] 56 | result = [] 57 | 58 | ss = StringScanner.new(str) 59 | until ss.eos? 60 | case 61 | when ss.scan(/\(/) 62 | if paren_stack.empty? 63 | paren_stack.push(result) 64 | else 65 | paren_stack.push([]) 66 | end 67 | when ss.scan(/\)/) 68 | if paren_stack != [result] 69 | result.push(paren_stack.pop) 70 | end 71 | when ss.scan(/\s+/) 72 | # skip spaces 73 | when ss.scan(/([^\s()]+)/) 74 | paren_stack.last.push(ss[1]) 75 | else 76 | raise "Failed at #{ss.charpos}." 77 | end 78 | end 79 | 80 | result 81 | end 82 | 83 | def markup(code, parsed_sexp, options = {}) 84 | options = { 85 | :indent => true, 86 | :highlight => true, 87 | :close_tag => false, 88 | }.merge(options) 89 | 90 | indents, highlights = parsed_sexp[0][3..-1], parsed_sexp[1..-1] 91 | tags = [] # [["«tag»", insert_position], ... ] 92 | 93 | if options[:indent] 94 | indents.each_slice(2).each do |symbol, index| 95 | tags << ["«@#{symbol}»", index.to_i] 96 | end 97 | end 98 | 99 | if options[:highlight] 100 | highlights.map do |(id, *ranges)| 101 | ranges.each_slice(2) do |open, close| 102 | tags << ["«#{id}»", open.to_i] 103 | tags << ["«/#{id}»", close.to_i] if options[:close_tag] 104 | end 105 | end 106 | end 107 | 108 | markup = code.dup 109 | offset = 0 110 | tags.sort_by { |(tag, index)| 111 | # Sort tags like «/close»«@indent»«open» for human-readability. 112 | type = case tag[1] 113 | when "/" # close tag 114 | 0 115 | when "@" # indent tag 116 | 1 117 | else # open tag 118 | 2 119 | end 120 | [index, type] 121 | }.each do |(tag, index)| 122 | markup.insert(index - 1 + offset, tag) 123 | offset += tag.size 124 | end 125 | 126 | markup 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /test/test_erm_buffer.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | gem "minitest" 4 | require "minitest/autorun" 5 | 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..')) 7 | 8 | require 'ruby/erm_buffer.rb' 9 | require 'test/helper' 10 | 11 | class TestErmBuffer < Minitest::Test 12 | def parse_text(text,buf=ErmBuffer.new) 13 | buf.add_content(:r,1,text.size,0,text.size,text) 14 | buf.parse 15 | end 16 | 17 | def setup 18 | super 19 | ErmBuffer.set_extra_keywords({}) 20 | end 21 | 22 | def test_continations 23 | assert_parse(%q{ 24 | «0» 25 | a, 26 | «@c»b 27 | }) 28 | end 29 | 30 | def test_symbols 31 | assert_parse('«5»:aaa') 32 | assert_parse('«5»:@aa') 33 | assert_parse('«5»:@@a') 34 | assert_parse('«5»:$aa') 35 | assert_parse('«5»:<=>') 36 | assert_parse('«5»:aa') 37 | assert_parse('«5»:==') 38 | assert_parse('«5»:a') 39 | assert_parse('«5»:+') 40 | assert_parse('«5»:=') 41 | end 42 | 43 | def test_extra_keywords 44 | ErmBuffer.set_extra_keywords(%w[require]) 45 | assert_parse(%q{ 46 | «10»require«0» «7»'«1»abc«7»'«0» 47 | x.require z 48 | x. 49 | «@c»«10»require 50 | }) 51 | end 52 | 53 | def test_buffer_local_extra_keywords 54 | ErmBuffer.set_extra_keywords(%w[global]) 55 | local_buf=ErmBuffer.new 56 | local_buf.set_extra_keywords(%w[local]) 57 | assert_parse(%q{«0»global «10»local}, local_buf) 58 | end 59 | 60 | def test_reset_mode 61 | assert_parse(%q{ 62 | «0»a«12»=«11»<«0» e 145 | raise 146 | «@e»«10»end 147 | }) 148 | end 149 | 150 | def test_endless_def 151 | assert_parse(%q{ 152 | «@b»«10»def«0» «10»self«0».«9»m«0» «@e»«12»=«0» ident 153 | 154 | «@b»«10»def«0» «9»m«0» «@e»«12»=«0» ident 155 | 156 | «@b»«10»def«0» «9»m«@l»«0»(a«@r») «@e»«12»=«0» ident 157 | 158 | «@b»«10»def«0» «9»m«@l»«0»(«12»*«0»a, «12»**«0»k, «12»&«0»b«@r») «@e»«12»=«0» ident 159 | 160 | «@b»«10»def«0» «9»m«@l»«0»(«12»...«@r»«0») «@e»«12»=«0» ident 161 | 162 | «@b»«10»def«0» «9»m«0» «@e»«12»=«0» 163 | «@c» ident 164 | }) 165 | end 166 | 167 | def test_heredoc_followed_by_if_arg 168 | assert_parse(%q{ 169 | «0»bob«@l»(«11»<<-END«0», «@b»«10»if«0» a 170 | «1»fdssdfdsf 171 | dfsdfs" 172 | «11»END 173 | «0» 174 | fds 175 | «@s»«10»elsif«0» b 176 | fds 177 | «@e»«10»end«0» 178 | «@r») 179 | 180 | a «10»if«0» b 181 | c 182 | 183 | a«@l»(sdfdsf«@l»(«@r»), 184 | «@b»«10»if«0» fds 185 | dfs 186 | «@e»«10»end«0» 187 | «@r») 188 | }) 189 | end 190 | 191 | def test_if 192 | assert_parse(%q{ 193 | «0»a «10»if«0» dsf 194 | «@b»«10»if«0» a 195 | b 196 | «@s»«10»else«0» 197 | c 198 | «@e»«10»end«0» 199 | 200 | b«12»=«0»d «10»if«0» sdf 201 | 202 | a«10»;«0» «@b»«10»if«0» b 203 | c 204 | «@e»«10»end«0» 205 | 206 | a«@l»(b,«@b»«10»if«0» c 207 | d 208 | «@e»«10»end«0» 209 | «@r») 210 | 211 | a«@l»(«@b»«10»if«0» c 212 | d 213 | «@e»«10»end«0» 214 | «@r») 215 | 216 | a«12»=«@l»«0»{«5»a:«0» fds, 217 | «5»:b«0» «12»=>«0» fds 218 | «@r»} 219 | }) 220 | end 221 | 222 | def test_utf8_here_docs 223 | assert_parse(%q{ 224 | «3»@ü«12»=«11»<«0» c, 337 | «@r»} 338 | ]) 339 | end 340 | end 341 | -------------------------------------------------------------------------------- /tools/debug.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -w 2 | 3 | require_relative '../ruby/erm_buffer' 4 | 5 | trace = ARGV.delete "--trace" 6 | debug = ARGV.delete "-d" 7 | 8 | class ErmBuffer::Parser 9 | alias :old_realadd :realadd 10 | def realadd(sym,tok,len) 11 | x = old_realadd(sym, tok, len) 12 | k = sym =~ /^rem_/ ? :rem : sym 13 | v = ErmBuffer::FONT_LOCK_NAMES[k] || -1 14 | puts "%2d %-20p %3d %p" % [v, sym, len, tok] 15 | x 16 | end 17 | end 18 | 19 | if trace then 20 | require "tracer" 21 | Tracer.on 22 | end 23 | 24 | ARGV.each do |file| 25 | buf = ErmBuffer.new 26 | buf.debug = true if debug 27 | content = File.read file 28 | point_min, point_max, pbeg, len = 1, content.size+1, 0, content.size 29 | 30 | buf.add_content :x, point_min, point_max, pbeg, len, content 31 | 32 | puts buf.parse 33 | end 34 | -------------------------------------------------------------------------------- /tools/lexer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -w 2 | 3 | require "ripper" 4 | require "pp" 5 | 6 | ARGV.each do |p| 7 | f = File.read p 8 | puts 9 | pp Ripper.lex f 10 | puts 11 | pp Ripper.sexp_raw f 12 | puts 13 | pp Ripper.sexp f 14 | end 15 | -------------------------------------------------------------------------------- /tools/markup.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -w 2 | 3 | require_relative '../test/markup' 4 | 5 | if ARGV.delete("--help") 6 | puts <<-HERE 7 | Usage: ruby #{__FILE__} 8 | 9 | Options: 10 | --help 11 | --no-indent 12 | --no-highlight 13 | HERE 14 | 15 | exit 16 | end 17 | 18 | options = { 19 | indent: !ARGV.delete("--no-indent"), 20 | highlight: !ARGV.delete("--no-highlight") 21 | } 22 | 23 | src = ARGF.read 24 | sexp = Markup.parse_code(src) 25 | markup = Markup.markup(src, Markup.parse_sexp(sexp), options) 26 | 27 | puts sexp 28 | puts "---" 29 | puts markup 30 | --------------------------------------------------------------------------------