Misc sendmail.el.
[bpt/emacs.git] / lisp / progmodes / js.el
CommitLineData
2e330adc 1;;; js.el --- Major mode for editing JavaScript
17b5d0f7 2
114f9c96 3;; Copyright (C) 2008, 2009, 2010 Free Software Foundation, Inc.
17b5d0f7
CY
4
5;; Author: Karl Landstrom <karl.landstrom@brgeight.se>
6;; Daniel Colascione <dan.colascione@gmail.com>
7;; Maintainer: Daniel Colascione <dan.colascione@gmail.com>
8;; Version: 9
9;; Date: 2009-07-25
bd78fa1d 10;; Keywords: languages, javascript
17b5d0f7
CY
11
12;; This file is part of GNU Emacs.
13
14;; GNU Emacs is free software: you can redistribute it and/or modify
15;; it under the terms of the GNU General Public License as published by
16;; the Free Software Foundation, either version 3 of the License, or
17;; (at your option) any later version.
18
19;; GNU Emacs is distributed in the hope that it will be useful,
20;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22;; GNU General Public License for more details.
23
24;; You should have received a copy of the GNU General Public License
25;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
26
27;;; Commentary
28
29;; This is based on Karl Landstrom's barebones javascript-mode. This
30;; is much more robust and works with cc-mode's comment filling
31;; (mostly).
32;;
33;; The main features of this JavaScript mode are syntactic
34;; highlighting (enabled with `font-lock-mode' or
35;; `global-font-lock-mode'), automatic indentation and filling of
36;; comments, C preprocessor fontification, and MozRepl integration.
37;;
38;; General Remarks:
39;;
40;; XXX: This mode assumes that block comments are not nested inside block
41;; XXX: comments
42;;
43;; Exported names start with "js-"; private names start with
44;; "js--".
45
46;;; Code:
47
b073dc4b
SM
48
49(require 'cc-mode)
50(require 'font-lock)
51(require 'newcomment)
52(require 'imenu)
53(require 'etags)
54(require 'thingatpt)
55(require 'easymenu)
56(require 'moz nil t)
57(require 'json nil t)
17b5d0f7
CY
58
59(eval-when-compile
60 (require 'cl)
61 (require 'comint)
2e330adc 62 (require 'ido))
17b5d0f7
CY
63
64(defvar inferior-moz-buffer)
65(defvar moz-repl-name)
66(defvar ido-cur-list)
67(declare-function ido-mode "ido")
3e1ea342 68(declare-function inferior-moz-process "ext:mozrepl" ())
17b5d0f7
CY
69
70;;; Constants
71
72(defconst js--name-start-re "[a-zA-Z_$]"
2e330adc 73 "Regexp matching the start of a JavaScript identifier, without grouping.")
17b5d0f7
CY
74
75(defconst js--stmt-delim-chars "^;{}?:")
76
77(defconst js--name-re (concat js--name-start-re
78 "\\(?:\\s_\\|\\sw\\)*")
2e330adc 79 "Regexp matching a JavaScript identifier, without grouping.")
17b5d0f7
CY
80
81(defconst js--objfield-re (concat js--name-re ":")
2e330adc 82 "Regexp matching the start of a JavaScript object field.")
17b5d0f7
CY
83
84(defconst js--dotted-name-re
85 (concat js--name-re "\\(?:\\." js--name-re "\\)*")
2e330adc 86 "Regexp matching a dot-separated sequence of JavaScript names.")
17b5d0f7
CY
87
88(defconst js--cpp-name-re js--name-re
2e330adc 89 "Regexp matching a C preprocessor name.")
17b5d0f7
CY
90
91(defconst js--opt-cpp-start "^\\s-*#\\s-*\\([[:alnum:]]+\\)"
2e330adc
CY
92 "Regexp matching the prefix of a cpp directive.
93This includes the directive name, or nil in languages without
94preprocessor support. The first submatch surrounds the directive
95name.")
17b5d0f7
CY
96
97(defconst js--plain-method-re
98 (concat "^\\s-*?\\(" js--dotted-name-re "\\)\\.prototype"
99 "\\.\\(" js--name-re "\\)\\s-*?=\\s-*?\\(function\\)\\_>")
2e330adc 100 "Regexp matching an explicit JavaScript prototype \"method\" declaration.
dd4fbf56
JB
101Group 1 is a (possibly-dotted) class name, group 2 is a method name,
102and group 3 is the 'function' keyword.")
17b5d0f7
CY
103
104(defconst js--plain-class-re
105 (concat "^\\s-*\\(" js--dotted-name-re "\\)\\.prototype"
106 "\\s-*=\\s-*{")
2e330adc
CY
107 "Regexp matching a JavaScript explicit prototype \"class\" declaration.
108An example of this is \"Class.prototype = { method1: ...}\".")
17b5d0f7 109
2e330adc 110;; var NewClass = BaseClass.extend(
17b5d0f7
CY
111(defconst js--mp-class-decl-re
112 (concat "^\\s-*var\\s-+"
113 "\\(" js--name-re "\\)"
114 "\\s-*=\\s-*"
115 "\\(" js--dotted-name-re
2e330adc 116 "\\)\\.extend\\(?:Final\\)?\\s-*(\\s-*{?\\s-*$"))
17b5d0f7 117
2e330adc 118;; var NewClass = Class.create()
17b5d0f7
CY
119(defconst js--prototype-obsolete-class-decl-re
120 (concat "^\\s-*\\(?:var\\s-+\\)?"
121 "\\(" js--dotted-name-re "\\)"
2e330adc 122 "\\s-*=\\s-*Class\\.create()"))
17b5d0f7
CY
123
124(defconst js--prototype-objextend-class-decl-re-1
125 (concat "^\\s-*Object\\.extend\\s-*("
126 "\\(" js--dotted-name-re "\\)"
127 "\\s-*,\\s-*{"))
128
129(defconst js--prototype-objextend-class-decl-re-2
130 (concat "^\\s-*\\(?:var\\s-+\\)?"
131 "\\(" js--dotted-name-re "\\)"
132 "\\s-*=\\s-*Object\\.extend\\s-*\("))
133
2e330adc 134;; var NewClass = Class.create({
17b5d0f7
CY
135(defconst js--prototype-class-decl-re
136 (concat "^\\s-*\\(?:var\\s-+\\)?"
137 "\\(" js--name-re "\\)"
138 "\\s-*=\\s-*Class\\.create\\s-*(\\s-*"
2e330adc 139 "\\(?:\\(" js--dotted-name-re "\\)\\s-*,\\s-*\\)?{?"))
17b5d0f7 140
2e330adc 141;; Parent class name(s) (yes, multiple inheritance in JavaScript) are
17b5d0f7
CY
142;; matched with dedicated font-lock matchers
143(defconst js--dojo-class-decl-re
144 (concat "^\\s-*dojo\\.declare\\s-*(\"\\(" js--dotted-name-re "\\)"))
145
146(defconst js--extjs-class-decl-re-1
147 (concat "^\\s-*Ext\\.extend\\s-*("
148 "\\s-*\\(" js--dotted-name-re "\\)"
149 "\\s-*,\\s-*\\(" js--dotted-name-re "\\)")
2e330adc 150 "Regexp matching an ExtJS class declaration (style 1).")
17b5d0f7
CY
151
152(defconst js--extjs-class-decl-re-2
153 (concat "^\\s-*\\(?:var\\s-+\\)?"
154 "\\(" js--name-re "\\)"
155 "\\s-*=\\s-*Ext\\.extend\\s-*(\\s-*"
156 "\\(" js--dotted-name-re "\\)")
2e330adc 157 "Regexp matching an ExtJS class declaration (style 2).")
17b5d0f7
CY
158
159(defconst js--mochikit-class-re
160 (concat "^\\s-*MochiKit\\.Base\\.update\\s-*(\\s-*"
161 "\\(" js--dotted-name-re "\\)")
2e330adc 162 "Regexp matching a MochiKit class declaration.")
17b5d0f7
CY
163
164(defconst js--dummy-class-style
165 '(:name "[Automatically Generated Class]"))
166
167(defconst js--class-styles
168 `((:name "Plain"
169 :class-decl ,js--plain-class-re
170 :prototype t
171 :contexts (toplevel)
172 :framework javascript)
173
174 (:name "MochiKit"
175 :class-decl ,js--mochikit-class-re
176 :prototype t
177 :contexts (toplevel)
178 :framework mochikit)
179
180 (:name "Prototype (Obsolete)"
181 :class-decl ,js--prototype-obsolete-class-decl-re
182 :contexts (toplevel)
183 :framework prototype)
184
185 (:name "Prototype (Modern)"
186 :class-decl ,js--prototype-class-decl-re
187 :contexts (toplevel)
188 :framework prototype)
189
190 (:name "Prototype (Object.extend)"
191 :class-decl ,js--prototype-objextend-class-decl-re-1
192 :prototype t
193 :contexts (toplevel)
194 :framework prototype)
195
196 (:name "Prototype (Object.extend) 2"
197 :class-decl ,js--prototype-objextend-class-decl-re-2
198 :prototype t
199 :contexts (toplevel)
200 :framework prototype)
201
202 (:name "Dojo"
203 :class-decl ,js--dojo-class-decl-re
204 :contexts (toplevel)
205 :framework dojo)
206
207 (:name "ExtJS (style 1)"
208 :class-decl ,js--extjs-class-decl-re-1
209 :prototype t
210 :contexts (toplevel)
211 :framework extjs)
212
213 (:name "ExtJS (style 2)"
214 :class-decl ,js--extjs-class-decl-re-2
215 :contexts (toplevel)
216 :framework extjs)
217
218 (:name "Merrill Press"
219 :class-decl ,js--mp-class-decl-re
220 :contexts (toplevel)
221 :framework merrillpress))
222
2e330adc 223 "List of JavaScript class definition styles.
17b5d0f7
CY
224
225A class definition style is a plist with the following keys:
226
227:name is a human-readable name of the class type
228
229:class-decl is a regular expression giving the start of the
dd4fbf56
JB
230class. Its first group must match the name of its class. If there
231is a parent class, the second group should match, and it should be
232the name of the class.
17b5d0f7
CY
233
234If :prototype is present and non-nil, the parser will merge
235declarations for this constructs with others at the same lexical
dd4fbf56
JB
236level that have the same name. Otherwise, multiple definitions
237will create multiple top-level entries. Don't use :prototype
17b5d0f7
CY
238unnecessarily: it has an associated cost in performance.
239
240If :strip-prototype is present and non-nil, then if the class
241name as matched contains
242")
243
244(defconst js--available-frameworks
245 (loop with available-frameworks
246 for style in js--class-styles
247 for framework = (plist-get style :framework)
248 unless (memq framework available-frameworks)
249 collect framework into available-frameworks
250 finally return available-frameworks)
2e330adc 251 "List of available JavaScript frameworks symbols.")
17b5d0f7
CY
252
253(defconst js--function-heading-1-re
254 (concat
255 "^\\s-*function\\s-+\\(" js--name-re "\\)")
2e330adc
CY
256 "Regexp matching the start of a JavaScript function header.
257Match group 1 is the name of the function.")
17b5d0f7
CY
258
259(defconst js--function-heading-2-re
260 (concat
261 "^\\s-*\\(" js--name-re "\\)\\s-*:\\s-*function\\_>")
2e330adc
CY
262 "Regexp matching the start of a function entry in an associative array.
263Match group 1 is the name of the function.")
17b5d0f7
CY
264
265(defconst js--function-heading-3-re
266 (concat
267 "^\\s-*\\(?:var\\s-+\\)?\\(" js--dotted-name-re "\\)"
268 "\\s-*=\\s-*function\\_>")
2e330adc
CY
269 "Regexp matching a line in the JavaScript form \"var MUMBLE = function\".
270Match group 1 is MUMBLE.")
17b5d0f7
CY
271
272(defconst js--macro-decl-re
273 (concat "^\\s-*#\\s-*define\\s-+\\(" js--cpp-name-re "\\)\\s-*(")
2e330adc 274 "Regexp matching a CPP macro definition, up to the opening parenthesis.
dd4fbf56 275Match group 1 is the name of the macro.")
17b5d0f7
CY
276
277(defun js--regexp-opt-symbol (list)
2e330adc 278 "Like `regexp-opt', but surround the result with `\\\\_<' and `\\\\_>'."
17b5d0f7
CY
279 (concat "\\_<" (regexp-opt list t) "\\_>"))
280
281(defconst js--keyword-re
282 (js--regexp-opt-symbol
283 '("abstract" "break" "case" "catch" "class" "const"
284 "continue" "debugger" "default" "delete" "do" "else"
285 "enum" "export" "extends" "final" "finally" "for"
286 "function" "goto" "if" "implements" "import" "in"
287 "instanceof" "interface" "native" "new" "package"
288 "private" "protected" "public" "return" "static"
289 "super" "switch" "synchronized" "throw"
290 "throws" "transient" "try" "typeof" "var" "void" "let"
291 "yield" "volatile" "while" "with"))
2e330adc 292 "Regexp matching any JavaScript keyword.")
17b5d0f7
CY
293
294(defconst js--basic-type-re
295 (js--regexp-opt-symbol
296 '("boolean" "byte" "char" "double" "float" "int" "long"
297 "short" "void"))
298 "Regular expression matching any predefined type in JavaScript.")
299
300(defconst js--constant-re
301 (js--regexp-opt-symbol '("false" "null" "undefined"
302 "Infinity" "NaN"
303 "true" "arguments" "this"))
304 "Regular expression matching any future reserved words in JavaScript.")
305
306
307(defconst js--font-lock-keywords-1
308 (list
309 "\\_<import\\_>"
310 (list js--function-heading-1-re 1 font-lock-function-name-face)
311 (list js--function-heading-2-re 1 font-lock-function-name-face))
2e330adc 312 "Level one font lock keywords for `js-mode'.")
17b5d0f7
CY
313
314(defconst js--font-lock-keywords-2
315 (append js--font-lock-keywords-1
316 (list (list js--keyword-re 1 font-lock-keyword-face)
317 (list "\\_<for\\_>"
318 "\\s-+\\(each\\)\\_>" nil nil
319 (list 1 'font-lock-keyword-face))
320 (cons js--basic-type-re font-lock-type-face)
321 (cons js--constant-re font-lock-constant-face)))
2e330adc 322 "Level two font lock keywords for `js-mode'.")
17b5d0f7
CY
323
324;; js--pitem is the basic building block of the lexical
325;; database. When one refers to a real part of the buffer, the region
326;; of text to which it refers is split into a conceptual header and
327;; body. Consider the (very short) block described by a hypothetical
328;; js--pitem:
329;;
330;; function foo(a,b,c) { return 42; }
331;; ^ ^ ^
332;; | | |
333;; +- h-begin +- h-end +- b-end
334;;
335;; (Remember that these are buffer positions, and therefore point
336;; between characters, not at them. An arrow drawn to a character
337;; indicates the corresponding position is between that character and
338;; the one immediately preceding it.)
339;;
340;; The header is the region of text [h-begin, h-end], and is
341;; the text needed to unambiguously recognize the start of the
342;; construct. If the entire header is not present, the construct is
343;; not recognized at all. No other pitems may be nested inside the
344;; header.
345;;
346;; The body is the region [h-end, b-end]. It may contain nested
347;; js--pitem instances. The body of a pitem may be empty: in
348;; that case, b-end is equal to header-end.
349;;
350;; The three points obey the following relationship:
351;;
352;; h-begin < h-end <= b-end
353;;
354;; We put a text property in the buffer on the character *before*
355;; h-end, and if we see it, on the character *before* b-end.
356;;
357;; The text property for h-end, js--pstate, is actually a list
358;; of all js--pitem instances open after the marked character.
359;;
360;; The text property for b-end, js--pend, is simply the
361;; js--pitem that ends after the marked character. (Because
362;; pitems always end when the paren-depth drops below a critical
363;; value, and because we can only drop one level per character, only
364;; one pitem may end at a given character.)
365;;
366;; In the structure below, we only store h-begin and (sometimes)
367;; b-end. We can trivially and quickly find h-end by going to h-begin
368;; and searching for an js--pstate text property. Since no other
369;; js--pitem instances can be nested inside the header of a
370;; pitem, the location after the character with this text property
371;; must be h-end.
372;;
373;; js--pitem instances are never modified (with the exception
374;; of the b-end field). Instead, modified copies are added at subseqnce parse points.
375;; (The exception for b-end and its caveats is described below.)
376;;
377
378(defstruct (js--pitem (:type list))
379 ;; IMPORTANT: Do not alter the position of fields within the list.
380 ;; Various bits of code depend on their positions, particularly
381 ;; anything that manipulates the list of children.
382
383 ;; List of children inside this pitem's body
384 (children nil :read-only t)
385
386 ;; When we reach this paren depth after h-end, the pitem ends
387 (paren-depth nil :read-only t)
388
389 ;; Symbol or class-style plist if this is a class
390 (type nil :read-only t)
391
392 ;; See above
393 (h-begin nil :read-only t)
394
395 ;; List of strings giving the parts of the name of this pitem (e.g.,
396 ;; '("MyClass" "myMethod"), or t if this pitem is anonymous
397 (name nil :read-only t)
398
399 ;; THIS FIELD IS MUTATED, and its value is shared by all copies of
400 ;; this pitem: when we copy-and-modify pitem instances, we share
401 ;; their tail structures, so all the copies actually have the same
402 ;; terminating cons cell. We modify that shared cons cell directly.
403 ;;
404 ;; The field value is either a number (buffer location) or nil if
405 ;; unknown.
406 ;;
407 ;; If the field's value is greater than `js--cache-end', the
408 ;; value is stale and must be treated as if it were nil. Conversely,
409 ;; if this field is nil, it is guaranteed that this pitem is open up
410 ;; to at least `js--cache-end'. (This property is handy when
411 ;; computing whether we're inside a given pitem.)
412 ;;
413 (b-end nil))
414
2e330adc 415;; The pitem we start parsing with.
17b5d0f7
CY
416(defconst js--initial-pitem
417 (make-js--pitem
418 :paren-depth most-negative-fixnum
2e330adc 419 :type 'toplevel))
17b5d0f7
CY
420
421;;; User Customization
422
423(defgroup js nil
2e330adc 424 "Customization variables for JavaScript mode."
17b5d0f7
CY
425 :tag "JavaScript"
426 :group 'languages)
427
428(defcustom js-indent-level 4
2e330adc 429 "Number of spaces for each indentation step in `js-mode'."
17b5d0f7
CY
430 :type 'integer
431 :group 'js)
432
433(defcustom js-expr-indent-offset 0
4142607e 434 "Number of additional spaces for indenting continued expressions.
2e330adc 435The value must be no less than minus `js-indent-level'."
17b5d0f7
CY
436 :type 'integer
437 :group 'js)
438
4142607e
NW
439(defcustom js-paren-indent-offset 0
440 "Number of additional spaces for indenting expressions in parentheses.
441The value must be no less than minus `js-indent-level'."
442 :type 'integer
443 :group 'js
444 :version "24.1")
445
446(defcustom js-square-indent-offset 0
447 "Number of additional spaces for indenting expressions in square braces.
448The value must be no less than minus `js-indent-level'."
449 :type 'integer
450 :group 'js
451 :version "24.1")
452
453(defcustom js-curly-indent-offset 0
454 "Number of additional spaces for indenting expressions in curly braces.
455The value must be no less than minus `js-indent-level'."
456 :type 'integer
457 :group 'js
458 :version "24.1")
459
2a793f7f
CY
460(defcustom js-auto-indent-flag t
461 "Whether to automatically indent when typing punctuation characters.
462If non-nil, the characters {}();,: also indent the current line
463in Javascript mode."
464 :type 'boolean
465 :group 'js)
466
17b5d0f7 467(defcustom js-flat-functions nil
2e330adc
CY
468 "Treat nested functions as top-level functions in `js-mode'.
469This applies to function movement, marking, and so on."
17b5d0f7
CY
470 :type 'boolean
471 :group 'js)
472
473(defcustom js-comment-lineup-func #'c-lineup-C-comments
2e330adc 474 "Lineup function for `cc-mode-style', for C comments in `js-mode'."
17b5d0f7
CY
475 :type 'function
476 :group 'js)
477
478(defcustom js-enabled-frameworks js--available-frameworks
2e330adc
CY
479 "Frameworks recognized by `js-mode'.
480To improve performance, you may turn off some frameworks you
481seldom use, either globally or on a per-buffer basis."
17b5d0f7
CY
482 :type (cons 'set (mapcar (lambda (x)
483 (list 'const x))
484 js--available-frameworks))
485 :group 'js)
486
487(defcustom js-js-switch-tabs
488 (and (memq system-type '(darwin)) t)
2e330adc
CY
489 "Whether `js-mode' should display tabs while selecting them.
490This is useful only if the windowing system has a good mechanism
491for preventing Firefox from stealing the keyboard focus."
17b5d0f7
CY
492 :type 'boolean
493 :group 'js)
494
495(defcustom js-js-tmpdir
496 "~/.emacs.d/js/js"
2e330adc 497 "Temporary directory used by `js-mode' to communicate with Mozilla.
943375a6 498This directory must be readable and writable by both Mozilla and Emacs."
17b5d0f7
CY
499 :type 'directory
500 :group 'js)
501
502(defcustom js-js-timeout 5
2e330adc
CY
503 "Reply timeout for executing commands in Mozilla via `js-mode'.
504The value is given in seconds. Increase this value if you are
505getting timeout messages."
17b5d0f7
CY
506 :type 'integer
507 :group 'js)
508
509;;; KeyMap
510
511(defvar js-mode-map
512 (let ((keymap (make-sparse-keymap)))
2a793f7f
CY
513 (mapc (lambda (key)
514 (define-key keymap key #'js-insert-and-indent))
515 '("{" "}" "(" ")" ":" ";" ","))
17b5d0f7
CY
516 (define-key keymap [(control ?c) (meta ?:)] #'js-eval)
517 (define-key keymap [(control ?c) (control ?j)] #'js-set-js-context)
518 (define-key keymap [(control meta ?x)] #'js-eval-defun)
519 (define-key keymap [(meta ?.)] #'js-find-symbol)
17b5d0f7
CY
520 (easy-menu-define nil keymap "Javascript Menu"
521 '("Javascript"
943375a6 522 ["Select New Mozilla Context..." js-set-js-context
17b5d0f7 523 (fboundp #'inferior-moz-process)]
943375a6 524 ["Evaluate Expression in Mozilla Context..." js-eval
17b5d0f7 525 (fboundp #'inferior-moz-process)]
943375a6 526 ["Send Current Function to Mozilla..." js-eval-defun
2e330adc 527 (fboundp #'inferior-moz-process)]))
17b5d0f7 528 keymap)
2e330adc 529 "Keymap for `js-mode'.")
17b5d0f7 530
2a793f7f
CY
531(defun js-insert-and-indent (key)
532 "Run the command bound to KEY, and indent if necessary.
533Indentation does not take place if point is in a string or
534comment."
535 (interactive (list (this-command-keys)))
536 (call-interactively (lookup-key (current-global-map) key))
537 (let ((syntax (save-restriction (widen) (syntax-ppss))))
538 (when (or (and (not (nth 8 syntax))
539 js-auto-indent-flag)
540 (and (nth 4 syntax)
541 (eq (current-column)
542 (1+ (current-indentation)))))
543 (indent-according-to-mode))))
544
545
17b5d0f7
CY
546;;; Syntax table and parsing
547
548(defvar js-mode-syntax-table
549 (let ((table (make-syntax-table)))
550 (c-populate-syntax-table table)
551 (modify-syntax-entry ?$ "_" table)
552 table)
2e330adc 553 "Syntax table for `js-mode'.")
17b5d0f7
CY
554
555(defvar js--quick-match-re nil
2e330adc 556 "Autogenerated regexp used by `js-mode' to match buffer constructs.")
17b5d0f7
CY
557
558(defvar js--quick-match-re-func nil
2e330adc 559 "Autogenerated regexp used by `js-mode' to match constructs and functions.")
17b5d0f7
CY
560
561(make-variable-buffer-local 'js--quick-match-re)
562(make-variable-buffer-local 'js--quick-match-re-func)
563
564(defvar js--cache-end 1
2e330adc 565 "Last valid buffer position for the `js-mode' function cache.")
17b5d0f7
CY
566(make-variable-buffer-local 'js--cache-end)
567
568(defvar js--last-parse-pos nil
2e330adc 569 "Latest parse position reached by `js--ensure-cache'.")
17b5d0f7
CY
570(make-variable-buffer-local 'js--last-parse-pos)
571
572(defvar js--state-at-last-parse-pos nil
2e330adc 573 "Parse state at `js--last-parse-pos'.")
17b5d0f7
CY
574(make-variable-buffer-local 'js--state-at-last-parse-pos)
575
576(defun js--flatten-list (list)
577 (loop for item in list
578 nconc (cond ((consp item)
579 (js--flatten-list item))
17b5d0f7
CY
580 (item (list item)))))
581
582(defun js--maybe-join (prefix separator suffix &rest list)
2e330adc
CY
583 "Helper function for `js--update-quick-match-re'.
584If LIST contains any element that is not nil, return its non-nil
585elements, separated by SEPARATOR, prefixed by PREFIX, and ended
dd4fbf56
JB
586with SUFFIX as with `concat'. Otherwise, if LIST is empty, return
587nil. If any element in LIST is itself a list, flatten that
17b5d0f7
CY
588element."
589 (setq list (js--flatten-list list))
590 (when list
591 (concat prefix (mapconcat #'identity list separator) suffix)))
592
593(defun js--update-quick-match-re ()
2e330adc
CY
594 "Internal function used by `js-mode' for caching buffer constructs.
595This updates `js--quick-match-re', based on the current set of
596enabled frameworks."
17b5d0f7
CY
597 (setq js--quick-match-re
598 (js--maybe-join
599 "^[ \t]*\\(?:" "\\|" "\\)"
600
601 ;; #define mumble
602 "#define[ \t]+[a-zA-Z_]"
603
604 (when (memq 'extjs js-enabled-frameworks)
605 "Ext\\.extend")
606
607 (when (memq 'prototype js-enabled-frameworks)
608 "Object\\.extend")
609
610 ;; var mumble = THING (
611 (js--maybe-join
612 "\\(?:var[ \t]+\\)?[a-zA-Z_$0-9.]+[ \t]*=[ \t]*\\(?:"
613 "\\|"
614 "\\)[ \t]*\("
615
616 (when (memq 'prototype js-enabled-frameworks)
617 "Class\\.create")
618
619 (when (memq 'extjs js-enabled-frameworks)
620 "Ext\\.extend")
621
622 (when (memq 'merrillpress js-enabled-frameworks)
623 "[a-zA-Z_$0-9]+\\.extend\\(?:Final\\)?"))
624
625 (when (memq 'dojo js-enabled-frameworks)
626 "dojo\\.declare[ \t]*\(")
627
628 (when (memq 'mochikit js-enabled-frameworks)
629 "MochiKit\\.Base\\.update[ \t]*\(")
630
631 ;; mumble.prototypeTHING
632 (js--maybe-join
633 "[a-zA-Z_$0-9.]+\\.prototype\\(?:" "\\|" "\\)"
634
635 (when (memq 'javascript js-enabled-frameworks)
636 '( ;; foo.prototype.bar = function(
637 "\\.[a-zA-Z_$0-9]+[ \t]*=[ \t]*function[ \t]*\("
638
639 ;; mumble.prototype = {
640 "[ \t]*=[ \t]*{")))))
641
642 (setq js--quick-match-re-func
643 (concat "function\\|" js--quick-match-re)))
644
645(defun js--forward-text-property (propname)
2e330adc
CY
646 "Move over the next value of PROPNAME in the buffer.
647If found, return that value and leave point after the character
648having that value; otherwise, return nil and leave point at EOB."
17b5d0f7
CY
649 (let ((next-value (get-text-property (point) propname)))
650 (if next-value
651 (forward-char)
652
653 (goto-char (next-single-property-change
654 (point) propname nil (point-max)))
655 (unless (eobp)
656 (setq next-value (get-text-property (point) propname))
657 (forward-char)))
658
659 next-value))
660
661(defun js--backward-text-property (propname)
2e330adc
CY
662 "Move over the previous value of PROPNAME in the buffer.
663If found, return that value and leave point just before the
664character that has that value, otherwise return nil and leave
665point at BOB."
17b5d0f7
CY
666 (unless (bobp)
667 (let ((prev-value (get-text-property (1- (point)) propname)))
668 (if prev-value
669 (backward-char)
670
671 (goto-char (previous-single-property-change
672 (point) propname nil (point-min)))
673
674 (unless (bobp)
675 (backward-char)
676 (setq prev-value (get-text-property (point) propname))))
677
678 prev-value)))
679
680(defsubst js--forward-pstate ()
681 (js--forward-text-property 'js--pstate))
682
683(defsubst js--backward-pstate ()
684 (js--backward-text-property 'js--pstate))
685
686(defun js--pitem-goto-h-end (pitem)
687 (goto-char (js--pitem-h-begin pitem))
688 (js--forward-pstate))
689
690(defun js--re-search-forward-inner (regexp &optional bound count)
2e330adc 691 "Helper function for `js--re-search-forward'."
17b5d0f7
CY
692 (let ((parse)
693 str-terminator
694 (orig-macro-end (save-excursion
695 (when (js--beginning-of-macro)
696 (c-end-of-macro)
697 (point)))))
698 (while (> count 0)
699 (re-search-forward regexp bound)
700 (setq parse (syntax-ppss))
701 (cond ((setq str-terminator (nth 3 parse))
702 (when (eq str-terminator t)
703 (setq str-terminator ?/))
704 (re-search-forward
705 (concat "\\([^\\]\\|^\\)" (string str-terminator))
e180ab9f 706 (point-at-eol) t))
17b5d0f7
CY
707 ((nth 7 parse)
708 (forward-line))
709 ((or (nth 4 parse)
710 (and (eq (char-before) ?\/) (eq (char-after) ?\*)))
711 (re-search-forward "\\*/"))
712 ((and (not (and orig-macro-end
713 (<= (point) orig-macro-end)))
714 (js--beginning-of-macro))
715 (c-end-of-macro))
716 (t
717 (setq count (1- count))))))
718 (point))
719
720
721(defun js--re-search-forward (regexp &optional bound noerror count)
2e330adc
CY
722 "Search forward, ignoring strings, cpp macros, and comments.
723This function invokes `re-search-forward', but treats the buffer
724as if strings, cpp macros, and comments have been removed.
17b5d0f7 725
2e330adc
CY
726If invoked while inside a macro, it treats the contents of the
727macro as normal text."
b073dc4b 728 (unless count (setq count 1))
17b5d0f7 729 (let ((saved-point (point))
b073dc4b
SM
730 (search-fun
731 (cond ((< count 0) (setq count (- count))
732 #'js--re-search-backward-inner)
733 ((> count 0) #'js--re-search-forward-inner)
734 (t #'ignore))))
17b5d0f7 735 (condition-case err
b073dc4b 736 (funcall search-fun regexp bound count)
17b5d0f7
CY
737 (search-failed
738 (goto-char saved-point)
739 (unless noerror
b073dc4b 740 (signal (car err) (cdr err)))))))
17b5d0f7
CY
741
742
743(defun js--re-search-backward-inner (regexp &optional bound count)
744 "Auxiliary function for `js--re-search-backward'."
745 (let ((parse)
746 str-terminator
747 (orig-macro-start
748 (save-excursion
749 (and (js--beginning-of-macro)
750 (point)))))
751 (while (> count 0)
752 (re-search-backward regexp bound)
753 (when (and (> (point) (point-min))
754 (save-excursion (backward-char) (looking-at "/[/*]")))
755 (forward-char))
756 (setq parse (syntax-ppss))
757 (cond ((setq str-terminator (nth 3 parse))
758 (when (eq str-terminator t)
759 (setq str-terminator ?/))
760 (re-search-backward
761 (concat "\\([^\\]\\|^\\)" (string str-terminator))
e180ab9f 762 (point-at-bol) t))
17b5d0f7
CY
763 ((nth 7 parse)
764 (goto-char (nth 8 parse)))
765 ((or (nth 4 parse)
766 (and (eq (char-before) ?/) (eq (char-after) ?*)))
767 (re-search-backward "/\\*"))
768 ((and (not (and orig-macro-start
769 (>= (point) orig-macro-start)))
770 (js--beginning-of-macro)))
771 (t
772 (setq count (1- count))))))
773 (point))
774
775
776(defun js--re-search-backward (regexp &optional bound noerror count)
2e330adc
CY
777 "Search backward, ignoring strings, preprocessor macros, and comments.
778
779This function invokes `re-search-backward' but treats the buffer
780as if strings, preprocessor macros, and comments have been
781removed.
17b5d0f7 782
2e330adc 783If invoked while inside a macro, treat the macro as normal text."
b073dc4b 784 (js--re-search-forward regexp bound noerror (if count (- count) -1)))
17b5d0f7
CY
785
786(defun js--forward-expression ()
2e330adc
CY
787 "Move forward over a whole JavaScript expression.
788This function doesn't move over expressions continued across
789lines."
17b5d0f7
CY
790 (loop
791 ;; non-continued case; simplistic, but good enough?
792 do (loop until (or (eolp)
793 (progn
794 (forward-comment most-positive-fixnum)
795 (memq (char-after) '(?\, ?\; ?\] ?\) ?\}))))
796 do (forward-sexp))
797
798 while (and (eq (char-after) ?\n)
799 (save-excursion
800 (forward-char)
801 (js--continued-expression-p)))))
802
803(defun js--forward-function-decl ()
2e330adc
CY
804 "Move forward over a JavaScript function declaration.
805This puts point at the 'function' keyword.
806
807If this is a syntactically-correct non-expression function,
808return the name of the function, or t if the name could not be
809determined. Otherwise, return nil."
17b5d0f7
CY
810 (assert (looking-at "\\_<function\\_>"))
811 (let ((name t))
812 (forward-word)
813 (forward-comment most-positive-fixnum)
814 (when (looking-at js--name-re)
815 (setq name (match-string-no-properties 0))
816 (goto-char (match-end 0)))
817 (forward-comment most-positive-fixnum)
818 (and (eq (char-after) ?\( )
819 (ignore-errors (forward-list) t)
820 (progn (forward-comment most-positive-fixnum)
821 (and (eq (char-after) ?{)
822 name)))))
823
824(defun js--function-prologue-beginning (&optional pos)
2e330adc
CY
825 "Return the start of the JavaScript function prologue containing POS.
826A function prologue is everything from start of the definition up
dd4fbf56 827to and including the opening brace. POS defaults to point.
2e330adc 828If POS is not in a function prologue, return nil."
17b5d0f7
CY
829 (let (prologue-begin)
830 (save-excursion
831 (if pos
832 (goto-char pos)
833 (setq pos (point)))
834
835 (when (save-excursion
836 (forward-line 0)
837 (or (looking-at js--function-heading-2-re)
838 (looking-at js--function-heading-3-re)))
839
840 (setq prologue-begin (match-beginning 1))
841 (when (<= prologue-begin pos)
842 (goto-char (match-end 0))))
843
844 (skip-syntax-backward "w_")
845 (and (or (looking-at "\\_<function\\_>")
846 (js--re-search-backward "\\_<function\\_>" nil t))
847
848 (save-match-data (goto-char (match-beginning 0))
849 (js--forward-function-decl))
850
851 (<= pos (point))
852 (or prologue-begin (match-beginning 0))))))
853
854(defun js--beginning-of-defun-raw ()
2e330adc
CY
855 "Helper function for `js-beginning-of-defun'.
856Go to previous defun-beginning and return the parse state for it,
857or nil if we went all the way back to bob and don't find
858anything."
17b5d0f7
CY
859 (js--ensure-cache)
860 (let (pstate)
861 (while (and (setq pstate (js--backward-pstate))
862 (not (eq 'function (js--pitem-type (car pstate))))))
863 (and (not (bobp)) pstate)))
864
865(defun js--pstate-is-toplevel-defun (pstate)
2e330adc
CY
866 "Helper function for `js--beginning-of-defun-nested'.
867If PSTATE represents a non-empty top-level defun, return the
868top-most pitem. Otherwise, return nil."
17b5d0f7
CY
869 (loop for pitem in pstate
870 with func-depth = 0
871 with func-pitem
872 if (eq 'function (js--pitem-type pitem))
873 do (incf func-depth)
874 and do (setq func-pitem pitem)
875 finally return (if (eq func-depth 1) func-pitem)))
876
877(defun js--beginning-of-defun-nested ()
2e330adc
CY
878 "Helper function for `js--beginning-of-defun'.
879Return the pitem of the function we went to the beginning of."
17b5d0f7
CY
880 (or
881 ;; Look for the smallest function that encloses point...
882 (loop for pitem in (js--parse-state-at-point)
883 if (and (eq 'function (js--pitem-type pitem))
884 (js--inside-pitem-p pitem))
885 do (goto-char (js--pitem-h-begin pitem))
886 and return pitem)
887
888 ;; ...and if that isn't found, look for the previous top-level
889 ;; defun
890 (loop for pstate = (js--backward-pstate)
891 while pstate
892 if (js--pstate-is-toplevel-defun pstate)
893 do (goto-char (js--pitem-h-begin it))
894 and return it)))
895
896(defun js--beginning-of-defun-flat ()
2e330adc 897 "Helper function for `js-beginning-of-defun'."
17b5d0f7
CY
898 (let ((pstate (js--beginning-of-defun-raw)))
899 (when pstate
900 (goto-char (js--pitem-h-begin (car pstate))))))
901
2e330adc
CY
902(defun js-beginning-of-defun (&optional arg)
903 "Value of `beginning-of-defun-function' for `js-mode'."
17b5d0f7
CY
904 (setq arg (or arg 1))
905 (while (and (not (eobp)) (< arg 0))
906 (incf arg)
907 (when (and (not js-flat-functions)
908 (or (eq (js-syntactic-context) 'function)
909 (js--function-prologue-beginning)))
2e330adc 910 (js-end-of-defun))
17b5d0f7
CY
911
912 (if (js--re-search-forward
913 "\\_<function\\_>" nil t)
914 (goto-char (js--function-prologue-beginning))
915 (goto-char (point-max))))
916
917 (while (> arg 0)
918 (decf arg)
919 ;; If we're just past the end of a function, the user probably wants
920 ;; to go to the beginning of *that* function
921 (when (eq (char-before) ?})
922 (backward-char))
923
924 (let ((prologue-begin (js--function-prologue-beginning)))
925 (cond ((and prologue-begin (< prologue-begin (point)))
926 (goto-char prologue-begin))
927
928 (js-flat-functions
929 (js--beginning-of-defun-flat))
930 (t
931 (js--beginning-of-defun-nested))))))
932
933(defun js--flush-caches (&optional beg ignored)
2e330adc 934 "Flush the `js-mode' syntax cache after position BEG.
dd4fbf56 935BEG defaults to `point-min', meaning to flush the entire cache."
17b5d0f7
CY
936 (interactive)
937 (setq beg (or beg (save-restriction (widen) (point-min))))
938 (setq js--cache-end (min js--cache-end beg)))
939
940(defmacro js--debug (&rest arguments)
941 ;; `(message ,@arguments)
942 )
943
944(defun js--ensure-cache--pop-if-ended (open-items paren-depth)
945 (let ((top-item (car open-items)))
946 (when (<= paren-depth (js--pitem-paren-depth top-item))
947 (assert (not (get-text-property (1- (point)) 'js-pend)))
948 (put-text-property (1- (point)) (point) 'js--pend top-item)
949 (setf (js--pitem-b-end top-item) (point))
950 (setq open-items
951 ;; open-items must contain at least two items for this to
952 ;; work, but because we push a dummy item to start with,
953 ;; that assumption holds.
954 (cons (js--pitem-add-child (second open-items) top-item)
955 (cddr open-items)))))
17b5d0f7
CY
956 open-items)
957
958(defmacro js--ensure-cache--update-parse ()
2e330adc
CY
959 "Helper function for `js--ensure-cache'.
960Update parsing information up to point, referring to parse,
961prev-parse-point, goal-point, and open-items bound lexically in
962the body of `js--ensure-cache'."
17b5d0f7
CY
963 `(progn
964 (setq goal-point (point))
965 (goto-char prev-parse-point)
966 (while (progn
967 (setq open-items (js--ensure-cache--pop-if-ended
968 open-items (car parse)))
969 ;; Make sure parse-partial-sexp doesn't stop because we *entered*
970 ;; the given depth -- i.e., make sure we're deeper than the target
971 ;; depth.
972 (assert (> (nth 0 parse)
973 (js--pitem-paren-depth (car open-items))))
974 (setq parse (parse-partial-sexp
975 prev-parse-point goal-point
976 (js--pitem-paren-depth (car open-items))
977 nil parse))
978
979;; (let ((overlay (make-overlay prev-parse-point (point))))
980;; (overlay-put overlay 'face '(:background "red"))
981;; (unwind-protect
982;; (progn
983;; (js--debug "parsed: %S" parse)
984;; (sit-for 1))
985;; (delete-overlay overlay)))
986
987 (setq prev-parse-point (point))
988 (< (point) goal-point)))
989
990 (setq open-items (js--ensure-cache--pop-if-ended
991 open-items (car parse)))))
992
993(defun js--show-cache-at-point ()
994 (interactive)
995 (require 'pp)
996 (let ((prop (get-text-property (point) 'js--pstate)))
997 (with-output-to-temp-buffer "*Help*"
998 (pp prop))))
999
1000(defun js--split-name (string)
2e330adc 1001 "Split a JavaScript name into its dot-separated parts.
dd4fbf56
JB
1002This also removes any prototype parts from the split name
1003\(unless the name is just \"prototype\" to start with)."
17b5d0f7
CY
1004 (let ((name (save-match-data
1005 (split-string string "\\." t))))
1006 (unless (and (= (length name) 1)
1007 (equal (car name) "prototype"))
1008
1009 (setq name (remove "prototype" name)))))
1010
2e330adc 1011(defvar js--guess-function-name-start nil)
17b5d0f7
CY
1012
1013(defun js--guess-function-name (position)
2e330adc
CY
1014 "Guess the name of the JavaScript function at POSITION.
1015POSITION should be just after the end of the word \"function\".
1016Return the name of the function, or nil if the name could not be
1017guessed.
1018
1019This function clobbers match data. If we find the preamble
1020begins earlier than expected while guessing the function name,
1021set `js--guess-function-name-start' to that position; otherwise,
1022set that variable to nil."
17b5d0f7
CY
1023 (setq js--guess-function-name-start nil)
1024 (save-excursion
1025 (goto-char position)
1026 (forward-line 0)
1027 (cond
1028 ((looking-at js--function-heading-3-re)
1029 (and (eq (match-end 0) position)
1030 (setq js--guess-function-name-start (match-beginning 1))
1031 (match-string-no-properties 1)))
1032
1033 ((looking-at js--function-heading-2-re)
1034 (and (eq (match-end 0) position)
1035 (setq js--guess-function-name-start (match-beginning 1))
1036 (match-string-no-properties 1))))))
1037
1038(defun js--clear-stale-cache ()
1039 ;; Clear any endings that occur after point
1040 (let (end-prop)
1041 (save-excursion
1042 (while (setq end-prop (js--forward-text-property
1043 'js--pend))
1044 (setf (js--pitem-b-end end-prop) nil))))
1045
1046 ;; Remove any cache properties after this point
1047 (remove-text-properties (point) (point-max)
1048 '(js--pstate t js--pend t)))
1049
1050(defun js--ensure-cache (&optional limit)
1051 "Ensures brace cache is valid up to the character before LIMIT.
1052LIMIT defaults to point."
1053 (setq limit (or limit (point)))
1054 (when (< js--cache-end limit)
1055
1056 (c-save-buffer-state
1057 (open-items
1058 orig-match-start
1059 orig-match-end
1060 orig-depth
1061 parse
1062 prev-parse-point
1063 name
1064 case-fold-search
1065 filtered-class-styles
1066 new-item
1067 goal-point
1068 end-prop)
1069
1070 ;; Figure out which class styles we need to look for
1071 (setq filtered-class-styles
1072 (loop for style in js--class-styles
1073 if (memq (plist-get style :framework)
1074 js-enabled-frameworks)
1075 collect style))
1076
1077 (save-excursion
1078 (save-restriction
1079 (widen)
1080
1081 ;; Find last known good position
1082 (goto-char js--cache-end)
1083 (unless (bobp)
1084 (setq open-items (get-text-property
1085 (1- (point)) 'js--pstate))
1086
1087 (unless open-items
1088 (goto-char (previous-single-property-change
1089 (point) 'js--pstate nil (point-min)))
1090
1091 (unless (bobp)
1092 (setq open-items (get-text-property (1- (point))
1093 'js--pstate))
1094 (assert open-items))))
1095
1096 (unless open-items
1097 ;; Make a placeholder for the top-level definition
1098 (setq open-items (list js--initial-pitem)))
1099
1100 (setq parse (syntax-ppss))
1101 (setq prev-parse-point (point))
1102
1103 (js--clear-stale-cache)
1104
1105 (narrow-to-region (point-min) limit)
1106
1107 (loop while (re-search-forward js--quick-match-re-func nil t)
1108 for orig-match-start = (goto-char (match-beginning 0))
1109 for orig-match-end = (match-end 0)
1110 do (js--ensure-cache--update-parse)
1111 for orig-depth = (nth 0 parse)
1112
1113 ;; Each of these conditions should return non-nil if
1114 ;; we should add a new item and leave point at the end
1115 ;; of the new item's header (h-end in the
1116 ;; js--pitem diagram). This point is the one
1117 ;; after the last character we need to unambiguously
1118 ;; detect this construct. If one of these evaluates to
1119 ;; nil, the location of the point is ignored.
1120 if (cond
1121 ;; In comment or string
1122 ((nth 8 parse) nil)
1123
1124 ;; Regular function declaration
1125 ((and (looking-at "\\_<function\\_>")
1126 (setq name (js--forward-function-decl)))
1127
1128 (when (eq name t)
1129 (setq name (js--guess-function-name orig-match-end))
1130 (if name
1131 (when js--guess-function-name-start
1132 (setq orig-match-start
1133 js--guess-function-name-start))
1134
1135 (setq name t)))
1136
1137 (assert (eq (char-after) ?{))
1138 (forward-char)
1139 (make-js--pitem
1140 :paren-depth orig-depth
1141 :h-begin orig-match-start
1142 :type 'function
1143 :name (if (eq name t)
1144 name
1145 (js--split-name name))))
1146
1147 ;; Macro
1148 ((looking-at js--macro-decl-re)
1149
1150 ;; Macros often contain unbalanced parentheses.
1151 ;; Make sure that h-end is at the textual end of
1152 ;; the macro no matter what the parenthesis say.
1153 (c-end-of-macro)
1154 (js--ensure-cache--update-parse)
1155
1156 (make-js--pitem
1157 :paren-depth (nth 0 parse)
1158 :h-begin orig-match-start
1159 :type 'macro
1160 :name (list (match-string-no-properties 1))))
1161
1162 ;; "Prototype function" declaration
1163 ((looking-at js--plain-method-re)
1164 (goto-char (match-beginning 3))
1165 (when (save-match-data
1166 (js--forward-function-decl))
1167 (forward-char)
1168 (make-js--pitem
1169 :paren-depth orig-depth
1170 :h-begin orig-match-start
1171 :type 'function
1172 :name (nconc (js--split-name
1173 (match-string-no-properties 1))
1174 (list (match-string-no-properties 2))))))
1175
1176 ;; Class definition
1177 ((loop with syntactic-context =
1178 (js--syntactic-context-from-pstate open-items)
1179 for class-style in filtered-class-styles
1180 if (and (memq syntactic-context
1181 (plist-get class-style :contexts))
1182 (looking-at (plist-get class-style
1183 :class-decl)))
1184 do (goto-char (match-end 0))
1185 and return
1186 (make-js--pitem
1187 :paren-depth orig-depth
1188 :h-begin orig-match-start
1189 :type class-style
1190 :name (js--split-name
1191 (match-string-no-properties 1))))))
1192
1193 do (js--ensure-cache--update-parse)
1194 and do (push it open-items)
1195 and do (put-text-property
1196 (1- (point)) (point) 'js--pstate open-items)
1197 else do (goto-char orig-match-end))
1198
1199 (goto-char limit)
1200 (js--ensure-cache--update-parse)
1201 (setq js--cache-end limit)
1202 (setq js--last-parse-pos limit)
1203 (setq js--state-at-last-parse-pos open-items)
1204 )))))
1205
1206(defun js--end-of-defun-flat ()
2e330adc 1207 "Helper function for `js-end-of-defun'."
17b5d0f7
CY
1208 (loop while (js--re-search-forward "}" nil t)
1209 do (js--ensure-cache)
1210 if (get-text-property (1- (point)) 'js--pend)
1211 if (eq 'function (js--pitem-type it))
1212 return t
1213 finally do (goto-char (point-max))))
1214
1215(defun js--end-of-defun-nested ()
2e330adc 1216 "Helper function for `js-end-of-defun'."
17b5d0f7
CY
1217 (message "test")
1218 (let* (pitem
1219 (this-end (save-excursion
1220 (and (setq pitem (js--beginning-of-defun-nested))
1221 (js--pitem-goto-h-end pitem)
1222 (progn (backward-char)
1223 (forward-list)
1224 (point)))))
1225 found)
1226
1227 (if (and this-end (< (point) this-end))
1228 ;; We're already inside a function; just go to its end.
1229 (goto-char this-end)
1230
1231 ;; Otherwise, go to the end of the next function...
1232 (while (and (js--re-search-forward "\\_<function\\_>" nil t)
1233 (not (setq found (progn
1234 (goto-char (match-beginning 0))
1235 (js--forward-function-decl))))))
1236
1237 (if found (forward-list)
1238 ;; ... or eob.
1239 (goto-char (point-max))))))
1240
2e330adc
CY
1241(defun js-end-of-defun (&optional arg)
1242 "Value of `end-of-defun-function' for `js-mode'."
17b5d0f7
CY
1243 (setq arg (or arg 1))
1244 (while (and (not (bobp)) (< arg 0))
07eae5c5
GM
1245 (incf arg)
1246 (js-beginning-of-defun)
1247 (js-beginning-of-defun)
1248 (unless (bobp)
1249 (js-end-of-defun)))
17b5d0f7
CY
1250
1251 (while (> arg 0)
1252 (decf arg)
1253 ;; look for function backward. if we're inside it, go to that
1254 ;; function's end. otherwise, search for the next function's end and
1255 ;; go there
1256 (if js-flat-functions
1257 (js--end-of-defun-flat)
1258
1259 ;; if we're doing nested functions, see whether we're in the
1260 ;; prologue. If we are, go to the end of the function; otherwise,
1261 ;; call js--end-of-defun-nested to do the real work
1262 (let ((prologue-begin (js--function-prologue-beginning)))
1263 (cond ((and prologue-begin (<= prologue-begin (point)))
1264 (goto-char prologue-begin)
1265 (re-search-forward "\\_<function")
1266 (goto-char (match-beginning 0))
1267 (js--forward-function-decl)
1268 (forward-list))
1269
1270 (t (js--end-of-defun-nested)))))))
1271
1272(defun js--beginning-of-macro (&optional lim)
1273 (let ((here (point)))
1274 (save-restriction
1275 (if lim (narrow-to-region lim (point-max)))
1276 (beginning-of-line)
1277 (while (eq (char-before (1- (point))) ?\\)
1278 (forward-line -1))
1279 (back-to-indentation)
1280 (if (and (<= (point) here)
1281 (looking-at js--opt-cpp-start))
1282 t
1283 (goto-char here)
1284 nil))))
1285
1286(defun js--backward-syntactic-ws (&optional lim)
2e330adc 1287 "Simple implementation of `c-backward-syntactic-ws' for `js-mode'."
17b5d0f7
CY
1288 (save-restriction
1289 (when lim (narrow-to-region lim (point-max)))
1290
1291 (let ((in-macro (save-excursion (js--beginning-of-macro)))
1292 (pos (point)))
1293
1294 (while (progn (unless in-macro (js--beginning-of-macro))
1295 (forward-comment most-negative-fixnum)
1296 (/= (point)
1297 (prog1
1298 pos
1299 (setq pos (point)))))))))
1300
1301(defun js--forward-syntactic-ws (&optional lim)
2e330adc 1302 "Simple implementation of `c-forward-syntactic-ws' for `js-mode'."
17b5d0f7
CY
1303 (save-restriction
1304 (when lim (narrow-to-region (point-min) lim))
1305 (let ((pos (point)))
1306 (while (progn
1307 (forward-comment most-positive-fixnum)
1308 (when (eq (char-after) ?#)
1309 (c-end-of-macro))
1310 (/= (point)
1311 (prog1
1312 pos
1313 (setq pos (point)))))))))
1314
2e330adc 1315;; Like (up-list -1), but only considers lists that end nearby"
17b5d0f7 1316(defun js--up-nearby-list ()
17b5d0f7
CY
1317 (save-restriction
1318 ;; Look at a very small region so our compuation time doesn't
1319 ;; explode in pathological cases.
1320 (narrow-to-region (max (point-min) (- (point) 500)) (point))
1321 (up-list -1)))
1322
1323(defun js--inside-param-list-p ()
2e330adc 1324 "Return non-nil iff point is in a function parameter list."
17b5d0f7
CY
1325 (ignore-errors
1326 (save-excursion
1327 (js--up-nearby-list)
1328 (and (looking-at "(")
1329 (progn (forward-symbol -1)
1330 (or (looking-at "function")
1331 (progn (forward-symbol -1)
1332 (looking-at "function"))))))))
1333
1334(defun js--inside-dojo-class-list-p ()
2e330adc 1335 "Return non-nil iff point is in a Dojo multiple-inheritance class block."
17b5d0f7
CY
1336 (ignore-errors
1337 (save-excursion
1338 (js--up-nearby-list)
1339 (let ((list-begin (point)))
1340 (forward-line 0)
1341 (and (looking-at js--dojo-class-decl-re)
1342 (goto-char (match-end 0))
1343 (looking-at "\"\\s-*,\\s-*\\[")
1344 (eq (match-end 0) (1+ list-begin)))))))
1345
1346(defun js--syntax-begin-function ()
1347 (when (< js--cache-end (point))
1348 (goto-char (max (point-min) js--cache-end)))
1349
1350 (let ((pitem))
1351 (while (and (setq pitem (car (js--backward-pstate)))
1352 (not (eq 0 (js--pitem-paren-depth pitem)))))
1353
1354 (when pitem
1355 (goto-char (js--pitem-h-begin pitem )))))
1356
1357;;; Font Lock
1358(defun js--make-framework-matcher (framework &rest regexps)
2e330adc
CY
1359 "Helper function for building `js--font-lock-keywords'.
1360Create a byte-compiled function for matching a concatenation of
1361REGEXPS, but only if FRAMEWORK is in `js-enabled-frameworks'."
17b5d0f7
CY
1362 (setq regexps (apply #'concat regexps))
1363 (byte-compile
1364 `(lambda (limit)
1365 (when (memq (quote ,framework) js-enabled-frameworks)
1366 (re-search-forward ,regexps limit t)))))
1367
1368(defvar js--tmp-location nil)
1369(make-variable-buffer-local 'js--tmp-location)
1370
1371(defun js--forward-destructuring-spec (&optional func)
2e330adc
CY
1372 "Move forward over a JavaScript destructuring spec.
1373If FUNC is supplied, call it with no arguments before every
1374variable name in the spec. Return true iff this was actually a
1375spec. FUNC must preserve the match data."
17b5d0f7
CY
1376 (case (char-after)
1377 (?\[
1378 (forward-char)
1379 (while
1380 (progn
1381 (forward-comment most-positive-fixnum)
1382 (cond ((memq (char-after) '(?\[ ?\{))
1383 (js--forward-destructuring-spec func))
1384
1385 ((eq (char-after) ?,)
1386 (forward-char)
1387 t)
1388
1389 ((looking-at js--name-re)
1390 (and func (funcall func))
1391 (goto-char (match-end 0))
1392 t))))
1393 (when (eq (char-after) ?\])
1394 (forward-char)
1395 t))
1396
1397 (?\{
1398 (forward-char)
1399 (forward-comment most-positive-fixnum)
1400 (while
1401 (when (looking-at js--objfield-re)
1402 (goto-char (match-end 0))
1403 (forward-comment most-positive-fixnum)
1404 (and (cond ((memq (char-after) '(?\[ ?\{))
1405 (js--forward-destructuring-spec func))
1406 ((looking-at js--name-re)
1407 (and func (funcall func))
1408 (goto-char (match-end 0))
1409 t))
1410 (progn (forward-comment most-positive-fixnum)
1411 (when (eq (char-after) ?\,)
1412 (forward-char)
1413 (forward-comment most-positive-fixnum)
1414 t)))))
1415 (when (eq (char-after) ?\})
1416 (forward-char)
1417 t))))
1418
1419(defun js--variable-decl-matcher (limit)
2e330adc
CY
1420 "Font-lock matcher for variable names in a variable declaration.
1421This is a cc-mode-style matcher that *always* fails, from the
dd4fbf56
JB
1422point of view of font-lock. It applies highlighting directly with
1423`font-lock-apply-highlight'."
17b5d0f7
CY
1424 (condition-case nil
1425 (save-restriction
1426 (narrow-to-region (point-min) limit)
1427
1428 (let ((first t))
1429 (forward-comment most-positive-fixnum)
1430 (while
1431 (and (or first
1432 (when (eq (char-after) ?,)
1433 (forward-char)
1434 (forward-comment most-positive-fixnum)
1435 t))
1436 (cond ((looking-at js--name-re)
1437 (font-lock-apply-highlight
1438 '(0 font-lock-variable-name-face))
1439 (goto-char (match-end 0)))
1440
1441 ((save-excursion
1442 (js--forward-destructuring-spec))
1443
1444 (js--forward-destructuring-spec
1445 (lambda ()
1446 (font-lock-apply-highlight
1447 '(0 font-lock-variable-name-face)))))))
1448
1449 (forward-comment most-positive-fixnum)
1450 (when (eq (char-after) ?=)
1451 (forward-char)
1452 (js--forward-expression)
1453 (forward-comment most-positive-fixnum))
1454
1455 (setq first nil))))
1456
1457 ;; Conditions to handle
1458 (scan-error nil)
1459 (end-of-buffer nil))
1460
1461 ;; Matcher always "fails"
1462 nil)
1463
1464(defconst js--font-lock-keywords-3
1465 `(
1466 ;; This goes before keywords-2 so it gets used preferentially
1467 ;; instead of the keywords in keywords-2. Don't use override
1468 ;; because that will override syntactic fontification too, which
1469 ;; will fontify commented-out directives as if they weren't
1470 ;; commented out.
1471 ,@cpp-font-lock-keywords ; from font-lock.el
1472
1473 ,@js--font-lock-keywords-2
1474
1475 ("\\.\\(prototype\\)\\_>"
1476 (1 font-lock-constant-face))
1477
1478 ;; Highlights class being declared, in parts
1479 (js--class-decl-matcher
1480 ,(concat "\\(" js--name-re "\\)\\(?:\\.\\|.*$\\)")
1481 (goto-char (match-beginning 1))
1482 nil
1483 (1 font-lock-type-face))
1484
1485 ;; Highlights parent class, in parts, if available
1486 (js--class-decl-matcher
1487 ,(concat "\\(" js--name-re "\\)\\(?:\\.\\|.*$\\)")
1488 (if (match-beginning 2)
1489 (progn
1490 (setq js--tmp-location (match-end 2))
1491 (goto-char js--tmp-location)
1492 (insert "=")
1493 (goto-char (match-beginning 2)))
1494 (setq js--tmp-location nil)
1495 (goto-char (point-at-eol)))
1496 (when js--tmp-location
1497 (save-excursion
1498 (goto-char js--tmp-location)
1499 (delete-char 1)))
1500 (1 font-lock-type-face))
1501
1502 ;; Highlights parent class
1503 (js--class-decl-matcher
1504 (2 font-lock-type-face nil t))
1505
1506 ;; Dojo needs its own matcher to override the string highlighting
1507 (,(js--make-framework-matcher
1508 'dojo
1509 "^\\s-*dojo\\.declare\\s-*(\""
1510 "\\(" js--dotted-name-re "\\)"
1511 "\\(?:\"\\s-*,\\s-*\\(" js--dotted-name-re "\\)\\)?")
1512 (1 font-lock-type-face t)
1513 (2 font-lock-type-face nil t))
1514
1515 ;; Match Dojo base classes. Of course Mojo has to be different
1516 ;; from everything else under the sun...
1517 (,(js--make-framework-matcher
1518 'dojo
1519 "^\\s-*dojo\\.declare\\s-*(\""
1520 "\\(" js--dotted-name-re "\\)\"\\s-*,\\s-*\\[")
1521 ,(concat "[[,]\\s-*\\(" js--dotted-name-re "\\)\\s-*"
1522 "\\(?:\\].*$\\)?")
1523 (backward-char)
1524 (end-of-line)
1525 (1 font-lock-type-face))
1526
1527 ;; continued Dojo base-class list
1528 (,(js--make-framework-matcher
1529 'dojo
1530 "^\\s-*" js--dotted-name-re "\\s-*[],]")
1531 ,(concat "\\(" js--dotted-name-re "\\)"
1532 "\\s-*\\(?:\\].*$\\)?")
1533 (if (save-excursion (backward-char)
1534 (js--inside-dojo-class-list-p))
1535 (forward-symbol -1)
1536 (end-of-line))
1537 (end-of-line)
1538 (1 font-lock-type-face))
1539
1540 ;; variable declarations
1541 ,(list
1542 (concat "\\_<\\(const\\|var\\|let\\)\\_>\\|" js--basic-type-re)
1543 (list #'js--variable-decl-matcher nil nil nil))
1544
1545 ;; class instantiation
1546 ,(list
1547 (concat "\\_<new\\_>\\s-+\\(" js--dotted-name-re "\\)")
1548 (list 1 'font-lock-type-face))
1549
1550 ;; instanceof
1551 ,(list
1552 (concat "\\_<instanceof\\_>\\s-+\\(" js--dotted-name-re "\\)")
1553 (list 1 'font-lock-type-face))
1554
1555 ;; formal parameters
1556 ,(list
1557 (concat
1558 "\\_<function\\_>\\(\\s-+" js--name-re "\\)?\\s-*(\\s-*"
1559 js--name-start-re)
1560 (list (concat "\\(" js--name-re "\\)\\(\\s-*).*\\)?")
1561 '(backward-char)
1562 '(end-of-line)
1563 '(1 font-lock-variable-name-face)))
1564
1565 ;; continued formal parameter list
1566 ,(list
1567 (concat
1568 "^\\s-*" js--name-re "\\s-*[,)]")
1569 (list js--name-re
1570 '(if (save-excursion (backward-char)
1571 (js--inside-param-list-p))
1572 (forward-symbol -1)
1573 (end-of-line))
1574 '(end-of-line)
1575 '(0 font-lock-variable-name-face))))
2e330adc 1576 "Level three font lock for `js-mode'.")
17b5d0f7
CY
1577
1578(defun js--inside-pitem-p (pitem)
dd4fbf56 1579 "Return whether point is inside the given pitem's header or body."
17b5d0f7
CY
1580 (js--ensure-cache)
1581 (assert (js--pitem-h-begin pitem))
1582 (assert (js--pitem-paren-depth pitem))
1583
1584 (and (> (point) (js--pitem-h-begin pitem))
1585 (or (null (js--pitem-b-end pitem))
1586 (> (js--pitem-b-end pitem) (point)))))
1587
1588(defun js--parse-state-at-point ()
2e330adc
CY
1589 "Parse the JavaScript program state at point.
1590Return a list of `js--pitem' instances that apply to point, most
dd4fbf56 1591specific first. In the worst case, the current toplevel instance
2e330adc 1592will be returned."
17b5d0f7
CY
1593 (save-excursion
1594 (save-restriction
1595 (widen)
1596 (js--ensure-cache)
1597 (let* ((bound (if (eobp) (point) (1+ (point))))
1598 (pstate (or (save-excursion
1599 (js--backward-pstate))
1600 (list js--initial-pitem))))
1601
1602 ;; Loop until we either hit a pitem at BOB or pitem ends after
1603 ;; point (or at point if we're at eob)
1604 (loop for pitem = (car pstate)
1605 until (or (eq (js--pitem-type pitem)
1606 'toplevel)
1607 (js--inside-pitem-p pitem))
1608 do (pop pstate))
1609
1610 pstate))))
1611
1612(defun js--syntactic-context-from-pstate (pstate)
2e330adc 1613 "Return the JavaScript syntactic context corresponding to PSTATE."
17b5d0f7
CY
1614 (let ((type (js--pitem-type (car pstate))))
1615 (cond ((memq type '(function macro))
1616 type)
17b5d0f7
CY
1617 ((consp type)
1618 'class)
17b5d0f7
CY
1619 (t 'toplevel))))
1620
1621(defun js-syntactic-context ()
2e330adc
CY
1622 "Return the JavaScript syntactic context at point.
1623When called interatively, also display a message with that
1624context."
17b5d0f7
CY
1625 (interactive)
1626 (let* ((syntactic-context (js--syntactic-context-from-pstate
1627 (js--parse-state-at-point))))
1628
32226619 1629 (when (called-interactively-p 'interactive)
17b5d0f7
CY
1630 (message "Syntactic context: %s" syntactic-context))
1631
1632 syntactic-context))
1633
1634(defun js--class-decl-matcher (limit)
2e330adc
CY
1635 "Font lock function used by `js-mode'.
1636This performs fontification according to `js--class-styles'."
17b5d0f7
CY
1637 (loop initially (js--ensure-cache limit)
1638 while (re-search-forward js--quick-match-re limit t)
1639 for orig-end = (match-end 0)
1640 do (goto-char (match-beginning 0))
1641 if (loop for style in js--class-styles
1642 for decl-re = (plist-get style :class-decl)
1643 if (and (memq (plist-get style :framework)
1644 js-enabled-frameworks)
1645 (memq (js-syntactic-context)
1646 (plist-get style :contexts))
1647 decl-re
1648 (looking-at decl-re))
1649 do (goto-char (match-end 0))
1650 and return t)
1651 return t
1652 else do (goto-char orig-end)))
1653
1654(defconst js--font-lock-keywords
1655 '(js--font-lock-keywords-3 js--font-lock-keywords-1
1656 js--font-lock-keywords-2
1657 js--font-lock-keywords-3)
2e330adc 1658 "Font lock keywords for `js-mode'. See `font-lock-keywords'.")
17b5d0f7
CY
1659
1660;; XXX: Javascript can continue a regexp literal across lines so long
1661;; as the newline is escaped with \. Account for that in the regexp
1662;; below.
cf38dd42
SM
1663(eval-and-compile
1664 (defconst js--regexp-literal
17b5d0f7 1665 "[=(,:]\\(?:\\s-\\|\n\\)*\\(/\\)\\(?:\\\\/\\|[^/*]\\)\\(?:\\\\/\\|[^/]\\)*\\(/\\)"
2e330adc
CY
1666 "Regexp matching a JavaScript regular expression literal.
1667Match groups 1 and 2 are the characters forming the beginning and
cf38dd42
SM
1668end of the literal."))
1669
17b5d0f7 1670
cf38dd42
SM
1671(defconst js-syntax-propertize-function
1672 (syntax-propertize-rules
1673 ;; We want to match regular expressions only at the beginning of
1674 ;; expressions.
1675 (js--regexp-literal (1 "\"") (2 "\""))))
17b5d0f7
CY
1676
1677;;; Indentation
1678
1679(defconst js--possibly-braceless-keyword-re
1680 (js--regexp-opt-symbol
1681 '("catch" "do" "else" "finally" "for" "if" "try" "while" "with"
1682 "each"))
2e330adc 1683 "Regexp matching keywords optionally followed by an opening brace.")
17b5d0f7
CY
1684
1685(defconst js--indent-operator-re
1686 (concat "[-+*/%<>=&^|?:.]\\([^-+*/]\\|$\\)\\|"
1687 (js--regexp-opt-symbol '("in" "instanceof")))
2e330adc 1688 "Regexp matching operators that affect indentation of continued expressions.")
17b5d0f7
CY
1689
1690
1691(defun js--looking-at-operator-p ()
2e330adc 1692 "Return non-nil if point is on a JavaScript operator, other than a comma."
17b5d0f7
CY
1693 (save-match-data
1694 (and (looking-at js--indent-operator-re)
1695 (or (not (looking-at ":"))
1696 (save-excursion
1697 (and (js--re-search-backward "[?:{]\\|\\_<case\\_>" nil t)
1698 (looking-at "?")))))))
1699
1700
1701(defun js--continued-expression-p ()
2e330adc 1702 "Return non-nil if the current line continues an expression."
17b5d0f7
CY
1703 (save-excursion
1704 (back-to-indentation)
1705 (or (js--looking-at-operator-p)
1706 (and (js--re-search-backward "\n" nil t)
1707 (progn
1708 (skip-chars-backward " \t")
1709 (or (bobp) (backward-char))
1710 (and (> (point) (point-min))
1711 (save-excursion (backward-char) (not (looking-at "[/*]/")))
1712 (js--looking-at-operator-p)
1713 (and (progn (backward-char)
1714 (not (looking-at "++\\|--\\|/[/*]"))))))))))
1715
1716
1717(defun js--end-of-do-while-loop-p ()
2e330adc
CY
1718 "Return non-nil if point is on the \"while\" of a do-while statement.
1719Otherwise, return nil. A braceless do-while statement spanning
1720several lines requires that the start of the loop is indented to
1721the same column as the current line."
17b5d0f7
CY
1722 (interactive)
1723 (save-excursion
1724 (save-match-data
1725 (when (looking-at "\\s-*\\_<while\\_>")
1726 (if (save-excursion
1727 (skip-chars-backward "[ \t\n]*}")
1728 (looking-at "[ \t\n]*}"))
1729 (save-excursion
1730 (backward-list) (forward-symbol -1) (looking-at "\\_<do\\_>"))
1731 (js--re-search-backward "\\_<do\\_>" (point-at-bol) t)
1732 (or (looking-at "\\_<do\\_>")
1733 (let ((saved-indent (current-indentation)))
1734 (while (and (js--re-search-backward "^\\s-*\\_<" nil t)
1735 (/= (current-indentation) saved-indent)))
1736 (and (looking-at "\\s-*\\_<do\\_>")
1737 (not (js--re-search-forward
1738 "\\_<while\\_>" (point-at-eol) t))
1739 (= (current-indentation) saved-indent)))))))))
1740
1741
1742(defun js--ctrl-statement-indentation ()
2e330adc
CY
1743 "Helper function for `js--proper-indentation'.
1744Return the proper indentation of the current line if it starts
1745the body of a control statement without braces; otherwise, return
1746nil."
17b5d0f7
CY
1747 (save-excursion
1748 (back-to-indentation)
1749 (when (save-excursion
1750 (and (not (eq (point-at-bol) (point-min)))
1751 (not (looking-at "[{]"))
1752 (progn
1753 (js--re-search-backward "[[:graph:]]" nil t)
1754 (or (eobp) (forward-char))
1755 (when (= (char-before) ?\)) (backward-list))
1756 (skip-syntax-backward " ")
1757 (skip-syntax-backward "w_")
1758 (looking-at js--possibly-braceless-keyword-re))
1759 (not (js--end-of-do-while-loop-p))))
1760 (save-excursion
1761 (goto-char (match-beginning 0))
1762 (+ (current-indentation) js-indent-level)))))
1763
1764(defun js--get-c-offset (symbol anchor)
1765 (let ((c-offsets-alist
1766 (list (cons 'c js-comment-lineup-func))))
1767 (c-get-syntactic-indentation (list (cons symbol anchor)))))
1768
1769(defun js--proper-indentation (parse-status)
1770 "Return the proper indentation for the current line."
1771 (save-excursion
1772 (back-to-indentation)
1773 (cond ((nth 4 parse-status)
1774 (js--get-c-offset 'c (nth 8 parse-status)))
1775 ((nth 8 parse-status) 0) ; inside string
1776 ((js--ctrl-statement-indentation))
1777 ((eq (char-after) ?#) 0)
1778 ((save-excursion (js--beginning-of-macro)) 4)
1779 ((nth 1 parse-status)
4142607e
NW
1780 ;; A single closing paren/bracket should be indented at the
1781 ;; same level as the opening statement. Same goes for
1782 ;; "case" and "default".
17b5d0f7
CY
1783 (let ((same-indent-p (looking-at
1784 "[]})]\\|\\_<case\\_>\\|\\_<default\\_>"))
1785 (continued-expr-p (js--continued-expression-p)))
4142607e 1786 (goto-char (nth 1 parse-status)) ; go to the opening char
17b5d0f7 1787 (if (looking-at "[({[]\\s-*\\(/[/*]\\|$\\)")
4142607e 1788 (progn ; nothing following the opening paren/bracket
17b5d0f7 1789 (skip-syntax-backward " ")
4142607e 1790 (when (eq (char-before) ?\)) (backward-list))
17b5d0f7
CY
1791 (back-to-indentation)
1792 (cond (same-indent-p
1793 (current-column))
1794 (continued-expr-p
1795 (+ (current-column) (* 2 js-indent-level)
1796 js-expr-indent-offset))
1797 (t
4142607e
NW
1798 (+ (current-column) js-indent-level
1799 (case (char-after (nth 1 parse-status))
1800 (?\( js-paren-indent-offset)
1801 (?\[ js-square-indent-offset)
1802 (?\{ js-curly-indent-offset))))))
1803 ;; If there is something following the opening
1804 ;; paren/bracket, everything else should be indented at
1805 ;; the same level.
17b5d0f7
CY
1806 (unless same-indent-p
1807 (forward-char)
1808 (skip-chars-forward " \t"))
1809 (current-column))))
1810
1811 ((js--continued-expression-p)
1812 (+ js-indent-level js-expr-indent-offset))
1813 (t 0))))
1814
1815(defun js-indent-line ()
2e330adc 1816 "Indent the current line as JavaScript."
17b5d0f7
CY
1817 (interactive)
1818 (save-restriction
1819 (widen)
1820 (let* ((parse-status
1821 (save-excursion (syntax-ppss (point-at-bol))))
1822 (offset (- (current-column) (current-indentation))))
17b5d0f7
CY
1823 (indent-line-to (js--proper-indentation parse-status))
1824 (when (> offset 0) (forward-char offset)))))
1825
1826;;; Filling
1827
1828(defun js-c-fill-paragraph (&optional justify)
2e330adc 1829 "Fill the paragraph with `c-fill-paragraph'."
17b5d0f7 1830 (interactive "*P")
17b5d0f7
CY
1831 (flet ((c-forward-sws
1832 (&optional limit)
1833 (js--forward-syntactic-ws limit))
17b5d0f7
CY
1834 (c-backward-sws
1835 (&optional limit)
1836 (js--backward-syntactic-ws limit))
17b5d0f7
CY
1837 (c-beginning-of-macro
1838 (&optional limit)
1839 (js--beginning-of-macro limit)))
17b5d0f7
CY
1840 (let ((fill-paragraph-function 'c-fill-paragraph))
1841 (c-fill-paragraph justify))))
1842
1843;;; Type database and Imenu
1844
1845;; We maintain a cache of semantic information, i.e., the classes and
1846;; functions we've encountered so far. In order to avoid having to
1847;; re-parse the buffer on every change, we cache the parse state at
1848;; each interesting point in the buffer. Each parse state is a
1849;; modified copy of the previous one, or in the case of the first
1850;; parse state, the empty state.
1851;;
1852;; The parse state itself is just a stack of js--pitem
1853;; instances. It starts off containing one element that is never
1854;; closed, that is initially js--initial-pitem.
1855;;
1856
1857
1858(defun js--pitem-format (pitem)
1859 (let ((name (js--pitem-name pitem))
1860 (type (js--pitem-type pitem)))
1861
1862 (format "name:%S type:%S"
1863 name
1864 (if (atom type)
1865 type
1866 (plist-get type :name)))))
1867
1868(defun js--make-merged-item (item child name-parts)
2e330adc
CY
1869 "Helper function for `js--splice-into-items'.
1870Return a new item that is the result of merging CHILD into
dd4fbf56
JB
1871ITEM. NAME-PARTS is a list of parts of the name of CHILD
1872that we haven't consumed yet."
17b5d0f7
CY
1873 (js--debug "js--make-merged-item: {%s} into {%s}"
1874 (js--pitem-format child)
1875 (js--pitem-format item))
1876
1877 ;; If the item we're merging into isn't a class, make it into one
1878 (unless (consp (js--pitem-type item))
1879 (js--debug "js--make-merged-item: changing dest into class")
1880 (setq item (make-js--pitem
1881 :children (list item)
1882
1883 ;; Use the child's class-style if it's available
1884 :type (if (atom (js--pitem-type child))
1885 js--dummy-class-style
1886 (js--pitem-type child))
1887
1888 :name (js--pitem-strname item))))
1889
1890 ;; Now we can merge either a function or a class into a class
1891 (cons (cond
1892 ((cdr name-parts)
1893 (js--debug "js--make-merged-item: recursing")
1894 ;; if we have more name-parts to go before we get to the
1895 ;; bottom of the class hierarchy, call the merger
1896 ;; recursively
1897 (js--splice-into-items (car item) child
1898 (cdr name-parts)))
1899
1900 ((atom (js--pitem-type child))
1901 (js--debug "js--make-merged-item: straight merge")
1902 ;; Not merging a class, but something else, so just prepend
1903 ;; it
1904 (cons child (car item)))
1905
1906 (t
1907 ;; Otherwise, merge the new child's items into those
1908 ;; of the new class
1909 (js--debug "js--make-merged-item: merging class contents")
1910 (append (car child) (car item))))
1911 (cdr item)))
1912
1913(defun js--pitem-strname (pitem)
2e330adc 1914 "Last part of the name of PITEM, as a string or symbol."
17b5d0f7
CY
1915 (let ((name (js--pitem-name pitem)))
1916 (if (consp name)
1917 (car (last name))
1918 name)))
1919
1920(defun js--splice-into-items (items child name-parts)
2e330adc 1921 "Splice CHILD into the `js--pitem' ITEMS at NAME-PARTS.
dd4fbf56
JB
1922If a class doesn't exist in the tree, create it. Return
1923the new items list. NAME-PARTS is a list of strings given
1924the broken-down class name of the item to insert."
17b5d0f7
CY
1925
1926 (let ((top-name (car name-parts))
1927 (item-ptr items)
1928 new-items last-new-item new-cons item)
1929
1930 (js--debug "js--splice-into-items: name-parts: %S items:%S"
1931 name-parts
1932 (mapcar #'js--pitem-name items))
1933
1934 (assert (stringp top-name))
1935 (assert (> (length top-name) 0))
1936
1937 ;; If top-name isn't found in items, then we build a copy of items
1938 ;; and throw it away. But that's okay, since most of the time, we
1939 ;; *will* find an instance.
1940
1941 (while (and item-ptr
1942 (cond ((equal (js--pitem-strname (car item-ptr)) top-name)
1943 ;; Okay, we found an entry with the right name. Splice
1944 ;; the merged item into the list...
1945 (setq new-cons (cons (js--make-merged-item
1946 (car item-ptr) child
1947 name-parts)
1948 (cdr item-ptr)))
1949
1950 (if last-new-item
1951 (setcdr last-new-item new-cons)
1952 (setq new-items new-cons))
1953
1954 ;; ...and terminate the loop
1955 nil)
1956
1957 (t
1958 ;; Otherwise, copy the current cons and move onto the
1959 ;; text. This is tricky; we keep track of the tail of
1960 ;; the list that begins with new-items in
1961 ;; last-new-item.
1962 (setq new-cons (cons (car item-ptr) nil))
1963 (if last-new-item
1964 (setcdr last-new-item new-cons)
1965 (setq new-items new-cons))
1966 (setq last-new-item new-cons)
1967
1968 ;; Go to the next cell in items
1969 (setq item-ptr (cdr item-ptr))))))
1970
1971 (if item-ptr
1972 ;; Yay! We stopped because we found something, not because
1973 ;; we ran out of items to search. Just return the new
1974 ;; list.
1975 (progn
1976 (js--debug "search succeeded: %S" name-parts)
1977 new-items)
1978
1979 ;; We didn't find anything. If the child is a class and we don't
1980 ;; have any classes to drill down into, just push that class;
1981 ;; otherwise, make a fake class and carry on.
1982 (js--debug "search failed: %S" name-parts)
1983 (cons (if (cdr name-parts)
1984 ;; We have name-parts left to process. Make a fake
1985 ;; class for this particular part...
1986 (make-js--pitem
1987 ;; ...and recursively digest the rest of the name
1988 :children (js--splice-into-items
1989 nil child (cdr name-parts))
1990 :type js--dummy-class-style
1991 :name top-name)
1992
1993 ;; Otherwise, this is the only name we have, so stick
1994 ;; the item on the front of the list
1995 child)
1996 items))))
1997
1998(defun js--pitem-add-child (pitem child)
2e330adc 1999 "Copy `js--pitem' PITEM, and push CHILD onto its list of children."
17b5d0f7
CY
2000 (assert (integerp (js--pitem-h-begin child)))
2001 (assert (if (consp (js--pitem-name child))
2002 (loop for part in (js--pitem-name child)
2003 always (stringp part))
2004 t))
2005
2006 ;; This trick works because we know (based on our defstructs) that
2007 ;; the child list is always the first element, and so the second
2008 ;; element and beyond can be shared when we make our "copy".
2009 (cons
2010
2011 (let ((name (js--pitem-name child))
2012 (type (js--pitem-type child)))
2013
2014 (cond ((cdr-safe name) ; true if a list of at least two elements
2015 ;; Use slow path because we need class lookup
2016 (js--splice-into-items (car pitem) child name))
2017
2018 ((and (consp type)
2019 (plist-get type :prototype))
2020
2021 ;; Use slow path because we need class merging. We know
2022 ;; name is a list here because down in
2023 ;; `js--ensure-cache', we made sure to only add
2024 ;; class entries with lists for :name
2025 (assert (consp name))
2026 (js--splice-into-items (car pitem) child name))
2027
2028 (t
2029 ;; Fast path
2030 (cons child (car pitem)))))
2031
2032 (cdr pitem)))
2033
2034(defun js--maybe-make-marker (location)
2e330adc 2035 "Return a marker for LOCATION if `imenu-use-markers' is non-nil."
17b5d0f7
CY
2036 (if imenu-use-markers
2037 (set-marker (make-marker) location)
2038 location))
2039
2040(defun js--pitems-to-imenu (pitems unknown-ctr)
2e330adc 2041 "Convert PITEMS, a list of `js--pitem' structures, to imenu format."
17b5d0f7
CY
2042
2043 (let (imenu-items pitem pitem-type pitem-name subitems)
2044
2045 (while (setq pitem (pop pitems))
2046 (setq pitem-type (js--pitem-type pitem))
2047 (setq pitem-name (js--pitem-strname pitem))
2048 (when (eq pitem-name t)
2049 (setq pitem-name (format "[unknown %s]"
2050 (incf (car unknown-ctr)))))
2051
2052 (cond
2053 ((memq pitem-type '(function macro))
2054 (assert (integerp (js--pitem-h-begin pitem)))
2055 (push (cons pitem-name
2056 (js--maybe-make-marker
2057 (js--pitem-h-begin pitem)))
2058 imenu-items))
2059
2060 ((consp pitem-type) ; class definition
2061 (setq subitems (js--pitems-to-imenu
2062 (js--pitem-children pitem)
2063 unknown-ctr))
2064 (cond (subitems
2065 (push (cons pitem-name subitems)
2066 imenu-items))
2067
2068 ((js--pitem-h-begin pitem)
2069 (assert (integerp (js--pitem-h-begin pitem)))
2070 (setq subitems (list
2071 (cons "[empty]"
2072 (js--maybe-make-marker
2073 (js--pitem-h-begin pitem)))))
2074 (push (cons pitem-name subitems)
2075 imenu-items))))
2076
2077 (t (error "Unknown item type: %S" pitem-type))))
2078
2079 imenu-items))
2080
2081(defun js--imenu-create-index ()
2e330adc 2082 "Return an imenu index for the current buffer."
17b5d0f7
CY
2083 (save-excursion
2084 (save-restriction
2085 (widen)
2086 (goto-char (point-max))
2087 (js--ensure-cache)
2088 (assert (or (= (point-min) (point-max))
2089 (eq js--last-parse-pos (point))))
2090 (when js--last-parse-pos
2091 (let ((state js--state-at-last-parse-pos)
2092 (unknown-ctr (cons -1 nil)))
2093
2094 ;; Make sure everything is closed
2095 (while (cdr state)
2096 (setq state
2097 (cons (js--pitem-add-child (second state) (car state))
2098 (cddr state))))
2099
2100 (assert (= (length state) 1))
2101
2102 ;; Convert the new-finalized state into what imenu expects
2103 (js--pitems-to-imenu
2104 (car (js--pitem-children state))
2105 unknown-ctr))))))
2106
2e330adc
CY
2107;; Silence the compiler.
2108(defvar which-func-imenu-joiner-function)
2109
17b5d0f7
CY
2110(defun js--which-func-joiner (parts)
2111 (mapconcat #'identity parts "."))
2112
2113(defun js--imenu-to-flat (items prefix symbols)
2114 (loop for item in items
2115 if (imenu--subalist-p item)
2116 do (js--imenu-to-flat
2117 (cdr item) (concat prefix (car item) ".")
2118 symbols)
2119 else
2120 do (let* ((name (concat prefix (car item)))
2121 (name2 name)
2122 (ctr 0))
2123
2124 (while (gethash name2 symbols)
2125 (setq name2 (format "%s<%d>" name (incf ctr))))
2126
2127 (puthash name2 (cdr item) symbols))))
2128
2129(defun js--get-all-known-symbols ()
dd4fbf56 2130 "Return a hash table of all JavaScript symbols.
2e330adc
CY
2131This searches all existing `js-mode' buffers. Each key is the
2132name of a symbol (possibly disambiguated with <N>, where N > 1),
2133and each value is a marker giving the location of that symbol."
17b5d0f7
CY
2134 (loop with symbols = (make-hash-table :test 'equal)
2135 with imenu-use-markers = t
2136 for buffer being the buffers
2137 for imenu-index = (with-current-buffer buffer
175069ef 2138 (when (derived-mode-p 'js-mode)
17b5d0f7
CY
2139 (js--imenu-create-index)))
2140 do (js--imenu-to-flat imenu-index "" symbols)
2141 finally return symbols))
2142
2143(defvar js--symbol-history nil
dd4fbf56 2144 "History of entered JavaScript symbols.")
17b5d0f7
CY
2145
2146(defun js--read-symbol (symbols-table prompt &optional initial-input)
2e330adc
CY
2147 "Helper function for `js-find-symbol'.
2148Read a symbol from SYMBOLS-TABLE, which is a hash table like the
2149one from `js--get-all-known-symbols', using prompt PROMPT and
2150initial input INITIAL-INPUT. Return a cons of (SYMBOL-NAME
2151. LOCATION), where SYMBOL-NAME is a string and LOCATION is a
2152marker."
17b5d0f7
CY
2153 (unless ido-mode
2154 (ido-mode t)
2155 (ido-mode nil))
2156
2157 (let ((choice (ido-completing-read
2158 prompt
2159 (loop for key being the hash-keys of symbols-table
2160 collect key)
2161 nil t initial-input 'js--symbol-history)))
2162 (cons choice (gethash choice symbols-table))))
2163
2164(defun js--guess-symbol-at-point ()
2165 (let ((bounds (bounds-of-thing-at-point 'symbol)))
2166 (when bounds
2167 (save-excursion
2168 (goto-char (car bounds))
2169 (when (eq (char-before) ?.)
2170 (backward-char)
2171 (setf (car bounds) (point))))
2172 (buffer-substring (car bounds) (cdr bounds)))))
2173
2174(defun js-find-symbol (&optional arg)
dd4fbf56 2175 "Read a JavaScript symbol and jump to it.
2e330adc 2176With a prefix argument, restrict symbols to those from the
dd4fbf56 2177current buffer. Pushes a mark onto the tag ring just like
2e330adc 2178`find-tag'."
17b5d0f7
CY
2179 (interactive "P")
2180 (let (symbols marker)
2181 (if (not arg)
2182 (setq symbols (js--get-all-known-symbols))
2183 (setq symbols (make-hash-table :test 'equal))
2184 (js--imenu-to-flat (js--imenu-create-index)
2185 "" symbols))
2186
2187 (setq marker (cdr (js--read-symbol
2188 symbols "Jump to: "
2189 (js--guess-symbol-at-point))))
2190
2191 (ring-insert find-tag-marker-ring (point-marker))
2192 (switch-to-buffer (marker-buffer marker))
2193 (push-mark)
2194 (goto-char marker)))
2195
2196;;; MozRepl integration
2197
2198(put 'js-moz-bad-rpc 'error-conditions '(error timeout))
2199(put 'js-moz-bad-rpc 'error-message "Mozilla RPC Error")
2200
2201(put 'js-js-error 'error-conditions '(error js-error))
2202(put 'js-js-error 'error-message "Javascript Error")
2203
2204(defun js--wait-for-matching-output
2205 (process regexp timeout &optional start)
2e330adc
CY
2206 "Wait TIMEOUT seconds for PROCESS to output a match for REGEXP.
2207On timeout, return nil. On success, return t with match data
2208set. If START is non-nil, look for output starting from START.
2209Otherwise, use the current value of `process-mark'."
17b5d0f7
CY
2210 (with-current-buffer (process-buffer process)
2211 (loop with start-pos = (or start
2212 (marker-position (process-mark process)))
2213 with end-time = (+ (float-time) timeout)
2214 for time-left = (- end-time (float-time))
2215 do (goto-char (point-max))
2216 if (looking-back regexp start-pos) return t
2217 while (> time-left 0)
2218 do (accept-process-output process time-left nil t)
2219 do (goto-char (process-mark process))
2220 finally do (signal
2221 'js-moz-bad-rpc
2222 (list (format "Timed out waiting for output matching %S" regexp))))))
2223
2224(defstruct js--js-handle
2225 ;; Integer, mirrors the value we see in JS
2226 (id nil :read-only t)
2227
2228 ;; Process to which this thing belongs
2229 (process nil :read-only t))
2230
2231(defun js--js-handle-expired-p (x)
2232 (not (eq (js--js-handle-process x)
2233 (inferior-moz-process))))
2234
2235(defvar js--js-references nil
dd4fbf56 2236 "Maps Elisp JavaScript proxy objects to their JavaScript IDs.")
17b5d0f7
CY
2237
2238(defvar js--js-process nil
2e330adc 2239 "The most recent MozRepl process object.")
17b5d0f7
CY
2240
2241(defvar js--js-gc-idle-timer nil
2e330adc 2242 "Idle timer for cleaning up JS object references.")
17b5d0f7 2243
2e330adc 2244(defvar js--js-last-gcs-done nil)
17b5d0f7
CY
2245
2246(defconst js--moz-interactor
2247 (replace-regexp-in-string
2248 "[ \n]+" " "
2249 ; */" Make Emacs happy
2250"(function(repl) {
2251 repl.defineInteractor('js', {
2252 onStart: function onStart(repl) {
2253 if(!repl._jsObjects) {
2254 repl._jsObjects = {};
2255 repl._jsLastID = 0;
2256 repl._jsGC = this._jsGC;
2257 }
2258 this._input = '';
2259 },
2260
2261 _jsGC: function _jsGC(ids_in_use) {
2262 var objects = this._jsObjects;
2263 var keys = [];
2264 var num_freed = 0;
2265
2266 for(var pn in objects) {
2267 keys.push(Number(pn));
2268 }
2269
2270 keys.sort(function(x, y) x - y);
2271 ids_in_use.sort(function(x, y) x - y);
2272 var i = 0;
2273 var j = 0;
2274
2275 while(i < ids_in_use.length && j < keys.length) {
2276 var id = ids_in_use[i++];
2277 while(j < keys.length && keys[j] !== id) {
2278 var k_id = keys[j++];
2279 delete objects[k_id];
2280 ++num_freed;
2281 }
2282 ++j;
2283 }
2284
2285 while(j < keys.length) {
2286 var k_id = keys[j++];
2287 delete objects[k_id];
2288 ++num_freed;
2289 }
2290
2291 return num_freed;
2292 },
2293
2294 _mkArray: function _mkArray() {
2295 var result = [];
2296 for(var i = 0; i < arguments.length; ++i) {
2297 result.push(arguments[i]);
2298 }
2299 return result;
2300 },
2301
2302 _parsePropDescriptor: function _parsePropDescriptor(parts) {
2303 if(typeof parts === 'string') {
2304 parts = [ parts ];
2305 }
2306
2307 var obj = parts[0];
2308 var start = 1;
2309
2310 if(typeof obj === 'string') {
2311 obj = window;
2312 start = 0;
2313 } else if(parts.length < 2) {
2314 throw new Error('expected at least 2 arguments');
2315 }
2316
2317 for(var i = start; i < parts.length - 1; ++i) {
2318 obj = obj[parts[i]];
2319 }
2320
2321 return [obj, parts[parts.length - 1]];
2322 },
2323
2324 _getProp: function _getProp(/*...*/) {
2325 if(arguments.length === 0) {
2326 throw new Error('no arguments supplied to getprop');
2327 }
2328
2329 if(arguments.length === 1 &&
2330 (typeof arguments[0]) !== 'string')
2331 {
2332 return arguments[0];
2333 }
2334
2335 var [obj, propname] = this._parsePropDescriptor(arguments);
2336 return obj[propname];
2337 },
2338
2339 _putProp: function _putProp(properties, value) {
2340 var [obj, propname] = this._parsePropDescriptor(properties);
2341 obj[propname] = value;
2342 },
2343
2344 _delProp: function _delProp(propname) {
2345 var [obj, propname] = this._parsePropDescriptor(arguments);
2346 delete obj[propname];
2347 },
2348
2349 _typeOf: function _typeOf(thing) {
2350 return typeof thing;
2351 },
2352
2353 _callNew: function(constructor) {
2354 if(typeof constructor === 'string')
2355 {
2356 constructor = window[constructor];
2357 } else if(constructor.length === 1 &&
2358 typeof constructor[0] !== 'string')
2359 {
2360 constructor = constructor[0];
2361 } else {
2362 var [obj,propname] = this._parsePropDescriptor(constructor);
2363 constructor = obj[propname];
2364 }
2365
2366 /* Hacky, but should be robust */
2367 var s = 'new constructor(';
2368 for(var i = 1; i < arguments.length; ++i) {
2369 if(i != 1) {
2370 s += ',';
2371 }
2372
2373 s += 'arguments[' + i + ']';
2374 }
2375
2376 s += ')';
2377 return eval(s);
2378 },
2379
2380 _callEval: function(thisobj, js) {
2381 return eval.call(thisobj, js);
2382 },
2383
2384 getPrompt: function getPrompt(repl) {
2385 return 'EVAL>'
2386 },
2387
2388 _lookupObject: function _lookupObject(repl, id) {
2389 if(typeof id === 'string') {
2390 switch(id) {
2391 case 'global':
2392 return window;
2393 case 'nil':
2394 return null;
2395 case 't':
2396 return true;
2397 case 'false':
2398 return false;
2399 case 'undefined':
2400 return undefined;
2401 case 'repl':
2402 return repl;
2403 case 'interactor':
2404 return this;
2405 case 'NaN':
2406 return NaN;
2407 case 'Infinity':
2408 return Infinity;
2409 case '-Infinity':
2410 return -Infinity;
2411 default:
2412 throw new Error('No object with special id:' + id);
2413 }
2414 }
2415
2416 var ret = repl._jsObjects[id];
2417 if(ret === undefined) {
2418 throw new Error('No object with id:' + id + '(' + typeof id + ')');
2419 }
2420 return ret;
2421 },
2422
2423 _findOrAllocateObject: function _findOrAllocateObject(repl, value) {
2424 if(typeof value !== 'object' && typeof value !== 'function') {
2425 throw new Error('_findOrAllocateObject called on non-object('
2426 + typeof(value) + '): '
2427 + value)
2428 }
2429
2430 for(var id in repl._jsObjects) {
2431 id = Number(id);
2432 var obj = repl._jsObjects[id];
2433 if(obj === value) {
2434 return id;
2435 }
2436 }
2437
2438 var id = ++repl._jsLastID;
2439 repl._jsObjects[id] = value;
2440 return id;
2441 },
2442
2443 _fixupList: function _fixupList(repl, list) {
2444 for(var i = 0; i < list.length; ++i) {
2445 if(list[i] instanceof Array) {
2446 this._fixupList(repl, list[i]);
2447 } else if(typeof list[i] === 'object') {
2448 var obj = list[i];
2449 if(obj.funcall) {
2450 var parts = obj.funcall;
2451 this._fixupList(repl, parts);
2452 var [thisobj, func] = this._parseFunc(parts[0]);
2453 list[i] = func.apply(thisobj, parts.slice(1));
2454 } else if(obj.objid) {
2455 list[i] = this._lookupObject(repl, obj.objid);
2456 } else {
2457 throw new Error('Unknown object type: ' + obj.toSource());
2458 }
2459 }
2460 }
2461 },
2462
2463 _parseFunc: function(func) {
2464 var thisobj = null;
2465
2466 if(typeof func === 'string') {
2467 func = window[func];
2468 } else if(func instanceof Array) {
2469 if(func.length === 1 && typeof func[0] !== 'string') {
2470 func = func[0];
2471 } else {
2472 [thisobj, func] = this._parsePropDescriptor(func);
2473 func = thisobj[func];
2474 }
2475 }
2476
2477 return [thisobj,func];
2478 },
2479
2480 _encodeReturn: function(value, array_as_mv) {
2481 var ret;
2482
2483 if(value === null) {
2484 ret = ['special', 'null'];
2485 } else if(value === true) {
2486 ret = ['special', 'true'];
2487 } else if(value === false) {
2488 ret = ['special', 'false'];
2489 } else if(value === undefined) {
2490 ret = ['special', 'undefined'];
2491 } else if(typeof value === 'number') {
2492 if(isNaN(value)) {
2493 ret = ['special', 'NaN'];
2494 } else if(value === Infinity) {
2495 ret = ['special', 'Infinity'];
2496 } else if(value === -Infinity) {
2497 ret = ['special', '-Infinity'];
2498 } else {
2499 ret = ['atom', value];
2500 }
2501 } else if(typeof value === 'string') {
2502 ret = ['atom', value];
2503 } else if(array_as_mv && value instanceof Array) {
2504 ret = ['array', value.map(this._encodeReturn, this)];
2505 } else {
2506 ret = ['objid', this._findOrAllocateObject(repl, value)];
2507 }
2508
2509 return ret;
2510 },
2511
2512 _handleInputLine: function _handleInputLine(repl, line) {
2513 var ret;
2514 var array_as_mv = false;
2515
2516 try {
2517 if(line[0] === '*') {
2518 array_as_mv = true;
2519 line = line.substring(1);
2520 }
2521 var parts = eval(line);
2522 this._fixupList(repl, parts);
2523 var [thisobj, func] = this._parseFunc(parts[0]);
2524 ret = this._encodeReturn(
2525 func.apply(thisobj, parts.slice(1)),
2526 array_as_mv);
2527 } catch(x) {
2528 ret = ['error', x.toString() ];
2529 }
2530
2531 var JSON = Components.classes['@mozilla.org/dom/json;1'].createInstance(Components.interfaces.nsIJSON);
2532 repl.print(JSON.encode(ret));
2533 repl._prompt();
2534 },
2535
2536 handleInput: function handleInput(repl, chunk) {
2537 this._input += chunk;
2538 var match, line;
2539 while(match = this._input.match(/.*\\n/)) {
2540 line = match[0];
2541
2542 if(line === 'EXIT\\n') {
2543 repl.popInteractor();
2544 repl._prompt();
2545 return;
2546 }
2547
2548 this._input = this._input.substring(line.length);
2549 this._handleInputLine(repl, line);
2550 }
2551 }
2552 });
2553})
2554")
2555
dd4fbf56 2556 "String to set MozRepl up into a simple-minded evaluation mode.")
17b5d0f7
CY
2557
2558(defun js--js-encode-value (x)
2e330adc
CY
2559 "Marshall the given value for JS.
2560Strings and numbers are JSON-encoded. Lists (including nil) are
dd4fbf56 2561made into JavaScript array literals and their contents encoded
2e330adc 2562with `js--js-encode-value'."
17b5d0f7
CY
2563 (cond ((stringp x) (json-encode-string x))
2564 ((numberp x) (json-encode-number x))
2565 ((symbolp x) (format "{objid:%S}" (symbol-name x)))
2566 ((js--js-handle-p x)
2567
2568 (when (js--js-handle-expired-p x)
2569 (error "Stale JS handle"))
2570
2571 (format "{objid:%s}" (js--js-handle-id x)))
2572
2573 ((sequencep x)
2574 (if (eq (car-safe x) 'js--funcall)
2575 (format "{funcall:[%s]}"
2576 (mapconcat #'js--js-encode-value (cdr x) ","))
2577 (concat
2578 "[" (mapconcat #'js--js-encode-value x ",") "]")))
2579 (t
2580 (error "Unrecognized item: %S" x))))
2581
2582(defconst js--js-prompt-regexp "\\(repl[0-9]*\\)> $")
2583(defconst js--js-repl-prompt-regexp "^EVAL>$")
2584(defvar js--js-repl-depth 0)
2585
2586(defun js--js-wait-for-eval-prompt ()
2587 (js--wait-for-matching-output
2588 (inferior-moz-process)
2589 js--js-repl-prompt-regexp js-js-timeout
2590
2591 ;; start matching against the beginning of the line in
2592 ;; order to catch a prompt that's only partially arrived
2593 (save-excursion (forward-line 0) (point))))
2594
2595(defun js--js-enter-repl ()
2596 (inferior-moz-process) ; called for side-effect
2597 (with-current-buffer inferior-moz-buffer
2598 (goto-char (point-max))
2599
2600 ;; Do some initialization the first time we see a process
2601 (unless (eq (inferior-moz-process) js--js-process)
2602 (setq js--js-process (inferior-moz-process))
2603 (setq js--js-references (make-hash-table :test 'eq :weakness t))
2604 (setq js--js-repl-depth 0)
2605
2606 ;; Send interactor definition
2607 (comint-send-string js--js-process js--moz-interactor)
2608 (comint-send-string js--js-process
2609 (concat "(" moz-repl-name ")\n"))
2610 (js--wait-for-matching-output
2611 (inferior-moz-process) js--js-prompt-regexp
2612 js-js-timeout))
2613
2614 ;; Sanity check
2615 (when (looking-back js--js-prompt-regexp
2616 (save-excursion (forward-line 0) (point)))
2617 (setq js--js-repl-depth 0))
2618
2619 (if (> js--js-repl-depth 0)
2620 ;; If js--js-repl-depth > 0, we *should* be seeing an
2621 ;; EVAL> prompt. If we don't, give Mozilla a chance to catch
2622 ;; up with us.
2623 (js--js-wait-for-eval-prompt)
2624
2625 ;; Otherwise, tell Mozilla to enter the interactor mode
2626 (insert (match-string-no-properties 1)
2627 ".pushInteractor('js')")
2628 (comint-send-input nil t)
2629 (js--wait-for-matching-output
2630 (inferior-moz-process) js--js-repl-prompt-regexp
2631 js-js-timeout))
2632
2633 (incf js--js-repl-depth)))
2634
2635(defun js--js-leave-repl ()
2636 (assert (> js--js-repl-depth 0))
2637 (when (= 0 (decf js--js-repl-depth))
2638 (with-current-buffer inferior-moz-buffer
2639 (goto-char (point-max))
2640 (js--js-wait-for-eval-prompt)
2641 (insert "EXIT")
2642 (comint-send-input nil t)
2643 (js--wait-for-matching-output
2644 (inferior-moz-process) js--js-prompt-regexp
2645 js-js-timeout))))
2646
2647(defsubst js--js-not (value)
b857059c 2648 (memq value '(nil null false undefined)))
17b5d0f7
CY
2649
2650(defsubst js--js-true (value)
2651 (not (js--js-not value)))
2652
2653(eval-and-compile
2654 (defun js--optimize-arglist (arglist)
2e330adc 2655 "Convert immediate js< and js! references to deferred ones."
17b5d0f7
CY
2656 (loop for item in arglist
2657 if (eq (car-safe item) 'js<)
2658 collect (append (list 'list ''js--funcall
2659 '(list 'interactor "_getProp"))
2660 (js--optimize-arglist (cdr item)))
2661 else if (eq (car-safe item) 'js>)
2662 collect (append (list 'list ''js--funcall
2663 '(list 'interactor "_putProp"))
2664
2665 (if (atom (cadr item))
2666 (list (cadr item))
2667 (list
2668 (append
2669 (list 'list ''js--funcall
2670 '(list 'interactor "_mkArray"))
2671 (js--optimize-arglist (cadr item)))))
2672 (js--optimize-arglist (cddr item)))
2673 else if (eq (car-safe item) 'js!)
2674 collect (destructuring-bind (ignored function &rest body) item
2675 (append (list 'list ''js--funcall
2676 (if (consp function)
2677 (cons 'list
2678 (js--optimize-arglist function))
2679 function))
2680 (js--optimize-arglist body)))
2681 else
2682 collect item)))
2683
2684(defmacro js--js-get-service (class-name interface-name)
2685 `(js! ("Components" "classes" ,class-name "getService")
2686 (js< "Components" "interfaces" ,interface-name)))
2687
2688(defmacro js--js-create-instance (class-name interface-name)
2689 `(js! ("Components" "classes" ,class-name "createInstance")
2690 (js< "Components" "interfaces" ,interface-name)))
2691
2692(defmacro js--js-qi (object interface-name)
2693 `(js! (,object "QueryInterface")
2694 (js< "Components" "interfaces" ,interface-name)))
2695
2696(defmacro with-js (&rest forms)
2e330adc 2697 "Run FORMS with the Mozilla repl set up for js commands.
17b5d0f7
CY
2698Inside the lexical scope of `with-js', `js?', `js!',
2699`js-new', `js-eval', `js-list', `js<', `js>', `js-get-service',
2700`js-create-instance', and `js-qi' are defined."
2701
2702 `(progn
2703 (js--js-enter-repl)
2704 (unwind-protect
2705 (macrolet ((js? (&rest body) `(js--js-true ,@body))
2706 (js! (function &rest body)
2707 `(js--js-funcall
2708 ,(if (consp function)
2709 (cons 'list
2710 (js--optimize-arglist function))
2711 function)
2712 ,@(js--optimize-arglist body)))
2713
2714 (js-new (function &rest body)
2715 `(js--js-new
2716 ,(if (consp function)
2717 (cons 'list
2718 (js--optimize-arglist function))
2719 function)
2720 ,@body))
2721
2722 (js-eval (thisobj js)
2723 `(js--js-eval
2724 ,@(js--optimize-arglist
2725 (list thisobj js))))
2726
2727 (js-list (&rest args)
2728 `(js--js-list
2729 ,@(js--optimize-arglist args)))
2730
2731 (js-get-service (&rest args)
2732 `(js--js-get-service
2733 ,@(js--optimize-arglist args)))
2734
2735 (js-create-instance (&rest args)
2736 `(js--js-create-instance
2737 ,@(js--optimize-arglist args)))
2738
2739 (js-qi (&rest args)
2740 `(js--js-qi
2741 ,@(js--optimize-arglist args)))
2742
2743 (js< (&rest body) `(js--js-get
2744 ,@(js--optimize-arglist body)))
2745 (js> (props value)
2746 `(js--js-funcall
2747 '(interactor "_putProp")
2748 ,(if (consp props)
2749 (cons 'list
2750 (js--optimize-arglist props))
2751 props)
2752 ,@(js--optimize-arglist (list value))
2753 ))
2754 (js-handle? (arg) `(js--js-handle-p ,arg)))
2755 ,@forms)
2756 (js--js-leave-repl))))
2757
2758(defvar js--js-array-as-list nil
2e330adc
CY
2759 "Whether to listify any Array returned by a Mozilla function.
2760If nil, the whole Array is treated as a JS symbol.")
17b5d0f7
CY
2761
2762(defun js--js-decode-retval (result)
2763 (ecase (intern (first result))
2764 (atom (second result))
2765 (special (intern (second result)))
2766 (array
2767 (mapcar #'js--js-decode-retval (second result)))
2768 (objid
2769 (or (gethash (second result)
2770 js--js-references)
2771 (puthash (second result)
2772 (make-js--js-handle
2773 :id (second result)
2774 :process (inferior-moz-process))
2775 js--js-references)))
2776
2777 (error (signal 'js-js-error (list (second result))))))
2778
2779(defun js--js-funcall (function &rest arguments)
2780 "Call the Mozilla function FUNCTION with arguments ARGUMENTS.
2781If function is a string, look it up as a property on the global
2e330adc
CY
2782object and use the global object for `this'.
2783If FUNCTION is a list with one element, use that element as the
2784function with the global object for `this', except that if that
2785single element is a string, look it up on the global object.
2786If FUNCTION is a list with more than one argument, use the list
2787up to the last value as a property descriptor and the last
2788argument as a function."
17b5d0f7
CY
2789
2790 (with-js
2791 (let ((argstr (js--js-encode-value
2792 (cons function arguments))))
2793
2794 (with-current-buffer inferior-moz-buffer
2795 ;; Actual funcall
2796 (when js--js-array-as-list
2797 (insert "*"))
2798 (insert argstr)
2799 (comint-send-input nil t)
2800 (js--wait-for-matching-output
2801 (inferior-moz-process) "EVAL>"
2802 js-js-timeout)
2803 (goto-char comint-last-input-end)
2804
2805 ;; Read the result
2806 (let* ((json-array-type 'list)
2807 (result (prog1 (json-read)
2808 (goto-char (point-max)))))
2809 (js--js-decode-retval result))))))
2810
2811(defun js--js-new (constructor &rest arguments)
2e330adc
CY
2812 "Call CONSTRUCTOR as a constructor, with arguments ARGUMENTS.
2813CONSTRUCTOR is a JS handle, a string, or a list of these things."
17b5d0f7
CY
2814 (apply #'js--js-funcall
2815 '(interactor "_callNew")
2816 constructor arguments))
2817
2818(defun js--js-eval (thisobj js)
2819 (js--js-funcall '(interactor "_callEval") thisobj js))
2820
2821(defun js--js-list (&rest arguments)
2e330adc 2822 "Return a Lisp array resulting from evaluating each of ARGUMENTS."
17b5d0f7
CY
2823 (let ((js--js-array-as-list t))
2824 (apply #'js--js-funcall '(interactor "_mkArray")
2825 arguments)))
2826
2827(defun js--js-get (&rest props)
2828 (apply #'js--js-funcall '(interactor "_getProp") props))
2829
2830(defun js--js-put (props value)
2831 (js--js-funcall '(interactor "_putProp") props value))
2832
2833(defun js-gc (&optional force)
2834 "Tell the repl about any objects we don't reference anymore.
2835With argument, run even if no intervening GC has happened."
2836 (interactive)
2837
2838 (when force
2839 (setq js--js-last-gcs-done nil))
2840
2841 (let ((this-gcs-done gcs-done) keys num)
2842 (when (and js--js-references
2843 (boundp 'inferior-moz-buffer)
2844 (buffer-live-p inferior-moz-buffer)
2845
2846 ;; Don't bother running unless we've had an intervening
2847 ;; garbage collection; without a gc, nothing is deleted
2848 ;; from the weak hash table, so it's pointless telling
2849 ;; MozRepl about that references we still hold
2850 (not (eq js--js-last-gcs-done this-gcs-done))
2851
2852 ;; Are we looking at a normal prompt? Make sure not to
2853 ;; interrupt the user if he's doing something
2854 (with-current-buffer inferior-moz-buffer
2855 (save-excursion
2856 (goto-char (point-max))
2857 (looking-back js--js-prompt-regexp
2858 (save-excursion (forward-line 0) (point))))))
2859
2860 (setq keys (loop for x being the hash-keys
2861 of js--js-references
2862 collect x))
2863 (setq num (js--js-funcall '(repl "_jsGC") (or keys [])))
2864
2865 (setq js--js-last-gcs-done this-gcs-done)
32226619 2866 (when (called-interactively-p 'interactive)
17b5d0f7
CY
2867 (message "Cleaned %s entries" num))
2868
2869 num)))
2870
2871(run-with-idle-timer 30 t #'js-gc)
2872
2873(defun js-eval (js)
2e330adc 2874 "Evaluate the JavaScript in JS and return JSON-decoded result."
17b5d0f7
CY
2875 (interactive "MJavascript to evaluate: ")
2876 (with-js
2877 (let* ((content-window (js--js-content-window
2878 (js--get-js-context)))
2879 (result (js-eval content-window js)))
32226619 2880 (when (called-interactively-p 'interactive)
17b5d0f7
CY
2881 (message "%s" (js! "String" result)))
2882 result)))
2883
2884(defun js--get-tabs ()
2e330adc
CY
2885 "Enumerate all JavaScript contexts available.
2886Each context is a list:
2887 (TITLE URL BROWSER TAB TABBROWSER) for content documents
2888 (TITLE URL WINDOW) for windows
2889
2890All tabs of a given window are grouped together. The most recent
2891window is first. Within each window, the tabs are returned
2892left-to-right."
17b5d0f7
CY
2893 (with-js
2894 (let (windows)
2895
2896 (loop with window-mediator = (js! ("Components" "classes"
2897 "@mozilla.org/appshell/window-mediator;1"
2898 "getService")
2899 (js< "Components" "interfaces"
2900 "nsIWindowMediator"))
2901 with enumerator = (js! (window-mediator "getEnumerator") nil)
2902
2903 while (js? (js! (enumerator "hasMoreElements")))
2904 for window = (js! (enumerator "getNext"))
2905 for window-info = (js-list window
2906 (js< window "document" "title")
2907 (js! (window "location" "toString"))
2908 (js< window "closed")
2909 (js< window "windowState"))
2910
2911 unless (or (js? (fourth window-info))
2912 (eq (fifth window-info) 2))
2913 do (push window-info windows))
2914
2915 (loop for window-info in windows
2916 for window = (first window-info)
2917 collect (list (second window-info)
2918 (third window-info)
2919 window)
2920
2921 for gbrowser = (js< window "gBrowser")
2922 if (js-handle? gbrowser)
2923 nconc (loop
2924 for x below (js< gbrowser "browsers" "length")
2925 collect (js-list (js< gbrowser
2926 "browsers"
2927 x
2928 "contentDocument"
2929 "title")
2930
2931 (js! (gbrowser
2932 "browsers"
2933 x
2934 "contentWindow"
2935 "location"
2936 "toString"))
2937 (js< gbrowser
2938 "browsers"
2939 x)
2940
2941 (js! (gbrowser
2942 "tabContainer"
2943 "childNodes"
2944 "item")
2945 x)
2946
2947 gbrowser))))))
2948
2949(defvar js-read-tab-history nil)
2950
2951(defun js--read-tab (prompt)
2e330adc
CY
2952 "Read a Mozilla tab with prompt PROMPT.
2953Return a cons of (TYPE . OBJECT). TYPE is either 'window or
dd4fbf56 2954'tab, and OBJECT is a JavaScript handle to a ChromeWindow or a
2e330adc 2955browser, respectively."
17b5d0f7
CY
2956
2957 ;; Prime IDO
2958 (unless ido-mode
2959 (ido-mode t)
2960 (ido-mode nil))
2961
2962 (with-js
2963 (lexical-let ((tabs (js--get-tabs)) selected-tab-cname
2964 selected-tab prev-hitab)
2965
2966 ;; Disambiguate names
2967 (setq tabs (loop with tab-names = (make-hash-table :test 'equal)
2968 for tab in tabs
2969 for cname = (format "%s (%s)" (second tab) (first tab))
2970 for num = (incf (gethash cname tab-names -1))
2971 if (> num 0)
2972 do (setq cname (format "%s <%d>" cname num))
2973 collect (cons cname tab)))
2974
2975 (labels ((find-tab-by-cname
2976 (cname)
2977 (loop for tab in tabs
2978 if (equal (car tab) cname)
2979 return (cdr tab)))
2980
2981 (mogrify-highlighting
2982 (hitab unhitab)
2983
2984 ;; Hack to reduce the number of
2985 ;; round-trips to mozilla
2986 (let (cmds)
2987 (cond
2988 ;; Highlighting tab
2989 ((fourth hitab)
2990 (push '(js! ((fourth hitab) "setAttribute")
2991 "style"
2992 "color: red; font-weight: bold")
2993 cmds)
2994
2995 ;; Highlight window proper
2996 (push '(js! ((third hitab)
2997 "setAttribute")
2998 "style"
2999 "border: 8px solid red")
3000 cmds)
3001
3002 ;; Select tab, when appropriate
3003 (when js-js-switch-tabs
3004 (push
3005 '(js> ((fifth hitab) "selectedTab") (fourth hitab))
3006 cmds)))
3007
3008 ;; Hilighting whole window
3009 ((third hitab)
3010 (push '(js! ((third hitab) "document"
3011 "documentElement" "setAttribute")
3012 "style"
3013 (concat "-moz-appearance: none;"
3014 "border: 8px solid red;"))
3015 cmds)))
3016
3017 (cond
3018 ;; Unhighlighting tab
3019 ((fourth unhitab)
3020 (push '(js! ((fourth unhitab) "setAttribute") "style" "")
3021 cmds)
3022 (push '(js! ((third unhitab) "setAttribute") "style" "")
3023 cmds))
3024
3025 ;; Unhighlighting window
3026 ((third unhitab)
3027 (push '(js! ((third unhitab) "document"
3028 "documentElement" "setAttribute")
3029 "style" "")
3030 cmds)))
3031
3032 (eval (list 'with-js
3033 (cons 'js-list (nreverse cmds))))))
3034
3035 (command-hook
3036 ()
3037 (let* ((tab (find-tab-by-cname (car ido-matches))))
3038 (mogrify-highlighting tab prev-hitab)
3039 (setq prev-hitab tab)))
3040
3041 (setup-hook
3042 ()
3043 ;; Fiddle with the match list a bit: if our first match
3044 ;; is a tabbrowser window, rotate the match list until
3045 ;; the active tab comes up
3046 (let ((matched-tab (find-tab-by-cname (car ido-matches))))
3047 (when (and matched-tab
3048 (null (fourth matched-tab))
3049 (equal "navigator:browser"
3050 (js! ((third matched-tab)
3051 "document"
3052 "documentElement"
3053 "getAttribute")
3054 "windowtype")))
3055
3056 (loop with tab-to-match = (js< (third matched-tab)
3057 "gBrowser"
3058 "selectedTab")
3059
3060 with index = 0
3061 for match in ido-matches
3062 for candidate-tab = (find-tab-by-cname match)
3063 if (eq (fourth candidate-tab) tab-to-match)
3064 do (setq ido-cur-list (ido-chop ido-cur-list match))
3065 and return t)))
3066
3067 (add-hook 'post-command-hook #'command-hook t t)))
3068
3069
3070 (unwind-protect
3071 (setq selected-tab-cname
3072 (let ((ido-minibuffer-setup-hook
3073 (cons #'setup-hook ido-minibuffer-setup-hook)))
3074 (ido-completing-read
3075 prompt
3076 (mapcar #'car tabs)
3077 nil t nil
3078 'js-read-tab-history)))
3079
3080 (when prev-hitab
3081 (mogrify-highlighting nil prev-hitab)
3082 (setq prev-hitab nil)))
3083
3084 (add-to-history 'js-read-tab-history selected-tab-cname)
3085
3086 (setq selected-tab (loop for tab in tabs
3087 if (equal (car tab) selected-tab-cname)
3088 return (cdr tab)))
3089
3090 (if (fourth selected-tab)
3091 (cons 'browser (third selected-tab))
3092 (cons 'window (third selected-tab)))))))
3093
3094(defun js--guess-eval-defun-info (pstate)
2e330adc
CY
3095 "Helper function for `js-eval-defun'.
3096Return a list (NAME . CLASSPARTS), where CLASSPARTS is a list of
3097strings making up the class name and NAME is the name of the
3098function part."
17b5d0f7
CY
3099 (cond ((and (= (length pstate) 3)
3100 (eq (js--pitem-type (first pstate)) 'function)
3101 (= (length (js--pitem-name (first pstate))) 1)
3102 (consp (js--pitem-type (second pstate))))
3103
3104 (append (js--pitem-name (second pstate))
3105 (list (first (js--pitem-name (first pstate))))))
3106
3107 ((and (= (length pstate) 2)
3108 (eq (js--pitem-type (first pstate)) 'function))
3109
3110 (append
3111 (butlast (js--pitem-name (first pstate)))
3112 (list (car (last (js--pitem-name (first pstate)))))))
3113
3114 (t (error "Function not a toplevel defun or class member"))))
3115
3116(defvar js--js-context nil
2e330adc
CY
3117 "The current JavaScript context.
3118This is a cons like the one returned from `js--read-tab'.
3119Change with `js-set-js-context'.")
17b5d0f7
CY
3120
3121(defconst js--js-inserter
3122 "(function(func_info,func) {
3123 func_info.unshift('window');
3124 var obj = window;
3125 for(var i = 1; i < func_info.length - 1; ++i) {
3126 var next = obj[func_info[i]];
3127 if(typeof next !== 'object' && typeof next !== 'function') {
3128 next = obj.prototype && obj.prototype[func_info[i]];
3129 if(typeof next !== 'object' && typeof next !== 'function') {
3130 alert('Could not find ' + func_info.slice(0, i+1).join('.') +
3131 ' or ' + func_info.slice(0, i+1).join('.') + '.prototype');
3132 return;
3133 }
3134
3135 func_info.splice(i+1, 0, 'prototype');
3136 ++i;
3137 }
3138 }
3139
3140 obj[func_info[i]] = func;
3141 alert('Successfully updated '+func_info.join('.'));
3142 })")
3143
3144(defun js-set-js-context (context)
2e330adc
CY
3145 "Set the JavaScript context to CONTEXT.
3146When called interactively, prompt for CONTEXT."
17b5d0f7
CY
3147 (interactive (list (js--read-tab "Javascript Context: ")))
3148 (setq js--js-context context))
3149
3150(defun js--get-js-context ()
2e330adc
CY
3151 "Return a valid JavaScript context.
3152If one hasn't been set, or if it's stale, prompt for a new one."
17b5d0f7
CY
3153 (with-js
3154 (when (or (null js--js-context)
3155 (js--js-handle-expired-p (cdr js--js-context))
3156 (ecase (car js--js-context)
3157 (window (js? (js< (cdr js--js-context) "closed")))
3158 (browser (not (js? (js< (cdr js--js-context)
3159 "contentDocument"))))))
3160 (setq js--js-context (js--read-tab "Javascript Context: ")))
17b5d0f7
CY
3161 js--js-context))
3162
3163(defun js--js-content-window (context)
3164 (with-js
3165 (ecase (car context)
3166 (window (cdr context))
3167 (browser (js< (cdr context)
3168 "contentWindow" "wrappedJSObject")))))
3169
3170(defun js--make-nsilocalfile (path)
3171 (with-js
3172 (let ((file (js-create-instance "@mozilla.org/file/local;1"
3173 "nsILocalFile")))
3174 (js! (file "initWithPath") path)
3175 file)))
3176
3177(defun js--js-add-resource-alias (alias path)
3178 (with-js
3179 (let* ((io-service (js-get-service "@mozilla.org/network/io-service;1"
3180 "nsIIOService"))
3181 (res-prot (js! (io-service "getProtocolHandler") "resource"))
3182 (res-prot (js-qi res-prot "nsIResProtocolHandler"))
3183 (path-file (js--make-nsilocalfile path))
3184 (path-uri (js! (io-service "newFileURI") path-file)))
3185 (js! (res-prot "setSubstitution") alias path-uri))))
3186
3187(defun* js-eval-defun ()
2e330adc 3188 "Update a Mozilla tab using the JavaScript defun at point."
17b5d0f7
CY
3189 (interactive)
3190
3191 ;; This function works by generating a temporary file that contains
3192 ;; the function we'd like to insert. We then use the elisp-js bridge
3193 ;; to command mozilla to load this file by inserting a script tag
3194 ;; into the document we set. This way, debuggers and such will have
3195 ;; a way to find the source of the just-inserted function.
3196 ;;
3197 ;; We delete the temporary file if there's an error, but otherwise
3198 ;; we add an unload event listener on the Mozilla side to delete the
3199 ;; file.
3200
3201 (save-excursion
3202 (let (begin end pstate defun-info temp-name defun-body)
2e330adc 3203 (js-end-of-defun)
17b5d0f7
CY
3204 (setq end (point))
3205 (js--ensure-cache)
2e330adc 3206 (js-beginning-of-defun)
17b5d0f7
CY
3207 (re-search-forward "\\_<function\\_>")
3208 (setq begin (match-beginning 0))
3209 (setq pstate (js--forward-pstate))
3210
3211 (when (or (null pstate)
3212 (> (point) end))
3213 (error "Could not locate function definition"))
3214
3215 (setq defun-info (js--guess-eval-defun-info pstate))
3216
3217 (let ((overlay (make-overlay begin end)))
3218 (overlay-put overlay 'face 'highlight)
3219 (unwind-protect
3220 (unless (y-or-n-p (format "Send %s to Mozilla? "
3221 (mapconcat #'identity defun-info ".")))
3222 (message "") ; question message lingers until next command
3223 (return-from js-eval-defun))
3224 (delete-overlay overlay)))
3225
3226 (setq defun-body (buffer-substring-no-properties begin end))
3227
3228 (make-directory js-js-tmpdir t)
3229
3230 ;; (Re)register a Mozilla resource URL to point to the
3231 ;; temporary directory
3232 (js--js-add-resource-alias "js" js-js-tmpdir)
3233
3234 (setq temp-name (make-temp-file (concat js-js-tmpdir
3235 "/js-")
3236 nil ".js"))
3237 (unwind-protect
3238 (with-js
3239 (with-temp-buffer
3240 (insert js--js-inserter)
3241 (insert "(")
3242 (insert (json-encode-list defun-info))
3243 (insert ",\n")
3244 (insert defun-body)
3245 (insert "\n)")
3246 (write-region (point-min) (point-max) temp-name
3247 nil 1))
3248
3249 ;; Give Mozilla responsibility for deleting this file
3250 (let* ((content-window (js--js-content-window
3251 (js--get-js-context)))
3252 (content-document (js< content-window "document"))
3253 (head (if (js? (js< content-document "body"))
3254 ;; Regular content
3255 (js< (js! (content-document "getElementsByTagName")
3256 "head")
3257 0)
3258 ;; Chrome
3259 (js< content-document "documentElement")))
3260 (elem (js! (content-document "createElementNS")
3261 "http://www.w3.org/1999/xhtml" "script")))
3262
3263 (js! (elem "setAttribute") "type" "text/javascript")
3264 (js! (elem "setAttribute") "src"
3265 (format "resource://js/%s"
3266 (file-name-nondirectory temp-name)))
3267
3268 (js! (head "appendChild") elem)
3269
3270 (js! (content-window "addEventListener") "unload"
3271 (js! ((js-new
3272 "Function" "file"
3273 "return function() { file.remove(false) }"))
3274 (js--make-nsilocalfile temp-name))
3275 'false)
3276 (setq temp-name nil)
3277
3278
3279
3280 ))
3281
3282 ;; temp-name is set to nil on success
3283 (when temp-name
3284 (delete-file temp-name))))))
3285
3286;;; Main Function
3287
3288;;;###autoload
175069ef
SM
3289(define-derived-mode js-mode prog-mode "Javascript"
3290 "Major mode for editing JavaScript."
17b5d0f7 3291 :group 'js
17b5d0f7
CY
3292
3293 (set (make-local-variable 'indent-line-function) 'js-indent-line)
3294 (set (make-local-variable 'beginning-of-defun-function)
2e330adc 3295 'js-beginning-of-defun)
17b5d0f7 3296 (set (make-local-variable 'end-of-defun-function)
2e330adc 3297 'js-end-of-defun)
17b5d0f7
CY
3298
3299 (set (make-local-variable 'open-paren-in-column-0-is-defun-start) nil)
3300 (set (make-local-variable 'font-lock-defaults)
b879a6e2 3301 (list js--font-lock-keywords))
cf38dd42
SM
3302 (set (make-local-variable 'syntax-propertize-function)
3303 js-syntax-propertize-function)
17b5d0f7
CY
3304
3305 (set (make-local-variable 'parse-sexp-ignore-comments) t)
3306 (set (make-local-variable 'parse-sexp-lookup-properties) t)
3307 (set (make-local-variable 'which-func-imenu-joiner-function)
3308 #'js--which-func-joiner)
3309
3310 ;; Comments
3311 (setq comment-start "// ")
3312 (setq comment-end "")
3313 (set (make-local-variable 'fill-paragraph-function)
3314 'js-c-fill-paragraph)
3315
3316 ;; Parse cache
3317 (add-hook 'before-change-functions #'js--flush-caches t t)
3318
3319 ;; Frameworks
3320 (js--update-quick-match-re)
3321
3322 ;; Imenu
3323 (setq imenu-case-fold-search nil)
3324 (set (make-local-variable 'imenu-create-index-function)
3325 #'js--imenu-create-index)
3326
17b5d0f7
CY
3327 ;; for filling, pretend we're cc-mode
3328 (setq c-comment-prefix-regexp "//+\\|\\**"
3329 c-paragraph-start "$"
3330 c-paragraph-separate "$"
3331 c-block-comment-prefix "* "
3332 c-line-comment-starter "//"
3333 c-comment-start-regexp "/[*/]\\|\\s!"
3334 comment-start-skip "\\(//+\\|/\\*+\\)\\s *")
3335
3336 (let ((c-buffer-is-cc-mode t))
f5d6ff44
CY
3337 ;; FIXME: These are normally set by `c-basic-common-init'. Should
3338 ;; we call it instead? (Bug#6071)
3339 (make-local-variable 'paragraph-start)
3340 (make-local-variable 'paragraph-separate)
3341 (make-local-variable 'paragraph-ignore-fill-prefix)
3342 (make-local-variable 'adaptive-fill-mode)
3343 (make-local-variable 'adaptive-fill-regexp)
17b5d0f7
CY
3344 (c-setup-paragraph-variables))
3345
3346 (set (make-local-variable 'syntax-begin-function)
3347 #'js--syntax-begin-function)
3348
3349 ;; Important to fontify the whole buffer syntactically! If we don't,
3350 ;; then we might have regular expression literals that aren't marked
3351 ;; as strings, which will screw up parse-partial-sexp, scan-lists,
3352 ;; etc. and and produce maddening "unbalanced parenthesis" errors.
3353 ;; When we attempt to find the error and scroll to the portion of
3354 ;; the buffer containing the problem, JIT-lock will apply the
3355 ;; correct syntax to the regular expresion literal and the problem
3356 ;; will mysteriously disappear.
175069ef
SM
3357 ;; FIXME: We should actually do this fontification lazily by adding
3358 ;; calls to syntax-propertize wherever it's really needed.
3359 (syntax-propertize (point-max)))
17b5d0f7 3360
92b1c416 3361;;;###autoload
17b5d0f7
CY
3362(defalias 'javascript-mode 'js-mode)
3363
3364(eval-after-load 'folding
3365 '(when (fboundp 'folding-add-to-marks-list)
3366 (folding-add-to-marks-list 'js-mode "// {{{" "// }}}" )))
3367
3368(provide 'js)
3369
3370;; js.el ends here