;;; js.el --- Major mode for editing JavaScript -*- lexical-binding: t -*-
-;; Copyright (C) 2008-2013 Free Software Foundation, Inc.
+;; Copyright (C) 2008-2015 Free Software Foundation, Inc.
;; Author: Karl Landstrom <karl.landstrom@brgeight.se>
;; Daniel Colascione <dan.colascione@gmail.com>
(eval-when-compile
(require 'cl-lib)
- (require 'comint)
(require 'ido))
(defvar inferior-moz-buffer)
(defconst js--function-heading-1-re
(concat
- "^\\s-*function\\s-+\\(" js--name-re "\\)")
+ "^\\s-*function\\(?:\\s-\\|\\*\\)+\\(" js--name-re "\\)")
"Regexp matching the start of a JavaScript function header.
Match group 1 is the name of the function.")
:group 'js
:version "24.1")
-(defcustom js-auto-indent-flag t
- "Whether to automatically indent when typing punctuation characters.
-If non-nil, the characters {}();,: also indent the current line
-in Javascript mode."
- :type 'boolean
- :group 'js)
+(defcustom js-switch-indent-offset 0
+ "Number of additional spaces for indenting the contents of a switch block.
+The value must not be negative."
+ :type 'integer
+ :safe 'integerp
+ :group 'js
+ :version "24.4")
(defcustom js-flat-functions nil
"Treat nested functions as top-level functions in `js-mode'.
:type 'integer
:group 'js)
+(defcustom js-indent-first-init nil
+ "Non-nil means specially indent the first variable declaration's initializer.
+Normally, the first declaration's initializer is unindented, and
+subsequent declarations have their identifiers aligned with it:
+
+ var o = {
+ foo: 3
+ };
+
+ var o = {
+ foo: 3
+ },
+ bar = 2;
+
+If this option has the value t, indent the first declaration's
+initializer by an additional level:
+
+ var o = {
+ foo: 3
+ };
+
+ var o = {
+ foo: 3
+ },
+ bar = 2;
+
+If this option has the value `dynamic', if there is only one declaration,
+don't indent the first one's initializer; otherwise, indent it.
+
+ var o = {
+ foo: 3
+ };
+
+ var o = {
+ foo: 3
+ },
+ bar = 2;"
+ :version "25.1"
+ :type '(choice (const nil) (const t) (const dynamic))
+ :safe 'symbolp
+ :group 'js)
+
;;; KeyMap
(defvar js-mode-map
(let ((table (make-syntax-table)))
(c-populate-syntax-table table)
(modify-syntax-entry ?$ "_" table)
+ (modify-syntax-entry ?` "\"" table)
table)
"Syntax table for `js-mode'.")
(let ((name t))
(forward-word)
(forward-comment most-positive-fixnum)
+ (when (eq (char-after) ?*)
+ (forward-char)
+ (forward-comment most-positive-fixnum))
(when (looking-at js--name-re)
(setq name (match-string-no-properties 0))
(goto-char (match-end 0)))
(up-list -1)))
(defun js--inside-param-list-p ()
- "Return non-nil iff point is in a function parameter list."
+ "Return non-nil if point is in a function parameter list."
(ignore-errors
(save-excursion
(js--up-nearby-list)
(looking-at "function"))))))))
(defun js--inside-dojo-class-list-p ()
- "Return non-nil iff point is in a Dojo multiple-inheritance class block."
+ "Return non-nil if point is in a Dojo multiple-inheritance class block."
(ignore-errors
(save-excursion
(js--up-nearby-list)
(defun js--forward-destructuring-spec (&optional func)
"Move forward over a JavaScript destructuring spec.
If FUNC is supplied, call it with no arguments before every
-variable name in the spec. Return true iff this was actually a
+variable name in the spec. Return true if this was actually a
spec. FUNC must preserve the match data."
(pcase (char-after)
(?\[
js--font-lock-keywords-3)
"Font lock keywords for `js-mode'. See `font-lock-keywords'.")
+(defconst js--syntax-propertize-regexp-syntax-table
+ (let ((st (make-char-table 'syntax-table (string-to-syntax "."))))
+ (modify-syntax-entry ?\[ "(]" st)
+ (modify-syntax-entry ?\] ")[" st)
+ (modify-syntax-entry ?\\ "\\" st)
+ st))
+
(defun js-syntax-propertize-regexp (end)
- (when (eq (nth 3 (syntax-ppss)) ?/)
- ;; A /.../ regexp.
- (when (re-search-forward "\\(?:\\=\\|[^\\]\\)\\(?:\\\\\\\\\\)*/" end 'move)
- (put-text-property (1- (point)) (point)
- 'syntax-table (string-to-syntax "\"/")))))
+ (let ((ppss (syntax-ppss)))
+ (when (eq (nth 3 ppss) ?/)
+ ;; A /.../ regexp.
+ (while
+ (when (re-search-forward "\\(?:\\=\\|[^\\]\\)\\(?:\\\\\\\\\\)*/"
+ end 'move)
+ (if (nth 1 (with-syntax-table
+ js--syntax-propertize-regexp-syntax-table
+ (let ((parse-sexp-lookup-properties nil))
+ (parse-partial-sexp (nth 8 ppss) (point)))))
+ ;; A / within a character class is not the end of a regexp.
+ t
+ (put-text-property (1- (point)) (point)
+ 'syntax-table (string-to-syntax "\"/"))
+ nil))))))
(defun js-syntax-propertize (start end)
;; Javascript allows immediate regular expression objects, written /.../.
;; We can probably just add +, -, !, <, >, %, ^, ~, |, &, ?, : at which
;; point I think only * and / would be missing which could also be added,
;; but need care to avoid affecting the // and */ comment markers.
- ("\\(?:^\\|[=([{,:;]\\)\\(?:[ \t]\\)*\\(/\\)[^/*]"
+ ("\\(?:^\\|[=([{,:;]\\|\\_<return\\_>\\)\\(?:[ \t]\\)*\\(/\\)[^/*]"
(1 (ignore
(forward-char -1)
(when (or (not (memq (char-after (match-beginning 0)) '(?\s ?\t)))
"Regular expression matching variable declaration keywords.")
(defconst js--indent-operator-re
- (concat "[-+*/%<>=&^|?:.]\\([^-+*/]\\|$\\)\\|"
+ (concat "[-+*/%<>&^|?:.]\\([^-+*/]\\|$\\)\\|!?=\\|"
(js--regexp-opt-symbol '("in" "instanceof")))
"Regexp matching operators that affect indentation of continued expressions.")
(save-excursion (backward-char) (not (looking-at "[/*]/")))
(js--looking-at-operator-p)
(and (progn (backward-char)
- (not (looking-at "++\\|--\\|/[/*]"))))))))))
+ (not (looking-at "+\\+\\|--\\|/[/*]"))))))))))
(defun js--end-of-do-while-loop-p ()
(when (save-excursion
(and (not (eq (point-at-bol) (point-min)))
(not (looking-at "[{]"))
+ (js--re-search-backward "[[:graph:]]" nil t)
(progn
- (js--re-search-backward "[[:graph:]]" nil t)
(or (eobp) (forward-char))
(when (= (char-before) ?\)) (backward-list))
(skip-syntax-backward " ")
(list (cons 'c js-comment-lineup-func))))
(c-get-syntactic-indentation (list (cons symbol anchor)))))
+(defun js--same-line (pos)
+ (and (>= pos (point-at-bol))
+ (<= pos (point-at-eol))))
+
(defun js--multi-line-declaration-indentation ()
"Helper function for `js--proper-indentation'.
Return the proper indentation of the current line if it belongs to a declaration
(or (eq (char-before) ?,)
(and (not (eq (char-before) ?\;))
(prog2
- (skip-chars-backward "[[:punct:]]")
+ (skip-syntax-backward ".")
(looking-at js--indent-operator-re)
(js--backward-syntactic-ws))
(not (eq (char-before) ?\;)))
- (and (>= pos (point-at-bol))
- (<= pos (point-at-eol)))))))
+ (js--same-line pos)))))
(condition-case nil
(backward-sexp)
(scan-error (setq at-opening-bracket t))))
(goto-char (match-end 0))
(1+ (current-column)))))))
+(defun js--indent-in-array-comp (bracket)
+ "Return non-nil if we think we're in an array comprehension.
+In particular, return the buffer position of the first `for' kwd."
+ (let ((end (point)))
+ (save-excursion
+ (goto-char bracket)
+ (when (looking-at "\\[")
+ (forward-char 1)
+ (js--forward-syntactic-ws)
+ (if (looking-at "[[{]")
+ (let (forward-sexp-function) ; Use Lisp version.
+ (forward-sexp) ; Skip destructuring form.
+ (js--forward-syntactic-ws)
+ (if (and (/= (char-after) ?,) ; Regular array.
+ (looking-at "for"))
+ (match-beginning 0)))
+ ;; To skip arbitrary expressions we need the parser,
+ ;; so we'll just guess at it.
+ (if (and (> end (point)) ; Not empty literal.
+ (re-search-forward "[^,]]* \\(for\\) " end t)
+ ;; Not inside comment or string literal.
+ (not (nth 8 (parse-partial-sexp bracket (point)))))
+ (match-beginning 1)))))))
+
+(defun js--array-comp-indentation (bracket for-kwd)
+ (if (js--same-line for-kwd)
+ ;; First continuation line.
+ (save-excursion
+ (goto-char bracket)
+ (forward-char 1)
+ (skip-chars-forward " \t")
+ (current-column))
+ (save-excursion
+ (goto-char for-kwd)
+ (current-column))))
+
+(defun js--maybe-goto-declaration-keyword-end (parse-status)
+ "Helper function for `js--proper-indentation'.
+Depending on the value of `js-indent-first-init', move
+point to the end of a variable declaration keyword so that
+indentation is aligned to that column."
+ (cond
+ ((eq js-indent-first-init t)
+ (when (looking-at js--declaration-keyword-re)
+ (goto-char (1+ (match-end 0)))))
+ ((eq js-indent-first-init 'dynamic)
+ (let ((bracket (nth 1 parse-status))
+ declaration-keyword-end
+ at-closing-bracket-p
+ comma-p)
+ (when (looking-at js--declaration-keyword-re)
+ (setq declaration-keyword-end (match-end 0))
+ (save-excursion
+ (goto-char bracket)
+ (setq at-closing-bracket-p
+ (condition-case nil
+ (progn
+ (forward-sexp)
+ t)
+ (error nil)))
+ (when at-closing-bracket-p
+ (while (forward-comment 1))
+ (setq comma-p (looking-at-p ","))))
+ (when comma-p
+ (goto-char (1+ declaration-keyword-end))))))))
+
(defun js--proper-indentation (parse-status)
"Return the proper indentation for the current line."
(save-excursion
(back-to-indentation)
- (cond ((nth 4 parse-status)
+ (cond ((nth 4 parse-status) ; inside comment
(js--get-c-offset 'c (nth 8 parse-status)))
- ((nth 8 parse-status) 0) ; inside string
- ((js--ctrl-statement-indentation))
- ((js--multi-line-declaration-indentation))
+ ((nth 3 parse-status) 0) ; inside string
((eq (char-after) ?#) 0)
((save-excursion (js--beginning-of-macro)) 4)
+ ;; Indent array comprehension continuation lines specially.
+ ((let ((bracket (nth 1 parse-status))
+ beg)
+ (and bracket
+ (not (js--same-line bracket))
+ (setq beg (js--indent-in-array-comp bracket))
+ ;; At or after the first loop?
+ (>= (point) beg)
+ (js--array-comp-indentation bracket beg))))
+ ((js--ctrl-statement-indentation))
+ ((js--multi-line-declaration-indentation))
((nth 1 parse-status)
;; A single closing paren/bracket should be indented at the
;; same level as the opening statement. Same goes for
;; "case" and "default".
- (let ((same-indent-p (looking-at
- "[]})]\\|\\_<case\\_>\\|\\_<default\\_>"))
+ (let ((same-indent-p (looking-at "[]})]"))
+ (switch-keyword-p (looking-at "default\\_>\\|case\\_>[^:]"))
(continued-expr-p (js--continued-expression-p)))
(goto-char (nth 1 parse-status)) ; go to the opening char
(if (looking-at "[({[]\\s-*\\(/[/*]\\|$\\)")
(skip-syntax-backward " ")
(when (eq (char-before) ?\)) (backward-list))
(back-to-indentation)
- (cond (same-indent-p
- (current-column))
- (continued-expr-p
- (+ (current-column) (* 2 js-indent-level)
- js-expr-indent-offset))
- (t
- (+ (current-column) js-indent-level
- (pcase (char-after (nth 1 parse-status))
- (?\( js-paren-indent-offset)
- (?\[ js-square-indent-offset)
- (?\{ js-curly-indent-offset))))))
+ (js--maybe-goto-declaration-keyword-end parse-status)
+ (let* ((in-switch-p (unless same-indent-p
+ (looking-at "\\_<switch\\_>")))
+ (same-indent-p (or same-indent-p
+ (and switch-keyword-p
+ in-switch-p)))
+ (indent
+ (cond (same-indent-p
+ (current-column))
+ (continued-expr-p
+ (+ (current-column) (* 2 js-indent-level)
+ js-expr-indent-offset))
+ (t
+ (+ (current-column) js-indent-level
+ (pcase (char-after (nth 1 parse-status))
+ (?\( js-paren-indent-offset)
+ (?\[ js-square-indent-offset)
+ (?\{ js-curly-indent-offset)))))))
+ (if in-switch-p
+ (+ indent js-switch-indent-offset)
+ indent)))
;; If there is something following the opening
;; paren/bracket, everything else should be indented at
;; the same level.
(defun js-indent-line ()
"Indent the current line as JavaScript."
(interactive)
- (save-restriction
- (widen)
- (let* ((parse-status
- (save-excursion (syntax-ppss (point-at-bol))))
- (offset (- (current-column) (current-indentation))))
- (indent-line-to (js--proper-indentation parse-status))
- (when (> offset 0) (forward-char offset)))))
+ (let* ((parse-status
+ (save-excursion (syntax-ppss (point-at-bol))))
+ (offset (- (point) (save-excursion (back-to-indentation) (point)))))
+ (indent-line-to (js--proper-indentation parse-status))
+ (when (> offset 0) (forward-char offset))))
;;; Filling
(defvar find-tag-marker-ring) ; etags
+;; etags loads ring.
+(declare-function ring-insert "ring" (ring item))
+
(defun js-find-symbol (&optional arg)
"Read a JavaScript symbol and jump to it.
With a prefix argument, restrict symbols to those from the
;;; MozRepl integration
-(put 'js-moz-bad-rpc 'error-conditions '(error timeout))
-(put 'js-moz-bad-rpc 'error-message "Mozilla RPC Error")
-
-(put 'js-js-error 'error-conditions '(error js-error))
-(put 'js-js-error 'error-message "Javascript Error")
+(define-error 'js-moz-bad-rpc "Mozilla RPC Error") ;; '(timeout error))
+(define-error 'js-js-error "Javascript Error") ;; '(js-error error))
(defun js--wait-for-matching-output
(process regexp timeout &optional start)
;; order to catch a prompt that's only partially arrived
(save-excursion (forward-line 0) (point))))
+;; Presumably "inferior-moz-process" loads comint.
+(declare-function comint-send-string "comint" (process string))
+(declare-function comint-send-input "comint"
+ (&optional no-newline artificial))
+
(defun js--js-enter-repl ()
(inferior-moz-process) ; called for side-effect
(with-current-buffer inferior-moz-buffer
(defsubst js--js-true (value)
(not (js--js-not value)))
+;; The somewhat complex code layout confuses the byte-compiler into
+;; thinking this function "might not be defined at runtime".
+(declare-function js--optimize-arglist "js" (arglist))
+
(eval-and-compile
(defun js--optimize-arglist (arglist)
"Convert immediate js< and js! references to deferred ones."
(`error (signal 'js-js-error (list (cl-second result))))
(x (error "Unmatched case in js--js-decode-retval: %S" x))))
+(defvar comint-last-input-end)
+
(defun js--js-funcall (function &rest arguments)
"Call the Mozilla function FUNCTION with arguments ARGUMENTS.
If function is a string, look it up as a property on the global
(defvar js-read-tab-history nil)
+(declare-function ido-chop "ido" (items elem))
+
(defun js--read-tab (prompt)
"Read a Mozilla tab with prompt PROMPT.
Return a cons of (TYPE . OBJECT). TYPE is either 'window or
'(when (fboundp 'folding-add-to-marks-list)
(folding-add-to-marks-list 'js-mode "// {{{" "// }}}" )))
+;;;###autoload
+(dolist (name (list "node" "nodejs" "gjs" "rhino"))
+ (add-to-list 'interpreter-mode-alist (cons (purecopy name) 'js-mode)))
+
(provide 'js)
;; js.el ends here