merge.min.js (37526B)
1 // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 // Distributed under an MIT license: http://codemirror.net/LICENSE 3 4 // declare global: diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL 5 6 (function(mod) { 7 if (typeof exports == "object" && typeof module == "object") // CommonJS 8 mod(require("../../lib/codemirror")); // Note non-packaged dependency diff_match_patch 9 else if (typeof define == "function" && define.amd) // AMD 10 define(["../../lib/codemirror", "diff_match_patch"], mod); 11 else // Plain browser env 12 mod(CodeMirror); 13 })(function(CodeMirror) { 14 "use strict"; 15 var Pos = CodeMirror.Pos; 16 var svgNS = "http://www.w3.org/2000/svg"; 17 18 function DiffView(mv, type) { 19 this.mv = mv; 20 this.type = type; 21 this.classes = type == "left" 22 ? {chunk: "CodeMirror-merge-l-chunk", 23 start: "CodeMirror-merge-l-chunk-start", 24 end: "CodeMirror-merge-l-chunk-end", 25 insert: "CodeMirror-merge-l-inserted", 26 del: "CodeMirror-merge-l-deleted", 27 connect: "CodeMirror-merge-l-connect"} 28 : {chunk: "CodeMirror-merge-r-chunk", 29 start: "CodeMirror-merge-r-chunk-start", 30 end: "CodeMirror-merge-r-chunk-end", 31 insert: "CodeMirror-merge-r-inserted", 32 del: "CodeMirror-merge-r-deleted", 33 connect: "CodeMirror-merge-r-connect"}; 34 } 35 36 DiffView.prototype = { 37 constructor: DiffView, 38 init: function(pane, orig, options) { 39 this.edit = this.mv.edit; 40 ;(this.edit.state.diffViews || (this.edit.state.diffViews = [])).push(this); 41 this.orig = CodeMirror(pane, copyObj({value: orig, readOnly: !this.mv.options.allowEditingOriginals}, copyObj(options))); 42 if (this.mv.options.connect == "align") { 43 if (!this.edit.state.trackAlignable) this.edit.state.trackAlignable = new TrackAlignable(this.edit) 44 this.orig.state.trackAlignable = new TrackAlignable(this.orig) 45 } 46 47 this.orig.state.diffViews = [this]; 48 var classLocation = options.chunkClassLocation || "background"; 49 if (Object.prototype.toString.call(classLocation) != "[object Array]") classLocation = [classLocation] 50 this.classes.classLocation = classLocation 51 52 this.diff = getDiff(asString(orig), asString(options.value), this.mv.options.ignoreWhitespace); 53 this.chunks = getChunks(this.diff); 54 this.diffOutOfDate = this.dealigned = false; 55 this.needsScrollSync = null 56 57 this.showDifferences = options.showDifferences !== false; 58 }, 59 registerEvents: function(otherDv) { 60 this.forceUpdate = registerUpdate(this); 61 setScrollLock(this, true, false); 62 registerScroll(this, otherDv); 63 }, 64 setShowDifferences: function(val) { 65 val = val !== false; 66 if (val != this.showDifferences) { 67 this.showDifferences = val; 68 this.forceUpdate("full"); 69 } 70 } 71 }; 72 73 function ensureDiff(dv) { 74 if (dv.diffOutOfDate) { 75 dv.diff = getDiff(dv.orig.getValue(), dv.edit.getValue(), dv.mv.options.ignoreWhitespace); 76 dv.chunks = getChunks(dv.diff); 77 dv.diffOutOfDate = false; 78 CodeMirror.signal(dv.edit, "updateDiff", dv.diff); 79 } 80 } 81 82 var updating = false; 83 function registerUpdate(dv) { 84 var edit = {from: 0, to: 0, marked: []}; 85 var orig = {from: 0, to: 0, marked: []}; 86 var debounceChange, updatingFast = false; 87 function update(mode) { 88 updating = true; 89 updatingFast = false; 90 if (mode == "full") { 91 if (dv.svg) clear(dv.svg); 92 if (dv.copyButtons) clear(dv.copyButtons); 93 clearMarks(dv.edit, edit.marked, dv.classes); 94 clearMarks(dv.orig, orig.marked, dv.classes); 95 edit.from = edit.to = orig.from = orig.to = 0; 96 } 97 ensureDiff(dv); 98 if (dv.showDifferences) { 99 updateMarks(dv.edit, dv.diff, edit, DIFF_INSERT, dv.classes); 100 updateMarks(dv.orig, dv.diff, orig, DIFF_DELETE, dv.classes); 101 } 102 103 if (dv.mv.options.connect == "align") 104 alignChunks(dv); 105 makeConnections(dv); 106 if (dv.needsScrollSync != null) syncScroll(dv, dv.needsScrollSync) 107 108 updating = false; 109 } 110 function setDealign(fast) { 111 if (updating) return; 112 dv.dealigned = true; 113 set(fast); 114 } 115 function set(fast) { 116 if (updating || updatingFast) return; 117 clearTimeout(debounceChange); 118 if (fast === true) updatingFast = true; 119 debounceChange = setTimeout(update, fast === true ? 20 : 250); 120 } 121 function change(_cm, change) { 122 if (!dv.diffOutOfDate) { 123 dv.diffOutOfDate = true; 124 edit.from = edit.to = orig.from = orig.to = 0; 125 } 126 // Update faster when a line was added/removed 127 setDealign(change.text.length - 1 != change.to.line - change.from.line); 128 } 129 function swapDoc() { 130 dv.diffOutOfDate = true; 131 dv.dealigned = true; 132 update("full"); 133 } 134 dv.edit.on("change", change); 135 dv.orig.on("change", change); 136 dv.edit.on("swapDoc", swapDoc); 137 dv.orig.on("swapDoc", swapDoc); 138 if (dv.mv.options.connect == "align") { 139 CodeMirror.on(dv.edit.state.trackAlignable, "realign", setDealign) 140 CodeMirror.on(dv.orig.state.trackAlignable, "realign", setDealign) 141 } 142 dv.edit.on("viewportChange", function() { set(false); }); 143 dv.orig.on("viewportChange", function() { set(false); }); 144 update(); 145 return update; 146 } 147 148 function registerScroll(dv, otherDv) { 149 dv.edit.on("scroll", function() { 150 syncScroll(dv, true) && makeConnections(dv); 151 }); 152 dv.orig.on("scroll", function() { 153 syncScroll(dv, false) && makeConnections(dv); 154 if (otherDv) syncScroll(otherDv, true) && makeConnections(otherDv); 155 }); 156 } 157 158 function syncScroll(dv, toOrig) { 159 // Change handler will do a refresh after a timeout when diff is out of date 160 if (dv.diffOutOfDate) { 161 if (dv.lockScroll && dv.needsScrollSync == null) dv.needsScrollSync = toOrig 162 return false 163 } 164 dv.needsScrollSync = null 165 if (!dv.lockScroll) return true; 166 var editor, other, now = +new Date; 167 if (toOrig) { editor = dv.edit; other = dv.orig; } 168 else { editor = dv.orig; other = dv.edit; } 169 // Don't take action if the position of this editor was recently set 170 // (to prevent feedback loops) 171 if (editor.state.scrollSetBy == dv && (editor.state.scrollSetAt || 0) + 250 > now) return false; 172 173 var sInfo = editor.getScrollInfo(); 174 if (dv.mv.options.connect == "align") { 175 targetPos = sInfo.top; 176 } else { 177 var halfScreen = .5 * sInfo.clientHeight, midY = sInfo.top + halfScreen; 178 var mid = editor.lineAtHeight(midY, "local"); 179 var around = chunkBoundariesAround(dv.chunks, mid, toOrig); 180 var off = getOffsets(editor, toOrig ? around.edit : around.orig); 181 var offOther = getOffsets(other, toOrig ? around.orig : around.edit); 182 var ratio = (midY - off.top) / (off.bot - off.top); 183 var targetPos = (offOther.top - halfScreen) + ratio * (offOther.bot - offOther.top); 184 185 var botDist, mix; 186 // Some careful tweaking to make sure no space is left out of view 187 // when scrolling to top or bottom. 188 if (targetPos > sInfo.top && (mix = sInfo.top / halfScreen) < 1) { 189 targetPos = targetPos * mix + sInfo.top * (1 - mix); 190 } else if ((botDist = sInfo.height - sInfo.clientHeight - sInfo.top) < halfScreen) { 191 var otherInfo = other.getScrollInfo(); 192 var botDistOther = otherInfo.height - otherInfo.clientHeight - targetPos; 193 if (botDistOther > botDist && (mix = botDist / halfScreen) < 1) 194 targetPos = targetPos * mix + (otherInfo.height - otherInfo.clientHeight - botDist) * (1 - mix); 195 } 196 } 197 198 other.scrollTo(sInfo.left, targetPos); 199 other.state.scrollSetAt = now; 200 other.state.scrollSetBy = dv; 201 return true; 202 } 203 204 function getOffsets(editor, around) { 205 var bot = around.after; 206 if (bot == null) bot = editor.lastLine() + 1; 207 return {top: editor.heightAtLine(around.before || 0, "local"), 208 bot: editor.heightAtLine(bot, "local")}; 209 } 210 211 function setScrollLock(dv, val, action) { 212 dv.lockScroll = val; 213 if (val && action != false) syncScroll(dv, DIFF_INSERT) && makeConnections(dv); 214 dv.lockButton.innerHTML = val ? "\u21db\u21da" : "\u21db \u21da"; 215 } 216 217 // Updating the marks for editor content 218 219 function removeClass(editor, line, classes) { 220 var locs = classes.classLocation 221 for (var i = 0; i < locs.length; i++) { 222 editor.removeLineClass(line, locs[i], classes.chunk); 223 editor.removeLineClass(line, locs[i], classes.start); 224 editor.removeLineClass(line, locs[i], classes.end); 225 } 226 } 227 228 function clearMarks(editor, arr, classes) { 229 for (var i = 0; i < arr.length; ++i) { 230 var mark = arr[i]; 231 if (mark instanceof CodeMirror.TextMarker) 232 mark.clear(); 233 else if (mark.parent) 234 removeClass(editor, mark, classes); 235 } 236 arr.length = 0; 237 } 238 239 // FIXME maybe add a margin around viewport to prevent too many updates 240 function updateMarks(editor, diff, state, type, classes) { 241 var vp = editor.getViewport(); 242 editor.operation(function() { 243 if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { 244 clearMarks(editor, state.marked, classes); 245 markChanges(editor, diff, type, state.marked, vp.from, vp.to, classes); 246 state.from = vp.from; state.to = vp.to; 247 } else { 248 if (vp.from < state.from) { 249 markChanges(editor, diff, type, state.marked, vp.from, state.from, classes); 250 state.from = vp.from; 251 } 252 if (vp.to > state.to) { 253 markChanges(editor, diff, type, state.marked, state.to, vp.to, classes); 254 state.to = vp.to; 255 } 256 } 257 }); 258 } 259 260 function addClass(editor, lineNr, classes, main, start, end) { 261 var locs = classes.classLocation, line = editor.getLineHandle(lineNr); 262 for (var i = 0; i < locs.length; i++) { 263 if (main) editor.addLineClass(line, locs[i], classes.chunk); 264 if (start) editor.addLineClass(line, locs[i], classes.start); 265 if (end) editor.addLineClass(line, locs[i], classes.end); 266 } 267 return line; 268 } 269 270 function markChanges(editor, diff, type, marks, from, to, classes) { 271 var pos = Pos(0, 0); 272 var top = Pos(from, 0), bot = editor.clipPos(Pos(to - 1)); 273 var cls = type == DIFF_DELETE ? classes.del : classes.insert; 274 function markChunk(start, end) { 275 var bfrom = Math.max(from, start), bto = Math.min(to, end); 276 for (var i = bfrom; i < bto; ++i) 277 marks.push(addClass(editor, i, classes, true, i == start, i == end - 1)); 278 // When the chunk is empty, make sure a horizontal line shows up 279 if (start == end && bfrom == end && bto == end) { 280 if (bfrom) 281 marks.push(addClass(editor, bfrom - 1, classes, false, false, true)); 282 else 283 marks.push(addClass(editor, bfrom, classes, false, true, false)); 284 } 285 } 286 287 var chunkStart = 0, pending = false; 288 for (var i = 0; i < diff.length; ++i) { 289 var part = diff[i], tp = part[0], str = part[1]; 290 if (tp == DIFF_EQUAL) { 291 var cleanFrom = pos.line + (startOfLineClean(diff, i) ? 0 : 1); 292 moveOver(pos, str); 293 var cleanTo = pos.line + (endOfLineClean(diff, i) ? 1 : 0); 294 if (cleanTo > cleanFrom) { 295 if (pending) { markChunk(chunkStart, cleanFrom); pending = false } 296 chunkStart = cleanTo; 297 } 298 } else { 299 pending = true 300 if (tp == type) { 301 var end = moveOver(pos, str, true); 302 var a = posMax(top, pos), b = posMin(bot, end); 303 if (!posEq(a, b)) 304 marks.push(editor.markText(a, b, {className: cls})); 305 pos = end; 306 } 307 } 308 } 309 if (pending) markChunk(chunkStart, pos.line + 1); 310 } 311 312 // Updating the gap between editor and original 313 314 function makeConnections(dv) { 315 if (!dv.showDifferences) return; 316 317 if (dv.svg) { 318 clear(dv.svg); 319 var w = dv.gap.offsetWidth; 320 attrs(dv.svg, "width", w, "height", dv.gap.offsetHeight); 321 } 322 if (dv.copyButtons) clear(dv.copyButtons); 323 324 var vpEdit = dv.edit.getViewport(), vpOrig = dv.orig.getViewport(); 325 var outerTop = dv.mv.wrap.getBoundingClientRect().top 326 var sTopEdit = outerTop - dv.edit.getScrollerElement().getBoundingClientRect().top + dv.edit.getScrollInfo().top 327 var sTopOrig = outerTop - dv.orig.getScrollerElement().getBoundingClientRect().top + dv.orig.getScrollInfo().top; 328 for (var i = 0; i < dv.chunks.length; i++) { 329 var ch = dv.chunks[i]; 330 if (ch.editFrom <= vpEdit.to && ch.editTo >= vpEdit.from && 331 ch.origFrom <= vpOrig.to && ch.origTo >= vpOrig.from) 332 drawConnectorsForChunk(dv, ch, sTopOrig, sTopEdit, w); 333 } 334 } 335 336 function getMatchingOrigLine(editLine, chunks) { 337 var editStart = 0, origStart = 0; 338 for (var i = 0; i < chunks.length; i++) { 339 var chunk = chunks[i]; 340 if (chunk.editTo > editLine && chunk.editFrom <= editLine) return null; 341 if (chunk.editFrom > editLine) break; 342 editStart = chunk.editTo; 343 origStart = chunk.origTo; 344 } 345 return origStart + (editLine - editStart); 346 } 347 348 // Combines information about chunks and widgets/markers to return 349 // an array of lines, in a single editor, that probably need to be 350 // aligned with their counterparts in the editor next to it. 351 function alignableFor(cm, chunks, isOrig) { 352 var tracker = cm.state.trackAlignable 353 var start = cm.firstLine(), trackI = 0 354 var result = [] 355 for (var i = 0;; i++) { 356 var chunk = chunks[i] 357 var chunkStart = !chunk ? 1e9 : isOrig ? chunk.origFrom : chunk.editFrom 358 for (; trackI < tracker.alignable.length; trackI += 2) { 359 var n = tracker.alignable[trackI] + 1 360 if (n <= start) continue 361 if (n <= chunkStart) result.push(n) 362 else break 363 } 364 if (!chunk) break 365 result.push(start = isOrig ? chunk.origTo : chunk.editTo) 366 } 367 return result 368 } 369 370 // Given information about alignable lines in two editors, fill in 371 // the result (an array of three-element arrays) to reflect the 372 // lines that need to be aligned with each other. 373 function mergeAlignable(result, origAlignable, chunks, setIndex) { 374 var rI = 0, origI = 0, chunkI = 0, diff = 0 375 outer: for (;; rI++) { 376 var nextR = result[rI], nextO = origAlignable[origI] 377 if (!nextR && nextO == null) break 378 379 var rLine = nextR ? nextR[0] : 1e9, oLine = nextO == null ? 1e9 : nextO 380 while (chunkI < chunks.length) { 381 var chunk = chunks[chunkI] 382 if (chunk.origFrom <= oLine && chunk.origTo > oLine) { 383 origI++ 384 rI-- 385 continue outer; 386 } 387 if (chunk.editTo > rLine) { 388 if (chunk.editFrom <= rLine) continue outer; 389 break 390 } 391 diff += (chunk.origTo - chunk.origFrom) - (chunk.editTo - chunk.editFrom) 392 chunkI++ 393 } 394 if (rLine == oLine - diff) { 395 nextR[setIndex] = oLine 396 origI++ 397 } else if (rLine < oLine - diff) { 398 nextR[setIndex] = rLine + diff 399 } else { 400 var record = [oLine - diff, null, null] 401 record[setIndex] = oLine 402 result.splice(rI, 0, record) 403 origI++ 404 } 405 } 406 } 407 408 function findAlignedLines(dv, other) { 409 var alignable = alignableFor(dv.edit, dv.chunks, false), result = [] 410 if (other) for (var i = 0, j = 0; i < other.chunks.length; i++) { 411 var n = other.chunks[i].editTo 412 while (j < alignable.length && alignable[j] < n) j++ 413 if (j == alignable.length || alignable[j] != n) alignable.splice(j++, 0, n) 414 } 415 for (var i = 0; i < alignable.length; i++) 416 result.push([alignable[i], null, null]) 417 418 mergeAlignable(result, alignableFor(dv.orig, dv.chunks, true), dv.chunks, 1) 419 if (other) 420 mergeAlignable(result, alignableFor(other.orig, other.chunks, true), other.chunks, 2) 421 422 return result 423 } 424 425 function alignChunks(dv, force) { 426 if (!dv.dealigned && !force) return; 427 if (!dv.orig.curOp) return dv.orig.operation(function() { 428 alignChunks(dv, force); 429 }); 430 431 dv.dealigned = false; 432 var other = dv.mv.left == dv ? dv.mv.right : dv.mv.left; 433 if (other) { 434 ensureDiff(other); 435 other.dealigned = false; 436 } 437 var linesToAlign = findAlignedLines(dv, other); 438 439 // Clear old aligners 440 var aligners = dv.mv.aligners; 441 for (var i = 0; i < aligners.length; i++) 442 aligners[i].clear(); 443 aligners.length = 0; 444 445 var cm = [dv.edit, dv.orig], scroll = []; 446 if (other) cm.push(other.orig); 447 for (var i = 0; i < cm.length; i++) 448 scroll.push(cm[i].getScrollInfo().top); 449 450 for (var ln = 0; ln < linesToAlign.length; ln++) 451 alignLines(cm, linesToAlign[ln], aligners); 452 453 for (var i = 0; i < cm.length; i++) 454 cm[i].scrollTo(null, scroll[i]); 455 } 456 457 function alignLines(cm, lines, aligners) { 458 var maxOffset = 0, offset = []; 459 for (var i = 0; i < cm.length; i++) if (lines[i] != null) { 460 var off = cm[i].heightAtLine(lines[i], "local"); 461 offset[i] = off; 462 maxOffset = Math.max(maxOffset, off); 463 } 464 for (var i = 0; i < cm.length; i++) if (lines[i] != null) { 465 var diff = maxOffset - offset[i]; 466 if (diff > 1) 467 aligners.push(padAbove(cm[i], lines[i], diff)); 468 } 469 } 470 471 function padAbove(cm, line, size) { 472 var above = true; 473 if (line > cm.lastLine()) { 474 line--; 475 above = false; 476 } 477 var elt = document.createElement("div"); 478 elt.className = "CodeMirror-merge-spacer"; 479 elt.style.height = size + "px"; elt.style.minWidth = "1px"; 480 return cm.addLineWidget(line, elt, {height: size, above: above, mergeSpacer: true, handleMouseEvents: true}); 481 } 482 483 function drawConnectorsForChunk(dv, chunk, sTopOrig, sTopEdit, w) { 484 var flip = dv.type == "left"; 485 var top = dv.orig.heightAtLine(chunk.origFrom, "local", true) - sTopOrig; 486 if (dv.svg) { 487 var topLpx = top; 488 var topRpx = dv.edit.heightAtLine(chunk.editFrom, "local", true) - sTopEdit; 489 if (flip) { var tmp = topLpx; topLpx = topRpx; topRpx = tmp; } 490 var botLpx = dv.orig.heightAtLine(chunk.origTo, "local", true) - sTopOrig; 491 var botRpx = dv.edit.heightAtLine(chunk.editTo, "local", true) - sTopEdit; 492 if (flip) { var tmp = botLpx; botLpx = botRpx; botRpx = tmp; } 493 var curveTop = " C " + w/2 + " " + topRpx + " " + w/2 + " " + topLpx + " " + (w + 2) + " " + topLpx; 494 var curveBot = " C " + w/2 + " " + botLpx + " " + w/2 + " " + botRpx + " -1 " + botRpx; 495 attrs(dv.svg.appendChild(document.createElementNS(svgNS, "path")), 496 "d", "M -1 " + topRpx + curveTop + " L " + (w + 2) + " " + botLpx + curveBot + " z", 497 "class", dv.classes.connect); 498 } 499 if (dv.copyButtons) { 500 var copy = dv.copyButtons.appendChild(elt("div", dv.type == "left" ? "\u21dd" : "\u21dc", 501 "CodeMirror-merge-copy")); 502 var editOriginals = dv.mv.options.allowEditingOriginals; 503 copy.title = editOriginals ? "Push to left" : "Revert chunk"; 504 copy.chunk = chunk; 505 copy.style.top = (chunk.origTo > chunk.origFrom ? top : dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit) + "px"; 506 507 if (editOriginals) { 508 var topReverse = dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit; 509 var copyReverse = dv.copyButtons.appendChild(elt("div", dv.type == "right" ? "\u21dd" : "\u21dc", 510 "CodeMirror-merge-copy-reverse")); 511 copyReverse.title = "Push to right"; 512 copyReverse.chunk = {editFrom: chunk.origFrom, editTo: chunk.origTo, 513 origFrom: chunk.editFrom, origTo: chunk.editTo}; 514 copyReverse.style.top = topReverse + "px"; 515 dv.type == "right" ? copyReverse.style.left = "2px" : copyReverse.style.right = "2px"; 516 } 517 } 518 } 519 520 function copyChunk(dv, to, from, chunk) { 521 if (dv.diffOutOfDate) return; 522 var origStart = chunk.origTo > from.lastLine() ? Pos(chunk.origFrom - 1) : Pos(chunk.origFrom, 0) 523 var origEnd = Pos(chunk.origTo, 0) 524 var editStart = chunk.editTo > to.lastLine() ? Pos(chunk.editFrom - 1) : Pos(chunk.editFrom, 0) 525 var editEnd = Pos(chunk.editTo, 0) 526 var handler = dv.mv.options.revertChunk 527 if (handler) 528 handler(dv.mv, from, origStart, origEnd, to, editStart, editEnd) 529 else 530 to.replaceRange(from.getRange(origStart, origEnd), editStart, editEnd) 531 } 532 533 // Merge view, containing 0, 1, or 2 diff views. 534 535 var MergeView = CodeMirror.MergeView = function(node, options) { 536 if (!(this instanceof MergeView)) return new MergeView(node, options); 537 538 this.options = options; 539 var origLeft = options.origLeft, origRight = options.origRight == null ? options.orig : options.origRight; 540 541 var hasLeft = origLeft != null, hasRight = origRight != null; 542 var panes = 1 + (hasLeft ? 1 : 0) + (hasRight ? 1 : 0); 543 var wrap = [], left = this.left = null, right = this.right = null; 544 var self = this; 545 546 if (hasLeft) { 547 left = this.left = new DiffView(this, "left"); 548 var leftPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-left"); 549 wrap.push(leftPane); 550 wrap.push(buildGap(left)); 551 } 552 553 var editPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-editor"); 554 wrap.push(editPane); 555 556 if (hasRight) { 557 right = this.right = new DiffView(this, "right"); 558 wrap.push(buildGap(right)); 559 var rightPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-right"); 560 wrap.push(rightPane); 561 } 562 563 (hasRight ? rightPane : editPane).className += " CodeMirror-merge-pane-rightmost"; 564 565 wrap.push(elt("div", null, null, "height: 0; clear: both;")); 566 567 var wrapElt = this.wrap = node.appendChild(elt("div", wrap, "CodeMirror-merge CodeMirror-merge-" + panes + "pane")); 568 this.edit = CodeMirror(editPane, copyObj(options)); 569 570 if (left) left.init(leftPane, origLeft, options); 571 if (right) right.init(rightPane, origRight, options); 572 if (options.collapseIdentical) 573 this.editor().operation(function() { 574 collapseIdenticalStretches(self, options.collapseIdentical); 575 }); 576 if (options.connect == "align") { 577 this.aligners = []; 578 alignChunks(this.left || this.right, true); 579 } 580 if (left) left.registerEvents(right) 581 if (right) right.registerEvents(left) 582 583 584 var onResize = function() { 585 if (left) makeConnections(left); 586 if (right) makeConnections(right); 587 }; 588 CodeMirror.on(window, "resize", onResize); 589 var resizeInterval = setInterval(function() { 590 for (var p = wrapElt.parentNode; p && p != document.body; p = p.parentNode) {} 591 if (!p) { clearInterval(resizeInterval); CodeMirror.off(window, "resize", onResize); } 592 }, 5000); 593 }; 594 595 function buildGap(dv) { 596 var lock = dv.lockButton = elt("div", null, "CodeMirror-merge-scrolllock"); 597 lock.title = "Toggle locked scrolling"; 598 var lockWrap = elt("div", [lock], "CodeMirror-merge-scrolllock-wrap"); 599 CodeMirror.on(lock, "click", function() { setScrollLock(dv, !dv.lockScroll); }); 600 var gapElts = [lockWrap]; 601 if (dv.mv.options.revertButtons !== false) { 602 dv.copyButtons = elt("div", null, "CodeMirror-merge-copybuttons-" + dv.type); 603 CodeMirror.on(dv.copyButtons, "click", function(e) { 604 var node = e.target || e.srcElement; 605 if (!node.chunk) return; 606 if (node.className == "CodeMirror-merge-copy-reverse") { 607 copyChunk(dv, dv.orig, dv.edit, node.chunk); 608 return; 609 } 610 copyChunk(dv, dv.edit, dv.orig, node.chunk); 611 }); 612 gapElts.unshift(dv.copyButtons); 613 } 614 if (dv.mv.options.connect != "align") { 615 var svg = document.createElementNS && document.createElementNS(svgNS, "svg"); 616 if (svg && !svg.createSVGRect) svg = null; 617 dv.svg = svg; 618 if (svg) gapElts.push(svg); 619 } 620 621 return dv.gap = elt("div", gapElts, "CodeMirror-merge-gap"); 622 } 623 624 MergeView.prototype = { 625 constructor: MergeView, 626 editor: function() { return this.edit; }, 627 rightOriginal: function() { return this.right && this.right.orig; }, 628 leftOriginal: function() { return this.left && this.left.orig; }, 629 setShowDifferences: function(val) { 630 if (this.right) this.right.setShowDifferences(val); 631 if (this.left) this.left.setShowDifferences(val); 632 }, 633 rightChunks: function() { 634 if (this.right) { ensureDiff(this.right); return this.right.chunks; } 635 }, 636 leftChunks: function() { 637 if (this.left) { ensureDiff(this.left); return this.left.chunks; } 638 } 639 }; 640 641 function asString(obj) { 642 if (typeof obj == "string") return obj; 643 else return obj.getValue(); 644 } 645 646 // Operations on diffs 647 var dmp; 648 function getDiff(a, b, ignoreWhitespace) { 649 if (!dmp) dmp = new diff_match_patch(); 650 651 var diff = dmp.diff_main(a, b); 652 // The library sometimes leaves in empty parts, which confuse the algorithm 653 for (var i = 0; i < diff.length; ++i) { 654 var part = diff[i]; 655 if (ignoreWhitespace ? !/[^ \t]/.test(part[1]) : !part[1]) { 656 diff.splice(i--, 1); 657 } else if (i && diff[i - 1][0] == part[0]) { 658 diff.splice(i--, 1); 659 diff[i][1] += part[1]; 660 } 661 } 662 return diff; 663 } 664 665 function getChunks(diff) { 666 var chunks = []; 667 var startEdit = 0, startOrig = 0; 668 var edit = Pos(0, 0), orig = Pos(0, 0); 669 for (var i = 0; i < diff.length; ++i) { 670 var part = diff[i], tp = part[0]; 671 if (tp == DIFF_EQUAL) { 672 var startOff = !startOfLineClean(diff, i) || edit.line < startEdit || orig.line < startOrig ? 1 : 0; 673 var cleanFromEdit = edit.line + startOff, cleanFromOrig = orig.line + startOff; 674 moveOver(edit, part[1], null, orig); 675 var endOff = endOfLineClean(diff, i) ? 1 : 0; 676 var cleanToEdit = edit.line + endOff, cleanToOrig = orig.line + endOff; 677 if (cleanToEdit > cleanFromEdit) { 678 if (i) chunks.push({origFrom: startOrig, origTo: cleanFromOrig, 679 editFrom: startEdit, editTo: cleanFromEdit}); 680 startEdit = cleanToEdit; startOrig = cleanToOrig; 681 } 682 } else { 683 moveOver(tp == DIFF_INSERT ? edit : orig, part[1]); 684 } 685 } 686 if (startEdit <= edit.line || startOrig <= orig.line) 687 chunks.push({origFrom: startOrig, origTo: orig.line + 1, 688 editFrom: startEdit, editTo: edit.line + 1}); 689 return chunks; 690 } 691 692 function endOfLineClean(diff, i) { 693 if (i == diff.length - 1) return true; 694 var next = diff[i + 1][1]; 695 if ((next.length == 1 && i < diff.length - 2) || next.charCodeAt(0) != 10) return false; 696 if (i == diff.length - 2) return true; 697 next = diff[i + 2][1]; 698 return (next.length > 1 || i == diff.length - 3) && next.charCodeAt(0) == 10; 699 } 700 701 function startOfLineClean(diff, i) { 702 if (i == 0) return true; 703 var last = diff[i - 1][1]; 704 if (last.charCodeAt(last.length - 1) != 10) return false; 705 if (i == 1) return true; 706 last = diff[i - 2][1]; 707 return last.charCodeAt(last.length - 1) == 10; 708 } 709 710 function chunkBoundariesAround(chunks, n, nInEdit) { 711 var beforeE, afterE, beforeO, afterO; 712 for (var i = 0; i < chunks.length; i++) { 713 var chunk = chunks[i]; 714 var fromLocal = nInEdit ? chunk.editFrom : chunk.origFrom; 715 var toLocal = nInEdit ? chunk.editTo : chunk.origTo; 716 if (afterE == null) { 717 if (fromLocal > n) { afterE = chunk.editFrom; afterO = chunk.origFrom; } 718 else if (toLocal > n) { afterE = chunk.editTo; afterO = chunk.origTo; } 719 } 720 if (toLocal <= n) { beforeE = chunk.editTo; beforeO = chunk.origTo; } 721 else if (fromLocal <= n) { beforeE = chunk.editFrom; beforeO = chunk.origFrom; } 722 } 723 return {edit: {before: beforeE, after: afterE}, orig: {before: beforeO, after: afterO}}; 724 } 725 726 function collapseSingle(cm, from, to) { 727 cm.addLineClass(from, "wrap", "CodeMirror-merge-collapsed-line"); 728 var widget = document.createElement("span"); 729 widget.className = "CodeMirror-merge-collapsed-widget"; 730 widget.title = "Identical text collapsed. Click to expand."; 731 var mark = cm.markText(Pos(from, 0), Pos(to - 1), { 732 inclusiveLeft: true, 733 inclusiveRight: true, 734 replacedWith: widget, 735 clearOnEnter: true 736 }); 737 function clear() { 738 mark.clear(); 739 cm.removeLineClass(from, "wrap", "CodeMirror-merge-collapsed-line"); 740 } 741 if (mark.explicitlyCleared) clear(); 742 CodeMirror.on(widget, "click", clear); 743 mark.on("clear", clear); 744 CodeMirror.on(widget, "click", clear); 745 return {mark: mark, clear: clear}; 746 } 747 748 function collapseStretch(size, editors) { 749 var marks = []; 750 function clear() { 751 for (var i = 0; i < marks.length; i++) marks[i].clear(); 752 } 753 for (var i = 0; i < editors.length; i++) { 754 var editor = editors[i]; 755 var mark = collapseSingle(editor.cm, editor.line, editor.line + size); 756 marks.push(mark); 757 mark.mark.on("clear", clear); 758 } 759 return marks[0].mark; 760 } 761 762 function unclearNearChunks(dv, margin, off, clear) { 763 for (var i = 0; i < dv.chunks.length; i++) { 764 var chunk = dv.chunks[i]; 765 for (var l = chunk.editFrom - margin; l < chunk.editTo + margin; l++) { 766 var pos = l + off; 767 if (pos >= 0 && pos < clear.length) clear[pos] = false; 768 } 769 } 770 } 771 772 function collapseIdenticalStretches(mv, margin) { 773 if (typeof margin != "number") margin = 2; 774 var clear = [], edit = mv.editor(), off = edit.firstLine(); 775 for (var l = off, e = edit.lastLine(); l <= e; l++) clear.push(true); 776 if (mv.left) unclearNearChunks(mv.left, margin, off, clear); 777 if (mv.right) unclearNearChunks(mv.right, margin, off, clear); 778 779 for (var i = 0; i < clear.length; i++) { 780 if (clear[i]) { 781 var line = i + off; 782 for (var size = 1; i < clear.length - 1 && clear[i + 1]; i++, size++) {} 783 if (size > margin) { 784 var editors = [{line: line, cm: edit}]; 785 if (mv.left) editors.push({line: getMatchingOrigLine(line, mv.left.chunks), cm: mv.left.orig}); 786 if (mv.right) editors.push({line: getMatchingOrigLine(line, mv.right.chunks), cm: mv.right.orig}); 787 var mark = collapseStretch(size, editors); 788 if (mv.options.onCollapse) mv.options.onCollapse(mv, line, size, mark); 789 } 790 } 791 } 792 } 793 794 // General utilities 795 796 function elt(tag, content, className, style) { 797 var e = document.createElement(tag); 798 if (className) e.className = className; 799 if (style) e.style.cssText = style; 800 if (typeof content == "string") e.appendChild(document.createTextNode(content)); 801 else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); 802 return e; 803 } 804 805 function clear(node) { 806 for (var count = node.childNodes.length; count > 0; --count) 807 node.removeChild(node.firstChild); 808 } 809 810 function attrs(elt) { 811 for (var i = 1; i < arguments.length; i += 2) 812 elt.setAttribute(arguments[i], arguments[i+1]); 813 } 814 815 function copyObj(obj, target) { 816 if (!target) target = {}; 817 for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop]; 818 return target; 819 } 820 821 function moveOver(pos, str, copy, other) { 822 var out = copy ? Pos(pos.line, pos.ch) : pos, at = 0; 823 for (;;) { 824 var nl = str.indexOf("\n", at); 825 if (nl == -1) break; 826 ++out.line; 827 if (other) ++other.line; 828 at = nl + 1; 829 } 830 out.ch = (at ? 0 : out.ch) + (str.length - at); 831 if (other) other.ch = (at ? 0 : other.ch) + (str.length - at); 832 return out; 833 } 834 835 // Tracks collapsed markers and line widgets, in order to be able to 836 // accurately align the content of two editors. 837 838 var F_WIDGET = 1, F_WIDGET_BELOW = 2, F_MARKER = 4 839 840 function TrackAlignable(cm) { 841 this.cm = cm 842 this.alignable = [] 843 this.height = cm.doc.height 844 var self = this 845 cm.on("markerAdded", function(_, marker) { 846 if (!marker.collapsed) return 847 var found = marker.find(1) 848 if (found != null) self.set(found.line, F_MARKER) 849 }) 850 cm.on("markerCleared", function(_, marker, _min, max) { 851 if (max != null && marker.collapsed) 852 self.check(max, F_MARKER, self.hasMarker) 853 }) 854 cm.on("markerChanged", this.signal.bind(this)) 855 cm.on("lineWidgetAdded", function(_, widget, lineNo) { 856 if (widget.mergeSpacer) return 857 if (widget.above) self.set(lineNo - 1, F_WIDGET_BELOW) 858 else self.set(lineNo, F_WIDGET) 859 }) 860 cm.on("lineWidgetCleared", function(_, widget, lineNo) { 861 if (widget.mergeSpacer) return 862 if (widget.above) self.check(lineNo - 1, F_WIDGET_BELOW, self.hasWidgetBelow) 863 else self.check(lineNo, F_WIDGET, self.hasWidget) 864 }) 865 cm.on("lineWidgetChanged", this.signal.bind(this)) 866 cm.on("change", function(_, change) { 867 var start = change.from.line, nBefore = change.to.line - change.from.line 868 var nAfter = change.text.length - 1, end = start + nAfter 869 if (nBefore || nAfter) self.map(start, nBefore, nAfter) 870 self.check(end, F_MARKER, self.hasMarker) 871 if (nBefore || nAfter) self.check(change.from.line, F_MARKER, self.hasMarker) 872 }) 873 cm.on("viewportChange", function() { 874 if (self.cm.doc.height != self.height) self.signal() 875 }) 876 } 877 878 TrackAlignable.prototype = { 879 signal: function() { 880 CodeMirror.signal(this, "realign") 881 this.height = this.cm.doc.height 882 }, 883 884 set: function(n, flags) { 885 var pos = -1 886 for (; pos < this.alignable.length; pos += 2) { 887 var diff = this.alignable[pos] - n 888 if (diff == 0) { 889 if ((this.alignable[pos + 1] & flags) == flags) return 890 this.alignable[pos + 1] |= flags 891 this.signal() 892 return 893 } 894 if (diff > 0) break 895 } 896 this.signal() 897 this.alignable.splice(pos, 0, n, flags) 898 }, 899 900 find: function(n) { 901 for (var i = 0; i < this.alignable.length; i += 2) 902 if (this.alignable[i] == n) return i 903 return -1 904 }, 905 906 check: function(n, flag, pred) { 907 var found = this.find(n) 908 if (found == -1 || !(this.alignable[found + 1] & flag)) return 909 if (!pred.call(this, n)) { 910 this.signal() 911 var flags = this.alignable[found + 1] & ~flag 912 if (flags) this.alignable[found + 1] = flags 913 else this.alignable.splice(found, 2) 914 } 915 }, 916 917 hasMarker: function(n) { 918 var handle = this.cm.getLineHandle(n) 919 if (handle.markedSpans) for (var i = 0; i < handle.markedSpans.length; i++) 920 if (handle.markedSpans[i].mark.collapsed && handle.markedSpans[i].to != null) 921 return true 922 return false 923 }, 924 925 hasWidget: function(n) { 926 var handle = this.cm.getLineHandle(n) 927 if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++) 928 if (!handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true 929 return false 930 }, 931 932 hasWidgetBelow: function(n) { 933 if (n == this.cm.lastLine()) return false 934 var handle = this.cm.getLineHandle(n + 1) 935 if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++) 936 if (handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true 937 return false 938 }, 939 940 map: function(from, nBefore, nAfter) { 941 var diff = nAfter - nBefore, to = from + nBefore, widgetFrom = -1, widgetTo = -1 942 for (var i = 0; i < this.alignable.length; i += 2) { 943 var n = this.alignable[i] 944 if (n == from && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetFrom = i 945 if (n == to && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetTo = i 946 if (n <= from) continue 947 else if (n < to) this.alignable.splice(i--, 2) 948 else this.alignable[i] += diff 949 } 950 if (widgetFrom > -1) { 951 var flags = this.alignable[widgetFrom + 1] 952 if (flags == F_WIDGET_BELOW) this.alignable.splice(widgetFrom, 2) 953 else this.alignable[widgetFrom + 1] = flags & ~F_WIDGET_BELOW 954 } 955 if (widgetTo > -1 && nAfter) 956 this.set(from + nAfter, F_WIDGET_BELOW) 957 } 958 } 959 960 function posMin(a, b) { return (a.line - b.line || a.ch - b.ch) < 0 ? a : b; } 961 function posMax(a, b) { return (a.line - b.line || a.ch - b.ch) > 0 ? a : b; } 962 function posEq(a, b) { return a.line == b.line && a.ch == b.ch; } 963 964 function findPrevDiff(chunks, start, isOrig) { 965 for (var i = chunks.length - 1; i >= 0; i--) { 966 var chunk = chunks[i]; 967 var to = (isOrig ? chunk.origTo : chunk.editTo) - 1; 968 if (to < start) return to; 969 } 970 } 971 972 function findNextDiff(chunks, start, isOrig) { 973 for (var i = 0; i < chunks.length; i++) { 974 var chunk = chunks[i]; 975 var from = (isOrig ? chunk.origFrom : chunk.editFrom); 976 if (from > start) return from; 977 } 978 } 979 980 function goNearbyDiff(cm, dir) { 981 var found = null, views = cm.state.diffViews, line = cm.getCursor().line; 982 if (views) for (var i = 0; i < views.length; i++) { 983 var dv = views[i], isOrig = cm == dv.orig; 984 ensureDiff(dv); 985 var pos = dir < 0 ? findPrevDiff(dv.chunks, line, isOrig) : findNextDiff(dv.chunks, line, isOrig); 986 if (pos != null && (found == null || (dir < 0 ? pos > found : pos < found))) 987 found = pos; 988 } 989 if (found != null) 990 cm.setCursor(found, 0); 991 else 992 return CodeMirror.Pass; 993 } 994 995 CodeMirror.commands.goNextDiff = function(cm) { 996 return goNearbyDiff(cm, 1); 997 }; 998 CodeMirror.commands.goPrevDiff = function(cm) { 999 return goNearbyDiff(cm, -1); 1000 }; 1001 });