diff options
Diffstat (limited to 'dgbuilder/public/ace/ace-diff.js')
-rwxr-xr-x | dgbuilder/public/ace/ace-diff.js | 961 |
1 files changed, 961 insertions, 0 deletions
diff --git a/dgbuilder/public/ace/ace-diff.js b/dgbuilder/public/ace/ace-diff.js new file mode 100755 index 00000000..27b7a587 --- /dev/null +++ b/dgbuilder/public/ace/ace-diff.js @@ -0,0 +1,961 @@ +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof exports === 'object') { + module.exports = factory(require()); + } else { + root.AceDiff = factory(root); + } +}(this, function() { + 'use strict'; + + var Range = require('ace/range').Range; + + var C = { + DIFF_EQUAL: 0, + DIFF_DELETE: -1, + DIFF_INSERT: 1, + EDITOR_RIGHT: 'right', + EDITOR_LEFT: 'left', + RTL: 'rtl', + LTR: 'ltr', + SVG_NS: 'http://www.w3.org/2000/svg', + DIFF_GRANULARITY_SPECIFIC: 'specific', + DIFF_GRANULARITY_BROAD: 'broad' + }; + + // our constructor + function AceDiff(options) { + this.options = {}; + + extend(true, this.options, { + mode: null, + theme: null, + diffGranularity: C.DIFF_GRANULARITY_BROAD, + lockScrolling: false, // not implemented yet + showDiffs: true, + showConnectors: true, + maxDiffs: 5000, + left: { + id: 'acediff-left-editor', + content: null, + mode: null, + theme: null, + editable: true, + copyLinkEnabled: true + }, + right: { + id: 'acediff-right-editor', + content: null, + mode: null, + theme: null, + editable: true, + copyLinkEnabled: true + }, + classes: { + gutterID: 'acediff-gutter', + diff: 'acediff-diff', + connector: 'acediff-connector', + newCodeConnectorLink: 'acediff-new-code-connector-copy', + newCodeConnectorLinkContent: '→', + deletedCodeConnectorLink: 'acediff-deleted-code-connector-copy', + deletedCodeConnectorLinkContent: '←', + copyRightContainer: 'acediff-copy-right', + copyLeftContainer: 'acediff-copy-left' + }, + connectorYOffset: 0 + }, options); + + // instantiate the editors in an internal data structure that will store a little info about the diffs and + // editor content + this.editors = { + left: { + ace: ace.edit(this.options.left.id), + markers: [], + lineLengths: [] + }, + right: { + ace: ace.edit(this.options.right.id), + markers: [], + lineLengths: [] + }, + editorHeight: null + }; + + addEventHandlers(this); + + this.lineHeight = this.editors.left.ace.renderer.lineHeight; // assumption: both editors have same line heights + + // set up the editors + this.editors.left.ace.getSession().setMode(getMode(this, C.EDITOR_LEFT)); + this.editors.right.ace.getSession().setMode(getMode(this, C.EDITOR_RIGHT)); + this.editors.left.ace.setReadOnly(!this.options.left.editable); + this.editors.right.ace.setReadOnly(!this.options.right.editable); + this.editors.left.ace.setTheme(getTheme(this, C.EDITOR_LEFT)); + this.editors.right.ace.setTheme(getTheme(this, C.EDITOR_RIGHT)); + + createCopyContainers(this); + createGutter(this); + + // if the data is being supplied by an option, set the editor values now + if (this.options.left.content) { + this.editors.left.ace.setValue(this.options.left.content, -1); + } + if (this.options.right.content) { + this.editors.right.ace.setValue(this.options.right.content, -1); + } + + // store the visible height of the editors (assumed the same) + this.editors.editorHeight = getEditorHeight(this); + + this.diff(); + } + + + // our public API + AceDiff.prototype = { + + // allows on-the-fly changes to the AceDiff instance settings + setOptions: function(options) { + extend(true, this.options, options); + this.diff(); + }, + + getNumDiffs: function() { + return this.diffs.length; + }, + + // exposes the Ace editors in case the dev needs it + getEditors: function() { + return { + left: this.editors.left.ace, + right: this.editors.right.ace + } + }, + + // our main diffing function. I actually don't think this needs to exposed: it's called automatically, + // but just to be safe, it's included + diff: function() { + var dmp = new diff_match_patch(); + var val1 = this.editors.left.ace.getSession().getValue(); + var val2 = this.editors.right.ace.getSession().getValue(); + var diff = dmp.diff_main(val2, val1); + dmp.diff_cleanupSemantic(diff); + + this.editors.left.lineLengths = getLineLengths(this.editors.left); + this.editors.right.lineLengths = getLineLengths(this.editors.right); + + // parse the raw diff into something a little more palatable + var diffs = []; + var offset = { + left: 0, + right: 0 + }; + + diff.forEach(function(chunk) { + var chunkType = chunk[0]; + var text = chunk[1]; + + // oddly, occasionally the algorithm returns a diff with no changes made + if (text.length === 0) { + return; + } + if (chunkType === C.DIFF_EQUAL) { + offset.left += text.length; + offset.right += text.length; + } else if (chunkType === C.DIFF_DELETE) { + diffs.push(computeDiff(this, C.DIFF_DELETE, offset.left, offset.right, text)); + offset.right += text.length; + + } else if (chunkType === C.DIFF_INSERT) { + diffs.push(computeDiff(this, C.DIFF_INSERT, offset.left, offset.right, text)); + offset.left += text.length; + } + }, this); + + // simplify our computed diffs; this groups together multiple diffs on subsequent lines + this.diffs = simplifyDiffs(this, diffs); + + // if we're dealing with too many diffs, fail silently + if (this.diffs.length > this.options.maxDiffs) { + return; + } + + clearDiffs(this); + decorate(this); + }, + + destroy: function() { + + // destroy the two editors + var leftValue = this.editors.left.ace.getValue(); + this.editors.left.ace.destroy(); + var oldDiv = this.editors.left.ace.container; + var newDiv = oldDiv.cloneNode(false); + newDiv.textContent = leftValue; + oldDiv.parentNode.replaceChild(newDiv, oldDiv); + + var rightValue = this.editors.right.ace.getValue(); + this.editors.right.ace.destroy(); + oldDiv = this.editors.right.ace.container; + newDiv = oldDiv.cloneNode(false); + newDiv.textContent = rightValue; + oldDiv.parentNode.replaceChild(newDiv, oldDiv); + + document.getElementById(this.options.classes.gutterID).innerHTML = ''; + } + }; + + + function getMode(acediff, editor) { + var mode = acediff.options.mode; + if (editor === C.EDITOR_LEFT && acediff.options.left.mode !== null) { + mode = acediff.options.left.mode; + } + if (editor === C.EDITOR_RIGHT && acediff.options.right.mode !== null) { + mode = acediff.options.right.mode; + } + return mode; + } + + + function getTheme(acediff, editor) { + var theme = acediff.options.theme; + if (editor === C.EDITOR_LEFT && acediff.options.left.theme !== null) { + theme = acediff.options.left.theme; + } + if (editor === C.EDITOR_RIGHT && acediff.options.right.theme !== null) { + theme = acediff.options.right.theme; + } + return theme; + } + + + function addEventHandlers(acediff) { + var leftLastScrollTime = new Date().getTime(), + rightLastScrollTime = new Date().getTime(), + now; + + acediff.editors.left.ace.getSession().on('changeScrollTop', function(scroll) { + now = new Date().getTime(); + if (rightLastScrollTime + 50 < now) { + updateGap(acediff, 'left', scroll); + } + }); + + acediff.editors.right.ace.getSession().on('changeScrollTop', function(scroll) { + now = new Date().getTime(); + if (leftLastScrollTime + 50 < now) { + updateGap(acediff, 'right', scroll); + } + }); + + var diff = acediff.diff.bind(acediff); + acediff.editors.left.ace.on('change', diff); + acediff.editors.right.ace.on('change', diff); + + if (acediff.options.left.copyLinkEnabled) { + on('#' + acediff.options.classes.gutterID, 'click', '.' + acediff.options.classes.newCodeConnectorLink, function(e) { + copy(acediff, e, C.LTR); + }); + } + if (acediff.options.right.copyLinkEnabled) { + on('#' + acediff.options.classes.gutterID, 'click', '.' + acediff.options.classes.deletedCodeConnectorLink, function(e) { + copy(acediff, e, C.RTL); + }); + } + + var onResize = debounce(function() { + acediff.editors.availableHeight = document.getElementById(acediff.options.left.id).offsetHeight; + + // TODO this should re-init gutter + acediff.diff(); + }, 250); + + window.addEventListener('resize', onResize); + } + + + function copy(acediff, e, dir) { + var diffIndex = parseInt(e.target.getAttribute('data-diff-index'), 10); + var diff = acediff.diffs[diffIndex]; + var sourceEditor, targetEditor; + + var startLine, endLine, targetStartLine, targetEndLine; + if (dir === C.LTR) { + sourceEditor = acediff.editors.left; + targetEditor = acediff.editors.right; + startLine = diff.leftStartLine; + endLine = diff.leftEndLine; + targetStartLine = diff.rightStartLine; + targetEndLine = diff.rightEndLine; + } else { + sourceEditor = acediff.editors.right; + targetEditor = acediff.editors.left; + startLine = diff.rightStartLine; + endLine = diff.rightEndLine; + targetStartLine = diff.leftStartLine; + targetEndLine = diff.leftEndLine; + } + + var contentToInsert = ''; + for (var i=startLine; i<endLine; i++) { + contentToInsert += getLine(sourceEditor, i) + '\n'; + } + + var startContent = ''; + for (var i=0; i<targetStartLine; i++) { + startContent += getLine(targetEditor, i) + '\n'; + } + + var endContent = ''; + var totalLines = targetEditor.ace.getSession().getLength(); + for (var i=targetEndLine; i<totalLines; i++) { + endContent += getLine(targetEditor, i); + if (i<totalLines-1) { + endContent += '\n'; + } + } + + endContent = endContent.replace(/\s*$/, ''); + + // keep track of the scroll height + var h = targetEditor.ace.getSession().getScrollTop(); + targetEditor.ace.getSession().setValue(startContent + contentToInsert + endContent); + targetEditor.ace.getSession().setScrollTop(parseInt(h)); + + acediff.diff(); + } + + + function getLineLengths(editor) { + var lines = editor.ace.getSession().doc.getAllLines(); + var lineLengths = []; + lines.forEach(function(line) { + lineLengths.push(line.length + 1); // +1 for the newline char + }); + return lineLengths; + } + + + // shows a diff in one of the two editors. + function showDiff(acediff, editor, startLine, endLine, className) { + var editor = acediff.editors[editor]; + + if (endLine < startLine) { // can this occur? Just in case. + endLine = startLine; + } + + var classNames = className + ' ' + ((endLine > startLine) ? 'lines' : 'targetOnly'); + endLine--; // because endLine is always + 1 + + // to get Ace to highlight the full row we just set the start and end chars to 0 and 1 + editor.markers.push(editor.ace.session.addMarker(new Range(startLine, 0, endLine, 1), classNames, 'fullLine')); + } + + + // called onscroll. Updates the gap to ensure the connectors are all lining up + function updateGap(acediff, editor, scroll) { + + clearDiffs(acediff); + decorate(acediff); + + // reposition the copy containers containing all the arrows + positionCopyContainers(acediff); + } + + + function clearDiffs(acediff) { + acediff.editors.left.markers.forEach(function(marker) { + this.editors.left.ace.getSession().removeMarker(marker); + }, acediff); + acediff.editors.right.markers.forEach(function(marker) { + this.editors.right.ace.getSession().removeMarker(marker); + }, acediff); + } + + + function addConnector(acediff, leftStartLine, leftEndLine, rightStartLine, rightEndLine) { + var leftScrollTop = acediff.editors.left.ace.getSession().getScrollTop(); + var rightScrollTop = acediff.editors.right.ace.getSession().getScrollTop(); + + // All connectors, regardless of ltr or rtl have the same point system, even if p1 === p3 or p2 === p4 + // p1 p2 + // + // p3 p4 + + acediff.connectorYOffset = 1; + + var p1_x = -1; + var p1_y = (leftStartLine * acediff.lineHeight) - leftScrollTop; + var p2_x = acediff.gutterWidth + 1; + var p2_y = rightStartLine * acediff.lineHeight - rightScrollTop; + var p3_x = -1; + var p3_y = (leftEndLine * acediff.lineHeight) - leftScrollTop + acediff.connectorYOffset; + var p4_x = acediff.gutterWidth + 1; + var p4_y = (rightEndLine * acediff.lineHeight) - rightScrollTop + acediff.connectorYOffset; + var curve1 = getCurve(p1_x, p1_y, p2_x, p2_y); + var curve2 = getCurve(p4_x, p4_y, p3_x, p3_y); + + var verticalLine1 = 'L' + p2_x + ',' + p2_y + ' ' + p4_x + ',' + p4_y; + var verticalLine2 = 'L' + p3_x + ',' + p3_y + ' ' + p1_x + ',' + p1_y; + var d = curve1 + ' ' + verticalLine1 + ' ' + curve2 + ' ' + verticalLine2; + + var el = document.createElementNS(C.SVG_NS, 'path'); + el.setAttribute('d', d); + el.setAttribute('class', acediff.options.classes.connector); + acediff.gutterSVG.appendChild(el); + } + + + function addCopyArrows(acediff, info, diffIndex) { + if (info.leftEndLine > info.leftStartLine && acediff.options.left.copyLinkEnabled) { + var arrow = createArrow({ + className: acediff.options.classes.newCodeConnectorLink, + topOffset: info.leftStartLine * acediff.lineHeight, + tooltip: 'Copy to right', + diffIndex: diffIndex, + arrowContent: acediff.options.classes.newCodeConnectorLinkContent + }); + acediff.copyRightContainer.appendChild(arrow); + } + + if (info.rightEndLine > info.rightStartLine && acediff.options.right.copyLinkEnabled) { + var arrow = createArrow({ + className: acediff.options.classes.deletedCodeConnectorLink, + topOffset: info.rightStartLine * acediff.lineHeight, + tooltip: 'Copy to left', + diffIndex: diffIndex, + arrowContent: acediff.options.classes.deletedCodeConnectorLinkContent + }); + acediff.copyLeftContainer.appendChild(arrow); + } + } + + + function positionCopyContainers(acediff) { + var leftTopOffset = acediff.editors.left.ace.getSession().getScrollTop(); + var rightTopOffset = acediff.editors.right.ace.getSession().getScrollTop(); + + acediff.copyRightContainer.style.cssText = 'top: ' + (-leftTopOffset) + 'px'; + acediff.copyLeftContainer.style.cssText = 'top: ' + (-rightTopOffset) + 'px'; + } + + + /** + * This method takes the raw diffing info from the Google lib and returns a nice clean object of the following + * form: + * { + * leftStartLine: + * leftEndLine: + * rightStartLine: + * rightEndLine: + * } + * + * Ultimately, that's all the info we need to highlight the appropriate lines in the left + right editor, add the + * SVG connectors, and include the appropriate <<, >> arrows. + * + * Note: leftEndLine and rightEndLine are always the start of the NEXT line, so for a single line diff, there will + * be 1 separating the startLine and endLine values. So if leftStartLine === leftEndLine or rightStartLine === + * rightEndLine, it means that new content from the other editor is being inserted and a single 1px line will be + * drawn. + */ + function computeDiff(acediff, diffType, offsetLeft, offsetRight, diffText) { + var lineInfo = {}; + + // this was added in to hack around an oddity with the Google lib. Sometimes it would include a newline + // as the first char for a diff, other times not - and it would change when you were typing on-the-fly. This + // is used to level things out so the diffs don't appear to shift around + var newContentStartsWithNewline = /^\n/.test(diffText); + + if (diffType === C.DIFF_INSERT) { + + // pretty confident this returns the right stuff for the left editor: start & end line & char + var info = getSingleDiffInfo(acediff.editors.left, offsetLeft, diffText); + + // this is the ACTUAL undoctored current line in the other editor. It's always right. Doesn't mean it's + // going to be used as the start line for the diff though. + var currentLineOtherEditor = getLineForCharPosition(acediff.editors.right, offsetRight); + var numCharsOnLineOtherEditor = getCharsOnLine(acediff.editors.right, currentLineOtherEditor); + var numCharsOnLeftEditorStartLine = getCharsOnLine(acediff.editors.left, info.startLine); + var numCharsOnLine = getCharsOnLine(acediff.editors.left, info.startLine); + + // this is necessary because if a new diff starts on the FIRST char of the left editor, the diff can comes + // back from google as being on the last char of the previous line so we need to bump it up one + var rightStartLine = currentLineOtherEditor; + if (numCharsOnLine === 0 && newContentStartsWithNewline) { + newContentStartsWithNewline = false; + } + if (info.startChar === 0 && isLastChar(acediff.editors.right, offsetRight, newContentStartsWithNewline)) { + rightStartLine = currentLineOtherEditor + 1; + } + + var sameLineInsert = info.startLine === info.endLine; + + // whether or not this diff is a plain INSERT into the other editor, or overwrites a line take a little work to + // figure out. This feels like the hardest part of the entire script. + var numRows = 0; + if ( + + // dense, but this accommodates two scenarios: + // 1. where a completely fresh new line is being inserted in left editor, we want the line on right to stay a 1px line + // 2. where a new character is inserted at the start of a newline on the left but the line contains other stuff, + // we DO want to make it a full line + (info.startChar > 0 || (sameLineInsert && diffText.length < numCharsOnLeftEditorStartLine)) && + + // if the right editor line was empty, it's ALWAYS a single line insert [not an OR above?] + numCharsOnLineOtherEditor > 0 && + + // if the text being inserted starts mid-line + (info.startChar < numCharsOnLeftEditorStartLine)) { + numRows++; + } + + lineInfo = { + leftStartLine: info.startLine, + leftEndLine: info.endLine + 1, + rightStartLine: rightStartLine, + rightEndLine: rightStartLine + numRows + }; + + } else { + var info = getSingleDiffInfo(acediff.editors.right, offsetRight, diffText); + + var currentLineOtherEditor = getLineForCharPosition(acediff.editors.left, offsetLeft); + var numCharsOnLineOtherEditor = getCharsOnLine(acediff.editors.left, currentLineOtherEditor); + var numCharsOnRightEditorStartLine = getCharsOnLine(acediff.editors.right, info.startLine); + var numCharsOnLine = getCharsOnLine(acediff.editors.right, info.startLine); + + // this is necessary because if a new diff starts on the FIRST char of the left editor, the diff can comes + // back from google as being on the last char of the previous line so we need to bump it up one + var leftStartLine = currentLineOtherEditor; + if (numCharsOnLine === 0 && newContentStartsWithNewline) { + newContentStartsWithNewline = false; + } + if (info.startChar === 0 && isLastChar(acediff.editors.left, offsetLeft, newContentStartsWithNewline)) { + leftStartLine = currentLineOtherEditor + 1; + } + + var sameLineInsert = info.startLine === info.endLine; + var numRows = 0; + if ( + + // dense, but this accommodates two scenarios: + // 1. where a completely fresh new line is being inserted in left editor, we want the line on right to stay a 1px line + // 2. where a new character is inserted at the start of a newline on the left but the line contains other stuff, + // we DO want to make it a full line + (info.startChar > 0 || (sameLineInsert && diffText.length < numCharsOnRightEditorStartLine)) && + + // if the right editor line was empty, it's ALWAYS a single line insert [not an OR above?] + numCharsOnLineOtherEditor > 0 && + + // if the text being inserted starts mid-line + (info.startChar < numCharsOnRightEditorStartLine)) { + numRows++; + } + + lineInfo = { + leftStartLine: leftStartLine, + leftEndLine: leftStartLine + numRows, + rightStartLine: info.startLine, + rightEndLine: info.endLine + 1 + }; + } + + return lineInfo; + } + + + // helper to return the startline, endline, startChar and endChar for a diff in a particular editor. Pretty + // fussy function + function getSingleDiffInfo(editor, offset, diffString) { + var info = { + startLine: 0, + startChar: 0, + endLine: 0, + endChar: 0 + }; + var endCharNum = offset + diffString.length; + var runningTotal = 0; + var startLineSet = false, + endLineSet = false; + + editor.lineLengths.forEach(function(lineLength, lineIndex) { + runningTotal += lineLength; + + if (!startLineSet && offset < runningTotal) { + info.startLine = lineIndex; + info.startChar = offset - runningTotal + lineLength; + startLineSet = true; + } + + if (!endLineSet && endCharNum <= runningTotal) { + info.endLine = lineIndex; + info.endChar = endCharNum - runningTotal + lineLength; + endLineSet = true; + } + }); + + // if the start char is the final char on the line, it's a newline & we ignore it + if (info.startChar > 0 && getCharsOnLine(editor, info.startLine) === info.startChar) { + info.startLine++; + info.startChar = 0; + } + + // if the end char is the first char on the line, we don't want to highlight that extra line + if (info.endChar === 0) { + info.endLine--; + } + + var endsWithNewline = /\n$/.test(diffString); + if (info.startChar > 0 && endsWithNewline) { + info.endLine++; + } + + return info; + } + + + // note that this and everything else in this script uses 0-indexed row numbers + function getCharsOnLine(editor, line) { + return getLine(editor, line).length; + } + + + function getLine(editor, line) { + return editor.ace.getSession().doc.getLine(line); + } + + + function getLineForCharPosition(editor, offsetChars) { + var lines = editor.ace.getSession().doc.getAllLines(), + foundLine = 0, + runningTotal = 0; + + for (var i=0; i<lines.length; i++) { + runningTotal += lines[i].length + 1; // +1 needed for newline char + if (offsetChars <= runningTotal) { + foundLine = i; + break; + } + } + return foundLine; + } + + + function isLastChar(editor, char, startsWithNewline) { + var lines = editor.ace.getSession().doc.getAllLines(), + runningTotal = 0, + isLastChar = false; + + for (var i=0; i<lines.length; i++) { + runningTotal += lines[i].length + 1; // +1 needed for newline char + var comparison = runningTotal; + if (startsWithNewline) { + comparison--; + } + + if (char === comparison) { + isLastChar = true; + break; + } + } + return isLastChar; + } + + + function createArrow(info) { + var el = document.createElement('div'); + var props = { + 'class': info.className, + 'style': 'top:' + info.topOffset + 'px', + title: info.tooltip, + 'data-diff-index': info.diffIndex + }; + for (var key in props) { + el.setAttribute(key, props[key]); + } + el.innerHTML = info.arrowContent; + return el; + } + + + function createGutter(acediff) { + acediff.gutterHeight = document.getElementById(acediff.options.classes.gutterID).clientHeight; + acediff.gutterWidth = document.getElementById(acediff.options.classes.gutterID).clientWidth; + + var leftHeight = getTotalHeight(acediff, C.EDITOR_LEFT); + var rightHeight = getTotalHeight(acediff, C.EDITOR_RIGHT); + var height = Math.max(leftHeight, rightHeight, acediff.gutterHeight); + + acediff.gutterSVG = document.createElementNS(C.SVG_NS, 'svg'); + acediff.gutterSVG.setAttribute('width', acediff.gutterWidth); + acediff.gutterSVG.setAttribute('height', height); + + document.getElementById(acediff.options.classes.gutterID).appendChild(acediff.gutterSVG); + } + + // acediff.editors.left.ace.getSession().getLength() * acediff.lineHeight + function getTotalHeight(acediff, editor) { + var ed = (editor === C.EDITOR_LEFT) ? acediff.editors.left : acediff.editors.right; + return ed.ace.getSession().getLength() * acediff.lineHeight; + } + + // creates two contains for positioning the copy left + copy right arrows + function createCopyContainers(acediff) { + acediff.copyRightContainer = document.createElement('div'); + acediff.copyRightContainer.setAttribute('class', acediff.options.classes.copyRightContainer); + acediff.copyLeftContainer = document.createElement('div'); + acediff.copyLeftContainer.setAttribute('class', acediff.options.classes.copyLeftContainer); + + document.getElementById(acediff.options.classes.gutterID).appendChild(acediff.copyRightContainer); + document.getElementById(acediff.options.classes.gutterID).appendChild(acediff.copyLeftContainer); + } + + + function clearGutter(acediff) { + //gutter.innerHTML = ''; + + var gutterEl = document.getElementById(acediff.options.classes.gutterID); + try{ + gutterEl.removeChild(acediff.gutterSVG); + }catch(err){ + } + + createGutter(acediff); + } + + + function clearArrows(acediff) { + acediff.copyLeftContainer.innerHTML = ''; + acediff.copyRightContainer.innerHTML = ''; + } + + + /* + * This combines multiple rows where, say, line 1 => line 1, line 2 => line 2, line 3-4 => line 3. That could be + * reduced to a single connector line 1=4 => line 1-3 + */ + function simplifyDiffs(acediff, diffs) { + var groupedDiffs = []; + + function compare(val) { + return (acediff.options.diffGranularity === C.DIFF_GRANULARITY_SPECIFIC) ? val < 1 : val <= 1; + } + + diffs.forEach(function(diff, index) { + if (index === 0) { + groupedDiffs.push(diff); + return; + } + + // loop through all grouped diffs. If this new diff lies between an existing one, we'll just add to it, rather + // than create a new one + var isGrouped = false; + for (var i=0; i<groupedDiffs.length; i++) { + if (compare(Math.abs(diff.leftStartLine - groupedDiffs[i].leftEndLine)) && + compare(Math.abs(diff.rightStartLine - groupedDiffs[i].rightEndLine))) { + + // update the existing grouped diff to expand its horizons to include this new diff start + end lines + groupedDiffs[i].leftStartLine = Math.min(diff.leftStartLine, groupedDiffs[i].leftStartLine); + groupedDiffs[i].rightStartLine = Math.min(diff.rightStartLine, groupedDiffs[i].rightStartLine); + groupedDiffs[i].leftEndLine = Math.max(diff.leftEndLine, groupedDiffs[i].leftEndLine); + groupedDiffs[i].rightEndLine = Math.max(diff.rightEndLine, groupedDiffs[i].rightEndLine); + isGrouped = true; + break; + } + } + + if (!isGrouped) { + groupedDiffs.push(diff); + } + }); + + // clear out any single line diffs (i.e. single line on both editors) + var fullDiffs = []; + groupedDiffs.forEach(function(diff) { + if (diff.leftStartLine === diff.leftEndLine && diff.rightStartLine === diff.rightEndLine) { + return; + } + fullDiffs.push(diff); + }); + + return fullDiffs; + } + + + function decorate(acediff) { + clearGutter(acediff); + clearArrows(acediff); + + acediff.diffs.forEach(function(info, diffIndex) { + if (this.options.showDiffs) { + showDiff(this, C.EDITOR_LEFT, info.leftStartLine, info.leftEndLine, this.options.classes.diff); + showDiff(this, C.EDITOR_RIGHT, info.rightStartLine, info.rightEndLine, this.options.classes.diff); + + if (this.options.showConnectors) { + addConnector(this, info.leftStartLine, info.leftEndLine, info.rightStartLine, info.rightEndLine); + } + addCopyArrows(this, info, diffIndex); + } + }, acediff); + } + + + function extend() { + var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false, + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + class2type = { + "[object Boolean]": "boolean", + "[object Number]": "number", + "[object String]": "string", + "[object Function]": "function", + "[object Array]": "array", + "[object Date]": "date", + "[object RegExp]": "regexp", + "[object Object]": "object" + }, + + jQuery = { + isFunction: function(obj) { + return jQuery.type(obj) === "function"; + }, + isArray: Array.isArray || + function(obj) { + return jQuery.type(obj) === "array"; + }, + isWindow: function(obj) { + return obj !== null && obj === obj.window; + }, + isNumeric: function(obj) { + return !isNaN(parseFloat(obj)) && isFinite(obj); + }, + type: function(obj) { + return obj === null ? String(obj) : class2type[toString.call(obj)] || "object"; + }, + isPlainObject: function(obj) { + if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) { + return false; + } + try { + if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) { + return false; + } + } catch (e) { + return false; + } + var key; + for (key in obj) {} + return key === undefined || hasOwn.call(obj, key); + } + }; + if (typeof target === "boolean") { + deep = target; + target = arguments[1] || {}; + i = 2; + } + if (typeof target !== "object" && !jQuery.isFunction(target)) { + target = {}; + } + if (length === i) { + target = this; + --i; + } + for (i; i < length; i++) { + if ((options = arguments[i]) !== null) { + for (name in options) { + src = target[name]; + copy = options[name]; + if (target === copy) { + continue; + } + if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + // WARNING: RECURSION + target[name] = extend(deep, clone, copy); + } else if (copy !== undefined) { + target[name] = copy; + } + } + } + } + + return target; + } + + + function getScrollingInfo(acediff, dir) { + return (dir == C.EDITOR_LEFT) ? acediff.editors.left.ace.getSession().getScrollTop() : acediff.editors.right.ace.getSession().getScrollTop(); + } + + + function getEditorHeight(acediff) { + //editorHeight: document.getElementById(acediff.options.left.id).clientHeight + return document.getElementById(acediff.options.left.id).offsetHeight; + } + + // generates a Bezier curve in SVG format + function getCurve(startX, startY, endX, endY) { + var w = endX - startX; + var halfWidth = startX + (w / 2); + + // position it at the initial x,y coords + var curve = 'M ' + startX + ' ' + startY + + + // now create the curve. This is of the form "C M,N O,P Q,R" where C is a directive for SVG ("curveto"), + // M,N are the first curve control point, O,P the second control point and Q,R are the final coords + ' C ' + halfWidth + ',' + startY + ' ' + halfWidth + ',' + endY + ' ' + endX + ',' + endY; + + return curve; + } + + + function on(elSelector, eventName, selector, fn) { + var element = (elSelector === 'document') ? document : document.querySelector(elSelector); + + element.addEventListener(eventName, function(event) { + var possibleTargets = element.querySelectorAll(selector); + var target = event.target; + + for (var i = 0, l = possibleTargets.length; i < l; i++) { + var el = target; + var p = possibleTargets[i]; + + while(el && el !== element) { + if (el === p) { + return fn.call(p, event); + } + el = el.parentNode; + } + } + }); + } + + + function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + } + + return AceDiff; + +})); |