* lisp/progmodes/ruby-mode.el (ruby-font-lock-keywords): Move `catch',
[bpt/emacs.git] / lisp / progmodes / ruby-mode.el
index 4d50eec..ab5634e 100644 (file)
@@ -1,6 +1,6 @@
 ;;; ruby-mode.el --- Major mode for editing Ruby files
 
-;; Copyright (C) 1994-201 Free Software Foundation, Inc.
+;; Copyright (C) 1994-2013 Free Software Foundation, Inc.
 
 ;; Authors: Yukihiro Matsumoto
 ;;     Nobuyoshi Nakada
   '"\\(def\\|class\\|module\\)"
   "Regexp to match the beginning of a defun, in the general sense.")
 
+(defconst ruby-singleton-class-re
+  "class\\s *<<"
+  "Regexp to match the beginning of a singleton class context.")
+
 (eval-and-compile
   (defconst ruby-here-doc-beg-re
   "\\(<\\)<\\(-\\)?\\(\\([a-zA-Z0-9_]+\\)\\|[\"]\\([^\"]+\\)[\"]\\|[']\\([^']+\\)[']\\)"
-    "Regexp to match the beginning of a heredoc."))
+  "Regexp to match the beginning of a heredoc.")
+
+  (defconst ruby-expression-expansion-re
+    "\\(?:[^\\]\\|\\=\\)\\(\\\\\\\\\\)*\\(#\\({[^}\n\\\\]*\\(\\\\.[^}\n\\\\]*\\)*}\\|\\(\\$\\|@\\|@@\\)\\(\\w\\|_\\)+\\)\\)"))
 
 (defun ruby-here-doc-end-match ()
   "Return a regexp to find the end of a heredoc.
@@ -141,13 +148,16 @@ This should only be called after matching against `ruby-here-doc-beg-re'."
 (define-abbrev-table 'ruby-mode-abbrev-table ()
   "Abbrev table in use in Ruby mode buffers.")
 
+(defvar ruby-use-smie nil)
+
 (defvar ruby-mode-map
   (let ((map (make-sparse-keymap)))
-    (define-key map (kbd "M-C-b") 'ruby-backward-sexp)
-    (define-key map (kbd "M-C-f") 'ruby-forward-sexp)
+    (unless ruby-use-smie
+      (define-key map (kbd "M-C-b") 'ruby-backward-sexp)
+      (define-key map (kbd "M-C-f") 'ruby-forward-sexp)
+      (define-key map (kbd "M-C-q") 'ruby-indent-exp))
     (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 "M-C-q") 'ruby-indent-exp)
     (define-key map (kbd "C-c {") 'ruby-toggle-block)
     map)
   "Keymap used in Ruby mode.")
@@ -229,6 +239,111 @@ Also ignores spaces after parenthesis when 'space."
 (put 'ruby-comment-column 'safe-local-variable 'integerp)
 (put 'ruby-deep-arglist 'safe-local-variable 'booleanp)
 
+;;; SMIE support
+
+(require 'smie)
+
+(defconst ruby-smie-grammar
+  ;; FIXME: Add support for Cucumber.
+  (smie-prec2->grammar
+   (smie-bnf->prec2
+    '((id)
+      (insts (inst) (insts ";" insts))
+      (inst (exp) (inst "iuwu-mod" exp))
+      (exp  (exp1) (exp "," exp))
+      (exp1 (exp2) (exp2 "?" exp1 ":" exp1))
+      (exp2 ("def" insts "end")
+            ("begin" insts-rescue-insts "end")
+            ("do" insts "end")
+            ("class" insts "end") ("module" insts "end")
+            ("for" for-body "end")
+            ("[" expseq "]")
+            ("{" hashvals "}")
+            ("while" insts "end")
+            ("until" insts "end")
+            ("unless" insts "end")
+            ("if" if-body "end")
+            ("case"  cases "end"))
+      (for-body (for-head ";" insts))
+      (for-head (id "in" exp))
+      (cases (exp "then" insts) ;; FIXME: Ruby also allows (exp ":" insts).
+             (cases "when" cases) (insts "else" insts))
+      (expseq (exp) );;(expseq "," expseq)
+      (hashvals (id "=>" exp1) (hashvals "," hashvals))
+      (insts-rescue-insts (insts)
+                          (insts-rescue-insts "rescue" insts-rescue-insts)
+                          (insts-rescue-insts "ensure" insts-rescue-insts))
+      (itheni (insts) (exp "then" insts))
+      (ielsei (itheni) (itheni "else" insts))
+      (if-body (ielsei) (if-body "elsif" if-body)))
+    '((nonassoc "in") (assoc ";") (assoc ","))
+    '((assoc "when"))
+    '((assoc "elsif"))
+    '((assoc "rescue" "ensure"))
+    '((assoc ",")))))
+
+(defun ruby-smie--bosp ()
+  (save-excursion (skip-chars-backward " \t")
+                  (or (bolp) (eq (char-before) ?\;))))
+
+(defun ruby-smie--implicit-semi-p ()
+  (save-excursion
+    (skip-chars-backward " \t")
+    (not (or (bolp)
+             (memq (char-before) '(?\; ?- ?+ ?* ?/ ?:))
+             (and (memq (char-before) '(?\? ?=))
+                  (not (memq (char-syntax (char-before (1- (point))))
+                             '(?w ?_))))))))
+
+(defun ruby-smie--forward-token ()
+  (skip-chars-forward " \t")
+  (if (and (looking-at "[\n#]")
+           ;; Only add implicit ; when needed.
+           (ruby-smie--implicit-semi-p))
+      (progn
+        (if (eolp) (forward-char 1) (forward-comment 1))
+        ";")
+    (forward-comment (point-max))
+    (let ((tok (smie-default-forward-token)))
+      (cond
+       ((member tok '("unless" "if" "while" "until"))
+        (if (save-excursion (forward-word -1) (ruby-smie--bosp))
+            tok "iuwu-mod"))
+       (t tok)))))
+
+(defun ruby-smie--backward-token ()
+  (let ((pos (point)))
+    (forward-comment (- (point)))
+    (if (and (> pos (line-end-position))
+             (ruby-smie--implicit-semi-p))
+        (progn (skip-chars-forward " \t")
+               ";")
+      (let ((tok (smie-default-backward-token)))
+        (cond
+         ((member tok '("unless" "if" "while" "until"))
+          (if (ruby-smie--bosp)
+              tok "iuwu-mod"))
+         (t tok))))))
+
+(defun ruby-smie-rules (kind token)
+  (pcase (cons kind token)
+    (`(:elem . basic) ruby-indent-level)
+    (`(:after . ";")
+     (if (smie-rule-parent-p "def" "begin" "do" "class" "module" "for"
+                             "[" "{" "while" "until" "unless"
+                             "if" "then" "elsif" "else" "when"
+                             "rescue" "ensure")
+         (smie-rule-parent ruby-indent-level)
+       ;; For (invalid) code between switch and case.
+       ;; (if (smie-parent-p "switch") 4)
+       0))
+    (`(:before . ,(or `"else" `"then" `"elsif")) 0)
+    (`(:before . ,(or `"when"))
+     (if (not (smie-rule-sibling-p)) 0)) ;; ruby-indent-level
+    ;; Hack attack: Since newlines are separators, don't try to align args that
+    ;; appear on a separate line.
+    (`(:list-intro . ";") t)))
+
 (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)
@@ -283,7 +398,11 @@ Also ignores spaces after parenthesis when 'space."
   (set-syntax-table ruby-mode-syntax-table)
   (setq local-abbrev-table ruby-mode-abbrev-table)
   (setq indent-tabs-mode ruby-indent-tabs-mode)
-  (set (make-local-variable 'indent-line-function) 'ruby-indent-line)
+  (if ruby-use-smie
+      (smie-setup ruby-smie-grammar #'ruby-smie-rules
+                  :forward-token  #'ruby-smie--forward-token
+                  :backward-token #'ruby-smie--backward-token)
+    (set (make-local-variable 'indent-line-function) 'ruby-indent-line))
   (set (make-local-variable 'require-final-newline) t)
   (set (make-local-variable 'comment-start) "# ")
   (set (make-local-variable 'comment-end) "")
@@ -382,10 +501,12 @@ and `\\' when preceded by `?'."
     (when pos (goto-char pos))
     (forward-word -1)
     (and (or (bolp) (not (eq (char-before (point)) ?_)))
-         (looking-at "class\\s *<<"))))
+         (looking-at ruby-singleton-class-re))))
 
 (defun ruby-expr-beg (&optional option)
-  "TODO: document."
+  "Check if point is possibly at the beginning of an expression.
+OPTION specifies the type of the expression.
+Can be one of `heredoc', `modifier', `expr-qstr', `expr-re'."
   (save-excursion
     (store-match-data nil)
     (let ((space (skip-chars-backward " \t"))
@@ -398,10 +519,10 @@ and `\\' when preceded by `?'."
                (or (eq (char-syntax (char-before (point))) ?w)
                    (ruby-special-char-p))))
         nil)
-       ((and (eq option 'heredoc) (< space 0))
-        (not (progn (goto-char start) (ruby-singleton-class-p))))
-       ((or (looking-at ruby-operator-re)
-            (looking-at "[\\[({,;]")
+       ((looking-at ruby-operator-re))
+       ((eq option 'heredoc)
+        (and (< space 0) (not (ruby-singleton-class-p start))))
+       ((or (looking-at "[\\[({,;]")
             (and (looking-at "[!?]")
                  (or (not (eq option 'modifier))
                      (bolp)
@@ -586,8 +707,7 @@ and `\\' when preceded by `?'."
                (not (or (eq ?_ w)
                         (eq ?. w)))))
          (goto-char pnt)
-         (setq w (char-after (point)))
-         (not (eq ?! w))
+         (not (eq ?! (char-after (point))))
          (skip-chars-forward " \t")
          (goto-char (match-beginning 0))
          (or (not (looking-at ruby-modifier-re))
@@ -839,22 +959,24 @@ and `\\' when preceded by `?'."
         indent))))
 
 (defun ruby-beginning-of-defun (&optional arg)
-  "Move backward to the beginning of the current top-level defun.
+  "Move backward to the beginning of the current defun.
 With ARG, move backward multiple defuns.  Negative ARG means
 move forward."
   (interactive "p")
-  (and (re-search-backward (concat "^\\s *" ruby-defun-beg-re "\\_>")
-                           nil t (or arg 1))
-       (beginning-of-line)))
-
-(defun ruby-end-of-defun (&optional arg)
-  "Move forward to the end of the current top-level defun.
-With ARG, move forward multiple defuns.  Negative ARG means
-move backward."
+  (let (case-fold-search)
+    (and (re-search-backward (concat "^\\s *" ruby-defun-beg-re "\\_>")
+                             nil t (or arg 1))
+         (beginning-of-line))))
+
+(defun ruby-end-of-defun ()
+  "Move point to the end of the current defun.
+The defun begins at or after the point.  This function is called
+by `end-of-defun'."
   (interactive "p")
   (ruby-forward-sexp)
-  (when (looking-back (concat "^\\s *" ruby-block-end-re))
-    (forward-line 1)))
+  (let (case-fold-search)
+    (when (looking-back (concat "^\\s *" ruby-block-end-re))
+      (forward-line 1))))
 
 (defun ruby-beginning-of-indent ()
   "Backtrack to a line which can be used as a reference for
@@ -866,39 +988,67 @@ calculating indentation on the lines after it."
                 (beginning-of-line)))))
 
 (defun ruby-move-to-block (n)
-  "Move to the beginning (N < 0) or the end (N > 0) of the current block
-or blocks containing the current block."
-  ;; TODO: Make this work for n > 1,
-  ;; make it not loop for n = 0,
-  ;; document body
-  (let ((orig (point))
-        (start (ruby-calculate-indent))
-        (down (looking-at (if (< n 0) ruby-block-end-re
-                            (concat "\\<\\(" ruby-block-beg-re "\\)\\>"))))
-        pos done)
-    (while (and (not done) (not (if (< n 0) (bobp) (eobp))))
-      (forward-line n)
-      (cond
-       ((looking-at "^\\s *$"))
-       ((looking-at "^\\s *#"))
-       ((and (> n 0) (looking-at "^=begin\\>"))
-        (re-search-forward "^=end\\>"))
-       ((and (< n 0) (looking-at "^=end\\>"))
-        (re-search-backward "^=begin\\>"))
-       (t
-        (setq pos (current-indentation))
+  "Move to the beginning (N < 0) or the end (N > 0) of the
+current block, a sibling block, or an outer block.  Do that (abs N) times."
+  (let ((signum (if (> n 0) 1 -1))
+        (backward (< n 0))
+        (depth (or (nth 2 (ruby-parse-region (line-beginning-position)
+                                             (line-end-position)))
+                   0))
+        case-fold-search
+        down done)
+    (when (< (* depth signum) 0)
+      ;; Moving end -> end or beginning -> beginning.
+      (setq depth 0))
+    (dotimes (_ (abs n))
+      (setq done nil)
+      (setq down (save-excursion
+                   (back-to-indentation)
+                   ;; There is a block start or block end keyword on this
+                   ;; line, don't need to look for another block.
+                   (and (re-search-forward
+                         (if backward ruby-block-end-re
+                           (concat "\\_<\\(" ruby-block-beg-re "\\)\\_>"))
+                         (line-end-position) t)
+                        (not (nth 8 (syntax-ppss))))))
+      (while (and (not done) (not (if backward (bobp) (eobp))))
+        (forward-line signum)
         (cond
-         ((< start pos)
-          (setq down t))
-         ((and down (= pos start))
-          (setq done t))
-         ((> start pos)
-          (setq done t)))))
-      (if done
-          (save-excursion
-            (back-to-indentation)
-            (if (looking-at (concat "\\<\\(" ruby-block-mid-re "\\)\\>"))
-                (setq done nil)))))
+         ;; Skip empty and commented out lines.
+         ((looking-at "^\\s *$"))
+         ((looking-at "^\\s *#"))
+         ;; Skip block comments;
+         ((and (not backward) (looking-at "^=begin\\>"))
+          (re-search-forward "^=end\\>"))
+         ((and backward (looking-at "^=end\\>"))
+          (re-search-backward "^=begin\\>"))
+         ;; Jump over a multiline literal.
+         ((ruby-in-ppss-context-p 'string)
+          (goto-char (nth 8 (syntax-ppss)))
+          (unless backward
+            (forward-sexp)
+            (when (bolp) (forward-char -1)))) ; After a heredoc.
+         (t
+          (let ((state (ruby-parse-region (point) (line-end-position))))
+            (unless (car state) ; Line ends with unfinished string.
+              (setq depth (+ (nth 2 state) depth))))
+          (cond
+           ;; Deeper indentation, we found a block.
+           ;; FIXME: We can't recognize empty blocks this way.
+           ((> (* signum depth) 0)
+            (setq down t))
+           ;; Block found, and same indentation as when started, stop.
+           ((and down (zerop depth))
+            (setq done t))
+           ;; Shallower indentation, means outer block, can stop now.
+           ((< (* signum depth) 0)
+            (setq done t)))))
+        (if done
+            (save-excursion
+              (back-to-indentation)
+              ;; Not really at the first or last line of the block, move on.
+              (if (looking-at (concat "\\<\\(" ruby-block-mid-re "\\)\\>"))
+                  (setq done nil))))))
     (back-to-indentation)))
 
 (defun ruby-beginning-of-block (&optional arg)
@@ -910,8 +1060,7 @@ With ARG, move up multiple blocks."
 (defun ruby-end-of-block (&optional arg)
   "Move forward to the end of the current block.
 With ARG, move out of multiple blocks."
-  ;; Passing a value > 1 to ruby-move-to-block currently doesn't work.
-  (interactive)
+  (interactive "p")
   (ruby-move-to-block (or arg 1)))
 
 (defun ruby-forward-sexp (&optional arg)
@@ -1034,70 +1183,87 @@ For example:
   #exit
   String#gsub
   Net::HTTP#active?
-  File::open.
+  File.open
 
 See `add-log-current-defun-function'."
-  ;; TODO: Document body
-  ;; Why does this append a period to class methods?
   (condition-case nil
       (save-excursion
-        (let (mname mlist (indent 0))
-          ;; get current method (or class/module)
-          (if (re-search-backward
-               (concat "^[ \t]*" ruby-defun-beg-re "[ \t]+"
-                       "\\("
-                       ;; \\. and :: for class method
-                        "\\([A-Za-z_]" ruby-symbol-re "*\\|\\.\\|::" "\\)"
-                        "+\\)")
-               nil t)
-              (progn
-                (setq mname (match-string 2))
-                (unless (string-equal "def" (match-string 1))
-                  (setq mlist (list mname) mname nil))
-                (goto-char (match-beginning 1))
-                (setq indent (current-column))
-                (beginning-of-line)))
-          ;; nest class/module
+        (let* ((indent 0) mname mlist
+               (start (point))
+               (make-definition-re
+                (lambda (re)
+                  (concat "^[ \t]*" re "[ \t]+"
+                          "\\("
+                          ;; \\. and :: for class methods
+                          "\\([A-Za-z_]" ruby-symbol-re "*\\|\\.\\|::" "\\)"
+                          "+\\)")))
+               (definition-re (funcall make-definition-re ruby-defun-beg-re))
+               (module-re (funcall make-definition-re "\\(class\\|module\\)")))
+          ;; Get the current method definition (or class/module).
+          (when (re-search-backward definition-re nil t)
+            (goto-char (match-beginning 1))
+            (if (not (string-equal "def" (match-string 1)))
+                (setq mlist (list (match-string 2)))
+              ;; We're inside the method. For classes and modules,
+              ;; this check is skipped for performance.
+              (when (ruby-block-contains-point start)
+                (setq mname (match-string 2))))
+            (setq indent (current-column))
+            (beginning-of-line))
+          ;; Walk up the class/module nesting.
           (while (and (> indent 0)
-                      (re-search-backward
-                       (concat
-                        "^[ \t]*\\(class\\|module\\)[ \t]+"
-                        "\\([A-Z]" ruby-symbol-re "*\\)")
-                       nil t))
+                      (re-search-backward module-re nil t))
             (goto-char (match-beginning 1))
-            (if (< (current-column) indent)
-                (progn
-                  (setq mlist (cons (match-string 2) mlist))
-                  (setq indent (current-column))
-                  (beginning-of-line))))
+            (when (< (current-column) indent)
+              (setq mlist (cons (match-string 2) mlist))
+              (setq indent (current-column))
+              (beginning-of-line)))
+          ;; Process the method name.
           (when mname
             (let ((mn (split-string mname "\\.\\|::")))
               (if (cdr mn)
                   (progn
-                    (cond
-                     ((string-equal "" (car mn))
-                      (setq mn (cdr mn) mlist nil))
-                     ((string-equal "self" (car mn))
-                      (setq mn (cdr mn)))
-                     ((let ((ml (nreverse mlist)))
+                    (unless (string-equal "self" (car mn)) ; def self.foo
+                      ;; def C.foo
+                      (let ((ml (nreverse mlist)))
+                        ;; If the method name references one of the
+                        ;; containing modules, drop the more nested ones.
                         (while ml
                           (if (string-equal (car ml) (car mn))
                               (setq mlist (nreverse (cdr ml)) ml nil))
-                          (or (setq ml (cdr ml)) (nreverse mlist))))))
-                    (if mlist
-                        (setcdr (last mlist) mn)
-                      (setq mlist mn))
-                    (setq mn (last mn 2))
-                    (setq mname (concat "." (cadr mn)))
-                    (setcdr mn nil))
-                (setq mname (concat "#" mname)))))
-          ;; generate string
+                          (or (setq ml (cdr ml)) (nreverse mlist))))
+                      (if mlist
+                          (setcdr (last mlist) (butlast mn))
+                        (setq mlist (butlast mn))))
+                    (setq mname (concat "." (car (last mn)))))
+                ;; See if the method is in singleton class context.
+                (let ((in-singleton-class
+                       (when (re-search-forward ruby-singleton-class-re start t)
+                         (goto-char (match-beginning 0))
+                         ;; FIXME: Optimize it out, too?
+                         ;; This can be slow in a large file, but
+                         ;; unlike class/module declaration
+                         ;; indentations, method definitions can be
+                         ;; intermixed with these, and may or may not
+                         ;; be additionally indented after visibility
+                         ;; keywords.
+                         (ruby-block-contains-point start))))
+                  (setq mname (concat
+                               (if in-singleton-class "." "#")
+                               mname))))))
+          ;; Generate the string.
           (if (consp mlist)
               (setq mlist (mapconcat (function identity) mlist "::")))
           (if mname
               (if mlist (concat mlist mname) mname)
             mlist)))))
 
+(defun ruby-block-contains-point (pt)
+  (save-excursion
+    (save-match-data
+      (ruby-forward-sexp)
+      (> (point) pt))))
+
 (defun ruby-brace-to-do-end (orig end)
   (let (beg-marker end-marker)
     (goto-char end)
@@ -1181,6 +1347,9 @@ If the result is do-end block, it will always be multiline."
 (declare-function ruby-syntax-propertize-heredoc "ruby-mode" (limit))
 (declare-function ruby-syntax-enclosing-percent-literal "ruby-mode" (limit))
 (declare-function ruby-syntax-propertize-percent-literal "ruby-mode" (limit))
+;; Unusual code layout confuses the byte-compiler.
+(declare-function ruby-syntax-propertize-expansion "ruby-mode" ())
+(declare-function ruby-syntax-expansion-allowed-p "ruby-mode" (parse-state))
 
 (if (eval-when-compile (fboundp #'syntax-propertize-rules))
     ;; New code that works independently from font-lock.
@@ -1194,51 +1363,73 @@ If the result is do-end block, it will always be multiline."
           '("gsub" "gsub!" "sub" "sub!" "scan" "split" "split!" "index" "match"
             "assert_match" "Given" "Then" "When")
           "Methods that can take regexp as the first argument.
-It will be properly highlighted even when the call omits parens."))
+It will be properly highlighted even when the call omits parens.")
+
+        (defvar ruby-syntax-before-regexp-re
+          (concat
+           ;; Special tokens that can't be followed by a division operator.
+           "\\(^\\|[[=(,~;<>]"
+           ;; Distinguish ternary operator tokens.
+           ;; FIXME: They don't really have to be separated with spaces.
+           "\\|[?:] "
+           ;; Control flow keywords and operators following bol or whitespace.
+           "\\|\\(?:^\\|\\s \\)"
+           (regexp-opt '("if" "elsif" "unless" "while" "until" "when" "and"
+                         "or" "not" "&&" "||"))
+           ;; Method name from the list.
+           "\\|\\_<"
+           (regexp-opt ruby-syntax-methods-before-regexp)
+           "\\)\\s *")
+          "Regexp to match text that can be followed by a regular expression."))
 
       (defun ruby-syntax-propertize-function (start end)
         "Syntactic keywords for Ruby mode.  See `syntax-propertize-function'."
-        (goto-char start)
-        (ruby-syntax-propertize-heredoc end)
-        (ruby-syntax-enclosing-percent-literal end)
-        (funcall
-         (syntax-propertize-rules
-          ;; $' $" $` .... are variables.
-          ;; ?' ?" ?` are ascii codes.
-          ("\\([?$]\\)[#\"'`]"
-           (1 (unless (save-excursion
-                        ;; Not within a string.
-                        (nth 3 (syntax-ppss (match-beginning 0))))
-                (string-to-syntax "\\"))))
-          ;; Regexps: regexps are distinguished from division because
-          ;; of the keyword, symbol, or method name before them.
-          ((concat
-            ;; Special tokens that can't be followed by a division operator.
-            "\\(^\\|[[=(,~?:;<>]"
-            ;; Control flow keywords and operators following bol or whitespace.
-            "\\|\\(?:^\\|\\s \\)"
-            (regexp-opt '("if" "elsif" "unless" "while" "until" "when" "and"
-                          "or" "not" "&&" "||"))
-            ;; Method name from the list.
-            "\\|\\_<"
-            (regexp-opt ruby-syntax-methods-before-regexp)
-            "\\)\\s *"
-            ;; The regular expression itself.
-            "\\(/\\)[^/\n\\\\]*\\(?:\\\\.[^/\n\\\\]*\\)*\\(/\\)")
-           (2 (string-to-syntax "\"/"))
-           (3 (string-to-syntax "\"/")))
-          ("^=en\\(d\\)\\_>" (1 "!"))
-          ("^\\(=\\)begin\\_>" (1 "!"))
-          ;; Handle here documents.
-          ((concat ruby-here-doc-beg-re ".*\\(\n\\)")
-           (7 (unless (ruby-singleton-class-p (match-beginning 0))
-                (put-text-property (match-beginning 7) (match-end 7)
-                                   'syntax-table (string-to-syntax "\""))
-                (ruby-syntax-propertize-heredoc end))))
-          ;; Handle percent literals: %w(), %q{}, etc.
-          ((concat "\\(?:^\\|[[ \t\n<+(,=]\\)" ruby-percent-literal-beg-re)
-           (1 (prog1 "|" (ruby-syntax-propertize-percent-literal end)))))
-         (point) end))
+        (let (case-fold-search)
+          (goto-char start)
+          (remove-text-properties start end '(ruby-expansion-match-data))
+          (ruby-syntax-propertize-heredoc end)
+          (ruby-syntax-enclosing-percent-literal end)
+          (funcall
+           (syntax-propertize-rules
+            ;; $' $" $` .... are variables.
+            ;; ?' ?" ?` are ascii codes.
+            ("\\([?$]\\)[#\"'`]"
+             (1 (unless (save-excursion
+                          ;; Not within a string.
+                          (nth 3 (syntax-ppss (match-beginning 0))))
+                  (string-to-syntax "\\"))))
+            ;; Regular expressions.  Start with matching unescaped slash.
+            ("\\(?:\\=\\|[^\\]\\)\\(?:\\\\\\\\\\)*\\(/\\)"
+             (1 (let ((state (save-excursion (syntax-ppss (match-beginning 1)))))
+                  (when (or
+                         ;; Beginning of a regexp.
+                         (and (null (nth 8 state))
+                              (save-excursion
+                                (forward-char -1)
+                                (looking-back ruby-syntax-before-regexp-re
+                                              (point-at-bol))))
+                         ;; End of regexp.  We don't match the whole
+                         ;; regexp at once because it can have
+                         ;; string interpolation inside, or span
+                         ;; several lines.
+                         (eq ?/ (nth 3 state)))
+                    (string-to-syntax "\"/")))))
+            ;; Expression expansions in strings.  We're handling them
+            ;; here, so that the regexp rule never matches inside them.
+            (ruby-expression-expansion-re
+             (0 (ignore (ruby-syntax-propertize-expansion))))
+            ("^=en\\(d\\)\\_>" (1 "!"))
+            ("^\\(=\\)begin\\_>" (1 "!"))
+            ;; Handle here documents.
+            ((concat ruby-here-doc-beg-re ".*\\(\n\\)")
+             (7 (unless (ruby-singleton-class-p (match-beginning 0))
+                  (put-text-property (match-beginning 7) (match-end 7)
+                                     'syntax-table (string-to-syntax "\""))
+                  (ruby-syntax-propertize-heredoc end))))
+            ;; Handle percent literals: %w(), %q{}, etc.
+            ((concat "\\(?:^\\|[[ \t\n<+(,=]\\)" ruby-percent-literal-beg-re)
+             (1 (prog1 "|" (ruby-syntax-propertize-percent-literal end)))))
+           (point) end)))
 
       (defun ruby-syntax-propertize-heredoc (limit)
         (let ((ppss (syntax-ppss))
@@ -1251,7 +1442,7 @@ It will be properly highlighted even when the call omits parens."))
                                         (line-end-position) t)
                 (unless (ruby-singleton-class-p (match-beginning 0))
                   (push (concat (ruby-here-doc-end-match) "\n") res))))
-            (let ((start (point)))
+            (save-excursion
               ;; With multiple openers on the same line, we don't know in which
               ;; part `start' is, so we have to go back to the beginning.
               (when (cdr res)
@@ -1261,9 +1452,9 @@ It will be properly highlighted even when the call omits parens."))
                 (if (null res)
                     (put-text-property (1- (point)) (point)
                                        'syntax-table (string-to-syntax "\""))))
-              ;; Make extra sure we don't move back, lest we could fall into an
-              ;; inf-loop.
-              (if (< (point) start) (goto-char start))))))
+              ;; End up at bol following the heredoc openers.
+              ;; Propertize expression expansions from this point forward.
+              ))))
 
       (defun ruby-syntax-enclosing-percent-literal (limit)
         (let ((state (syntax-ppss))
@@ -1284,27 +1475,59 @@ It will be properly highlighted even when the call omits parens."))
                  (cl (or (cdr (aref (syntax-table) op))
                          (cdr (assoc op '((?< . ?>))))))
                  parse-sexp-lookup-properties)
-            (condition-case nil
-                (progn
-                  (if cl ; Paired delimiters.
-                      ;; Delimiter pairs of the same kind can be nested
-                      ;; inside the literal, as long as they are balanced.
-                      ;; Create syntax table that ignores other characters.
-                      (with-syntax-table (make-char-table 'syntax-table nil)
-                        (modify-syntax-entry op (concat "(" (char-to-string cl)))
-                        (modify-syntax-entry cl (concat ")" ops))
-                        (modify-syntax-entry ?\\ "\\")
-                        (save-restriction
-                          (narrow-to-region (point) limit)
-                          (forward-list))) ; skip to the paired character
-                    ;; Single character delimiter.
-                    (re-search-forward (concat "[^\\]\\(?:\\\\\\\\\\)*"
-                                               (regexp-quote ops)) limit nil))
-                  ;; Found the closing delimiter.
-                  (put-text-property (1- (point)) (point) 'syntax-table
-                                     (string-to-syntax "|")))
-              ;; Unclosed literal, leave the following text unpropertized.
-              ((scan-error search-failed) (goto-char limit))))))
+            (save-excursion
+              (condition-case nil
+                  (progn
+                    (if cl              ; Paired delimiters.
+                        ;; Delimiter pairs of the same kind can be nested
+                        ;; inside the literal, as long as they are balanced.
+                        ;; Create syntax table that ignores other characters.
+                        (with-syntax-table (make-char-table 'syntax-table nil)
+                          (modify-syntax-entry op (concat "(" (char-to-string cl)))
+                          (modify-syntax-entry cl (concat ")" ops))
+                          (modify-syntax-entry ?\\ "\\")
+                          (save-restriction
+                            (narrow-to-region (point) limit)
+                            (forward-list))) ; skip to the paired character
+                      ;; Single character delimiter.
+                      (re-search-forward (concat "[^\\]\\(?:\\\\\\\\\\)*"
+                                                 (regexp-quote ops)) limit nil))
+                    ;; Found the closing delimiter.
+                    (put-text-property (1- (point)) (point) 'syntax-table
+                                       (string-to-syntax "|")))
+                ;; Unclosed literal, do nothing.
+                ((scan-error search-failed)))))))
+
+      (defun ruby-syntax-propertize-expansion ()
+        ;; Save the match data to a text property, for font-locking later.
+        ;; Set the syntax of all double quotes and backticks to punctuation.
+        (let* ((beg (match-beginning 2))
+               (end (match-end 2))
+               (state (and beg (save-excursion (syntax-ppss beg)))))
+          (when (ruby-syntax-expansion-allowed-p state)
+            (put-text-property beg (1+ beg) 'ruby-expansion-match-data
+                               (match-data))
+            (goto-char beg)
+            (while (re-search-forward "[\"`]" end 'move)
+              (put-text-property (match-beginning 0) (match-end 0)
+                                 'syntax-table (string-to-syntax "."))))))
+
+      (defun ruby-syntax-expansion-allowed-p (parse-state)
+        "Return non-nil if expression expansion is allowed."
+        (let ((term (nth 3 parse-state)))
+          (cond
+           ((memq term '(?\" ?` ?\n ?/)))
+           ((eq term t)
+            (save-match-data
+              (save-excursion
+                (goto-char (nth 8 parse-state))
+                (looking-at "%\\(?:[QWrx]\\|\\W\\)")))))))
+
+      (defun ruby-syntax-propertize-expansions (start end)
+        (save-excursion
+          (goto-char start)
+          (while (re-search-forward ruby-expression-expansion-re end 'move)
+            (ruby-syntax-propertize-expansion))))
       )
 
   ;; For Emacsen where syntax-propertize-rules is not (yet) available,
@@ -1503,19 +1726,18 @@ See `font-lock-syntax-table'.")
    ;; functions
    '("^\\s *def\\s +\\([^( \t\n]+\\)"
      1 font-lock-function-name-face)
-   ;; keywords
-   (cons (concat
-          "\\(^\\|[^_:.@$]\\|\\.\\.\\)\\b\\(defined\\?\\|"
+   (list (concat
+          "\\(^\\|[^.@$]\\|\\.\\.\\)\\("
+          ;; keywords
           (regexp-opt
-           '("alias_method"
-             "alias"
+           '("alias"
              "and"
              "begin"
              "break"
              "case"
-             "catch"
              "class"
              "def"
+             "defined?"
              "do"
              "elsif"
              "else"
@@ -1525,21 +1747,15 @@ See `font-lock-syntax-table'.")
              "end"
              "if"
              "in"
-             "module_function"
              "module"
              "next"
              "not"
              "or"
-             "public"
-             "private"
-             "protected"
-             "raise"
              "redo"
              "rescue"
              "retry"
              "return"
              "then"
-             "throw"
              "super"
              "unless"
              "undef"
@@ -1547,14 +1763,45 @@ See `font-lock-syntax-table'.")
              "when"
              "while"
              "yield")
-           t)
-          "\\)"
-          ruby-keyword-end-re)
-         2)
+           'symbols)
+          "\\|"
+          ;; keyword-like methods on Kernel and Module
+          (regexp-opt
+           '("alias_method"
+             "autoload"
+             "attr"
+             "attr_accessor"
+             "attr_reader"
+             "attr_writer"
+             "catch"
+             "define_method"
+             "extend"
+             "fail"
+             "include"
+             "lambda"
+             "loop"
+             "module_function"
+             "private"
+             "proc"
+             "protected"
+             "public"
+             "raise"
+             "refine"
+             "require"
+             "require_relative"
+             "throw"
+             "using")
+           'symbols)
+          "\\)")
+         2
+         '(if (match-beginning 4)
+              font-lock-builtin-face
+            font-lock-keyword-face))
    ;; here-doc beginnings
-   (list ruby-here-doc-beg-re 0 'font-lock-string-face)
+   `(,ruby-here-doc-beg-re 0 (unless (ruby-singleton-class-p (match-beginning 0))
+                               'font-lock-string-face))
    ;; variables
-   '("\\(^\\|[^_:.@$]\\|\\.\\.\\)\\b\\(nil\\|self\\|true\\|false\\)\\>"
+   '("\\(^\\|[^.@$]\\|\\.\\.\\)\\_<\\(nil\\|self\\|true\\|false\\)\\>"
      2 font-lock-variable-name-face)
    ;; symbols
    '("\\(^\\|[^:]\\)\\(:\\([-+~]@?\\|[/%&|^`]\\|\\*\\*?\\|<\\(<\\|=>?\\)?\\|>[>=]?\\|===?\\|=~\\|![~=]?\\|\\[\\]=?\\|@?\\(\\w\\|_\\)+\\([!?=]\\|\\b_*\\)\\|#{[^}\n\\\\]*\\(\\\\.[^}\n\\\\]*\\)*}\\)\\)"
@@ -1565,12 +1812,12 @@ See `font-lock-syntax-table'.")
    '("\\(\\$\\|@\\|@@\\)\\(\\w\\|_\\)+"
      0 font-lock-variable-name-face)
    ;; constants
-   '("\\(^\\|[^_]\\)\\b\\([A-Z]+\\(\\w\\|_\\)*\\)"
-     2 font-lock-type-face)
+   '("\\(?:\\_<\\|::\\)\\([A-Z]+\\(\\w\\|_\\)*\\)"
+     1 font-lock-type-face)
    '("\\(^\\s *\\|[\[\{\(,]\\s *\\|\\sw\\s +\\)\\(\\(\\sw\\|_\\)+\\):[^:]" 2 font-lock-constant-face)
    ;; expression expansion
    '(ruby-match-expression-expansion
-     0 font-lock-variable-name-face t)
+     2 font-lock-variable-name-face t)
    ;; warn lower camel case
                                         ;'("\\<[a-z]+[a-z0-9]*[A-Z][A-Za-z0-9]*\\([!?]?\\|\\>\\)"
                                         ;  0 font-lock-warning-face)
@@ -1578,9 +1825,14 @@ See `font-lock-syntax-table'.")
   "Additional expressions to highlight in Ruby mode.")
 
 (defun ruby-match-expression-expansion (limit)
-  (when (re-search-forward "[^\\]\\(\\\\\\\\\\)*\\(#\\({[^}\n\\\\]*\\(\\\\.[^}\n\\\\]*\\)*}\\|\\(\\$\\|@\\|@@\\)\\(\\w\\|_\\)+\\)\\)" limit 'move)
-    (or (ruby-in-ppss-context-p 'string)
-        (ruby-match-expression-expansion limit))))
+  (let* ((prop 'ruby-expansion-match-data)
+         (pos (next-single-char-property-change (point) prop nil limit))
+         value)
+    (when (and pos (> pos (point)))
+      (goto-char pos)
+      (or (and (setq value (get-text-property pos prop))
+               (progn (set-match-data value) t))
+          (ruby-match-expression-expansion limit)))))
 
 ;;;###autoload
 (define-derived-mode ruby-mode prog-mode "Ruby"
@@ -1628,7 +1880,14 @@ The variable `ruby-indent-level' controls the amount of indentation.
 ;;; Invoke ruby-mode when appropriate
 
 ;;;###autoload
-(add-to-list 'auto-mode-alist (cons (purecopy "\\.rb\\'") 'ruby-mode))
+(add-to-list 'auto-mode-alist
+             (cons (purecopy (concat "\\(?:\\."
+                                     "rb\\|ru\\|rake\\|thor"
+                                     "\\|jbuilder\\|gemspec"
+                                     "\\|/"
+                                     "\\(?:Gem\\|Rake\\|Cap\\|Thor"
+                                     "Vagrant\\|Guard\\)file"
+                                     "\\)\\'")) 'ruby-mode))
 
 ;;;###autoload
 (dolist (name (list "ruby" "rbx" "jruby" "ruby1.9" "ruby1.8"))