removed dependency on built-editor.min.js
[ccsdk/distribution.git] / dgbuilder / public / ace / ace-diff.js
1 (function(root, factory) {
2   if (typeof define === 'function' && define.amd) {
3     define([], factory);
4   } else if (typeof exports === 'object') {
5     module.exports = factory(require());
6   } else {
7     root.AceDiff = factory(root);
8   }
9 }(this, function() {
10   'use strict';
11
12   var Range = require('ace/range').Range;
13
14   var C = {
15     DIFF_EQUAL: 0,
16     DIFF_DELETE: -1,
17     DIFF_INSERT: 1,
18     EDITOR_RIGHT: 'right',
19     EDITOR_LEFT: 'left',
20     RTL: 'rtl',
21     LTR: 'ltr',
22     SVG_NS: 'http://www.w3.org/2000/svg',
23     DIFF_GRANULARITY_SPECIFIC: 'specific',
24     DIFF_GRANULARITY_BROAD: 'broad'
25   };
26
27   // our constructor
28   function AceDiff(options) {
29     this.options = {};
30
31     extend(true, this.options, {
32       mode: null,
33       theme: null,
34       diffGranularity: C.DIFF_GRANULARITY_BROAD,
35       lockScrolling: false, // not implemented yet
36       showDiffs: true,
37       showConnectors: true,
38       maxDiffs: 5000,
39       left: {
40         id: 'acediff-left-editor',
41         content: null,
42         mode: null,
43         theme: null,
44         editable: true,
45         copyLinkEnabled: true
46       },
47       right: {
48         id: 'acediff-right-editor',
49         content: null,
50         mode: null,
51         theme: null,
52         editable: true,
53         copyLinkEnabled: true
54       },
55       classes: {
56         gutterID: 'acediff-gutter',
57         diff: 'acediff-diff',
58         connector: 'acediff-connector',
59         newCodeConnectorLink: 'acediff-new-code-connector-copy',
60         newCodeConnectorLinkContent: '→',
61         deletedCodeConnectorLink: 'acediff-deleted-code-connector-copy',
62         deletedCodeConnectorLinkContent: '←',
63         copyRightContainer: 'acediff-copy-right',
64         copyLeftContainer: 'acediff-copy-left'
65       },
66       connectorYOffset: 0
67     }, options);
68
69     // instantiate the editors in an internal data structure that will store a little info about the diffs and
70     // editor content
71     this.editors = {
72       left: {
73         ace: ace.edit(this.options.left.id),
74         markers: [],
75         lineLengths: []
76       },
77       right: {
78         ace: ace.edit(this.options.right.id),
79         markers: [],
80         lineLengths: []
81       },
82       editorHeight: null
83     };
84
85     addEventHandlers(this);
86
87     this.lineHeight = this.editors.left.ace.renderer.lineHeight; // assumption: both editors have same line heights
88
89     // set up the editors
90     this.editors.left.ace.getSession().setMode(getMode(this, C.EDITOR_LEFT));
91     this.editors.right.ace.getSession().setMode(getMode(this, C.EDITOR_RIGHT));
92     this.editors.left.ace.setReadOnly(!this.options.left.editable);
93     this.editors.right.ace.setReadOnly(!this.options.right.editable);
94     this.editors.left.ace.setTheme(getTheme(this, C.EDITOR_LEFT));
95     this.editors.right.ace.setTheme(getTheme(this, C.EDITOR_RIGHT));
96
97     createCopyContainers(this);
98     createGutter(this);
99
100     // if the data is being supplied by an option, set the editor values now
101     if (this.options.left.content) {
102       this.editors.left.ace.setValue(this.options.left.content, -1);
103     }
104     if (this.options.right.content) {
105       this.editors.right.ace.setValue(this.options.right.content, -1);
106     }
107
108     // store the visible height of the editors (assumed the same)
109     this.editors.editorHeight = getEditorHeight(this);
110
111     this.diff();
112   }
113
114
115   // our public API
116   AceDiff.prototype = {
117
118     // allows on-the-fly changes to the AceDiff instance settings
119     setOptions: function(options) {
120       extend(true, this.options, options);
121       this.diff();
122     },
123
124     getNumDiffs: function() {
125       return this.diffs.length;
126     },
127
128     // exposes the Ace editors in case the dev needs it
129     getEditors: function() {
130       return {
131         left: this.editors.left.ace,
132         right: this.editors.right.ace
133       }
134     },
135
136     // our main diffing function. I actually don't think this needs to exposed: it's called automatically,
137     // but just to be safe, it's included
138     diff: function() {
139       var dmp = new diff_match_patch();
140       var val1 = this.editors.left.ace.getSession().getValue();
141       var val2 = this.editors.right.ace.getSession().getValue();
142       var diff = dmp.diff_main(val2, val1);
143       dmp.diff_cleanupSemantic(diff);
144
145       this.editors.left.lineLengths  = getLineLengths(this.editors.left);
146       this.editors.right.lineLengths = getLineLengths(this.editors.right);
147
148       // parse the raw diff into something a little more palatable
149       var diffs = [];
150       var offset = {
151         left: 0,
152         right: 0
153       };
154
155       diff.forEach(function(chunk) {
156         var chunkType = chunk[0];
157         var text = chunk[1];
158
159         // oddly, occasionally the algorithm returns a diff with no changes made
160         if (text.length === 0) {
161           return;
162         }
163         if (chunkType === C.DIFF_EQUAL) {
164           offset.left += text.length;
165           offset.right += text.length;
166         } else if (chunkType === C.DIFF_DELETE) {
167           diffs.push(computeDiff(this, C.DIFF_DELETE, offset.left, offset.right, text));
168           offset.right += text.length;
169
170         } else if (chunkType === C.DIFF_INSERT) {
171           diffs.push(computeDiff(this, C.DIFF_INSERT, offset.left, offset.right, text));
172           offset.left += text.length;
173         }
174       }, this);
175
176       // simplify our computed diffs; this groups together multiple diffs on subsequent lines
177       this.diffs = simplifyDiffs(this, diffs);
178
179       // if we're dealing with too many diffs, fail silently
180       if (this.diffs.length > this.options.maxDiffs) {
181         return;
182       }
183
184       clearDiffs(this);
185       decorate(this);
186     },
187
188     destroy: function() {
189
190       // destroy the two editors
191       var leftValue = this.editors.left.ace.getValue();
192       this.editors.left.ace.destroy();
193       var oldDiv = this.editors.left.ace.container;
194       var newDiv = oldDiv.cloneNode(false);
195       newDiv.textContent = leftValue;
196       oldDiv.parentNode.replaceChild(newDiv, oldDiv);
197
198       var rightValue = this.editors.right.ace.getValue();
199       this.editors.right.ace.destroy();
200       oldDiv = this.editors.right.ace.container;
201       newDiv = oldDiv.cloneNode(false);
202       newDiv.textContent = rightValue;
203       oldDiv.parentNode.replaceChild(newDiv, oldDiv);
204
205       document.getElementById(this.options.classes.gutterID).innerHTML = '';
206     }
207   };
208
209
210   function getMode(acediff, editor) {
211     var mode = acediff.options.mode;
212     if (editor === C.EDITOR_LEFT && acediff.options.left.mode !== null) {
213       mode = acediff.options.left.mode;
214     }
215     if (editor === C.EDITOR_RIGHT && acediff.options.right.mode !== null) {
216       mode = acediff.options.right.mode;
217     }
218     return mode;
219   }
220
221
222   function getTheme(acediff, editor) {
223     var theme = acediff.options.theme;
224     if (editor === C.EDITOR_LEFT && acediff.options.left.theme !== null) {
225       theme = acediff.options.left.theme;
226     }
227     if (editor === C.EDITOR_RIGHT && acediff.options.right.theme !== null) {
228       theme = acediff.options.right.theme;
229     }
230     return theme;
231   }
232
233
234   function addEventHandlers(acediff) {
235     var leftLastScrollTime = new Date().getTime(),
236         rightLastScrollTime = new Date().getTime(),
237         now;
238
239     acediff.editors.left.ace.getSession().on('changeScrollTop', function(scroll) {
240       now = new Date().getTime();
241       if (rightLastScrollTime + 50 < now) {
242         updateGap(acediff, 'left', scroll);
243       }
244     });
245
246     acediff.editors.right.ace.getSession().on('changeScrollTop', function(scroll) {
247       now = new Date().getTime();
248       if (leftLastScrollTime + 50 < now) {
249         updateGap(acediff, 'right', scroll);
250       }
251     });
252
253     var diff = acediff.diff.bind(acediff);
254     acediff.editors.left.ace.on('change', diff);
255     acediff.editors.right.ace.on('change', diff);
256
257     if (acediff.options.left.copyLinkEnabled) {
258       on('#' + acediff.options.classes.gutterID, 'click', '.' + acediff.options.classes.newCodeConnectorLink, function(e) {
259         copy(acediff, e, C.LTR);
260       });
261     }
262     if (acediff.options.right.copyLinkEnabled) {
263       on('#' + acediff.options.classes.gutterID, 'click', '.' + acediff.options.classes.deletedCodeConnectorLink, function(e) {
264         copy(acediff, e, C.RTL);
265       });
266     }
267
268     var onResize = debounce(function() {
269       acediff.editors.availableHeight = document.getElementById(acediff.options.left.id).offsetHeight;
270
271       // TODO this should re-init gutter
272       acediff.diff();
273     }, 250);
274
275     window.addEventListener('resize', onResize);
276   }
277
278
279   function copy(acediff, e, dir) {
280     var diffIndex = parseInt(e.target.getAttribute('data-diff-index'), 10);
281     var diff = acediff.diffs[diffIndex];
282     var sourceEditor, targetEditor;
283
284     var startLine, endLine, targetStartLine, targetEndLine;
285     if (dir === C.LTR) {
286       sourceEditor = acediff.editors.left;
287       targetEditor = acediff.editors.right;
288       startLine = diff.leftStartLine;
289       endLine = diff.leftEndLine;
290       targetStartLine = diff.rightStartLine;
291       targetEndLine = diff.rightEndLine;
292     } else {
293       sourceEditor = acediff.editors.right;
294       targetEditor = acediff.editors.left;
295       startLine = diff.rightStartLine;
296       endLine = diff.rightEndLine;
297       targetStartLine = diff.leftStartLine;
298       targetEndLine = diff.leftEndLine;
299     }
300
301     var contentToInsert = '';
302     for (var i=startLine; i<endLine; i++) {
303       contentToInsert += getLine(sourceEditor, i) + '\n';
304     }
305
306     var startContent = '';
307     for (var i=0; i<targetStartLine; i++) {
308       startContent += getLine(targetEditor, i) + '\n';
309     }
310
311     var endContent = '';
312     var totalLines = targetEditor.ace.getSession().getLength();
313     for (var i=targetEndLine; i<totalLines; i++) {
314       endContent += getLine(targetEditor, i);
315       if (i<totalLines-1) {
316         endContent += '\n';
317       }
318     }
319
320     endContent = endContent.replace(/\s*$/, '');
321
322     // keep track of the scroll height
323     var h = targetEditor.ace.getSession().getScrollTop();
324     targetEditor.ace.getSession().setValue(startContent + contentToInsert + endContent);
325     targetEditor.ace.getSession().setScrollTop(parseInt(h));
326
327     acediff.diff();
328   }
329
330
331   function getLineLengths(editor) {
332     var lines = editor.ace.getSession().doc.getAllLines();
333     var lineLengths = [];
334     lines.forEach(function(line) {
335       lineLengths.push(line.length + 1); // +1 for the newline char
336     });
337     return lineLengths;
338   }
339
340
341   // shows a diff in one of the two editors.
342   function showDiff(acediff, editor, startLine, endLine, className) {
343     var editor = acediff.editors[editor];
344
345     if (endLine < startLine) { // can this occur? Just in case.
346       endLine = startLine;
347     }
348
349     var classNames = className + ' ' + ((endLine > startLine) ? 'lines' : 'targetOnly');
350     endLine--; // because endLine is always + 1
351
352     // to get Ace to highlight the full row we just set the start and end chars to 0 and 1
353     editor.markers.push(editor.ace.session.addMarker(new Range(startLine, 0, endLine, 1), classNames, 'fullLine'));
354   }
355
356
357   // called onscroll. Updates the gap to ensure the connectors are all lining up
358   function updateGap(acediff, editor, scroll) {
359
360     clearDiffs(acediff);
361     decorate(acediff);
362
363     // reposition the copy containers containing all the arrows
364     positionCopyContainers(acediff);
365   }
366
367
368   function clearDiffs(acediff) {
369     acediff.editors.left.markers.forEach(function(marker) {
370       this.editors.left.ace.getSession().removeMarker(marker);
371     }, acediff);
372     acediff.editors.right.markers.forEach(function(marker) {
373       this.editors.right.ace.getSession().removeMarker(marker);
374     }, acediff);
375   }
376
377
378   function addConnector(acediff, leftStartLine, leftEndLine, rightStartLine, rightEndLine) {
379     var leftScrollTop  = acediff.editors.left.ace.getSession().getScrollTop();
380     var rightScrollTop = acediff.editors.right.ace.getSession().getScrollTop();
381
382     // All connectors, regardless of ltr or rtl have the same point system, even if p1 === p3 or p2 === p4
383     //  p1   p2
384     //
385     //  p3   p4
386
387     acediff.connectorYOffset = 1;
388
389     var p1_x = -1;
390     var p1_y = (leftStartLine * acediff.lineHeight) - leftScrollTop;
391     var p2_x = acediff.gutterWidth + 1;
392     var p2_y = rightStartLine * acediff.lineHeight - rightScrollTop;
393     var p3_x = -1;
394     var p3_y = (leftEndLine * acediff.lineHeight) - leftScrollTop + acediff.connectorYOffset;
395     var p4_x = acediff.gutterWidth + 1;
396     var p4_y = (rightEndLine * acediff.lineHeight) - rightScrollTop + acediff.connectorYOffset;
397     var curve1 = getCurve(p1_x, p1_y, p2_x, p2_y);
398     var curve2 = getCurve(p4_x, p4_y, p3_x, p3_y);
399
400     var verticalLine1 = 'L' + p2_x + ',' + p2_y + ' ' + p4_x + ',' + p4_y;
401     var verticalLine2 = 'L' + p3_x + ',' + p3_y + ' ' + p1_x + ',' + p1_y;
402     var d = curve1 + ' ' + verticalLine1 + ' ' + curve2 + ' ' + verticalLine2;
403
404     var el = document.createElementNS(C.SVG_NS, 'path');
405     el.setAttribute('d', d);
406     el.setAttribute('class', acediff.options.classes.connector);
407     acediff.gutterSVG.appendChild(el);
408   }
409
410
411   function addCopyArrows(acediff, info, diffIndex) {
412     if (info.leftEndLine > info.leftStartLine && acediff.options.left.copyLinkEnabled) {
413       var arrow = createArrow({
414         className: acediff.options.classes.newCodeConnectorLink,
415         topOffset: info.leftStartLine * acediff.lineHeight,
416         tooltip: 'Copy to right',
417         diffIndex: diffIndex,
418         arrowContent: acediff.options.classes.newCodeConnectorLinkContent
419       });
420       acediff.copyRightContainer.appendChild(arrow);
421     }
422
423     if (info.rightEndLine > info.rightStartLine && acediff.options.right.copyLinkEnabled) {
424       var arrow = createArrow({
425         className: acediff.options.classes.deletedCodeConnectorLink,
426         topOffset: info.rightStartLine * acediff.lineHeight,
427         tooltip: 'Copy to left',
428         diffIndex: diffIndex,
429         arrowContent: acediff.options.classes.deletedCodeConnectorLinkContent
430       });
431       acediff.copyLeftContainer.appendChild(arrow);
432     }
433   }
434
435
436   function positionCopyContainers(acediff) {
437     var leftTopOffset = acediff.editors.left.ace.getSession().getScrollTop();
438     var rightTopOffset = acediff.editors.right.ace.getSession().getScrollTop();
439
440     acediff.copyRightContainer.style.cssText = 'top: ' + (-leftTopOffset) + 'px';
441     acediff.copyLeftContainer.style.cssText = 'top: ' + (-rightTopOffset) + 'px';
442   }
443
444
445   /**
446    * This method takes the raw diffing info from the Google lib and returns a nice clean object of the following
447    * form:
448    * {
449    *   leftStartLine:
450    *   leftEndLine:
451    *   rightStartLine:
452    *   rightEndLine:
453    * }
454    *
455    * Ultimately, that's all the info we need to highlight the appropriate lines in the left + right editor, add the
456    * SVG connectors, and include the appropriate <<, >> arrows.
457    *
458    * Note: leftEndLine and rightEndLine are always the start of the NEXT line, so for a single line diff, there will
459    * be 1 separating the startLine and endLine values. So if leftStartLine === leftEndLine or rightStartLine ===
460    * rightEndLine, it means that new content from the other editor is being inserted and a single 1px line will be
461    * drawn.
462    */
463   function computeDiff(acediff, diffType, offsetLeft, offsetRight, diffText) {
464     var lineInfo = {};
465
466     // this was added in to hack around an oddity with the Google lib. Sometimes it would include a newline
467     // as the first char for a diff, other times not - and it would change when you were typing on-the-fly. This
468     // is used to level things out so the diffs don't appear to shift around
469     var newContentStartsWithNewline = /^\n/.test(diffText);
470
471     if (diffType === C.DIFF_INSERT) {
472
473       // pretty confident this returns the right stuff for the left editor: start & end line & char
474       var info = getSingleDiffInfo(acediff.editors.left, offsetLeft, diffText);
475
476       // this is the ACTUAL undoctored current line in the other editor. It's always right. Doesn't mean it's
477       // going to be used as the start line for the diff though.
478       var currentLineOtherEditor = getLineForCharPosition(acediff.editors.right, offsetRight);
479       var numCharsOnLineOtherEditor = getCharsOnLine(acediff.editors.right, currentLineOtherEditor);
480       var numCharsOnLeftEditorStartLine = getCharsOnLine(acediff.editors.left, info.startLine);
481       var numCharsOnLine = getCharsOnLine(acediff.editors.left, info.startLine);
482
483       // this is necessary because if a new diff starts on the FIRST char of the left editor, the diff can comes
484       // back from google as being on the last char of the previous line so we need to bump it up one
485       var rightStartLine = currentLineOtherEditor;
486       if (numCharsOnLine === 0 && newContentStartsWithNewline) {
487         newContentStartsWithNewline = false;
488       }
489       if (info.startChar === 0 && isLastChar(acediff.editors.right, offsetRight, newContentStartsWithNewline)) {
490         rightStartLine = currentLineOtherEditor + 1;
491       }
492
493       var sameLineInsert = info.startLine === info.endLine;
494
495       // whether or not this diff is a plain INSERT into the other editor, or overwrites a line take a little work to
496       // figure out. This feels like the hardest part of the entire script.
497       var numRows = 0;
498       if (
499
500         // dense, but this accommodates two scenarios:
501         // 1. where a completely fresh new line is being inserted in left editor, we want the line on right to stay a 1px line
502         // 2. where a new character is inserted at the start of a newline on the left but the line contains other stuff,
503         //    we DO want to make it a full line
504         (info.startChar > 0 || (sameLineInsert && diffText.length < numCharsOnLeftEditorStartLine)) &&
505
506         // if the right editor line was empty, it's ALWAYS a single line insert [not an OR above?]
507         numCharsOnLineOtherEditor > 0 &&
508
509         // if the text being inserted starts mid-line
510         (info.startChar < numCharsOnLeftEditorStartLine)) {
511         numRows++;
512       }
513
514       lineInfo = {
515         leftStartLine: info.startLine,
516         leftEndLine: info.endLine + 1,
517         rightStartLine: rightStartLine,
518         rightEndLine: rightStartLine + numRows
519       };
520
521     } else {
522       var info = getSingleDiffInfo(acediff.editors.right, offsetRight, diffText);
523
524       var currentLineOtherEditor = getLineForCharPosition(acediff.editors.left, offsetLeft);
525       var numCharsOnLineOtherEditor = getCharsOnLine(acediff.editors.left, currentLineOtherEditor);
526       var numCharsOnRightEditorStartLine = getCharsOnLine(acediff.editors.right, info.startLine);
527       var numCharsOnLine = getCharsOnLine(acediff.editors.right, info.startLine);
528
529       // this is necessary because if a new diff starts on the FIRST char of the left editor, the diff can comes
530       // back from google as being on the last char of the previous line so we need to bump it up one
531       var leftStartLine = currentLineOtherEditor;
532       if (numCharsOnLine === 0 && newContentStartsWithNewline) {
533         newContentStartsWithNewline = false;
534       }
535       if (info.startChar === 0 && isLastChar(acediff.editors.left, offsetLeft, newContentStartsWithNewline)) {
536         leftStartLine = currentLineOtherEditor + 1;
537       }
538
539       var sameLineInsert = info.startLine === info.endLine;
540       var numRows = 0;
541       if (
542
543         // dense, but this accommodates two scenarios:
544         // 1. where a completely fresh new line is being inserted in left editor, we want the line on right to stay a 1px line
545         // 2. where a new character is inserted at the start of a newline on the left but the line contains other stuff,
546         //    we DO want to make it a full line
547         (info.startChar > 0 || (sameLineInsert && diffText.length < numCharsOnRightEditorStartLine)) &&
548
549         // if the right editor line was empty, it's ALWAYS a single line insert [not an OR above?]
550         numCharsOnLineOtherEditor > 0 &&
551
552         // if the text being inserted starts mid-line
553         (info.startChar < numCharsOnRightEditorStartLine)) {
554           numRows++;
555       }
556
557       lineInfo = {
558         leftStartLine: leftStartLine,
559         leftEndLine: leftStartLine + numRows,
560         rightStartLine: info.startLine,
561         rightEndLine: info.endLine + 1
562       };
563     }
564
565     return lineInfo;
566   }
567
568
569   // helper to return the startline, endline, startChar and endChar for a diff in a particular editor. Pretty
570   // fussy function
571   function getSingleDiffInfo(editor, offset, diffString) {
572     var info = {
573       startLine: 0,
574       startChar: 0,
575       endLine: 0,
576       endChar: 0
577     };
578     var endCharNum = offset + diffString.length;
579     var runningTotal = 0;
580     var startLineSet = false,
581         endLineSet = false;
582
583     editor.lineLengths.forEach(function(lineLength, lineIndex) {
584       runningTotal += lineLength;
585
586       if (!startLineSet && offset < runningTotal) {
587         info.startLine = lineIndex;
588         info.startChar = offset - runningTotal + lineLength;
589         startLineSet = true;
590       }
591
592       if (!endLineSet && endCharNum <= runningTotal) {
593         info.endLine = lineIndex;
594         info.endChar = endCharNum - runningTotal + lineLength;
595         endLineSet = true;
596       }
597     });
598
599     // if the start char is the final char on the line, it's a newline & we ignore it
600     if (info.startChar > 0 && getCharsOnLine(editor, info.startLine) === info.startChar) {
601       info.startLine++;
602       info.startChar = 0;
603     }
604
605     // if the end char is the first char on the line, we don't want to highlight that extra line
606     if (info.endChar === 0) {
607       info.endLine--;
608     }
609
610     var endsWithNewline = /\n$/.test(diffString);
611     if (info.startChar > 0 && endsWithNewline) {
612       info.endLine++;
613     }
614
615     return info;
616   }
617
618
619   // note that this and everything else in this script uses 0-indexed row numbers
620   function getCharsOnLine(editor, line) {
621     return getLine(editor, line).length;
622   }
623
624
625   function getLine(editor, line) {
626     return editor.ace.getSession().doc.getLine(line);
627   }
628
629
630   function getLineForCharPosition(editor, offsetChars) {
631     var lines = editor.ace.getSession().doc.getAllLines(),
632         foundLine = 0,
633         runningTotal = 0;
634
635     for (var i=0; i<lines.length; i++) {
636       runningTotal += lines[i].length + 1; // +1 needed for newline char
637       if (offsetChars <= runningTotal) {
638         foundLine = i;
639         break;
640       }
641     }
642     return foundLine;
643   }
644
645
646   function isLastChar(editor, char, startsWithNewline) {
647     var lines = editor.ace.getSession().doc.getAllLines(),
648         runningTotal = 0,
649         isLastChar = false;
650
651     for (var i=0; i<lines.length; i++) {
652       runningTotal += lines[i].length + 1; // +1 needed for newline char
653       var comparison = runningTotal;
654       if (startsWithNewline) {
655         comparison--;
656       }
657
658       if (char === comparison) {
659         isLastChar = true;
660         break;
661       }
662     }
663     return isLastChar;
664   }
665
666
667   function createArrow(info) {
668     var el = document.createElement('div');
669     var props = {
670       'class': info.className,
671       'style': 'top:' + info.topOffset + 'px',
672       title: info.tooltip,
673       'data-diff-index': info.diffIndex
674     };
675     for (var key in props) {
676       el.setAttribute(key, props[key]);
677     }
678     el.innerHTML = info.arrowContent;
679     return el;
680   }
681
682
683   function createGutter(acediff) {
684     acediff.gutterHeight = document.getElementById(acediff.options.classes.gutterID).clientHeight;
685     acediff.gutterWidth = document.getElementById(acediff.options.classes.gutterID).clientWidth;
686
687     var leftHeight = getTotalHeight(acediff, C.EDITOR_LEFT);
688     var rightHeight = getTotalHeight(acediff, C.EDITOR_RIGHT);
689     var height = Math.max(leftHeight, rightHeight, acediff.gutterHeight);
690
691     acediff.gutterSVG = document.createElementNS(C.SVG_NS, 'svg');
692     acediff.gutterSVG.setAttribute('width', acediff.gutterWidth);
693     acediff.gutterSVG.setAttribute('height', height);
694
695     document.getElementById(acediff.options.classes.gutterID).appendChild(acediff.gutterSVG);
696   }
697
698   // acediff.editors.left.ace.getSession().getLength() * acediff.lineHeight
699   function getTotalHeight(acediff, editor) {
700     var ed = (editor === C.EDITOR_LEFT) ? acediff.editors.left : acediff.editors.right;
701     return ed.ace.getSession().getLength() * acediff.lineHeight;
702   }
703
704   // creates two contains for positioning the copy left + copy right arrows
705   function createCopyContainers(acediff) {
706     acediff.copyRightContainer = document.createElement('div');
707     acediff.copyRightContainer.setAttribute('class', acediff.options.classes.copyRightContainer);
708     acediff.copyLeftContainer = document.createElement('div');
709     acediff.copyLeftContainer.setAttribute('class', acediff.options.classes.copyLeftContainer);
710
711     document.getElementById(acediff.options.classes.gutterID).appendChild(acediff.copyRightContainer);
712     document.getElementById(acediff.options.classes.gutterID).appendChild(acediff.copyLeftContainer);
713   }
714
715
716   function clearGutter(acediff) {
717     //gutter.innerHTML = '';
718
719     var gutterEl  = document.getElementById(acediff.options.classes.gutterID);
720         try{
721                 gutterEl.removeChild(acediff.gutterSVG);
722         }catch(err){
723         }
724
725     createGutter(acediff);
726   }
727
728
729   function clearArrows(acediff) {
730     acediff.copyLeftContainer.innerHTML = '';
731     acediff.copyRightContainer.innerHTML = '';
732   }
733
734
735   /*
736    * This combines multiple rows where, say, line 1 => line 1, line 2 => line 2, line 3-4 => line 3. That could be
737    * reduced to a single connector line 1=4 => line 1-3
738    */
739   function simplifyDiffs(acediff, diffs) {
740     var groupedDiffs = [];
741
742     function compare(val) {
743       return (acediff.options.diffGranularity === C.DIFF_GRANULARITY_SPECIFIC) ? val < 1 : val <= 1;
744     }
745
746     diffs.forEach(function(diff, index) {
747       if (index === 0) {
748         groupedDiffs.push(diff);
749         return;
750       }
751
752       // loop through all grouped diffs. If this new diff lies between an existing one, we'll just add to it, rather
753       // than create a new one
754       var isGrouped = false;
755       for (var i=0; i<groupedDiffs.length; i++) {
756         if (compare(Math.abs(diff.leftStartLine - groupedDiffs[i].leftEndLine)) &&
757             compare(Math.abs(diff.rightStartLine - groupedDiffs[i].rightEndLine))) {
758
759           // update the existing grouped diff to expand its horizons to include this new diff start + end lines
760           groupedDiffs[i].leftStartLine = Math.min(diff.leftStartLine, groupedDiffs[i].leftStartLine);
761           groupedDiffs[i].rightStartLine = Math.min(diff.rightStartLine, groupedDiffs[i].rightStartLine);
762           groupedDiffs[i].leftEndLine = Math.max(diff.leftEndLine, groupedDiffs[i].leftEndLine);
763           groupedDiffs[i].rightEndLine = Math.max(diff.rightEndLine, groupedDiffs[i].rightEndLine);
764           isGrouped = true;
765           break;
766         }
767       }
768
769       if (!isGrouped) {
770         groupedDiffs.push(diff);
771       }
772     });
773
774     // clear out any single line diffs (i.e. single line on both editors)
775     var fullDiffs = [];
776     groupedDiffs.forEach(function(diff) {
777       if (diff.leftStartLine === diff.leftEndLine && diff.rightStartLine === diff.rightEndLine) {
778         return;
779       }
780       fullDiffs.push(diff);
781     });
782
783     return fullDiffs;
784   }
785
786
787   function decorate(acediff) {
788     clearGutter(acediff);
789     clearArrows(acediff);
790
791     acediff.diffs.forEach(function(info, diffIndex) {
792       if (this.options.showDiffs) {
793         showDiff(this, C.EDITOR_LEFT, info.leftStartLine, info.leftEndLine, this.options.classes.diff);
794         showDiff(this, C.EDITOR_RIGHT, info.rightStartLine, info.rightEndLine, this.options.classes.diff);
795
796         if (this.options.showConnectors) {
797           addConnector(this, info.leftStartLine, info.leftEndLine, info.rightStartLine, info.rightEndLine);
798         }
799         addCopyArrows(this, info, diffIndex);
800       }
801     }, acediff);
802   }
803
804
805   function extend() {
806     var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
807       i = 1,
808       length = arguments.length,
809       deep = false,
810       toString = Object.prototype.toString,
811       hasOwn = Object.prototype.hasOwnProperty,
812       class2type = {
813         "[object Boolean]": "boolean",
814         "[object Number]": "number",
815         "[object String]": "string",
816         "[object Function]": "function",
817         "[object Array]": "array",
818         "[object Date]": "date",
819         "[object RegExp]": "regexp",
820         "[object Object]": "object"
821       },
822
823       jQuery = {
824         isFunction: function(obj) {
825           return jQuery.type(obj) === "function";
826         },
827         isArray: Array.isArray ||
828         function(obj) {
829           return jQuery.type(obj) === "array";
830         },
831         isWindow: function(obj) {
832           return obj !== null && obj === obj.window;
833         },
834         isNumeric: function(obj) {
835           return !isNaN(parseFloat(obj)) && isFinite(obj);
836         },
837         type: function(obj) {
838           return obj === null ? String(obj) : class2type[toString.call(obj)] || "object";
839         },
840         isPlainObject: function(obj) {
841           if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) {
842             return false;
843           }
844           try {
845             if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
846               return false;
847             }
848           } catch (e) {
849             return false;
850           }
851           var key;
852           for (key in obj) {}
853           return key === undefined || hasOwn.call(obj, key);
854         }
855       };
856     if (typeof target === "boolean") {
857       deep = target;
858       target = arguments[1] || {};
859       i = 2;
860     }
861     if (typeof target !== "object" && !jQuery.isFunction(target)) {
862       target = {};
863     }
864     if (length === i) {
865       target = this;
866       --i;
867     }
868     for (i; i < length; i++) {
869       if ((options = arguments[i]) !== null) {
870         for (name in options) {
871           src = target[name];
872           copy = options[name];
873           if (target === copy) {
874             continue;
875           }
876           if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
877             if (copyIsArray) {
878               copyIsArray = false;
879               clone = src && jQuery.isArray(src) ? src : [];
880             } else {
881               clone = src && jQuery.isPlainObject(src) ? src : {};
882             }
883             // WARNING: RECURSION
884             target[name] = extend(deep, clone, copy);
885           } else if (copy !== undefined) {
886             target[name] = copy;
887           }
888         }
889       }
890     }
891
892     return target;
893   }
894
895
896   function getScrollingInfo(acediff, dir) {
897     return (dir == C.EDITOR_LEFT) ? acediff.editors.left.ace.getSession().getScrollTop() : acediff.editors.right.ace.getSession().getScrollTop();
898   }
899
900
901   function getEditorHeight(acediff) {
902     //editorHeight: document.getElementById(acediff.options.left.id).clientHeight
903     return document.getElementById(acediff.options.left.id).offsetHeight;
904   }
905
906   // generates a Bezier curve in SVG format
907   function getCurve(startX, startY, endX, endY) {
908     var w = endX - startX;
909     var halfWidth = startX + (w / 2);
910
911     // position it at the initial x,y coords
912     var curve = 'M ' + startX + ' ' + startY +
913
914       // now create the curve. This is of the form "C M,N O,P Q,R" where C is a directive for SVG ("curveto"),
915       // M,N are the first curve control point, O,P the second control point and Q,R are the final coords
916       ' C ' + halfWidth + ',' + startY + ' ' + halfWidth + ',' + endY + ' ' + endX + ',' + endY;
917
918     return curve;
919   }
920
921
922   function on(elSelector, eventName, selector, fn) {
923     var element = (elSelector === 'document') ? document : document.querySelector(elSelector);
924
925     element.addEventListener(eventName, function(event) {
926       var possibleTargets = element.querySelectorAll(selector);
927       var target = event.target;
928
929       for (var i = 0, l = possibleTargets.length; i < l; i++) {
930         var el = target;
931         var p = possibleTargets[i];
932
933         while(el && el !== element) {
934           if (el === p) {
935             return fn.call(p, event);
936           }
937           el = el.parentNode;
938         }
939       }
940     });
941   }
942
943
944   function debounce(func, wait, immediate) {
945     var timeout;
946     return function() {
947       var context = this, args = arguments;
948       var later = function() {
949         timeout = null;
950         if (!immediate) func.apply(context, args);
951       };
952       var callNow = immediate && !timeout;
953       clearTimeout(timeout);
954       timeout = setTimeout(later, wait);
955       if (callNow) func.apply(context, args);
956     };
957   }
958
959   return AceDiff;
960
961 }));