├── README.md └── ruby-electric.el /README.md: -------------------------------------------------------------------------------- 1 | # elisp-ruby-electric 2 | -------------------------------------------------------------------------------- /ruby-electric.el: -------------------------------------------------------------------------------- 1 | ;;; ruby-electric.el --- Minor mode for electrically editing ruby code 2 | ;; 3 | ;; Authors: Dee Zsombor 4 | ;; Yukihiro Matsumoto 5 | ;; Nobuyoshi Nakada 6 | ;; Akinori MUSHA 7 | ;; Jakub Kuźma 8 | ;; Maintainer: Akinori MUSHA 9 | ;; Created: 6 Mar 2005 10 | ;; URL: https://github.com/ruby/elisp-ruby-electric 11 | ;; Keywords: languages ruby 12 | ;; License: The same license terms as Ruby 13 | ;; Version: 2.3.3 14 | 15 | ;;; Commentary: 16 | ;; 17 | ;; `ruby-electric-mode' accelerates code writing in ruby by making 18 | ;; some keys "electric" and automatically supplying with closing 19 | ;; parentheses and "end" as appropriate. 20 | ;; 21 | ;; This work was originally inspired by a code snippet posted by 22 | ;; [Frederick Ros](https://github.com/sleeper). 23 | ;; 24 | ;; Add the following line to enable ruby-electric-mode under 25 | ;; ruby-mode. 26 | ;; 27 | ;; (eval-after-load "ruby-mode" 28 | ;; '(add-hook 'ruby-mode-hook 'ruby-electric-mode)) 29 | ;; 30 | ;; Or add the following line for enh-ruby-mode. 31 | ;; 32 | ;; (eval-after-load "enh-ruby-mode" 33 | ;; '(add-hook 'enh-ruby-mode-hook 'ruby-electric-mode)) 34 | ;; 35 | ;; Type M-x customize-group ruby-electric for configuration. 36 | 37 | ;;; Code: 38 | 39 | (require 'ruby-mode) 40 | 41 | (eval-when-compile 42 | (require 'cl-lib)) 43 | 44 | (defgroup ruby-electric nil 45 | "Minor mode providing electric editing commands for ruby files." 46 | :group 'ruby) 47 | 48 | (defconst ruby-electric-expandable-bar-re 49 | "\\s-\\(do\\|{\\)\\s-*|") 50 | 51 | (defconst ruby-electric-delimiters-alist 52 | '((?\{ :name "Curly brace" :handler ruby-electric-curlies :closing ?\}) 53 | (?\[ :name "Square brace" :handler ruby-electric-matching-char :closing ?\]) 54 | (?\( :name "Round brace" :handler ruby-electric-matching-char :closing ?\)) 55 | (?\' :name "Quote" :handler ruby-electric-matching-char) 56 | (?\" :name "Double quote" :handler ruby-electric-matching-char) 57 | (?\` :name "Back quote" :handler ruby-electric-matching-char) 58 | (?\| :name "Vertical bar" :handler ruby-electric-bar) 59 | (?\# :name "Hash" :handler ruby-electric-hash))) 60 | 61 | (defvar ruby-electric-matching-delimeter-alist 62 | (apply 'nconc 63 | (mapcar #'(lambda (x) 64 | (let ((delim (car x)) 65 | (plist (cdr x))) 66 | (if (eq (plist-get plist :handler) 'ruby-electric-matching-char) 67 | (list (cons delim (or (plist-get plist :closing) 68 | delim)))))) 69 | ruby-electric-delimiters-alist))) 70 | 71 | (defvar ruby-electric-expandable-keyword-re) 72 | 73 | (defmacro ruby-electric--try-insert-and-do (string &rest body) 74 | (declare (indent 1)) 75 | `(let ((before (point)) 76 | (after (progn 77 | (insert ,string) 78 | (point)))) 79 | (unwind-protect 80 | (progn ,@body) 81 | (delete-region before after) 82 | (goto-char before)))) 83 | 84 | (defconst ruby-modifier-beg-symbol-re 85 | (regexp-opt ruby-modifier-beg-keywords 'symbols)) 86 | 87 | (defun ruby-electric--modifier-keyword-at-point-p () 88 | "Test if there is a modifier keyword at point." 89 | (and (not (looking-back "\\." nil)) 90 | (looking-at ruby-modifier-beg-symbol-re) 91 | (let ((end (match-end 1))) 92 | (save-excursion 93 | (let ((indent1 (ruby-electric--try-insert-and-do "\n" 94 | (ruby-electric-calculate-indent))) 95 | (indent2 (save-excursion 96 | (goto-char end) 97 | (ruby-electric--try-insert-and-do " x\n" 98 | (ruby-electric-calculate-indent))))) 99 | (and indent1 indent2 100 | (= indent1 indent2))))))) 101 | 102 | (defconst ruby-block-mid-symbol-re 103 | (regexp-opt ruby-block-mid-keywords 'symbols)) 104 | 105 | (defun ruby-electric--block-mid-keyword-at-point-p () 106 | "Test if there is a block mid keyword at point." 107 | (and (looking-at ruby-block-mid-symbol-re) 108 | (looking-back "^\\s-*" nil))) 109 | 110 | (defconst ruby-block-beg-symbol-re 111 | (regexp-opt ruby-block-beg-keywords 'symbols)) 112 | 113 | (defun ruby-electric--block-beg-keyword-at-point-p () 114 | "Test if there is a block beginning keyword at point." 115 | (and (looking-at ruby-block-beg-symbol-re) 116 | (if (string= (match-string 1) "do") 117 | (looking-back "\\s-" nil) 118 | (not (looking-back "\\." nil))) 119 | ;; (not (ruby-electric--modifier-keyword-at-point-p)) ;; implicit assumption 120 | )) 121 | 122 | (defcustom ruby-electric-keywords-alist 123 | '(("begin" . end) 124 | ("case" . end) 125 | ("class" . end) 126 | ("def" . end) 127 | ("do" . end) 128 | ("else" . reindent) 129 | ("elsif" . reindent) 130 | ("end" . reindent) 131 | ("ensure" . reindent) 132 | ("for" . end) 133 | ("if" . end) 134 | ("module" . end) 135 | ("rescue" . reindent) 136 | ("unless" . end) 137 | ("until" . end) 138 | ("when" . reindent) 139 | ("while" . end)) 140 | "Alist of keywords and actions to define how to react to space 141 | or return right after each keyword. In each (KEYWORD . ACTION) 142 | cons, ACTION can be set to one of the following values: 143 | 144 | `reindent' Reindent the line. 145 | 146 | `end' Reindent the line and auto-close the keyword with 147 | end if applicable. 148 | 149 | `nil' Do nothing. 150 | " 151 | :type '(repeat (cons (string :tag "Keyword") 152 | (choice :tag "Action" 153 | :menu-tag "Action" 154 | (const :tag "Auto-close with end" 155 | :value end) 156 | (const :tag "Auto-reindent" 157 | :value reindent) 158 | (const :tag "None" 159 | :value nil)))) 160 | :set (lambda (sym val) 161 | (set sym val) 162 | (let (keywords) 163 | (dolist (x val) 164 | (let ((keyword (car x)) 165 | (action (cdr x))) 166 | (if action 167 | (setq keywords (cons keyword keywords))))) 168 | (setq ruby-electric-expandable-keyword-re 169 | (concat (regexp-opt keywords 'symbols) 170 | "$")))) 171 | :group 'ruby-electric) 172 | 173 | (defvar ruby-electric-mode-map 174 | (let ((map (make-sparse-keymap))) 175 | (define-key map " " 'ruby-electric-space/return) 176 | (define-key map [remap delete-backward-char] 'ruby-electric-delete-backward-char) 177 | (define-key map [remap newline] 'ruby-electric-space/return) 178 | (define-key map [remap newline-and-indent] 'ruby-electric-space/return) 179 | (define-key map [remap electric-newline-and-maybe-indent] 'ruby-electric-space/return) 180 | (define-key map [remap reindent-then-newline-and-indent] 'ruby-electric-space/return) 181 | (dolist (x ruby-electric-delimiters-alist) 182 | (let* ((delim (car x)) 183 | (plist (cdr x)) 184 | (name (plist-get plist :name)) 185 | (func (plist-get plist :handler)) 186 | (closing (plist-get plist :closing))) 187 | (define-key map (char-to-string delim) func) 188 | (if closing 189 | (define-key map (char-to-string closing) 'ruby-electric-closing-char)))) 190 | map) 191 | "Keymap used in ruby-electric-mode.") 192 | 193 | (defcustom ruby-electric-expand-delimiters-list '(all) 194 | "*List of contexts where matching delimiter should be inserted. 195 | The word `all' will do all insertions." 196 | :type `(set :extra-offset 8 197 | (const :tag "Everything" all) 198 | ,@(apply 'list 199 | (mapcar #'(lambda (x) 200 | `(const :tag ,(plist-get (cdr x) :name) 201 | ,(car x))) 202 | ruby-electric-delimiters-alist))) 203 | :group 'ruby-electric) 204 | 205 | (defcustom ruby-electric-newline-before-closing-bracket nil 206 | "*Non-nil means a newline should be inserted before an 207 | automatically inserted closing bracket." 208 | :type 'boolean :group 'ruby-electric) 209 | 210 | (defcustom ruby-electric-autoindent-on-closing-char nil 211 | "*Non-nil means the current line should be automatically 212 | indented when a closing character is manually typed in." 213 | :type 'boolean :group 'ruby-electric) 214 | 215 | (defvar ruby-electric-mode-hook nil 216 | "Called after `ruby-electric-mode' is turned on.") 217 | 218 | ;;;###autoload 219 | (define-minor-mode ruby-electric-mode 220 | "Toggle Ruby Electric minor mode. 221 | With no argument, this command toggles the mode. Non-null prefix 222 | argument turns on the mode. Null prefix argument turns off the 223 | mode. 224 | 225 | When Ruby Electric mode is enabled, an indented `end' is 226 | heuristicaly inserted whenever typing a word like `module', 227 | `class', `def', `if', `unless', `case', `until', `for', `begin', 228 | `do' followed by a space. Single, double and back quotes as well 229 | as braces are paired auto-magically. Expansion does not occur 230 | inside comments and strings. Note that you must have Font Lock 231 | enabled." 232 | :init-value nil 233 | :lighter " REl" 234 | :keymap ruby-electric-mode-map 235 | (if ruby-electric-mode 236 | (run-hooks 'ruby-electric-mode-hook))) 237 | 238 | (defun ruby-electric-space/return-fallback () 239 | (if (or (eq this-original-command 'ruby-electric-space/return) 240 | (null (ignore-errors 241 | ;; ac-complete may fail if there is nothing left to complete 242 | (call-interactively this-original-command) 243 | (setq this-command this-original-command)))) 244 | ;; fall back to a globally bound command 245 | (let ((command (global-key-binding (char-to-string last-command-event) t))) 246 | (and command 247 | (call-interactively (setq this-command command)))))) 248 | 249 | (defun ruby-electric-indent-line (&optional ignored) 250 | (if (eq major-mode 'enh-ruby-mode) 251 | (progn 252 | (declare-function enh-ruby-indent-line 'enh-ruby-mode) 253 | (enh-ruby-indent-line ignored)) 254 | (ruby-indent-line ignored))) 255 | 256 | (defun ruby-electric-calculate-indent (&optional start-point) 257 | (if (eq major-mode 'enh-ruby-mode) 258 | (progn 259 | (declare-function enh-ruby-calculate-indent 'enh-ruby-mode) 260 | (enh-ruby-calculate-indent start-point)) 261 | (ruby-calculate-indent start-point))) 262 | 263 | (defun ruby-electric-space/return (arg) 264 | (interactive "*P") 265 | (defvar sp-delayed-pair) ;defined in smartparens.el 266 | (and (boundp 'sp-last-operation) 267 | (setq sp-delayed-pair nil)) 268 | (cond ((or arg 269 | (region-active-p)) 270 | (or (= last-command-event ?\s) 271 | (setq last-command-event ?\n)) 272 | (ruby-electric-replace-region-or-insert)) 273 | ((ruby-electric-space/return-can-be-expanded-p) 274 | (let (action) 275 | (save-excursion 276 | (goto-char (match-beginning 0)) 277 | (let* ((keyword (match-string 1)) 278 | (allowed-actions 279 | (cond ((ruby-electric--modifier-keyword-at-point-p) 280 | '(reindent)) ;; no end necessary 281 | ((ruby-electric--block-mid-keyword-at-point-p) 282 | '(reindent)) ;; ditto 283 | ((ruby-electric--block-beg-keyword-at-point-p) 284 | '(end reindent))))) 285 | (if allowed-actions 286 | (setq action 287 | (let ((action (cdr (assoc keyword ruby-electric-keywords-alist)))) 288 | (and (memq action allowed-actions) 289 | action)))))) 290 | (cond ((eq action 'end) 291 | (ruby-electric-indent-line) 292 | (save-excursion 293 | (newline) 294 | (ruby-insert-end))) 295 | ((eq action 'reindent) 296 | (ruby-electric-indent-line))) 297 | (ruby-electric-space/return-fallback))) 298 | ((and (eq this-original-command 'newline-and-indent) 299 | (ruby-electric-comment-at-point-p)) 300 | (call-interactively (setq this-command 'comment-indent-new-line))) 301 | (t 302 | (ruby-electric-space/return-fallback)))) 303 | 304 | (defun ruby-electric--get-faces-at-point () 305 | (let* ((point (point)) 306 | (value (or 307 | (get-text-property point 'read-face-name) 308 | (get-text-property point 'face)))) 309 | (if (listp value) value (list value)))) 310 | 311 | (defun ruby-electric--faces-include-p (pfaces &rest faces) 312 | (and ruby-electric-mode 313 | (cl-loop for face in faces 314 | thereis (memq face pfaces)))) 315 | 316 | (defun ruby-electric--faces-at-point-include-p (&rest faces) 317 | (apply 'ruby-electric--faces-include-p 318 | (ruby-electric--get-faces-at-point) 319 | faces)) 320 | 321 | (defun ruby-electric-code-face-p (faces) 322 | (not (ruby-electric--faces-include-p 323 | faces 324 | 'font-lock-string-face 325 | 'font-lock-comment-face 326 | 'enh-ruby-string-delimiter-face 327 | 'enh-ruby-heredoc-delimiter-face 328 | 'enh-ruby-regexp-delimiter-face 329 | 'enh-ruby-regexp-face))) 330 | 331 | (defun ruby-electric-code-at-point-p () 332 | (ruby-electric-code-face-p 333 | (ruby-electric--get-faces-at-point))) 334 | 335 | (defun ruby-electric-string-face-p (faces) 336 | (ruby-electric--faces-include-p 337 | faces 338 | 'font-lock-string-face 339 | 'enh-ruby-string-delimiter-face 340 | 'enh-ruby-heredoc-delimiter-face 341 | 'enh-ruby-regexp-delimiter-face 342 | 'enh-ruby-regexp-face)) 343 | 344 | (defun ruby-electric-string-at-point-p () 345 | (ruby-electric-string-face-p 346 | (ruby-electric--get-faces-at-point))) 347 | 348 | (defun ruby-electric-comment-at-point-p () 349 | (ruby-electric--faces-at-point-include-p 350 | 'font-lock-comment-face)) 351 | 352 | (defun ruby-electric-escaped-p() 353 | (let ((f nil)) 354 | (save-excursion 355 | (while (char-equal ?\\ (preceding-char)) 356 | (backward-char 1) 357 | (setq f (not f)))) 358 | f)) 359 | 360 | (defun ruby-electric-command-char-expandable-punct-p(char) 361 | (or (memq 'all ruby-electric-expand-delimiters-list) 362 | (memq char ruby-electric-expand-delimiters-list))) 363 | 364 | (defun ruby-electric-space/return-can-be-expanded-p() 365 | (and (ruby-electric-code-at-point-p) 366 | (looking-back ruby-electric-expandable-keyword-re nil))) 367 | 368 | (defun ruby-electric-replace-region-or-insert () 369 | (and (region-active-p) 370 | (bound-and-true-p delete-selection-mode) 371 | (fboundp 'delete-selection-helper) 372 | (delete-selection-helper (get 'self-insert-command 'delete-selection))) 373 | (insert (make-string (prefix-numeric-value current-prefix-arg) 374 | last-command-event)) 375 | (setq this-command 'self-insert-command)) 376 | 377 | (defmacro ruby-electric-insert (arg &rest body) 378 | `(cond ((and 379 | (null ,arg) 380 | (ruby-electric-command-char-expandable-punct-p last-command-event)) 381 | (let ((region-beginning 382 | (cond ((region-active-p) 383 | (prog1 384 | (save-excursion 385 | (goto-char (region-beginning)) 386 | (insert last-command-event) 387 | (point)) 388 | (goto-char (region-end)))) 389 | (t 390 | (insert last-command-event) 391 | nil))) 392 | (faces-at-point 393 | (ruby-electric--get-faces-at-point))) 394 | ,@body 395 | (and region-beginning 396 | ;; If no extra character is inserted, go back to the 397 | ;; region beginning. 398 | (eq this-command 'self-insert-command) 399 | (goto-char region-beginning)))) 400 | ((ruby-electric-replace-region-or-insert)))) 401 | 402 | (defun ruby-electric-curlies (arg) 403 | (interactive "*P") 404 | (ruby-electric-insert 405 | arg 406 | (cond 407 | ((or (ruby-electric-code-at-point-p) 408 | (ruby-electric--faces-include-p 409 | faces-at-point 410 | 'enh-ruby-string-delimiter-face 411 | 'enh-ruby-regexp-delimiter-face)) 412 | (save-excursion 413 | (insert "}") 414 | (font-lock-fontify-region (line-beginning-position) (point))) 415 | (cond 416 | ((or (ruby-electric-string-at-point-p) ;; %w{}, %r{}, etc. 417 | (looking-back "%[QqWwRrxIis]{" nil)) 418 | (if region-beginning 419 | (forward-char 1))) 420 | (ruby-electric-newline-before-closing-bracket 421 | (cond (region-beginning 422 | (save-excursion 423 | (goto-char region-beginning) 424 | (newline)) 425 | (newline) 426 | (forward-char 1) 427 | (indent-region region-beginning (line-end-position))) 428 | (t 429 | (insert " ") 430 | (save-excursion 431 | (newline) 432 | (ruby-electric-indent-line t))))) 433 | (t 434 | (if region-beginning 435 | (save-excursion 436 | (goto-char region-beginning) 437 | (insert " ")) 438 | (insert " ")) 439 | (insert " ") 440 | (backward-char 1) 441 | (and region-beginning 442 | (forward-char 1))))) 443 | ((ruby-electric-string-at-point-p) 444 | (let ((start-position (1- (or region-beginning (point))))) 445 | (cond 446 | ((char-equal ?\# (char-before start-position)) 447 | (unless (save-excursion 448 | (goto-char (1- start-position)) 449 | (ruby-electric-escaped-p)) 450 | (insert "}") 451 | (or region-beginning 452 | (backward-char 1)))) 453 | ((or 454 | (ruby-electric-command-char-expandable-punct-p ?\#) 455 | (save-excursion 456 | (goto-char start-position) 457 | (ruby-electric-escaped-p))) 458 | (if region-beginning 459 | (goto-char region-beginning)) 460 | (setq this-command 'self-insert-command)) 461 | (t 462 | (save-excursion 463 | (goto-char start-position) 464 | (insert "#")) 465 | (insert "}") 466 | (or region-beginning 467 | (backward-char 1)))))) 468 | (t 469 | (delete-char -1) 470 | (ruby-electric-replace-region-or-insert))))) 471 | 472 | (defun ruby-electric-hash (arg) 473 | (interactive "*P") 474 | (ruby-electric-insert 475 | arg 476 | (if (ruby-electric-string-at-point-p) 477 | (let ((start-position (1- (or region-beginning (point))))) 478 | (cond 479 | ((char-equal (following-char) ?')) ;; likely to be in '' 480 | ((save-excursion 481 | (goto-char start-position) 482 | (ruby-electric-escaped-p))) 483 | (region-beginning 484 | (save-excursion 485 | (goto-char (1+ start-position)) 486 | (insert "{")) 487 | (insert "}")) 488 | (t 489 | (insert "{") 490 | (save-excursion 491 | (insert "}"))))) 492 | (delete-char -1) 493 | (ruby-electric-replace-region-or-insert)))) 494 | 495 | (defun ruby-electric-matching-char (arg) 496 | (interactive "*P") 497 | (ruby-electric-insert 498 | arg 499 | (let ((closing (cdr (assoc last-command-event 500 | ruby-electric-matching-delimeter-alist)))) 501 | (cond 502 | ;; quotes 503 | ((char-equal closing last-command-event) 504 | (cond ((not (ruby-electric-string-face-p faces-at-point)) 505 | (if region-beginning 506 | ;; escape quotes of the same kind, backslash and hash 507 | (let ((re (format "[%c\\%s]" 508 | last-command-event 509 | (if (char-equal last-command-event ?\") 510 | "#" ""))) 511 | (bound (point))) 512 | (save-excursion 513 | (goto-char region-beginning) 514 | (while (re-search-forward re bound t) 515 | (let ((end (point))) 516 | (replace-match "\\\\\\&") 517 | (setq bound (+ bound (- (point) end)))))))) 518 | (insert closing) 519 | (or region-beginning 520 | (backward-char 1))) 521 | (t 522 | (and (eq last-command 'ruby-electric-matching-char) 523 | (char-equal (following-char) closing) ;; repeated quotes 524 | (delete-char 1)) 525 | (setq this-command 'self-insert-command)))) 526 | ((ruby-electric-code-at-point-p) 527 | (insert closing) 528 | (or region-beginning 529 | (backward-char 1))))))) 530 | 531 | (defun ruby-electric-closing-char(arg) 532 | (interactive "*P") 533 | (cond 534 | (arg 535 | (ruby-electric-replace-region-or-insert)) 536 | ((and 537 | (eq last-command 'ruby-electric-curlies) 538 | (= last-command-event ?}) 539 | (not (char-equal (preceding-char) last-command-event))) ;; {} 540 | (if (char-equal (following-char) ?\n) (delete-char 1)) 541 | (delete-horizontal-space) 542 | (forward-char)) 543 | ((and 544 | (= last-command-event (following-char)) 545 | (not (char-equal (preceding-char) last-command-event)) 546 | (memq last-command '(ruby-electric-matching-char 547 | ruby-electric-closing-char))) ;; ()/[] and (())/[[]] 548 | (forward-char)) 549 | (t 550 | (ruby-electric-replace-region-or-insert) 551 | (if ruby-electric-autoindent-on-closing-char 552 | (ruby-electric-indent-line))))) 553 | 554 | (defun ruby-electric-bar(arg) 555 | (interactive "*P") 556 | (ruby-electric-insert 557 | arg 558 | (cond ((and (ruby-electric-code-at-point-p) 559 | (looking-back ruby-electric-expandable-bar-re nil)) 560 | (save-excursion (insert "|"))) 561 | (t 562 | (delete-char -1) 563 | (ruby-electric-replace-region-or-insert))))) 564 | 565 | (defun ruby-electric-delete-backward-char(arg) 566 | (interactive "*p") 567 | (cond ((memq last-command '(ruby-electric-matching-char 568 | ruby-electric-bar)) 569 | (delete-char 1)) 570 | ((eq last-command 'ruby-electric-curlies) 571 | (cond ((eolp) 572 | (cond ((char-equal (preceding-char) ?\s) 573 | (setq this-command last-command)) 574 | ((char-equal (preceding-char) ?{) 575 | (and (looking-at "[ \t\n]*}") 576 | (delete-char (- (match-end 0) (match-beginning 0))))))) 577 | ((char-equal (following-char) ?\s) 578 | (setq this-command last-command) 579 | (delete-char 1)) 580 | ((char-equal (following-char) ?}) 581 | (delete-char 1)))) 582 | ((eq last-command 'ruby-electric-hash) 583 | (and (char-equal (preceding-char) ?{) 584 | (delete-char 1)))) 585 | (delete-char (- arg))) 586 | 587 | (put 'ruby-electric-delete-backward-char 'delete-selection 'supersede) 588 | 589 | (defun ruby-insert-end () 590 | (interactive) 591 | (if (eq (char-syntax (preceding-char)) ?w) 592 | (insert " ")) 593 | (insert "end") 594 | (save-excursion 595 | (if (eq (char-syntax (following-char)) ?w) 596 | (insert " ")) 597 | (ruby-indent-line t) 598 | (end-of-line))) 599 | 600 | (provide 'ruby-electric) 601 | 602 | ;;; ruby-electric.el ends here 603 | --------------------------------------------------------------------------------