0
|
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 });
|