;;; ruby-mode.el --- Major mode for editing Ruby files
-;; Copyright (C) 1994-2012 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.
(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.")
(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)
(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) "")
(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"))
(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)
(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))
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
(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)
(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)
#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)
(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.
'("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))
(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)
(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))
(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,
;; 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"
"end"
"if"
"in"
- "module_function"
"module"
"next"
"not"
"or"
- "public"
- "private"
- "protected"
- "raise"
"redo"
"rescue"
"retry"
"return"
"then"
- "throw"
"super"
"unless"
"undef"
"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\\\\]*\\)*}\\)\\)"
'("\\(\\$\\|@\\|@@\\)\\(\\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)
"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"
;;; 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"))