;;; 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
;;; Code:
-(eval-when-compile (require 'cl))
-
(defgroup ruby nil
"Major mode for editing Ruby code."
:prefix "ruby-"
"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.
(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.")
["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
(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)
: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
: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
(const :tag "Emacs Style" emacs)
(const :tag "Ruby Style" ruby)
(const :tag "Custom Style" custom))
- :group 'ruby)
+ :group 'ruby
+ :version "24.4")
-(defcustom ruby-custom-encoding-magic-comment-template "# coding: %s"
- "The encoding comment template to be used when
-`ruby-encoding-magic-comment-style' is set to `custom'."
+(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)
+ :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."
;; but avoids lots of conflicts:
(exp "and" exp) (exp "or" exp))
(exp (exp1) (exp "," exp) (exp "=" exp)
- (id " @ " exp)
- (exp "." id))
+ (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")
("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))
(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"))
(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")
(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)
(member (save-excursion (ruby-smie--backward-token))
'("iuwu-mod" "and" "or")))
(or (eq ?\{ (char-before))
(looking-back "\\_<do" (- (point) 2)))))
+(defun ruby-smie--closing-pipe-p ()
+ (save-excursion
+ (if (eq ?| (char-before)) (forward-char -1))
+ (and (re-search-backward "|" (line-beginning-position) t)
+ (ruby-smie--opening-pipe-p))))
+
(defun ruby-smie--args-separator-p (pos)
(and
(< pos (line-end-position))
(save-excursion
(goto-char pos)
(or (and (eq (char-syntax (char-after)) ?w)
- (not (looking-at (regexp-opt '("unless" "if" "while" "until"
- "else" "elsif" "do" "end" "and" "or")
+ (not (looking-at (regexp-opt '("unless" "if" "while" "until" "or"
+ "else" "elsif" "do" "end" "and")
'symbols))))
- (memq (syntax-after pos) '(7 15))
- (looking-at "[([]\\|[-+!~:]\\sw")))))
+ (memq (car (syntax-after pos)) '(7 15))
+ (looking-at "[([]\\|[-+!~]\\sw\\|:\\(?:\\sw\\|\\s.\\)")))))
(defun ruby-smie--at-dot-call ()
(and (eq ?w (char-syntax (following-char)))
(let ((pos (point)))
(skip-chars-forward " \t")
(cond
- ((looking-at "\\s\"") ;A heredoc or a string.
- (if (not (looking-at "\n"))
- ""
- ;; Tokenize the whole heredoc as semicolon.
- (goto-char (scan-sexps (point) 1))
- ";"))
+ ((and (looking-at "\n") (looking-at "\\s\"")) ;A heredoc.
+ ;; Tokenize the whole heredoc as semicolon.
+ (goto-char (scan-sexps (point) 1))
+ ";")
((and (looking-at "[\n#]")
(ruby-smie--implicit-semi-p)) ;Only add implicit ; when needed.
(if (eolp) (forward-char 1) (forward-comment 1))
(t
(forward-comment (point-max))
(cond
- ((looking-at ":\\s.+")
- (goto-char (match-end 0)) (match-string 0)) ;; bug#15208.
((and (< pos (point))
(save-excursion
(ruby-smie--args-separator-p (prog1 (point) (goto-char pos)))))
" @ ")
+ ((looking-at ":\\s.+")
+ (goto-char (match-end 0)) (match-string 0)) ;bug#15208.
+ ((looking-at "\\s\"") "") ;A string.
(t
(let ((dot (ruby-smie--at-dot-call))
(tok (smie-default-forward-token)))
((string-match-p "\\`|[*&]?\\'" tok)
(forward-char (- 1 (length tok)))
(setq tok "|")
- (if (ruby-smie--opening-pipe-p) "opening-|" tok))
+ (cond
+ ((ruby-smie--opening-pipe-p) "opening-|")
+ ((ruby-smie--closing-pipe-p) "closing-|")
+ (t tok)))
((and (equal tok "") (looking-at "\\\\\n"))
(goto-char (match-end 0)) (ruby-smie--forward-token))
((equal tok "do")
(if (ruby-smie--bosp)
tok "iuwu-mod"))
((equal tok "|")
- (if (ruby-smie--opening-pipe-p) "opening-|" tok))
+ (cond
+ ((ruby-smie--opening-pipe-p) "opening-|")
+ ((ruby-smie--closing-pipe-p) "closing-|")
+ (t tok)))
((string-match-p "\\`|[*&]\\'" tok)
(forward-char 1)
(substring tok 1))
(smie-backward-sexp ";")
(cons 'column (smie-indent-virtual))))
+(defun ruby-smie--indent-to-stmt-p (keyword)
+ (or (eq t ruby-align-to-stmt-keywords)
+ (memq (intern keyword) ruby-align-to-stmt-keywords)))
+
(defun ruby-smie-rules (kind token)
(pcase (cons kind token)
(`(:elem . basic) ruby-indent-level)
(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 . " @ ") (smie-rule-parent))
+ ;; 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))
- (`(,(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
+ (`(: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"))
- (if (smie-rule-parent-p ";" nil) ruby-indent-level))
+ (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)
(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."
: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)
(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
- (let ((encoding-magic-comment-template
- (case ruby-encoding-magic-comment-style
- ('ruby "# coding: %s")
- ('emacs "# -*- coding: %s -*-")
- ('custom ruby-custom-encoding-magic-comment-template))))
- (insert
- (format encoding-magic-comment-template 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
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)
(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)
((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))
(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))
(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))
(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)
(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)
(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))))
(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)))))
(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)
(let ((start (point)) beg end)
(end-of-line)
(unless
- (if (and (re-search-backward "\\({\\)\\|\\_<do\\(\\s \\|$\\||\\)")
+ (if (and (re-search-backward "\\(?:[^#]\\)\\({\\)\\|\\(\\_<do\\_>\\)")
(progn
+ (goto-char (or (match-beginning 1) (match-beginning 2)))
(setq beg (point))
(save-match-data (ruby-forward-sexp))
(setq end (point))
(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:]]\\)"
;; $' $" $` .... 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)))))
(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)
(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)
(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