# HG changeset patch # User Chong Yidong # Date 1250290958 0 # Node ID ea67ac46d172eafff28d9bbef2481ef5ffd6e7d5 # Parent 73a2da4c1a392f612e9007a883df00ab740e3413 * progmodes/js2-mode.el: File removed. * Makefile.in (ELCFILES): Add js.el, and remove js2-mode.el. * speedbar.el (speedbar-supported-extension-expressions): Add .js. * progmodes/hideshow.el (hs-special-modes-alist): Add js-mode entry. * progmodes/js.el: New file. diff -r 73a2da4c1a39 -r ea67ac46d172 lisp/ChangeLog --- a/lisp/ChangeLog Fri Aug 14 18:18:39 2009 +0000 +++ b/lisp/ChangeLog Fri Aug 14 23:02:38 2009 +0000 @@ -1,3 +1,18 @@ +2009-08-14 Chong Yidong + + * progmodes/js2-mode.el: File removed. + + * Makefile.in (ELCFILES): Add js.el, and remove js2-mode.el. + + * speedbar.el (speedbar-supported-extension-expressions): Add .js. + + * progmodes/hideshow.el (hs-special-modes-alist): Add js-mode + entry. + +2009-08-14 Daniel Colascione + + * progmodes/js.el: New file. + 2009-08-14 Mark A. Hershberger * timezone.el (timezone-parse-date): Add ability to understand ISO diff -r 73a2da4c1a39 -r ea67ac46d172 lisp/Makefile.in --- a/lisp/Makefile.in Fri Aug 14 18:18:39 2009 +0000 +++ b/lisp/Makefile.in Fri Aug 14 23:02:38 2009 +0000 @@ -1064,7 +1064,7 @@ $(lisp)/progmodes/idlw-toolbar.elc \ $(lisp)/progmodes/idlwave.elc \ $(lisp)/progmodes/inf-lisp.elc \ - $(lisp)/progmodes/js2-mode.elc \ + $(lisp)/progmodes/js.elc \ $(lisp)/progmodes/ld-script.elc \ $(lisp)/progmodes/m4-mode.elc \ $(lisp)/progmodes/make-mode.elc \ diff -r 73a2da4c1a39 -r ea67ac46d172 lisp/progmodes/hideshow.el --- a/lisp/progmodes/hideshow.el Fri Aug 14 18:18:39 2009 +0000 +++ b/lisp/progmodes/hideshow.el Fri Aug 14 23:02:38 2009 +0000 @@ -270,7 +270,8 @@ '((c-mode "{" "}" "/[*/]" nil nil) (c++-mode "{" "}" "/[*/]" nil nil) (bibtex-mode ("@\\S(*\\(\\s(\\)" 1)) - (java-mode "{" "}" "/[*/]" nil nil)) + (java-mode "{" "}" "/[*/]" nil nil) + (js-mode "{" "}" "/[*/]" nil)) "*Alist for initializing the hideshow variables for different modes. Each element has the form (MODE START END COMMENT-START FORWARD-SEXP-FUNC ADJUST-BEG-FUNC). diff -r 73a2da4c1a39 -r ea67ac46d172 lisp/progmodes/js.el --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lisp/progmodes/js.el Fri Aug 14 23:02:38 2009 +0000 @@ -0,0 +1,3391 @@ +;;; js.el --- Major mode for editing JavaScript source text + +;; Copyright (C) 2008, 2009 Free Software Foundation, Inc. + +;; Author: Karl Landstrom +;; Daniel Colascione +;; Maintainer: Daniel Colascione +;; Version: 9 +;; Date: 2009-07-25 +;; Keywords: languages, oop, javascript + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary + +;; This is based on Karl Landstrom's barebones javascript-mode. This +;; is much more robust and works with cc-mode's comment filling +;; (mostly). +;; +;; The main features of this JavaScript mode are syntactic +;; highlighting (enabled with `font-lock-mode' or +;; `global-font-lock-mode'), automatic indentation and filling of +;; comments, C preprocessor fontification, and MozRepl integration. +;; +;; General Remarks: +;; +;; XXX: This mode assumes that block comments are not nested inside block +;; XXX: comments +;; +;; Exported names start with "js-"; private names start with +;; "js--". + +;;; Code: + +(eval-and-compile + (require 'cc-mode) + (require 'font-lock) + (require 'newcomment) + (require 'imenu) + (require 'etags) + (require 'thingatpt) + (require 'easymenu) + (require 'moz nil t) + (require 'json nil t)) + +(eval-when-compile + (require 'cl) + (require 'comint) + (require 'ido) + ;; Swap the speed and safety values for debugging + (proclaim '(optimize (speed 3) (safety 0)))) + +(defvar inferior-moz-buffer) +(defvar moz-repl-name) +(defvar ido-cur-list) +(declare-function ido-mode "ido") +(declare-function inferior-moz-process "mozrepl") + +;;; Constants + +(defconst js--name-start-re "[a-zA-Z_$]" + "Matches the first character of a Javascript identifier. No grouping") + +(defconst js--stmt-delim-chars "^;{}?:") + +(defconst js--name-re (concat js--name-start-re + "\\(?:\\s_\\|\\sw\\)*") + "Matches a Javascript identifier. No grouping.") + +(defconst js--objfield-re (concat js--name-re ":") + "Matches a Javascript object field start") + +(defconst js--dotted-name-re + (concat js--name-re "\\(?:\\." js--name-re "\\)*") + "Matches a dot-separated sequence of Javascript names") + +(defconst js--cpp-name-re js--name-re + "Matches a C preprocessor name") + +(defconst js--opt-cpp-start "^\\s-*#\\s-*\\([[:alnum:]]+\\)" + "Regexp matching the prefix of a cpp directive including the directive +name, or nil in languages without preprocessor support. The first +submatch surrounds the directive name.") + +(defconst js--plain-method-re + (concat "^\\s-*?\\(" js--dotted-name-re "\\)\\.prototype" + "\\.\\(" js--name-re "\\)\\s-*?=\\s-*?\\(function\\)\\_>") + "Regexp matching an old-fashioned explicit prototype \"method\" + declaration. Group 1 is a (possibly-dotted) class name, group 2 + is a method name, and group 3 is the 'function' keyword." ) + +(defconst js--plain-class-re + (concat "^\\s-*\\(" js--dotted-name-re "\\)\\.prototype" + "\\s-*=\\s-*{") + "Regexp matching an old-fashioned explicit prototype \"class\" + declaration, as in Class.prototype = { method1: ...} ") + +(defconst js--mp-class-decl-re + (concat "^\\s-*var\\s-+" + "\\(" js--name-re "\\)" + "\\s-*=\\s-*" + "\\(" js--dotted-name-re + "\\)\\.extend\\(?:Final\\)?\\s-*(\\s-*{?\\s-*$") + "var NewClass = BaseClass.extend(") + +(defconst js--prototype-obsolete-class-decl-re + (concat "^\\s-*\\(?:var\\s-+\\)?" + "\\(" js--dotted-name-re "\\)" + "\\s-*=\\s-*Class\\.create()") + "var NewClass = Class.create()") + +(defconst js--prototype-objextend-class-decl-re-1 + (concat "^\\s-*Object\\.extend\\s-*(" + "\\(" js--dotted-name-re "\\)" + "\\s-*,\\s-*{")) + +(defconst js--prototype-objextend-class-decl-re-2 + (concat "^\\s-*\\(?:var\\s-+\\)?" + "\\(" js--dotted-name-re "\\)" + "\\s-*=\\s-*Object\\.extend\\s-*\(")) + +(defconst js--prototype-class-decl-re + (concat "^\\s-*\\(?:var\\s-+\\)?" + "\\(" js--name-re "\\)" + "\\s-*=\\s-*Class\\.create\\s-*(\\s-*" + "\\(?:\\(" js--dotted-name-re "\\)\\s-*,\\s-*\\)?{?") + "var NewClass = Class.create({") + +;; Parent class name(s) (yes, multiple inheritance in Javascript) are +;; matched with dedicated font-lock matchers +(defconst js--dojo-class-decl-re + (concat "^\\s-*dojo\\.declare\\s-*(\"\\(" js--dotted-name-re "\\)")) + +(defconst js--extjs-class-decl-re-1 + (concat "^\\s-*Ext\\.extend\\s-*(" + "\\s-*\\(" js--dotted-name-re "\\)" + "\\s-*,\\s-*\\(" js--dotted-name-re "\\)") + "ExtJS class declaration (style 1)") + +(defconst js--extjs-class-decl-re-2 + (concat "^\\s-*\\(?:var\\s-+\\)?" + "\\(" js--name-re "\\)" + "\\s-*=\\s-*Ext\\.extend\\s-*(\\s-*" + "\\(" js--dotted-name-re "\\)") + "ExtJS class declaration (style 2)") + +(defconst js--mochikit-class-re + (concat "^\\s-*MochiKit\\.Base\\.update\\s-*(\\s-*" + "\\(" js--dotted-name-re "\\)") + "MochiKit class declaration?") + +(defconst js--dummy-class-style + '(:name "[Automatically Generated Class]")) + +(defconst js--class-styles + `((:name "Plain" + :class-decl ,js--plain-class-re + :prototype t + :contexts (toplevel) + :framework javascript) + + (:name "MochiKit" + :class-decl ,js--mochikit-class-re + :prototype t + :contexts (toplevel) + :framework mochikit) + + (:name "Prototype (Obsolete)" + :class-decl ,js--prototype-obsolete-class-decl-re + :contexts (toplevel) + :framework prototype) + + (:name "Prototype (Modern)" + :class-decl ,js--prototype-class-decl-re + :contexts (toplevel) + :framework prototype) + + (:name "Prototype (Object.extend)" + :class-decl ,js--prototype-objextend-class-decl-re-1 + :prototype t + :contexts (toplevel) + :framework prototype) + + (:name "Prototype (Object.extend) 2" + :class-decl ,js--prototype-objextend-class-decl-re-2 + :prototype t + :contexts (toplevel) + :framework prototype) + + (:name "Dojo" + :class-decl ,js--dojo-class-decl-re + :contexts (toplevel) + :framework dojo) + + (:name "ExtJS (style 1)" + :class-decl ,js--extjs-class-decl-re-1 + :prototype t + :contexts (toplevel) + :framework extjs) + + (:name "ExtJS (style 2)" + :class-decl ,js--extjs-class-decl-re-2 + :contexts (toplevel) + :framework extjs) + + (:name "Merrill Press" + :class-decl ,js--mp-class-decl-re + :contexts (toplevel) + :framework merrillpress)) + + "A list of class definition styles. + +A class definition style is a plist with the following keys: + +:name is a human-readable name of the class type + +:class-decl is a regular expression giving the start of the +class. Its first group must match the name of its class. If there +is a parent class, the second group should match, and it should +be the name of the class. + +If :prototype is present and non-nil, the parser will merge +declarations for this constructs with others at the same lexical +level that have the same name. Otherwise, multiple definitions +will create multiple top-level entries. Don't use :prototype +unnecessarily: it has an associated cost in performance. + +If :strip-prototype is present and non-nil, then if the class +name as matched contains +") + +(defconst js--available-frameworks + (loop with available-frameworks + for style in js--class-styles + for framework = (plist-get style :framework) + unless (memq framework available-frameworks) + collect framework into available-frameworks + finally return available-frameworks) + + "List of available frameworks symbols") + +(defconst js--function-heading-1-re + (concat + "^\\s-*function\\s-+\\(" js--name-re "\\)") + "Regular expression matching the start of a function header. Match group 1 +is the name of the function.") + +(defconst js--function-heading-2-re + (concat + "^\\s-*\\(" js--name-re "\\)\\s-*:\\s-*function\\_>") + "Regular expression matching the start of a function entry in + an associative array. Match group 1 is the name of the function.") + +(defconst js--function-heading-3-re + (concat + "^\\s-*\\(?:var\\s-+\\)?\\(" js--dotted-name-re "\\)" + "\\s-*=\\s-*function\\_>") + "Matches a line in the form var MUMBLE = function. Match group + 1 is MUMBLE.") + +(defconst js--macro-decl-re + (concat "^\\s-*#\\s-*define\\s-+\\(" js--cpp-name-re "\\)\\s-*(") + "Regular expression matching a CPP macro definition up to the opening +parenthesis. Match group 1 is the name of the function.") + +(defun js--regexp-opt-symbol (list) + "Like regexp-opt, but surround the optimized regular expression +with `\\\\_<' and `\\\\_>'." + (concat "\\_<" (regexp-opt list t) "\\_>")) + +(defconst js--keyword-re + (js--regexp-opt-symbol + '("abstract" "break" "case" "catch" "class" "const" + "continue" "debugger" "default" "delete" "do" "else" + "enum" "export" "extends" "final" "finally" "for" + "function" "goto" "if" "implements" "import" "in" + "instanceof" "interface" "native" "new" "package" + "private" "protected" "public" "return" "static" + "super" "switch" "synchronized" "throw" + "throws" "transient" "try" "typeof" "var" "void" "let" + "yield" "volatile" "while" "with")) + "Regular expression matching any JavaScript keyword.") + +(defconst js--basic-type-re + (js--regexp-opt-symbol + '("boolean" "byte" "char" "double" "float" "int" "long" + "short" "void")) + "Regular expression matching any predefined type in JavaScript.") + +(defconst js--constant-re + (js--regexp-opt-symbol '("false" "null" "undefined" + "Infinity" "NaN" + "true" "arguments" "this")) + "Regular expression matching any future reserved words in JavaScript.") + + +(defconst js--font-lock-keywords-1 + (list + "\\_" + (list js--function-heading-1-re 1 font-lock-function-name-face) + (list js--function-heading-2-re 1 font-lock-function-name-face)) + "Level one font lock.") + +(defconst js--font-lock-keywords-2 + (append js--font-lock-keywords-1 + (list (list js--keyword-re 1 font-lock-keyword-face) + (list "\\_" + "\\s-+\\(each\\)\\_>" nil nil + (list 1 'font-lock-keyword-face)) + (cons js--basic-type-re font-lock-type-face) + (cons js--constant-re font-lock-constant-face))) + "Level two font lock.") + +;; js--pitem is the basic building block of the lexical +;; database. When one refers to a real part of the buffer, the region +;; of text to which it refers is split into a conceptual header and +;; body. Consider the (very short) block described by a hypothetical +;; js--pitem: +;; +;; function foo(a,b,c) { return 42; } +;; ^ ^ ^ +;; | | | +;; +- h-begin +- h-end +- b-end +;; +;; (Remember that these are buffer positions, and therefore point +;; between characters, not at them. An arrow drawn to a character +;; indicates the corresponding position is between that character and +;; the one immediately preceding it.) +;; +;; The header is the region of text [h-begin, h-end], and is +;; the text needed to unambiguously recognize the start of the +;; construct. If the entire header is not present, the construct is +;; not recognized at all. No other pitems may be nested inside the +;; header. +;; +;; The body is the region [h-end, b-end]. It may contain nested +;; js--pitem instances. The body of a pitem may be empty: in +;; that case, b-end is equal to header-end. +;; +;; The three points obey the following relationship: +;; +;; h-begin < h-end <= b-end +;; +;; We put a text property in the buffer on the character *before* +;; h-end, and if we see it, on the character *before* b-end. +;; +;; The text property for h-end, js--pstate, is actually a list +;; of all js--pitem instances open after the marked character. +;; +;; The text property for b-end, js--pend, is simply the +;; js--pitem that ends after the marked character. (Because +;; pitems always end when the paren-depth drops below a critical +;; value, and because we can only drop one level per character, only +;; one pitem may end at a given character.) +;; +;; In the structure below, we only store h-begin and (sometimes) +;; b-end. We can trivially and quickly find h-end by going to h-begin +;; and searching for an js--pstate text property. Since no other +;; js--pitem instances can be nested inside the header of a +;; pitem, the location after the character with this text property +;; must be h-end. +;; +;; js--pitem instances are never modified (with the exception +;; of the b-end field). Instead, modified copies are added at subseqnce parse points. +;; (The exception for b-end and its caveats is described below.) +;; + +(defstruct (js--pitem (:type list)) + ;; IMPORTANT: Do not alter the position of fields within the list. + ;; Various bits of code depend on their positions, particularly + ;; anything that manipulates the list of children. + + ;; List of children inside this pitem's body + (children nil :read-only t) + + ;; When we reach this paren depth after h-end, the pitem ends + (paren-depth nil :read-only t) + + ;; Symbol or class-style plist if this is a class + (type nil :read-only t) + + ;; See above + (h-begin nil :read-only t) + + ;; List of strings giving the parts of the name of this pitem (e.g., + ;; '("MyClass" "myMethod"), or t if this pitem is anonymous + (name nil :read-only t) + + ;; THIS FIELD IS MUTATED, and its value is shared by all copies of + ;; this pitem: when we copy-and-modify pitem instances, we share + ;; their tail structures, so all the copies actually have the same + ;; terminating cons cell. We modify that shared cons cell directly. + ;; + ;; The field value is either a number (buffer location) or nil if + ;; unknown. + ;; + ;; If the field's value is greater than `js--cache-end', the + ;; value is stale and must be treated as if it were nil. Conversely, + ;; if this field is nil, it is guaranteed that this pitem is open up + ;; to at least `js--cache-end'. (This property is handy when + ;; computing whether we're inside a given pitem.) + ;; + (b-end nil)) + +(defconst js--initial-pitem + (make-js--pitem + :paren-depth most-negative-fixnum + :type 'toplevel) + + "The pitem we start parsing with") + +;;; User Customization + +(defgroup js nil + "Customization variables for `js-mode'." + :tag "JavaScript" + :group 'languages) + +(defcustom js-indent-level 4 + "Number of spaces for each indentation step." + :type 'integer + :group 'js) + +(defcustom js-expr-indent-offset 0 + "Number of additional spaces used for indentation of continued +expressions. The value must be no less than minus +`js-indent-level'." + :type 'integer + :group 'js) + +(defcustom js-auto-indent-flag t + "Automatic indentation with punctuation characters. If non-nil, the +current line is indented when certain punctuations are inserted." + :type 'boolean + :group 'js) + +(defcustom js-flat-functions nil + "Treat nested functions as if they were top-level functions for +function movement, marking, and so on." + :type 'boolean + :group 'js) + +(defcustom js-comment-lineup-func #'c-lineup-C-comments + "cc-mode-style lineup function for C comments" + :type 'function + :group 'js) + +(defcustom js-enabled-frameworks js--available-frameworks + "Select which frameworks js-mode will recognize. + +Turn off some frameworks you seldom use to improve performance. +The set of recognized frameworks can also be overriden on a +per-buffer basis." + :type (cons 'set (mapcar (lambda (x) + (list 'const x)) + js--available-frameworks)) + :group 'js) + +(defcustom js-js-switch-tabs + (and (memq system-type '(darwin)) t) + "Non-nil if Emacs should display tabs while selecting them. + Useful only if the windowing system has a good mechanism for + preventing Firefox from stealing the keyboard focus." + :type 'boolean + :group 'js) + +(defcustom js-js-tmpdir + "~/.emacs.d/js/js" + "Temporary directory used for communicating with Mozilla. It + must be readable and writable by both Mozilla and Emacs." + :type 'directory + :group 'js) + +(defcustom js-js-timeout 5 + "Wait this many seconds for a reply from Mozilla when executing +commands. Increase this value if you are getting timeout +messages." + :type 'integer + :group 'js) + +;;; KeyMap + +(defvar js-mode-map + (let ((keymap (make-sparse-keymap))) + (mapc (lambda (key) + (define-key keymap key #'js-insert-and-indent)) + '("+" "-" "*" "{" "}" "(" ")" ":" ";" ",")) + (define-key keymap [(control ?c) (meta ?:)] #'js-eval) + (define-key keymap [(control ?c) (control ?j)] #'js-set-js-context) + (define-key keymap [(control meta ?x)] #'js-eval-defun) + (define-key keymap [(meta ?.)] #'js-find-symbol) + + (easy-menu-define nil keymap "Javascript Menu" + '("Javascript" + ["Select new Mozilla context…" js-set-js-context + (fboundp #'inferior-moz-process)] + ["Evaluate expression in Mozilla context…" js-eval + (fboundp #'inferior-moz-process)] + ["Send current function to Mozilla…" js-eval-defun + (fboundp #'inferior-moz-process)] + ) + ) + + keymap) + "Keymap for js-mode") + +(defun js-insert-and-indent (key) + "Runs the command bound to KEY in the global keymap, and if +we're not in a string or comment, indents the current line." + (interactive (list (this-command-keys))) + (call-interactively (lookup-key (current-global-map) key)) + (let ((syntax (save-restriction (widen) (syntax-ppss)))) + (when (or (and (not (nth 8 syntax)) + js-auto-indent-flag) + (and (nth 4 syntax) + (eq (current-column) + (1+ (current-indentation))))) + + (indent-according-to-mode)))) + +;;; Syntax table and parsing + +(defvar js-mode-syntax-table + (let ((table (make-syntax-table))) + (c-populate-syntax-table table) + (modify-syntax-entry ?$ "_" table) + table) + "Syntax table used in JS mode.") + +(defvar js--quick-match-re nil + "Autogenerated regular expression to match buffer constructs") + +(defvar js--quick-match-re-func nil + "Autogenerated regular expression to match buffer constructs +and functions") + +(make-variable-buffer-local 'js--quick-match-re) +(make-variable-buffer-local 'js--quick-match-re-func) + +(defvar js--cache-end 1 + "Last place in the buffer the function cache is valid") +(make-variable-buffer-local 'js--cache-end) + +(defvar js--last-parse-pos nil + "Last place we parsed up to in js--ensure-cache") +(make-variable-buffer-local 'js--last-parse-pos) + +(defvar js--state-at-last-parse-pos nil + "pstate at js--last-parse-pos") +(make-variable-buffer-local 'js--state-at-last-parse-pos) + +(defun js--flatten-list (list) + (loop for item in list + nconc (cond ((consp item) + (js--flatten-list item)) + + (item (list item))))) + +(defun js--maybe-join (prefix separator suffix &rest list) + "If LIST contains any element that is not nil, return its +non-nil elements, separated by SEPARATOR, prefixed by PREFIX, and +ended with SUFFIX as with `concat'. Otherwise, if LIST is empty, +return nil. If any element in LIST is itself a list, flatten that +element." + (setq list (js--flatten-list list)) + (when list + (concat prefix (mapconcat #'identity list separator) suffix))) + +(defun js--update-quick-match-re () + "Update js--quick-match-re based on the current set of +enabled frameworks" + (setq js--quick-match-re + (js--maybe-join + "^[ \t]*\\(?:" "\\|" "\\)" + + ;; #define mumble + "#define[ \t]+[a-zA-Z_]" + + (when (memq 'extjs js-enabled-frameworks) + "Ext\\.extend") + + (when (memq 'prototype js-enabled-frameworks) + "Object\\.extend") + + ;; var mumble = THING ( + (js--maybe-join + "\\(?:var[ \t]+\\)?[a-zA-Z_$0-9.]+[ \t]*=[ \t]*\\(?:" + "\\|" + "\\)[ \t]*\(" + + (when (memq 'prototype js-enabled-frameworks) + "Class\\.create") + + (when (memq 'extjs js-enabled-frameworks) + "Ext\\.extend") + + (when (memq 'merrillpress js-enabled-frameworks) + "[a-zA-Z_$0-9]+\\.extend\\(?:Final\\)?")) + + (when (memq 'dojo js-enabled-frameworks) + "dojo\\.declare[ \t]*\(") + + (when (memq 'mochikit js-enabled-frameworks) + "MochiKit\\.Base\\.update[ \t]*\(") + + ;; mumble.prototypeTHING + (js--maybe-join + "[a-zA-Z_$0-9.]+\\.prototype\\(?:" "\\|" "\\)" + + (when (memq 'javascript js-enabled-frameworks) + '( ;; foo.prototype.bar = function( + "\\.[a-zA-Z_$0-9]+[ \t]*=[ \t]*function[ \t]*\(" + + ;; mumble.prototype = { + "[ \t]*=[ \t]*{"))))) + + (setq js--quick-match-re-func + (concat "function\\|" js--quick-match-re))) + +(defun js--forward-text-property (propname) + "Move over the next value of PROPNAME in the buffer. If found, +return that value and leave point after the character having that +value, otherwise return nil and leave point at EOB." + (let ((next-value (get-text-property (point) propname))) + (if next-value + (forward-char) + + (goto-char (next-single-property-change + (point) propname nil (point-max))) + (unless (eobp) + (setq next-value (get-text-property (point) propname)) + (forward-char))) + + next-value)) + +(defun js--backward-text-property (propname) + "Move over the previous value of PROPNAME in the buffer. If found, +return that value and leave point just before the character that +has that value, otherwise return nil and leave point at BOB." + (unless (bobp) + (let ((prev-value (get-text-property (1- (point)) propname))) + (if prev-value + (backward-char) + + (goto-char (previous-single-property-change + (point) propname nil (point-min))) + + (unless (bobp) + (backward-char) + (setq prev-value (get-text-property (point) propname)))) + + prev-value))) + +(defsubst js--forward-pstate () + (js--forward-text-property 'js--pstate)) + +(defsubst js--backward-pstate () + (js--backward-text-property 'js--pstate)) + +(defun js--pitem-goto-h-end (pitem) + (goto-char (js--pitem-h-begin pitem)) + (js--forward-pstate)) + +(defun js--re-search-forward-inner (regexp &optional bound count) + "Auxiliary function for `js--re-search-forward'." + (let ((parse) + str-terminator + (orig-macro-end (save-excursion + (when (js--beginning-of-macro) + (c-end-of-macro) + (point))))) + (while (> count 0) + (re-search-forward regexp bound) + (setq parse (syntax-ppss)) + (cond ((setq str-terminator (nth 3 parse)) + (when (eq str-terminator t) + (setq str-terminator ?/)) + (re-search-forward + (concat "\\([^\\]\\|^\\)" (string str-terminator)) + (save-excursion (end-of-line) (point)) t)) + ((nth 7 parse) + (forward-line)) + ((or (nth 4 parse) + (and (eq (char-before) ?\/) (eq (char-after) ?\*))) + (re-search-forward "\\*/")) + ((and (not (and orig-macro-end + (<= (point) orig-macro-end))) + (js--beginning-of-macro)) + (c-end-of-macro)) + (t + (setq count (1- count)))))) + (point)) + + +(defun js--re-search-forward (regexp &optional bound noerror count) + "Search forward but ignore strings, cpp macros, and comments. +Invokes `re-search-forward' but treats the buffer as if strings, +cpp macros, and comments have been removed. + +If invoked while inside a macro, treat the contents of the macro +as normal text. + +" + (let ((saved-point (point)) + (search-expr + (cond ((null count) + '(js--re-search-forward-inner regexp bound 1)) + ((< count 0) + '(js--re-search-backward-inner regexp bound (- count))) + ((> count 0) + '(js--re-search-forward-inner regexp bound count))))) + (condition-case err + (eval search-expr) + (search-failed + (goto-char saved-point) + (unless noerror + (error (error-message-string err))))))) + + +(defun js--re-search-backward-inner (regexp &optional bound count) + "Auxiliary function for `js--re-search-backward'." + (let ((parse) + str-terminator + (orig-macro-start + (save-excursion + (and (js--beginning-of-macro) + (point))))) + (while (> count 0) + (re-search-backward regexp bound) + (when (and (> (point) (point-min)) + (save-excursion (backward-char) (looking-at "/[/*]"))) + (forward-char)) + (setq parse (syntax-ppss)) + (cond ((setq str-terminator (nth 3 parse)) + (when (eq str-terminator t) + (setq str-terminator ?/)) + (re-search-backward + (concat "\\([^\\]\\|^\\)" (string str-terminator)) + (save-excursion (beginning-of-line) (point)) t)) + ((nth 7 parse) + (goto-char (nth 8 parse))) + ((or (nth 4 parse) + (and (eq (char-before) ?/) (eq (char-after) ?*))) + (re-search-backward "/\\*")) + ((and (not (and orig-macro-start + (>= (point) orig-macro-start))) + (js--beginning-of-macro))) + (t + (setq count (1- count)))))) + (point)) + + +(defun js--re-search-backward (regexp &optional bound noerror count) + "Search backward but ignore strings, preprocessor macros, and +comments. Invokes `re-search-backward' but treats the buffer as +if strings, preprocessor macros, and comments have been removed. + +If inside a macro when called, treat the macro as normal text. +" + (let ((saved-point (point)) + (search-expr + (cond ((null count) + '(js--re-search-backward-inner regexp bound 1)) + ((< count 0) + '(js--re-search-forward-inner regexp bound (- count))) + ((> count 0) + '(js--re-search-backward-inner regexp bound count))))) + (condition-case err + (eval search-expr) + (search-failed + (goto-char saved-point) + (unless noerror + (error (error-message-string err))))))) + +(defun js--forward-expression () + "Move forward over a whole expression. Doesn't move over +expressions continued across lines, but we don't actually care" + (loop + ;; non-continued case; simplistic, but good enough? + do (loop until (or (eolp) + (progn + (forward-comment most-positive-fixnum) + (memq (char-after) '(?\, ?\; ?\] ?\) ?\})))) + do (forward-sexp)) + + while (and (eq (char-after) ?\n) + (save-excursion + (forward-char) + (js--continued-expression-p))))) + +(defun js--forward-function-decl () + "Move forward over a function declaration with point at the +'function' keyword. Return non-nil if this is a +syntactically-correct non-expression function, nil otherwise. +Specifically, return the name of the function, or t if the name +could not be determined." + (assert (looking-at "\\_")) + (let ((name t)) + (forward-word) + (forward-comment most-positive-fixnum) + (when (looking-at js--name-re) + (setq name (match-string-no-properties 0)) + (goto-char (match-end 0))) + (forward-comment most-positive-fixnum) + (and (eq (char-after) ?\( ) + (ignore-errors (forward-list) t) + (progn (forward-comment most-positive-fixnum) + (and (eq (char-after) ?{) + name))))) + +(defun js--function-prologue-beginning (&optional pos) + "Return the start of the function prologue that contains POS, +or nil if we're not in a function prologue. A function prologue +is everything from start of the definition up to and including +the opening brace. POS defaults to point." + + (let (prologue-begin) + (save-excursion + (if pos + (goto-char pos) + (setq pos (point))) + + (when (save-excursion + (forward-line 0) + (or (looking-at js--function-heading-2-re) + (looking-at js--function-heading-3-re))) + + (setq prologue-begin (match-beginning 1)) + (when (<= prologue-begin pos) + (goto-char (match-end 0)))) + + (skip-syntax-backward "w_") + (and (or (looking-at "\\_") + (js--re-search-backward "\\_" nil t)) + + (save-match-data (goto-char (match-beginning 0)) + (js--forward-function-decl)) + + (<= pos (point)) + (or prologue-begin (match-beginning 0)))))) + +(defun js--beginning-of-defun-raw () + "Internal helper for js--beginning-of-defun. Go to +previous defun-beginning and return the parse state for it, or +nil if we went all the way back to bob and don't find anything." + (js--ensure-cache) + (let (pstate) + (while (and (setq pstate (js--backward-pstate)) + (not (eq 'function (js--pitem-type (car pstate)))))) + (and (not (bobp)) pstate))) + +(defun js--pstate-is-toplevel-defun (pstate) + "If PSTATE represents a non-empty top-level defun, return the +top-most pitem. Otherwise, return nil." + (loop for pitem in pstate + with func-depth = 0 + with func-pitem + if (eq 'function (js--pitem-type pitem)) + do (incf func-depth) + and do (setq func-pitem pitem) + finally return (if (eq func-depth 1) func-pitem))) + +(defun js--beginning-of-defun-nested () + "Internal helper for js--beginning-of-defun. Returns the +pitem of the function we went to the beginning of." + + (or + ;; Look for the smallest function that encloses point... + (loop for pitem in (js--parse-state-at-point) + if (and (eq 'function (js--pitem-type pitem)) + (js--inside-pitem-p pitem)) + do (goto-char (js--pitem-h-begin pitem)) + and return pitem) + + ;; ...and if that isn't found, look for the previous top-level + ;; defun + (loop for pstate = (js--backward-pstate) + while pstate + if (js--pstate-is-toplevel-defun pstate) + do (goto-char (js--pitem-h-begin it)) + and return it))) + +(defun js--beginning-of-defun-flat () + "Internal helper for js--beginning-of-defun" + + (let ((pstate (js--beginning-of-defun-raw))) + (when pstate + (goto-char (js--pitem-h-begin (car pstate)))))) + +(defun js--beginning-of-defun (&optional arg) + "Used as beginning-of-defun-function" + + (setq arg (or arg 1)) + (while (and (not (eobp)) (< arg 0)) + (incf arg) + (when (and (not js-flat-functions) + (or (eq (js-syntactic-context) 'function) + (js--function-prologue-beginning))) + (js--end-of-defun)) + + (if (js--re-search-forward + "\\_" nil t) + (goto-char (js--function-prologue-beginning)) + (goto-char (point-max)))) + + (while (> arg 0) + (decf arg) + ;; If we're just past the end of a function, the user probably wants + ;; to go to the beginning of *that* function + (when (eq (char-before) ?}) + (backward-char)) + + (let ((prologue-begin (js--function-prologue-beginning))) + (cond ((and prologue-begin (< prologue-begin (point))) + (goto-char prologue-begin)) + + (js-flat-functions + (js--beginning-of-defun-flat)) + (t + (js--beginning-of-defun-nested)))))) + +(defun js--flush-caches (&optional beg ignored) + "Flush syntax cache info after position BEG. BEG defaults to +point-min, flushing the entire cache." + (interactive) + (setq beg (or beg (save-restriction (widen) (point-min)))) + (setq js--cache-end (min js--cache-end beg))) + +(defmacro js--debug (&rest arguments) + ;; `(message ,@arguments) + ) + +(defun js--ensure-cache--pop-if-ended (open-items paren-depth) + (let ((top-item (car open-items))) + (when (<= paren-depth (js--pitem-paren-depth top-item)) + (assert (not (get-text-property (1- (point)) 'js-pend))) + (put-text-property (1- (point)) (point) 'js--pend top-item) + (setf (js--pitem-b-end top-item) (point)) + (setq open-items + ;; open-items must contain at least two items for this to + ;; work, but because we push a dummy item to start with, + ;; that assumption holds. + (cons (js--pitem-add-child (second open-items) top-item) + (cddr open-items))))) + + open-items) + +(defmacro js--ensure-cache--update-parse () + "Helper for use inside js--ensure-cache. Updates parsing +information up to point. Refers to parse, prev-parse-point, +goal-point, and open-items bound lexically in the body of +`js--ensure-cache'." + `(progn + (setq goal-point (point)) + (goto-char prev-parse-point) + (while (progn + (setq open-items (js--ensure-cache--pop-if-ended + open-items (car parse))) + ;; Make sure parse-partial-sexp doesn't stop because we *entered* + ;; the given depth -- i.e., make sure we're deeper than the target + ;; depth. + (assert (> (nth 0 parse) + (js--pitem-paren-depth (car open-items)))) + (setq parse (parse-partial-sexp + prev-parse-point goal-point + (js--pitem-paren-depth (car open-items)) + nil parse)) + +;; (let ((overlay (make-overlay prev-parse-point (point)))) +;; (overlay-put overlay 'face '(:background "red")) +;; (unwind-protect +;; (progn +;; (js--debug "parsed: %S" parse) +;; (sit-for 1)) +;; (delete-overlay overlay))) + + (setq prev-parse-point (point)) + (< (point) goal-point))) + + (setq open-items (js--ensure-cache--pop-if-ended + open-items (car parse))))) + +(defun js--show-cache-at-point () + (interactive) + (require 'pp) + (let ((prop (get-text-property (point) 'js--pstate))) + (with-output-to-temp-buffer "*Help*" + (pp prop)))) + +(defun js--split-name (string) + "Splits a name into its dot-separated parts. Also removes any +prototype parts from the split name (unless the name is just +\"prototype\" to start with, that is)." + (let ((name (save-match-data + (split-string string "\\." t)))) + (unless (and (= (length name) 1) + (equal (car name) "prototype")) + + (setq name (remove "prototype" name))))) + +(defvar js--guess-function-name-start nil + "Secondary out-variable for js--guess-function-name") + +(defun js--guess-function-name (position) + "Guess the name of the function at POSITION, which should be +the just after the end of the word 'function'. Return the name of +the function or nil if the name could not be guessed. Clobbers +match data. If in guessing the function name we find the preamble +begins earlier than expected, set `js--guess-function-name-start' +to that position, otherwise set that variable to nil." + + (setq js--guess-function-name-start nil) + (save-excursion + (goto-char position) + (forward-line 0) + (cond + ((looking-at js--function-heading-3-re) + (and (eq (match-end 0) position) + (setq js--guess-function-name-start (match-beginning 1)) + (match-string-no-properties 1))) + + ((looking-at js--function-heading-2-re) + (and (eq (match-end 0) position) + (setq js--guess-function-name-start (match-beginning 1)) + (match-string-no-properties 1)))))) + +(defun js--clear-stale-cache () + ;; Clear any endings that occur after point + (let (end-prop) + (save-excursion + (while (setq end-prop (js--forward-text-property + 'js--pend)) + (setf (js--pitem-b-end end-prop) nil)))) + + ;; Remove any cache properties after this point + (remove-text-properties (point) (point-max) + '(js--pstate t js--pend t))) + +(defun js--ensure-cache (&optional limit) + "Ensures brace cache is valid up to the character before LIMIT. +LIMIT defaults to point." + (setq limit (or limit (point))) + (when (< js--cache-end limit) + + (c-save-buffer-state + (open-items + orig-match-start + orig-match-end + orig-depth + parse + prev-parse-point + name + case-fold-search + filtered-class-styles + new-item + goal-point + end-prop) + + ;; Figure out which class styles we need to look for + (setq filtered-class-styles + (loop for style in js--class-styles + if (memq (plist-get style :framework) + js-enabled-frameworks) + collect style)) + + (save-excursion + (save-restriction + (widen) + + ;; Find last known good position + (goto-char js--cache-end) + (unless (bobp) + (setq open-items (get-text-property + (1- (point)) 'js--pstate)) + + (unless open-items + (goto-char (previous-single-property-change + (point) 'js--pstate nil (point-min))) + + (unless (bobp) + (setq open-items (get-text-property (1- (point)) + 'js--pstate)) + (assert open-items)))) + + (unless open-items + ;; Make a placeholder for the top-level definition + (setq open-items (list js--initial-pitem))) + + (setq parse (syntax-ppss)) + (setq prev-parse-point (point)) + + (js--clear-stale-cache) + + (narrow-to-region (point-min) limit) + + (loop while (re-search-forward js--quick-match-re-func nil t) + for orig-match-start = (goto-char (match-beginning 0)) + for orig-match-end = (match-end 0) + do (js--ensure-cache--update-parse) + for orig-depth = (nth 0 parse) + + ;; Each of these conditions should return non-nil if + ;; we should add a new item and leave point at the end + ;; of the new item's header (h-end in the + ;; js--pitem diagram). This point is the one + ;; after the last character we need to unambiguously + ;; detect this construct. If one of these evaluates to + ;; nil, the location of the point is ignored. + if (cond + ;; In comment or string + ((nth 8 parse) nil) + + ;; Regular function declaration + ((and (looking-at "\\_") + (setq name (js--forward-function-decl))) + + (when (eq name t) + (setq name (js--guess-function-name orig-match-end)) + (if name + (when js--guess-function-name-start + (setq orig-match-start + js--guess-function-name-start)) + + (setq name t))) + + (assert (eq (char-after) ?{)) + (forward-char) + (make-js--pitem + :paren-depth orig-depth + :h-begin orig-match-start + :type 'function + :name (if (eq name t) + name + (js--split-name name)))) + + ;; Macro + ((looking-at js--macro-decl-re) + + ;; Macros often contain unbalanced parentheses. + ;; Make sure that h-end is at the textual end of + ;; the macro no matter what the parenthesis say. + (c-end-of-macro) + (js--ensure-cache--update-parse) + + (make-js--pitem + :paren-depth (nth 0 parse) + :h-begin orig-match-start + :type 'macro + :name (list (match-string-no-properties 1)))) + + ;; "Prototype function" declaration + ((looking-at js--plain-method-re) + (goto-char (match-beginning 3)) + (when (save-match-data + (js--forward-function-decl)) + (forward-char) + (make-js--pitem + :paren-depth orig-depth + :h-begin orig-match-start + :type 'function + :name (nconc (js--split-name + (match-string-no-properties 1)) + (list (match-string-no-properties 2)))))) + + ;; Class definition + ((loop with syntactic-context = + (js--syntactic-context-from-pstate open-items) + for class-style in filtered-class-styles + if (and (memq syntactic-context + (plist-get class-style :contexts)) + (looking-at (plist-get class-style + :class-decl))) + do (goto-char (match-end 0)) + and return + (make-js--pitem + :paren-depth orig-depth + :h-begin orig-match-start + :type class-style + :name (js--split-name + (match-string-no-properties 1)))))) + + do (js--ensure-cache--update-parse) + and do (push it open-items) + and do (put-text-property + (1- (point)) (point) 'js--pstate open-items) + else do (goto-char orig-match-end)) + + (goto-char limit) + (js--ensure-cache--update-parse) + (setq js--cache-end limit) + (setq js--last-parse-pos limit) + (setq js--state-at-last-parse-pos open-items) + ))))) + +(defun js--end-of-defun-flat () + "Internal helper for js--end-of-defun" + (loop while (js--re-search-forward "}" nil t) + do (js--ensure-cache) + if (get-text-property (1- (point)) 'js--pend) + if (eq 'function (js--pitem-type it)) + return t + finally do (goto-char (point-max)))) + +(defun js--end-of-defun-nested () + "Internal helper for js--end-of-defun" + (message "test") + (let* (pitem + (this-end (save-excursion + (and (setq pitem (js--beginning-of-defun-nested)) + (js--pitem-goto-h-end pitem) + (progn (backward-char) + (forward-list) + (point))))) + found) + + (if (and this-end (< (point) this-end)) + ;; We're already inside a function; just go to its end. + (goto-char this-end) + + ;; Otherwise, go to the end of the next function... + (while (and (js--re-search-forward "\\_" nil t) + (not (setq found (progn + (goto-char (match-beginning 0)) + (js--forward-function-decl)))))) + + (if found (forward-list) + ;; ... or eob. + (goto-char (point-max)))))) + +(defun js--end-of-defun (&optional arg) + "Used as end-of-defun-function" + (setq arg (or arg 1)) + (while (and (not (bobp)) (< arg 0)) + (let (orig-pos (point)) + (incf arg) + (js--beginning-of-defun) + (js--beginning-of-defun) + (unless (bobp) + (js--end-of-defun)))) + + (while (> arg 0) + (decf arg) + ;; look for function backward. if we're inside it, go to that + ;; function's end. otherwise, search for the next function's end and + ;; go there + (if js-flat-functions + (js--end-of-defun-flat) + + ;; if we're doing nested functions, see whether we're in the + ;; prologue. If we are, go to the end of the function; otherwise, + ;; call js--end-of-defun-nested to do the real work + (let ((prologue-begin (js--function-prologue-beginning))) + (cond ((and prologue-begin (<= prologue-begin (point))) + (goto-char prologue-begin) + (re-search-forward "\\_" + (1 font-lock-constant-face)) + + ;; Highlights class being declared, in parts + (js--class-decl-matcher + ,(concat "\\(" js--name-re "\\)\\(?:\\.\\|.*$\\)") + (goto-char (match-beginning 1)) + nil + (1 font-lock-type-face)) + + ;; Highlights parent class, in parts, if available + (js--class-decl-matcher + ,(concat "\\(" js--name-re "\\)\\(?:\\.\\|.*$\\)") + (if (match-beginning 2) + (progn + (setq js--tmp-location (match-end 2)) + (goto-char js--tmp-location) + (insert "=") + (goto-char (match-beginning 2))) + (setq js--tmp-location nil) + (goto-char (point-at-eol))) + (when js--tmp-location + (save-excursion + (goto-char js--tmp-location) + (delete-char 1))) + (1 font-lock-type-face)) + + ;; Highlights parent class + (js--class-decl-matcher + (2 font-lock-type-face nil t)) + + ;; Dojo needs its own matcher to override the string highlighting + (,(js--make-framework-matcher + 'dojo + "^\\s-*dojo\\.declare\\s-*(\"" + "\\(" js--dotted-name-re "\\)" + "\\(?:\"\\s-*,\\s-*\\(" js--dotted-name-re "\\)\\)?") + (1 font-lock-type-face t) + (2 font-lock-type-face nil t)) + + ;; Match Dojo base classes. Of course Mojo has to be different + ;; from everything else under the sun... + (,(js--make-framework-matcher + 'dojo + "^\\s-*dojo\\.declare\\s-*(\"" + "\\(" js--dotted-name-re "\\)\"\\s-*,\\s-*\\[") + ,(concat "[[,]\\s-*\\(" js--dotted-name-re "\\)\\s-*" + "\\(?:\\].*$\\)?") + (backward-char) + (end-of-line) + (1 font-lock-type-face)) + + ;; continued Dojo base-class list + (,(js--make-framework-matcher + 'dojo + "^\\s-*" js--dotted-name-re "\\s-*[],]") + ,(concat "\\(" js--dotted-name-re "\\)" + "\\s-*\\(?:\\].*$\\)?") + (if (save-excursion (backward-char) + (js--inside-dojo-class-list-p)) + (forward-symbol -1) + (end-of-line)) + (end-of-line) + (1 font-lock-type-face)) + + ;; variable declarations + ,(list + (concat "\\_<\\(const\\|var\\|let\\)\\_>\\|" js--basic-type-re) + (list #'js--variable-decl-matcher nil nil nil)) + + ;; class instantiation + ,(list + (concat "\\_\\s-+\\(" js--dotted-name-re "\\)") + (list 1 'font-lock-type-face)) + + ;; instanceof + ,(list + (concat "\\_\\s-+\\(" js--dotted-name-re "\\)") + (list 1 'font-lock-type-face)) + + ;; formal parameters + ,(list + (concat + "\\_\\(\\s-+" js--name-re "\\)?\\s-*(\\s-*" + js--name-start-re) + (list (concat "\\(" js--name-re "\\)\\(\\s-*).*\\)?") + '(backward-char) + '(end-of-line) + '(1 font-lock-variable-name-face))) + + ;; continued formal parameter list + ,(list + (concat + "^\\s-*" js--name-re "\\s-*[,)]") + (list js--name-re + '(if (save-excursion (backward-char) + (js--inside-param-list-p)) + (forward-symbol -1) + (end-of-line)) + '(end-of-line) + '(0 font-lock-variable-name-face)))) + + "Level three font lock.") + +(defun js--inside-pitem-p (pitem) + "Return whether point is inside the given pitem's header or body" + (js--ensure-cache) + (assert (js--pitem-h-begin pitem)) + (assert (js--pitem-paren-depth pitem)) + + (and (> (point) (js--pitem-h-begin pitem)) + (or (null (js--pitem-b-end pitem)) + (> (js--pitem-b-end pitem) (point))))) + +(defun js--parse-state-at-point () + "Get a list of js--pitem instances that apply to point, +most specific first. In the worst case, the current toplevel +instance will be returned." + + (save-excursion + (save-restriction + (widen) + (js--ensure-cache) + (let* ((bound (if (eobp) (point) (1+ (point)))) + (pstate (or (save-excursion + (js--backward-pstate)) + (list js--initial-pitem)))) + + ;; Loop until we either hit a pitem at BOB or pitem ends after + ;; point (or at point if we're at eob) + (loop for pitem = (car pstate) + until (or (eq (js--pitem-type pitem) + 'toplevel) + (js--inside-pitem-p pitem)) + do (pop pstate)) + + pstate)))) + +(defun js--syntactic-context-from-pstate (pstate) + "Return the syntactic context corresponding to PSTATE" + (let ((type (js--pitem-type (car pstate)))) + (cond ((memq type '(function macro)) + type) + + ((consp type) + 'class) + + (t 'toplevel)))) + +(defun js-syntactic-context () + "Get the current syntactic context of point. When called +interatively, also display a message with that context." + (interactive) + (let* ((syntactic-context (js--syntactic-context-from-pstate + (js--parse-state-at-point)))) + + (when (interactive-p) + (message "Syntactic context: %s" syntactic-context)) + + syntactic-context)) + +(defun js--class-decl-matcher (limit) + "Fontifies according to js--class-styles" + (loop initially (js--ensure-cache limit) + while (re-search-forward js--quick-match-re limit t) + for orig-end = (match-end 0) + do (goto-char (match-beginning 0)) + if (loop for style in js--class-styles + for decl-re = (plist-get style :class-decl) + if (and (memq (plist-get style :framework) + js-enabled-frameworks) + (memq (js-syntactic-context) + (plist-get style :contexts)) + decl-re + (looking-at decl-re)) + do (goto-char (match-end 0)) + and return t) + return t + else do (goto-char orig-end))) + +(defconst js--font-lock-keywords + '(js--font-lock-keywords-3 js--font-lock-keywords-1 + js--font-lock-keywords-2 + js--font-lock-keywords-3) + "See `font-lock-keywords'.") + +;; XXX: Javascript can continue a regexp literal across lines so long +;; as the newline is escaped with \. Account for that in the regexp +;; below. +(defconst js--regexp-literal + "[=(,:]\\(?:\\s-\\|\n\\)*\\(/\\)\\(?:\\\\/\\|[^/*]\\)\\(?:\\\\/\\|[^/]\\)*\\(/\\)" + "Match a regular expression literal. Match groups 1 and 2 are +the characters forming the beginning and end of the literal") + +;; we want to match regular expressions only at the beginning of +;; expressions +(defconst js--font-lock-syntactic-keywords + `((,js--regexp-literal (1 "|") (2 "|"))) + "Highlighting of regular expressions. See also the variable + `font-lock-keywords'.") + +;;; Indentation + +(defconst js--possibly-braceless-keyword-re + (js--regexp-opt-symbol + '("catch" "do" "else" "finally" "for" "if" "try" "while" "with" + "each")) + "Regular expression matching keywords that are optionally + followed by an opening brace.") + +(defconst js--indent-operator-re + (concat "[-+*/%<>=&^|?:.]\\([^-+*/]\\|$\\)\\|" + (js--regexp-opt-symbol '("in" "instanceof"))) + "Regular expression matching operators that affect indentation + of continued expressions.") + + +(defun js--looking-at-operator-p () + "Return non-nil if text after point is an operator (that is not +a comma)." + (save-match-data + (and (looking-at js--indent-operator-re) + (or (not (looking-at ":")) + (save-excursion + (and (js--re-search-backward "[?:{]\\|\\_" nil t) + (looking-at "?"))))))) + + +(defun js--continued-expression-p () + "Returns non-nil if the current line continues an expression." + (save-excursion + (back-to-indentation) + (or (js--looking-at-operator-p) + (and (js--re-search-backward "\n" nil t) + (progn + (skip-chars-backward " \t") + (or (bobp) (backward-char)) + (and (> (point) (point-min)) + (save-excursion (backward-char) (not (looking-at "[/*]/"))) + (js--looking-at-operator-p) + (and (progn (backward-char) + (not (looking-at "++\\|--\\|/[/*]")))))))))) + + +(defun js--end-of-do-while-loop-p () + "Returns non-nil if word after point is `while' of a do-while +statement, else returns nil. A braceless do-while statement +spanning several lines requires that the start of the loop is +indented to the same column as the current line." + (interactive) + (save-excursion + (save-match-data + (when (looking-at "\\s-*\\_") + (if (save-excursion + (skip-chars-backward "[ \t\n]*}") + (looking-at "[ \t\n]*}")) + (save-excursion + (backward-list) (forward-symbol -1) (looking-at "\\_")) + (js--re-search-backward "\\_" (point-at-bol) t) + (or (looking-at "\\_") + (let ((saved-indent (current-indentation))) + (while (and (js--re-search-backward "^\\s-*\\_<" nil t) + (/= (current-indentation) saved-indent))) + (and (looking-at "\\s-*\\_") + (not (js--re-search-forward + "\\_" (point-at-eol) t)) + (= (current-indentation) saved-indent))))))))) + + +(defun js--ctrl-statement-indentation () + "Returns the proper indentation of the current line if it +starts the body of a control statement without braces, else +returns nil." + (save-excursion + (back-to-indentation) + (when (save-excursion + (and (not (eq (point-at-bol) (point-min))) + (not (looking-at "[{]")) + (progn + (js--re-search-backward "[[:graph:]]" nil t) + (or (eobp) (forward-char)) + (when (= (char-before) ?\)) (backward-list)) + (skip-syntax-backward " ") + (skip-syntax-backward "w_") + (looking-at js--possibly-braceless-keyword-re)) + (not (js--end-of-do-while-loop-p)))) + (save-excursion + (goto-char (match-beginning 0)) + (+ (current-indentation) js-indent-level))))) + +(defun js--get-c-offset (symbol anchor) + (let ((c-offsets-alist + (list (cons 'c js-comment-lineup-func)))) + (c-get-syntactic-indentation (list (cons symbol anchor))))) + +(defun js--proper-indentation (parse-status) + "Return the proper indentation for the current line." + (save-excursion + (back-to-indentation) + (cond ((nth 4 parse-status) + (js--get-c-offset 'c (nth 8 parse-status))) + ((nth 8 parse-status) 0) ; inside string + ((js--ctrl-statement-indentation)) + ((eq (char-after) ?#) 0) + ((save-excursion (js--beginning-of-macro)) 4) + ((nth 1 parse-status) + (let ((same-indent-p (looking-at + "[]})]\\|\\_\\|\\_")) + (continued-expr-p (js--continued-expression-p))) + (goto-char (nth 1 parse-status)) + (if (looking-at "[({[]\\s-*\\(/[/*]\\|$\\)") + (progn + (skip-syntax-backward " ") + (when (= (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)))) + (unless same-indent-p + (forward-char) + (skip-chars-forward " \t")) + (current-column)))) + + ((js--continued-expression-p) + (+ js-indent-level js-expr-indent-offset)) + (t 0)))) + +(defun js-indent-line () + "Indent the current line as JavaScript source text." + (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))))) + +;;; Filling + +(defun js-c-fill-paragraph (&optional justify) + "Fill the paragraph with c-fill-paragraph" + (interactive "*P") + + (flet ((c-forward-sws + (&optional limit) + (js--forward-syntactic-ws limit)) + + (c-backward-sws + (&optional limit) + (js--backward-syntactic-ws limit)) + + (c-beginning-of-macro + (&optional limit) + (js--beginning-of-macro limit))) + + (let ((fill-paragraph-function 'c-fill-paragraph)) + (c-fill-paragraph justify)))) + +;;; Type database and Imenu + +;; We maintain a cache of semantic information, i.e., the classes and +;; functions we've encountered so far. In order to avoid having to +;; re-parse the buffer on every change, we cache the parse state at +;; each interesting point in the buffer. Each parse state is a +;; modified copy of the previous one, or in the case of the first +;; parse state, the empty state. +;; +;; The parse state itself is just a stack of js--pitem +;; instances. It starts off containing one element that is never +;; closed, that is initially js--initial-pitem. +;; + + +(defun js--pitem-format (pitem) + (let ((name (js--pitem-name pitem)) + (type (js--pitem-type pitem))) + + (format "name:%S type:%S" + name + (if (atom type) + type + (plist-get type :name))))) + +(defun js--make-merged-item (item child name-parts) + "Internal helper for js--splice-into-items. Return a new +item that is the result of merging CHILD into ITEM. NAME-PARTS is +a list of parts of the name of CHILD that we haven't consumed +yet." + + (js--debug "js--make-merged-item: {%s} into {%s}" + (js--pitem-format child) + (js--pitem-format item)) + + ;; If the item we're merging into isn't a class, make it into one + (unless (consp (js--pitem-type item)) + (js--debug "js--make-merged-item: changing dest into class") + (setq item (make-js--pitem + :children (list item) + + ;; Use the child's class-style if it's available + :type (if (atom (js--pitem-type child)) + js--dummy-class-style + (js--pitem-type child)) + + :name (js--pitem-strname item)))) + + ;; Now we can merge either a function or a class into a class + (cons (cond + ((cdr name-parts) + (js--debug "js--make-merged-item: recursing") + ;; if we have more name-parts to go before we get to the + ;; bottom of the class hierarchy, call the merger + ;; recursively + (js--splice-into-items (car item) child + (cdr name-parts))) + + ((atom (js--pitem-type child)) + (js--debug "js--make-merged-item: straight merge") + ;; Not merging a class, but something else, so just prepend + ;; it + (cons child (car item))) + + (t + ;; Otherwise, merge the new child's items into those + ;; of the new class + (js--debug "js--make-merged-item: merging class contents") + (append (car child) (car item)))) + (cdr item))) + +(defun js--pitem-strname (pitem) + "Last part of the name of PITEM as a string or symbol" + (let ((name (js--pitem-name pitem))) + (if (consp name) + (car (last name)) + name))) + +(defun js--splice-into-items (items child name-parts) + "Non-destructively inserts CHILD into the item list ITEMS in +the proper place as given by NAME-PARTS. If a class doesn't +exist in the tree, create it. Return the new items list. +NAME-PARTS is a list of strings given the broken-down class +name of the item to insert." + + (let ((top-name (car name-parts)) + (item-ptr items) + new-items last-new-item new-cons item) + + (js--debug "js--splice-into-items: name-parts: %S items:%S" + name-parts + (mapcar #'js--pitem-name items)) + + (assert (stringp top-name)) + (assert (> (length top-name) 0)) + + ;; If top-name isn't found in items, then we build a copy of items + ;; and throw it away. But that's okay, since most of the time, we + ;; *will* find an instance. + + (while (and item-ptr + (cond ((equal (js--pitem-strname (car item-ptr)) top-name) + ;; Okay, we found an entry with the right name. Splice + ;; the merged item into the list... + (setq new-cons (cons (js--make-merged-item + (car item-ptr) child + name-parts) + (cdr item-ptr))) + + (if last-new-item + (setcdr last-new-item new-cons) + (setq new-items new-cons)) + + ;; ...and terminate the loop + nil) + + (t + ;; Otherwise, copy the current cons and move onto the + ;; text. This is tricky; we keep track of the tail of + ;; the list that begins with new-items in + ;; last-new-item. + (setq new-cons (cons (car item-ptr) nil)) + (if last-new-item + (setcdr last-new-item new-cons) + (setq new-items new-cons)) + (setq last-new-item new-cons) + + ;; Go to the next cell in items + (setq item-ptr (cdr item-ptr)))))) + + (if item-ptr + ;; Yay! We stopped because we found something, not because + ;; we ran out of items to search. Just return the new + ;; list. + (progn + (js--debug "search succeeded: %S" name-parts) + new-items) + + ;; We didn't find anything. If the child is a class and we don't + ;; have any classes to drill down into, just push that class; + ;; otherwise, make a fake class and carry on. + (js--debug "search failed: %S" name-parts) + (cons (if (cdr name-parts) + ;; We have name-parts left to process. Make a fake + ;; class for this particular part... + (make-js--pitem + ;; ...and recursively digest the rest of the name + :children (js--splice-into-items + nil child (cdr name-parts)) + :type js--dummy-class-style + :name top-name) + + ;; Otherwise, this is the only name we have, so stick + ;; the item on the front of the list + child) + items)))) + +(defun js--pitem-add-child (pitem child) + "Copy js--pitem PITEM and while copying, push CHILD onto +its list of children." + + (assert (integerp (js--pitem-h-begin child))) + (assert (if (consp (js--pitem-name child)) + (loop for part in (js--pitem-name child) + always (stringp part)) + t)) + + ;; This trick works because we know (based on our defstructs) that + ;; the child list is always the first element, and so the second + ;; element and beyond can be shared when we make our "copy". + (cons + + (let ((name (js--pitem-name child)) + (type (js--pitem-type child))) + + (cond ((cdr-safe name) ; true if a list of at least two elements + ;; Use slow path because we need class lookup + (js--splice-into-items (car pitem) child name)) + + ((and (consp type) + (plist-get type :prototype)) + + ;; Use slow path because we need class merging. We know + ;; name is a list here because down in + ;; `js--ensure-cache', we made sure to only add + ;; class entries with lists for :name + (assert (consp name)) + (js--splice-into-items (car pitem) child name)) + + (t + ;; Fast path + (cons child (car pitem))))) + + (cdr pitem))) + +(defun js--maybe-make-marker (location) + "Make LOCATION into a marker if imenu-use-markers" + (if imenu-use-markers + (set-marker (make-marker) location) + location)) + +(defun js--pitems-to-imenu (pitems unknown-ctr) + "Convert list of pitems PITEMS to imenu format" + + (let (imenu-items pitem pitem-type pitem-name subitems) + + (while (setq pitem (pop pitems)) + (setq pitem-type (js--pitem-type pitem)) + (setq pitem-name (js--pitem-strname pitem)) + (when (eq pitem-name t) + (setq pitem-name (format "[unknown %s]" + (incf (car unknown-ctr))))) + + (cond + ((memq pitem-type '(function macro)) + (assert (integerp (js--pitem-h-begin pitem))) + (push (cons pitem-name + (js--maybe-make-marker + (js--pitem-h-begin pitem))) + imenu-items)) + + ((consp pitem-type) ; class definition + (setq subitems (js--pitems-to-imenu + (js--pitem-children pitem) + unknown-ctr)) + (cond (subitems + (push (cons pitem-name subitems) + imenu-items)) + + ((js--pitem-h-begin pitem) + (assert (integerp (js--pitem-h-begin pitem))) + (setq subitems (list + (cons "[empty]" + (js--maybe-make-marker + (js--pitem-h-begin pitem))))) + (push (cons pitem-name subitems) + imenu-items)))) + + (t (error "Unknown item type: %S" pitem-type)))) + + imenu-items)) + +(defun js--imenu-create-index () + "Creates an imenu index for the current buffer" + (save-excursion + (save-restriction + (widen) + (goto-char (point-max)) + (js--ensure-cache) + (assert (or (= (point-min) (point-max)) + (eq js--last-parse-pos (point)))) + (when js--last-parse-pos + (let ((state js--state-at-last-parse-pos) + (unknown-ctr (cons -1 nil))) + + ;; Make sure everything is closed + (while (cdr state) + (setq state + (cons (js--pitem-add-child (second state) (car state)) + (cddr state)))) + + (assert (= (length state) 1)) + + ;; Convert the new-finalized state into what imenu expects + (js--pitems-to-imenu + (car (js--pitem-children state)) + unknown-ctr)))))) + +(defun js--which-func-joiner (parts) + (mapconcat #'identity parts ".")) + +(defun js--imenu-to-flat (items prefix symbols) + (loop for item in items + if (imenu--subalist-p item) + do (js--imenu-to-flat + (cdr item) (concat prefix (car item) ".") + symbols) + else + do (let* ((name (concat prefix (car item))) + (name2 name) + (ctr 0)) + + (while (gethash name2 symbols) + (setq name2 (format "%s<%d>" name (incf ctr)))) + + (puthash name2 (cdr item) symbols)))) + +(defun js--get-all-known-symbols () + "Get a hash table of all relevant Javascript symbols across all +js-mode buffers. Each key is the name of a symbol (possibly +disambiguated with , where N > 1), and each value is a marker +giving the location of that symbol." + (loop with symbols = (make-hash-table :test 'equal) + with imenu-use-markers = t + for buffer being the buffers + for imenu-index = (with-current-buffer buffer + (when (eq major-mode 'js-mode) + (js--imenu-create-index))) + do (js--imenu-to-flat imenu-index "" symbols) + finally return symbols)) + +(defvar js--symbol-history nil + "History of entered Javascript symbols") + +(defun js--read-symbol (symbols-table prompt &optional initial-input) + "Read a symbol from SYMBOLS-TABLE, which is a hash table like +the one from `js--get-all-known-symbols'. Return value is a +cons of (SYMBOL-NAME . LOCATION), where SYMBOL-NAME is a string +and LOCATION is a marker. + +Prompt is PROMPT. + +If INITIAL-INPUT is not nil, use it as the initial input" + + (unless ido-mode + (ido-mode t) + (ido-mode nil)) + + (let ((choice (ido-completing-read + prompt + (loop for key being the hash-keys of symbols-table + collect key) + nil t initial-input 'js--symbol-history))) + (cons choice (gethash choice symbols-table)))) + +(defun js--guess-symbol-at-point () + (let ((bounds (bounds-of-thing-at-point 'symbol))) + (when bounds + (save-excursion + (goto-char (car bounds)) + (when (eq (char-before) ?.) + (backward-char) + (setf (car bounds) (point)))) + (buffer-substring (car bounds) (cdr bounds))))) + +(defun js-find-symbol (&optional arg) + "Jump to a Javascript symbol we read in from the user. With +prefix argument, restrict symbols to those from the current +buffer. Pushes a mark onto the tag ring just like `find-tag'." + + (interactive "P") + (let (symbols marker) + (if (not arg) + (setq symbols (js--get-all-known-symbols)) + (setq symbols (make-hash-table :test 'equal)) + (js--imenu-to-flat (js--imenu-create-index) + "" symbols)) + + (setq marker (cdr (js--read-symbol + symbols "Jump to: " + (js--guess-symbol-at-point)))) + + (ring-insert find-tag-marker-ring (point-marker)) + (switch-to-buffer (marker-buffer marker)) + (push-mark) + (goto-char marker))) + +;;; 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") + +(defun js--wait-for-matching-output + (process regexp timeout &optional start) + "Wait TIMEOUT seconds for PROCESS to output something that +matches REGEXP. On timeout, return nil. On success, return t with +match data set. If START is non-nil, look for output starting +from START. Otherwise, use the current value of `process-mark'." + (with-current-buffer (process-buffer process) + (loop with start-pos = (or start + (marker-position (process-mark process))) + with end-time = (+ (float-time) timeout) + for time-left = (- end-time (float-time)) + do (goto-char (point-max)) + if (looking-back regexp start-pos) return t + while (> time-left 0) + do (accept-process-output process time-left nil t) + do (goto-char (process-mark process)) + finally do (signal + 'js-moz-bad-rpc + (list (format "Timed out waiting for output matching %S" regexp)))))) + +(defstruct js--js-handle + ;; Integer, mirrors the value we see in JS + (id nil :read-only t) + + ;; Process to which this thing belongs + (process nil :read-only t)) + +(defun js--js-handle-expired-p (x) + (not (eq (js--js-handle-process x) + (inferior-moz-process)))) + +(defvar js--js-references nil + "Maps Elisp Javascript proxy objects to their Javascript IDs.") + +(defvar js--js-process nil + "The last MozRepl process object we saw") + +(defvar js--js-gc-idle-timer nil + "Idle timer for cleaning up JS object references") + +(defvar js--js-last-gcs-done nil + "Last number of gcs we GCed with") + +(defconst js--moz-interactor + (replace-regexp-in-string + "[ \n]+" " " + ; */" Make Emacs happy +"(function(repl) { + repl.defineInteractor('js', { + onStart: function onStart(repl) { + if(!repl._jsObjects) { + repl._jsObjects = {}; + repl._jsLastID = 0; + repl._jsGC = this._jsGC; + } + this._input = ''; + }, + + _jsGC: function _jsGC(ids_in_use) { + var objects = this._jsObjects; + var keys = []; + var num_freed = 0; + + for(var pn in objects) { + keys.push(Number(pn)); + } + + keys.sort(function(x, y) x - y); + ids_in_use.sort(function(x, y) x - y); + var i = 0; + var j = 0; + + while(i < ids_in_use.length && j < keys.length) { + var id = ids_in_use[i++]; + while(j < keys.length && keys[j] !== id) { + var k_id = keys[j++]; + delete objects[k_id]; + ++num_freed; + } + ++j; + } + + while(j < keys.length) { + var k_id = keys[j++]; + delete objects[k_id]; + ++num_freed; + } + + return num_freed; + }, + + _mkArray: function _mkArray() { + var result = []; + for(var i = 0; i < arguments.length; ++i) { + result.push(arguments[i]); + } + return result; + }, + + _parsePropDescriptor: function _parsePropDescriptor(parts) { + if(typeof parts === 'string') { + parts = [ parts ]; + } + + var obj = parts[0]; + var start = 1; + + if(typeof obj === 'string') { + obj = window; + start = 0; + } else if(parts.length < 2) { + throw new Error('expected at least 2 arguments'); + } + + for(var i = start; i < parts.length - 1; ++i) { + obj = obj[parts[i]]; + } + + return [obj, parts[parts.length - 1]]; + }, + + _getProp: function _getProp(/*...*/) { + if(arguments.length === 0) { + throw new Error('no arguments supplied to getprop'); + } + + if(arguments.length === 1 && + (typeof arguments[0]) !== 'string') + { + return arguments[0]; + } + + var [obj, propname] = this._parsePropDescriptor(arguments); + return obj[propname]; + }, + + _putProp: function _putProp(properties, value) { + var [obj, propname] = this._parsePropDescriptor(properties); + obj[propname] = value; + }, + + _delProp: function _delProp(propname) { + var [obj, propname] = this._parsePropDescriptor(arguments); + delete obj[propname]; + }, + + _typeOf: function _typeOf(thing) { + return typeof thing; + }, + + _callNew: function(constructor) { + if(typeof constructor === 'string') + { + constructor = window[constructor]; + } else if(constructor.length === 1 && + typeof constructor[0] !== 'string') + { + constructor = constructor[0]; + } else { + var [obj,propname] = this._parsePropDescriptor(constructor); + constructor = obj[propname]; + } + + /* Hacky, but should be robust */ + var s = 'new constructor('; + for(var i = 1; i < arguments.length; ++i) { + if(i != 1) { + s += ','; + } + + s += 'arguments[' + i + ']'; + } + + s += ')'; + return eval(s); + }, + + _callEval: function(thisobj, js) { + return eval.call(thisobj, js); + }, + + getPrompt: function getPrompt(repl) { + return 'EVAL>' + }, + + _lookupObject: function _lookupObject(repl, id) { + if(typeof id === 'string') { + switch(id) { + case 'global': + return window; + case 'nil': + return null; + case 't': + return true; + case 'false': + return false; + case 'undefined': + return undefined; + case 'repl': + return repl; + case 'interactor': + return this; + case 'NaN': + return NaN; + case 'Infinity': + return Infinity; + case '-Infinity': + return -Infinity; + default: + throw new Error('No object with special id:' + id); + } + } + + var ret = repl._jsObjects[id]; + if(ret === undefined) { + throw new Error('No object with id:' + id + '(' + typeof id + ')'); + } + return ret; + }, + + _findOrAllocateObject: function _findOrAllocateObject(repl, value) { + if(typeof value !== 'object' && typeof value !== 'function') { + throw new Error('_findOrAllocateObject called on non-object(' + + typeof(value) + '): ' + + value) + } + + for(var id in repl._jsObjects) { + id = Number(id); + var obj = repl._jsObjects[id]; + if(obj === value) { + return id; + } + } + + var id = ++repl._jsLastID; + repl._jsObjects[id] = value; + return id; + }, + + _fixupList: function _fixupList(repl, list) { + for(var i = 0; i < list.length; ++i) { + if(list[i] instanceof Array) { + this._fixupList(repl, list[i]); + } else if(typeof list[i] === 'object') { + var obj = list[i]; + if(obj.funcall) { + var parts = obj.funcall; + this._fixupList(repl, parts); + var [thisobj, func] = this._parseFunc(parts[0]); + list[i] = func.apply(thisobj, parts.slice(1)); + } else if(obj.objid) { + list[i] = this._lookupObject(repl, obj.objid); + } else { + throw new Error('Unknown object type: ' + obj.toSource()); + } + } + } + }, + + _parseFunc: function(func) { + var thisobj = null; + + if(typeof func === 'string') { + func = window[func]; + } else if(func instanceof Array) { + if(func.length === 1 && typeof func[0] !== 'string') { + func = func[0]; + } else { + [thisobj, func] = this._parsePropDescriptor(func); + func = thisobj[func]; + } + } + + return [thisobj,func]; + }, + + _encodeReturn: function(value, array_as_mv) { + var ret; + + if(value === null) { + ret = ['special', 'null']; + } else if(value === true) { + ret = ['special', 'true']; + } else if(value === false) { + ret = ['special', 'false']; + } else if(value === undefined) { + ret = ['special', 'undefined']; + } else if(typeof value === 'number') { + if(isNaN(value)) { + ret = ['special', 'NaN']; + } else if(value === Infinity) { + ret = ['special', 'Infinity']; + } else if(value === -Infinity) { + ret = ['special', '-Infinity']; + } else { + ret = ['atom', value]; + } + } else if(typeof value === 'string') { + ret = ['atom', value]; + } else if(array_as_mv && value instanceof Array) { + ret = ['array', value.map(this._encodeReturn, this)]; + } else { + ret = ['objid', this._findOrAllocateObject(repl, value)]; + } + + return ret; + }, + + _handleInputLine: function _handleInputLine(repl, line) { + var ret; + var array_as_mv = false; + + try { + if(line[0] === '*') { + array_as_mv = true; + line = line.substring(1); + } + var parts = eval(line); + this._fixupList(repl, parts); + var [thisobj, func] = this._parseFunc(parts[0]); + ret = this._encodeReturn( + func.apply(thisobj, parts.slice(1)), + array_as_mv); + } catch(x) { + ret = ['error', x.toString() ]; + } + + var JSON = Components.classes['@mozilla.org/dom/json;1'].createInstance(Components.interfaces.nsIJSON); + repl.print(JSON.encode(ret)); + repl._prompt(); + }, + + handleInput: function handleInput(repl, chunk) { + this._input += chunk; + var match, line; + while(match = this._input.match(/.*\\n/)) { + line = match[0]; + + if(line === 'EXIT\\n') { + repl.popInteractor(); + repl._prompt(); + return; + } + + this._input = this._input.substring(line.length); + this._handleInputLine(repl, line); + } + } + }); +}) +") + + "String to set MozRepl up into a simple-minded evaluation mode") + +(defun js--js-encode-value (x) + "Marshall the given value for JS. Strings and numbers get +JSON-encoded. Lists (including nil) are made into Javascript +array literals and their contents encoded with +js--js-encode-value." + + (cond ((stringp x) (json-encode-string x)) + ((numberp x) (json-encode-number x)) + ((symbolp x) (format "{objid:%S}" (symbol-name x))) + ((js--js-handle-p x) + + (when (js--js-handle-expired-p x) + (error "Stale JS handle")) + + (format "{objid:%s}" (js--js-handle-id x))) + + ((sequencep x) + (if (eq (car-safe x) 'js--funcall) + (format "{funcall:[%s]}" + (mapconcat #'js--js-encode-value (cdr x) ",")) + (concat + "[" (mapconcat #'js--js-encode-value x ",") "]"))) + (t + (error "Unrecognized item: %S" x)))) + +(defconst js--js-prompt-regexp "\\(repl[0-9]*\\)> $") +(defconst js--js-repl-prompt-regexp "^EVAL>$") +(defvar js--js-repl-depth 0) + +(defun js--js-wait-for-eval-prompt () + (js--wait-for-matching-output + (inferior-moz-process) + js--js-repl-prompt-regexp js-js-timeout + + ;; start matching against the beginning of the line in + ;; order to catch a prompt that's only partially arrived + (save-excursion (forward-line 0) (point)))) + +(defun js--js-enter-repl () + (inferior-moz-process) ; called for side-effect + (with-current-buffer inferior-moz-buffer + (goto-char (point-max)) + + ;; Do some initialization the first time we see a process + (unless (eq (inferior-moz-process) js--js-process) + (setq js--js-process (inferior-moz-process)) + (setq js--js-references (make-hash-table :test 'eq :weakness t)) + (setq js--js-repl-depth 0) + + ;; Send interactor definition + (comint-send-string js--js-process js--moz-interactor) + (comint-send-string js--js-process + (concat "(" moz-repl-name ")\n")) + (js--wait-for-matching-output + (inferior-moz-process) js--js-prompt-regexp + js-js-timeout)) + + ;; Sanity check + (when (looking-back js--js-prompt-regexp + (save-excursion (forward-line 0) (point))) + (setq js--js-repl-depth 0)) + + (if (> js--js-repl-depth 0) + ;; If js--js-repl-depth > 0, we *should* be seeing an + ;; EVAL> prompt. If we don't, give Mozilla a chance to catch + ;; up with us. + (js--js-wait-for-eval-prompt) + + ;; Otherwise, tell Mozilla to enter the interactor mode + (insert (match-string-no-properties 1) + ".pushInteractor('js')") + (comint-send-input nil t) + (js--wait-for-matching-output + (inferior-moz-process) js--js-repl-prompt-regexp + js-js-timeout)) + + (incf js--js-repl-depth))) + +(defun js--js-leave-repl () + (assert (> js--js-repl-depth 0)) + (when (= 0 (decf js--js-repl-depth)) + (with-current-buffer inferior-moz-buffer + (goto-char (point-max)) + (js--js-wait-for-eval-prompt) + (insert "EXIT") + (comint-send-input nil t) + (js--wait-for-matching-output + (inferior-moz-process) js--js-prompt-regexp + js-js-timeout)))) + +(defsubst js--js-not (value) + (memq value '(nil false undefined))) + +(defsubst js--js-true (value) + (not (js--js-not value))) + +(eval-and-compile + (defun js--optimize-arglist (arglist) + "Convert immediate js< and js! references to deferred ones" + (loop for item in arglist + if (eq (car-safe item) 'js<) + collect (append (list 'list ''js--funcall + '(list 'interactor "_getProp")) + (js--optimize-arglist (cdr item))) + else if (eq (car-safe item) 'js>) + collect (append (list 'list ''js--funcall + '(list 'interactor "_putProp")) + + (if (atom (cadr item)) + (list (cadr item)) + (list + (append + (list 'list ''js--funcall + '(list 'interactor "_mkArray")) + (js--optimize-arglist (cadr item))))) + (js--optimize-arglist (cddr item))) + else if (eq (car-safe item) 'js!) + collect (destructuring-bind (ignored function &rest body) item + (append (list 'list ''js--funcall + (if (consp function) + (cons 'list + (js--optimize-arglist function)) + function)) + (js--optimize-arglist body))) + else + collect item))) + +(defmacro js--js-get-service (class-name interface-name) + `(js! ("Components" "classes" ,class-name "getService") + (js< "Components" "interfaces" ,interface-name))) + +(defmacro js--js-create-instance (class-name interface-name) + `(js! ("Components" "classes" ,class-name "createInstance") + (js< "Components" "interfaces" ,interface-name))) + +(defmacro js--js-qi (object interface-name) + `(js! (,object "QueryInterface") + (js< "Components" "interfaces" ,interface-name))) + +(defmacro with-js (&rest forms) + "Runs FORMS with the Mozilla repl set up for js commands. +Inside the lexical scope of `with-js', `js?', `js!', +`js-new', `js-eval', `js-list', `js<', `js>', `js-get-service', +`js-create-instance', and `js-qi' are defined." + + `(progn + (js--js-enter-repl) + (unwind-protect + (macrolet ((js? (&rest body) `(js--js-true ,@body)) + (js! (function &rest body) + `(js--js-funcall + ,(if (consp function) + (cons 'list + (js--optimize-arglist function)) + function) + ,@(js--optimize-arglist body))) + + (js-new (function &rest body) + `(js--js-new + ,(if (consp function) + (cons 'list + (js--optimize-arglist function)) + function) + ,@body)) + + (js-eval (thisobj js) + `(js--js-eval + ,@(js--optimize-arglist + (list thisobj js)))) + + (js-list (&rest args) + `(js--js-list + ,@(js--optimize-arglist args))) + + (js-get-service (&rest args) + `(js--js-get-service + ,@(js--optimize-arglist args))) + + (js-create-instance (&rest args) + `(js--js-create-instance + ,@(js--optimize-arglist args))) + + (js-qi (&rest args) + `(js--js-qi + ,@(js--optimize-arglist args))) + + (js< (&rest body) `(js--js-get + ,@(js--optimize-arglist body))) + (js> (props value) + `(js--js-funcall + '(interactor "_putProp") + ,(if (consp props) + (cons 'list + (js--optimize-arglist props)) + props) + ,@(js--optimize-arglist (list value)) + )) + (js-handle? (arg) `(js--js-handle-p ,arg))) + ,@forms) + (js--js-leave-repl)))) + +(defvar js--js-array-as-list nil +"If bound to t and a function returns an Array, return the +elements of that Array as a list instead of returning the whole +Array as a JS symbol" ) + +(defun js--js-decode-retval (result) + (ecase (intern (first result)) + (atom (second result)) + (special (intern (second result))) + (array + (mapcar #'js--js-decode-retval (second result))) + (objid + (or (gethash (second result) + js--js-references) + (puthash (second result) + (make-js--js-handle + :id (second result) + :process (inferior-moz-process)) + js--js-references))) + + (error (signal 'js-js-error (list (second result)))))) + +(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 +object and use the global object for `this'; if FUNCTION is a +list with one element, use that element as the function with the +global object for `this', except that if that single element is a +string, look it up on the global object. If FUNCTION is a list +with more than one argument, use the list up to the last value as +a property descriptor and the last argument as a function." + + (with-js + (let ((argstr (js--js-encode-value + (cons function arguments)))) + + (with-current-buffer inferior-moz-buffer + ;; Actual funcall + (when js--js-array-as-list + (insert "*")) + (insert argstr) + (comint-send-input nil t) + (js--wait-for-matching-output + (inferior-moz-process) "EVAL>" + js-js-timeout) + (goto-char comint-last-input-end) + + ;; Read the result + (let* ((json-array-type 'list) + (result (prog1 (json-read) + (goto-char (point-max))))) + (js--js-decode-retval result)))))) + +(defun js--js-new (constructor &rest arguments) + "Call CONSTRUCTOR as a constructor (as with new), with the +given arguments ARGUMENTS. CONSTRUCTOR is a JS handle, a string, +or a list of these things." + + (apply #'js--js-funcall + '(interactor "_callNew") + constructor arguments)) + +(defun js--js-eval (thisobj js) + (js--js-funcall '(interactor "_callEval") thisobj js)) + +(defun js--js-list (&rest arguments) + "Return a Lisp array that is the result of evaluating each of + the elements of ARGUMENTS" + + (let ((js--js-array-as-list t)) + (apply #'js--js-funcall '(interactor "_mkArray") + arguments))) + +(defun js--js-get (&rest props) + (apply #'js--js-funcall '(interactor "_getProp") props)) + +(defun js--js-put (props value) + (js--js-funcall '(interactor "_putProp") props value)) + +(defun js-gc (&optional force) + "Tell the repl about any objects we don't reference anymore. +With argument, run even if no intervening GC has happened." + (interactive) + + (when force + (setq js--js-last-gcs-done nil)) + + (let ((this-gcs-done gcs-done) keys num) + (when (and js--js-references + (boundp 'inferior-moz-buffer) + (buffer-live-p inferior-moz-buffer) + + ;; Don't bother running unless we've had an intervening + ;; garbage collection; without a gc, nothing is deleted + ;; from the weak hash table, so it's pointless telling + ;; MozRepl about that references we still hold + (not (eq js--js-last-gcs-done this-gcs-done)) + + ;; Are we looking at a normal prompt? Make sure not to + ;; interrupt the user if he's doing something + (with-current-buffer inferior-moz-buffer + (save-excursion + (goto-char (point-max)) + (looking-back js--js-prompt-regexp + (save-excursion (forward-line 0) (point)))))) + + (setq keys (loop for x being the hash-keys + of js--js-references + collect x)) + (setq num (js--js-funcall '(repl "_jsGC") (or keys []))) + + (setq js--js-last-gcs-done this-gcs-done) + (when (interactive-p) + (message "Cleaned %s entries" num)) + + num))) + +(run-with-idle-timer 30 t #'js-gc) + +(defun js-eval (js) + "Evaluate the Javascript contained in JS and return the +JSON-decoded result. eval must be called using this function +because it is special to the Javascript interpreter." + (interactive "MJavascript to evaluate: ") + (with-js + (let* ((content-window (js--js-content-window + (js--get-js-context))) + (result (js-eval content-window js))) + + (when (interactive-p) + (message "%s" (js! "String" result))) + result))) + +(defun js--get-tabs () + "Enumerate all the contexts available. Each one is a list: + + The list is (TITLE URL BROWSER TAB TABBROWSER) for content documents + The list is (TITLE URL WINDOW) for windows + + All tabs of a given window are grouped together. The most + recent window is first. Within each window, the tabs are + returned left-to-right. +" + (with-js + (let (windows) + + (loop with window-mediator = (js! ("Components" "classes" + "@mozilla.org/appshell/window-mediator;1" + "getService") + (js< "Components" "interfaces" + "nsIWindowMediator")) + with enumerator = (js! (window-mediator "getEnumerator") nil) + + while (js? (js! (enumerator "hasMoreElements"))) + for window = (js! (enumerator "getNext")) + for window-info = (js-list window + (js< window "document" "title") + (js! (window "location" "toString")) + (js< window "closed") + (js< window "windowState")) + + unless (or (js? (fourth window-info)) + (eq (fifth window-info) 2)) + do (push window-info windows)) + + (loop for window-info in windows + for window = (first window-info) + collect (list (second window-info) + (third window-info) + window) + + for gbrowser = (js< window "gBrowser") + if (js-handle? gbrowser) + nconc (loop + for x below (js< gbrowser "browsers" "length") + collect (js-list (js< gbrowser + "browsers" + x + "contentDocument" + "title") + + (js! (gbrowser + "browsers" + x + "contentWindow" + "location" + "toString")) + (js< gbrowser + "browsers" + x) + + (js! (gbrowser + "tabContainer" + "childNodes" + "item") + x) + + gbrowser)))))) + +(defvar js-read-tab-history nil) + +(defun js--read-tab (prompt) + "Read a Mozilla tab with prompt PROMPT. Return a cons of (TYPE . OBJECT). +TYPE is either 'window or 'tab, and OBJECT is a Javascript handle +to a ChromeWindow or a browser, respectively." + + ;; Prime IDO + (unless ido-mode + (ido-mode t) + (ido-mode nil)) + + (with-js + (lexical-let ((tabs (js--get-tabs)) selected-tab-cname + selected-tab prev-hitab) + + ;; Disambiguate names + (setq tabs (loop with tab-names = (make-hash-table :test 'equal) + for tab in tabs + for cname = (format "%s (%s)" (second tab) (first tab)) + for num = (incf (gethash cname tab-names -1)) + if (> num 0) + do (setq cname (format "%s <%d>" cname num)) + collect (cons cname tab))) + + (labels ((find-tab-by-cname + (cname) + (loop for tab in tabs + if (equal (car tab) cname) + return (cdr tab))) + + (mogrify-highlighting + (hitab unhitab) + + ;; Hack to reduce the number of + ;; round-trips to mozilla + (let (cmds) + (cond + ;; Highlighting tab + ((fourth hitab) + (push '(js! ((fourth hitab) "setAttribute") + "style" + "color: red; font-weight: bold") + cmds) + + ;; Highlight window proper + (push '(js! ((third hitab) + "setAttribute") + "style" + "border: 8px solid red") + cmds) + + ;; Select tab, when appropriate + (when js-js-switch-tabs + (push + '(js> ((fifth hitab) "selectedTab") (fourth hitab)) + cmds))) + + ;; Hilighting whole window + ((third hitab) + (push '(js! ((third hitab) "document" + "documentElement" "setAttribute") + "style" + (concat "-moz-appearance: none;" + "border: 8px solid red;")) + cmds))) + + (cond + ;; Unhighlighting tab + ((fourth unhitab) + (push '(js! ((fourth unhitab) "setAttribute") "style" "") + cmds) + (push '(js! ((third unhitab) "setAttribute") "style" "") + cmds)) + + ;; Unhighlighting window + ((third unhitab) + (push '(js! ((third unhitab) "document" + "documentElement" "setAttribute") + "style" "") + cmds))) + + (eval (list 'with-js + (cons 'js-list (nreverse cmds)))))) + + (command-hook + () + (let* ((tab (find-tab-by-cname (car ido-matches)))) + (mogrify-highlighting tab prev-hitab) + (setq prev-hitab tab))) + + (setup-hook + () + ;; Fiddle with the match list a bit: if our first match + ;; is a tabbrowser window, rotate the match list until + ;; the active tab comes up + (let ((matched-tab (find-tab-by-cname (car ido-matches)))) + (when (and matched-tab + (null (fourth matched-tab)) + (equal "navigator:browser" + (js! ((third matched-tab) + "document" + "documentElement" + "getAttribute") + "windowtype"))) + + (loop with tab-to-match = (js< (third matched-tab) + "gBrowser" + "selectedTab") + + with index = 0 + for match in ido-matches + for candidate-tab = (find-tab-by-cname match) + if (eq (fourth candidate-tab) tab-to-match) + do (setq ido-cur-list (ido-chop ido-cur-list match)) + and return t))) + + (add-hook 'post-command-hook #'command-hook t t))) + + + (unwind-protect + (setq selected-tab-cname + (let ((ido-minibuffer-setup-hook + (cons #'setup-hook ido-minibuffer-setup-hook))) + (ido-completing-read + prompt + (mapcar #'car tabs) + nil t nil + 'js-read-tab-history))) + + (when prev-hitab + (mogrify-highlighting nil prev-hitab) + (setq prev-hitab nil))) + + (add-to-history 'js-read-tab-history selected-tab-cname) + + (setq selected-tab (loop for tab in tabs + if (equal (car tab) selected-tab-cname) + return (cdr tab))) + + (if (fourth selected-tab) + (cons 'browser (third selected-tab)) + (cons 'window (third selected-tab))))))) + +(defun js--guess-eval-defun-info (pstate) + "Internal helper for `js-eval-defun'. Returns a list (NAME . CLASSPARTS), +where CLASSPARTS is a list of strings making up the class name +and NAME is the name of the function part." + (cond ((and (= (length pstate) 3) + (eq (js--pitem-type (first pstate)) 'function) + (= (length (js--pitem-name (first pstate))) 1) + (consp (js--pitem-type (second pstate)))) + + (append (js--pitem-name (second pstate)) + (list (first (js--pitem-name (first pstate)))))) + + ((and (= (length pstate) 2) + (eq (js--pitem-type (first pstate)) 'function)) + + (append + (butlast (js--pitem-name (first pstate))) + (list (car (last (js--pitem-name (first pstate))))))) + + (t (error "Function not a toplevel defun or class member")))) + +(defvar js--js-context nil + "The current JS context. This is a cons like the one returned +from `js--read-tab'. Change with +`js-set-js-context'.") + +(defconst js--js-inserter + "(function(func_info,func) { + func_info.unshift('window'); + var obj = window; + for(var i = 1; i < func_info.length - 1; ++i) { + var next = obj[func_info[i]]; + if(typeof next !== 'object' && typeof next !== 'function') { + next = obj.prototype && obj.prototype[func_info[i]]; + if(typeof next !== 'object' && typeof next !== 'function') { + alert('Could not find ' + func_info.slice(0, i+1).join('.') + + ' or ' + func_info.slice(0, i+1).join('.') + '.prototype'); + return; + } + + func_info.splice(i+1, 0, 'prototype'); + ++i; + } + } + + obj[func_info[i]] = func; + alert('Successfully updated '+func_info.join('.')); + })") + +(defun js-set-js-context (context) + "Set the Javascript context to CONTEXT, reading from the user +if interactive" + (interactive (list (js--read-tab "Javascript Context: "))) + (setq js--js-context context)) + +(defun js--get-js-context () + "Return a valid JS context. If one hasn't been set, or if it's +stale, ask the user for a new one." + + (with-js + (when (or (null js--js-context) + (js--js-handle-expired-p (cdr js--js-context)) + (ecase (car js--js-context) + (window (js? (js< (cdr js--js-context) "closed"))) + (browser (not (js? (js< (cdr js--js-context) + "contentDocument")))))) + (setq js--js-context (js--read-tab "Javascript Context: "))) + + js--js-context)) + +(defun js--js-content-window (context) + (with-js + (ecase (car context) + (window (cdr context)) + (browser (js< (cdr context) + "contentWindow" "wrappedJSObject"))))) + +(defun js--make-nsilocalfile (path) + (with-js + (let ((file (js-create-instance "@mozilla.org/file/local;1" + "nsILocalFile"))) + (js! (file "initWithPath") path) + file))) + +(defun js--js-add-resource-alias (alias path) + (with-js + (let* ((io-service (js-get-service "@mozilla.org/network/io-service;1" + "nsIIOService")) + (res-prot (js! (io-service "getProtocolHandler") "resource")) + (res-prot (js-qi res-prot "nsIResProtocolHandler")) + (path-file (js--make-nsilocalfile path)) + (path-uri (js! (io-service "newFileURI") path-file))) + (js! (res-prot "setSubstitution") alias path-uri)))) + +(defun* js-eval-defun () + "Update some Mozilla tab with defun containing point or after +point" + (interactive) + + ;; This function works by generating a temporary file that contains + ;; the function we'd like to insert. We then use the elisp-js bridge + ;; to command mozilla to load this file by inserting a script tag + ;; into the document we set. This way, debuggers and such will have + ;; a way to find the source of the just-inserted function. + ;; + ;; We delete the temporary file if there's an error, but otherwise + ;; we add an unload event listener on the Mozilla side to delete the + ;; file. + + (save-excursion + (let (begin end pstate defun-info temp-name defun-body) + (js--end-of-defun) + (setq end (point)) + (js--ensure-cache) + (js--beginning-of-defun) + (re-search-forward "\\_") + (setq begin (match-beginning 0)) + (setq pstate (js--forward-pstate)) + + (when (or (null pstate) + (> (point) end)) + (error "Could not locate function definition")) + + (setq defun-info (js--guess-eval-defun-info pstate)) + + (let ((overlay (make-overlay begin end))) + (overlay-put overlay 'face 'highlight) + (unwind-protect + (unless (y-or-n-p (format "Send %s to Mozilla? " + (mapconcat #'identity defun-info "."))) + (message "") ; question message lingers until next command + (return-from js-eval-defun)) + (delete-overlay overlay))) + + (setq defun-body (buffer-substring-no-properties begin end)) + + (make-directory js-js-tmpdir t) + + ;; (Re)register a Mozilla resource URL to point to the + ;; temporary directory + (js--js-add-resource-alias "js" js-js-tmpdir) + + (setq temp-name (make-temp-file (concat js-js-tmpdir + "/js-") + nil ".js")) + (unwind-protect + (with-js + (with-temp-buffer + (insert js--js-inserter) + (insert "(") + (insert (json-encode-list defun-info)) + (insert ",\n") + (insert defun-body) + (insert "\n)") + (write-region (point-min) (point-max) temp-name + nil 1)) + + ;; Give Mozilla responsibility for deleting this file + (let* ((content-window (js--js-content-window + (js--get-js-context))) + (content-document (js< content-window "document")) + (head (if (js? (js< content-document "body")) + ;; Regular content + (js< (js! (content-document "getElementsByTagName") + "head") + 0) + ;; Chrome + (js< content-document "documentElement"))) + (elem (js! (content-document "createElementNS") + "http://www.w3.org/1999/xhtml" "script"))) + + (js! (elem "setAttribute") "type" "text/javascript") + (js! (elem "setAttribute") "src" + (format "resource://js/%s" + (file-name-nondirectory temp-name))) + + (js! (head "appendChild") elem) + + (js! (content-window "addEventListener") "unload" + (js! ((js-new + "Function" "file" + "return function() { file.remove(false) }")) + (js--make-nsilocalfile temp-name)) + 'false) + (setq temp-name nil) + + + + )) + + ;; temp-name is set to nil on success + (when temp-name + (delete-file temp-name)))))) + +;;; Main Function + +;;;###autoload +(define-derived-mode js-mode nil "js" + "Major mode for editing JavaScript source text. + +Key bindings: + +\\{js-mode-map}" + + :group 'js + :syntax-table js-mode-syntax-table + + (set (make-local-variable 'indent-line-function) 'js-indent-line) + (set (make-local-variable 'beginning-of-defun-function) + 'js--beginning-of-defun) + (set (make-local-variable 'end-of-defun-function) + 'js--end-of-defun) + + (set (make-local-variable 'open-paren-in-column-0-is-defun-start) nil) + (set (make-local-variable 'font-lock-defaults) + (list js--font-lock-keywords + nil nil nil nil + '(font-lock-syntactic-keywords + . js--font-lock-syntactic-keywords))) + + (set (make-local-variable 'parse-sexp-ignore-comments) t) + (set (make-local-variable 'parse-sexp-lookup-properties) t) + (set (make-local-variable 'which-func-imenu-joiner-function) + #'js--which-func-joiner) + + ;; Comments + (setq comment-start "// ") + (setq comment-end "") + (set (make-local-variable 'fill-paragraph-function) + 'js-c-fill-paragraph) + + ;; Parse cache + (add-hook 'before-change-functions #'js--flush-caches t t) + + ;; Frameworks + (js--update-quick-match-re) + + ;; Imenu + (setq imenu-case-fold-search nil) + (set (make-local-variable 'imenu-create-index-function) + #'js--imenu-create-index) + + (setq major-mode 'js-mode) + (setq mode-name "Javascript") + + ;; for filling, pretend we're cc-mode + (setq c-comment-prefix-regexp "//+\\|\\**" + c-paragraph-start "$" + c-paragraph-separate "$" + c-block-comment-prefix "* " + c-line-comment-starter "//" + c-comment-start-regexp "/[*/]\\|\\s!" + comment-start-skip "\\(//+\\|/\\*+\\)\\s *") + + (let ((c-buffer-is-cc-mode t)) + (c-setup-paragraph-variables)) + + (set (make-local-variable 'syntax-begin-function) + #'js--syntax-begin-function) + + ;; Important to fontify the whole buffer syntactically! If we don't, + ;; then we might have regular expression literals that aren't marked + ;; as strings, which will screw up parse-partial-sexp, scan-lists, + ;; etc. and and produce maddening "unbalanced parenthesis" errors. + ;; When we attempt to find the error and scroll to the portion of + ;; the buffer containing the problem, JIT-lock will apply the + ;; correct syntax to the regular expresion literal and the problem + ;; will mysteriously disappear. + (font-lock-set-defaults) + + (let (font-lock-keywords) ; leaves syntactic keywords intact + (font-lock-fontify-buffer))) + +(defalias 'javascript-mode 'js-mode) + +(eval-after-load 'folding + '(when (fboundp 'folding-add-to-marks-list) + (folding-add-to-marks-list 'js-mode "// {{{" "// }}}" ))) + +(provide 'js) + +;; js.el ends here diff -r 73a2da4c1a39 -r ea67ac46d172 lisp/progmodes/js2-mode.el --- a/lisp/progmodes/js2-mode.el Fri Aug 14 18:18:39 2009 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11359 +0,0 @@ -;;; js2-mode.el --- an improved JavaScript editing mode - -;; Copyright (C) 2009 Free Software Foundation, Inc. - -;; Author: Steve Yegge -;; Version: See `js2-mode-version' -;; Keywords: languages, javascript - -;; This file is part of GNU Emacs. - -;; GNU Emacs is free software: you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation, either version 3 of the License, or -;; (at your option) any later version. - -;; GNU Emacs is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with GNU Emacs. If not, see . - -;;; Commentary: - -;; This JavaScript editing mode supports: - -;; - strict recognition of the Ecma-262 language standard -;; - support for most Rhino and SpiderMonkey extensions from 1.5 to 1.8 -;; - parsing support for ECMAScript for XML (E4X, ECMA-357) -;; - accurate syntax highlighting using a recursive-descent parser -;; - on-the-fly reporting of syntax errors and strict-mode warnings -;; - undeclared-variable warnings using a configurable externs framework -;; - "bouncing" line indentation to choose among alternate indentation points -;; - smart line-wrapping within comments and strings -;; - code folding: -;; - show some or all function bodies as {...} -;; - show some or all block comments as /*...*/ -;; - context-sensitive menu bar and popup menus -;; - code browsing using the `imenu' package -;; - typing helpers such as automatic insertion of matching braces/parens -;; - many customization options - -;; To customize how it works: -;; M-x customize-group RET js2-mode RET - -;; Notes: - -;; This mode includes a port of Mozilla Rhino's scanner, parser and -;; symbol table. Ideally it should stay in sync with Rhino, keeping -;; `js2-mode' current as the EcmaScript language standard evolves. - -;; Unlike cc-engine based language modes, js2-mode's line-indentation is not -;; customizable. It is a surprising amount of work to support customizable -;; indentation. The current compromise is that the tab key lets you cycle among -;; various likely indentation points, similar to the behavior of python-mode. - -;; This mode does not yet work with "multi-mode" modes such as `mmm-mode' -;; and `mumamo', although it could be made to do so with some effort. -;; This means that `js2-mode' is currently only useful for editing JavaScript -;; files, and not for editing JavaScript within