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