X-Git-Url: https://code.delx.au/gnu-emacs/blobdiff_plain/2ea53115b435f84ce093b7cb9ee5e7ce31120349..7e09ef09a479731d01b1ca46e94ddadd73ac98e3:/lisp/progmodes/ruby-mode.el diff --git a/lisp/progmodes/ruby-mode.el b/lisp/progmodes/ruby-mode.el index 2d13cc7cc4..d1e42ca344 100644 --- a/lisp/progmodes/ruby-mode.el +++ b/lisp/progmodes/ruby-mode.el @@ -1,6 +1,6 @@ ;;; ruby-mode.el --- Major mode for editing Ruby files -;; Copyright (C) 1994-2013 Free Software Foundation, Inc. +;; Copyright (C) 1994-2015 Free Software Foundation, Inc. ;; Authors: Yukihiro Matsumoto ;; Nobuyoshi Nakada @@ -39,8 +39,6 @@ ;;; Code: -(eval-when-compile (require 'cl)) - (defgroup ruby nil "Major mode for editing Ruby code." :prefix "ruby-" @@ -108,7 +106,7 @@ "Regexp to match the beginning of a heredoc.") (defconst ruby-expression-expansion-re - "\\(?:[^\\]\\|\\=\\)\\(\\\\\\\\\\)*\\(#\\({[^}\n\\\\]*\\(\\\\.[^}\n\\\\]*\\)*}\\|\\(\\$\\|@\\|@@\\)\\(\\w\\|_\\)+\\)\\)")) + "\\(?:[^\\]\\|\\=\\)\\(\\\\\\\\\\)*\\(#\\({[^}\n\\\\]*\\(\\\\.[^}\n\\\\]*\\)*}\\|\\(\\$\\|@\\|@@\\)\\(\\w\\|_\\)+\\|\\$[^a-zA-Z \n]\\)\\)")) (defun ruby-here-doc-end-match () "Return a regexp to find the end of a heredoc. @@ -154,6 +152,7 @@ This should only be called after matching against `ruby-here-doc-beg-re'." (define-key map (kbd "M-C-p") 'ruby-beginning-of-block) (define-key map (kbd "M-C-n") 'ruby-end-of-block) (define-key map (kbd "C-c {") 'ruby-toggle-block) + (define-key map (kbd "C-c '") 'ruby-toggle-string-quotes) map) "Keymap used in Ruby mode.") @@ -166,6 +165,8 @@ This should only be called after matching against `ruby-here-doc-beg-re'." ["End of Block" ruby-end-of-block t] ["Toggle Block" ruby-toggle-block t] "--" + ["Toggle String Quotes" ruby-toggle-string-quotes t] + "--" ["Backward Sexp" ruby-backward-sexp :visible (not ruby-use-smie)] ["Backward Sexp" backward-sexp @@ -188,7 +189,6 @@ This should only be called after matching against `ruby-here-doc-beg-re'." (modify-syntax-entry ?\n ">" table) (modify-syntax-entry ?\\ "\\" table) (modify-syntax-entry ?$ "." table) - (modify-syntax-entry ?? "_" table) (modify-syntax-entry ?_ "_" table) (modify-syntax-entry ?: "_" table) (modify-syntax-entry ?< "." table) @@ -229,22 +229,84 @@ This should only be called after matching against `ruby-here-doc-beg-re'." :group 'ruby :safe 'integerp) +(defconst ruby-alignable-keywords '(if while unless until begin case for def) + "Keywords that can be used in `ruby-align-to-stmt-keywords'.") + +(defcustom ruby-align-to-stmt-keywords '(def) + "Keywords after which we align the expression body to statement. + +When nil, an expression that begins with one these keywords is +indented to the column of the keyword. Example: + + tee = if foo + bar + else + qux + end + +If this value is t or contains a symbol with the name of given +keyword, the expression is indented to align to the beginning of +the statement: + + tee = if foo + bar + else + qux + end + +Only has effect when `ruby-use-smie' is t. +" + :type `(choice + (const :tag "None" nil) + (const :tag "All" t) + (repeat :tag "User defined" + (choice ,@(mapcar + (lambda (kw) (list 'const kw)) + ruby-alignable-keywords)))) + :group 'ruby + :safe 'listp + :version "24.4") + +(defcustom ruby-align-chained-calls nil + "If non-nil, align chained method calls. + +Each method call on a separate line will be aligned to the column +of its parent. + +Only has effect when `ruby-use-smie' is t." + :type 'boolean + :group 'ruby + :safe 'booleanp + :version "24.4") + (defcustom ruby-deep-arglist t "Deep indent lists in parenthesis when non-nil. -Also ignores spaces after parenthesis when 'space." +Also ignores spaces after parenthesis when `space'. +Only has effect when `ruby-use-smie' is nil." :type 'boolean :group 'ruby :safe 'booleanp) +;; FIXME Woefully under documented. What is the point of the last `t'?. (defcustom ruby-deep-indent-paren '(?\( ?\[ ?\] t) "Deep indent lists in parenthesis when non-nil. The value t means continuous line. -Also ignores spaces after parenthesis when 'space." +Also ignores spaces after parenthesis when `space'. +Only has effect when `ruby-use-smie' is nil." + :type '(choice (const nil) + character + (repeat (choice character + (cons character (choice (const nil) + (const t))) + (const t) ; why? + ))) :group 'ruby) (defcustom ruby-deep-indent-paren-style 'space - "Default deep indent style." - :options '(t nil space) :group 'ruby) + "Default deep indent style. +Only has effect when `ruby-use-smie' is nil." + :type '(choice (const t) (const nil) (const space)) + :group 'ruby) (defcustom ruby-encoding-map '((us-ascii . nil) ;; Do not put coding: us-ascii @@ -258,9 +320,30 @@ explicitly declared in magic comment." :group 'ruby) (defcustom ruby-insert-encoding-magic-comment t - "Insert a magic Emacs 'coding' comment upon save if this is non-nil." + "Insert a magic Ruby encoding comment upon save if this is non-nil. +The encoding will be auto-detected. The format of the encoding comment +is customizable via `ruby-encoding-magic-comment-style'. + +When set to `always-utf8' an utf-8 comment will always be added, +even if it's not required." :type 'boolean :group 'ruby) +(defcustom ruby-encoding-magic-comment-style 'ruby + "The style of the magic encoding comment to use." + :type '(choice + (const :tag "Emacs Style" emacs) + (const :tag "Ruby Style" ruby) + (const :tag "Custom Style" custom)) + :group 'ruby + :version "24.4") + +(defcustom ruby-custom-encoding-magic-comment-template "# encoding: %s" + "A custom encoding comment template. +It is used when `ruby-encoding-magic-comment-style' is set to `custom'." + :type 'string + :group 'ruby + :version "24.4") + (defcustom ruby-use-encoding-map t "Use `ruby-encoding-map' to set encoding magic comment if this is non-nil." :type 'boolean :group 'ruby) @@ -277,12 +360,15 @@ explicitly declared in magic comment." (smie-bnf->prec2 '((id) (insts (inst) (insts ";" insts)) - (inst (exp) (inst "iuwu-mod" exp)) + (inst (exp) (inst "iuwu-mod" exp) + ;; Somewhat incorrect (both can be used multiple times), + ;; but avoids lots of conflicts: + (exp "and" exp) (exp "or" exp)) (exp (exp1) (exp "," exp) (exp "=" exp) - (id " @ " exp) - (exp "." exp)) + (id " @ " exp)) (exp1 (exp2) (exp2 "?" exp1 ":" exp1)) - (exp2 ("def" insts "end") + (exp2 (exp3) (exp3 "." exp2)) + (exp3 ("def" insts "end") ("begin" insts-rescue-insts "end") ("do" insts "end") ("class" insts "end") ("module" insts "end") @@ -295,10 +381,10 @@ explicitly declared in magic comment." ("unless" insts "end") ("if" if-body "end") ("case" cases "end")) - (formal-params ("opening-|" exp "|")) + (formal-params ("opening-|" exp "closing-|")) (for-body (for-head ";" insts)) (for-head (id "in" exp)) - (cases (exp "then" insts) ;; FIXME: Ruby also allows (exp ":" insts). + (cases (exp "then" insts) (cases "when" cases) (insts "else" insts)) (expseq (exp) );;(expseq "," expseq) (hashvals (id "=>" exp1) (hashvals "," hashvals)) @@ -309,7 +395,7 @@ explicitly declared in magic comment." (ielsei (itheni) (itheni "else" insts)) (if-body (ielsei) (if-body "elsif" if-body))) '((nonassoc "in") (assoc ";") (right " @ ") - (assoc ",") (right "=") (assoc ".")) + (assoc ",") (right "=")) '((assoc "when")) '((assoc "elsif")) '((assoc "rescue" "ensure")) @@ -322,14 +408,14 @@ explicitly declared in magic comment." (left ".." "...") (left "+" "-") (left "*" "/" "%" "**") - ;; (left "|") ; FIXME: Conflicts with | after block parameters. - (left "^" "&") + (left "&&" "||") + (left "^" "&" "|") (nonassoc "<=>") (nonassoc ">" ">=" "<" "<=") (nonassoc "==" "===" "!=") (nonassoc "=~" "!~") (left "<<" ">>") - (left "&&" "||")))))) + (right ".")))))) (defun ruby-smie--bosp () (save-excursion (skip-chars-backward " \t") @@ -339,18 +425,25 @@ explicitly declared in magic comment." (save-excursion (skip-chars-backward " \t") (not (or (bolp) + (memq (char-before) '(?\[ ?\()) (and (memq (char-before) - '(?\; ?- ?+ ?* ?/ ?: ?. ?, ?\[ ?\( ?\{ ?\\ ?& ?> ?< ?% ?~)) - ;; Make sure it's not the end of a regexp. - (not (eq (car (syntax-after (1- (point)))) 7))) + '(?\; ?- ?+ ?* ?/ ?: ?. ?, ?\\ ?& ?> ?< ?% ?~ ?^)) + ;; Not a binary operator symbol. + (not (eq (char-before (1- (point))) ?:)) + ;; Not the end of a regexp or a percent literal. + (not (memq (car (syntax-after (1- (point)))) '(7 15)))) (and (eq (char-before) ?\?) (equal (save-excursion (ruby-smie--backward-token)) "?")) (and (eq (char-before) ?=) + ;; Not a symbol :==, :!=, or a foo= method. (string-match "\\`\\s." (save-excursion (ruby-smie--backward-token)))) + (and (eq (char-before) ?|) + (member (save-excursion (ruby-smie--backward-token)) + '("|" "||"))) (and (eq (car (syntax-after (1- (point)))) 2) - (equal (save-excursion (ruby-smie--backward-token)) - "iuwu-mod")) + (member (save-excursion (ruby-smie--backward-token)) + '("iuwu-mod" "and" "or"))) (save-excursion (forward-comment 1) (eq (char-after) ?.)))))) @@ -367,6 +460,12 @@ explicitly declared in magic comment." (or (eq ?\{ (char-before)) (looking-back "\\_" "=" "return" ";"))) + (not (smie-rule-prev-p "(" "{" "[" "," "=>" "=" "return" ";")) + (save-excursion + (forward-comment -1) + (not (eq (preceding-char) ?:)))) ;; Curly block opener. - (smie-rule-parent)) + (ruby-smie--indent-to-stmt)) ((smie-rule-hanging-p) ;; Treat purely syntactic block-constructs as being part of their parent, - ;; when the opening statement is hanging. - (let ((state (smie-backward-sexp 'halfsexp))) - (when (eq t (car state)) (goto-char (cadr state)))) - (cons 'column (smie-indent-virtual))))) - (`(:after . ,(or "=" "iuwu-mod")) 2) - (`(:after . " @ ") (smie-rule-parent)) - (`(:before . "do") (smie-rule-parent)) - (`(,(or :before :after) . ".") - (unless (smie-rule-parent-p ".") - (smie-rule-parent ruby-indent-level))) - (`(:before . ,(or `"else" `"then" `"elsif" `"rescue" `"ensure")) 0) - (`(:before . ,(or `"when")) - (if (not (smie-rule-sibling-p)) 0)) ;; ruby-indent-level - (`(:after . "+") ;FIXME: Probably applicable to most infix operators. - (if (smie-rule-parent-p ";") ruby-indent-level)) + ;; when the opening token is hanging and the parent is not an + ;; open-paren. + (cond + ((eq (car (smie-indent--parent)) t) nil) + ;; When after `.', let's always de-indent, + ;; because when `.' is inside the line, the + ;; additional indentation from it looks out of place. + ((smie-rule-parent-p ".") + (let (smie--parent) + (save-excursion + ;; Traverse up the parents until the parent is "." at + ;; indentation, or any other token. + (while (and (let ((parent (smie-indent--parent))) + (goto-char (cadr parent)) + (save-excursion + (unless (integerp (car parent)) (forward-char -1)) + (not (ruby-smie--bosp)))) + (progn + (setq smie--parent nil) + (smie-rule-parent-p ".")))) + (smie-rule-parent)))) + (t (smie-rule-parent)))))) + (`(:after . ,(or `"(" "[" "{")) + ;; FIXME: Shouldn't this be the default behavior of + ;; `smie-indent-after-keyword'? + (save-excursion + (forward-char 1) + (skip-chars-forward " \t") + ;; `smie-rule-hanging-p' is not good enough here, + ;; because we want to reject hanging tokens at bol, too. + (unless (or (eolp) (forward-comment 1)) + (cons 'column (current-column))))) + (`(:before . " @ ") + (save-excursion + (skip-chars-forward " \t") + (cons 'column (current-column)))) + (`(:before . "do") (ruby-smie--indent-to-stmt)) + (`(:before . ".") + (if (smie-rule-sibling-p) + (and ruby-align-chained-calls 0) + ruby-indent-level)) + (`(:before . ,(or `"else" `"then" `"elsif" `"rescue" `"ensure")) + (smie-rule-parent)) + (`(:before . "when") + ;; Align to the previous `when', but look up the virtual + ;; indentation of `case'. + (if (smie-rule-sibling-p) 0 (smie-rule-parent))) + (`(:after . ,(or "=" "iuwu-mod" "+" "-" "*" "/" "&&" "||" "%" "**" "^" "&" + "<=>" ">" "<" ">=" "<=" "==" "===" "!=" "<<" ">>" + "+=" "-=" "*=" "/=" "%=" "**=" "&=" "|=" "^=" "|" + "<<=" ">>=" "&&=" "||=" "and" "or")) + (and (smie-rule-parent-p ";" nil) + (smie-indent--hanging-p) + ruby-indent-level)) + (`(:after . ,(or "?" ":")) ruby-indent-level) + (`(:before . ,(guard (memq (intern-soft token) ruby-alignable-keywords))) + (when (not (ruby--at-indentation-p)) + (if (ruby-smie--indent-to-stmt-p token) + (ruby-smie--indent-to-stmt) + (cons 'column (current-column))))) )) +(defun ruby--at-indentation-p (&optional point) + (save-excursion + (unless point (setq point (point))) + (forward-line 0) + (skip-chars-forward " \t") + (eq (point) point))) + (defun ruby-imenu-create-index-in-block (prefix beg end) "Create an imenu index of methods inside a block." (let ((index-alist '()) (case-fold-search nil) @@ -560,11 +733,16 @@ explicitly declared in magic comment." (nreverse (ruby-imenu-create-index-in-block nil (point-min) nil))) (defun ruby-accurate-end-of-block (&optional end) - "TODO: document." + "Jump to the end of the current block or END, whichever is closer." (let (state (end (or end (point-max)))) - (while (and (setq state (apply 'ruby-parse-partial end state)) - (>= (nth 2 state) 0) (< (point) end))))) + (if ruby-use-smie + (save-restriction + (back-to-indentation) + (narrow-to-region (point) end) + (smie-forward-sexp)) + (while (and (setq state (apply 'ruby-parse-partial end state)) + (>= (nth 2 state) 0) (< (point) end)))))) (defun ruby-mode-variables () "Set up initial buffer-local variables for Ruby mode." @@ -574,7 +752,6 @@ explicitly declared in magic comment." :forward-token #'ruby-smie--forward-token :backward-token #'ruby-smie--backward-token) (setq-local indent-line-function 'ruby-indent-line)) - (setq-local require-final-newline t) (setq-local comment-start "# ") (setq-local comment-end "") (setq-local comment-column ruby-comment-column) @@ -585,46 +762,86 @@ explicitly declared in magic comment." (setq-local paragraph-separate paragraph-start) (setq-local paragraph-ignore-fill-prefix t)) +(defun ruby--insert-coding-comment (encoding) + "Insert a magic coding comment for ENCODING. +The style of the comment is controlled by `ruby-encoding-magic-comment-style'." + (let ((encoding-magic-comment-template + (pcase ruby-encoding-magic-comment-style + (`ruby "# coding: %s") + (`emacs "# -*- coding: %s -*-") + (`custom + ruby-custom-encoding-magic-comment-template)))) + (insert + (format encoding-magic-comment-template encoding) + "\n"))) + +(defun ruby--detect-encoding () + (if (eq ruby-insert-encoding-magic-comment 'always-utf8) + "utf-8" + (let ((coding-system + (or save-buffer-coding-system + buffer-file-coding-system))) + (if coding-system + (setq coding-system + (or (coding-system-get coding-system 'mime-charset) + (coding-system-change-eol-conversion coding-system nil)))) + (if coding-system + (symbol-name + (if ruby-use-encoding-map + (let ((elt (assq coding-system ruby-encoding-map))) + (if elt (cdr elt) coding-system)) + coding-system)) + "ascii-8bit")))) + +(defun ruby--encoding-comment-required-p () + (or (eq ruby-insert-encoding-magic-comment 'always-utf8) + (re-search-forward "[^\0-\177]" nil t))) + (defun ruby-mode-set-encoding () "Insert a magic comment header with the proper encoding if necessary." (save-excursion (widen) (goto-char (point-min)) - (when (re-search-forward "[^\0-\177]" nil t) + (when (ruby--encoding-comment-required-p) (goto-char (point-min)) - (let ((coding-system - (or save-buffer-coding-system - buffer-file-coding-system))) - (if coding-system - (setq coding-system - (or (coding-system-get coding-system 'mime-charset) - (coding-system-change-eol-conversion coding-system nil)))) - (setq coding-system - (if coding-system - (symbol-name - (if ruby-use-encoding-map - (let ((elt (assq coding-system ruby-encoding-map))) - (if elt (cdr elt) coding-system)) - coding-system)) - "ascii-8bit")) + (let ((coding-system (ruby--detect-encoding))) (when coding-system (if (looking-at "^#!") (beginning-of-line 2)) - (cond ((looking-at "\\s *#.*-\*-\\s *\\(en\\)?coding\\s *:\\s *\\([-a-z0-9_]*\\)\\s *\\(;\\|-\*-\\)") + (cond ((looking-at "\\s *#\\s *.*\\(en\\)?coding\\s *:\\s *\\([-a-z0-9_]*\\)") + ;; update existing encoding comment if necessary (unless (string= (match-string 2) coding-system) (goto-char (match-beginning 2)) (delete-region (point) (match-end 2)) - (and (looking-at "-\*-") - (let ((n (skip-chars-backward " "))) - (cond ((= n 0) (insert " ") (backward-char)) - ((= n -1) (insert " ")) - ((forward-char))))) (insert coding-system))) ((looking-at "\\s *#.*coding\\s *[:=]")) (t (when ruby-insert-encoding-magic-comment - (insert "# -*- coding: " coding-system " -*-\n")))) + (ruby--insert-coding-comment coding-system)))) (when (buffer-modified-p) (basic-save-buffer-1))))))) +(defvar ruby--electric-indent-chars '(?. ?\) ?} ?\])) + +(defun ruby--electric-indent-p (char) + (cond + ((memq char ruby--electric-indent-chars) + ;; Reindent after typing a char affecting indentation. + (ruby--at-indentation-p (1- (point)))) + ((memq (char-after) ruby--electric-indent-chars) + ;; Reindent after inserting something in front of the above. + (ruby--at-indentation-p (1- (point)))) + ((or (and (>= char ?a) (<= char ?z)) (memq char '(?_ ?? ?! ?:))) + (let ((pt (point))) + (save-excursion + (skip-chars-backward "[:alpha:]:_?!") + (and (ruby--at-indentation-p) + (looking-at (regexp-opt (cons "end" ruby-block-mid-keywords))) + ;; Outdent after typing a keyword. + (or (eq (match-end 0) pt) + ;; Reindent if it wasn't a keyword after all. + (eq (match-end 0) (1- pt))))))))) + +;; FIXME: Remove this? It's unused here, but some redefinitions of +;; `ruby-calculate-indent' in user init files still call it. (defun ruby-current-indentation () "Return the indentation level of current line." (save-excursion @@ -641,7 +858,7 @@ explicitly declared in magic comment." "Indent the current line to COLUMN." (when column (let (shift top beg) - (and (< column 0) (error "invalid nest")) + (and (< column 0) (error "Invalid nesting")) (setq shift (current-column)) (beginning-of-line) (setq beg (point)) @@ -711,7 +928,7 @@ Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'." ruby-block-mid-keywords) 'words)) (goto-char (match-end 0)) - (not (looking-at "\\s_\\|!"))) + (not (looking-at "\\s_"))) ((eq option 'expr-qstr) (looking-at "[a-zA-Z][a-zA-z0-9_]* +%[^ \t]")) ((eq option 'expr-re) @@ -719,11 +936,28 @@ Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'." (t nil))))))))) (defun ruby-forward-string (term &optional end no-error expand) - "TODO: document." + "Move forward across one balanced pair of string delimiters. +Skips escaped delimiters. If EXPAND is non-nil, also ignores +delimiters in interpolated strings. + +TERM should be a string containing either a single, self-matching +delimiter (e.g. \"/\"), or a pair of matching delimiters with the +close delimiter first (e.g. \"][\"). + +When non-nil, search is bounded by position END. + +Throws an error if a balanced match is not found, unless NO-ERROR +is non-nil, in which case nil will be returned. + +This command assumes the character after point is an opening +delimiter." (let ((n 1) (c (string-to-char term)) - (re (if expand - (concat "[^\\]\\(\\\\\\\\\\)*\\([" term "]\\|\\(#{\\)\\)") - (concat "[^\\]\\(\\\\\\\\\\)*[" term "]")))) + (re (concat "[^\\]\\(\\\\\\\\\\)*\\(" + (if (string= term "^") ;[^] is not a valid regexp + "\\^" + (concat "[" term "]")) + (when expand "\\|\\(#{\\)") + "\\)"))) (while (and (re-search-forward re end no-error) (if (match-beginning 3) (ruby-forward-string "}{" end no-error nil) @@ -732,7 +966,7 @@ Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'." (forward-char -1)) (cond ((zerop n)) (no-error nil) - ((error "unterminated string"))))) + ((error "Unterminated string"))))) (defun ruby-deep-indent-paren-p (c) "TODO: document." @@ -758,7 +992,8 @@ Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'." ((looking-at "[\"`]") ;skip string (cond ((and (not (eobp)) - (ruby-forward-string (buffer-substring (point) (1+ (point))) end t t)) + (ruby-forward-string (buffer-substring (point) (1+ (point))) + end t t)) nil) (t (setq in-string (point)) @@ -943,7 +1178,7 @@ Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'." (setq in-string (match-end 0)) (goto-char ruby-indent-point))) (t - (error (format "bad string %s" + (error (format "Bad string %s" (buffer-substring (point) pnt) )))))) (list in-string nest depth pcol)) @@ -1018,7 +1253,7 @@ Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'." (setq indent (current-column))))) ((and (nth 2 state) (> (nth 2 state) 0)) ; in nest (if (null (cdr (nth 1 state))) - (error "invalid nest")) + (error "Invalid nesting")) (goto-char (cdr (nth 1 state))) (forward-word -1) ; skip back a keyword (setq begin (point)) @@ -1065,7 +1300,8 @@ Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'." (while (and (re-search-forward "#" pos t) (setq end (1- (point))) (or (ruby-special-char-p end) - (and (setq state (ruby-parse-region parse-start end)) + (and (setq state (ruby-parse-region + parse-start end)) (nth 0 state)))) (setq end nil)) (goto-char (or end pos)) @@ -1076,7 +1312,8 @@ Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'." (and (or (and (looking-at ruby-symbol-re) (skip-chars-backward ruby-symbol-chars) - (looking-at (concat "\\<\\(" ruby-block-hanging-re "\\)\\>")) + (looking-at (concat "\\<\\(" ruby-block-hanging-re + "\\)\\>")) (not (eq (point) (nth 3 state))) (save-excursion (goto-char (match-end 0)) @@ -1120,7 +1357,8 @@ Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'." (cond ((and (null op-end) - (not (looking-at (concat "\\<\\(" ruby-block-hanging-re "\\)\\>"))) + (not (looking-at (concat "\\<\\(" ruby-block-hanging-re + "\\)\\>"))) (eq (ruby-deep-indent-paren-p t) 'space) (not (bobp))) (widen) @@ -1257,7 +1495,8 @@ With ARG, do it many times. Negative ARG means move backward." (skip-chars-forward ",.:;|&^~=!?\\+\\-\\*") (looking-at "\\s(")) (goto-char (scan-sexps (point) 1))) - ((and (looking-at (concat "\\<\\(" ruby-block-beg-re "\\)\\>")) + ((and (looking-at (concat "\\<\\(" ruby-block-beg-re + "\\)\\>")) (not (eq (char-before (point)) ?.)) (not (eq (char-before (point)) ?:))) (ruby-end-of-block) @@ -1274,7 +1513,8 @@ With ARG, do it many times. Negative ARG means move backward." (progn (setq expr (or expr (ruby-expr-beg) (looking-at "%\\sw?\\Sw\\|[\"'`/]"))) - (nth 1 (setq state (apply 'ruby-parse-partial nil state)))) + (nth 1 (setq state (apply #'ruby-parse-partial + nil state)))) (setq expr t) (skip-chars-forward "<")) (not expr)))) @@ -1298,10 +1538,11 @@ With ARG, do it many times. Negative ARG means move forward." (forward-char -1) (cond ((looking-at "\\s)") (goto-char (scan-sexps (1+ (point)) -1)) - (case (char-before) - (?% (forward-char -1)) - ((?q ?Q ?w ?W ?r ?x) - (if (eq (char-before (1- (point))) ?%) (forward-char -2)))) + (pcase (char-before) + (`?% (forward-char -1)) + ((or `?q `?Q `?w `?W `?r `?x) + (if (eq (char-before (1- (point))) ?%) + (forward-char -2)))) nil) ((looking-at "\\s\"\\|\\\\\\S_") (let ((c (char-to-string (char-before (match-end 0))))) @@ -1315,13 +1556,14 @@ With ARG, do it many times. Negative ARG means move forward." (t (forward-char 1) (while (progn (forward-word -1) - (case (char-before) - (?_ t) - (?. (forward-char -1) t) - ((?$ ?@) + (pcase (char-before) + (`?_ t) + (`?. (forward-char -1) t) + ((or `?$ `?@) (forward-char -1) - (and (eq (char-before) (char-after)) (forward-char -1))) - (?: + (and (eq (char-before) (char-after)) + (forward-char -1))) + (`?: (forward-char -1) (eq (char-before) :))))) (if (looking-at ruby-block-end-re) @@ -1512,8 +1754,9 @@ If the result is do-end block, it will always be multiline." (let ((start (point)) beg end) (end-of-line) (unless - (if (and (re-search-backward "\\({\\)\\|\\_\\)") (progn + (goto-char (or (match-beginning 1) (match-beginning 2))) (setq beg (point)) (save-match-data (ruby-forward-sexp)) (setq end (point)) @@ -1523,6 +1766,43 @@ If the result is do-end block, it will always be multiline." (ruby-do-end-to-brace beg end))) (goto-char start)))) +(defun ruby--string-region () + "Return region for string at point." + (let ((state (syntax-ppss))) + (when (memq (nth 3 state) '(?' ?\")) + (save-excursion + (goto-char (nth 8 state)) + (forward-sexp) + (list (nth 8 state) (point)))))) + +(defun ruby-string-at-point-p () + "Check if cursor is at a string or not." + (ruby--string-region)) + +(defun ruby--inverse-string-quote (string-quote) + "Get the inverse string quoting for STRING-QUOTE." + (if (equal string-quote "\"") "'" "\"")) + +(defun ruby-toggle-string-quotes () + "Toggle string literal quoting between single and double." + (interactive) + (when (ruby-string-at-point-p) + (let* ((region (ruby--string-region)) + (min (nth 0 region)) + (max (nth 1 region)) + (string-quote (ruby--inverse-string-quote (buffer-substring-no-properties min (1+ min)))) + (content + (buffer-substring-no-properties (1+ min) (1- max)))) + (setq content + (if (equal string-quote "\"") + (replace-regexp-in-string "\\\\\"" "\"" (replace-regexp-in-string "\\([^\\\\]\\)'" "\\1\\\\'" content)) + (replace-regexp-in-string "\\\\\'" "'" (replace-regexp-in-string "\\([^\\\\]\\)\"" "\\1\\\\\"" content)))) + (let ((orig-point (point))) + (delete-region min max) + (insert + (format "%s%s%s" string-quote content string-quote)) + (goto-char orig-point))))) + (eval-and-compile (defconst ruby-percent-literal-beg-re "\\(%\\)[qQrswWxIi]?\\([[:punct:]]\\)" @@ -1563,10 +1843,20 @@ It will be properly highlighted even when the call omits parens.") ;; $' $" $` .... are variables. ;; ?' ?" ?` are character literals (one-char strings in 1.9+). ("\\([?$]\\)[#\"'`]" - (1 (unless (save-excursion - ;; Not within a string. - (nth 3 (syntax-ppss (match-beginning 0)))) + (1 (if (save-excursion + (nth 3 (syntax-ppss (match-beginning 0)))) + ;; Within a string, skip. + (goto-char (match-end 1)) (string-to-syntax "\\")))) + ;; Part of symbol when at the end of a method name. + ("[!?]" + (0 (unless (save-excursion + (or (nth 8 (syntax-ppss (match-beginning 0))) + (eq (char-before) ?:) + (let (parse-sexp-lookup-properties) + (zerop (skip-syntax-backward "w_"))) + (memq (preceding-char) '(?@ ?$)))) + (string-to-syntax "_")))) ;; Regular expressions. Start with matching unescaped slash. ("\\(?:\\=\\|[^\\]\\)\\(?:\\\\\\\\\\)*\\(/\\)" (1 (let ((state (save-excursion (syntax-ppss (match-beginning 1))))) @@ -1731,153 +2021,192 @@ See `font-lock-syntax-table'.") (defconst ruby-font-lock-keyword-beg-re "\\(?:^\\|[^.@$]\\|\\.\\.\\)") (defconst ruby-font-lock-keywords - (list - ;; functions - '("^\\s *def\\s +\\(?:[^( \t\n.]*\\.\\)?\\([^( \t\n]+\\)" + `(;; Functions. + ("^\\s *def\\s +\\(?:[^( \t\n.]*\\.\\)?\\([^( \t\n]+\\)" 1 font-lock-function-name-face) - ;; keywords - (list (concat - ruby-font-lock-keyword-beg-re - (regexp-opt - '("alias" - "and" - "begin" - "break" - "case" - "class" - "def" - "defined?" - "do" - "elsif" - "else" - "fail" - "ensure" - "for" - "end" - "if" - "in" - "module" - "next" - "not" - "or" - "redo" - "rescue" - "retry" - "return" - "then" - "super" - "unless" - "undef" - "until" - "when" - "while" - "yield") - 'symbols)) - 1 'font-lock-keyword-face) - ;; some core methods - (list (concat - ruby-font-lock-keyword-beg-re - (regexp-opt - '(;; built-in methods on Kernel - "__callee__" - "__dir__" - "__method__" - "abort" - "at_exit" - "autoload" - "autoload?" - "binding" - "block_given?" - "caller" - "catch" - "eval" - "exec" - "exit" - "exit!" - "fail" - "fork" - "format" - "lambda" - "load" - "loop" - "open" - "p" - "print" - "printf" - "proc" - "putc" - "puts" - "raise" - "rand" - "readline" - "readlines" - "require" - "require_relative" - "sleep" - "spawn" - "sprintf" - "srand" - "syscall" - "system" - "throw" - "trap" - "warn" - ;; keyword-like private methods on Module - "alias_method" - "attr" - "attr_accessor" - "attr_reader" - "attr_writer" - "define_method" - "extend" - "include" - "module_function" - "prepend" - "private" - "protected" - "public" - "refine" - "using") - 'symbols)) - 1 'font-lock-builtin-face) - ;; here-doc beginnings - `(,ruby-here-doc-beg-re 0 (unless (ruby-singleton-class-p (match-beginning 0)) - 'font-lock-string-face)) - ;; Perl-ish keywords - "\\_<\\(?:BEGIN\\|END\\)\\_>\\|^__END__$" - ;; variables - `(,(concat ruby-font-lock-keyword-beg-re - "\\_<\\(nil\\|self\\|true\\|false\\)\\>") + ;; Keywords. + (,(concat + ruby-font-lock-keyword-beg-re + (regexp-opt + '("alias" + "and" + "begin" + "break" + "case" + "class" + "def" + "defined?" + "do" + "elsif" + "else" + "fail" + "ensure" + "for" + "end" + "if" + "in" + "module" + "next" + "not" + "or" + "redo" + "rescue" + "retry" + "return" + "then" + "super" + "unless" + "undef" + "until" + "when" + "while" + "yield") + 'symbols)) + (1 font-lock-keyword-face)) + ;; Core methods that have required arguments. + (,(concat + ruby-font-lock-keyword-beg-re + (regexp-opt + '( ;; built-in methods on Kernel + "at_exit" + "autoload" + "autoload?" + "catch" + "eval" + "exec" + "fork" + "format" + "lambda" + "load" + "loop" + "open" + "p" + "print" + "printf" + "proc" + "putc" + "puts" + "require" + "require_relative" + "spawn" + "sprintf" + "syscall" + "system" + "trap" + "warn" + ;; keyword-like private methods on Module + "alias_method" + "attr" + "attr_accessor" + "attr_reader" + "attr_writer" + "define_method" + "extend" + "include" + "module_function" + "prepend" + "private_class_method" + "private_constant" + "public_class_method" + "public_constant" + "refine" + "using") + 'symbols)) + (1 (unless (looking-at " *\\(?:[]|,.)}=]\\|$\\)") + font-lock-builtin-face))) + ;; Kernel methods that have no required arguments. + (,(concat + ruby-font-lock-keyword-beg-re + (regexp-opt + '("__callee__" + "__dir__" + "__method__" + "abort" + "at_exit" + "binding" + "block_given?" + "caller" + "exit" + "exit!" + "fail" + "private" + "protected" + "public" + "raise" + "rand" + "readline" + "readlines" + "sleep" + "srand" + "throw") + 'symbols)) + (1 font-lock-builtin-face)) + ;; Here-doc beginnings. + (,ruby-here-doc-beg-re + (0 (unless (ruby-singleton-class-p (match-beginning 0)) + 'font-lock-string-face))) + ;; Perl-ish keywords. + "\\_<\\(?:BEGIN\\|END\\)\\_>\\|^__END__$" + ;; Variables. + (,(concat ruby-font-lock-keyword-beg-re + "\\_<\\(nil\\|self\\|true\\|false\\)\\_>") 1 font-lock-variable-name-face) - ;; keywords that evaluate to certain values - '("\\_<__\\(?:LINE\\|ENCODING\\|FILE\\)__\\_>" 0 font-lock-variable-name-face) - ;; symbols - '("\\(^\\|[^:]\\)\\(:\\([-+~]@?\\|[/%&|^`]\\|\\*\\*?\\|<\\(<\\|=>?\\)?\\|>[>=]?\\|===?\\|=~\\|![~=]?\\|\\[\\]=?\\|@?\\(\\w\\|_\\)+\\([!?=]\\|\\b_*\\)\\|#{[^}\n\\\\]*\\(\\\\.[^}\n\\\\]*\\)*}\\)\\)" + ;; Keywords that evaluate to certain values. + ("\\_<__\\(?:LINE\\|ENCODING\\|FILE\\)__\\_>" + (0 font-lock-builtin-face)) + ;; Symbols. + ("\\(^\\|[^:]\\)\\(:\\([-+~]@?\\|[/%&|^`]\\|\\*\\*?\\|<\\(<\\|=>?\\)?\\|>[>=]?\\|===?\\|=~\\|![~=]?\\|\\[\\]=?\\|@?\\(\\w\\|_\\)+\\([!?=]\\|\\b_*\\)\\|#{[^}\n\\\\]*\\(\\\\.[^}\n\\\\]*\\)*}\\)\\)" 2 font-lock-constant-face) - ;; variables - '("\\(\\$\\([^a-zA-Z0-9 \n]\\|[0-9]\\)\\)\\W" - 1 font-lock-variable-name-face) - '("\\(\\$\\|@\\|@@\\)\\(\\w\\|_\\)+" + ;; Special globals. + (,(concat "\\$\\(?:[:\"!@;,/\\._><\\$?~=*&`'+0-9]\\|-[0adFiIlpvw]\\|" + (regexp-opt '("LOAD_PATH" "LOADED_FEATURES" "PROGRAM_NAME" + "ERROR_INFO" "ERROR_POSITION" + "FS" "FIELD_SEPARATOR" + "OFS" "OUTPUT_FIELD_SEPARATOR" + "RS" "INPUT_RECORD_SEPARATOR" + "ORS" "OUTPUT_RECORD_SEPARATOR" + "NR" "INPUT_LINE_NUMBER" + "LAST_READ_LINE" "DEFAULT_OUTPUT" "DEFAULT_INPUT" + "PID" "PROCESS_ID" "CHILD_STATUS" + "LAST_MATCH_INFO" "IGNORECASE" + "ARGV" "MATCH" "PREMATCH" "POSTMATCH" + "LAST_PAREN_MATCH" "stdin" "stdout" "stderr" + "DEBUG" "FILENAME" "VERBOSE" "SAFE" "CLASSPATH" + "JRUBY_VERSION" "JRUBY_REVISION" "ENV_JAVA")) + "\\_>\\)") + 0 font-lock-builtin-face) + ("\\(\\$\\|@\\|@@\\)\\(\\w\\|_\\)+" 0 font-lock-variable-name-face) - ;; constants - '("\\(?:\\_<\\|::\\)\\([A-Z]+\\(\\w\\|_\\)*\\)" + ;; Constants. + ("\\(?:\\_<\\|::\\)\\([A-Z]+\\(\\w\\|_\\)*\\)" 1 (unless (eq ?\( (char-after)) font-lock-type-face)) - '("\\(^\\s *\\|[\[\{\(,]\\s *\\|\\sw\\s +\\)\\(\\(\\sw\\|_\\)+\\):[^:]" 2 font-lock-constant-face) - ;; conversion methods on Kernel - (list (concat ruby-font-lock-keyword-beg-re - (regexp-opt '("Array" "Complex" "Float" "Hash" - "Integer" "Rational" "String") 'symbols)) - 1 font-lock-builtin-face) - ;; expression expansion - '(ruby-match-expression-expansion + ("\\(^\\s *\\|[\[\{\(,]\\s *\\|\\sw\\s +\\)\\(\\(\\sw\\|_\\)+\\):[^:]" + (2 font-lock-constant-face)) + ;; Conversion methods on Kernel. + (,(concat ruby-font-lock-keyword-beg-re + (regexp-opt '("Array" "Complex" "Float" "Hash" + "Integer" "Rational" "String") 'symbols)) + (1 font-lock-builtin-face)) + ;; Expression expansion. + (ruby-match-expression-expansion 2 font-lock-variable-name-face t) - ;; negation char - '("[^[:alnum:]_]\\(!\\)[^=]" + ;; Negation char. + ("\\(?:^\\|[^[:alnum:]_]\\)\\(!+\\)[^=~]" 1 font-lock-negation-char-face) - ;; character literals - ;; FIXME: Support longer escape sequences. - '("\\_<\\?\\\\?\\S " 0 font-lock-string-face) - ) + ;; Character literals. + ;; FIXME: Support longer escape sequences. + ("\\_<\\?\\\\?\\S " 0 font-lock-string-face) + ;; Regexp options. + ("\\(?:\\s|\\|/\\)\\([imxo]+\\)" + 1 (when (save-excursion + (let ((state (syntax-ppss (match-beginning 0)))) + (and (nth 3 state) + (or (eq (char-after) ?/) + (progn + (goto-char (nth 8 state)) + (looking-at "%r")))))) + font-lock-preprocessor-face)) + ) "Additional expressions to highlight in Ruby mode.") (defun ruby-match-expression-expansion (limit) @@ -1894,10 +2223,7 @@ See `font-lock-syntax-table'.") (define-derived-mode ruby-mode prog-mode "Ruby" "Major mode for editing Ruby code. -\\{ruby-mode-map} - -Entry to this mode calls the value of `ruby-mode-hook' -if that value is non-nil." +\\{ruby-mode-map}" (ruby-mode-variables) (setq-local imenu-create-index-function 'ruby-imenu-create-index) @@ -1906,8 +2232,7 @@ if that value is non-nil." (setq-local end-of-defun-function 'ruby-end-of-defun) (add-hook 'after-save-hook 'ruby-mode-set-encoding nil 'local) - - (setq-local electric-indent-chars (append '(?\{ ?\}) electric-indent-chars)) + (add-hook 'electric-indent-functions 'ruby--electric-indent-p nil 'local) (setq-local font-lock-defaults '((ruby-font-lock-keywords) nil nil)) (setq-local font-lock-keywords ruby-font-lock-keywords) @@ -1921,10 +2246,11 @@ if that value is non-nil." (add-to-list 'auto-mode-alist (cons (purecopy (concat "\\(?:\\." "rb\\|ru\\|rake\\|thor" - "\\|jbuilder\\|gemspec" + "\\|jbuilder\\|rabl\\|gemspec\\|podspec" "\\|/" "\\(?:Gem\\|Rake\\|Cap\\|Thor" - "Vagrant\\|Guard\\)file" + "\\|Puppet\\|Berks" + "\\|Vagrant\\|Guard\\|Pod\\)file" "\\)\\'")) 'ruby-mode)) ;;;###autoload