maglev-webtools 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. data/LICENSE.txt +25 -0
  2. data/README.rdoc +121 -0
  3. data/bin/webtools +15 -0
  4. data/lib/web_tools/#debugger.rb# +212 -0
  5. data/lib/web_tools/browser.rb +45 -0
  6. data/lib/web_tools/debugger.rb +219 -0
  7. data/lib/web_tools/info.rb +29 -0
  8. data/lib/web_tools/middleware/debugger.rb +118 -0
  9. data/lib/web_tools/support/app_model.rb +117 -0
  10. data/lib/web_tools/support/code_browser.rb +109 -0
  11. data/lib/web_tools/support/ruby.rb +132 -0
  12. data/lib/web_tools/support/service_helper.rb +22 -0
  13. data/lib/web_tools/support/smalltalk_extensions.rb +65 -0
  14. data/lib/web_tools/support/smalltalk_tools.rb +16 -0
  15. data/lib/web_tools/ui.rb +67 -0
  16. data/lib/web_tools.rb +10 -0
  17. data/public/images/favicon.ico +0 -0
  18. data/public/javascript/CodeMirror/LICENSE +23 -0
  19. data/public/javascript/CodeMirror/css/Smalltalk.css +34 -0
  20. data/public/javascript/CodeMirror/js/codemirror.js +582 -0
  21. data/public/javascript/CodeMirror/js/editor.js +1671 -0
  22. data/public/javascript/CodeMirror/js/highlight.js +68 -0
  23. data/public/javascript/CodeMirror/js/parseSmalltalk.js +126 -0
  24. data/public/javascript/CodeMirror/js/parsedummy.js +32 -0
  25. data/public/javascript/CodeMirror/js/select.js +699 -0
  26. data/public/javascript/CodeMirror/js/stringstream.js +159 -0
  27. data/public/javascript/CodeMirror/js/tokenize.js +57 -0
  28. data/public/javascript/CodeMirror/js/undo.js +413 -0
  29. data/public/javascript/CodeMirror/js/util.js +133 -0
  30. data/public/javascript/CodeMirror/testSmalltalkParser.html +116 -0
  31. data/public/javascript/ace/ace-uncompressed.js +17299 -0
  32. data/public/javascript/ace/ace.js +1 -0
  33. data/public/javascript/ace/keybinding-emacs.js +1 -0
  34. data/public/javascript/ace/keybinding-vim.js +1 -0
  35. data/public/javascript/ace/mode-c_cpp.js +1 -0
  36. data/public/javascript/ace/mode-clojure.js +1 -0
  37. data/public/javascript/ace/mode-coffee.js +1 -0
  38. data/public/javascript/ace/mode-csharp.js +1 -0
  39. data/public/javascript/ace/mode-css.js +1 -0
  40. data/public/javascript/ace/mode-groovy.js +1 -0
  41. data/public/javascript/ace/mode-html.js +1 -0
  42. data/public/javascript/ace/mode-java.js +1 -0
  43. data/public/javascript/ace/mode-javascript.js +1 -0
  44. data/public/javascript/ace/mode-json.js +1 -0
  45. data/public/javascript/ace/mode-lua.js +1 -0
  46. data/public/javascript/ace/mode-markdown.js +1 -0
  47. data/public/javascript/ace/mode-ocaml.js +1 -0
  48. data/public/javascript/ace/mode-perl.js +1 -0
  49. data/public/javascript/ace/mode-php.js +1 -0
  50. data/public/javascript/ace/mode-python.js +1 -0
  51. data/public/javascript/ace/mode-ruby.js +1 -0
  52. data/public/javascript/ace/mode-scad.js +1 -0
  53. data/public/javascript/ace/mode-scala.js +1 -0
  54. data/public/javascript/ace/mode-scss.js +1 -0
  55. data/public/javascript/ace/mode-svg.js +1 -0
  56. data/public/javascript/ace/mode-textile.js +1 -0
  57. data/public/javascript/ace/mode-xml.js +1 -0
  58. data/public/javascript/ace/theme-clouds.js +1 -0
  59. data/public/javascript/ace/theme-clouds_midnight.js +1 -0
  60. data/public/javascript/ace/theme-cobalt.js +1 -0
  61. data/public/javascript/ace/theme-crimson_editor.js +1 -0
  62. data/public/javascript/ace/theme-dawn.js +1 -0
  63. data/public/javascript/ace/theme-eclipse.js +1 -0
  64. data/public/javascript/ace/theme-idle_fingers.js +1 -0
  65. data/public/javascript/ace/theme-kr_theme.js +1 -0
  66. data/public/javascript/ace/theme-merbivore.js +1 -0
  67. data/public/javascript/ace/theme-merbivore_soft.js +1 -0
  68. data/public/javascript/ace/theme-mono_industrial.js +1 -0
  69. data/public/javascript/ace/theme-monokai.js +1 -0
  70. data/public/javascript/ace/theme-pastel_on_dark.js +1 -0
  71. data/public/javascript/ace/theme-solarized_dark.js +1 -0
  72. data/public/javascript/ace/theme-solarized_light.js +1 -0
  73. data/public/javascript/ace/theme-textmate.js +1 -0
  74. data/public/javascript/ace/theme-twilight.js +1 -0
  75. data/public/javascript/ace/theme-vibrant_ink.js +1 -0
  76. data/public/javascript/ace/worker-coffee.js +1 -0
  77. data/public/javascript/ace/worker-css.js +1 -0
  78. data/public/javascript/ace/worker-javascript.js +1 -0
  79. data/public/javascript/webtools/#debugger.coffee# +253 -0
  80. data/public/javascript/webtools/browser.js +260 -0
  81. data/public/javascript/webtools/debugger.coffee +286 -0
  82. data/public/javascript/webtools/debugger.js +366 -0
  83. data/public/javascript/webtools/sessions.coffee +17 -0
  84. data/public/javascript/webtools/sessions.js +27 -0
  85. data/public/javascript/webtools/version.coffee +14 -0
  86. data/public/javascript/webtools/version.js +20 -0
  87. data/public/stylesheets/base/images/ui-anim_basic_16x16.gif +0 -0
  88. data/public/stylesheets/base/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  89. data/public/stylesheets/base/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  90. data/public/stylesheets/base/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  91. data/public/stylesheets/base/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  92. data/public/stylesheets/base/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  93. data/public/stylesheets/base/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  94. data/public/stylesheets/base/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  95. data/public/stylesheets/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  96. data/public/stylesheets/base/images/ui-icons_222222_256x240.png +0 -0
  97. data/public/stylesheets/base/images/ui-icons_2e83ff_256x240.png +0 -0
  98. data/public/stylesheets/base/images/ui-icons_454545_256x240.png +0 -0
  99. data/public/stylesheets/base/images/ui-icons_888888_256x240.png +0 -0
  100. data/public/stylesheets/base/images/ui-icons_cd0a0a_256x240.png +0 -0
  101. data/public/stylesheets/base/jquery.ui.accordion.css +12 -0
  102. data/public/stylesheets/base/jquery.ui.all.css +2 -0
  103. data/public/stylesheets/base/jquery.ui.autocomplete.css +39 -0
  104. data/public/stylesheets/base/jquery.ui.base.css +11 -0
  105. data/public/stylesheets/base/jquery.ui.button.css +35 -0
  106. data/public/stylesheets/base/jquery.ui.core.css +37 -0
  107. data/public/stylesheets/base/jquery.ui.datepicker.css +61 -0
  108. data/public/stylesheets/base/jquery.ui.dialog.css +13 -0
  109. data/public/stylesheets/base/jquery.ui.progressbar.css +4 -0
  110. data/public/stylesheets/base/jquery.ui.resizable.css +13 -0
  111. data/public/stylesheets/base/jquery.ui.selectable.css +3 -0
  112. data/public/stylesheets/base/jquery.ui.slider.css +17 -0
  113. data/public/stylesheets/base/jquery.ui.tabs.css +11 -0
  114. data/public/stylesheets/base/jquery.ui.theme.css +247 -0
  115. data/public/stylesheets/jquery.contextMenu.css +62 -0
  116. data/public/stylesheets/reset.css +18 -0
  117. data/public/stylesheets/webtools.css +53 -0
  118. data/public/test.html +47 -0
  119. data/views/browser.rhtml +63 -0
  120. data/views/debugger.rhtml +59 -0
  121. data/views/index.rhtml +8 -0
  122. data/views/layout.rhtml +24 -0
  123. data/views/sessions.rhtml +12 -0
  124. data/views/version.rhtml +10 -0
  125. metadata +316 -0
@@ -0,0 +1,1671 @@
1
+ /* The Editor object manages the content of the editable frame. It
2
+ * catches events, colours nodes, and indents lines. This file also
3
+ * holds some functions for transforming arbitrary DOM structures into
4
+ * plain sequences of <span> and <br> elements
5
+ */
6
+
7
+ var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
8
+ var webkit = /AppleWebKit/.test(navigator.userAgent);
9
+ var safari = /Apple Computer, Inc/.test(navigator.vendor);
10
+ var gecko = navigator.userAgent.match(/gecko\/(\d{8})/i);
11
+ if (gecko) gecko = Number(gecko[1]);
12
+ var mac = /Mac/.test(navigator.platform);
13
+
14
+ // TODO this is related to the backspace-at-end-of-line bug. Remove
15
+ // this if Opera gets their act together, make the version check more
16
+ // broad if they don't.
17
+ var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent);
18
+ // TODO remove this once WebKit 533 becomes less common.
19
+ var slowWebkit = /AppleWebKit\/533/.test(navigator.userAgent);
20
+
21
+ // Make sure a string does not contain two consecutive 'collapseable'
22
+ // whitespace characters.
23
+ function makeWhiteSpace(n) {
24
+ var buffer = [], nb = true;
25
+ for (; n > 0; n--) {
26
+ buffer.push((nb || n == 1) ? nbsp : " ");
27
+ nb ^= true;
28
+ }
29
+ return buffer.join("");
30
+ }
31
+
32
+ // Create a set of white-space characters that will not be collapsed
33
+ // by the browser, but will not break text-wrapping either.
34
+ function fixSpaces(string) {
35
+ if (string.charAt(0) == " ") string = nbsp + string.slice(1);
36
+ return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);})
37
+ .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
38
+ }
39
+
40
+ function cleanText(text) {
41
+ return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
42
+ }
43
+
44
+ // Create a SPAN node with the expected properties for document part
45
+ // spans.
46
+ function makePartSpan(value) {
47
+ var text = value;
48
+ if (value.nodeType == 3) text = value.nodeValue;
49
+ else value = document.createTextNode(text);
50
+
51
+ var span = document.createElement("span");
52
+ span.isPart = true;
53
+ span.appendChild(value);
54
+ span.currentText = text;
55
+ return span;
56
+ }
57
+
58
+ function alwaysZero() {return 0;}
59
+
60
+ // On webkit, when the last BR of the document does not have text
61
+ // behind it, the cursor can not be put on the line after it. This
62
+ // makes pressing enter at the end of the document occasionally do
63
+ // nothing (or at least seem to do nothing). To work around it, this
64
+ // function makes sure the document ends with a span containing a
65
+ // zero-width space character. The traverseDOM iterator filters such
66
+ // character out again, so that the parsers won't see them. This
67
+ // function is called from a few strategic places to make sure the
68
+ // zwsp is restored after the highlighting process eats it.
69
+ var webkitLastLineHack = webkit ?
70
+ function(container) {
71
+ var last = container.lastChild;
72
+ if (!last || !last.hackBR) {
73
+ var br = document.createElement("br");
74
+ br.hackBR = true;
75
+ container.appendChild(br);
76
+ }
77
+ } : function() {};
78
+
79
+ function asEditorLines(string) {
80
+ var tab = makeWhiteSpace(indentUnit);
81
+ return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces);
82
+ }
83
+
84
+ var Editor = (function(){
85
+ // The HTML elements whose content should be suffixed by a newline
86
+ // when converting them to flat text.
87
+ var newlineElements = {"P": true, "DIV": true, "LI": true};
88
+
89
+ // Helper function for traverseDOM. Flattens an arbitrary DOM node
90
+ // into an array of textnodes and <br> tags.
91
+ function simplifyDOM(root, atEnd) {
92
+ var result = [];
93
+ var leaving = true;
94
+
95
+ function simplifyNode(node, top) {
96
+ if (node.nodeType == 3) {
97
+ var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
98
+ if (text.length) leaving = false;
99
+ result.push(node);
100
+ }
101
+ else if (isBR(node) && node.childNodes.length == 0) {
102
+ leaving = true;
103
+ result.push(node);
104
+ }
105
+ else {
106
+ for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n);
107
+ if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
108
+ leaving = true;
109
+ if (!atEnd || !top)
110
+ result.push(document.createElement("br"));
111
+ }
112
+ }
113
+ }
114
+
115
+ simplifyNode(root, true);
116
+ return result;
117
+ }
118
+
119
+ // Creates a MochiKit-style iterator that goes over a series of DOM
120
+ // nodes. The values it yields are strings, the textual content of
121
+ // the nodes. It makes sure that all nodes up to and including the
122
+ // one whose text is being yielded have been 'normalized' to be just
123
+ // <span> and <br> elements.
124
+ function traverseDOM(start){
125
+ var nodeQueue = [];
126
+
127
+ // Create a function that can be used to insert nodes after the
128
+ // one given as argument.
129
+ function pointAt(node){
130
+ var parent = node.parentNode;
131
+ var next = node.nextSibling;
132
+ return function(newnode) {
133
+ parent.insertBefore(newnode, next);
134
+ };
135
+ }
136
+ var point = null;
137
+
138
+ // This an Opera-specific hack -- always insert an empty span
139
+ // between two BRs, because Opera's cursor code gets terribly
140
+ // confused when the cursor is between two BRs.
141
+ var afterBR = true;
142
+
143
+ // Insert a normalized node at the current point. If it is a text
144
+ // node, wrap it in a <span>, and give that span a currentText
145
+ // property -- this is used to cache the nodeValue, because
146
+ // directly accessing nodeValue is horribly slow on some browsers.
147
+ // The dirty property is used by the highlighter to determine
148
+ // which parts of the document have to be re-highlighted.
149
+ function insertPart(part){
150
+ var text = "\n";
151
+ if (part.nodeType == 3) {
152
+ select.snapshotChanged();
153
+ part = makePartSpan(part);
154
+ text = part.currentText;
155
+ afterBR = false;
156
+ }
157
+ else {
158
+ if (afterBR && window.opera)
159
+ point(makePartSpan(""));
160
+ afterBR = true;
161
+ }
162
+ part.dirty = true;
163
+ nodeQueue.push(part);
164
+ point(part);
165
+ return text;
166
+ }
167
+
168
+ // Extract the text and newlines from a DOM node, insert them into
169
+ // the document, and return the textual content. Used to replace
170
+ // non-normalized nodes.
171
+ function writeNode(node, end) {
172
+ var simplified = simplifyDOM(node, end);
173
+ for (var i = 0; i < simplified.length; i++)
174
+ simplified[i] = insertPart(simplified[i]);
175
+ return simplified.join("");
176
+ }
177
+
178
+ // Check whether a node is a normalized <span> element.
179
+ function partNode(node){
180
+ if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
181
+ var text = node.firstChild.nodeValue;
182
+ node.dirty = node.dirty || text != node.currentText;
183
+ node.currentText = text;
184
+ return !/[\n\t\r]/.test(node.currentText);
185
+ }
186
+ return false;
187
+ }
188
+
189
+ // Advance to next node, return string for current node.
190
+ function next() {
191
+ if (!start) throw StopIteration;
192
+ var node = start;
193
+ start = node.nextSibling;
194
+
195
+ if (partNode(node)){
196
+ nodeQueue.push(node);
197
+ afterBR = false;
198
+ return node.currentText;
199
+ }
200
+ else if (isBR(node)) {
201
+ if (afterBR && window.opera)
202
+ node.parentNode.insertBefore(makePartSpan(""), node);
203
+ nodeQueue.push(node);
204
+ afterBR = true;
205
+ return "\n";
206
+ }
207
+ else {
208
+ var end = !node.nextSibling;
209
+ point = pointAt(node);
210
+ removeElement(node);
211
+ return writeNode(node, end);
212
+ }
213
+ }
214
+
215
+ // MochiKit iterators are objects with a next function that
216
+ // returns the next value or throws StopIteration when there are
217
+ // no more values.
218
+ return {next: next, nodes: nodeQueue};
219
+ }
220
+
221
+ // Determine the text size of a processed node.
222
+ function nodeSize(node) {
223
+ return isBR(node) ? 1 : node.currentText.length;
224
+ }
225
+
226
+ // Search backwards through the top-level nodes until the next BR or
227
+ // the start of the frame.
228
+ function startOfLine(node) {
229
+ while (node && !isBR(node)) node = node.previousSibling;
230
+ return node;
231
+ }
232
+ function endOfLine(node, container) {
233
+ if (!node) node = container.firstChild;
234
+ else if (isBR(node)) node = node.nextSibling;
235
+
236
+ while (node && !isBR(node)) node = node.nextSibling;
237
+ return node;
238
+ }
239
+
240
+ function time() {return new Date().getTime();}
241
+
242
+ // Client interface for searching the content of the editor. Create
243
+ // these by calling CodeMirror.getSearchCursor. To use, call
244
+ // findNext on the resulting object -- this returns a boolean
245
+ // indicating whether anything was found, and can be called again to
246
+ // skip to the next find. Use the select and replace methods to
247
+ // actually do something with the found locations.
248
+ function SearchCursor(editor, pattern, from, caseFold) {
249
+ this.editor = editor;
250
+ this.history = editor.history;
251
+ this.history.commit();
252
+ this.valid = !!pattern;
253
+ this.atOccurrence = false;
254
+ if (caseFold == undefined) caseFold = typeof pattern == "string" && pattern == pattern.toLowerCase();
255
+
256
+ function getText(node){
257
+ var line = cleanText(editor.history.textAfter(node));
258
+ return (caseFold ? line.toLowerCase() : line);
259
+ }
260
+
261
+ var topPos = {node: null, offset: 0}, self = this;
262
+ if (from && typeof from == "object" && typeof from.character == "number") {
263
+ editor.checkLine(from.line);
264
+ var pos = {node: from.line, offset: from.character};
265
+ this.pos = {from: pos, to: pos};
266
+ }
267
+ else if (from) {
268
+ this.pos = {from: select.cursorPos(editor.container, true) || topPos,
269
+ to: select.cursorPos(editor.container, false) || topPos};
270
+ }
271
+ else {
272
+ this.pos = {from: topPos, to: topPos};
273
+ }
274
+
275
+ if (typeof pattern != "string") { // Regexp match
276
+ this.matches = function(reverse, node, offset) {
277
+ if (reverse) {
278
+ var line = getText(node).slice(0, offset), match = line.match(pattern), start = 0;
279
+ while (match) {
280
+ var ind = line.indexOf(match[0]);
281
+ start += ind;
282
+ line = line.slice(ind + 1);
283
+ var newmatch = line.match(pattern);
284
+ if (newmatch) match = newmatch;
285
+ else break;
286
+ }
287
+ }
288
+ else {
289
+ var line = getText(node).slice(offset), match = line.match(pattern),
290
+ start = match && offset + line.indexOf(match[0]);
291
+ }
292
+ if (match) {
293
+ self.currentMatch = match;
294
+ return {from: {node: node, offset: start},
295
+ to: {node: node, offset: start + match[0].length}};
296
+ }
297
+ };
298
+ return;
299
+ }
300
+
301
+ if (caseFold) pattern = pattern.toLowerCase();
302
+ // Create a matcher function based on the kind of string we have.
303
+ var target = pattern.split("\n");
304
+ this.matches = (target.length == 1) ?
305
+ // For one-line strings, searching can be done simply by calling
306
+ // indexOf or lastIndexOf on the current line.
307
+ function(reverse, node, offset) {
308
+ var line = getText(node), len = pattern.length, match;
309
+ if (reverse ? (offset >= len && (match = line.lastIndexOf(pattern, offset - len)) != -1)
310
+ : (match = line.indexOf(pattern, offset)) != -1)
311
+ return {from: {node: node, offset: match},
312
+ to: {node: node, offset: match + len}};
313
+ } :
314
+ // Multi-line strings require internal iteration over lines, and
315
+ // some clunky checks to make sure the first match ends at the
316
+ // end of the line and the last match starts at the start.
317
+ function(reverse, node, offset) {
318
+ var idx = (reverse ? target.length - 1 : 0), match = target[idx], line = getText(node);
319
+ var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match));
320
+ if (reverse ? offsetA >= offset || offsetA != match.length
321
+ : offsetA <= offset || offsetA != line.length - match.length)
322
+ return;
323
+
324
+ var pos = node;
325
+ while (true) {
326
+ if (reverse && !pos) return;
327
+ pos = (reverse ? this.history.nodeBefore(pos) : this.history.nodeAfter(pos) );
328
+ if (!reverse && !pos) return;
329
+
330
+ line = getText(pos);
331
+ match = target[reverse ? --idx : ++idx];
332
+
333
+ if (idx > 0 && idx < target.length - 1) {
334
+ if (line != match) return;
335
+ else continue;
336
+ }
337
+ var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length);
338
+ if (reverse ? offsetB != line.length - match.length : offsetB != match.length)
339
+ return;
340
+ return {from: {node: reverse ? pos : node, offset: reverse ? offsetB : offsetA},
341
+ to: {node: reverse ? node : pos, offset: reverse ? offsetA : offsetB}};
342
+ }
343
+ };
344
+ }
345
+
346
+ SearchCursor.prototype = {
347
+ findNext: function() {return this.find(false);},
348
+ findPrevious: function() {return this.find(true);},
349
+
350
+ find: function(reverse) {
351
+ if (!this.valid) return false;
352
+
353
+ var self = this, pos = reverse ? this.pos.from : this.pos.to,
354
+ node = pos.node, offset = pos.offset;
355
+ // Reset the cursor if the current line is no longer in the DOM tree.
356
+ if (node && !node.parentNode) {
357
+ node = null; offset = 0;
358
+ }
359
+ function savePosAndFail() {
360
+ var pos = {node: node, offset: offset};
361
+ self.pos = {from: pos, to: pos};
362
+ self.atOccurrence = false;
363
+ return false;
364
+ }
365
+
366
+ while (true) {
367
+ if (this.pos = this.matches(reverse, node, offset)) {
368
+ this.atOccurrence = true;
369
+ return true;
370
+ }
371
+
372
+ if (reverse) {
373
+ if (!node) return savePosAndFail();
374
+ node = this.history.nodeBefore(node);
375
+ offset = this.history.textAfter(node).length;
376
+ }
377
+ else {
378
+ var next = this.history.nodeAfter(node);
379
+ if (!next) {
380
+ offset = this.history.textAfter(node).length;
381
+ return savePosAndFail();
382
+ }
383
+ node = next;
384
+ offset = 0;
385
+ }
386
+ }
387
+ },
388
+
389
+ select: function() {
390
+ if (this.atOccurrence) {
391
+ select.setCursorPos(this.editor.container, this.pos.from, this.pos.to);
392
+ select.scrollToCursor(this.editor.container);
393
+ }
394
+ },
395
+
396
+ replace: function(string) {
397
+ if (this.atOccurrence) {
398
+ var fragments = this.currentMatch;
399
+ if (fragments)
400
+ string = string.replace(/\\(\d)/, function(m, i){return fragments[i];});
401
+ var end = this.editor.replaceRange(this.pos.from, this.pos.to, string);
402
+ this.pos.to = end;
403
+ this.atOccurrence = false;
404
+ }
405
+ },
406
+
407
+ position: function() {
408
+ if (this.atOccurrence)
409
+ return {line: this.pos.from.node, character: this.pos.from.offset};
410
+ }
411
+ };
412
+
413
+ // The Editor object is the main inside-the-iframe interface.
414
+ function Editor(options) {
415
+ this.options = options;
416
+ window.indentUnit = options.indentUnit;
417
+ var container = this.container = document.body;
418
+ this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this);
419
+ var self = this;
420
+
421
+ if (!Editor.Parser)
422
+ throw "No parser loaded.";
423
+ if (options.parserConfig && Editor.Parser.configure)
424
+ Editor.Parser.configure(options.parserConfig);
425
+
426
+ if (!options.readOnly && !internetExplorer)
427
+ select.setCursorPos(container, {node: null, offset: 0});
428
+
429
+ this.dirty = [];
430
+ this.importCode(options.content || "");
431
+ this.history.onChange = options.onChange;
432
+
433
+ if (!options.readOnly) {
434
+ if (options.continuousScanning !== false) {
435
+ this.scanner = this.documentScanner(options.passTime);
436
+ this.delayScanning();
437
+ }
438
+
439
+ function setEditable() {
440
+ // Use contentEditable instead of designMode on IE, since designMode frames
441
+ // can not run any scripts. It would be nice if we could use contentEditable
442
+ // everywhere, but it is significantly flakier than designMode on every
443
+ // single non-IE browser.
444
+ if (document.body.contentEditable != undefined && internetExplorer)
445
+ document.body.contentEditable = "true";
446
+ else
447
+ document.designMode = "on";
448
+
449
+ // Work around issue where you have to click on the actual
450
+ // body of the document to focus it in IE, making focusing
451
+ // hard when the document is small.
452
+ if (internetExplorer && options.height != "dynamic")
453
+ document.body.style.minHeight = (
454
+ window.frameElement.clientHeight - 2 * document.body.offsetTop - 5) + "px";
455
+
456
+ document.documentElement.style.borderWidth = "0";
457
+ if (!options.textWrapping)
458
+ container.style.whiteSpace = "nowrap";
459
+ }
460
+
461
+ // If setting the frame editable fails, try again when the user
462
+ // focus it (happens when the frame is not visible on
463
+ // initialisation, in Firefox).
464
+ try {
465
+ setEditable();
466
+ }
467
+ catch(e) {
468
+ var focusEvent = addEventHandler(document, "focus", function() {
469
+ focusEvent();
470
+ setEditable();
471
+ }, true);
472
+ }
473
+
474
+ addEventHandler(document, "keydown", method(this, "keyDown"));
475
+ addEventHandler(document, "keypress", method(this, "keyPress"));
476
+ addEventHandler(document, "keyup", method(this, "keyUp"));
477
+
478
+ function cursorActivity() {self.cursorActivity(false);}
479
+ addEventHandler(internetExplorer ? document.body : window, "mouseup", cursorActivity);
480
+ addEventHandler(document.body, "cut", cursorActivity);
481
+
482
+ // workaround for a gecko bug [?] where going forward and then
483
+ // back again breaks designmode (no more cursor)
484
+ if (gecko)
485
+ addEventHandler(window, "pagehide", function(){self.unloaded = true;});
486
+
487
+ addEventHandler(document.body, "paste", function(event) {
488
+ cursorActivity();
489
+ var text = null;
490
+ try {
491
+ var clipboardData = event.clipboardData || window.clipboardData;
492
+ if (clipboardData) text = clipboardData.getData('Text');
493
+ }
494
+ catch(e) {}
495
+ if (text !== null) {
496
+ event.stop();
497
+ self.replaceSelection(text);
498
+ select.scrollToCursor(self.container);
499
+ }
500
+ });
501
+
502
+ if (this.options.autoMatchParens)
503
+ addEventHandler(document.body, "click", method(this, "scheduleParenHighlight"));
504
+ }
505
+ else if (!options.textWrapping) {
506
+ container.style.whiteSpace = "nowrap";
507
+ }
508
+ }
509
+
510
+ function isSafeKey(code) {
511
+ return (code >= 16 && code <= 18) || // shift, control, alt
512
+ (code >= 33 && code <= 40); // arrows, home, end
513
+ }
514
+
515
+ Editor.prototype = {
516
+ // Import a piece of code into the editor.
517
+ importCode: function(code) {
518
+ var lines = asEditorLines(code), chunk = 1000;
519
+ if (!this.options.incrementalLoading || lines.length < chunk) {
520
+ this.history.push(null, null, lines);
521
+ this.history.reset();
522
+ }
523
+ else {
524
+ var cur = 0, self = this;
525
+ function addChunk() {
526
+ var chunklines = lines.slice(cur, cur + chunk);
527
+ chunklines.push("");
528
+ self.history.push(self.history.nodeBefore(null), null, chunklines);
529
+ self.history.reset();
530
+ cur += chunk;
531
+ if (cur < lines.length)
532
+ parent.setTimeout(addChunk, 1000);
533
+ }
534
+ addChunk();
535
+ }
536
+ },
537
+
538
+ // Extract the code from the editor.
539
+ getCode: function() {
540
+ if (!this.container.firstChild)
541
+ return "";
542
+
543
+ var accum = [];
544
+ select.markSelection();
545
+ forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
546
+ select.selectMarked();
547
+ // On webkit, don't count last (empty) line if the webkitLastLineHack BR is present
548
+ if (webkit && this.container.lastChild.hackBR)
549
+ accum.pop();
550
+ webkitLastLineHack(this.container);
551
+ return cleanText(accum.join(""));
552
+ },
553
+
554
+ checkLine: function(node) {
555
+ if (node === false || !(node == null || node.parentNode == this.container || node.hackBR))
556
+ throw parent.CodeMirror.InvalidLineHandle;
557
+ },
558
+
559
+ cursorPosition: function(start) {
560
+ if (start == null) start = true;
561
+ var pos = select.cursorPos(this.container, start);
562
+ if (pos) return {line: pos.node, character: pos.offset};
563
+ else return {line: null, character: 0};
564
+ },
565
+
566
+ firstLine: function() {
567
+ return null;
568
+ },
569
+
570
+ lastLine: function() {
571
+ var last = this.container.lastChild;
572
+ if (last) last = startOfLine(last);
573
+ if (last && last.hackBR) last = startOfLine(last.previousSibling);
574
+ return last;
575
+ },
576
+
577
+ nextLine: function(line) {
578
+ this.checkLine(line);
579
+ var end = endOfLine(line, this.container);
580
+ if (!end || end.hackBR) return false;
581
+ else return end;
582
+ },
583
+
584
+ prevLine: function(line) {
585
+ this.checkLine(line);
586
+ if (line == null) return false;
587
+ return startOfLine(line.previousSibling);
588
+ },
589
+
590
+ visibleLineCount: function() {
591
+ var line = this.container.firstChild;
592
+ while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable
593
+ if (!line) return false;
594
+ var innerHeight = (window.innerHeight
595
+ || document.documentElement.clientHeight
596
+ || document.body.clientHeight);
597
+ return Math.floor(innerHeight / line.offsetHeight);
598
+ },
599
+
600
+ selectLines: function(startLine, startOffset, endLine, endOffset) {
601
+ this.checkLine(startLine);
602
+ var start = {node: startLine, offset: startOffset}, end = null;
603
+ if (endOffset !== undefined) {
604
+ this.checkLine(endLine);
605
+ end = {node: endLine, offset: endOffset};
606
+ }
607
+ select.setCursorPos(this.container, start, end);
608
+ select.scrollToCursor(this.container);
609
+ },
610
+
611
+ lineContent: function(line) {
612
+ var accum = [];
613
+ for (line = line ? line.nextSibling : this.container.firstChild;
614
+ line && !isBR(line); line = line.nextSibling)
615
+ accum.push(nodeText(line));
616
+ return cleanText(accum.join(""));
617
+ },
618
+
619
+ setLineContent: function(line, content) {
620
+ this.history.commit();
621
+ this.replaceRange({node: line, offset: 0},
622
+ {node: line, offset: this.history.textAfter(line).length},
623
+ content);
624
+ this.addDirtyNode(line);
625
+ this.scheduleHighlight();
626
+ },
627
+
628
+ removeLine: function(line) {
629
+ var node = line ? line.nextSibling : this.container.firstChild;
630
+ while (node) {
631
+ var next = node.nextSibling;
632
+ removeElement(node);
633
+ if (isBR(node)) break;
634
+ node = next;
635
+ }
636
+ this.addDirtyNode(line);
637
+ this.scheduleHighlight();
638
+ },
639
+
640
+ insertIntoLine: function(line, position, content) {
641
+ var before = null;
642
+ if (position == "end") {
643
+ before = endOfLine(line, this.container);
644
+ }
645
+ else {
646
+ for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
647
+ if (position == 0) {
648
+ before = cur;
649
+ break;
650
+ }
651
+ var text = nodeText(cur);
652
+ if (text.length > position) {
653
+ before = cur.nextSibling;
654
+ content = text.slice(0, position) + content + text.slice(position);
655
+ removeElement(cur);
656
+ break;
657
+ }
658
+ position -= text.length;
659
+ }
660
+ }
661
+
662
+ var lines = asEditorLines(content);
663
+ for (var i = 0; i < lines.length; i++) {
664
+ if (i > 0) this.container.insertBefore(document.createElement("BR"), before);
665
+ this.container.insertBefore(makePartSpan(lines[i]), before);
666
+ }
667
+ this.addDirtyNode(line);
668
+ this.scheduleHighlight();
669
+ },
670
+
671
+ // Retrieve the selected text.
672
+ selectedText: function() {
673
+ var h = this.history;
674
+ h.commit();
675
+
676
+ var start = select.cursorPos(this.container, true),
677
+ end = select.cursorPos(this.container, false);
678
+ if (!start || !end) return "";
679
+
680
+ if (start.node == end.node)
681
+ return h.textAfter(start.node).slice(start.offset, end.offset);
682
+
683
+ var text = [h.textAfter(start.node).slice(start.offset)];
684
+ for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
685
+ text.push(h.textAfter(pos));
686
+ text.push(h.textAfter(end.node).slice(0, end.offset));
687
+ return cleanText(text.join("\n"));
688
+ },
689
+
690
+ // Replace the selection with another piece of text.
691
+ replaceSelection: function(text) {
692
+ this.history.commit();
693
+
694
+ var start = select.cursorPos(this.container, true),
695
+ end = select.cursorPos(this.container, false);
696
+ if (!start || !end) return;
697
+
698
+ end = this.replaceRange(start, end, text);
699
+ select.setCursorPos(this.container, end);
700
+ webkitLastLineHack(this.container);
701
+ },
702
+
703
+ cursorCoords: function(start, internal) {
704
+ var sel = select.cursorPos(this.container, start);
705
+ if (!sel) return null;
706
+ var off = sel.offset, node = sel.node, self = this;
707
+ function measureFromNode(node, xOffset) {
708
+ var y = -(document.body.scrollTop || document.documentElement.scrollTop || 0),
709
+ x = -(document.body.scrollLeft || document.documentElement.scrollLeft || 0) + xOffset;
710
+ forEach([node, internal ? null : window.frameElement], function(n) {
711
+ while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;}
712
+ });
713
+ return {x: x, y: y, yBot: y + node.offsetHeight};
714
+ }
715
+ function withTempNode(text, f) {
716
+ var node = document.createElement("SPAN");
717
+ node.appendChild(document.createTextNode(text));
718
+ try {return f(node);}
719
+ finally {if (node.parentNode) node.parentNode.removeChild(node);}
720
+ }
721
+
722
+ while (off) {
723
+ node = node ? node.nextSibling : this.container.firstChild;
724
+ var txt = nodeText(node);
725
+ if (off < txt.length)
726
+ return withTempNode(txt.substr(0, off), function(tmp) {
727
+ tmp.style.position = "absolute"; tmp.style.visibility = "hidden";
728
+ tmp.className = node.className;
729
+ self.container.appendChild(tmp);
730
+ return measureFromNode(node, tmp.offsetWidth);
731
+ });
732
+ off -= txt.length;
733
+ }
734
+ if (node && isSpan(node))
735
+ return measureFromNode(node, node.offsetWidth);
736
+ else if (node && node.nextSibling && isSpan(node.nextSibling))
737
+ return measureFromNode(node.nextSibling, 0);
738
+ else
739
+ return withTempNode("\u200b", function(tmp) {
740
+ if (node) node.parentNode.insertBefore(tmp, node.nextSibling);
741
+ else self.container.insertBefore(tmp, self.container.firstChild);
742
+ return measureFromNode(tmp, 0);
743
+ });
744
+ },
745
+
746
+ reroutePasteEvent: function() {
747
+ if (this.capturingPaste || window.opera || (gecko && gecko >= 20101026)) return;
748
+ this.capturingPaste = true;
749
+ var te = window.frameElement.CodeMirror.textareaHack;
750
+ var coords = this.cursorCoords(true, true);
751
+ te.style.top = coords.y + "px";
752
+ if (internetExplorer) {
753
+ var snapshot = select.getBookmark(this.container);
754
+ if (snapshot) this.selectionSnapshot = snapshot;
755
+ }
756
+ parent.focus();
757
+ te.value = "";
758
+ te.focus();
759
+
760
+ var self = this;
761
+ parent.setTimeout(function() {
762
+ self.capturingPaste = false;
763
+ window.focus();
764
+ if (self.selectionSnapshot) // IE hack
765
+ window.select.setBookmark(self.container, self.selectionSnapshot);
766
+ var text = te.value;
767
+ if (text) {
768
+ self.replaceSelection(text);
769
+ select.scrollToCursor(self.container);
770
+ }
771
+ }, 10);
772
+ },
773
+
774
+ replaceRange: function(from, to, text) {
775
+ var lines = asEditorLines(text);
776
+ lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
777
+ var lastLine = lines[lines.length - 1];
778
+ lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
779
+ var end = this.history.nodeAfter(to.node);
780
+ this.history.push(from.node, end, lines);
781
+ return {node: this.history.nodeBefore(end),
782
+ offset: lastLine.length};
783
+ },
784
+
785
+ getSearchCursor: function(string, fromCursor, caseFold) {
786
+ return new SearchCursor(this, string, fromCursor, caseFold);
787
+ },
788
+
789
+ // Re-indent the whole buffer
790
+ reindent: function() {
791
+ if (this.container.firstChild)
792
+ this.indentRegion(null, this.container.lastChild);
793
+ },
794
+
795
+ reindentSelection: function(direction) {
796
+ if (!select.somethingSelected()) {
797
+ this.indentAtCursor(direction);
798
+ }
799
+ else {
800
+ var start = select.selectionTopNode(this.container, true),
801
+ end = select.selectionTopNode(this.container, false);
802
+ if (start === false || end === false) return;
803
+ this.indentRegion(start, end, direction, true);
804
+ }
805
+ },
806
+
807
+ grabKeys: function(eventHandler, filter) {
808
+ this.frozen = eventHandler;
809
+ this.keyFilter = filter;
810
+ },
811
+ ungrabKeys: function() {
812
+ this.frozen = "leave";
813
+ },
814
+
815
+ setParser: function(name, parserConfig) {
816
+ Editor.Parser = window[name];
817
+ parserConfig = parserConfig || this.options.parserConfig;
818
+ if (parserConfig && Editor.Parser.configure)
819
+ Editor.Parser.configure(parserConfig);
820
+
821
+ if (this.container.firstChild) {
822
+ forEach(this.container.childNodes, function(n) {
823
+ if (n.nodeType != 3) n.dirty = true;
824
+ });
825
+ this.addDirtyNode(this.firstChild);
826
+ this.scheduleHighlight();
827
+ }
828
+ },
829
+
830
+ // Intercept enter and tab, and assign their new functions.
831
+ keyDown: function(event) {
832
+ if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;}
833
+ if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) {
834
+ event.stop();
835
+ this.frozen(event);
836
+ return;
837
+ }
838
+
839
+ var code = event.keyCode;
840
+ // Don't scan when the user is typing.
841
+ this.delayScanning();
842
+ // Schedule a paren-highlight event, if configured.
843
+ if (this.options.autoMatchParens)
844
+ this.scheduleParenHighlight();
845
+
846
+ // The various checks for !altKey are there because AltGr sets both
847
+ // ctrlKey and altKey to true, and should not be recognised as
848
+ // Control.
849
+ if (code == 13) { // enter
850
+ if (event.ctrlKey && !event.altKey) {
851
+ this.reparseBuffer();
852
+ }
853
+ else {
854
+ select.insertNewlineAtCursor();
855
+ var mode = this.options.enterMode;
856
+ if (mode != "flat") this.indentAtCursor(mode == "keep" ? "keep" : undefined);
857
+ select.scrollToCursor(this.container);
858
+ }
859
+ event.stop();
860
+ }
861
+ else if (code == 9 && this.options.tabMode != "default" && !event.ctrlKey) { // tab
862
+ this.handleTab(!event.shiftKey);
863
+ event.stop();
864
+ }
865
+ else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
866
+ this.handleTab(true);
867
+ event.stop();
868
+ }
869
+ else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
870
+ if (this.home()) event.stop();
871
+ }
872
+ else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end
873
+ if (this.end()) event.stop();
874
+ }
875
+ // Only in Firefox is the default behavior for PgUp/PgDn correct.
876
+ else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp
877
+ if (this.pageUp()) event.stop();
878
+ }
879
+ else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgDn
880
+ if (this.pageDown()) event.stop();
881
+ }
882
+ else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
883
+ this.highlightParens(event.shiftKey, true);
884
+ event.stop();
885
+ }
886
+ else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
887
+ var cursor = select.selectionTopNode(this.container);
888
+ if (cursor === false || !this.container.firstChild) return;
889
+
890
+ if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
891
+ else {
892
+ var end = endOfLine(cursor, this.container);
893
+ select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
894
+ }
895
+ event.stop();
896
+ }
897
+ else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
898
+ if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
899
+ select.scrollToNode(this.history.redo());
900
+ event.stop();
901
+ }
902
+ else if (code == 90 || (safari && code == 8)) { // Z, backspace
903
+ select.scrollToNode(this.history.undo());
904
+ event.stop();
905
+ }
906
+ else if (code == 83 && this.options.saveFunction) { // S
907
+ this.options.saveFunction();
908
+ event.stop();
909
+ }
910
+ else if (code == 86 && !mac) { // V
911
+ this.reroutePasteEvent();
912
+ }
913
+ }
914
+ },
915
+
916
+ // Check for characters that should re-indent the current line,
917
+ // and prevent Opera from handling enter and tab anyway.
918
+ keyPress: function(event) {
919
+ var electric = this.options.electricChars && Editor.Parser.electricChars, self = this;
920
+ // Hack for Opera, and Firefox on OS X, in which stopping a
921
+ // keydown event does not prevent the associated keypress event
922
+ // from happening, so we have to cancel enter and tab again
923
+ // here.
924
+ if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) ||
925
+ event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
926
+ (event.code == 32 && event.shiftKey && this.options.tabMode == "default"))
927
+ event.stop();
928
+ else if (mac && (event.ctrlKey || event.metaKey) && event.character == "v") {
929
+ this.reroutePasteEvent();
930
+ }
931
+ else if (electric && electric.indexOf(event.character) != -1)
932
+ parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
933
+ // Work around a bug where pressing backspace at the end of a
934
+ // line, or delete at the start, often causes the cursor to jump
935
+ // to the start of the line in Opera 10.60.
936
+ else if (brokenOpera) {
937
+ if (event.code == 8) { // backspace
938
+ var sel = select.selectionTopNode(this.container), self = this,
939
+ next = sel ? sel.nextSibling : this.container.firstChild;
940
+ if (sel !== false && next && isBR(next))
941
+ parent.setTimeout(function(){
942
+ if (select.selectionTopNode(self.container) == next)
943
+ select.focusAfterNode(next.previousSibling, self.container);
944
+ }, 20);
945
+ }
946
+ else if (event.code == 46) { // delete
947
+ var sel = select.selectionTopNode(this.container), self = this;
948
+ if (sel && isBR(sel)) {
949
+ parent.setTimeout(function(){
950
+ if (select.selectionTopNode(self.container) != sel)
951
+ select.focusAfterNode(sel, self.container);
952
+ }, 20);
953
+ }
954
+ }
955
+ }
956
+ // In 533.* WebKit versions, when the document is big, typing
957
+ // something at the end of a line causes the browser to do some
958
+ // kind of stupid heavy operation, creating delays of several
959
+ // seconds before the typed characters appear. This very crude
960
+ // hack inserts a temporary zero-width space after the cursor to
961
+ // make it not be at the end of the line.
962
+ else if (slowWebkit) {
963
+ var sel = select.selectionTopNode(this.container),
964
+ next = sel ? sel.nextSibling : this.container.firstChild;
965
+ // Doesn't work on empty lines, for some reason those always
966
+ // trigger the delay.
967
+ if (sel && next && isBR(next) && !isBR(sel)) {
968
+ var cheat = document.createTextNode("\u200b");
969
+ this.container.insertBefore(cheat, next);
970
+ parent.setTimeout(function() {
971
+ if (cheat.nodeValue == "\u200b") removeElement(cheat);
972
+ else cheat.nodeValue = cheat.nodeValue.replace("\u200b", "");
973
+ }, 20);
974
+ }
975
+ }
976
+
977
+ // Magic incantation that works abound a webkit bug when you
978
+ // can't type on a blank line following a line that's wider than
979
+ // the window.
980
+ if (webkit && !this.options.textWrapping)
981
+ setTimeout(function () {
982
+ var node = select.selectionTopNode(self.container, true);
983
+ if (node && node.nodeType == 3 && node.previousSibling && isBR(node.previousSibling)
984
+ && node.nextSibling && isBR(node.nextSibling))
985
+ node.parentNode.replaceChild(document.createElement("BR"), node.previousSibling);
986
+ }, 50);
987
+ },
988
+
989
+ // Mark the node at the cursor dirty when a non-safe key is
990
+ // released.
991
+ keyUp: function(event) {
992
+ this.cursorActivity(isSafeKey(event.keyCode));
993
+ },
994
+
995
+ // Indent the line following a given <br>, or null for the first
996
+ // line. If given a <br> element, this must have been highlighted
997
+ // so that it has an indentation method. Returns the whitespace
998
+ // element that has been modified or created (if any).
999
+ indentLineAfter: function(start, direction) {
1000
+ function whiteSpaceAfter(node) {
1001
+ var ws = node ? node.nextSibling : self.container.firstChild;
1002
+ if (!ws || !hasClass(ws, "whitespace")) return null;
1003
+ return ws;
1004
+ }
1005
+
1006
+ // whiteSpace is the whitespace span at the start of the line,
1007
+ // or null if there is no such node.
1008
+ var self = this, whiteSpace = whiteSpaceAfter(start);
1009
+ var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
1010
+
1011
+ var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
1012
+ if (direction == "keep") {
1013
+ if (start) {
1014
+ var prevWS = whiteSpaceAfter(startOfLine(start.previousSibling))
1015
+ if (prevWS) newIndent = prevWS.currentText.length;
1016
+ }
1017
+ }
1018
+ else {
1019
+ // Sometimes the start of the line can influence the correct
1020
+ // indentation, so we retrieve it.
1021
+ var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
1022
+
1023
+ // Ask the lexical context for the correct indentation, and
1024
+ // compute how much this differs from the current indentation.
1025
+ if (direction != null && this.options.tabMode != "indent")
1026
+ newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
1027
+ else if (start)
1028
+ newIndent = start.indentation(nextChars, curIndent, direction, firstText);
1029
+ else if (Editor.Parser.firstIndentation)
1030
+ newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction, firstText);
1031
+ }
1032
+
1033
+ var indentDiff = newIndent - curIndent;
1034
+
1035
+ // If there is too much, this is just a matter of shrinking a span.
1036
+ if (indentDiff < 0) {
1037
+ if (newIndent == 0) {
1038
+ if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild || firstText, 0);
1039
+ removeElement(whiteSpace);
1040
+ whiteSpace = null;
1041
+ }
1042
+ else {
1043
+ select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
1044
+ whiteSpace.currentText = makeWhiteSpace(newIndent);
1045
+ whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
1046
+ }
1047
+ }
1048
+ // Not enough...
1049
+ else if (indentDiff > 0) {
1050
+ // If there is whitespace, we grow it.
1051
+ if (whiteSpace) {
1052
+ whiteSpace.currentText = makeWhiteSpace(newIndent);
1053
+ whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
1054
+ select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
1055
+ }
1056
+ // Otherwise, we have to add a new whitespace node.
1057
+ else {
1058
+ whiteSpace = makePartSpan(makeWhiteSpace(newIndent));
1059
+ whiteSpace.className = "whitespace";
1060
+ if (start) insertAfter(whiteSpace, start);
1061
+ else this.container.insertBefore(whiteSpace, this.container.firstChild);
1062
+ select.snapshotMove(firstText && (firstText.firstChild || firstText),
1063
+ whiteSpace.firstChild, newIndent, false, true);
1064
+ }
1065
+ }
1066
+ // Make sure cursor ends up after the whitespace
1067
+ else if (whiteSpace) {
1068
+ select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, newIndent, false);
1069
+ }
1070
+ if (indentDiff != 0) this.addDirtyNode(start);
1071
+ },
1072
+
1073
+ // Re-highlight the selected part of the document.
1074
+ highlightAtCursor: function() {
1075
+ var pos = select.selectionTopNode(this.container, true);
1076
+ var to = select.selectionTopNode(this.container, false);
1077
+ if (pos === false || to === false) return false;
1078
+
1079
+ select.markSelection();
1080
+ if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
1081
+ return false;
1082
+ select.selectMarked();
1083
+ return true;
1084
+ },
1085
+
1086
+ // When tab is pressed with text selected, the whole selection is
1087
+ // re-indented, when nothing is selected, the line with the cursor
1088
+ // is re-indented.
1089
+ handleTab: function(direction) {
1090
+ if (this.options.tabMode == "spaces" && !select.somethingSelected())
1091
+ select.insertTabAtCursor();
1092
+ else
1093
+ this.reindentSelection(direction);
1094
+ },
1095
+
1096
+ // Custom home behaviour that doesn't land the cursor in front of
1097
+ // leading whitespace unless pressed twice.
1098
+ home: function() {
1099
+ var cur = select.selectionTopNode(this.container, true), start = cur;
1100
+ if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild)
1101
+ return false;
1102
+
1103
+ while (cur && !isBR(cur)) cur = cur.previousSibling;
1104
+ var next = cur ? cur.nextSibling : this.container.firstChild;
1105
+ if (next && next != start && next.isPart && hasClass(next, "whitespace"))
1106
+ select.focusAfterNode(next, this.container);
1107
+ else
1108
+ select.focusAfterNode(cur, this.container);
1109
+
1110
+ select.scrollToCursor(this.container);
1111
+ return true;
1112
+ },
1113
+
1114
+ // Some browsers (Opera) don't manage to handle the end key
1115
+ // properly in the face of vertical scrolling.
1116
+ end: function() {
1117
+ var cur = select.selectionTopNode(this.container, true);
1118
+ if (cur === false) return false;
1119
+ cur = endOfLine(cur, this.container);
1120
+ if (!cur) return false;
1121
+ select.focusAfterNode(cur.previousSibling, this.container);
1122
+ select.scrollToCursor(this.container);
1123
+ return true;
1124
+ },
1125
+
1126
+ pageUp: function() {
1127
+ var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
1128
+ if (line === false || scrollAmount === false) return false;
1129
+ // Try to keep one line on the screen.
1130
+ scrollAmount -= 2;
1131
+ for (var i = 0; i < scrollAmount; i++) {
1132
+ line = this.prevLine(line);
1133
+ if (line === false) break;
1134
+ }
1135
+ if (i == 0) return false; // Already at first line
1136
+ select.setCursorPos(this.container, {node: line, offset: 0});
1137
+ select.scrollToCursor(this.container);
1138
+ return true;
1139
+ },
1140
+
1141
+ pageDown: function() {
1142
+ var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
1143
+ if (line === false || scrollAmount === false) return false;
1144
+ // Try to move to the last line of the current page.
1145
+ scrollAmount -= 2;
1146
+ for (var i = 0; i < scrollAmount; i++) {
1147
+ var nextLine = this.nextLine(line);
1148
+ if (nextLine === false) break;
1149
+ line = nextLine;
1150
+ }
1151
+ if (i == 0) return false; // Already at last line
1152
+ select.setCursorPos(this.container, {node: line, offset: 0});
1153
+ select.scrollToCursor(this.container);
1154
+ return true;
1155
+ },
1156
+
1157
+ // Delay (or initiate) the next paren highlight event.
1158
+ scheduleParenHighlight: function() {
1159
+ if (this.parenEvent) parent.clearTimeout(this.parenEvent);
1160
+ var self = this;
1161
+ this.parenEvent = parent.setTimeout(function(){self.highlightParens();}, 300);
1162
+ },
1163
+
1164
+ // Take the token before the cursor. If it contains a character in
1165
+ // '()[]{}', search for the matching paren/brace/bracket, and
1166
+ // highlight them in green for a moment, or red if no proper match
1167
+ // was found.
1168
+ highlightParens: function(jump, fromKey) {
1169
+ var self = this, mark = this.options.markParen;
1170
+ if (typeof mark == "string") mark = [mark, mark];
1171
+ // give the relevant nodes a colour.
1172
+ function highlight(node, ok) {
1173
+ if (!node) return;
1174
+ if (!mark) {
1175
+ node.style.fontWeight = "bold";
1176
+ node.style.color = ok ? "#8F8" : "#F88";
1177
+ }
1178
+ else if (mark.call) mark(node, ok);
1179
+ else node.className += " " + mark[ok ? 0 : 1];
1180
+ }
1181
+ function unhighlight(node) {
1182
+ if (!node) return;
1183
+ if (mark && !mark.call)
1184
+ removeClass(removeClass(node, mark[0]), mark[1]);
1185
+ else if (self.options.unmarkParen)
1186
+ self.options.unmarkParen(node);
1187
+ else {
1188
+ node.style.fontWeight = "";
1189
+ node.style.color = "";
1190
+ }
1191
+ }
1192
+ if (!fromKey && self.highlighted) {
1193
+ unhighlight(self.highlighted[0]);
1194
+ unhighlight(self.highlighted[1]);
1195
+ }
1196
+
1197
+ if (!window || !window.parent || !window.select) return;
1198
+ // Clear the event property.
1199
+ if (this.parenEvent) parent.clearTimeout(this.parenEvent);
1200
+ this.parenEvent = null;
1201
+
1202
+ // Extract a 'paren' from a piece of text.
1203
+ function paren(node) {
1204
+ if (node.currentText) {
1205
+ var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
1206
+ return match && match[1];
1207
+ }
1208
+ }
1209
+ // Determine the direction a paren is facing.
1210
+ function forward(ch) {
1211
+ return /[\(\[\{]/.test(ch);
1212
+ }
1213
+
1214
+ var ch, cursor = select.selectionTopNode(this.container, true);
1215
+ if (!cursor || !this.highlightAtCursor()) return;
1216
+ cursor = select.selectionTopNode(this.container, true);
1217
+ if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
1218
+ return;
1219
+ // We only look for tokens with the same className.
1220
+ var className = cursor.className, dir = forward(ch), match = matching[ch];
1221
+
1222
+ // Since parts of the document might not have been properly
1223
+ // highlighted, and it is hard to know in advance which part we
1224
+ // have to scan, we just try, and when we find dirty nodes we
1225
+ // abort, parse them, and re-try.
1226
+ function tryFindMatch() {
1227
+ var stack = [], ch, ok = true;
1228
+ for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
1229
+ if (runner.className == className && isSpan(runner) && (ch = paren(runner))) {
1230
+ if (forward(ch) == dir)
1231
+ stack.push(ch);
1232
+ else if (!stack.length)
1233
+ ok = false;
1234
+ else if (stack.pop() != matching[ch])
1235
+ ok = false;
1236
+ if (!stack.length) break;
1237
+ }
1238
+ else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
1239
+ return {node: runner, status: "dirty"};
1240
+ }
1241
+ }
1242
+ return {node: runner, status: runner && ok};
1243
+ }
1244
+
1245
+ while (true) {
1246
+ var found = tryFindMatch();
1247
+ if (found.status == "dirty") {
1248
+ this.highlight(found.node, endOfLine(found.node));
1249
+ // Needed because in some corner cases a highlight does not
1250
+ // reach a node.
1251
+ found.node.dirty = false;
1252
+ continue;
1253
+ }
1254
+ else {
1255
+ highlight(cursor, found.status);
1256
+ highlight(found.node, found.status);
1257
+ if (fromKey)
1258
+ parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500);
1259
+ else
1260
+ self.highlighted = [cursor, found.node];
1261
+ if (jump && found.node)
1262
+ select.focusAfterNode(found.node.previousSibling, this.container);
1263
+ break;
1264
+ }
1265
+ }
1266
+ },
1267
+
1268
+ // Adjust the amount of whitespace at the start of the line that
1269
+ // the cursor is on so that it is indented properly.
1270
+ indentAtCursor: function(direction) {
1271
+ if (!this.container.firstChild) return;
1272
+ // The line has to have up-to-date lexical information, so we
1273
+ // highlight it first.
1274
+ if (!this.highlightAtCursor()) return;
1275
+ var cursor = select.selectionTopNode(this.container, false);
1276
+ // If we couldn't determine the place of the cursor,
1277
+ // there's nothing to indent.
1278
+ if (cursor === false)
1279
+ return;
1280
+ select.markSelection();
1281
+ this.indentLineAfter(startOfLine(cursor), direction);
1282
+ select.selectMarked();
1283
+ },
1284
+
1285
+ // Indent all lines whose start falls inside of the current
1286
+ // selection.
1287
+ indentRegion: function(start, end, direction, selectAfter) {
1288
+ var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
1289
+ if (!isBR(end)) end = endOfLine(end, this.container);
1290
+ this.addDirtyNode(start);
1291
+
1292
+ do {
1293
+ var next = endOfLine(current, this.container);
1294
+ if (current) this.highlight(before, next, true);
1295
+ this.indentLineAfter(current, direction);
1296
+ before = current;
1297
+ current = next;
1298
+ } while (current != end);
1299
+ if (selectAfter)
1300
+ select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
1301
+ },
1302
+
1303
+ // Find the node that the cursor is in, mark it as dirty, and make
1304
+ // sure a highlight pass is scheduled.
1305
+ cursorActivity: function(safe) {
1306
+ // pagehide event hack above
1307
+ if (this.unloaded) {
1308
+ window.document.designMode = "off";
1309
+ window.document.designMode = "on";
1310
+ this.unloaded = false;
1311
+ }
1312
+
1313
+ if (internetExplorer) {
1314
+ this.container.createTextRange().execCommand("unlink");
1315
+ clearTimeout(this.saveSelectionSnapshot);
1316
+ var self = this;
1317
+ this.saveSelectionSnapshot = setTimeout(function() {
1318
+ var snapshot = select.getBookmark(self.container);
1319
+ if (snapshot) self.selectionSnapshot = snapshot;
1320
+ }, 200);
1321
+ }
1322
+
1323
+ var activity = this.options.onCursorActivity;
1324
+ if (!safe || activity) {
1325
+ var cursor = select.selectionTopNode(this.container, false);
1326
+ if (cursor === false || !this.container.firstChild) return;
1327
+ cursor = cursor || this.container.firstChild;
1328
+ if (activity) activity(cursor);
1329
+ if (!safe) {
1330
+ this.scheduleHighlight();
1331
+ this.addDirtyNode(cursor);
1332
+ }
1333
+ }
1334
+ },
1335
+
1336
+ reparseBuffer: function() {
1337
+ forEach(this.container.childNodes, function(node) {node.dirty = true;});
1338
+ if (this.container.firstChild)
1339
+ this.addDirtyNode(this.container.firstChild);
1340
+ },
1341
+
1342
+ // Add a node to the set of dirty nodes, if it isn't already in
1343
+ // there.
1344
+ addDirtyNode: function(node) {
1345
+ node = node || this.container.firstChild;
1346
+ if (!node) return;
1347
+
1348
+ for (var i = 0; i < this.dirty.length; i++)
1349
+ if (this.dirty[i] == node) return;
1350
+
1351
+ if (node.nodeType != 3)
1352
+ node.dirty = true;
1353
+ this.dirty.push(node);
1354
+ },
1355
+
1356
+ allClean: function() {
1357
+ return !this.dirty.length;
1358
+ },
1359
+
1360
+ // Cause a highlight pass to happen in options.passDelay
1361
+ // milliseconds. Clear the existing timeout, if one exists. This
1362
+ // way, the passes do not happen while the user is typing, and
1363
+ // should as unobtrusive as possible.
1364
+ scheduleHighlight: function() {
1365
+ // Timeouts are routed through the parent window, because on
1366
+ // some browsers designMode windows do not fire timeouts.
1367
+ var self = this;
1368
+ parent.clearTimeout(this.highlightTimeout);
1369
+ this.highlightTimeout = parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
1370
+ },
1371
+
1372
+ // Fetch one dirty node, and remove it from the dirty set.
1373
+ getDirtyNode: function() {
1374
+ while (this.dirty.length > 0) {
1375
+ var found = this.dirty.pop();
1376
+ // IE8 sometimes throws an unexplainable 'invalid argument'
1377
+ // exception for found.parentNode
1378
+ try {
1379
+ // If the node has been coloured in the meantime, or is no
1380
+ // longer in the document, it should not be returned.
1381
+ while (found && found.parentNode != this.container)
1382
+ found = found.parentNode;
1383
+ if (found && (found.dirty || found.nodeType == 3))
1384
+ return found;
1385
+ } catch (e) {}
1386
+ }
1387
+ return null;
1388
+ },
1389
+
1390
+ // Pick dirty nodes, and highlight them, until options.passTime
1391
+ // milliseconds have gone by. The highlight method will continue
1392
+ // to next lines as long as it finds dirty nodes. It returns
1393
+ // information about the place where it stopped. If there are
1394
+ // dirty nodes left after this function has spent all its lines,
1395
+ // it shedules another highlight to finish the job.
1396
+ highlightDirty: function(force) {
1397
+ // Prevent FF from raising an error when it is firing timeouts
1398
+ // on a page that's no longer loaded.
1399
+ if (!window || !window.parent || !window.select) return false;
1400
+
1401
+ if (!this.options.readOnly) select.markSelection();
1402
+ var start, endTime = force ? null : time() + this.options.passTime;
1403
+ while ((time() < endTime || force) && (start = this.getDirtyNode())) {
1404
+ var result = this.highlight(start, endTime);
1405
+ if (result && result.node && result.dirty)
1406
+ this.addDirtyNode(result.node.nextSibling);
1407
+ }
1408
+ if (!this.options.readOnly) select.selectMarked();
1409
+ if (start) this.scheduleHighlight();
1410
+ return this.dirty.length == 0;
1411
+ },
1412
+
1413
+ // Creates a function that, when called through a timeout, will
1414
+ // continuously re-parse the document.
1415
+ documentScanner: function(passTime) {
1416
+ var self = this, pos = null;
1417
+ return function() {
1418
+ // FF timeout weirdness workaround.
1419
+ if (!window || !window.parent || !window.select) return;
1420
+ // If the current node is no longer in the document... oh
1421
+ // well, we start over.
1422
+ if (pos && pos.parentNode != self.container)
1423
+ pos = null;
1424
+ select.markSelection();
1425
+ var result = self.highlight(pos, time() + passTime, true);
1426
+ select.selectMarked();
1427
+ var newPos = result ? (result.node && result.node.nextSibling) : null;
1428
+ pos = (pos == newPos) ? null : newPos;
1429
+ self.delayScanning();
1430
+ };
1431
+ },
1432
+
1433
+ // Starts the continuous scanning process for this document after
1434
+ // a given interval.
1435
+ delayScanning: function() {
1436
+ if (this.scanner) {
1437
+ parent.clearTimeout(this.documentScan);
1438
+ this.documentScan = parent.setTimeout(this.scanner, this.options.continuousScanning);
1439
+ }
1440
+ },
1441
+
1442
+ // The function that does the actual highlighting/colouring (with
1443
+ // help from the parser and the DOM normalizer). Its interface is
1444
+ // rather overcomplicated, because it is used in different
1445
+ // situations: ensuring that a certain line is highlighted, or
1446
+ // highlighting up to X milliseconds starting from a certain
1447
+ // point. The 'from' argument gives the node at which it should
1448
+ // start. If this is null, it will start at the beginning of the
1449
+ // document. When a timestamp is given with the 'target' argument,
1450
+ // it will stop highlighting at that time. If this argument holds
1451
+ // a DOM node, it will highlight until it reaches that node. If at
1452
+ // any time it comes across two 'clean' lines (no dirty nodes), it
1453
+ // will stop, except when 'cleanLines' is true. maxBacktrack is
1454
+ // the maximum number of lines to backtrack to find an existing
1455
+ // parser instance. This is used to give up in situations where a
1456
+ // highlight would take too long and freeze the browser interface.
1457
+ highlight: function(from, target, cleanLines, maxBacktrack){
1458
+ var container = this.container, self = this, active = this.options.activeTokens;
1459
+ var endTime = (typeof target == "number" ? target : null);
1460
+
1461
+ if (!container.firstChild)
1462
+ return false;
1463
+ // Backtrack to the first node before from that has a partial
1464
+ // parse stored.
1465
+ while (from && (!from.parserFromHere || from.dirty)) {
1466
+ if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0)
1467
+ return false;
1468
+ from = from.previousSibling;
1469
+ }
1470
+ // If we are at the end of the document, do nothing.
1471
+ if (from && !from.nextSibling)
1472
+ return false;
1473
+
1474
+ // Check whether a part (<span> node) and the corresponding token
1475
+ // match.
1476
+ function correctPart(token, part){
1477
+ return !part.reduced && part.currentText == token.value && part.className == token.style;
1478
+ }
1479
+ // Shorten the text associated with a part by chopping off
1480
+ // characters from the front. Note that only the currentText
1481
+ // property gets changed. For efficiency reasons, we leave the
1482
+ // nodeValue alone -- we set the reduced flag to indicate that
1483
+ // this part must be replaced.
1484
+ function shortenPart(part, minus){
1485
+ part.currentText = part.currentText.substring(minus);
1486
+ part.reduced = true;
1487
+ }
1488
+ // Create a part corresponding to a given token.
1489
+ function tokenPart(token){
1490
+ var part = makePartSpan(token.value);
1491
+ part.className = token.style;
1492
+ return part;
1493
+ }
1494
+
1495
+ function maybeTouch(node) {
1496
+ if (node) {
1497
+ var old = node.oldNextSibling;
1498
+ if (lineDirty || old === undefined || node.nextSibling != old)
1499
+ self.history.touch(node);
1500
+ node.oldNextSibling = node.nextSibling;
1501
+ }
1502
+ else {
1503
+ var old = self.container.oldFirstChild;
1504
+ if (lineDirty || old === undefined || self.container.firstChild != old)
1505
+ self.history.touch(null);
1506
+ self.container.oldFirstChild = self.container.firstChild;
1507
+ }
1508
+ }
1509
+
1510
+ // Get the token stream. If from is null, we start with a new
1511
+ // parser from the start of the frame, otherwise a partial parse
1512
+ // is resumed.
1513
+ var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
1514
+ stream = stringStream(traversal),
1515
+ parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
1516
+
1517
+ function surroundedByBRs(node) {
1518
+ return (node.previousSibling == null || isBR(node.previousSibling)) &&
1519
+ (node.nextSibling == null || isBR(node.nextSibling));
1520
+ }
1521
+
1522
+ // parts is an interface to make it possible to 'delay' fetching
1523
+ // the next DOM node until we are completely done with the one
1524
+ // before it. This is necessary because often the next node is
1525
+ // not yet available when we want to proceed past the current
1526
+ // one.
1527
+ var parts = {
1528
+ current: null,
1529
+ // Fetch current node.
1530
+ get: function(){
1531
+ if (!this.current)
1532
+ this.current = traversal.nodes.shift();
1533
+ return this.current;
1534
+ },
1535
+ // Advance to the next part (do not fetch it yet).
1536
+ next: function(){
1537
+ this.current = null;
1538
+ },
1539
+ // Remove the current part from the DOM tree, and move to the
1540
+ // next.
1541
+ remove: function(){
1542
+ container.removeChild(this.get());
1543
+ this.current = null;
1544
+ },
1545
+ // Advance to the next part that is not empty, discarding empty
1546
+ // parts.
1547
+ getNonEmpty: function(){
1548
+ var part = this.get();
1549
+ // Allow empty nodes when they are alone on a line, needed
1550
+ // for the FF cursor bug workaround (see select.js,
1551
+ // insertNewlineAtCursor).
1552
+ while (part && isSpan(part) && part.currentText == "") {
1553
+ // Leave empty nodes that are alone on a line alone in
1554
+ // Opera, since that browsers doesn't deal well with
1555
+ // having 2 BRs in a row.
1556
+ if (window.opera && surroundedByBRs(part)) {
1557
+ this.next();
1558
+ part = this.get();
1559
+ }
1560
+ else {
1561
+ var old = part;
1562
+ this.remove();
1563
+ part = this.get();
1564
+ // Adjust selection information, if any. See select.js for details.
1565
+ select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
1566
+ }
1567
+ }
1568
+
1569
+ return part;
1570
+ }
1571
+ };
1572
+
1573
+ var lineDirty = false, prevLineDirty = true, lineNodes = 0;
1574
+
1575
+ // This forEach loops over the tokens from the parsed stream, and
1576
+ // at the same time uses the parts object to proceed through the
1577
+ // corresponding DOM nodes.
1578
+ forEach(parsed, function(token){
1579
+ var part = parts.getNonEmpty();
1580
+
1581
+ if (token.value == "\n"){
1582
+ // The idea of the two streams actually staying synchronized
1583
+ // is such a long shot that we explicitly check.
1584
+ if (!isBR(part))
1585
+ throw "Parser out of sync. Expected BR.";
1586
+
1587
+ if (part.dirty || !part.indentation) lineDirty = true;
1588
+ maybeTouch(from);
1589
+ from = part;
1590
+
1591
+ // Every <br> gets a copy of the parser state and a lexical
1592
+ // context assigned to it. The first is used to be able to
1593
+ // later resume parsing from this point, the second is used
1594
+ // for indentation.
1595
+ part.parserFromHere = parsed.copy();
1596
+ part.indentation = token.indentation || alwaysZero;
1597
+ part.dirty = false;
1598
+
1599
+ // If the target argument wasn't an integer, go at least
1600
+ // until that node.
1601
+ if (endTime == null && part == target) throw StopIteration;
1602
+
1603
+ // A clean line with more than one node means we are done.
1604
+ // Throwing a StopIteration is the way to break out of a
1605
+ // MochiKit forEach loop.
1606
+ if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
1607
+ throw StopIteration;
1608
+ prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
1609
+ parts.next();
1610
+ }
1611
+ else {
1612
+ if (!isSpan(part))
1613
+ throw "Parser out of sync. Expected SPAN.";
1614
+ if (part.dirty)
1615
+ lineDirty = true;
1616
+ lineNodes++;
1617
+
1618
+ // If the part matches the token, we can leave it alone.
1619
+ if (correctPart(token, part)){
1620
+ if (active && part.dirty) active(part, token, self);
1621
+ part.dirty = false;
1622
+ parts.next();
1623
+ }
1624
+ // Otherwise, we have to fix it.
1625
+ else {
1626
+ lineDirty = true;
1627
+ // Insert the correct part.
1628
+ var newPart = tokenPart(token);
1629
+ container.insertBefore(newPart, part);
1630
+ if (active) active(newPart, token, self);
1631
+ var tokensize = token.value.length;
1632
+ var offset = 0;
1633
+ // Eat up parts until the text for this token has been
1634
+ // removed, adjusting the stored selection info (see
1635
+ // select.js) in the process.
1636
+ while (tokensize > 0) {
1637
+ part = parts.get();
1638
+ var partsize = part.currentText.length;
1639
+ select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
1640
+ if (partsize > tokensize){
1641
+ shortenPart(part, tokensize);
1642
+ tokensize = 0;
1643
+ }
1644
+ else {
1645
+ tokensize -= partsize;
1646
+ offset += partsize;
1647
+ parts.remove();
1648
+ }
1649
+ }
1650
+ }
1651
+ }
1652
+ });
1653
+ maybeTouch(from);
1654
+ webkitLastLineHack(this.container);
1655
+
1656
+ // The function returns some status information that is used by
1657
+ // hightlightDirty to determine whether and where it has to
1658
+ // continue.
1659
+ return {node: parts.getNonEmpty(),
1660
+ dirty: lineDirty};
1661
+ }
1662
+ };
1663
+
1664
+ return Editor;
1665
+ })();
1666
+
1667
+ addEventHandler(window, "load", function() {
1668
+ var CodeMirror = window.frameElement.CodeMirror;
1669
+ var e = CodeMirror.editor = new Editor(CodeMirror.options);
1670
+ parent.setTimeout(method(CodeMirror, "init"), 0);
1671
+ });