;;; ruby-mode.el --- Major mode for editing Ruby files
-;; Copyright (C) 1994, 1995, 1996 1997, 1998, 1999, 2000, 2001, 2002,
-;; 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011
-;; Free Software Foundation, Inc.
+;; Copyright (C) 1994-2012 Free Software Foundation, Inc.
;; Authors: Yukihiro Matsumoto
;; Nobuyoshi Nakada
(regexp-opt (append ruby-modifier-beg-keywords ruby-block-op-keywords))
"Regexp to match hanging block modifiers.")
-(defconst ruby-block-end-re "\\<end\\>")
+(defconst ruby-block-end-re "\\_<end\\_>")
(eval-and-compile
(defconst ruby-here-doc-beg-re
(match-string 6)))))
(defconst ruby-delimiter
- (concat "[?$/%(){}#\"'`.:]\\|<<\\|\\[\\|\\]\\|\\<\\("
+ (concat "[?$/%(){}#\"'`.:]\\|<<\\|\\[\\|\\]\\|\\_<\\("
ruby-block-beg-re
- "\\)\\>\\|" ruby-block-end-re
+ "\\)\\_>\\|" ruby-block-end-re
"\\|^=begin\\|" ruby-here-doc-beg-re))
(defconst ruby-negative
(define-key map (kbd "M-C-q") 'ruby-indent-exp)
(define-key map (kbd "C-M-h") 'backward-kill-word)
(define-key map (kbd "C-j") 'reindent-then-newline-and-indent)
- (define-key map (kbd "C-m") 'newline)
- (define-key map (kbd "C-c C-c") 'comment-region)
+ (define-key map (kbd "C-c {") 'ruby-toggle-block)
map)
"Keymap used in Ruby mode.")
(modify-syntax-entry ?$ "." 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)
((and (eq c ?:) (or (not b) (eq (char-syntax b) ? ))))
((eq c ?\\) (eq b ??)))))
+(defun ruby-singleton-class-p (&optional pos)
+ (save-excursion
+ (when pos (goto-char pos))
+ (forward-word -1)
+ (and (or (bolp) (not (eq (char-before (point)) ?_)))
+ (looking-at "class\\s *<<"))))
+
(defun ruby-expr-beg (&optional option)
"TODO: document."
(save-excursion
(store-match-data nil)
- (let ((space (skip-chars-backward " \t")))
+ (let ((space (skip-chars-backward " \t"))
+ (start (point)))
(cond
((bolp) t)
((progn
(or (eq (char-syntax (char-before (point))) ?w)
(ruby-special-char-p))))
nil)
- ((and (eq option 'heredoc) (< space 0)) t)
+ ((and (eq option 'heredoc) (< space 0))
+ (not (progn (goto-char start) (ruby-singleton-class-p))))
((or (looking-at ruby-operator-re)
(looking-at "[\\[({,;]")
(and (looking-at "[!?]")
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)
(setq nest (cons (cons nil pnt) nest))
(setq depth (1+ depth))))
(goto-char (match-end 0)))
- ((looking-at (concat "\\<\\(" ruby-block-beg-re "\\)\\>"))
+ ((looking-at (concat "\\_<\\(" ruby-block-beg-re "\\)\\_>"))
(and
(save-match-data
(or (not (looking-at (concat "do" ruby-keyword-end-re)))
(eq ?. w)))))
(goto-char pnt)
(setq w (char-after (point)))
- (not (eq ?_ w))
(not (eq ?! w))
- (not (eq ?? w))
(skip-chars-forward " \t")
(goto-char (match-beginning 0))
(or (not (looking-at ruby-modifier-re))
(goto-char pnt))
((looking-at ":\\(['\"]\\)")
(goto-char (match-beginning 1))
- (ruby-forward-string (buffer-substring (match-beginning 1) (match-end 1)) end))
+ (ruby-forward-string (match-string 1) end t))
((looking-at ":\\([-,.+*/%&|^~<>]=?\\|===?\\|<=>\\|![~=]?\\)")
(goto-char (match-end 0)))
((looking-at ":\\([a-zA-Z_][a-zA-Z_0-9]*[!?=]?\\)?")
(not (looking-at "[a-z_]"))))
(and (looking-at ruby-operator-re)
(not (ruby-special-char-p))
- ;; operator at the end of line
+ ;; Operator at the end of line.
(let ((c (char-after (point))))
(and
;; (or (null begin)
;; (not (or (eolp) (looking-at "#")
;; (and (eq (car (nth 1 state)) ?{)
;; (looking-at "|"))))))
- (or (not (eq ?/ c))
- (null (nth 0 (ruby-parse-region (or begin parse-start) (point)))))
+ ;; Not a regexp or general delimited literal.
+ (null (nth 0 (ruby-parse-region (or begin parse-start)
+ (point))))
(or (not (eq ?| (char-after (point))))
(save-excursion
(or (eolp) (forward-char -1))
;; It seems like it should move to the line where indentation should deepen,
;; but ruby-indent-beg-re only accounts for whitespace before class, module and def,
;; so this will only match other block beginners at the beginning of the line.
- (and (re-search-backward (concat "^\\(" ruby-indent-beg-re "\\)\\b") nil 'move)
+ (and (re-search-backward (concat "^\\(" ruby-indent-beg-re "\\)\\_>") nil 'move)
(beginning-of-line)))
(defun ruby-move-to-block (n)
;; TODO: Make this work for n > 1,
;; make it not loop for n = 0,
;; document body
- (let (start pos done down)
- (setq start (ruby-calculate-indent))
- (setq down (looking-at (if (< n 0) ruby-block-end-re
- (concat "\\<\\(" ruby-block-beg-re "\\)\\>"))))
+ (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
(save-excursion
(back-to-indentation)
(if (looking-at (concat "\\<\\(" ruby-block-mid-re "\\)\\>"))
- (setq done nil))))))
- (back-to-indentation))
+ (setq done nil)))))
+ (back-to-indentation)
+ (when (< n 0)
+ (let ((eol (point-at-eol)) state next)
+ (if (< orig eol) (setq eol orig))
+ (setq orig (point))
+ (while (and (setq next (apply 'ruby-parse-partial eol state))
+ (< (point) eol))
+ (setq state next))
+ (when (cdaadr state)
+ (goto-char (cdaadr state)))
+ (backward-word)))))
(defun ruby-beginning-of-block (&optional arg)
"Move backward to the beginning of the current block.
(goto-char (scan-sexps (1+ (point)) -1))
(case (char-before)
(?% (forward-char -1))
- ('(?q ?Q ?w ?W ?r ?x)
+ ((?q ?Q ?w ?W ?r ?x)
(if (eq (char-before (1- (point))) ?%) (forward-char -2))))
nil)
((looking-at "\\s\"\\|\\\\\\S_")
(if mlist (concat mlist mname) mname)
mlist)))))
+(defun ruby-brace-to-do-end ()
+ (when (looking-at "{")
+ (let ((orig (point)) (end (progn (ruby-forward-sexp) (point))))
+ (when (eq (char-before) ?\})
+ (delete-char -1)
+ (if (eq (char-syntax (char-before)) ?w)
+ (insert " "))
+ (insert "end")
+ (if (eq (char-syntax (char-after)) ?w)
+ (insert " "))
+ (goto-char orig)
+ (delete-char 1)
+ (if (eq (char-syntax (char-before)) ?w)
+ (insert " "))
+ (insert "do")
+ (when (looking-at "\\sw\\||")
+ (insert " ")
+ (backward-char))
+ t))))
+
+(defun ruby-do-end-to-brace ()
+ (when (and (or (bolp)
+ (not (memq (char-syntax (char-before)) '(?w ?_))))
+ (looking-at "\\<do\\(\\s \\|$\\)"))
+ (let ((orig (point)) (end (progn (ruby-forward-sexp) (point))))
+ (backward-char 3)
+ (when (looking-at ruby-block-end-re)
+ (delete-char 3)
+ (insert "}")
+ (goto-char orig)
+ (delete-char 2)
+ (insert "{")
+ (if (looking-at "\\s +|")
+ (delete-char (- (match-end 0) (match-beginning 0) 1)))
+ t))))
+
+(defun ruby-toggle-block ()
+ (interactive)
+ (or (ruby-brace-to-do-end)
+ (ruby-do-end-to-brace)))
+
(declare-function ruby-syntax-propertize-heredoc "ruby-mode" (limit))
+(declare-function ruby-syntax-general-delimiters-goto-beg "ruby-mode" ())
+(declare-function ruby-syntax-propertize-general-delimiters "ruby-mode" (limit))
(if (eval-when-compile (fboundp #'syntax-propertize-rules))
;; New code that works independently from font-lock.
"Syntactic keywords for Ruby mode. See `syntax-propertize-function'."
(goto-char start)
(ruby-syntax-propertize-heredoc end)
+ (ruby-syntax-general-delimiters-goto-beg)
(funcall
(syntax-propertize-rules
- ;; #{ }, #$hoge, #@foo are not comments
+ ;; #{ }, #$hoge, #@foo are not comments.
("\\(#\\)[{$@]" (1 "."))
- ;; $' $" $` .... are variables
- ;; ?' ?" ?` are ascii codes
+ ;; $' $" $` .... are variables.
+ ;; ?' ?" ?` are ascii codes.
("\\([?$]\\)[#\"'`]"
(1 (unless (save-excursion
;; Not within a string.
(nth 3 (syntax-ppss (match-beginning 0))))
(string-to-syntax "\\"))))
- ;; regexps
- ("\\(^\\|[=(,~?:;<>]\\|\\(^\\|\\s \\)\\(if\\|elsif\\|unless\\|while\\|until\\|when\\|and\\|or\\|&&\\|||\\)\\|g?sub!?\\|scan\\|split!?\\)\\s *\\(/\\)[^/\n\\\\]*\\(\\\\.[^/\n\\\\]*\\)*\\(/\\)"
- (4 "\"/")
- (6 "\"/"))
+ ;; Regexps: regexps are distinguished from division either because
+ ;; of the keyword/symbol before them, or because of the code
+ ;; following them.
+ ((concat
+ ;; Special tokens that can't be followed by a division operator.
+ "\\(?:\\(^\\|[[=(,~?:;<>]\\|\\(?:^\\|\\s \\)"
+ (regexp-opt '("if" "elsif" "unless" "while" "until" "when" "and"
+ "or" "&&" "||"
+ "gsub" "gsub!" "sub" "sub!" "scan" "split" "split!"))
+ "\\)\\s *\\)?"
+ ;; The regular expression itself.
+ "\\(/\\)[^/\n\\\\]*\\(?:\\\\.[^/\n\\\\]*\\)*\\(/\\)"
+ ;; Special code that cannot follow a division operator.
+ ;; FIXME: Just because the second slash of "/foo/ do bar" can't
+ ;; be a division, doesn't mean it can't *start* a regexp, as in
+ ;; "x = toto/foo; if /do bar/".
+ "\\([imxo]*\\s *\\(?:,\\|\\_<do\\_>\\)\\)?")
+ (2 (when (or (match-beginning 1) (match-beginning 4))
+ (string-to-syntax "\"/")))
+ (3 (if (or (match-beginning 1) (match-beginning 4))
+ (string-to-syntax "\"/")
+ (goto-char (match-end 2)))))
("^=en\\(d\\)\\_>" (1 "!"))
("^\\(=\\)begin\\_>" (1 "!"))
;; Handle here documents.
((concat ruby-here-doc-beg-re ".*\\(\n\\)")
- (7 (prog1 "\"" (ruby-syntax-propertize-heredoc end)))))
+ (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.
+ ("\\(?:^\\|[[ \t\n<+(,=]\\)\\(%\\)[qQrswWx]?\\([[:punct:]]\\)"
+ (1 (prog1 "|" (ruby-syntax-propertize-general-delimiters end)))))
(point) end))
(defun ruby-syntax-propertize-heredoc (limit)
(beginning-of-line)
(while (re-search-forward ruby-here-doc-beg-re
(line-end-position) t)
- (push (concat (ruby-here-doc-end-match) "\n") res)))
+ (unless (ruby-singleton-class-p (match-beginning 0))
+ (push (concat (ruby-here-doc-end-match) "\n") res))))
(let ((start (point)))
;; 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.
;; Make extra sure we don't move back, lest we could fall into an
;; inf-loop.
(if (< (point) start) (goto-char start))))))
+
+ (defun ruby-syntax-general-delimiters-goto-beg ()
+ (let ((state (syntax-ppss)))
+ ;; Move to the start of the literal, in case it's multiline.
+ ;; TODO: determine the literal type more reliably here?
+ (when (eq t (nth 3 state))
+ (goto-char (nth 8 state))
+ (beginning-of-line))))
+
+ (defun ruby-syntax-propertize-general-delimiters (limit)
+ (goto-char (match-beginning 2))
+ (let* ((op (char-after))
+ (ops (char-to-string op))
+ (cl (or (cdr (aref (syntax-table) op))
+ (cdr (assoc op '((?< . ?>))))))
+ parse-sexp-lookup-properties)
+ (ignore-errors
+ (if cl
+ (progn ; 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))
+ ;; If we reached here, the closing delimiter was found.
+ (put-text-property (1- (point)) (point)
+ 'syntax-table (string-to-syntax "|")))))
)
;; For Emacsen where syntax-propertize-rules is not (yet) available,
(4 (7 . ?/))
(6 (7 . ?/)))
("^=en\\(d\\)\\_>" 1 "!")
+ ;; General delimited string.
+ ("\\(^\\|[[ \t\n<+(,=]\\)\\(%[xrqQwW]?\\([^<[{(a-zA-Z0-9 \n]\\)[^\n\\\\]*\\(\\\\.[^\n\\\\]*\\)*\\(\\3\\)\\)"
+ (3 "\"")
+ (5 "\""))
("^\\(=\\)begin\\_>" 1 (ruby-comment-beg-syntax))
;; Currently, the following case is highlighted incorrectly:
;;
(let ((old-point (point)) (case-fold-search nil))
(beginning-of-line)
(catch 'found-beg
- (while (re-search-backward ruby-here-doc-beg-re nil t)
+ (while (and (re-search-backward ruby-here-doc-beg-re nil t)
+ (not (ruby-singleton-class-p)))
(if (not (or (ruby-in-ppss-context-p 'anything)
(ruby-here-doc-find-end old-point)))
(throw 'found-beg t)))))))
1 font-lock-variable-name-face)
'("\\(\\$\\|@\\|@@\\)\\(\\w\\|_\\)+"
0 font-lock-variable-name-face)
- ;; general delimited string
- '("\\(^\\|[[ \t\n<+(,=]\\)\\(%[xrqQwW]?\\([^<[{(a-zA-Z0-9 \n]\\)[^\n\\\\]*\\(\\\\.[^\n\\\\]*\\)*\\(\\3\\)\\)"
- (2 font-lock-string-face))
;; constants
'("\\(^\\|[^_]\\)\\b\\([A-Z]+\\(\\w\\|_\\)*\\)"
2 font-lock-type-face)