2 if (typeof exports
== "object" && typeof module
== "object") // CommonJS
3 mod(require("../../lib/codemirror"));
4 else if (typeof define
== "function" && define
.amd
) // AMD
5 define(["../../lib/codemirror"], mod
);
6 else // Plain browser env
8 })(function(CodeMirror
) {
10 // declare global: diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL
12 var Pos
= CodeMirror
.Pos
;
13 var svgNS
= "http://www.w3.org/2000/svg";
15 function DiffView(mv
, type
) {
18 this.classes
= type
== "left"
19 ? {chunk
: "CodeMirror-merge-l-chunk",
20 start
: "CodeMirror-merge-l-chunk-start",
21 end
: "CodeMirror-merge-l-chunk-end",
22 insert
: "CodeMirror-merge-l-inserted",
23 del
: "CodeMirror-merge-l-deleted",
24 connect
: "CodeMirror-merge-l-connect"}
25 : {chunk
: "CodeMirror-merge-r-chunk",
26 start
: "CodeMirror-merge-r-chunk-start",
27 end
: "CodeMirror-merge-r-chunk-end",
28 insert
: "CodeMirror-merge-r-inserted",
29 del
: "CodeMirror-merge-r-deleted",
30 connect
: "CodeMirror-merge-r-connect"};
33 DiffView
.prototype = {
34 constructor: DiffView
,
35 init: function(pane
, orig
, options
) {
36 this.edit
= this.mv
.edit
;
37 this.orig
= CodeMirror(pane
, copyObj({value
: orig
, readOnly
: true}, copyObj(options
)));
39 this.diff
= getDiff(asString(orig
), asString(options
.value
));
40 this.diffOutOfDate
= false;
42 this.showDifferences
= options
.showDifferences
!== false;
43 this.forceUpdate
= registerUpdate(this);
44 setScrollLock(this, true, false);
47 setShowDifferences: function(val
) {
49 if (val
!= this.showDifferences
) {
50 this.showDifferences
= val
;
51 this.forceUpdate("full");
56 function registerUpdate(dv
) {
57 var edit
= {from: 0, to
: 0, marked
: []};
58 var orig
= {from: 0, to
: 0, marked
: []};
60 function update(mode
) {
62 if (dv
.svg
) clear(dv
.svg
);
63 clear(dv
.copyButtons
);
64 clearMarks(dv
.edit
, edit
.marked
, dv
.classes
);
65 clearMarks(dv
.orig
, orig
.marked
, dv
.classes
);
66 edit
.from = edit
.to
= orig
.from = orig
.to
= 0;
68 if (dv
.diffOutOfDate
) {
69 dv
.diff
= getDiff(dv
.orig
.getValue(), dv
.edit
.getValue());
70 dv
.diffOutOfDate
= false;
71 CodeMirror
.signal(dv
.edit
, "updateDiff", dv
.diff
);
73 if (dv
.showDifferences
) {
74 updateMarks(dv
.edit
, dv
.diff
, edit
, DIFF_INSERT
, dv
.classes
);
75 updateMarks(dv
.orig
, dv
.diff
, orig
, DIFF_DELETE
, dv
.classes
);
80 clearTimeout(debounceChange
);
81 debounceChange
= setTimeout(update
, slow
== true ? 250 : 100);
84 if (!dv
.diffOutOfDate
) {
85 dv
.diffOutOfDate
= true;
86 edit
.from = edit
.to
= orig
.from = orig
.to
= 0;
90 dv
.edit
.on("change", change
);
91 dv
.orig
.on("change", change
);
92 dv
.edit
.on("markerAdded", set);
93 dv
.edit
.on("markerCleared", set);
94 dv
.orig
.on("markerAdded", set);
95 dv
.orig
.on("markerCleared", set);
96 dv
.edit
.on("viewportChange", set);
97 dv
.orig
.on("viewportChange", set);
102 function registerScroll(dv
) {
103 dv
.edit
.on("scroll", function() {
104 syncScroll(dv
, DIFF_INSERT
) && drawConnectors(dv
);
106 dv
.orig
.on("scroll", function() {
107 syncScroll(dv
, DIFF_DELETE
) && drawConnectors(dv
);
111 function syncScroll(dv
, type
) {
112 // Change handler will do a refresh after a timeout when diff is out of date
113 if (dv
.diffOutOfDate
) return false;
114 if (!dv
.lockScroll
) return true;
115 var editor
, other
, now
= +new Date
;
116 if (type
== DIFF_INSERT
) { editor
= dv
.edit
; other
= dv
.orig
; }
117 else { editor
= dv
.orig
; other
= dv
.edit
; }
118 // Don't take action if the position of this editor was recently set
119 // (to prevent feedback loops)
120 if (editor
.state
.scrollSetBy
== dv
&& (editor
.state
.scrollSetAt
|| 0) + 50 > now
) return false;
122 var sInfo
= editor
.getScrollInfo(), halfScreen
= .5 * sInfo
.clientHeight
, midY
= sInfo
.top
+ halfScreen
;
123 var mid
= editor
.lineAtHeight(midY
, "local");
124 var around
= chunkBoundariesAround(dv
.diff
, mid
, type
== DIFF_INSERT
);
125 var off
= getOffsets(editor
, type
== DIFF_INSERT
? around
.edit
: around
.orig
);
126 var offOther
= getOffsets(other
, type
== DIFF_INSERT
? around
.orig
: around
.edit
);
127 var ratio
= (midY
- off
.top
) / (off
.bot
- off
.top
);
128 var targetPos
= (offOther
.top
- halfScreen
) + ratio
* (offOther
.bot
- offOther
.top
);
131 // Some careful tweaking to make sure no space is left out of view
132 // when scrolling to top or bottom.
133 if (targetPos
> sInfo
.top
&& (mix
= sInfo
.top
/ halfScreen
) < 1) {
134 targetPos
= targetPos
* mix
+ sInfo
.top
* (1 - mix
);
135 } else if ((botDist
= sInfo
.height
- sInfo
.clientHeight
- sInfo
.top
) < halfScreen
) {
136 var otherInfo
= other
.getScrollInfo();
137 var botDistOther
= otherInfo
.height
- otherInfo
.clientHeight
- targetPos
;
138 if (botDistOther
> botDist
&& (mix
= botDist
/ halfScreen
) < 1)
139 targetPos
= targetPos
* mix
+ (otherInfo
.height
- otherInfo
.clientHeight
- botDist
) * (1 - mix
);
142 other
.scrollTo(sInfo
.left
, targetPos
);
143 other
.state
.scrollSetAt
= now
;
144 other
.state
.scrollSetBy
= dv
;
148 function getOffsets(editor
, around
) {
149 var bot
= around
.after
;
150 if (bot
== null) bot
= editor
.lastLine() + 1;
151 return {top
: editor
.heightAtLine(around
.before
|| 0, "local"),
152 bot
: editor
.heightAtLine(bot
, "local")};
155 function setScrollLock(dv
, val
, action
) {
157 if (val
&& action
!= false) syncScroll(dv
, DIFF_INSERT
) && drawConnectors(dv
);
158 dv
.lockButton
.innerHTML
= val
? "\u21db\u21da" : "\u21db \u21da";
161 // Updating the marks for editor content
163 function clearMarks(editor
, arr
, classes
) {
164 for (var i
= 0; i
< arr
.length
; ++i
) {
166 if (mark
instanceof CodeMirror
.TextMarker
) {
169 editor
.removeLineClass(mark
, "background", classes
.chunk
);
170 editor
.removeLineClass(mark
, "background", classes
.start
);
171 editor
.removeLineClass(mark
, "background", classes
.end
);
177 // FIXME maybe add a margin around viewport to prevent too many updates
178 function updateMarks(editor
, diff
, state
, type
, classes
) {
179 var vp
= editor
.getViewport();
180 editor
.operation(function() {
181 if (state
.from == state
.to
|| vp
.from - state
.to
> 20 || state
.from - vp
.to
> 20) {
182 clearMarks(editor
, state
.marked
, classes
);
183 markChanges(editor
, diff
, type
, state
.marked
, vp
.from, vp
.to
, classes
);
184 state
.from = vp
.from; state
.to
= vp
.to
;
186 if (vp
.from < state
.from) {
187 markChanges(editor
, diff
, type
, state
.marked
, vp
.from, state
.from, classes
);
188 state
.from = vp
.from;
190 if (vp
.to
> state
.to
) {
191 markChanges(editor
, diff
, type
, state
.marked
, state
.to
, vp
.to
, classes
);
198 function markChanges(editor
, diff
, type
, marks
, from, to
, classes
) {
200 var top
= Pos(from, 0), bot
= editor
.clipPos(Pos(to
- 1));
201 var cls
= type
== DIFF_DELETE
? classes
.del
: classes
.insert
;
202 function markChunk(start
, end
) {
203 var bfrom
= Math
.max(from, start
), bto
= Math
.min(to
, end
);
204 for (var i
= bfrom
; i
< bto
; ++i
) {
205 var line
= editor
.addLineClass(i
, "background", classes
.chunk
);
206 if (i
== start
) editor
.addLineClass(line
, "background", classes
.start
);
207 if (i
== end
- 1) editor
.addLineClass(line
, "background", classes
.end
);
210 // When the chunk is empty, make sure a horizontal line shows up
211 if (start
== end
&& bfrom
== end
&& bto
== end
) {
213 marks
.push(editor
.addLineClass(bfrom
- 1, "background", classes
.end
));
215 marks
.push(editor
.addLineClass(bfrom
, "background", classes
.start
));
220 for (var i
= 0; i
< diff
.length
; ++i
) {
221 var part
= diff
[i
], tp
= part
[0], str
= part
[1];
222 if (tp
== DIFF_EQUAL
) {
223 var cleanFrom
= pos
.line
+ (startOfLineClean(diff
, i
) ? 0 : 1);
225 var cleanTo
= pos
.line
+ (endOfLineClean(diff
, i
) ? 1 : 0);
226 if (cleanTo
> cleanFrom
) {
227 if (i
) markChunk(chunkStart
, cleanFrom
);
228 chunkStart
= cleanTo
;
232 var end
= moveOver(pos
, str
, true);
233 var a
= posMax(top
, pos
), b
= posMin(bot
, end
);
235 marks
.push(editor
.markText(a
, b
, {className
: cls
}));
240 if (chunkStart
<= pos
.line
) markChunk(chunkStart
, pos
.line
+ 1);
243 // Updating the gap between editor and original
245 function drawConnectors(dv
) {
246 if (!dv
.showDifferences
) return;
250 var w
= dv
.gap
.offsetWidth
;
251 attrs(dv
.svg
, "width", w
, "height", dv
.gap
.offsetHeight
);
253 clear(dv
.copyButtons
);
255 var flip
= dv
.type
== "left";
256 var vpEdit
= dv
.edit
.getViewport(), vpOrig
= dv
.orig
.getViewport();
257 var sTopEdit
= dv
.edit
.getScrollInfo().top
, sTopOrig
= dv
.orig
.getScrollInfo().top
;
258 iterateChunks(dv
.diff
, function(topOrig
, botOrig
, topEdit
, botEdit
) {
259 if (topEdit
> vpEdit
.to
|| botEdit
< vpEdit
.from ||
260 topOrig
> vpOrig
.to
|| botOrig
< vpOrig
.from)
262 var topLpx
= dv
.orig
.heightAtLine(topOrig
, "local") - sTopOrig
, top
= topLpx
;
264 var topRpx
= dv
.edit
.heightAtLine(topEdit
, "local") - sTopEdit
;
265 if (flip
) { var tmp
= topLpx
; topLpx
= topRpx
; topRpx
= tmp
; }
266 var botLpx
= dv
.orig
.heightAtLine(botOrig
, "local") - sTopOrig
;
267 var botRpx
= dv
.edit
.heightAtLine(botEdit
, "local") - sTopEdit
;
268 if (flip
) { var tmp
= botLpx
; botLpx
= botRpx
; botRpx
= tmp
; }
269 var curveTop
= " C " + w
/2 + " " + topRpx + " " + w/2 + " " + topLpx
+ " " + (w
+ 2) + " " + topLpx
;
270 var curveBot
= " C " + w
/2 + " " + botLpx + " " + w/2 + " " + botRpx
+ " -1 " + botRpx
;
271 attrs(dv
.svg
.appendChild(document
.createElementNS(svgNS
, "path")),
272 "d", "M -1 " + topRpx
+ curveTop
+ " L " + (w
+ 2) + " " + botLpx
+ curveBot
+ " z",
273 "class", dv
.classes
.connect
);
275 var copy
= dv
.copyButtons
.appendChild(elt("div", dv
.type
== "left" ? "\u21dd" : "\u21dc",
276 "CodeMirror-merge-copy"));
277 copy
.title
= "Revert chunk";
278 copy
.chunk
= {topEdit
: topEdit
, botEdit
: botEdit
, topOrig
: topOrig
, botOrig
: botOrig
};
279 copy
.style
.top
= top
+ "px";
283 function copyChunk(dv
, chunk
) {
284 if (dv
.diffOutOfDate
) return;
285 dv
.edit
.replaceRange(dv
.orig
.getRange(Pos(chunk
.topOrig
, 0), Pos(chunk
.botOrig
, 0)),
286 Pos(chunk
.topEdit
, 0), Pos(chunk
.botEdit
, 0));
289 // Merge view, containing 0, 1, or 2 diff views.
291 var MergeView
= CodeMirror
.MergeView = function(node
, options
) {
292 if (!(this instanceof MergeView
)) return new MergeView(node
, options
);
294 var origLeft
= options
.origLeft
, origRight
= options
.origRight
== null ? options
.orig
: options
.origRight
;
295 var hasLeft
= origLeft
!= null, hasRight
= origRight
!= null;
296 var panes
= 1 + (hasLeft
? 1 : 0) + (hasRight
? 1 : 0);
297 var wrap
= [], left
= this.left
= null, right
= this.right
= null;
300 left
= this.left
= new DiffView(this, "left");
301 var leftPane
= elt("div", null, "CodeMirror-merge-pane");
303 wrap
.push(buildGap(left
));
306 var editPane
= elt("div", null, "CodeMirror-merge-pane");
310 right
= this.right
= new DiffView(this, "right");
311 wrap
.push(buildGap(right
));
312 var rightPane
= elt("div", null, "CodeMirror-merge-pane");
313 wrap
.push(rightPane
);
316 (hasRight
? rightPane
: editPane
).className
+= " CodeMirror-merge-pane-rightmost";
318 wrap
.push(elt("div", null, null, "height: 0; clear: both;"));
319 var wrapElt
= this.wrap
= node
.appendChild(elt("div", wrap
, "CodeMirror-merge CodeMirror-merge-" + panes
+ "pane"));
320 this.edit
= CodeMirror(editPane
, copyObj(options
));
322 if (left
) left
.init(leftPane
, origLeft
, options
);
323 if (right
) right
.init(rightPane
, origRight
, options
);
325 var onResize = function() {
326 if (left
) drawConnectors(left
);
327 if (right
) drawConnectors(right
);
329 CodeMirror
.on(window
, "resize", onResize
);
330 var resizeInterval
= setInterval(function() {
331 for (var p
= wrapElt
.parentNode
; p
&& p
!= document
.body
; p
= p
.parentNode
) {}
332 if (!p
) { clearInterval(resizeInterval
); CodeMirror
.off(window
, "resize", onResize
); }
336 function buildGap(dv
) {
337 var lock
= dv
.lockButton
= elt("div", null, "CodeMirror-merge-scrolllock");
338 lock
.title
= "Toggle locked scrolling";
339 var lockWrap
= elt("div", [lock
], "CodeMirror-merge-scrolllock-wrap");
340 CodeMirror
.on(lock
, "click", function() { setScrollLock(dv
, !dv
.lockScroll
); });
341 dv
.copyButtons
= elt("div", null, "CodeMirror-merge-copybuttons-" + dv
.type
);
342 CodeMirror
.on(dv
.copyButtons
, "click", function(e
) {
343 var node
= e
.target
|| e
.srcElement
;
344 if (node
.chunk
) copyChunk(dv
, node
.chunk
);
346 var gapElts
= [dv
.copyButtons
, lockWrap
];
347 var svg
= document
.createElementNS
&& document
.createElementNS(svgNS
, "svg");
348 if (svg
&& !svg
.createSVGRect
) svg
= null;
350 if (svg
) gapElts
.push(svg
);
352 return dv
.gap
= elt("div", gapElts
, "CodeMirror-merge-gap");
355 MergeView
.prototype = {
356 constuctor
: MergeView
,
357 editor: function() { return this.edit
; },
358 rightOriginal: function() { return this.right
&& this.right
.orig
; },
359 leftOriginal: function() { return this.left
&& this.left
.orig
; },
360 setShowDifferences: function(val
) {
361 if (this.right
) this.right
.setShowDifferences(val
);
362 if (this.left
) this.left
.setShowDifferences(val
);
364 rightChunks: function() {
365 return this.right
&& getChunks(this.right
.diff
);
367 leftChunks: function() {
368 return this.left
&& getChunks(this.left
.diff
);
372 function asString(obj
) {
373 if (typeof obj
== "string") return obj
;
374 else return obj
.getValue();
377 // Operations on diffs
379 var dmp
= new diff_match_patch();
380 function getDiff(a
, b
) {
381 var diff
= dmp
.diff_main(a
, b
);
382 dmp
.diff_cleanupSemantic(diff
);
383 // The library sometimes leaves in empty parts, which confuse the algorithm
384 for (var i
= 0; i
< diff
.length
; ++i
) {
388 } else if (i
&& diff
[i
- 1][0] == part
[0]) {
390 diff
[i
][1] += part
[1];
396 function iterateChunks(diff
, f
) {
397 var startEdit
= 0, startOrig
= 0;
398 var edit
= Pos(0, 0), orig
= Pos(0, 0);
399 for (var i
= 0; i
< diff
.length
; ++i
) {
400 var part
= diff
[i
], tp
= part
[0];
401 if (tp
== DIFF_EQUAL
) {
402 var startOff
= startOfLineClean(diff
, i
) ? 0 : 1;
403 var cleanFromEdit
= edit
.line
+ startOff
, cleanFromOrig
= orig
.line
+ startOff
;
404 moveOver(edit
, part
[1], null, orig
);
405 var endOff
= endOfLineClean(diff
, i
) ? 1 : 0;
406 var cleanToEdit
= edit
.line
+ endOff
, cleanToOrig
= orig
.line
+ endOff
;
407 if (cleanToEdit
> cleanFromEdit
) {
408 if (i
) f(startOrig
, cleanFromOrig
, startEdit
, cleanFromEdit
);
409 startEdit
= cleanToEdit
; startOrig
= cleanToOrig
;
412 moveOver(tp
== DIFF_INSERT
? edit
: orig
, part
[1]);
415 if (startEdit
<= edit
.line
|| startOrig
<= orig
.line
)
416 f(startOrig
, orig
.line
+ 1, startEdit
, edit
.line
+ 1);
419 function getChunks(diff
) {
421 iterateChunks(diff
, function(topOrig
, botOrig
, topEdit
, botEdit
) {
422 collect
.push({origFrom
: topOrig
, origTo
: botOrig
,
423 editFrom
: topEdit
, editTo
: botEdit
});
428 function endOfLineClean(diff
, i
) {
429 if (i
== diff
.length
- 1) return true;
430 var next
= diff
[i
+ 1][1];
431 if (next
.length
== 1 || next
.charCodeAt(0) != 10) return false;
432 if (i
== diff
.length
- 2) return true;
433 next
= diff
[i
+ 2][1];
434 return next
.length
> 1 && next
.charCodeAt(0) == 10;
437 function startOfLineClean(diff
, i
) {
438 if (i
== 0) return true;
439 var last
= diff
[i
- 1][1];
440 if (last
.charCodeAt(last
.length
- 1) != 10) return false;
441 if (i
== 1) return true;
442 last
= diff
[i
- 2][1];
443 return last
.charCodeAt(last
.length
- 1) == 10;
446 function chunkBoundariesAround(diff
, n
, nInEdit
) {
447 var beforeE
, afterE
, beforeO
, afterO
;
448 iterateChunks(diff
, function(fromOrig
, toOrig
, fromEdit
, toEdit
) {
449 var fromLocal
= nInEdit
? fromEdit
: fromOrig
;
450 var toLocal
= nInEdit
? toEdit
: toOrig
;
451 if (afterE
== null) {
452 if (fromLocal
> n
) { afterE
= fromEdit
; afterO
= fromOrig
; }
453 else if (toLocal
> n
) { afterE
= toEdit
; afterO
= toOrig
; }
455 if (toLocal
<= n
) { beforeE
= toEdit
; beforeO
= toOrig
; }
456 else if (fromLocal
<= n
) { beforeE
= fromEdit
; beforeO
= fromOrig
; }
458 return {edit
: {before
: beforeE
, after
: afterE
}, orig
: {before
: beforeO
, after
: afterO
}};
463 function elt(tag
, content
, className
, style
) {
464 var e
= document
.createElement(tag
);
465 if (className
) e
.className
= className
;
466 if (style
) e
.style
.cssText
= style
;
467 if (typeof content
== "string") e
.appendChild(document
.createTextNode(content
));
468 else if (content
) for (var i
= 0; i
< content
.length
; ++i
) e
.appendChild(content
[i
]);
472 function clear(node
) {
473 for (var count
= node
.childNodes
.length
; count
> 0; --count
)
474 node
.removeChild(node
.firstChild
);
477 function attrs(elt
) {
478 for (var i
= 1; i
< arguments
.length
; i
+= 2)
479 elt
.setAttribute(arguments
[i
], arguments
[i
+1]);
482 function copyObj(obj
, target
) {
483 if (!target
) target
= {};
484 for (var prop
in obj
) if (obj
.hasOwnProperty(prop
)) target
[prop
] = obj
[prop
];
488 function moveOver(pos
, str
, copy
, other
) {
489 var out
= copy
? Pos(pos
.line
, pos
.ch
) : pos
, at
= 0;
491 var nl
= str
.indexOf("\n", at
);
494 if (other
) ++other
.line
;
497 out
.ch
= (at
? 0 : out
.ch
) + (str
.length
- at
);
498 if (other
) other
.ch
= (at
? 0 : other
.ch
) + (str
.length
- at
);
502 function posMin(a
, b
) { return (a
.line
- b
.line
|| a
.ch
- b
.ch
) < 0 ? a
: b
; }
503 function posMax(a
, b
) { return (a
.line
- b
.line
|| a
.ch
- b
.ch
) > 0 ? a
: b
; }
504 function posEq(a
, b
) { return a
.line
== b
.line
&& a
.ch
== b
.ch
; }