maglev-webtools 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ });