Chinthakayala,Sheshashailavas(sc2914) | 8f6a6c4 | 2018-06-27 16:11:44 +0000 | [diff] [blame] | 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 | })); |