(defconst ruby-symbol-re (concat "[" ruby-symbol-chars "]")
"Regexp to match symbols.")
-(define-abbrev-table 'ruby-mode-abbrev-table ()
- "Abbrev table in use in Ruby mode buffers.")
-
(defvar ruby-use-smie t)
(defvar ruby-mode-map
(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))
+ (when ruby-use-smie
+ (define-key map (kbd "M-C-d") 'smie-down-list))
(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)
map)
"Keymap used in Ruby mode.")
+(easy-menu-define
+ ruby-mode-menu
+ ruby-mode-map
+ "Ruby Mode Menu"
+ '("Ruby"
+ ["Beginning of Block" ruby-beginning-of-block t]
+ ["End of Block" ruby-end-of-block t]
+ ["Toggle Block" ruby-toggle-block t]
+ "--"
+ ["Backward Sexp" ruby-backward-sexp
+ :visible (not ruby-use-smie)]
+ ["Backward Sexp" backward-sexp
+ :visible ruby-use-smie]
+ ["Forward Sexp" ruby-forward-sexp
+ :visible (not ruby-use-smie)]
+ ["Forward Sexp" forward-sexp
+ :visible ruby-use-smie]
+ ["Indent Sexp" ruby-indent-exp
+ :visible (not ruby-use-smie)]
+ ["Indent Sexp" prog-indent-sexp
+ :visible ruby-use-smie]))
+
(defvar ruby-mode-syntax-table
(let ((table (make-syntax-table)))
(modify-syntax-entry ?\' "\"" table)
(defcustom ruby-indent-tabs-mode nil
"Indentation can insert tabs in Ruby mode if this is non-nil."
- :type 'boolean :group 'ruby)
+ :type 'boolean
+ :group 'ruby
+ :safe 'booleanp)
(defcustom ruby-indent-level 2
"Indentation of Ruby statements."
- :type 'integer :group 'ruby)
+ :type 'integer
+ :group 'ruby
+ :safe 'integerp)
(defcustom ruby-comment-column (default-value 'comment-column)
"Indentation column of comments."
- :type 'integer :group 'ruby)
+ :type 'integer
+ :group 'ruby
+ :safe 'integerp)
(defcustom ruby-deep-arglist t
"Deep indent lists in parenthesis when non-nil.
Also ignores spaces after parenthesis when 'space."
- :group 'ruby)
+ :type 'boolean
+ :group 'ruby
+ :safe 'booleanp)
(defcustom ruby-deep-indent-paren '(?\( ?\[ ?\] t)
"Deep indent lists in parenthesis when non-nil.
"Use `ruby-encoding-map' to set encoding magic comment if this is non-nil."
:type 'boolean :group 'ruby)
-;; Safe file variables
-(put 'ruby-indent-tabs-mode 'safe-local-variable 'booleanp)
-(put 'ruby-indent-level 'safe-local-variable 'integerp)
-(put 'ruby-comment-column 'safe-local-variable 'integerp)
-(put 'ruby-deep-arglist 'safe-local-variable 'booleanp)
-
;;; SMIE support
(require 'smie)
+;; Here's a simplified BNF grammar, for reference:
+;; http://www.cse.buffalo.edu/~regan/cse305/RubyBNF.pdf
(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) (exp "=" exp) (exp "-" exp) (exp "+" exp)
- (id " @ " 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 "}")
- ("{" insts "}")
- ("while" insts "end")
- ("until" insts "end")
- ("unless" insts "end")
- ("if" if-body "end")
- ("case" cases "end"))
- (formal-params ("opening-|" exp "|"))
- (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 ";") (right " @ ")
- (assoc ",") (right "=") (assoc "-" "+"))
- '((assoc "when"))
- '((assoc "elsif"))
- '((assoc "rescue" "ensure"))
- '((assoc ",")))))
+ (smie-merge-prec2s
+ (smie-bnf->prec2
+ '((id)
+ (insts (inst) (insts ";" insts))
+ (inst (exp) (inst "iuwu-mod" exp))
+ (exp (exp1) (exp "," exp) (exp "=" exp)
+ (id " @ " exp)
+ (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 "}")
+ ("{" insts "}")
+ ("while" insts "end")
+ ("until" insts "end")
+ ("unless" insts "end")
+ ("if" if-body "end")
+ ("case" cases "end"))
+ (formal-params ("opening-|" exp "|"))
+ (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 ";") (right " @ ")
+ (assoc ",") (right "=") (assoc "."))
+ '((assoc "when"))
+ '((assoc "elsif"))
+ '((assoc "rescue" "ensure"))
+ '((assoc ",")))
+
+ (smie-precs->prec2
+ '((right "=")
+ (right "+=" "-=" "*=" "/=" "%=" "**=" "&=" "|=" "^="
+ "<<=" ">>=" "&&=" "||=")
+ (left ".." "...")
+ (left "+" "-")
+ (left "*" "/" "%" "**")
+ ;; (left "|") ; FIXME: Conflicts with | after block parameters.
+ (left "^" "&")
+ (nonassoc "<=>")
+ (nonassoc ">" ">=" "<" "<=")
+ (nonassoc "==" "===" "!=")
+ (nonassoc "=~" "!~")
+ (left "<<" ">>")
+ (left "&&" "||"))))))
(defun ruby-smie--bosp ()
(save-excursion (skip-chars-backward " \t")
- (or (bolp) (eq (char-before) ?\;))))
+ (or (bolp) (memq (char-before) '(?\; ?=)))))
(defun ruby-smie--implicit-semi-p ()
(save-excursion
(skip-chars-backward " \t")
(not (or (bolp)
(and (memq (char-before)
- '(?\; ?- ?+ ?* ?/ ?: ?. ?, ?\[ ?\( ?\{ ?\\))
+ '(?\; ?- ?+ ?* ?/ ?: ?. ?, ?\[ ?\( ?\{ ?\\ ?& ?> ?< ?% ?~))
;; Make sure it's not the end of a regexp.
(not (eq (car (syntax-after (1- (point)))) 7)))
(and (eq (char-before) ?\?)
(defun ruby-smie--args-separator-p (pos)
(and
- (eq ?w (char-syntax (char-before)))
- (< pos (point-max))
- (memq (char-syntax (char-after pos)) '(?w ?\"))))
-
-(defun ruby-smie--forward-id ()
- (when (and (not (eobp))
- (eq ?w (char-syntax (char-after))))
- (let ((tok (smie-default-forward-token)))
- (when (eq ?. (char-after))
- (forward-char 1)
- (setq tok (concat tok "." (ruby-smie--forward-id))))
- tok)))
+ (< pos (line-end-position))
+ (or (eq (char-syntax (preceding-char)) '?w)
+ ;; FIXME: Check that the preceding token is not a keyword.
+ ;; This isn't very important most of the time, though.
+ (and (memq (preceding-char) '(?! ??))
+ (eq (char-syntax (char-before (1- (point)))) '?w)))
+ (or (and (eq (char-syntax (char-after pos)) ?w)
+ (not (looking-at (regexp-opt '("unless" "if" "while" "until"
+ "else" "elsif" "do" "end")
+ 'symbols))))
+ (memq (syntax-after pos) '(7 15))
+ (save-excursion
+ (goto-char pos)
+ (looking-at "\\s(\\|[-+!~:]\\sw")))))
+
+(defun ruby-smie--at-dot-call ()
+ (and (eq ?w (char-syntax (following-char)))
+ (eq (char-before) ?.)
+ (not (eq (char-before (1- (point))) ?.))))
(defun ruby-smie--forward-token ()
(let ((pos (point)))
(skip-chars-forward " \t")
(cond
- ((looking-at "\\s\"") "") ;A heredoc or a string.
+ ((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#]")
(ruby-smie--implicit-semi-p)) ;Only add implicit ; when needed.
(if (eolp) (forward-char 1) (forward-comment 1))
(ruby-smie--args-separator-p (prog1 (point) (goto-char pos)))))
" @ ")
(t
- (let ((tok (smie-default-forward-token)))
+ (let ((dot (ruby-smie--at-dot-call))
+ (tok (smie-default-forward-token)))
+ (when dot
+ (setq tok (concat "." tok)))
(cond
((member tok '("unless" "if" "while" "until"))
(if (save-excursion (forward-word -1) (ruby-smie--bosp))
(line-end-position))
(ruby-smie--forward-token)) ;Fully redundant.
(t ";")))
- ((equal tok ".") (concat tok (ruby-smie--forward-id)))
(t tok)))))))))
-(defun ruby-smie--backward-id ()
- (when (and (not (bobp))
- (eq ?w (char-syntax (char-before))))
- (let ((tok (smie-default-backward-token)))
- (when (eq ?. (char-before))
- (forward-char -1)
- (setq tok (concat (ruby-smie--backward-id) "." tok)))
- tok)))
-
(defun ruby-smie--backward-token ()
(let ((pos (point)))
(forward-comment (- (point)))
(cond
((and (> pos (line-end-position)) (ruby-smie--implicit-semi-p))
(skip-chars-forward " \t") ";")
- ((and (bolp) (not (bobp))) "") ;Presumably a heredoc.
+ ((and (bolp) (not (bobp))) ;Presumably a heredoc.
+ ;; Tokenize the whole heredoc as semicolon.
+ (goto-char (scan-sexps (point) -1))
+ ";")
((and (> pos (point)) (not (bolp))
(ruby-smie--args-separator-p pos))
;; We have "ID SPC ID", which is a method call, but it binds less tightly
;; In some textbooks, "e1 @ e2" is used to mean "call e1 with arg e2".
" @ ")
(t
- (let ((tok (smie-default-backward-token)))
- (when (eq ?. (char-before))
- (forward-char -1)
+ (let ((tok (smie-default-backward-token))
+ (dot (ruby-smie--at-dot-call)))
+ (when dot
(setq tok (concat "." tok)))
(when (and (eq ?: (char-before)) (string-match "\\`\\s." tok))
(forward-char -1) (setq tok (concat ":" tok))) ;; bug#15208.
(line-end-position))
(ruby-smie--backward-token)) ;Fully redundant.
(t ";")))
- ;; FIXME: We shouldn't merge the dot with preceding token here
- ;; either, but not doing that breaks indentation of hanging
- ;; method calls with dot on the first line.
- ((equal tok ".")
- (concat (ruby-smie--backward-id) tok))
(t tok)))))))
(defun ruby-smie-rules (kind token)
;; should be aligned with the first.
(`(:elem . args) (if (looking-at "\\s\"") 0))
;; (`(:after . ",") (smie-rule-separator kind))
- (`(: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 . ";")
+ (cond
+ ((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)
+ ))
(`(:before . ,(or `"(" `"[" `"{"))
- ;; Treat purely syntactic block-constructs as being part of their parent,
- ;; when the opening statement is hanging.
- (when (smie-rule-hanging-p)
- (smie-backward-sexp 'halfsexp) (smie-indent-virtual)))
+ (cond
+ ((and (equal token "{")
+ (not (smie-rule-prev-p "(" "{" "[" "," "=>" "=" "return" ";")))
+ ;; Curly block opener.
+ (smie-rule-parent))
+ ((smie-rule-hanging-p)
+ ;; Treat purely syntactic block-constructs as being part of their parent,
+ ;; when the opening statement is hanging.
+ (let ((state (smie-backward-sexp 'halfsexp)))
+ (when (eq t (car state)) (goto-char (cadr state))))
+ (cons 'column (smie-indent-virtual)))))
(`(:after . ,(or "=" "iuwu-mod")) 2)
+ (`(:after . " @ ") (smie-rule-parent))
(`(:before . "do") (smie-rule-parent))
+ (`(,(or :before :after) . ".")
+ (unless (smie-rule-parent-p ".")
+ (smie-rule-parent ruby-indent-level)))
(`(:before . ,(or `"else" `"then" `"elsif" `"rescue" `"ensure")) 0)
(`(:before . ,(or `"when"))
(if (not (smie-rule-sibling-p)) 0)) ;; ruby-indent-level
+ (`(:after . "+") ;FIXME: Probably applicable to most infix operators.
+ (if (smie-rule-parent-p ";") ruby-indent-level))
))
(defun ruby-imenu-create-index-in-block (prefix beg end)
(defun ruby-mode-variables ()
"Set up initial buffer-local variables for Ruby mode."
- (set-syntax-table ruby-mode-syntax-table)
- (setq local-abbrev-table ruby-mode-abbrev-table)
(setq indent-tabs-mode ruby-indent-tabs-mode)
(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) "")
- (set (make-local-variable 'comment-column) ruby-comment-column)
- (set (make-local-variable 'comment-start-skip) "#+ *")
- (set (make-local-variable 'parse-sexp-ignore-comments) t)
- (set (make-local-variable 'parse-sexp-lookup-properties) t)
- (set (make-local-variable 'paragraph-start) (concat "$\\|" page-delimiter))
- (set (make-local-variable 'paragraph-separate) paragraph-start)
- (set (make-local-variable 'paragraph-ignore-fill-prefix) t))
+ (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 comment-start-skip "#+ *")
+ (setq-local parse-sexp-ignore-comments t)
+ (setq-local parse-sexp-lookup-properties t)
+ (setq-local paragraph-start (concat "$\\|" page-delimiter))
+ (setq-local paragraph-separate paragraph-start)
+ (setq-local paragraph-ignore-fill-prefix t))
(defun ruby-mode-set-encoding ()
"Insert a magic comment header with the proper encoding if necessary."
(insert "}")
(goto-char orig)
(delete-char 2)
- (insert "{")
+ ;; Maybe this should be customizable, let's see if anyone asks.
+ (insert "{ ")
(setq beg-marker (point-marker))
(when (looking-at "\\s +|")
(delete-char (- (match-end 0) (match-beginning 0) 1))
(ruby-do-end-to-brace beg end)))
(goto-char start))))
-(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))
-(declare-function ruby-syntax-propertize-function "ruby-mode" (start end))
-
-(if (eval-when-compile (fboundp #'syntax-propertize-rules))
- ;; New code that works independently from font-lock.
- (progn
- (eval-and-compile
- (defconst ruby-percent-literal-beg-re
- "\\(%\\)[qQrswWxIi]?\\([[:punct:]]\\)"
- "Regexp to match the beginning of percent literal.")
-
- (defconst ruby-syntax-methods-before-regexp
- '("gsub" "gsub!" "sub" "sub!" "scan" "split" "split!" "index" "match"
- "assert_match" "Given" "Then" "When")
- "Methods that can take regexp as the first argument.
+(eval-and-compile
+ (defconst ruby-percent-literal-beg-re
+ "\\(%\\)[qQrswWxIi]?\\([[:punct:]]\\)"
+ "Regexp to match the beginning of percent literal.")
+
+ (defconst ruby-syntax-methods-before-regexp
+ '("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.")
- (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'."
- (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 character literals (one-char strings in 1.9+).
- ("\\([?$]\\)[#\"'`]"
- (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 (or (nth 8 (save-excursion
- (syntax-ppss (match-beginning 0))))
- (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))
- (res '()))
- (when (eq ?\n (nth 3 ppss))
- (save-excursion
- (goto-char (nth 8 ppss))
- (beginning-of-line)
- (while (re-search-forward ruby-here-doc-beg-re
- (line-end-position) t)
- (unless (ruby-singleton-class-p (match-beginning 0))
- (push (concat (ruby-here-doc-end-match) "\n") res))))
- (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)
- (goto-char (nth 8 ppss))
- (setq res (nreverse res)))
- (while (and res (re-search-forward (pop res) limit 'move))
- (if (null res)
- (put-text-property (1- (point)) (point)
- 'syntax-table (string-to-syntax "\""))))
- ;; 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))
- (start (point)))
- ;; When already inside percent literal, re-propertize it.
- (when (eq t (nth 3 state))
- (goto-char (nth 8 state))
- (when (looking-at ruby-percent-literal-beg-re)
- (ruby-syntax-propertize-percent-literal limit))
- (when (< (point) start) (goto-char start)))))
-
- (defun ruby-syntax-propertize-percent-literal (limit)
- (goto-char (match-beginning 2))
- ;; Not inside a simple string or comment.
- (when (eq t (nth 3 (syntax-ppss)))
- (let* ((op (char-after))
- (ops (char-to-string op))
- (cl (or (cdr (aref (syntax-table) op))
- (cdr (assoc op '((?< . ?>))))))
- parse-sexp-lookup-properties)
- (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 "%\\(?:[QWrxI]\\|\\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,
- ;; fallback on the old font-lock-syntactic-keywords stuff.
-
- (defconst ruby-here-doc-end-re
- "^\\([ \t]+\\)?\\(.*\\)\\(\n\\)"
- "Regexp to match the end of heredocs.
-
-This will actually match any line with one or more characters.
-It's useful in that it divides up the match string so that
-`ruby-here-doc-beg-match' can search for the beginning of the heredoc.")
-
- (defun ruby-here-doc-beg-match ()
- "Return a regexp to find the beginning of a heredoc.
-
-This should only be called after matching against `ruby-here-doc-end-re'."
- (let ((contents (concat
- (regexp-quote (concat (match-string 2) (match-string 3)))
- (if (string= (match-string 3) "_") "\\B" "\\b"))))
- (concat "<<"
- (let ((match (match-string 1)))
- (if (and match (> (length match) 0))
- (concat "\\(?:-\\([\"']?\\)\\|\\([\"']\\)"
- (match-string 1) "\\)"
- contents "\\(\\1\\|\\2\\)")
- (concat "-?\\([\"']\\|\\)" contents "\\1"))))))
-
- (defconst ruby-font-lock-syntactic-keywords
- `(
- ;; the last $', $", $` in the respective string is not variable
- ;; the last ?', ?", ?` in the respective string is not ascii code
- ("\\(^\\|[\[ \t\n<+\(,=]\\)\\(['\"`]\\)\\(\\\\.\\|\\2\\|[^'\"`\n\\\\]\\)*?\\\\?[?$]\\(\\2\\)"
- (2 (7 . nil))
- (4 (7 . nil)))
- ;; $' $" $` .... are variables
- ;; ?' ?" ?` are ascii codes
- ("\\(^\\|[^\\\\]\\)\\(\\\\\\\\\\)*[?$]\\([#\"'`]\\)" 3 (1 . nil))
- ;; regexps
- ("\\(^\\|[[=(,~?:;<>]\\|\\(^\\|\\s \\)\\(if\\|elsif\\|unless\\|while\\|until\\|when\\|and\\|or\\|&&\\|||\\)\\|g?sub!?\\|scan\\|split!?\\)\\s *\\(/\\)[^/\n\\\\]*\\(\\\\.[^/\n\\\\]*\\)*\\(/\\)"
- (4 (7 . ?/))
- (6 (7 . ?/)))
- ("^=en\\(d\\)\\_>" 1 "!")
- ;; Percent literal.
- ("\\(^\\|[[ \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:
- ;;
- ;; <<FOO
- ;; FOO
- ;; <<BAR
- ;; <<BAZ
- ;; BAZ
- ;; BAR
- ;;
- ;; This is because all here-doc beginnings are highlighted before any endings,
- ;; so although <<BAR is properly marked as a beginning, when we get to <<BAZ
- ;; it thinks <<BAR is part of a string so it's marked as well.
- ;;
- ;; This may be fixable by modifying ruby-in-here-doc-p to use
- ;; ruby-in-non-here-doc-string-p rather than syntax-ppss-context,
- ;; but I don't want to try that until we've got unit tests set up
- ;; to make sure I don't break anything else.
- (,(concat ruby-here-doc-beg-re ".*\\(\n\\)")
- ,(+ 1 (regexp-opt-depth ruby-here-doc-beg-re))
- (ruby-here-doc-beg-syntax))
- (,ruby-here-doc-end-re 3 (ruby-here-doc-end-syntax)))
- "Syntactic keywords for Ruby mode. See `font-lock-syntactic-keywords'.")
-
- (defun ruby-comment-beg-syntax ()
- "Return the syntax cell for a the first character of a =begin.
-See the definition of `ruby-font-lock-syntactic-keywords'.
-
-This returns a comment-delimiter cell as long as the =begin
-isn't in a string or another comment."
- (when (not (nth 3 (syntax-ppss)))
- (string-to-syntax "!")))
-
- (defun ruby-in-here-doc-p ()
- "Return whether or not the point is in a heredoc."
- (save-excursion
- (let ((old-point (point)) (case-fold-search nil))
+ (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'."
+ (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 character literals (one-char strings in 1.9+).
+ ("\\([?$]\\)[#\"'`]"
+ (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 (or (nth 8 (save-excursion
+ (syntax-ppss (match-beginning 0))))
+ (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))
+ (res '()))
+ (when (eq ?\n (nth 3 ppss))
+ (save-excursion
+ (goto-char (nth 8 ppss))
(beginning-of-line)
- (catch 'found-beg
- (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)))))))
-
- (defun ruby-here-doc-find-end (&optional limit)
- "Expects the point to be on a line with one or more heredoc openers.
-Returns the buffer position at which all heredocs on the line
-are terminated, or nil if they aren't terminated before the
-buffer position `limit' or the end of the buffer."
- (save-excursion
- (beginning-of-line)
- (catch 'done
- (let ((eol (point-at-eol))
- (case-fold-search nil)
- ;; Fake match data such that (match-end 0) is at eol
- (end-match-data (progn (looking-at ".*$") (match-data)))
- beg-match-data end-re)
- (while (re-search-forward ruby-here-doc-beg-re eol t)
- (setq beg-match-data (match-data))
- (setq end-re (ruby-here-doc-end-match))
-
- (set-match-data end-match-data)
- (goto-char (match-end 0))
- (unless (re-search-forward end-re limit t) (throw 'done nil))
- (setq end-match-data (match-data))
-
- (set-match-data beg-match-data)
- (goto-char (match-end 0)))
- (set-match-data end-match-data)
- (goto-char (match-end 0))
- (point)))))
-
- (defun ruby-here-doc-beg-syntax ()
- "Return the syntax cell for a line that may begin a heredoc.
-See the definition of `ruby-font-lock-syntactic-keywords'.
-
-This sets the syntax cell for the newline ending the line
-containing the heredoc beginning so that cases where multiple
-heredocs are started on one line are handled correctly."
- (save-excursion
- (goto-char (match-beginning 0))
- (unless (or (ruby-in-ppss-context-p 'non-heredoc)
- (ruby-in-here-doc-p))
- (string-to-syntax "\""))))
-
- (defun ruby-here-doc-end-syntax ()
- "Return the syntax cell for a line that may end a heredoc.
-See the definition of `ruby-font-lock-syntactic-keywords'."
- (let ((pss (syntax-ppss)) (case-fold-search nil))
- ;; If we aren't in a string, we definitely aren't ending a heredoc,
- ;; so we can just give up.
- ;; This means we aren't doing a full-document search
- ;; every time we enter a character.
- (when (ruby-in-ppss-context-p 'heredoc pss)
+ (while (re-search-forward ruby-here-doc-beg-re
+ (line-end-position) t)
+ (unless (ruby-singleton-class-p (match-beginning 0))
+ (push (concat (ruby-here-doc-end-match) "\n") res))))
+ (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)
+ (goto-char (nth 8 ppss))
+ (setq res (nreverse res)))
+ (while (and res (re-search-forward (pop res) limit 'move))
+ (if (null res)
+ (put-text-property (1- (point)) (point)
+ 'syntax-table (string-to-syntax "\""))))
+ ;; 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))
+ (start (point)))
+ ;; When already inside percent literal, re-propertize it.
+ (when (eq t (nth 3 state))
+ (goto-char (nth 8 state))
+ (when (looking-at ruby-percent-literal-beg-re)
+ (ruby-syntax-propertize-percent-literal limit))
+ (when (< (point) start) (goto-char start)))))
+
+(defun ruby-syntax-propertize-percent-literal (limit)
+ (goto-char (match-beginning 2))
+ ;; Not inside a simple string or comment.
+ (when (eq t (nth 3 (syntax-ppss)))
+ (let* ((op (char-after))
+ (ops (char-to-string op))
+ (cl (or (cdr (aref (syntax-table) op))
+ (cdr (assoc op '((?< . ?>))))))
+ parse-sexp-lookup-properties)
+ (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 pss)) ; Go to the beginning of heredoc.
- (let ((eol (point)))
- (beginning-of-line)
- (if (and (re-search-forward (ruby-here-doc-beg-match) eol t) ; If there is a heredoc that matches this line...
- (not (ruby-in-ppss-context-p 'anything)) ; And that's not inside a heredoc/string/comment...
- (progn (goto-char (match-end 0)) ; And it's the last heredoc on its line...
- (not (re-search-forward ruby-here-doc-beg-re eol t))))
- (string-to-syntax "\"")))))))
+ (goto-char (nth 8 parse-state))
+ (looking-at "%\\(?:[QWrxI]\\|\\W\\)")))))))
- (unless (functionp 'syntax-ppss)
- (defun syntax-ppss (&optional pos)
- (parse-partial-sexp (point-min) (or pos (point)))))
- )
+(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))))
(defun ruby-in-ppss-context-p (context &optional ppss)
(let ((ppss (or ppss (syntax-ppss (point)))))
"context name `" (symbol-name context) "' is unknown"))))
t)))
-(if (featurep 'xemacs)
- (put 'ruby-mode 'font-lock-defaults
- '((ruby-font-lock-keywords)
- nil nil nil
- beginning-of-line
- (font-lock-syntactic-keywords
- . ruby-font-lock-syntactic-keywords))))
-
(defvar ruby-font-lock-syntax-table
(let ((tbl (copy-syntax-table ruby-mode-syntax-table)))
(modify-syntax-entry ?_ "w" tbl)
;;;###autoload
(define-derived-mode ruby-mode prog-mode "Ruby"
- "Major mode for editing Ruby scripts.
-\\[ruby-indent-line] properly indents subexpressions of multi-line
-class, module, def, if, while, for, do, and case statements, taking
-nesting into account.
-
-The variable `ruby-indent-level' controls the amount of indentation.
+ "Major mode for editing Ruby code.
\\{ruby-mode-map}"
(ruby-mode-variables)
- (set (make-local-variable 'imenu-create-index-function)
- 'ruby-imenu-create-index)
- (set (make-local-variable 'add-log-current-defun-function)
- 'ruby-add-log-current-method)
- (set (make-local-variable 'beginning-of-defun-function)
- 'ruby-beginning-of-defun)
- (set (make-local-variable 'end-of-defun-function)
- 'ruby-end-of-defun)
+ (setq-local imenu-create-index-function 'ruby-imenu-create-index)
+ (setq-local add-log-current-defun-function 'ruby-add-log-current-method)
+ (setq-local beginning-of-defun-function 'ruby-beginning-of-defun)
+ (setq-local end-of-defun-function 'ruby-end-of-defun)
(add-hook 'after-save-hook 'ruby-mode-set-encoding nil 'local)
- (set (make-local-variable 'electric-indent-chars)
- (append '(?\{ ?\}) electric-indent-chars))
-
- (set (make-local-variable 'font-lock-defaults)
- '((ruby-font-lock-keywords) nil nil))
- (set (make-local-variable 'font-lock-keywords)
- ruby-font-lock-keywords)
- (set (make-local-variable 'font-lock-syntax-table)
- ruby-font-lock-syntax-table)
-
- (if (eval-when-compile (fboundp 'syntax-propertize-rules))
- (set (make-local-variable 'syntax-propertize-function)
- #'ruby-syntax-propertize-function)
- (set (make-local-variable 'font-lock-syntactic-keywords)
- ruby-font-lock-syntactic-keywords)))
+ (setq-local electric-indent-chars (append '(?\{ ?\}) electric-indent-chars))
+
+ (setq-local font-lock-defaults '((ruby-font-lock-keywords) nil nil))
+ (setq-local font-lock-keywords ruby-font-lock-keywords)
+ (setq-local font-lock-syntax-table ruby-font-lock-syntax-table)
+
+ (setq-local syntax-propertize-function #'ruby-syntax-propertize-function))
;;; Invoke ruby-mode when appropriate