define(function(require, exports, module) { "use strict"; var oop = require("ace/lib/oop"); var event = require("ace/lib/event"); var Range = require("ace/range").Range; var dom = require("ace/lib/dom"); var config = require("ace/config"); var LineWidgets = require("ace/line_widgets").LineWidgets; var css = require("text!./styles.css"); var diff_match_patch = require("./diff_match_patch").diff_match_patch; var SVG_NS = "http://www.w3.org/2000/svg"; var Editor = require("ace/editor").Editor; var Renderer = require("ace/virtual_renderer").VirtualRenderer; var UndoManager = require("ace/undomanager").UndoManager; var EditSession = require("ace/edit_session").EditSession; require("ace/theme/textmate"); function createEditor() { var editor = new Editor(new Renderer(), null); editor.session.setUndoManager(new UndoManager()); return editor; } function DiffView(element, options) { this.onInput = this.onInput.bind(this); this.onConnectorScroll = this.onConnectorScroll.bind(this); this.onMouseWheel = this.onMouseWheel.bind(this); this.onScroll = this.onScroll.bind(this); this.onChangeFold = this.onChangeFold.bind(this); this.onChangeTheme = this.onChangeTheme.bind(this); dom.importCssString(css, "diffview.css"); this.options = {}; this.container = element; oop.mixin(this.options, { showDiffs: true, maxDiffs: 5000 }, options); this.orig = this.left = createEditor(); this.edit = this.right = createEditor(); this.gutterEl = document.createElement("div"); element.appendChild(this.left.container); element.appendChild(this.gutterEl); element.appendChild(this.right.container); this.left.setOption("scrollPastEnd", 0.5); this.right.setOption("scrollPastEnd", 0.5); this.left.setOption("highlightActiveLine", false); this.right.setOption("highlightActiveLine", false); this.left.setOption("highlightGutterLine", false); this.right.setOption("highlightGutterLine", false); this.left.setOption("animatedScroll", true); this.right.setOption("animatedScroll", true); this.markerLeft = new DiffHighlight(this, -1); this.markerRight = new DiffHighlight(this, 1); this.setSession({ orig: this.orig.session, edit: this.edit.session, chunks: [] }); this.connector = new Connector(this); this.connector.createGutter(); this.onChangeTheme(); this.$initArrow(); this.$attachEventHandlers(); config.resetOptions(this); config._signal("diffView", this); } (function() { /*** theme/session ***/ this.setSession = function(session) { if (this.session) { this.$detachSessionEventHandlers(); } this.session = session; if (this.session) { this.chunks = this.session.chunks; this.orig.setSession(session.orig); this.edit.setSession(session.edit); this.$attachSessionEventHandlers(); } }; this.getSession = function() { return this.session; }; this.createSession = function() { var session = new EditSession(""); session.setUndoManager(new UndoManager()); return session; }; this.setTheme = function(theme) { this.left.setTheme(theme); }; this.getTheme = function() { return this.left.getTheme(); }; this.onChangeTheme = function() { this.right.setTheme(this.left.getTheme()); var theme = this.right.renderer.theme; this.gutterEl.className = "ace_diff-gutter " + theme.cssClass; dom.setCssClass(this.gutterEl, "ace_dark", theme.isDark); }; this.resize = function() { this.edit.resize(); this.orig.resize(); }; /*** compute diff ***/ this.getDmp = function() { var dmp = new diff_match_patch(); return dmp; }; this.onInput = this.computeDiff = function() { var val1 = this.left.session.doc.getAllLines(); var val2 = this.right.session.doc.getAllLines(); var chunks = this.$diffLines(val1, val2); this.session.chunks = this.chunks = chunks; // if we"re dealing with too many chunks, fail silently if (this.chunks.length > this.options.maxDiffs) { return; } if (this.$alignDiffs) this.align(); this.left.renderer.updateBackMarkers(); this.right.renderer.updateBackMarkers(); }; this.$diffLines = function(val1, val2) { var dmp = this.getDmp(); var a = diff_linesToChars_(val1, val2); var diff = dmp.diff_main(a.chars1, a.chars2, false); var chunks = []; var offset = { left: 0, right: 0 }; var lastChunk; diff.forEach(function(chunk) { var chunkType = chunk[0]; var length = chunk[1].length; // oddly, occasionally the algorithm returns a diff with no changes made if (length === 0) { return; } if (chunkType === 0) { offset.left += length; offset.right += length; lastChunk = null; } else if (chunkType === -1) { if (lastChunk) { lastChunk.origEnd = Math.max(offset.left + length, lastChunk.origEnd); lastChunk.editEnd = Math.max(offset.right, lastChunk.editEnd); } else { chunks.push(lastChunk = { origStart: offset.left, origEnd: offset.left + length, editStart: offset.right, editEnd: offset.right, }); } offset.left += length; } else if (chunkType === 1) { if (lastChunk) { lastChunk.origEnd = Math.max(offset.left, lastChunk.origEnd); lastChunk.editEnd = Math.max(offset.right + length, lastChunk.editEnd); } else { chunks.push(lastChunk = { origStart: offset.left, origEnd: offset.left, editStart: offset.right, editEnd: offset.right + length, }); } offset.right += length; } }, this); chunks.forEach(function(diff) { var inlineChanges = []; var type = 0; if (diff.origStart == diff.origEnd) { type = 1; } else if (diff.editStart == diff.editEnd) { type = -1; } else { var inlineDiff = dmp.diff_main( val1.slice(diff.origStart, diff.origEnd).join("\n"), val2.slice(diff.editStart, diff.editEnd).join("\n"), false ); dmp.diff_cleanupSemantic(inlineDiff); inlineDiff.forEach(function(change) { var text = change[1]; var lines = text.split("\n"); var rowCh = lines.length - 1; var colCh = lines[rowCh].length; var changeType = change[0]; if (text.length) { inlineChanges.push([changeType, rowCh, colCh]); // if (changeType) { // if (!type) { // type = changeType; // } else if (type != changeType) { // type = 2; // } // } } }); } diff.inlineChanges = inlineChanges; diff.type = type; }); return chunks; }; /*** scroll locking ***/ this.align = function() { var diffView = this; function add(editor, w) { editor.session.lineWidgets[w.row] = w; editor.session.widgetManager.lineWidgets.push(w); } function init(editor) { var session = editor.session; if (!session.widgetManager) { session.widgetManager = new LineWidgets(session); session.widgetManager.attach(editor); } editor.session.lineWidgets = []; editor.session.widgetManager.lineWidgets = []; } init(diffView.edit); init(diffView.orig); diffView.chunks.forEach(function(ch) { var diff1 = ch.origEnd - ch.origStart; var diff2 = ch.editEnd - ch.editStart; if (diff1 < diff2) { add(diffView.orig, { rowCount: diff2 - diff1, row: ch.origEnd - 1 }); } else if (diff1 > diff2) { add(diffView.edit, { rowCount: diff1 - diff2, row: ch.editEnd - 1 }); } }); diffView.edit.session._emit("changeFold", { data: { start: { row: 0 }}}); diffView.orig.session._emit("changeFold", { data: { start: { row: 0 }}}); }; this.onScroll = function(e, session) { this.syncScroll(this.left.session == session ? this.left.renderer : this.right.renderer); }; this.syncScroll = function(renderer) { if (this.$syncScroll == false) return; var r1 = this.left.renderer; var r2 = this.right.renderer; var isOrig = renderer == r1; if (r1.$scrollAnimation && r2.$scrollAnimation) return; var now = Date.now(); if (this.scrollSetBy != renderer && now - this.scrollSetAt < 500) return; var r = isOrig ? r1 : r2; if (this.scrollSetBy != renderer) { if (isOrig && this.scrollOrig == r.session.getScrollTop()) return; else if (!isOrig && this.scrollEdit == r.session.getScrollTop()) return; } var rOther = isOrig ? r2 : r1; if (this.$alignDiffs) { targetPos = r.session.getScrollTop(); } else { var layerConfig = r.layerConfig; var chunks = this.chunks; var halfScreen = 0.4 * r.$size.scrollerHeight; var lc = layerConfig; var midY = halfScreen + r.scrollTop; var mid = r.session.screenToDocumentRow(midY / lc.lineHeight, 0); var i = findChunkIndex(chunks, mid, isOrig); var ch = chunks[i]; if (!ch) { ch = { editStart: 0, editEnd: 0, origStart: 0, origEnd: 0 }; } if (mid >= (isOrig ? ch.origEnd : ch.editEnd)) { var next = chunks[i + 1] || { editStart: r2.session.getLength(), editEnd: r2.session.getLength(), origStart: r1.session.getLength(), origEnd: r1.session.getLength() }; ch = { origStart: ch.origEnd, origEnd: next.origStart, editStart: ch.editEnd, editEnd: next.editStart }; } if (r == r1) { var start = ch.origStart; var end = ch.origEnd; var otherStart = ch.editStart; var otherEnd = ch.editEnd; } else { otherStart = ch.origStart; otherEnd = ch.origEnd; start = ch.editStart; end = ch.editEnd; } var offOtherTop = rOther.session.documentToScreenRow(otherStart, 0) * lc.lineHeight; var offOtherBot = rOther.session.documentToScreenRow(otherEnd, 0) * lc.lineHeight; var offTop = r.session.documentToScreenRow(start, 0) * lc.lineHeight; var offBot = r.session.documentToScreenRow(end, 0) * lc.lineHeight; var ratio = (midY - offTop) / (offBot - offTop || offOtherBot - offOtherTop); var targetPos = (offOtherTop - halfScreen) + ratio * (offOtherBot - offOtherTop); targetPos = Math.max(0, targetPos); } this.$syncScroll = false; if (isOrig) { this.scrollOrig = r.session.getScrollTop(); this.scrollEdit = targetPos; } else { this.scrollOrig = targetPos; this.scrollEdit = r.session.getScrollTop(); } this.scrollSetBy = renderer; rOther.session.setScrollTop(targetPos); this.$syncScroll = true; this.scrollSetAt = now; }; this.onConnectorScroll = function(ev) { var dir = ev.wheelY > 0 ? 1 : -1; var r1 = this.left.renderer; var r2 = this.right.renderer; if (r1.$scrollAnimation && r2.$scrollAnimation) return; var minAmount = ev.wheelY * 2; var r = r1.session.getLength() > r2.session.getLength() ? r1 : r2; var layerConfig = r.layerConfig; var chunks = this.chunks; var halfScreen = 0.5 * r.$size.scrollerHeight; var lc = layerConfig; var isOrig = r == r1; var midY = halfScreen + r.scrollTop; var mid = r.session.screenToDocumentRow(midY / lc.lineHeight, 0); var i = findChunkIndex(chunks, mid, isOrig); var ch = chunks[i]; if (dir < 0) { if (!ch) ch = { editStart: 0, editEnd: 0, origStart: 0, origEnd: 0 }; else if (mid < (isOrig ? ch.origEnd : ch.editEnd)) ch = chunks[i - 1] || ch; else ch = chunks[i]; } else { ch = chunks[i + 1] || { editStart: r2.session.getLength(), editEnd: r2.session.getLength(), origStart: r1.session.getLength(), origEnd: r1.session.getLength() }; } var scrollTop1 = r1.session.getScrollTop(); var scrollTop2 = r2.session.getScrollTop(); this.$syncScroll = false; r1.scrollToLine(ch.origStart, true, true); r2.scrollToLine(ch.editStart, true, true); this.$syncScroll = true; if (Math.abs(scrollTop1 - r1.session.getScrollTop()) <= 1 && Math.abs(scrollTop2 - r2.session.getScrollTop()) <= 1 ) { if (r1.isScrollableBy(0, minAmount)) r1.scrollBy(0, minAmount); } }; this.onMouseWheel = function(ev) { if (ev.getAccelKey()) return; if (ev.getShiftKey() && ev.wheelY && !ev.wheelX) { ev.wheelX = ev.wheelY; ev.wheelY = 0; } var editor = ev.editor; var isScrolable = editor.renderer.isScrollableBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed); if (!isScrolable) { var other = editor == this.left ? this.right : this.left; if (other.renderer.isScrollableBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed)) other.renderer.scrollBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed); return ev.stop(); } }; this.onChangeFold = function(ev, session) { if (ev.action == "remove") { var other = session == this.orig.session ? this.edit.session : this.orig.session; var fold = ev.data; if (fold && fold.other) { fold.other.other = null; other.removeFold(fold.other); } } }; this.$attachSessionEventHandlers = function() { this.left.session.on("changeScrollTop", this.onScroll); this.right.session.on("changeScrollTop", this.onScroll); this.left.session.on("changeFold", this.onChangeFold); this.right.session.on("changeFold", this.onChangeFold); this.left.session.addDynamicMarker(this.markerLeft); this.right.session.addDynamicMarker(this.markerRight); }; this.$detachSessionEventHandlers = function() { this.left.session.off("changeScrollTop", this.onScroll); this.right.session.off("changeScrollTop", this.onScroll); this.left.session.off("changeFold", this.onChangeFold); this.right.session.off("changeFold", this.onChangeFold); this.left.session.removeMarker(this.markerLeft.id); this.right.session.removeMarker(this.markerRight.id); }; this.$attachEventHandlers = function() { var _self = this; this.left.renderer.on("themeLoaded", this.onChangeTheme); this.right.renderer.on("afterRender", function() { if (!_self.left.renderer.$loop.changes) _self.connector.decorate(); }); this.left.renderer.on("afterRender", function() { if (!_self.right.renderer.$loop.changes) _self.connector.decorate(); }); event.addMouseWheelListener(this.gutterEl, this.onConnectorScroll); this.left.on("mousewheel", this.onMouseWheel); this.right.on("mousewheel", this.onMouseWheel); this.left.on("input", this.onInput); this.right.on("input", this.onInput); }; this.$initArrow = function() { var arrow = document.createElement("div"); this.gutterEl.appendChild(arrow); var region = 0; var diffView = this; arrow.addEventListener("click", function(e) { if (region && region.chunk) { var range = diffView.useChunk(region.chunk, region.side == 1); var editor = region.side == 1 ? diffView.orig : diffView.edit; editor.selection.moveToPosition(range.start); editor.focus(); } hide(); }); this.gutterEl.addEventListener("mousemove", function(e) { var rect = e.currentTarget.getBoundingClientRect(); var x = e.clientX - rect.left; var y = e.clientY; // - rect.top; if (!region) { arrow.style.display = ""; region = {}; } if (x < rect.width / 2) { if (region.side != -1) arrow.className = "diff-arrow diff-arrow-left"; region.side = -1; } else { if (region.side != 1) arrow.className = "diff-arrow diff-arrow-right"; region.side = 1; } var editor = region.side == 1 ? diffView.edit : diffView.orig; var other = editor == diffView.edit ? diffView.orig : diffView.edit; var renderer = editor.renderer; if (other.getReadOnly()) return hide(); var p = renderer.screenToTextCoordinates(x, y); var row = p.row; if (row == renderer.session.getLength() - 1) row++; var chunks = diffView.chunks; var i = findChunkIndex(chunks, row, region.side == -1); if (i == -1) i = 0; var ch = chunks[i] || chunks[chunks.length - 1]; var next = chunks[i + 1]; var side = region.side == -1 ? "orig" : "edit"; if (next && ch) { if (ch[side + "End"] + next[side + "Start"] < 2 * row) ch = next; } region.chunk = ch; if (!ch) return hide(); if (renderer.layerConfig.firstRow > ch[side + "End"]) return hide(); if (renderer.layerConfig.lastRow < ch[side + "Start"] - 1) return hide(); var screenPos = renderer.$cursorLayer.getPixelPosition({ row: ch[side + "Start"], column: 0 }, true).top; if (screenPos < renderer.layerConfig.offset) screenPos = renderer.layerConfig.offset; arrow.style.top = screenPos - renderer.layerConfig.offset + "px"; if (region.side == -1) { arrow.style.left = "2px"; arrow.style.right = ""; } else { arrow.style.left = ""; arrow.style.right = "2px"; } }.bind(this)); this.gutterEl.addEventListener("mouseout", function(e) { hide(); }); event.addMouseWheelListener(this.gutterEl, hide); function hide() { if (region) { region = null; arrow.style.display = "none"; } } }; /*** other ***/ this.destroy = function() { this.left.destroy(); this.right.destroy(); }; this.foldUnchanged = function() { this.edit.session.unfold(); this.orig.session.unfold(); var chunks = this.chunks; var sep = "---"; var prev = { editEnd: 0, origEnd: 0 }; for (var i = 0; i < chunks.length + 1; i++) { var ch = chunks[i] || { editStart: this.edit.session.getLength(), origStart: this.orig.session.getLength() }; var l = ch.editStart - prev.editEnd - 5; if (l > 2) { var s = prev.origEnd + 2; var f1 = this.orig.session.addFold(sep, new Range(s, 0, s + l, Number.MAX_VALUE)); s = prev.editEnd + 2; var f2 = this.edit.session.addFold(sep, new Range(s, 0, s + l, Number.MAX_VALUE)); f1.other = f2; f2.other = f1; } prev = ch; } }; this.gotoNext = function(dir) { var orig = false; var ace = orig ? this.orig : this.edit; var row = ace.selection.lead.row; var i = findChunkIndex(this.chunks, row, orig); var chunk = this.chunks[i + dir] || this.chunks[i]; var scrollTop = ace.session.getScrollTop(); if (chunk) { var line = Math.max(chunk.editStart, chunk.editEnd - 1); ace.selection.setRange(new Range(line, 0, line, 0)); } ace.renderer.scrollSelectionIntoView(ace.selection.lead, ace.selection.anchor, 0.5); ace.renderer.animateScrolling(scrollTop); }; this.transformPosition = function(pos, orig) { var chunkIndex = findChunkIndex(this.chunks, pos.row, orig); var chunk = this.chunks[chunkIndex]; var result = { row: pos.row, column: pos.column }; if (orig) { if (chunk.origEnd <= pos.row) { result.row = pos.row - chunk.origEnd + chunk.editEnd; } else { console.log("======================================"); var d = pos.row - chunk.origStart; var c = pos.column; var r1 = 0, c1 = 0, r2 = 0, c2 = 0; var inlineChanges = chunk.inlineChanges; for (var i = 0; i < inlineChanges.length; i++) { var diff = inlineChanges[i]; if (diff[1]) { if (diff[0] == 0) { r1 += diff[1]; r2 += diff[1]; if (r1 == d) c2 = c1 = diff[2]; } else if (diff[0] == 1) { r2 += diff[1]; if (r1 == d) c2 = diff[2]; } else if (diff[0] == -1) { r1 += diff[1]; if (r1 == d) c1 = diff[2]; } } else if (r1 == d) { if (diff[0] == 0) { c1 += diff[2]; c2 += diff[2]; } else if (diff[0] == 1) { c2 += diff[2]; } else if (diff[0] == -1) { c1 += diff[2]; } } console.log(diff + "", r1, c1, r2, c2, d, c); if (r1 > d || r1 == d && c1 >= c) { break; } } if (r1 > d) { r2 -= r1 - d; } if (c1 != c) { c2 -= c1 - c; } result.row = r2 + chunk.editStart; result.column = c2; } } return result; }; this.useChunk = function(chunk, toOrig) { var origRange = new Range(chunk.origStart, 0, chunk.origEnd, 0); var editRange = new Range(chunk.editStart, 0, chunk.editEnd, 0); var srcEditor = toOrig ? this.edit : this.orig; var destEditor = toOrig ? this.orig : this.edit; var destRange = toOrig ? origRange : editRange; var srcRange = toOrig ? editRange : origRange; var value = srcEditor.session.getTextRange(srcRange); // missing eol at the end of document if (srcRange.isEmpty() && !destRange.isEmpty()) { if (destRange.end.row == destEditor.session.getLength()) { destRange.start.row--; destRange.start.column = Number.MAX_VALUE; } } else if (destRange.isEmpty() && !srcRange.isEmpty()) { if (srcRange.end.row == srcEditor.session.getLength()) { value = "\n" + value; } } destRange.end = destEditor.session.replace(destRange, value); return destRange; }; this.transformRange = function(range, orig) { return Range.fromPoints( this.transformPosition(range.start, orig), this.transformPosition(range.end, orig) ); }; this.findChunkIndex = function(row, orig) { return findChunkIndex(this.chunks, row, orig); }; /*** patch ***/ this.createPatch = function(options) { var chunks = this.chunks; var editLines = this.edit.session.doc.getAllLines(); var origLines = this.orig.session.doc.getAllLines(); var path1 = options.path1 || options.path || "_"; var path2 = options.path2 || path1; var patch = [ "diff --git a/" + path1 + " b/" + path2, "--- a/" + path1, "+++ b/" + path2, ].join("\n"); if (!chunks.length) { chunks = [{ origStart: 0, origEnd: 0, editStart: 0, editEnd: 0 }]; } function header(s1, c1, s2, c2) { return "@@ -" + (c1 ? s1 + 1 : s1) + "," + c1 + " +" + (c2 ? s2 + 1 : s2) + "," + c2 + " @@"; } var context = options.context || 0; // changed newline at the end of file var editEOF = !editLines[editLines.length - 1]; var origEOF = !origLines[origLines.length - 1]; if (editEOF) editLines.pop(); if (origEOF) origLines.pop(); if (editEOF != origEOF) { chunks = chunks.slice(); var last = chunks.pop(); chunks.push(last = { origStart: Math.min(last.origStart, origLines.length - 1), origEnd: Math.min(last.origEnd, origLines.length), editStart: Math.min(last.editStart, editLines.length - 1), editEnd: Math.min(last.editEnd, editLines.length) }); } var hunk = ""; var start1 = 0; var start2 = 0; var end1 = 0; var end2 = 0; var length1 = 0; var length2 = 0; var mergeWithNext = false; for (var i = 0; i < chunks.length; i++) { var ch = chunks[i]; var s1 = ch.origStart; var e1 = ch.origEnd; var s2 = ch.editStart; var e2 = ch.editEnd; var next = chunks[i + 1]; start1 = Math.max(s1 - context, end1); start2 = Math.max(s2 - context, end2); end1 = Math.min(e1 + context, origLines.length); end2 = Math.min(e2 + context, editLines.length); mergeWithNext = false; if (next) { if (end1 >= next.origStart - context) { end1 = next.origStart; end2 = next.editStart; mergeWithNext = true; } } for (var j = start1; j < s1; j++) hunk += "\n " + origLines[j]; for (var j = s1; j < e1; j++) hunk += "\n-" + origLines[j]; if (ch == last && editEOF) hunk += "\n\\ No newline at end of file"; for (var j = s2; j < e2; j++) hunk += "\n+" + editLines[j]; if (ch == last && origEOF) hunk += "\n\\ No newline at end of file"; for (var j = e1; j < end1; j++) hunk += "\n " + origLines[j]; length1 += end1 - start1; length2 += end2 - start2; if (mergeWithNext) continue; patch += "\n" + header(end1 - length1, length1, end2 - length2, length2) + hunk; length2 = length1 = 0; hunk = ""; } if (!editEOF && !origEOF && end1 == origLines.length) { patch += "\n\\ No newline at end of file"; } return patch; }; this.setValueFromFullPatch = function(fullUniDiff) { var lines = fullUniDiff.split("\n"); var missingEOF = ""; var oldLines = []; var newLines = []; var i = 0; while (i < lines.length && !(/^@@/.test(lines[i]))) i++; while (++i < lines.length) { var tag = lines[i][0]; var line = lines[i].substr(1); if (tag === "+") { newLines.push(line); } else if (tag === "-") { oldLines.push(line); } else if (tag === " ") { newLines.push(line); oldLines.push(line); } else if (tag === "\\") { missingEOF = lines[i - 1][0]; } } if (missingEOF === "+") { oldLines.push(""); } else if (missingEOF === "-") { newLines.push(""); } else if (missingEOF === "") { newLines.push(""); oldLines.push(""); } this.orig.session.setValue(oldLines.join("\n")); this.edit.session.setValue(newLines.join("\n")); }; this.applyPatch = function(oldStr, uniDiff) { var lines = uniDiff.split("\n"); var hunks = []; var i = 0; var EOFChanged = 0; // Skip to the first change hunk while (i < lines.length && !(/^@@/.test(lines[i]))) { i++; } // Parse the unified diff for (; i < lines.length; i++) { var tag = lines[i][0]; var line = lines[i].substr(1); if (tag === "@") { var chunkHeader = /@@ -(\d+)(?:,(\d*))? \+(\d+)(?:,(\d*)) @@/.exec(line); hunks.unshift({ start: +chunkHeader[1], oldlength: +chunkHeader[2] || 1, removed: [], added: [] }); } else if (tag === "+") { hunks[0].added.push(line); } else if (tag === "-") { hunks[0].removed.push(line); } else if (tag === " ") { hunks[0].added.push(line); hunks[0].removed.push(line); } else if (tag === "\\") { if (lines[i - 1][0] === "+") EOFChanged = 1; else if (lines[i - 1][0] === "-") EOFChanged = -1; } } // Apply the diff to the input lines = oldStr.split("\n"); for (i = hunks.length - 1; i >= 0; i--) { var hunk = hunks[i]; // Sanity check the input string. Bail if we don't match. for (var j = 0; j < hunk.oldlength; j++) { if (lines[hunk.start - 1 + j] !== hunk.removed[j]) { return false; } } lines.splice.apply(lines, [hunk.start - 1, hunk.oldlength].concat(hunk.added)); } // Handle EOFNL insertion/removal if (EOFChanged == -1) { while (!lines[lines.length - 1]) { lines.pop(); } } else if (EOFChanged == 1) { lines.push(""); } return lines.join("\n"); }; /*** options ***/ config.defineOptions(this, "editor", { alignDiffs: { set: function(val) { if (val) this.align(); }, initialValue: false }, }); }).call(DiffView.prototype); var diff_linesToChars_ = function(text1, text2) { var lineHash = Object.create(null); var lineCount = 1; function diff_linesToCharsMunge_(lines) { var chars = ""; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (typeof lineHash[line] === "number") { chars += String.fromCharCode(lineHash[line]); } else { chars += String.fromCharCode(lineCount); lineHash[line] = lineCount++; } } return chars; } var chars1 = diff_linesToCharsMunge_(text1); var chars2 = diff_linesToCharsMunge_(text2); return { chars1: chars1, chars2: chars2 }; }; function findChunkIndex(chunks, row, orig) { if (orig) { for (var i = 0; i < chunks.length; i++) { var ch = chunks[i]; if (ch.origEnd < row) continue; if (ch.origStart > row) break; } } else { for (var i = 0; i < chunks.length; i++) { var ch = chunks[i]; if (ch.editEnd < row) continue; if (ch.editStart > row) break; } } return i - 1; } var Connector = function(diffView) { this.diffView = diffView; }; (function() { this.addConnector = function(diffView, origStart, origEnd, editStart, editEnd, type) { origStart = Math.ceil(origStart); origEnd = Math.ceil(origEnd); editStart = Math.ceil(editStart); editEnd = Math.ceil(editEnd); // p1 p2 // // p3 p4 var p1_x = -1; var p1_y = origStart + 0.5; var p2_x = diffView.gutterWidth + 1; var p2_y = editStart + 0.5; var p3_x = -1; var p3_y = (origEnd === origStart) ? origEnd + 0.5 : origEnd + 1.5; var p4_x = diffView.gutterWidth + 1; var p4_y = (editEnd === editStart) ? editEnd + 0.5 : editEnd + 1.5; var curve1 = this.getCurve(p1_x, p1_y, p2_x, p2_y); var curve2 = this.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(SVG_NS, "path"); el.setAttribute("d", d); el.setAttribute("class", "ace_diff-connector" + ( type == 1 ? " insert" : type == -1 ? " delete" : "" )); diffView.gutterSVG.appendChild(el); }; // generates a Bezier curve in SVG format this.getCurve = function(startX, startY, endX, endY) { var w = endX - startX; var halfWidth = startX + w * 0.5; // 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; }; this.createGutter = function() { var diffView = this.diffView; diffView.gutterWidth = diffView.gutterEl.clientWidth; var leftHeight = diffView.left.renderer.$size.height; var rightHeight = diffView.right.renderer.$size.height; var height = Math.max(leftHeight, rightHeight); diffView.gutterSVG = document.createElementNS(SVG_NS, "svg"); diffView.gutterSVG.setAttribute("width", diffView.gutterWidth); diffView.gutterSVG.setAttribute("height", height); diffView.gutterEl.appendChild(diffView.gutterSVG); }; this.clearGutter = function(diffView) { var gutterEl = diffView.gutterEl; gutterEl.removeChild(diffView.gutterSVG); this.createGutter(); }; this.decorate = function() { var diffView = this.diffView; if (diffView.$alignDiffs) return; this.clearGutter(diffView); var orig = diffView.left; var edit = diffView.right; var chunks = diffView.chunks; var c1 = orig.renderer.layerConfig; var c2 = edit.renderer.layerConfig; // var existing = for (var i = 0; i < chunks.length; i++) { var ch = chunks[i]; if (ch.origEnd < c1.firstRow && ch.editEnd < c2.firstRow) continue; if (ch.origStart > c1.lastRow && ch.editStart > c2.lastRow) break; var origStart = orig.renderer.$cursorLayer.getPixelPosition({ row: ch.origStart, column: 0 }, true).top - c1.offset; var origEnd = orig.renderer.$cursorLayer.getPixelPosition({ row: ch.origEnd, column: 0 }, true).top - c1.offset; var editStart = edit.renderer.$cursorLayer.getPixelPosition({ row: ch.editStart, column: 0 }, true).top - c2.offset; var editEnd = edit.renderer.$cursorLayer.getPixelPosition({ row: ch.editEnd, column: 0 }, true).top - c2.offset; if (i == chunks.length - 1) { if (ch.origEnd >= orig.session.getLength()) origEnd += c1.lineHeight; if (ch.origStart >= orig.session.getLength()) origStart += c1.lineHeight; if (ch.editEnd >= edit.session.getLength()) editEnd += c1.lineHeight; if (ch.editStart >= edit.session.getLength()) editStart += c1.lineHeight; } this.addConnector(diffView, origStart, origEnd, editStart, editEnd, ch.type); } }; }).call(Connector.prototype); var DiffHighlight = function(diffView, type) { this.diffView = diffView; this.type = type; }; (function() { this.MAX_RANGES = 500; this.update = function(html, markerLayer, session, config) { var start = config.firstRow; var end = config.lastRow; var diffView = this.diffView; var chunks = diffView.chunks; var isOrig = this.type == -1; var type = this.type; var index = findChunkIndex(chunks, start, isOrig); if (index == -1 && chunks.length && (isOrig ? chunks[0].origStart : chunks[0].editStart) > start) index = 0; var chunk = chunks[index]; while (chunk) { if (isOrig) { if (chunk.origStart > end && chunk.origStart != chunk.origEnd) return; var range = new Range(chunk.origStart, 0, chunk.origEnd - 1, 1); var l1 = chunk.origEnd - chunk.origStart; var l2 = chunk.editEnd - chunk.editStart; } else { if (chunk.editStart > end && chunk.editStart != chunk.editEnd) return; range = new Range(chunk.editStart, 0, chunk.editEnd - 1, 1); l1 = chunk.origEnd - chunk.origStart; l2 = chunk.editEnd - chunk.editStart; } var className = ""; if (!l1 && isOrig || !l2 && !isOrig) { className = range.start.row == session.getLength() ? "insertEnd" : "insertStart"; } className += chunk.type == -1 ? " delete" : chunk.type == 1 ? " insert" : ""; markerLayer.drawFullLineMarker(html, range.toScreenRange(session), "ace_diff " + className, config); var inlineChanges = chunk.inlineChanges; var row = range.start.row; var column = 0; for (var j = 0; j < inlineChanges.length; j++) { var diff = inlineChanges[j]; if (diff[0] == 0) { if (diff[1]) { row += diff[1]; column = diff[2]; } else { column += diff[2]; } } else { range.start.row = row; range.start.column = column; if (row > end) break; if (diff[0] == (isOrig ? -1 : 1)) { type = isOrig ? "delete" : "insert"; if (diff[1]) { row += diff[1]; column = diff[2]; } else { column += diff[2]; } } else { type = isOrig ? "insert" : "delete"; } if (row < start) continue; range.end.row = row; range.end.column = column; if (range.isEmpty()) type += " empty"; var screenRange = range.clipRows(start, end).toScreenRange(session); if (screenRange.isMultiLine()) { markerLayer.drawTextMarker(html, screenRange, "ace_diff inline " + type, config); } else { markerLayer.drawSingleLineMarker(html, screenRange, "ace_diff inline " + type, config); } } } chunk = chunks[++index]; } }; }).call(DiffHighlight.prototype); module.exports.DiffView = DiffView; });