blob: 27b7a587a9bed59f1da59cc1aad02c6cf4957be1 [file] [log] [blame]
Chinthakayala,Sheshashailavas(sc2914)8f6a6c42018-06-27 16:11:44 +00001(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}));