changeset 4125:aa3b55946089 rhodecode-2.2.5-gpl

Migrate to Mergely 3.3.4. RhodeCode 2.2.5 distributed Mergely 3.3.4 with some of the changes that Mergely 3.3.3 in RhodeCode 1.7.2 also had. That do however not seem to be changes we want for Kallithea this way and we take the 3.3.4 files as they are. I've also included the Mergely license file, as downloaded from: http://www.mergely.com/license.php That LICENSE file is kept in HTML just as it was downloaded from their website. While it's a bit annoying to keep the license file in HTML, this is the way it came from upstream so we'll leave it that way. Since the Javascript code is used with other GPLv3 Javascript, we are using the GPL option of Mergely's tri-license. Finally, note that previously, this was incorrectly called "mergerly", so the opportunity is taken here to correct the name. That required changes to diff_2way.html. As commands:: $ wget -N --output-document LICENSE-MERGELY.html http://www.mergely.com/license.php $ hg add LICENSE-MERGELY.html $ hg mv rhodecode/public/css/mergerly.css rhodecode/public/css/mergely.css $ hg mv rhodecode/public/js/mergerly.js rhodecode/public/js/mergely.js $ sed -i 's,mergerly\.,mergely,g' rhodecode/templates/files/diff_2way.html $ ( cd /tmp; \ wget -N http://www.mergely.com/releases/mergely-3.3.4.zip; \ unzip mergely-3.3.4.zip ) $ sha256sum /tmp/mergely-3.3.4.zip 87415d30494bbe829c248881aa7cdc0303f7e70b458a5f687615564d4498cc82 mergely-3.3.4.zip $ cp /tmp/mergely-3.3.4/lib/mergely.js rhodecode/public/js/mergely.js $ cp /tmp/mergely-3.3.4/lib/mergely.css rhodecode/public/css/mergely.css $ sed -i -e '/^ \* Version/a\ *\n * NOTE by bkuhn@sfconservancy.org for Kallithea:\n * Mergely license appears at http://www.mergely.com/license.php and in LICENSE-MERGELY.html' rhodecode/public/js/mergely.js rhodecode/public/css/mergely.css
author "Bradley M. Kuhn" <bkuhn@ebb.org>
date Mon, 26 May 2014 20:21:00 -0400
parents 34a887bd21ae
children 158ef33603b8
files LICENSE-MERGELY.html LICENSE.md rhodecode/public/css/mergely.css rhodecode/public/css/mergerly.css rhodecode/public/js/mergely.js rhodecode/public/js/mergerly.js rhodecode/templates/files/diff_2way.html
diffstat 7 files changed, 1684 insertions(+), 1503 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE-MERGELY.html	Mon May 26 20:21:00 2014 -0400
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html lang="en">
+<!--[if IE]>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+<![endif]-->
+<head>
+	<meta charset="utf-8" /><title>Mergely License</title>
+	<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
+	<meta name="description" content="Mergely license requirements for open source software and commercial software" />
+	<meta name="keywords" content="diff,merge,compare,compare documents,js diff,javascript diff,comparison,online diff,difference,file,text,unix,patch,algorithm,saas,longest common subsequence,diff online" />
+	<meta name="author" content="Jamie Peabody" />
+	<meta name="author" content="Jamie Peabody" />
+	<link rel="shortcut icon" href="http://www.mergely.com/favicon.ico" />
+    <link href='http://fonts.googleapis.com/css?family=Noto+Sans:400,700' rel='stylesheet' type='text/css' />
+    <link href='fonts/berlin-sans-fb-demi.css' rel='stylesheet' type='text/css' />
+    <link href='style/mergely.css' rel='stylesheet' type='text/css' />
+    <link href='/Mergely/lib/mergely.css' rel='stylesheet' type='text/css' />
+	<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
+	<script type="text/javascript">
+		var _gaq = _gaq || [];
+		_gaq.push(['_setAccount', 'UA-85576-5']);
+		_gaq.push(['_trackPageview']);
+		(function() {
+			var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+			ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+			var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+		})();
+	</script>
+</head>
+<body>
+    <div id="page">
+        <div id="content">
+            <div id="header">
+                <h1><span>Mergely License - Closed Distribution License</span></h1>
+                <div id="options">
+                    <a href="/editor" class="button">Online Diff</a>
+                    <a href="/download" class="button">Download</a>
+                </div>
+                <nav>
+                    <ul>
+                        <li><a href="/">Home</a></li>                        <li><a href="/doc">Documentation</a></li>                        <li><a href="/about">About Mergely</a></li>                        <li><a href="/license">License</a></li>                        <li><a href="#footer">Contact</a></li>                    </ul>
+                </nav>
+            </div>
+
+            <div id="main">
+				<h1>Mergely License</h1>
+				<p>
+					All Mergely code is Copyright 2014 by Jamie Peabody.
+					Mergely is distributed under the 
+					<a href="http://www.gnu.org/licenses/gpl.html">GPL</a>, 
+					<a href="http://www.gnu.org/licenses/lgpl.html">LGPL</a>
+					and 
+					<a href="http://www.mozilla.org/MPL/MPL-1.1.html">MPL</a> open source licenses. 
+					This triple <b>copyleft</b> licensing model avoids incompatibility with other open 
+					source licenses. These open source licenses are specially indicated for:
+					<ul>
+						<li>Integrating Mergely into Open Source software;</li>
+						<li>Personal and educational use of Mergely;</li>
+						<li>
+							Integrating Mergely in commercial software, taking care of satisfying 
+							the Open Source licenses terms, while not able or interested on supporting 
+							Mergely and its development.
+						</li>
+					</ul>
+				</p>
+				<h2>Mergely Commercial License - Closed Distribution License - CDL</h2>
+				<p>
+					You may contact <a href="mailto:jamie.peabody@gmail.com">Jamie Peabody</a> to enquire about
+					obtaining a CDL license.
+				</p>
+				<p>
+					This license offers a very flexible way to integrate Mergely in your commercial 
+					application. These are the main advantages it offers over an Open Source license:
+				</p>
+				<p>
+					Modifications and enhancements do not need to be released under an Open 
+					Source license; There is no need to distribute any Open Source license terms 
+					along with your product and no reference to it have to be done; You do not have
+					to mention any reference to Mergely in your product; Mergely source code does not
+					have to be distributed with your product; You can remove any file from Mergely 
+					when integrating it with your product.
+				</p>
+				<p>
+					The CDL is a lifetime license valid for all previous releases of Mergely published 
+                    prior to the year of purchase, and any releases in the following year. Please select 
+                    the license option that best fit your needs above. It includes 1 year of 
+                    <b>personal e-mail support</b>.
+				</p>
+				<h2>Third party codes</h2>
+				<p>
+					Mergely utilizes <b>CodeMirror</b>, a third-party library released under an 
+					<a href="http://en.wikipedia.org/wiki/MIT_License">MIT</a>
+					license.  Also used is <b>jQuery</b> and is released under the 
+					<a href="http://en.wikipedia.org/wiki/MIT_License">MIT</a> or 
+					<a href="http://www.gnu.org/licenses/gpl.html">GPL</a> Version 2 license.
+				</p>
+            </div>
+            
+            <div id="footer">
+                <a href="/download" class="download">Download</a>
+                <ul>
+                    <li id="google-plus"><a target="_blank" href="http://groups.google.com/group/mergely">http://groups.google.com/group/mergely</a></li>
+                    <li id="github"><a target="_blank" href="https://github.com/wickedest/Mergely">https://github.com/wickedest/Mergely</a></li>
+                    <li id="email"><a target="_blank" href="mailto:jamie.peabody@gmail.com">jamie.peabody@gmail.com</a></li>
+                </ul>
+            </div>
+        </div>
+        <div id="copyright">By <b>Jamie Peabody</b></div>
+    </div>
+</body>
+</html>
--- a/LICENSE.md	Mon May 26 17:56:53 2014 -0400
+++ b/LICENSE.md	Mon May 26 20:21:00 2014 -0400
@@ -55,4 +55,16 @@
 
 
 
+Mergely
+-------
+
+Kallithea incorporates some code from the Javascript system called
+[Mergely](http://http://www.mergely.com/).
+[Mergely's license](http://www.mergely.com/license.php), a
+[copy of which is included in this repository](LICENSE-MERGELY.html),
+is (GPL|LGPL|MPL).  Kallithea as GPLv3'd project chooses the GPL arm of that
+tri-license.
+
+
+
 EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/public/css/mergely.css	Mon May 26 20:21:00 2014 -0400
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2013 by Jamie Peabody, http://www.mergely.com
+ * All rights reserved.
+ * Version: 3.3.4 2013-11-02
+ *
+ * NOTE by bkuhn@sfconservancy.org for Kallithea:
+ * Mergely license appears at http://www.mergely.com/license.php and in LICENSE-MERGELY.html
+ */
+
+/* required */
+.mergely-column textarea { width: 80px; height: 200px; }
+.mergely-column { float: left; }
+.mergely-margin { float: left; }
+.mergely-canvas { float: left; width: 28px; }
+
+/* resizeable */
+.mergely-resizer { width: 100%; height: 100%; }
+
+/* style configuration */
+.mergely-column { border: 1px solid #ccc; }
+.mergely-active { border: 1px solid #a3d1ff; }
+
+.mergely.a.rhs.start { border-top: 1px solid #a3d1ff; }
+.mergely.a.lhs.start.end,
+.mergely.a.rhs.end { border-bottom: 1px solid #a3d1ff; }
+.mergely.a.rhs { background-color: #ddeeff; }
+.mergely.a.lhs.start.end.first { border-bottom: 0; border-top: 1px solid #a3d1ff; }
+
+.mergely.d.lhs { background-color: #edc0c0; }
+.mergely.d.lhs.end,
+.mergely.d.rhs.start.end { border-bottom: 1px solid #ff7f7f; }
+.mergely.d.rhs.start.end.first { border-bottom: 0; border-top: 1px solid #ff7f7f; }
+.mergely.d.lhs.start { border-top: 1px solid #ff7f7f; }
+
+.mergely.c.lhs,
+.mergely.c.rhs { background-color: #fafafa; }
+.mergely.c.lhs.start,
+.mergely.c.rhs.start { border-top: 1px solid #a3a3a3; }
+.mergely.c.lhs.end,
+.mergely.c.rhs.end { border-bottom: 1px solid #a3a3a3; }
+
+.mergely.ch.a.rhs { background-color: #ddeeff; }
+.mergely.ch.d.lhs { background-color: #edc0c0; text-decoration: line-through; color: #888; }
--- a/rhodecode/public/css/mergerly.css	Mon May 26 17:56:53 2014 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-
-/* required */
-.mergely-column textarea { width: 80px; height: 200px; }
-.mergely-column { float: left; }
-.mergely-margin { float: left; }
-.mergely-canvas { float: left; width: 28px; }
-
-/* resizeable */
-.mergely-resizer { width: 100%; height: 100%; }
-
-/* style configuration */
-.mergely-column { border: 1px solid #ccc; }
-.mergely-active { border: 1px solid #a3d1ff; }
-
-.mergely.a.rhs.start { border-top: 1px solid #ddffdd; }
-.mergely.a.lhs.start.end,
-.mergely.a.rhs.end { border-bottom: 1px solid #ddffdd; }
-.mergely.a.rhs { background-color: #ddffdd; }
-.mergely.a.lhs.start.end.first { border-bottom: 0; border-top: 1px solid #ddffdd; }
-
-.mergely.d.lhs { background-color: #edc0c0; }
-.mergely.d.lhs.end,
-.mergely.d.rhs.start.end { border-bottom: 1px solid #ffdddd; }
-.mergely.d.rhs.start.end.first { border-bottom: 0; border-top: 1px solid #ffdddd; }
-.mergely.d.lhs.start { border-top: 1px solid #ffdddd; }
-
-.mergely.c.lhs,
-.mergely.c.rhs { background-color: #fafafa; }
-.mergely.c.lhs.start,
-.mergely.c.rhs.start { border-top: 1px solid #a3a3a3; }
-.mergely.c.lhs.end,
-.mergely.c.rhs.end { border-bottom: 1px solid #a3a3a3; }
-
-.mergely.ch.a.rhs { background-color: #ddffdd; }
-.mergely.ch.d.lhs { background-color: #ffdddd; }
-
-
-.mergely-margin #compare-lhs-margin,
-.mergely-margin #compare-rhs-margin {
-    cursor: pointer
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/public/js/mergely.js	Mon May 26 20:21:00 2014 -0400
@@ -0,0 +1,1516 @@
+/**
+ * Copyright (c) 2013 by Jamie Peabody, http://www.mergely.com
+ * All rights reserved.
+ * Version: 3.3.4 2013-11-02
+ *
+ * NOTE by bkuhn@sfconservancy.org for Kallithea:
+ * Mergely license appears at http://www.mergely.com/license.php and in LICENSE-MERGELY.html
+ */
+Mgly = {};
+
+Mgly.Timer = function(){
+	var self = this;
+	self.start = function() { self.t0 = new Date().getTime(); }
+	self.stop = function() {
+		var t1 = new Date().getTime();
+		var d = t1 - self.t0; 
+		self.t0 = t1;
+		return d;
+	}
+	self.start();
+}
+
+Mgly.ChangeExpression = new RegExp(/(\d+(?:,\d+)?)([acd])(\d+(?:,\d+)?)/);
+
+Mgly.DiffParser = function(diff) {
+	var changes = [];
+	var change_id = 0;
+	// parse diff
+	var diff_lines = diff.split(/\n/);
+	for (var i = 0; i < diff_lines.length; ++i) {
+		if (diff_lines[i].length == 0) continue;
+		var change = {};
+		var test = Mgly.ChangeExpression.exec(diff_lines[i]);
+		if (test == null) continue;
+		// lines are zero-based
+		var fr = test[1].split(',');
+		change['lhs-line-from'] = fr[0] - 1;
+		if (fr.length == 1) change['lhs-line-to'] = fr[0] - 1;
+		else change['lhs-line-to'] = fr[1] - 1;
+		var to = test[3].split(',');
+		change['rhs-line-from'] = to[0] - 1;
+		if (to.length == 1) change['rhs-line-to'] = to[0] - 1;
+		else change['rhs-line-to'] = to[1] - 1;
+		change['op'] = test[2];
+		changes[change_id++] = change;
+	}
+	return changes;
+}
+
+Mgly.sizeOf = function(obj) {
+	var size = 0, key;
+	for (key in obj) {
+		if (obj.hasOwnProperty(key)) size++;
+	}
+	return size;
+}
+
+Mgly.LCS = function(x, y) {
+	this.x = x.replace(/[ ]{1}/g, '\n');
+	this.y = y.replace(/[ ]{1}/g, '\n');
+}
+jQuery.extend(Mgly.LCS.prototype, {
+	clear: function() { this.ready = 0; },
+	diff: function(added, removed) {
+		var d = new Mgly.diff(this.x, this.y, {ignorews: false});
+		var changes = Mgly.DiffParser(d.normal_form());
+		var li = 0, lj = 0;
+		for (var i = 0; i < changes.length; ++i) {
+			var change = changes[i];
+			if (change.op != 'a') {
+				// find the starting index of the line
+				li = d.getLines('lhs').slice(0, change['lhs-line-from']).join(' ').length;
+				// get the index of the the span of the change
+				lj = change['lhs-line-to'] + 1;
+				// get the changed text
+				var lchange = d.getLines('lhs').slice(change['lhs-line-from'], lj).join(' ');
+				if (change.op == 'd') lchange += ' ';// include the leading space
+				else if (li > 0 && change.op == 'c') li += 1; // ignore leading space if not first word
+				// output the changed index and text
+				removed(li, li + lchange.length);
+			}
+			if (change.op != 'd') {
+				// find the starting index of the line
+				li = d.getLines('lhs').slice(0, change['rhs-line-from']).join(' ').length;
+				// get the index of the the span of the change
+				lj = change['rhs-line-to'] + 1;
+				// get the changed text
+				var rchange = d.getLines('lhs').slice(change['rhs-line-from'], lj).join(' ');
+				if (change.op == 'a') rchange += ' ';// include the leading space
+				else if (li > 0 && change.op == 'c') li += 1; // ignore leading space if not first word
+				// output the changed index and text
+				added(li, li + rchange.length);
+			}
+		}
+	}
+});
+
+Mgly.CodeifyText = function(settings) {
+    this._max_code = 0;
+    this._diff_codes = {};
+	this.ctxs = {};
+	this.options = {ignorews: false};
+	jQuery.extend(this, settings);
+	this.lhs = settings.lhs.split('\n');
+	this.rhs = settings.rhs.split('\n');
+}
+
+jQuery.extend(Mgly.CodeifyText.prototype, {
+	getCodes: function(side) {
+		if (!this.ctxs.hasOwnProperty(side)) {
+			var ctx = this._diff_ctx(this[side]);
+			this.ctxs[side] = ctx;
+			ctx.codes.length = Object.keys(ctx.codes).length;
+		}
+		return this.ctxs[side].codes;
+	},
+	getLines: function(side) {
+		return this.ctxs[side].lines;
+	},
+	_diff_ctx: function(lines) {
+		var ctx = {i: 0, codes: {}, lines: lines};
+		this._codeify(lines, ctx);
+		return ctx;
+	},
+	_codeify: function(lines, ctx) {
+		var code = this._max_code;
+		for (var i = 0; i < lines.length; ++i) {
+			var line = lines[i];
+			if (this.options.ignorews) {
+				line = line.replace(/\s+/g, '');
+			}
+			var aCode = this._diff_codes[line];
+			if (aCode != undefined) {
+				ctx.codes[i] = aCode;
+			}
+			else {
+				this._max_code++;
+				this._diff_codes[line] = this._max_code;
+				ctx.codes[i] = this._max_code;
+			}
+		}
+	}
+});
+
+Mgly.diff = function(lhs, rhs, options) {
+	var opts = jQuery.extend({ignorews: false}, options);
+	this.codeify = new Mgly.CodeifyText({
+		lhs: lhs,
+		rhs: rhs,
+		options: opts
+	});
+	var lhs_ctx = {
+		codes: this.codeify.getCodes('lhs'),
+		modified: {}
+	};
+	var rhs_ctx = {
+		codes: this.codeify.getCodes('rhs'),
+		modified: {}
+	};
+	var max = (lhs_ctx.codes.length + rhs_ctx.codes.length + 1);
+	var vector_d = Array( 2 * max + 2 );
+	var vector_u = Array( 2 * max + 2 );
+	this._lcs(lhs_ctx, 0, lhs_ctx.codes.length, rhs_ctx, 0, rhs_ctx.codes.length, vector_u, vector_d);
+	this._optimize(lhs_ctx);
+	this._optimize(rhs_ctx);
+	this.items = this._create_diffs(lhs_ctx, rhs_ctx);
+};
+
+jQuery.extend(Mgly.diff.prototype, {
+	changes: function() { return this.items; },
+	getLines: function(side) {
+		return this.codeify.getLines(side);
+	},
+	normal_form: function() {
+		var nf = '';
+		for (var index = 0; index < this.items.length; ++index) {
+			var item = this.items[index];
+			var lhs_str = '';
+			var rhs_str = '';
+			var change = 'c';
+			if (item.lhs_deleted_count == 0 && item.rhs_inserted_count > 0) change = 'a';
+			else if (item.lhs_deleted_count > 0 && item.rhs_inserted_count == 0) change = 'd';
+			
+			if (item.lhs_deleted_count == 1) lhs_str = item.lhs_start + 1;
+			else if (item.lhs_deleted_count == 0) lhs_str = item.lhs_start;
+			else lhs_str = (item.lhs_start + 1) + ',' + (item.lhs_start + item.lhs_deleted_count);
+			
+			if (item.rhs_inserted_count == 1) rhs_str = item.rhs_start + 1;
+			else if (item.rhs_inserted_count == 0) rhs_str = item.rhs_start;
+			else rhs_str = (item.rhs_start + 1) + ',' + (item.rhs_start + item.rhs_inserted_count);
+			nf += lhs_str + change + rhs_str + '\n';
+			if (this.rhs_lines && this.lhs_lines) {
+				// if rhs/lhs lines have been retained, output contextual diff
+				for (var i = item.lhs_start; i < item.lhs_start + item.lhs_deleted_count; ++i) {
+					nf += '< ' + this.lhs_lines[i] + '\n';
+				}
+				if (item.rhs_inserted_count && item.lhs_deleted_count) nf += '---\n';
+				for (var i = item.rhs_start; i < item.rhs_start + item.rhs_inserted_count; ++i) {
+					nf += '> ' + this.rhs_lines[i] + '\n';
+				}
+			}
+		}
+		return nf;
+	},
+	_lcs: function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d) {
+		while ( (lhs_lower < lhs_upper) && (rhs_lower < rhs_upper) && (lhs_ctx.codes[lhs_lower] == rhs_ctx.codes[rhs_lower]) ) {
+			++lhs_lower;
+			++rhs_lower;
+		}
+		while ( (lhs_lower < lhs_upper) && (rhs_lower < rhs_upper) && (lhs_ctx.codes[lhs_upper - 1] == rhs_ctx.codes[rhs_upper - 1]) ) {
+			--lhs_upper;
+			--rhs_upper;
+		}
+		if (lhs_lower == lhs_upper) {
+			while (rhs_lower < rhs_upper) {
+				rhs_ctx.modified[ rhs_lower++ ] = true;
+			}
+		}
+		else if (rhs_lower == rhs_upper) {
+			while (lhs_lower < lhs_upper) {
+				lhs_ctx.modified[ lhs_lower++ ] = true;
+			}
+		}
+		else {
+			var sms = this._sms(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d);
+			this._lcs(lhs_ctx, lhs_lower, sms.x, rhs_ctx, rhs_lower, sms.y, vector_u, vector_d);
+			this._lcs(lhs_ctx, sms.x, lhs_upper, rhs_ctx, sms.y, rhs_upper, vector_u, vector_d);
+		}
+	},
+	_sms: function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d) {
+		var max = lhs_ctx.codes.length + rhs_ctx.codes.length + 1;
+		var kdown = lhs_lower - rhs_lower;
+		var kup = lhs_upper - rhs_upper;
+		var delta = (lhs_upper - lhs_lower) - (rhs_upper - rhs_lower);
+		var odd = (delta & 1) != 0;
+		var offset_down = max - kdown;
+		var offset_up = max - kup;
+		var maxd = ((lhs_upper - lhs_lower + rhs_upper - rhs_lower) / 2) + 1;
+		vector_d[ offset_down + kdown + 1 ] = lhs_lower;
+		vector_u[ offset_up + kup - 1 ] = lhs_upper;
+		var ret = {x:0,y:0};
+		for (var d = 0; d <= maxd; ++d) {
+			for (var k = kdown - d; k <= kdown + d; k += 2) {
+				var x, y;
+				if (k == kdown - d) {
+					x = vector_d[ offset_down + k + 1 ];//down
+				}
+				else {
+					x = vector_d[ offset_down + k - 1 ] + 1;//right
+					if ((k < (kdown + d)) && (vector_d[ offset_down + k + 1 ] >= x)) {
+						x = vector_d[ offset_down + k + 1 ];//down
+					}
+				}
+				y = x - k;
+				// find the end of the furthest reaching forward D-path in diagonal k.
+				while ((x < lhs_upper) && (y < rhs_upper) && (lhs_ctx.codes[x] == rhs_ctx.codes[y])) {
+					x++; y++;
+				}
+				vector_d[ offset_down + k ] = x;
+				// overlap ?
+				if (odd && (kup - d < k) && (k < kup + d)) {
+					if (vector_u[offset_up + k] <= vector_d[offset_down + k]) {
+						ret.x = vector_d[offset_down + k];
+						ret.y = vector_d[offset_down + k] - k;
+						return (ret);
+					}
+				}
+			}
+			// Extend the reverse path.
+			for (var k = kup - d; k <= kup + d; k += 2) {
+				// find the only or better starting point
+				var x, y;
+				if (k == kup + d) {
+					x = vector_u[offset_up + k - 1]; // up
+				} else {
+					x = vector_u[offset_up + k + 1] - 1; // left
+					if ((k > kup - d) && (vector_u[offset_up + k - 1] < x))
+						x = vector_u[offset_up + k - 1]; // up
+				}
+				y = x - k;
+				while ((x > lhs_lower) && (y > rhs_lower) && (lhs_ctx.codes[x - 1] == rhs_ctx.codes[y - 1])) {
+					// diagonal
+					x--;
+					y--;
+				}
+				vector_u[offset_up + k] = x;
+				// overlap ?
+				if (!odd && (kdown - d <= k) && (k <= kdown + d)) {
+					if (vector_u[offset_up + k] <= vector_d[offset_down + k]) {
+						ret.x = vector_d[offset_down + k];
+						ret.y = vector_d[offset_down + k] - k;
+						return (ret);
+					}
+				}
+			}
+		}
+		throw "the algorithm should never come here.";
+	},
+	_optimize: function(ctx) {
+		var start = 0, end = 0;
+		while (start < ctx.length) {
+			while ((start < ctx.length) && (ctx.modified[start] == undefined || ctx.modified[start] == false)) {
+				start++;
+			}
+			end = start;
+			while ((end < ctx.length) && (ctx.modified[end] == true)) {
+				end++;
+			}
+			if ((end < ctx.length) && (ctx.ctx[start] == ctx.codes[end])) {
+				ctx.modified[start] = false;
+				ctx.modified[end] = true;
+			}
+			else {
+				start = end;
+			}
+		}
+	},
+	_create_diffs: function(lhs_ctx, rhs_ctx) {
+		var items = [];
+		var lhs_start = 0, rhs_start = 0;
+		var lhs_line = 0, rhs_line = 0;
+
+		while (lhs_line < lhs_ctx.codes.length || rhs_line < rhs_ctx.codes.length) {
+			if ((lhs_line < lhs_ctx.codes.length) && (!lhs_ctx.modified[lhs_line])
+				&& (rhs_line < rhs_ctx.codes.length) && (!rhs_ctx.modified[rhs_line])) {
+				// equal lines
+				lhs_line++;
+				rhs_line++;
+			}
+			else {
+				// maybe deleted and/or inserted lines
+				lhs_start = lhs_line;
+				rhs_start = rhs_line;
+
+				while (lhs_line < lhs_ctx.codes.length && (rhs_line >= rhs_ctx.codes.length || lhs_ctx.modified[lhs_line]))
+					lhs_line++;
+
+				while (rhs_line < rhs_ctx.codes.length && (lhs_line >= lhs_ctx.codes.length || rhs_ctx.modified[rhs_line]))
+					rhs_line++;
+
+				if ((lhs_start < lhs_line) || (rhs_start < rhs_line)) {
+					// store a new difference-item
+					items.push({
+						lhs_start: lhs_start,
+						rhs_start: rhs_start,
+						lhs_deleted_count: lhs_line - lhs_start,
+						rhs_inserted_count: rhs_line - rhs_start
+					});
+				}
+			}
+		}
+		return items;
+	}
+});
+
+Mgly.mergely = function(el, options) {
+	if (el) {
+		this.init(el, options);
+	}
+};
+
+jQuery.extend(Mgly.mergely.prototype, {
+	name: 'mergely',
+	//http://jupiterjs.com/news/writing-the-perfect-jquery-plugin
+	init: function(el, options) {
+		this.diffView = new Mgly.CodeMirrorDiffView(el, options);
+		this.bind(el);
+	},
+	bind: function(el) {
+		this.diffView.bind(el);
+	}
+});
+
+Mgly.CodeMirrorDiffView = function(el, options) {
+	CodeMirror.defineExtension('centerOnCursor', function() {
+		var coords = this.cursorCoords(null, 'local');
+		this.scrollTo(null, 
+			(coords.y + coords.yBot) / 2 - (this.getScrollerElement().clientHeight / 2));
+	});
+	this.init(el, options);
+};
+
+jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
+	init: function(el, options) {
+		this.settings = {
+			autoupdate: true,
+			autoresize: true,
+			rhs_margin: 'right',
+			lcs: true,
+			sidebar: true,
+			viewport: false,
+			ignorews: false,
+			fadein: 'fast',
+			editor_width: '400px',
+			editor_height: '400px',
+			resize_timeout: 500,
+			change_timeout: 150,
+			fgcolor: {a:'#4ba3fa',c:'#a3a3a3',d:'#ff7f7f'},
+			bgcolor: '#eee',
+			vpcolor: 'rgba(0, 0, 200, 0.5)',
+			lhs: function(setValue) { },
+			rhs: function(setValue) { },
+			loaded: function() { },
+			//_auto_height: function(h) { return h - 20; },
+			_auto_width: function(w) { return w; },
+			resize: function(init) {
+				var scrollbar = init ? 16 : 0;
+				var w = jQuery(el).parent().width() + scrollbar;
+				if (this.width == 'auto') {
+					w = this._auto_width(w);
+				}
+				else {
+					w = this.width;
+					this.editor_width = w;
+				}
+				if (this.height == 'auto') {
+					//h = this._auto_height(h);
+					h = jQuery(el).parent().height();
+				}
+				else {
+					h = this.height;
+					this.editor_height = h;
+				}
+				var content_width = w / 2.0 - 2 * 8 - 8;
+				var content_height = h;
+				var self = jQuery(el);
+				self.find('.mergely-column').css({ width: content_width + 'px' });
+				self.find('.mergely-column, .mergely-canvas, .mergely-margin, .mergely-column textarea, .CodeMirror-scroll, .cm-s-default').css({ height: content_height + 'px' });
+				self.find('.mergely-canvas').css({ height: content_height + 'px' });
+				self.find('.mergely-column textarea').css({ width: content_width + 'px' });
+				self.css({ width: w, height: h, clear: 'both' });
+				if (self.css('display') == 'none') {
+					if (this.fadein != false) self.fadeIn(this.fadein);
+					else self.show();
+					if (this.loaded) this.loaded();
+				}
+				if (this.resized) this.resized();
+			},
+			_debug: '', //scroll,draw,calc,diff,markup,change
+			resized: function() { }
+		};
+		var cmsettings = {
+			mode: 'text/plain',
+			readOnly: false,
+			lineWrapping: false,
+			lineNumbers: true,
+			gutters: ['merge', 'CodeMirror-linenumbers']
+		}
+		this.lhs_cmsettings = {};
+		this.rhs_cmsettings = {};
+		
+		// save this element for faster queries
+		this.element = jQuery(el);
+		
+		// save options if there are any
+		if (options && options.cmsettings) jQuery.extend(this.lhs_cmsettings, cmsettings, options.cmsettings, options.lhs_cmsettings);
+		if (options && options.cmsettings) jQuery.extend(this.rhs_cmsettings, cmsettings, options.cmsettings, options.rhs_cmsettings);
+		if (options) jQuery.extend(this.settings, options);
+		
+		// bind if the element is destroyed
+		this.element.bind('destroyed', jQuery.proxy(this.teardown, this));
+
+		// save this instance in jQuery data, binding this view to the node
+		jQuery.data(el, 'mergely', this);
+	},
+	unbind: function() {
+		if (this.changed_timeout != null) clearTimeout(this.changed_timeout);
+		this.editor[this.id + '-lhs'].toTextArea();
+		this.editor[this.id + '-rhs'].toTextArea();
+	},
+	destroy: function() {
+		this.element.unbind('destroyed', this.teardown);
+		this.teardown();
+	},
+	teardown: function() {
+		this.unbind();
+	},
+	lhs: function(text) {
+		this.editor[this.id + '-lhs'].setValue(text);
+	},
+	rhs: function(text) {
+		this.editor[this.id + '-rhs'].setValue(text);
+	},
+	update: function() {
+		this._changing(this.id + '-lhs', this.id + '-rhs');
+	},
+	unmarkup: function() {
+		this._clear();
+	},
+	scrollTo: function(side, num) {
+		var le = this.editor[this.id + '-lhs'];
+		var re = this.editor[this.id + '-rhs'];
+		if (side == 'lhs') {
+			le.setCursor(num);
+			le.centerOnCursor();
+		}
+		else {
+			re.setCursor(num);
+			re.centerOnCursor();
+		}
+	},
+	options: function(opts) {
+		if (opts) {
+			jQuery.extend(this.settings, opts);
+			if (this.settings.autoresize) this.resize();
+			if (this.settings.autoupdate) this.update();
+			if (this.settings.hasOwnProperty('rhs_margin')) {
+				// dynamically swap the margin
+				if (this.settings.rhs_margin == 'left') {
+					this.element.find('.mergely-margin:last-child').insertAfter(
+						this.element.find('.mergely-canvas'));
+				}
+				else {
+					var target = this.element.find('.mergely-margin').last();
+					target.appendTo(target.parent());
+				}
+			}
+			if (this.settings.hasOwnProperty('sidebar')) {
+				// dynamically enable sidebars
+				if (this.settings.sidebar) {
+					jQuery(this.element).find('.mergely-margin').css({display: 'block'});
+				}
+				else {
+					jQuery(this.element).find('.mergely-margin').css({display: 'none'});
+				}
+			}
+		}
+		else {
+			return this.settings;
+		}
+	},
+	swap: function() {
+		if (this.lhs_cmsettings.readOnly || this.rhs_cmsettings.readOnly) return;
+		var le = this.editor[this.id + '-lhs'];
+		var re = this.editor[this.id + '-rhs'];
+		var tmp = re.getValue();
+		re.setValue(le.getValue());
+		le.setValue(tmp);
+	},
+	merge: function(side) {
+		var le = this.editor[this.id + '-lhs'];
+		var re = this.editor[this.id + '-rhs'];
+		if (side == 'lhs' && !this.lhs_cmsettings.readOnly) le.setValue(re.getValue());
+		else if (!this.rhs_cmsettings.readOnly) re.setValue(le.getValue());
+	},
+	get: function(side) {
+		var ed = this.editor[this.id + '-' + side];
+		var t = ed.getValue();
+		if (t == undefined) return '';
+		return t;
+	},
+	clear: function(side) {
+		if (side == 'lhs' && this.lhs_cmsettings.readOnly) return;
+		if (side == 'rhs' && this.rhs_cmsettings.readOnly) return;
+		var ed = this.editor[this.id + '-' + side];
+		ed.setValue('');
+	},
+	cm: function(side) {
+		return this.editor[this.id + '-' + side];
+	},
+	search: function(side, query, direction) {
+		var le = this.editor[this.id + '-lhs'];
+		var re = this.editor[this.id + '-rhs'];
+		var editor;
+		if (side == 'lhs') editor = le;
+		else editor = re;
+		direction = (direction == 'prev') ? 'findPrevious' : 'findNext';
+		if ((editor.getSelection().length == 0) || (this.prev_query[side] != query)) {
+			this.cursor[this.id] = editor.getSearchCursor(query, { line: 0, ch: 0 }, false);
+			this.prev_query[side] = query;
+		}
+		var cursor = this.cursor[this.id];
+		
+		if (cursor[direction]()) {
+			editor.setSelection(cursor.from(), cursor.to());
+		}
+		else {
+			cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, false);
+		}
+	},
+	resize: function() {
+		this.settings.resize();
+		this._changing(this.id + '-lhs', this.id + '-rhs');
+		this._set_top_offset(this.id + '-lhs');
+	},
+	diff: function() {
+		var lhs = this.editor[this.id + '-lhs'].getValue();
+		var rhs = this.editor[this.id + '-rhs'].getValue();
+		var d = new Mgly.diff(lhs, rhs, this.settings);
+		return d.normal_form();
+	},
+	bind: function(el) {
+		jQuery(this.element).hide();//hide
+		this.id = jQuery(el).attr('id');
+		var height = this.settings.editor_height;
+		var width = this.settings.editor_width;
+		this.changed_timeout = null;
+		this.chfns = {};
+		this.chfns[this.id + '-lhs'] = [];
+		this.chfns[this.id + '-rhs'] = [];
+		this.prev_query = [];
+		this.cursor = [];
+		this._skipscroll = {};
+		this.change_exp = new RegExp(/(\d+(?:,\d+)?)([acd])(\d+(?:,\d+)?)/);
+		var merge_lhs_button;
+		var merge_rhs_button;
+		if (jQuery.button != undefined) {
+			//jquery ui
+			merge_lhs_button = '<button title="Merge left"></button>';
+			merge_rhs_button = '<button title="Merge right"></button>';
+		}
+		else {
+			// homebrew
+			var style = 'opacity:0.4;width:10px;height:15px;background-color:#888;cursor:pointer;text-align:center;color:#eee;border:1px solid: #222;margin-right:5px;';
+			merge_lhs_button = '<div style="' + style + '" title="Merge left">&lt;</div>';
+			merge_rhs_button = '<div style="' + style + '" title="Merge right">&gt;</div>';
+		}
+		this.merge_rhs_button = jQuery(merge_rhs_button);
+		this.merge_lhs_button = jQuery(merge_lhs_button);
+		
+		// create the textarea and canvas elements
+		jQuery(this.element).append(jQuery('<div class="mergely-margin" style="height: ' + height + '"><canvas id="' + this.id + '-lhs-margin" width="8px" height="' + height + '"></canvas></div>'));
+		jQuery(this.element).append(jQuery('<div style="position:relative;width:' + width + '; height:' + height + '" id="' + this.id + '-editor-lhs" class="mergely-column"><textarea style="" id="' + this.id + '-lhs"></textarea></div>'));
+		jQuery(this.element).append(jQuery('<div class="mergely-canvas" style="height: ' + height + '"><canvas id="' + this.id + '-lhs-' + this.id + '-rhs-canvas" style="width:28px" width="28px" height="' + height + '"></canvas></div>'));
+		var rmargin = jQuery('<div class="mergely-margin" style="height: ' + height + '"><canvas id="' + this.id + '-rhs-margin" width="8px" height="' + height + '"></canvas></div>');
+		if (!this.settings.sidebar) {
+			jQuery(this.element).find('.mergely-margin').css({display: 'none'});
+		}
+		if (this.settings.rhs_margin == 'left') {
+			jQuery(this.element).append(rmargin);
+		}
+		jQuery(this.element).append(jQuery('<div style="width:' + width + '; height:' + height + '" id="' + this.id + '-editor-rhs" class="mergely-column"><textarea style="" id="' + this.id + '-rhs"></textarea></div>'));
+		if (this.settings.rhs_margin != 'left') {
+			jQuery(this.element).append(rmargin);
+		}
+		//codemirror
+		var cmstyle = '#' + this.id + ' .CodeMirror-gutter-text { padding: 5px 0 0 0; }' +
+			'#' + this.id + ' .CodeMirror-lines pre, ' + '#' + this.id + ' .CodeMirror-gutter-text pre { line-height: 18px; }' +
+			'.CodeMirror-linewidget { overflow: hidden; };';
+		if (this.settings.autoresize) {
+			cmstyle += this.id + ' .CodeMirror-scroll { height: 100%; overflow: auto; }';
+		}
+		jQuery('<style type="text/css">' + cmstyle + '</style>').appendTo('head');
+
+		//bind
+		var rhstx = jQuery('#' + this.id + '-rhs').get(0);
+		if (!rhstx) {
+			console.error('rhs textarea not defined - Mergely not initialized properly');
+			return;
+		}
+		var lhstx = jQuery('#' + this.id + '-lhs').get(0);
+		if (!rhstx) {
+			console.error('lhs textarea not defined - Mergely not initialized properly');
+			return;
+		}
+		var self = this;
+		this.editor = [];
+		this.editor[this.id + '-lhs'] = CodeMirror.fromTextArea(lhstx, this.lhs_cmsettings);
+		this.editor[this.id + '-rhs'] = CodeMirror.fromTextArea(rhstx, this.rhs_cmsettings);
+		this.editor[this.id + '-lhs'].on('change', function(){ if (self.settings.autoupdate) self._changing(self.id + '-lhs', self.id + '-rhs'); });
+		this.editor[this.id + '-lhs'].on('scroll', function(){ self._scrolling(self.id + '-lhs'); });
+		this.editor[this.id + '-rhs'].on('change', function(){ if (self.settings.autoupdate) self._changing(self.id + '-lhs', self.id + '-rhs'); });
+		this.editor[this.id + '-rhs'].on('scroll', function(){ self._scrolling(self.id + '-rhs'); });
+		// resize
+		if (this.settings.autoresize) {
+			var sz_timeout1 = null;
+			var sz = function(init) {
+				//self.em_height = null; //recalculate
+				if (self.settings.resize) self.settings.resize(init);
+				self.editor[self.id + '-lhs'].refresh();
+				self.editor[self.id + '-rhs'].refresh();
+				if (self.settings.autoupdate) {
+					self._changing(self.id + '-lhs', self.id + '-rhs');
+				}
+			}
+			jQuery(window).resize(
+				function () {
+					if (sz_timeout1) clearTimeout(sz_timeout1);
+					sz_timeout1 = setTimeout(sz, self.settings.resize_timeout);
+				}
+			);
+			sz(true);
+		}
+		//bind
+		
+		if (this.settings.lhs) {
+			var setv = this.editor[this.id + '-lhs'].getDoc().setValue;
+			this.settings.lhs(setv.bind(this.editor[this.id + '-lhs'].getDoc()));
+		}
+		if (this.settings.rhs) {
+			var setv = this.editor[this.id + '-rhs'].getDoc().setValue;
+			this.settings.rhs(setv.bind(this.editor[this.id + '-rhs'].getDoc()));
+		}
+	},
+	
+	_scrolling: function(editor_name) {
+		if (this._skipscroll[editor_name] === true) {
+			// scrolling one side causes the other to event - ignore it
+			this._skipscroll[editor_name] = false;
+			return;
+		}
+		var scroller = jQuery(this.editor[editor_name].getScrollerElement());
+		if (this.midway == undefined) {
+			this.midway = (scroller.height() / 2.0 + scroller.offset().top).toFixed(2);
+		}
+		// balance-line
+		var midline = this.editor[editor_name].coordsChar({left:0, top:this.midway});
+		var top_to = scroller.scrollTop();
+		var left_to = scroller.scrollLeft();
+		
+		this.trace('scroll', 'side', editor_name);
+		this.trace('scroll', 'midway', this.midway);
+		this.trace('scroll', 'midline', midline);
+		this.trace('scroll', 'top_to', top_to);
+		this.trace('scroll', 'left_to', left_to);
+		
+		var editor_name1 = this.id + '-lhs';
+		var editor_name2 = this.id + '-rhs';
+		
+		for (var name in this.editor) {
+			if (!this.editor.hasOwnProperty(name)) continue;
+			if (editor_name == name) continue; //same editor
+			var this_side = editor_name.replace(this.id + '-', '');
+			var other_side = name.replace(this.id + '-', '');
+			var top_adjust = 0;
+			
+			// find the last change that is less than or within the midway point
+			// do not move the rhs until the lhs end point is >= the rhs end point.
+			var last_change = null;
+			var force_scroll = false;
+			for (var i = 0; i < this.changes.length; ++i) {
+				var change = this.changes[i];
+				if ((midline.line >= change[this_side+'-line-from'])) {
+					last_change = change;
+					if (midline.line >= last_change[this_side+'-line-to']) {
+						if (!change.hasOwnProperty(this_side+'-y-start') ||
+							!change.hasOwnProperty(this_side+'-y-end') ||
+							!change.hasOwnProperty(other_side+'-y-start') ||
+							!change.hasOwnProperty(other_side+'-y-end')){
+							// change outside of viewport
+							force_scroll = true;
+						}
+						else {
+							top_adjust += 
+								(change[this_side+'-y-end'] - change[this_side+'-y-start']) - 
+								(change[other_side+'-y-end'] - change[other_side+'-y-start']);
+						}
+					}
+				}
+			}
+			
+			var vp = this.editor[name].getViewport();
+			var scroll = true;
+			if (last_change) {
+				this.trace('scroll', 'last change before midline', last_change);
+				if (midline.line >= vp.from && midline <= vp.to) {
+					scroll = false;
+				}
+			}
+			this.trace('scroll', 'scroll', scroll);
+			if (scroll || force_scroll) {
+				// scroll the other side
+				this.trace('scroll', 'scrolling other side', top_to - top_adjust);
+				var scroller = jQuery(this.editor[name].getScrollerElement());
+				this._skipscroll[name] = true;//disable next event
+				scroller.scrollTop(top_to - top_adjust).scrollLeft(left_to);
+			}
+			else this.trace('scroll', 'not scrolling other side');
+			
+			if (this.settings.autoupdate) {
+				var timer = new Mgly.Timer();
+				this._calculate_offsets(editor_name1, editor_name2, this.changes);
+				this.trace('change', 'offsets time', timer.stop());
+				this._markup_changes(editor_name1, editor_name2, this.changes);
+				this.trace('change', 'markup time', timer.stop());
+				this._draw_diff(editor_name1, editor_name2, this.changes);
+				this.trace('change', 'draw time', timer.stop());
+			}
+			this.trace('scroll', 'scrolled');
+		}
+	},
+	_changing: function(editor_name1, editor_name2) {
+		this.trace('change', 'changing-timeout', this.changed_timeout);
+		var self = this;
+		if (this.changed_timeout != null) clearTimeout(this.changed_timeout);
+		this.changed_timeout = setTimeout(function(){
+			var timer = new Mgly.Timer();
+			self._changed(editor_name1, editor_name2);
+			self.trace('change', 'total time', timer.stop());
+		}, this.settings.change_timeout);
+	},
+	_changed: function(editor_name1, editor_name2) {
+		this._clear();
+		this._diff(editor_name1, editor_name2);
+	},
+	_clear: function() {
+		var self = this;
+		for (var name in this.editor) {
+			if (!this.editor.hasOwnProperty(name)) continue;
+			var editor = this.editor[name];
+			var fns = self.chfns[name];
+			// clear editor changes
+			editor.operation(function() {
+				var timer = new Mgly.Timer();
+				for (var i = 0, l = editor.lineCount(); i < l; ++i) {
+					editor.removeLineClass(i, 'background');
+				}
+				for (var i = 0; i < fns.length; ++i) {
+					//var edid = editor.getDoc().id;
+					var change = fns[i];
+					//if (change.doc.id != edid) continue;
+					if (change.lines.length) {
+						self.trace('change', 'clear text', change.lines[0].text);
+					}
+					change.clear();
+				}
+				editor.clearGutter('merge');
+				self.trace('change', 'clear time', timer.stop());
+			});
+		}
+		self.chfns[name] = [];
+		
+		var ex = this._draw_info(this.id + '-lhs', this.id + '-rhs');
+		var ctx_lhs = ex.clhs.get(0).getContext('2d');
+		var ctx_rhs = ex.crhs.get(0).getContext('2d');
+		var ctx = ex.dcanvas.getContext('2d');
+		
+		ctx_lhs.beginPath();
+		ctx_lhs.fillStyle = this.settings.bgcolor;
+		ctx_lhs.strokeStyle = '#888';
+		ctx_lhs.fillRect(0, 0, 6.5, ex.visible_page_height);
+		ctx_lhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
+
+		ctx_rhs.beginPath();
+		ctx_rhs.fillStyle = this.settings.bgcolor;
+		ctx_rhs.strokeStyle = '#888';
+		ctx_rhs.fillRect(0, 0, 6.5, ex.visible_page_height);
+		ctx_rhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
+		
+		ctx.beginPath();
+		ctx.fillStyle = '#fff';
+		ctx.fillRect(0, 0, this.draw_mid_width, ex.visible_page_height);
+	},
+	_diff: function(editor_name1, editor_name2) {
+		var lhs = this.editor[editor_name1].getValue();
+		var rhs = this.editor[editor_name2].getValue();
+		var timer = new Mgly.Timer();
+		var d = new Mgly.diff(lhs, rhs, this.settings);
+		this.trace('change', 'diff time', timer.stop());
+		this.changes = Mgly.DiffParser(d.normal_form());
+		this.trace('change', 'parse time', timer.stop());
+		this._calculate_offsets(editor_name1, editor_name2, this.changes);
+		this.trace('change', 'offsets time', timer.stop());
+		this._markup_changes(editor_name1, editor_name2, this.changes);
+		this.trace('change', 'markup time', timer.stop());
+		this._draw_diff(editor_name1, editor_name2, this.changes);
+		this.trace('change', 'draw time', timer.stop());
+	},
+	_parse_diff: function (editor_name1, editor_name2, diff) {
+		this.trace('diff', 'diff results:\n', diff);
+		var changes = [];
+		var change_id = 0;
+		// parse diff
+		var diff_lines = diff.split(/\n/);
+		for (var i = 0; i < diff_lines.length; ++i) {
+			if (diff_lines[i].length == 0) continue;
+			var change = {};
+			var test = this.change_exp.exec(diff_lines[i]);
+			if (test == null) continue;
+			// lines are zero-based
+			var fr = test[1].split(',');
+			change['lhs-line-from'] = fr[0] - 1;
+			if (fr.length == 1) change['lhs-line-to'] = fr[0] - 1;
+			else change['lhs-line-to'] = fr[1] - 1;
+			var to = test[3].split(',');
+			change['rhs-line-from'] = to[0] - 1;
+			if (to.length == 1) change['rhs-line-to'] = to[0] - 1;
+			else change['rhs-line-to'] = to[1] - 1;
+			// TODO: optimize for changes that are adds/removes
+			if (change['lhs-line-from'] < 0) change['lhs-line-from'] = 0;
+			if (change['lhs-line-to'] < 0) change['lhs-line-to'] = 0;
+			if (change['rhs-line-from'] < 0) change['rhs-line-from'] = 0;
+			if (change['rhs-line-to'] < 0) change['rhs-line-to'] = 0;
+			change['op'] = test[2];
+			changes[change_id++] = change;
+			this.trace('diff', 'change', change);
+		}
+		return changes;
+	},
+	_get_viewport: function(editor_name1, editor_name2) {
+		var lhsvp = this.editor[editor_name1].getViewport();
+		var rhsvp = this.editor[editor_name2].getViewport();
+		return {from: Math.min(lhsvp.from, rhsvp.from), to: Math.max(lhsvp.to, rhsvp.to)};
+	},
+	_is_change_in_view: function(vp, change) {
+		if (!this.settings.viewport) return true;
+		if ((change['lhs-line-from'] < vp.from && change['lhs-line-to'] < vp.to) ||
+			(change['lhs-line-from'] > vp.from && change['lhs-line-to'] > vp.to) ||
+			(change['rhs-line-from'] < vp.from && change['rhs-line-to'] < vp.to) ||
+			(change['rhs-line-from'] > vp.from && change['rhs-line-to'] > vp.to)) {
+			// if the change is outside the viewport, skip
+			return false;
+		}
+		return true;
+	},
+	_set_top_offset: function (editor_name1) {
+		// save the current scroll position of the editor
+		var saveY = this.editor[editor_name1].getScrollInfo().top;
+		// temporarily scroll to top
+		this.editor[editor_name1].scrollTo(null, 0);
+		
+		// this is the distance from the top of the screen to the top of the 
+		// content of the first codemirror editor
+		var topnode = jQuery('#' + this.id + ' .CodeMirror-measure').first();
+		var top_offset = topnode.offset().top - 4;
+		if(!top_offset) return false;
+		
+		// restore editor's scroll position
+		this.editor[editor_name1].scrollTo(null, saveY);
+		
+		this.draw_top_offset = 0.5 - top_offset;
+		return true;
+	},
+	_calculate_offsets: function (editor_name1, editor_name2, changes) {
+		if (this.em_height == null) {
+			if(!this._set_top_offset(editor_name1)) return; //try again
+			this.em_height = this.editor[editor_name1].defaultTextHeight();
+			if (!this.em_height) {
+				console.warn('Failed to calculate offsets, using 18 by default');
+				this.em_height = 18;
+			}
+			this.draw_lhs_min = 0.5;
+			var c = jQuery('#' + editor_name1 + '-' + editor_name2 + '-canvas');
+			if (!c.length) {
+				console.error('failed to find canvas', '#' + editor_name1 + '-' + editor_name2 + '-canvas');
+			}
+			if (!c.width()) {
+				console.error('canvas width is 0');
+				return;
+			}
+			this.draw_mid_width = jQuery('#' + editor_name1 + '-' + editor_name2 + '-canvas').width();
+			this.draw_rhs_max = this.draw_mid_width - 0.5; //24.5;
+			this.draw_lhs_width = 5;
+			this.draw_rhs_width = 5;
+			this.trace('calc', 'change offsets calculated', {top_offset: this.draw_top_offset, lhs_min: this.draw_lhs_min, rhs_max: this.draw_rhs_max, lhs_width: this.draw_lhs_width, rhs_width: this.draw_rhs_width});
+		}
+		var lhschc = this.editor[editor_name1].charCoords({line: 0});
+		var rhschc = this.editor[editor_name2].charCoords({line: 0});
+		var vp = this._get_viewport(editor_name1, editor_name2);
+		
+		for (var i = 0; i < changes.length; ++i) {
+			var change = changes[i];
+			
+			if (!this.settings.sidebar && !this._is_change_in_view(vp, change)) {
+				// if the change is outside the viewport, skip
+				delete change['lhs-y-start'];
+				delete change['lhs-y-end'];
+				delete change['rhs-y-start'];
+				delete change['rhs-y-end'];
+				continue;
+			}
+			var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
+			var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
+			var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
+			var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
+			
+			var ls, le, rs, re;
+			if (this.editor[editor_name1].getOption('lineWrapping') || this.editor[editor_name1].getOption('lineWrapping')) {
+				// If using line-wrapping, we must get the height of the line
+				var tls = this.editor[editor_name1].cursorCoords({line: llf, ch: 0}, 'page');
+				var lhssh = this.editor[editor_name1].getLineHandle(llf);
+				ls = { top: tls.top, bottom: tls.top + lhssh.height };
+
+				var tle = this.editor[editor_name1].cursorCoords({line: llt, ch: 0}, 'page');
+				var lhseh = this.editor[editor_name1].getLineHandle(llt);
+				le = { top: tle.top, bottom: tle.top + lhseh.height };
+				
+				var tls = this.editor[editor_name2].cursorCoords({line: rlf, ch: 0}, 'page');
+				var rhssh = this.editor[editor_name2].getLineHandle(rlf);
+				rs = { top: tls.top, bottom: tls.top + rhssh.height };
+
+				var tle = this.editor[editor_name2].cursorCoords({line: rlt, ch: 0}, 'page');
+				var rhseh = this.editor[editor_name2].getLineHandle(rlt);
+				re = { top: tle.top, bottom: tle.top + rhseh.height };
+			}
+			else {
+				// If not using line-wrapping, we can calculate the line position
+				ls = { 
+					top: lhschc.top + llf * this.em_height, 
+					bottom: lhschc.bottom + llf * this.em_height + 2
+				};
+				le = {
+					top: lhschc.top + llt * this.em_height, 
+					bottom: lhschc.bottom + llt * this.em_height + 2
+				};
+				rs = {
+					top: rhschc.top + rlf * this.em_height, 
+					bottom: rhschc.bottom + rlf * this.em_height + 2
+				};
+				re = {
+					top: rhschc.top + rlt * this.em_height, 
+					bottom: rhschc.bottom + rlt * this.em_height + 2
+				};
+			}
+			
+			if (change['op'] == 'a') {
+				// adds (right), normally start from the end of the lhs,
+				// except for the case when the start of the rhs is 0
+				if (rlf > 0) {
+					ls.top = ls.bottom;
+					ls.bottom += this.em_height;
+					le = ls;
+				}
+			}
+			else if (change['op'] == 'd') {
+				// deletes (left) normally finish from the end of the rhs,
+				// except for the case when the start of the lhs is 0
+				if (llf > 0) {
+					rs.top = rs.bottom;
+					rs.bottom += this.em_height;
+					re = rs;
+				}
+			}
+			change['lhs-y-start'] = this.draw_top_offset + ls.top;
+			if (change['op'] == 'c' || change['op'] == 'd') {
+				change['lhs-y-end'] = this.draw_top_offset + le.bottom;
+			}
+			else {
+				change['lhs-y-end'] = this.draw_top_offset + le.top;
+			}
+			change['rhs-y-start'] = this.draw_top_offset + rs.top;
+			if (change['op'] == 'c' || change['op'] == 'a') {
+				change['rhs-y-end'] = this.draw_top_offset + re.bottom;
+			}
+			else {
+				change['rhs-y-end'] = this.draw_top_offset + re.top;
+			}
+			this.trace('calc', 'change calculated', i, change);
+		}
+		return changes;
+	},
+	_markup_changes: function (editor_name1, editor_name2, changes) {
+		jQuery('.merge-button').remove(); // clear
+		
+		var self = this;
+		var led = this.editor[editor_name1];
+		var red = this.editor[editor_name2];
+
+		var timer = new Mgly.Timer();
+		led.operation(function() {
+			for (var i = 0; i < changes.length; ++i) {
+				var change = changes[i];
+				var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
+				var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
+				var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
+				var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
+				
+				var clazz = ['mergely', 'lhs', change['op'], 'cid-' + i];
+				led.addLineClass(llf, 'background', 'start');
+				led.addLineClass(llt, 'background', 'end');
+				
+				if (llf == 0 && llt == 0 && rlf == 0) {
+					led.addLineClass(llf, 'background', clazz.join(' '));
+					led.addLineClass(llf, 'background', 'first');
+				}
+				else {
+					// apply change for each line in-between the changed lines
+					for (var j = llf; j <= llt; ++j) {
+						led.addLineClass(j, 'background', clazz.join(' '));
+						led.addLineClass(j, 'background', clazz.join(' '));
+					}
+				}
+				
+				if (!red.getOption('readOnly')) {
+					// add widgets to lhs, if rhs is not read only
+					var rhs_button = self.merge_rhs_button.clone();
+					if (rhs_button.button) {
+						//jquery-ui support
+						rhs_button.button({icons: {primary: 'ui-icon-triangle-1-e'}, text: false});
+					}
+					rhs_button.addClass('merge-button');
+					rhs_button.attr('id', 'merge-rhs-' + i);
+					led.setGutterMarker(llf, 'merge', rhs_button.get(0));
+				}
+			}
+		});
+
+		var vp = this._get_viewport(editor_name1, editor_name2);
+		
+		this.trace('change', 'markup lhs-editor time', timer.stop());
+		red.operation(function() {
+			for (var i = 0; i < changes.length; ++i) {
+				var change = changes[i];
+				var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
+				var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
+				var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
+				var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
+				
+				if (!self._is_change_in_view(vp, change)) {
+					// if the change is outside the viewport, skip
+					continue;
+				}
+				
+				var clazz = ['mergely', 'rhs', change['op'], 'cid-' + i];
+				red.addLineClass(rlf, 'background', 'start');
+				red.addLineClass(rlt, 'background', 'end');
+				
+				if (rlf == 0 && rlt == 0 && llf == 0) {
+					red.addLineClass(rlf, 'background', clazz.join(' '));
+					red.addLineClass(rlf, 'background', 'first');
+				}
+				else {
+					// apply change for each line in-between the changed lines
+					for (var j = rlf; j <= rlt; ++j) {
+						red.addLineClass(j, 'background', clazz.join(' '));
+						red.addLineClass(j, 'background', clazz.join(' '));
+					}
+				}
+
+				if (!led.getOption('readOnly')) {
+					// add widgets to rhs, if lhs is not read only
+					var lhs_button = self.merge_lhs_button.clone();
+					if (lhs_button.button) {
+						//jquery-ui support
+						lhs_button.button({icons: {primary: 'ui-icon-triangle-1-w'}, text: false});
+					}
+					lhs_button.addClass('merge-button');
+					lhs_button.attr('id', 'merge-lhs-' + i);
+					red.setGutterMarker(rlf, 'merge', lhs_button.get(0));
+				}
+			}
+		});
+		this.trace('change', 'markup rhs-editor time', timer.stop());
+		
+		// mark text deleted, LCS changes
+		var marktext = [];
+		for (var i = 0; this.settings.lcs && i < changes.length; ++i) {
+			var change = changes[i];
+			var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
+			var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
+			var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
+			var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
+			
+			if (!this._is_change_in_view(vp, change)) {
+				// if the change is outside the viewport, skip
+				continue;
+			}
+			if (change['op'] == 'd') {
+				// apply delete to cross-out (left-hand side only)
+				var from = llf;
+				var to = llt;
+				var to_ln = led.lineInfo(to);
+				if (to_ln) {
+					marktext.push([led, {line:from, ch:0}, {line:to, ch:to_ln.text.length}, {className: 'mergely ch d lhs'}]);
+				}
+			}
+			else if (change['op'] == 'c') {
+				// apply LCS changes to each line
+				for (var j = llf, k = rlf, p = 0; 
+					 ((j >= 0) && (j <= llt)) || ((k >= 0) && (k <= rlt));
+					 ++j, ++k) {
+					if (k + p > rlt) {
+						// lhs continues past rhs, mark lhs as deleted
+						var lhs_line = led.getLine( j );
+						marktext.push([led, {line:j, ch:0}, {line:j, ch:lhs_line.length}, {className: 'mergely ch d lhs'}]);
+						continue;
+					}
+					if (j + p > llt) {
+						// rhs continues past lhs, mark rhs as added
+						var rhs_line = red.getLine( k );
+						marktext.push([red, {line:k, ch:0}, {line:k, ch:rhs_line.length}, {className: 'mergely ch a rhs'}]);
+						continue;
+					}
+					var lhs_line = led.getLine( j );
+					var rhs_line = red.getLine( k );
+					var lhs_start = { line: -1, ch: -1 };
+					var lhs_stop = { line: -1, ch: -1 };
+					var rhs_start = { line: -1, ch: -1 };
+					var rhs_stop = { line: -1, ch: -1 };
+					
+					var lcs = new Mgly.LCS(lhs_line, rhs_line);
+					lcs.diff(
+						function (from, to) {//added
+							marktext.push([red, {line:k, ch:from}, {line:k, ch:to}, {className: 'mergely ch a rhs'}]);
+						},
+						removed = function (from, to) {//removed
+							marktext.push([led, {line:j, ch:from}, {line:j, ch:to}, {className: 'mergely ch d lhs'}]);
+						}
+					);
+				}
+			}
+		}
+		this.trace('change', 'LCS marktext time', timer.stop());
+		
+		// mark changes outside closure
+		led.operation(function() {
+			// apply lhs markup
+			for (var i = 0; i < marktext.length; ++i) {
+				var m = marktext[i];
+				if (m[0].doc.id != led.getDoc().id) continue;
+				self.chfns[self.id + '-lhs'].push(m[0].markText(m[1], m[2], m[3]));
+			}
+		});
+		red.operation(function() {
+			// apply lhs markup
+			for (var i = 0; i < marktext.length; ++i) {
+				var m = marktext[i];
+				if (m[0].doc.id != red.getDoc().id) continue;
+				self.chfns[self.id + '-rhs'].push(m[0].markText(m[1], m[2], m[3]));
+			}
+		});
+		this.trace('change', 'LCS markup time', timer.stop());
+		
+		// merge buttons
+		var ed = {lhs:led, rhs:red};
+		jQuery('.merge-button').on('click', function(ev){
+			// side of mouseenter
+			var side = 'rhs';
+			var oside = 'lhs';
+			var parent = jQuery(this).parents('#' + self.id + '-editor-lhs');
+			if (parent.length) {
+				side = 'lhs';
+				oside = 'rhs';
+			}
+			var pos = ed[side].coordsChar({left:ev.pageX, top:ev.pageY});
+
+			// get the change id
+			var cid = null;
+			var info = ed[side].lineInfo(pos.line);
+			jQuery.each(info.bgClass.split(' '), function(i, clazz) {
+				if (clazz.indexOf('cid-') == 0) {
+					cid = parseInt(clazz.split('-')[1], 10);
+					return false;
+				}
+			});
+			var change = self.changes[cid];
+
+			var line = {lhs: ed['lhs'].lineInfo(llt), rhs: ed['rhs'].lineInfo(rlt)};
+	
+			var text = ed[side].getRange(
+				CodeMirror.Pos(change[side + '-line-from'], 0),
+				CodeMirror.Pos(change[side + '-line-to'] + 1, 0));
+			
+			if (change['op'] == 'c') {
+				ed[oside].replaceRange(text,
+					CodeMirror.Pos(change[oside + '-line-from'], 0),
+					CodeMirror.Pos(change[oside + '-line-to'] + 1, 0));
+			}
+			else if (side == 'rhs') {
+				if (change['op'] == 'a') {
+					ed[oside].replaceRange(text,
+						CodeMirror.Pos(change[oside + '-line-from'] + 1, 0),
+						CodeMirror.Pos(change[oside + '-line-to'] + 1, 0));
+				}
+				else {// 'd'
+					var from = parseInt(change[oside + '-line-from']);
+					var to = parseInt(change[oside + '-line-to']);
+					for (var i = to; i >= from; --i) {
+						ed[oside].removeLine(i);
+					}
+				}
+			}
+			else if (side == 'lhs') {
+				if (change['op'] == 'a') {
+					var from = parseInt(change[oside + '-line-from']);
+					var to = parseInt(change[oside + '-line-to']);
+					for (var i = to; i >= from; --i) {
+						ed[oside].removeLine(i);
+					}
+				}
+				else {// 'd'
+					ed[oside].replaceRange( text,
+						CodeMirror.Pos(change[oside + '-line-from'] + 1, 0));
+				}
+			}
+			//reset
+			ed['lhs'].setValue(ed['lhs'].getValue());
+			ed['rhs'].setValue(ed['rhs'].getValue());
+			return false;
+		});
+		this.trace('change', 'markup buttons time', timer.stop());
+	},
+	_draw_info: function(editor_name1, editor_name2) {
+		var visible_page_height = jQuery(this.editor[editor_name1].getScrollerElement()).height();
+		var gutter_height = jQuery(this.editor[editor_name1].getScrollerElement()).children(':first-child').height();
+		var dcanvas = document.getElementById(editor_name1 + '-' + editor_name2 + '-canvas');
+		if (dcanvas == undefined) throw 'Failed to find: ' + editor_name1 + '-' + editor_name2 + '-canvas';
+		var clhs = jQuery('#' + this.id + '-lhs-margin');
+		var crhs = jQuery('#' + this.id + '-rhs-margin');
+		return {
+			visible_page_height: visible_page_height,
+			gutter_height: gutter_height,
+			visible_page_ratio: (visible_page_height / gutter_height),
+			margin_ratio: (visible_page_height / gutter_height),
+			lhs_scroller: jQuery(this.editor[editor_name1].getScrollerElement()),
+			rhs_scroller: jQuery(this.editor[editor_name2].getScrollerElement()),
+			lhs_lines: this.editor[editor_name1].lineCount(),
+			rhs_lines: this.editor[editor_name2].lineCount(),
+			dcanvas: dcanvas,
+			clhs: clhs,
+			crhs: crhs,
+			lhs_xyoffset: jQuery(clhs).offset(),
+			rhs_xyoffset: jQuery(crhs).offset()
+		};
+	},
+	_draw_diff: function(editor_name1, editor_name2, changes) {
+		var ex = this._draw_info(editor_name1, editor_name2);
+		var mcanvas_lhs = ex.clhs.get(0);
+		var mcanvas_rhs = ex.crhs.get(0);
+		var ctx = ex.dcanvas.getContext('2d');
+		var ctx_lhs = mcanvas_lhs.getContext('2d');
+		var ctx_rhs = mcanvas_rhs.getContext('2d');
+
+		this.trace('draw', 'visible_page_height', ex.visible_page_height);
+		this.trace('draw', 'gutter_height', ex.gutter_height);
+		this.trace('draw', 'visible_page_ratio', ex.visible_page_ratio);
+		this.trace('draw', 'lhs-scroller-top', ex.lhs_scroller.scrollTop());
+		this.trace('draw', 'rhs-scroller-top', ex.rhs_scroller.scrollTop());
+		
+		jQuery.each(jQuery.find('#' + this.id + ' canvas'), function () {
+			jQuery(this).get(0).height = ex.visible_page_height;
+		});
+		
+		ex.clhs.unbind('click');
+		ex.crhs.unbind('click');
+		
+		ctx_lhs.beginPath();
+		ctx_lhs.fillStyle = this.settings.bgcolor;
+		ctx_lhs.strokeStyle = '#888';
+		ctx_lhs.fillRect(0, 0, 6.5, ex.visible_page_height);
+		ctx_lhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
+
+		ctx_rhs.beginPath();
+		ctx_rhs.fillStyle = this.settings.bgcolor;
+		ctx_rhs.strokeStyle = '#888';
+		ctx_rhs.fillRect(0, 0, 6.5, ex.visible_page_height);
+		ctx_rhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
+
+		var vp = this._get_viewport(editor_name1, editor_name2);
+		for (var i = 0; i < changes.length; ++i) {
+			var change = changes[i];
+
+			this.trace('draw', change);
+			// margin indicators
+			var lhs_y_start = ((change['lhs-y-start'] + ex.lhs_scroller.scrollTop()) * ex.visible_page_ratio);
+			var lhs_y_end = ((change['lhs-y-end'] + ex.lhs_scroller.scrollTop()) * ex.visible_page_ratio) + 1;
+			var rhs_y_start = ((change['rhs-y-start'] + ex.rhs_scroller.scrollTop()) * ex.visible_page_ratio);
+			var rhs_y_end = ((change['rhs-y-end'] + ex.rhs_scroller.scrollTop()) * ex.visible_page_ratio) + 1;
+			this.trace('draw', 'marker calculated', lhs_y_start, lhs_y_end, rhs_y_start, rhs_y_end);
+
+			ctx_lhs.beginPath();
+			ctx_lhs.fillStyle = this.settings.fgcolor[change['op']];
+			ctx_lhs.strokeStyle = '#000';
+			ctx_lhs.lineWidth = 0.5;
+			ctx_lhs.fillRect(1.5, lhs_y_start, 4.5, Math.max(lhs_y_end - lhs_y_start, 5));
+			ctx_lhs.strokeRect(1.5, lhs_y_start, 4.5, Math.max(lhs_y_end - lhs_y_start, 5));
+
+			ctx_rhs.beginPath();
+			ctx_rhs.fillStyle = this.settings.fgcolor[change['op']];
+			ctx_rhs.strokeStyle = '#000';
+			ctx_rhs.lineWidth = 0.5;
+			ctx_rhs.fillRect(1.5, rhs_y_start, 4.5, Math.max(rhs_y_end - rhs_y_start, 5));
+			ctx_rhs.strokeRect(1.5, rhs_y_start, 4.5, Math.max(rhs_y_end - rhs_y_start, 5));
+			
+			if (!this._is_change_in_view(vp, change)) {
+				continue;
+			}
+			
+			lhs_y_start = change['lhs-y-start'];
+			lhs_y_end = change['lhs-y-end'];
+			rhs_y_start = change['rhs-y-start'];
+			rhs_y_end = change['rhs-y-end'];
+			
+			var radius = 3;
+			
+			// draw left box
+			ctx.beginPath();
+			ctx.strokeStyle = this.settings.fgcolor[change['op']];
+			ctx.lineWidth = 1;
+			
+			var rectWidth = this.draw_lhs_width;
+			var rectHeight = lhs_y_end - lhs_y_start - 1;
+			var rectX = this.draw_lhs_min;
+			var rectY = lhs_y_start;
+			// top and top top-right corner
+			
+			// draw left box
+			ctx.moveTo(rectX, rectY);
+			if (navigator.appName == 'Microsoft Internet Explorer') {
+				// IE arcs look awful
+				ctx.lineTo(this.draw_lhs_min + this.draw_lhs_width, lhs_y_start);
+				ctx.lineTo(this.draw_lhs_min + this.draw_lhs_width, lhs_y_end + 1);
+				ctx.lineTo(this.draw_lhs_min, lhs_y_end + 1);
+			}
+			else {
+				if (rectHeight <= 0) {
+					ctx.lineTo(rectX + rectWidth, rectY);
+				}
+				else {
+					ctx.arcTo(rectX + rectWidth, rectY, rectX + rectWidth, rectY + radius, radius);
+					ctx.arcTo(rectX + rectWidth, rectY + rectHeight, rectX + rectWidth - radius, rectY + rectHeight, radius);
+				}
+				// bottom line
+				ctx.lineTo(rectX, rectY + rectHeight);
+			}
+			ctx.stroke();
+			
+			rectWidth = this.draw_rhs_width;
+			rectHeight = rhs_y_end - rhs_y_start - 1;
+			rectX = this.draw_rhs_max;
+			rectY = rhs_y_start;
+
+			// draw right box
+			ctx.moveTo(rectX, rectY);
+			if (navigator.appName == 'Microsoft Internet Explorer') {
+				ctx.lineTo(this.draw_rhs_max - this.draw_rhs_width, rhs_y_start);
+				ctx.lineTo(this.draw_rhs_max - this.draw_rhs_width, rhs_y_end + 1);
+				ctx.lineTo(this.draw_rhs_max, rhs_y_end + 1);
+			}
+			else {
+				if (rectHeight <= 0) {
+					ctx.lineTo(rectX - rectWidth, rectY);
+				}
+				else {
+					ctx.arcTo(rectX - rectWidth, rectY, rectX - rectWidth, rectY + radius, radius);
+					ctx.arcTo(rectX - rectWidth, rectY + rectHeight, rectX - radius, rectY + rectHeight, radius);
+				}
+				ctx.lineTo(rectX, rectY + rectHeight);
+			}
+			ctx.stroke();
+			
+			// connect boxes
+			var cx = this.draw_lhs_min + this.draw_lhs_width;
+			var cy = lhs_y_start + (lhs_y_end + 1 - lhs_y_start) / 2.0;
+			var dx = this.draw_rhs_max - this.draw_rhs_width;
+			var dy = rhs_y_start + (rhs_y_end + 1 - rhs_y_start) / 2.0;
+			ctx.moveTo(cx, cy);
+			if (cy == dy) {
+				ctx.lineTo(dx, dy);
+			}
+			else {
+				// fancy!
+				ctx.bezierCurveTo(
+					cx + 12, cy - 3, // control-1 X,Y
+					dx - 12, dy - 3, // control-2 X,Y
+					dx, dy);
+			}
+			ctx.stroke();
+		}
+
+		// visible window feedback
+		ctx_lhs.fillStyle = this.settings.vpcolor;
+		ctx_rhs.fillStyle = this.settings.vpcolor;
+		
+		var lto = ex.clhs.height() * ex.visible_page_ratio;
+		var lfrom = (ex.lhs_scroller.scrollTop() / ex.gutter_height) * ex.clhs.height();
+		var rto = ex.crhs.height() * ex.visible_page_ratio;
+		var rfrom = (ex.rhs_scroller.scrollTop() / ex.gutter_height) * ex.crhs.height();
+		this.trace('draw', 'cls.height', ex.clhs.height());
+		this.trace('draw', 'lhs_scroller.scrollTop()', ex.lhs_scroller.scrollTop());
+		this.trace('draw', 'gutter_height', ex.gutter_height);
+		this.trace('draw', 'visible_page_ratio', ex.visible_page_ratio);
+		this.trace('draw', 'lhs from', lfrom, 'lhs to', lto);
+		this.trace('draw', 'rhs from', rfrom, 'rhs to', rto);
+		
+		ctx_lhs.fillRect(1.5, lfrom, 4.5, lto);
+		ctx_rhs.fillRect(1.5, rfrom, 4.5, rto);
+		
+		ex.clhs.click(function (ev) {
+			var y = ev.pageY - ex.lhs_xyoffset.top - (lto / 2);
+			var sto = Math.max(0, (y / mcanvas_lhs.height) * ex.lhs_scroller.get(0).scrollHeight);
+			ex.lhs_scroller.scrollTop(sto);
+		});
+		ex.crhs.click(function (ev) {
+			var y = ev.pageY - ex.rhs_xyoffset.top - (rto / 2);
+			var sto = Math.max(0, (y / mcanvas_rhs.height) * ex.rhs_scroller.get(0).scrollHeight);			
+			ex.rhs_scroller.scrollTop(sto);
+		});
+	},
+	trace: function(name) {
+		if(this.settings._debug.indexOf(name) >= 0) {
+			arguments[0] = name+':';
+			console.log([].slice.apply(arguments));
+		} 
+	}
+});
+
+jQuery.pluginMaker = function(plugin) {
+	// add the plugin function as a jQuery plugin
+	jQuery.fn[plugin.prototype.name] = function(options) {
+		// get the arguments 
+		var args = jQuery.makeArray(arguments),
+		after = args.slice(1);
+		var rc = undefined;
+		this.each(function() {
+			// see if we have an instance
+			var instance = jQuery.data(this, plugin.prototype.name);
+			if (instance) {
+				// call a method on the instance
+				if (typeof options == "string") {
+					rc = instance[options].apply(instance, after);
+				} else if (instance.update) {
+					// call update on the instance
+					return instance.update.apply(instance, args);
+				}
+			} else {
+				// create the plugin
+				new plugin(this, options);
+			}
+		});
+		if (rc != undefined) return rc;
+	};
+};
+
+// make the mergely widget
+jQuery.pluginMaker(Mgly.mergely);
--- a/rhodecode/public/js/mergerly.js	Mon May 26 17:56:53 2014 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1460 +0,0 @@
-Mgly = {};
-
-Mgly.Timer = function(){
-	var self = this;
-	self.start = function() { self.t0 = new Date().getTime(); }
-	self.stop = function() {
-		var t1 = new Date().getTime();
-		var d = t1 - self.t0;
-		self.t0 = t1;
-		return d;
-	}
-	self.start();
-}
-
-Mgly.ChangeExpression = new RegExp(/(\d+(?:,\d+)?)([acd])(\d+(?:,\d+)?)/);
-
-Mgly.DiffParser = function(diff) {
-	var changes = [];
-	var change_id = 0;
-	// parse diff
-	var diff_lines = diff.split(/\n/);
-	for (var i = 0; i < diff_lines.length; ++i) {
-		if (diff_lines[i].length == 0) continue;
-		var change = {};
-		var test = Mgly.ChangeExpression.exec(diff_lines[i]);
-		if (test == null) continue;
-		// lines are zero-based
-		var fr = test[1].split(',');
-		change['lhs-line-from'] = fr[0] - 1;
-		if (fr.length == 1) change['lhs-line-to'] = fr[0] - 1;
-		else change['lhs-line-to'] = fr[1] - 1;
-		var to = test[3].split(',');
-		change['rhs-line-from'] = to[0] - 1;
-		if (to.length == 1) change['rhs-line-to'] = to[0] - 1;
-		else change['rhs-line-to'] = to[1] - 1;
-		change['op'] = test[2];
-		changes[change_id++] = change;
-	}
-	return changes;
-}
-
-Mgly.sizeOf = function(obj) {
-	var size = 0, key;
-	for (key in obj) {
-		if (obj.hasOwnProperty(key)) size++;
-	}
-	return size;
-}
-
-Mgly.LCS = function(x, y) {
-	this.x = x.replace(/[ ]{1}/g, '\n');
-	this.y = y.replace(/[ ]{1}/g, '\n');
-}
-jQuery.extend(Mgly.LCS.prototype, {
-	clear: function() { this.ready = 0; },
-	diff: function(added, removed) {
-		var d = new Mgly.diff(this.x, this.y, retain_lines = true, ignore_ws = false);
-		var changes = Mgly.DiffParser(d.normal_form());
-		var li = 0, lj = 0;
-		for (var i = 0; i < changes.length; ++i) {
-			var change = changes[i];
-			if (change.op != 'a') {
-				// find the starting index of the line
-				li = d.lhs_lines.slice(0, change['lhs-line-from']).join(' ').length;
-				// get the index of the the span of the change
-				lj = change['lhs-line-to'] + 1;
-				// get the changed text
-				var lchange = d.lhs_lines.slice(change['lhs-line-from'], lj).join(' ');
-				if (change.op == 'd') lchange += ' ';// include the leading space
-				else if (li > 0 && change.op == 'c') li += 1; // ignore leading space if not first word
-				// output the changed index and text
-				removed(li, li + lchange.length);
-			}
-			if (change.op != 'd') {
-				// find the starting index of the line
-				li = d.rhs_lines.slice(0, change['rhs-line-from']).join(' ').length;
-				// get the index of the the span of the change
-				lj = change['rhs-line-to'] + 1;
-				// get the changed text
-				var rchange = d.rhs_lines.slice(change['rhs-line-from'], lj).join(' ');
-				if (change.op == 'a') rchange += ' ';// include the leading space
-				else if (li > 0 && change.op == 'c') li += 1; // ignore leading space if not first word
-				// output the changed index and text
-				added(li, li + rchange.length);
-			}
-		}
-	}
-});
-Mgly.diff = function(lhs, rhs, retain_lines, ignore_ws) {
-	this.diff_codes = {};
-	this.max_code = 0;
-	var lhs_lines = lhs.split('\n');
-	var rhs_lines = rhs.split('\n');
-	if (lhs.length == 0) lhs_lines = [];
-	if (rhs.length == 0) rhs_lines = [];
-
-	var lhs_data = new Object();
-	lhs_data.data = this._diff_codes(lhs_lines, ignore_ws);
-	lhs_data.modified = {};
-	lhs_data.length = Mgly.sizeOf(lhs_data.data);
-
-	var rhs_data = new Object();
-	rhs_data.data = this._diff_codes(rhs_lines, ignore_ws);
-	rhs_data.modified = {};
-	rhs_data.length = Mgly.sizeOf(rhs_data.data);
-
-	var max = (lhs_data.length + rhs_data.length + 1);
-	var vector_d = Array( 2 * max + 2 );
-	var vector_u = Array( 2 * max + 2 );
-
-	this._lcs(lhs_data, 0, lhs_data.length, rhs_data, 0, rhs_data.length, vector_u, vector_d);
-	this._optimize(lhs_data);
-	this._optimize(rhs_data);
-	this.items = this._create_diffs(lhs_data, rhs_data);
-	if (retain_lines) {
-		this.lhs_lines = lhs_lines;
-		this.rhs_lines = rhs_lines;
-	}
-};
-jQuery.extend(Mgly.diff.prototype, {
-	changes: function() { return this.items; },
-	normal_form: function() {
-		var nf = '';
-		for (var index = 0; index < this.items.length; ++index) {
-			var item = this.items[index];
-			var lhs_str = '';
-			var rhs_str = '';
-			var change = 'c';
-			if (item.lhs_deleted_count == 0 && item.rhs_inserted_count > 0) change = 'a';
-			else if (item.lhs_deleted_count > 0 && item.rhs_inserted_count == 0) change = 'd';
-
-			if (item.lhs_deleted_count == 1) lhs_str = item.lhs_start + 1;
-			else if (item.lhs_deleted_count == 0) lhs_str = item.lhs_start;
-			else lhs_str = (item.lhs_start + 1) + ',' + (item.lhs_start + item.lhs_deleted_count);
-
-			if (item.rhs_inserted_count == 1) rhs_str = item.rhs_start + 1;
-			else if (item.rhs_inserted_count == 0) rhs_str = item.rhs_start;
-			else rhs_str = (item.rhs_start + 1) + ',' + (item.rhs_start + item.rhs_inserted_count);
-			nf += lhs_str + change + rhs_str + '\n';
-			if (this.rhs_lines && this.lhs_lines) {
-				// if rhs/lhs lines have been retained, output contextual diff
-				for (var i = item.lhs_start; i < item.lhs_start + item.lhs_deleted_count; ++i) {
-					nf += '< ' + this.lhs_lines[i] + '\n';
-				}
-				if (item.rhs_inserted_count && item.lhs_deleted_count) nf += '---\n';
-				for (var i = item.rhs_start; i < item.rhs_start + item.rhs_inserted_count; ++i) {
-					nf += '> ' + this.rhs_lines[i] + '\n';
-				}
-			}
-		}
-		return nf;
-	},
-	_diff_codes: function(lines, ignore_ws) {
-		var code = this.max_code;
-		var codes = {};
-		for (var i = 0; i < lines.length; ++i) {
-			var line = lines[i];
-			if (ignore_ws) {
-				line = line.replace(/\s+/g, '');
-			}
-			var aCode = this.diff_codes[line];
-			if (aCode != undefined) {
-				codes[i] = aCode;
-			}
-			else {
-				this.max_code++;
-				this.diff_codes[line] = this.max_code;
-				codes[i] = this.max_code;
-			}
-		}
-		return codes;
-	},
-	_lcs: function(lhs, lhs_lower, lhs_upper, rhs, rhs_lower, rhs_upper, vector_u, vector_d) {
-		while ( (lhs_lower < lhs_upper) && (rhs_lower < rhs_upper) && (lhs.data[lhs_lower] == rhs.data[rhs_lower]) ) {
-			++lhs_lower;
-			++rhs_lower;
-		}
-		while ( (lhs_lower < lhs_upper) && (rhs_lower < rhs_upper) && (lhs.data[lhs_upper - 1] == rhs.data[rhs_upper - 1]) ) {
-			--lhs_upper;
-			--rhs_upper;
-		}
-		if (lhs_lower == lhs_upper) {
-			while (rhs_lower < rhs_upper) {
-				rhs.modified[ rhs_lower++ ] = true;
-			}
-		}
-		else if (rhs_lower == rhs_upper) {
-			while (lhs_lower < lhs_upper) {
-				lhs.modified[ lhs_lower++ ] = true;
-			}
-		}
-		else {
-			var sms = this._sms(lhs, lhs_lower, lhs_upper, rhs, rhs_lower, rhs_upper, vector_u, vector_d);
-			this._lcs(lhs, lhs_lower, sms.x, rhs, rhs_lower, sms.y, vector_u, vector_d);
-			this._lcs(lhs, sms.x, lhs_upper, rhs, sms.y, rhs_upper, vector_u, vector_d);
-		}
-	},
-	_sms: function(lhs, lhs_lower, lhs_upper, rhs, rhs_lower, rhs_upper, vector_u, vector_d) {
-		var max = lhs.length + rhs.length + 1;
-		var kdown = lhs_lower - rhs_lower;
-		var kup = lhs_upper - rhs_upper;
-		var delta = (lhs_upper - lhs_lower) - (rhs_upper - rhs_lower);
-		var odd = (delta & 1) != 0;
-		var offset_down = max - kdown;
-		var offset_up = max - kup;
-		var maxd = ((lhs_upper - lhs_lower + rhs_upper - rhs_lower) / 2) + 1;
-		vector_d[ offset_down + kdown + 1 ] = lhs_lower;
-		vector_u[ offset_up + kup - 1 ] = lhs_upper;
-		var ret = {x:0,y:0};
-		for (var d = 0; d <= maxd; ++d) {
-			for (var k = kdown - d; k <= kdown + d; k += 2) {
-				var x, y;
-				if (k == kdown - d) {
-					x = vector_d[ offset_down + k + 1 ];//down
-				}
-				else {
-					x = vector_d[ offset_down + k - 1 ] + 1;//right
-					if ((k < (kdown + d)) && (vector_d[ offset_down + k + 1 ] >= x)) {
-						x = vector_d[ offset_down + k + 1 ];//down
-					}
-				}
-				y = x - k;
-				// find the end of the furthest reaching forward D-path in diagonal k.
-				while ((x < lhs_upper) && (y < rhs_upper) && (lhs.data[x] == rhs.data[y])) {
-					x++; y++;
-				}
-				vector_d[ offset_down + k ] = x;
-				// overlap ?
-				if (odd && (kup - d < k) && (k < kup + d)) {
-					if (vector_u[offset_up + k] <= vector_d[offset_down + k]) {
-						ret.x = vector_d[offset_down + k];
-						ret.y = vector_d[offset_down + k] - k;
-						return (ret);
-					}
-				}
-			}
-			// Extend the reverse path.
-			for (var k = kup - d; k <= kup + d; k += 2) {
-				// find the only or better starting point
-				var x, y;
-				if (k == kup + d) {
-					x = vector_u[offset_up + k - 1]; // up
-				} else {
-					x = vector_u[offset_up + k + 1] - 1; // left
-					if ((k > kup - d) && (vector_u[offset_up + k - 1] < x))
-						x = vector_u[offset_up + k - 1]; // up
-				}
-				y = x - k;
-				while ((x > lhs_lower) && (y > rhs_lower) && (lhs.data[x - 1] == rhs.data[y - 1])) {
-					// diagonal
-					x--;
-					y--;
-				}
-				vector_u[offset_up + k] = x;
-				// overlap ?
-				if (!odd && (kdown - d <= k) && (k <= kdown + d)) {
-					if (vector_u[offset_up + k] <= vector_d[offset_down + k]) {
-						ret.x = vector_d[offset_down + k];
-						ret.y = vector_d[offset_down + k] - k;
-						return (ret);
-					}
-				}
-			}
-		}
-		throw "the algorithm should never come here.";
-	},
-	_optimize: function(data) {
-		var start = 0, end = 0;
-		while (start < data.length) {
-			while ((start < data.length) && (data.modified[start] == undefined || data.modified[start] == false)) {
-				start++;
-			}
-			end = start;
-			while ((end < data.length) && (data.modified[end] == true)) {
-				end++;
-			}
-			if ((end < data.length) && (data.data[start] == data.data[end])) {
-				data.modified[start] = false;
-				data.modified[end] = true;
-			}
-			else {
-				start = end;
-			}
-		}
-	},
-	_create_diffs: function(lhs_data, rhs_data) {
-		var items = [];
-		var lhs_start = 0, rhs_start = 0;
-		var lhs_line = 0, rhs_line = 0;
-
-		while (lhs_line < lhs_data.length || rhs_line < rhs_data.length) {
-			if ((lhs_line < lhs_data.length) && (!lhs_data.modified[lhs_line])
-				&& (rhs_line < rhs_data.length) && (!rhs_data.modified[rhs_line])) {
-				// equal lines
-				lhs_line++;
-				rhs_line++;
-			}
-			else {
-				// maybe deleted and/or inserted lines
-				lhs_start = lhs_line;
-				rhs_start = rhs_line;
-
-				while (lhs_line < lhs_data.length && (rhs_line >= rhs_data.length || lhs_data.modified[lhs_line]))
-					lhs_line++;
-
-				while (rhs_line < rhs_data.length && (lhs_line >= lhs_data.length || rhs_data.modified[rhs_line]))
-					rhs_line++;
-
-				if ((lhs_start < lhs_line) || (rhs_start < rhs_line)) {
-					// store a new difference-item
-					var aItem = new Object();
-					aItem.lhs_start = lhs_start;
-					aItem.rhs_start = rhs_start;
-					aItem.lhs_deleted_count = lhs_line - lhs_start;
-					aItem.rhs_inserted_count = rhs_line - rhs_start;
-					items.push(aItem);
-				}
-			}
-		}
-		return items;
-	}
-});
-
-Mgly.mergely = function(el, options) {
-	CodeMirror.defineExtension('centerOnCursor', function() {
-		var coords = this.cursorCoords(null, 'local');
-		this.scrollTo(null,
-			(coords.y + coords.yBot) / 2 - (this.getScrollerElement().clientHeight / 2));
-	});
-
-	if (el) {
-		this.init(el, options);
-	}
-};
-
-jQuery.extend(Mgly.mergely.prototype, {
-	name: 'mergely',
-	//http://jupiterjs.com/news/writing-the-perfect-jquery-plugin
-	init: function(el, options) {
-		this.settings = {
-			autoupdate: true,
-			autoresize: true,
-			rhs_margin: 'right',
-			lcs: true,
-			sidebar: true,
-			viewport: false,
-			ignorews: false,
-			fadein: 'fast',
-			editor_width: '400px',
-			editor_height: '400px',
-			resize_timeout: 500,
-			change_timeout: 150,
-			fgcolor: {a:'#4ba3fa',c:'#a3a3a3',d:'#ff7f7f'},
-			bgcolor: '#eee',
-			vpcolor: 'rgba(0, 0, 200, 0.2)',
-			lhs: function(setValue) { },
-			rhs: function(setValue) { },
-			loaded: function() { },
-			//_auto_height: function(h) { return h - 20; },
-			_auto_width: function(w) { return w; },
-			resize: function(init) {
-				var scrollbar = init ? -15 : 0;
-				var w = jQuery(el).parent().width() + scrollbar;
-				if (this.width == 'auto') {
-					w = this._auto_width(w);
-				}
-				else {
-					w = this.width;
-					this.editor_width = w;
-				}
-				if (this.height == 'auto') {
-					//h = this._auto_height(h);
-					h = jQuery(el).parent().height();
-				}
-				else {
-					h = this.height;
-					this.editor_height = h;
-				}
-				var content_width = w / 2.0 - 2 * 8 - 8;
-				var content_height = h;
-				var self = jQuery(el);
-				self.find('.mergely-column').css({ width: content_width + 'px' });
-				self.find('.mergely-column, .mergely-canvas, .mergely-margin, .mergely-column textarea, .CodeMirror-scroll, .cm-s-default').css({ height: content_height + 'px' });
-				self.find('.mergely-canvas').css({ height: content_height + 'px' });
-				self.find('.mergely-column textarea').css({ width: content_width + 'px' });
-				self.css({ width: w, height: h, clear: 'both' });
-				if (self.css('display') == 'none') {
-					if (this.fadein != false) self.fadeIn(this.fadein);
-					else self.show();
-					if (this.loaded) this.loaded();
-				}
-				if (this.resized) this.resized();
-			},
-			_debug: '', //scroll,draw,calc,diff,markup,change
-			resized: function() { }
-		};
-		var cmsettings = {
-			mode: 'text/plain',
-			readOnly: false,
-			lineWrapping: false,
-			lineNumbers: true,
-			gutters: ['merge', 'CodeMirror-linenumbers']
-		}
-		this.lhs_cmsettings = {};
-		this.rhs_cmsettings = {};
-
-		// save this element for faster queries
-		this.element = jQuery(el);
-
-		// save options if there are any
-		if (options && options.cmsettings) jQuery.extend(this.lhs_cmsettings, cmsettings, options.cmsettings, options.lhs_cmsettings);
-		if (options && options.cmsettings) jQuery.extend(this.rhs_cmsettings, cmsettings, options.cmsettings, options.rhs_cmsettings);
-		if (options) jQuery.extend(this.settings, options);
-
-		// bind if the element is destroyed
-		this.element.bind('destroyed', jQuery.proxy(this.teardown, this));
-
-		// save this instance in jQuery data
-		jQuery.data(el, this.name, this);
-
-		this._setup(el);
-	},
-	// bind events to this instance's methods
-	bind: function() {
-		var rhstx = jQuery('#' + this.id + '-rhs').get(0);
-		if (!rhstx) {
-			console.error('rhs textarea not defined - Mergely not initialized properly');
-			return;
-		}
-		var lhstx = jQuery('#' + this.id + '-lhs').get(0);
-		if (!rhstx) {
-			console.error('lhs textarea not defined - Mergely not initialized properly');
-			return;
-		}
-		var self = this;
-		this.editor = [];
-
-		this.editor[this.id + '-lhs'] = CodeMirror.fromTextArea(lhstx, this.lhs_cmsettings);
-		this.editor[this.id + '-rhs'] = CodeMirror.fromTextArea(rhstx, this.rhs_cmsettings);
-		this.editor[this.id + '-lhs'].on('change', function(){ if (self.settings.autoupdate) self._changing(self.id + '-lhs', self.id + '-rhs'); });
-		this.editor[this.id + '-lhs'].on('scroll', function(){ self._scrolling(self.id + '-lhs'); });
-		this.editor[this.id + '-rhs'].on('change', function(){ if (self.settings.autoupdate) self._changing(self.id + '-lhs', self.id + '-rhs'); });
-		this.editor[this.id + '-rhs'].on('scroll', function(){ self._scrolling(self.id + '-rhs'); });
-
-		// resize
-		if (this.settings.autoresize) {
-			var sz_timeout1 = null;
-			var sz = function(init) {
-				//self.em_height = null; //recalculate
-				if (self.settings.resize) self.settings.resize(init);
-				self.editor[self.id + '-lhs'].refresh();
-				self.editor[self.id + '-rhs'].refresh();
-				if (self.settings.autoupdate) {
-					self._changing(self.id + '-lhs', self.id + '-rhs');
-				}
-			}
-			jQuery(window).resize(
-				function () {
-					if (sz_timeout1) clearTimeout(sz_timeout1);
-					sz_timeout1 = setTimeout(sz, self.settings.resize_timeout);
-				}
-			);
-			sz(true);
-		}
-	},
-	unbind: function() {
-		if (this.changed_timeout != null) clearTimeout(this.changed_timeout);
-		this.editor[this.id + '-lhs'].toTextArea();
-		this.editor[this.id + '-rhs'].toTextArea();
-	},
-	destroy: function() {
-		this.element.unbind('destroyed', this.teardown);
-		this.teardown();
-	},
-	teardown: function() {
-		this.unbind();
-	},
-	lhs: function(text) {
-		this.editor[this.id + '-lhs'].setValue(text);
-	},
-	rhs: function(text) {
-		this.editor[this.id + '-rhs'].setValue(text);
-	},
-	update: function() {
-		this._changing(this.id + '-lhs', this.id + '-rhs');
-	},
-	unmarkup: function() {
-		this._clear();
-	},
-	scrollTo: function(side, num) {
-		var le = this.editor[this.id + '-lhs'];
-		var re = this.editor[this.id + '-rhs'];
-		if (side == 'lhs') {
-			le.setCursor(num);
-			le.centerOnCursor();
-		}
-		else {
-			re.setCursor(num);
-			re.centerOnCursor();
-		}
-	},
-	options: function(opts) {
-		if (opts) {
-			jQuery.extend(this.settings, opts);
-			if (opts.autoresize) this.resize();
-			if (opts.autoupdate) this.update();
-			if (opts.hasOwnProperty('rhs_margin')) {
-				// dynamically swap the margin
-				if (opts.rhs_margin == 'left') {
-					this.element.find('.mergely-margin:last-child').insertAfter(
-						this.element.find('.mergely-canvas'));
-				}
-				else {
-					var target = this.element.find('.mergely-margin').last();
-					target.appendTo(target.parent());
-				}
-			}
-			if (opts.hasOwnProperty('sidebar')) {
-				// dynamically enable sidebars
-				if (opts.sidebar) {
-					jQuery(this.element).find('.mergely-margin').css({display: 'block'});
-				}
-				else {
-					jQuery(this.element).find('.mergely-margin').css({display: 'none'});
-				}
-			}
-		}
-		else {
-			return this.settings;
-		}
-	},
-	swap: function() {
-		if (this.lhs_cmsettings.readOnly || this.rhs_cmsettings.readOnly) return;
-		var le = this.editor[this.id + '-lhs'];
-		var re = this.editor[this.id + '-rhs'];
-		var tmp = re.getValue();
-		re.setValue(le.getValue());
-		le.setValue(tmp);
-	},
-	merge: function(side) {
-		var le = this.editor[this.id + '-lhs'];
-		var re = this.editor[this.id + '-rhs'];
-		if (side == 'lhs' && !this.lhs_cmsettings.readOnly) le.setValue(re.getValue());
-		else if (!this.rhs_cmsettings.readOnly) re.setValue(le.getValue());
-	},
-	get: function(side) {
-		var ed = this.editor[this.id + '-' + side];
-		var t = ed.getValue();
-		if (t == undefined) return '';
-		return t;
-	},
-	clear: function(side) {
-		if (side == 'lhs' && this.lhs_cmsettings.readOnly) return;
-		if (side == 'rhs' && this.rhs_cmsettings.readOnly) return;
-		var ed = this.editor[this.id + '-' + side];
-		ed.setValue('');
-	},
-	cm: function(side) {
-		return this.editor[this.id + '-' + side];
-	},
-	search: function(side, query, direction) {
-		var le = this.editor[this.id + '-lhs'];
-		var re = this.editor[this.id + '-rhs'];
-		var editor;
-		if (side == 'lhs') editor = le;
-		else editor = re;
-		direction = (direction == 'prev') ? 'findPrevious' : 'findNext';
-		if ((editor.getSelection().length == 0) || (this.prev_query[side] != query)) {
-			this.cursor[this.id] = editor.getSearchCursor(query, { line: 0, ch: 0 }, false);
-			this.prev_query[side] = query;
-		}
-		var cursor = this.cursor[this.id];
-
-		if (cursor[direction]()) {
-			editor.setSelection(cursor.from(), cursor.to());
-		}
-		else {
-			cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, false);
-		}
-	},
-	resize: function() {
-		this.settings.resize();
-		this._changing(this.id + '-lhs', this.id + '-rhs');
-	},
-	diff: function() {
-		var lhs = this.editor[this.id + '-lhs'].getValue();
-		var rhs = this.editor[this.id + '-rhs'].getValue();
-		var d = new Mgly.diff(lhs, rhs, retain_lines = true, ignore_ws = this.settings.ignorews);
-		return d.normal_form();
-	},
-	_setup: function(el) {
-		jQuery(this.element).hide();//hide
-		this.id = jQuery(el).attr('id');
-		var height = this.settings.editor_height;
-		var width = this.settings.editor_width;
-		this.changed_timeout = null;
-		this.chfns = {};
-		this.chfns[this.id + '-lhs'] = [];
-		this.chfns[this.id + '-rhs'] = [];
-		this.prev_query = [];
-		this.cursor = [];
-		this._skipscroll = {};
-		this.change_exp = new RegExp(/(\d+(?:,\d+)?)([acd])(\d+(?:,\d+)?)/);
-		var merge_lhs_button;
-		var merge_rhs_button;
-		if (jQuery.button != undefined) {
-			//jquery ui
-			merge_lhs_button = '<button title="Merge left"></button>';
-			merge_rhs_button = '<button title="Merge right"></button>';
-		}
-		else {
-			// homebrew
-			var style = 'opacity:0.4;width:10px;height:15px;background-color:#888;cursor:pointer;text-align:center;color:#eee;border:1px solid: #222;margin-right:5px;';
-			merge_lhs_button = '<div style="' + style + '" title="Merge left">&lt;</div>';
-			merge_rhs_button = '<div style="' + style + '" title="Merge right">&gt;</div>';
-		}
-		this.merge_rhs_button = jQuery(merge_rhs_button);
-		this.merge_lhs_button = jQuery(merge_lhs_button);
-
-		// create the textarea and canvas elements
-		jQuery(this.element).append(jQuery('<div class="mergely-margin" style="height: ' + height + '"><canvas id="' + this.id + '-lhs-margin" width="8px" height="' + height + '"></canvas></div>'));
-		jQuery(this.element).append(jQuery('<div style="position:relative;width:' + width + '; height:' + height + '" id="' + this.id + '-editor-lhs" class="mergely-column"><textarea style="" id="' + this.id + '-lhs"></textarea></div>'));
-		jQuery(this.element).append(jQuery('<div class="mergely-canvas" style="height: ' + height + '"><canvas id="' + this.id + '-lhs-' + this.id + '-rhs-canvas" style="width:28px" width="28px" height="' + height + '"></canvas></div>'));
-		var rmargin = jQuery('<div class="mergely-margin" style="height: ' + height + '"><canvas id="' + this.id + '-rhs-margin" width="8px" height="' + height + '"></canvas></div>');
-		if (!this.settings.sidebar) {
-			jQuery(this.element).find('.mergely-margin').css({display: 'none'});
-		}
-		if (this.settings.rhs_margin == 'left') {
-			jQuery(this.element).append(rmargin);
-		}
-		jQuery(this.element).append(jQuery('<div style="width:' + width + '; height:' + height + '" id="' + this.id + '-editor-rhs" class="mergely-column"><textarea style="" id="' + this.id + '-rhs"></textarea></div>'));
-		if (this.settings.rhs_margin != 'left') {
-			jQuery(this.element).append(rmargin);
-		}
-		//codemirror
-		var cmstyle = '#' + this.id + ' .CodeMirror-gutter-text { padding: 5px 0 0 0; }' +
-			'#' + this.id + ' .CodeMirror-lines pre, ' + '#' + this.id + ' .CodeMirror-gutter-text pre { line-height: 18px; }' +
-			'.CodeMirror-linewidget { overflow: hidden; };';
-		if (this.settings.autoresize) {
-			cmstyle += this.id + ' .CodeMirror-scroll { height: 100%; overflow: auto; }';
-		}
-		jQuery('<style type="text/css">' + cmstyle + '</style>').appendTo('head');
-		this.bind();
-		if (this.settings.lhs) {
-			var setv = this.editor[this.id + '-lhs'].getDoc().setValue;
-			this.settings.lhs(setv.bind(this.editor[this.id + '-lhs'].getDoc()));
-		}
-		if (this.settings.rhs) {
-			var setv = this.editor[this.id + '-rhs'].getDoc().setValue;
-			this.settings.rhs(setv.bind(this.editor[this.id + '-rhs'].getDoc()));
-		}
-	},
-
-	_scrolling: function(editor_name) {
-		if (this._skipscroll[editor_name] === true) {
-			// scrolling one side causes the other to event - ignore it
-			this._skipscroll[editor_name] = false;
-			return;
-		}
-		var scroller = jQuery(this.editor[editor_name].getScrollerElement());
-		if (this.midway == undefined) {
-			this.midway = (scroller.height() / 2.0 + scroller.offset().top).toFixed(2);
-		}
-		// balance-line
-		var midline = this.editor[editor_name].coordsChar({left:0, top:this.midway});
-		var top_to = scroller.scrollTop();
-		var left_to = scroller.scrollLeft();
-
-		this.trace('scroll', 'side', editor_name);
-		this.trace('scroll', 'midway', this.midway);
-		this.trace('scroll', 'midline', midline);
-		this.trace('scroll', 'top_to', top_to);
-		this.trace('scroll', 'left_to', left_to);
-
-		var editor_name1 = this.id + '-lhs';
-		var editor_name2 = this.id + '-rhs';
-
-		for (var name in this.editor) {
-			if (!this.editor.hasOwnProperty(name)) continue;
-			if (editor_name == name) continue; //same editor
-			var this_side = editor_name.replace(this.id + '-', '');
-			var other_side = name.replace(this.id + '-', '');
-			var top_adjust = 0;
-
-			// find the last change that is less than or within the midway point
-			// do not move the rhs until the lhs end point is >= the rhs end point.
-			var last_change = null;
-			var force_scroll = false;
-			for (var i = 0; i < this.changes.length; ++i) {
-				var change = this.changes[i];
-				if ((midline.line >= change[this_side+'-line-from'])) {
-					last_change = change;
-					if (midline.line >= last_change[this_side+'-line-to']) {
-						if (!change.hasOwnProperty(this_side+'-y-start') ||
-							!change.hasOwnProperty(this_side+'-y-end') ||
-							!change.hasOwnProperty(other_side+'-y-start') ||
-							!change.hasOwnProperty(other_side+'-y-end')){
-							// change outside of viewport
-							force_scroll = true;
-						}
-						else {
-							top_adjust +=
-								(change[this_side+'-y-end'] - change[this_side+'-y-start']) -
-								(change[other_side+'-y-end'] - change[other_side+'-y-start']);
-						}
-					}
-				}
-			}
-
-			var vp = this.editor[name].getViewport();
-			var scroll = true;
-			if (last_change) {
-				this.trace('scroll', 'last change before midline', last_change);
-				if (midline.line >= vp.from && midline <= vp.to) {
-					scroll = false;
-				}
-			}
-			this.trace('scroll', 'scroll', scroll);
-			if (scroll || force_scroll) {
-				// scroll the other side
-				this.trace('scroll', 'scrolling other side', top_to - top_adjust);
-				var scroller = jQuery(this.editor[name].getScrollerElement());
-				this._skipscroll[name] = true;//disable next event
-				scroller.scrollTop(top_to - top_adjust).scrollLeft(left_to);
-			}
-			else this.trace('scroll', 'not scrolling other side');
-
-			if (this.settings.autoupdate) {
-				var timer = new Mgly.Timer();
-				this._calculate_offsets(editor_name1, editor_name2, this.changes);
-				this.trace('change', 'offsets time', timer.stop());
-				this._markup_changes(editor_name1, editor_name2, this.changes);
-				this.trace('change', 'markup time', timer.stop());
-				this._draw_diff(editor_name1, editor_name2, this.changes);
-				this.trace('change', 'draw time', timer.stop());
-			}
-			this.trace('scroll', 'scrolled');
-		}
-	},
-	_changing: function(editor_name1, editor_name2) {
-		this.trace('change', 'changing-timeout', this.changed_timeout);
-		var self = this;
-		if (this.changed_timeout != null) clearTimeout(this.changed_timeout);
-		this.changed_timeout = setTimeout(function(){
-			var timer = new Mgly.Timer();
-			self._changed(editor_name1, editor_name2);
-			self.trace('change', 'total time', timer.stop());
-		}, this.settings.change_timeout);
-	},
-	_changed: function(editor_name1, editor_name2) {
-		this._clear();
-		this._diff(editor_name1, editor_name2);
-	},
-	_clear: function() {
-		var self = this;
-		for (var name in this.editor) {
-			if (!this.editor.hasOwnProperty(name)) continue;
-			var editor = this.editor[name];
-			var fns = self.chfns[name];
-			// clear editor changes
-			editor.operation(function() {
-				var timer = new Mgly.Timer();
-				for (var i = 0, l = editor.lineCount(); i < l; ++i) {
-					editor.removeLineClass(i, 'background');
-				}
-				for (var i = 0; i < fns.length; ++i) {
-					//var edid = editor.getDoc().id;
-					var change = fns[i];
-					//if (change.doc.id != edid) continue;
-					if (change.lines.length) {
-						self.trace('change', 'clear text', change.lines[0].text);
-					}
-					change.clear();
-				}
-				editor.clearGutter('merge');
-				self.trace('change', 'clear time', timer.stop());
-			});
-		}
-		self.chfns[name] = [];
-
-		var ex = this._draw_info(this.id + '-lhs', this.id + '-rhs');
-		var ctx_lhs = ex.clhs.get(0).getContext('2d');
-		var ctx_rhs = ex.crhs.get(0).getContext('2d');
-		var ctx = ex.dcanvas.getContext('2d');
-
-		ctx_lhs.beginPath();
-		ctx_lhs.fillStyle = this.settings.bgcolor;
-		ctx_lhs.strokeStyle = '#888';
-		ctx_lhs.fillRect(0, 0, 6.5, ex.visible_page_height);
-		ctx_lhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
-
-		ctx_rhs.beginPath();
-		ctx_rhs.fillStyle = this.settings.bgcolor;
-		ctx_rhs.strokeStyle = '#888';
-		ctx_rhs.fillRect(0, 0, 6.5, ex.visible_page_height);
-		ctx_rhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
-
-		ctx.beginPath();
-		ctx.fillStyle = '#fff';
-		ctx.fillRect(0, 0, this.draw_mid_width, ex.visible_page_height);
-	},
-	_diff: function(editor_name1, editor_name2) {
-		var lhs = this.editor[editor_name1].getValue();
-		var rhs = this.editor[editor_name2].getValue();
-		var timer = new Mgly.Timer();
-		var d = new Mgly.diff(lhs, rhs, false, this.settings.ignorews);
-		this.trace('change', 'diff time', timer.stop());
-		this.changes = Mgly.DiffParser(d.normal_form());
-		this.trace('change', 'parse time', timer.stop());
-		this._calculate_offsets(editor_name1, editor_name2, this.changes);
-		this.trace('change', 'offsets time', timer.stop());
-		this._markup_changes(editor_name1, editor_name2, this.changes);
-		this.trace('change', 'markup time', timer.stop());
-		this._draw_diff(editor_name1, editor_name2, this.changes);
-		this.trace('change', 'draw time', timer.stop());
-	},
-	_parse_diff: function (editor_name1, editor_name2, diff) {
-		this.trace('diff', 'diff results:\n', diff);
-		var changes = [];
-		var change_id = 0;
-		// parse diff
-		var diff_lines = diff.split(/\n/);
-		for (var i = 0; i < diff_lines.length; ++i) {
-			if (diff_lines[i].length == 0) continue;
-			var change = {};
-			var test = this.change_exp.exec(diff_lines[i]);
-			if (test == null) continue;
-			// lines are zero-based
-			var fr = test[1].split(',');
-			change['lhs-line-from'] = fr[0] - 1;
-			if (fr.length == 1) change['lhs-line-to'] = fr[0] - 1;
-			else change['lhs-line-to'] = fr[1] - 1;
-			var to = test[3].split(',');
-			change['rhs-line-from'] = to[0] - 1;
-			if (to.length == 1) change['rhs-line-to'] = to[0] - 1;
-			else change['rhs-line-to'] = to[1] - 1;
-			// TODO: optimize for changes that are adds/removes
-			if (change['lhs-line-from'] < 0) change['lhs-line-from'] = 0;
-			if (change['lhs-line-to'] < 0) change['lhs-line-to'] = 0;
-			if (change['rhs-line-from'] < 0) change['rhs-line-from'] = 0;
-			if (change['rhs-line-to'] < 0) change['rhs-line-to'] = 0;
-			change['op'] = test[2];
-			changes[change_id++] = change;
-			this.trace('diff', 'change', change);
-		}
-		return changes;
-	},
-	_get_viewport: function(editor_name1, editor_name2) {
-		var lhsvp = this.editor[editor_name1].getViewport();
-		var rhsvp = this.editor[editor_name2].getViewport();
-		return {from: Math.min(lhsvp.from, rhsvp.from), to: Math.max(lhsvp.to, rhsvp.to)};
-	},
-	_is_change_in_view: function(vp, change) {
-		if (!this.settings.viewport) return true;
-		if ((change['lhs-line-from'] < vp.from && change['lhs-line-to'] < vp.to) ||
-			(change['lhs-line-from'] > vp.from && change['lhs-line-to'] > vp.to) ||
-			(change['rhs-line-from'] < vp.from && change['rhs-line-to'] < vp.to) ||
-			(change['rhs-line-from'] > vp.from && change['rhs-line-to'] > vp.to)) {
-			// if the change is outside the viewport, skip
-			return false;
-		}
-		return true;
-	},
-	_calculate_offsets: function (editor_name1, editor_name2, changes) {
-		if (this.em_height == null) {
-			// this is the distance from the top of the screen
-			var topnode = jQuery('#' + this.id + ' .CodeMirror-measure').first();
-			var top_offset = topnode.offset().top - 4;
-			if (!top_offset) return;//try again
-			this.draw_top_offset = 0.5 - top_offset;
-			this.em_height = this.editor[editor_name1].defaultTextHeight();
-			if (!this.em_height) {
-				console.warn('Failed to calculate offsets, using 18 by default');
-				this.em_height = 18;
-			}
-			this.draw_lhs_min = 0.5;
-			var c = jQuery('#' + editor_name1 + '-' + editor_name2 + '-canvas');
-			if (!c.length) {
-				console.error('failed to find canvas', '#' + editor_name1 + '-' + editor_name2 + '-canvas');
-			}
-			if (!c.width()) {
-				console.error('canvas width is 0');
-				return;
-			}
-			this.draw_mid_width = jQuery('#' + editor_name1 + '-' + editor_name2 + '-canvas').width();
-			this.draw_rhs_max = this.draw_mid_width - 0.5; //24.5;
-			this.draw_lhs_width = 5;
-			this.draw_rhs_width = 5;
-			this.trace('calc', 'change offsets calculated', {top_offset: top_offset, lhs_min: this.draw_lhs_min, rhs_max: this.draw_rhs_max, lhs_width: this.draw_lhs_width, rhs_width: this.draw_rhs_width});
-		}
-		var lhschc = this.editor[editor_name1].charCoords({line: 0});
-		var rhschc = this.editor[editor_name2].charCoords({line: 0});
-		var vp = this._get_viewport(editor_name1, editor_name2);
-
-		for (var i = 0; i < changes.length; ++i) {
-			var change = changes[i];
-
-			if (!this.settings.sidebar && !this._is_change_in_view(vp, change)) {
-				// if the change is outside the viewport, skip
-				delete change['lhs-y-start'];
-				delete change['lhs-y-end'];
-				delete change['rhs-y-start'];
-				delete change['rhs-y-end'];
-				continue;
-			}
-			var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
-			var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
-			var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
-			var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
-
-			var ls, le, rs, re;
-			if (this.editor[editor_name1].getOption('lineWrapping') || this.editor[editor_name1].getOption('lineWrapping')) {
-				// If using line-wrapping, we must get the height of the line
-				var tls = this.editor[editor_name1].cursorCoords({line: llf, ch: 0}, 'page');
-				var lhssh = this.editor[editor_name1].getLineHandle(llf);
-				ls = { top: tls.top, bottom: tls.top + lhssh.height };
-
-				var tle = this.editor[editor_name1].cursorCoords({line: llt, ch: 0}, 'page');
-				var lhseh = this.editor[editor_name1].getLineHandle(llt);
-				le = { top: tle.top, bottom: tle.top + lhseh.height };
-
-				var tls = this.editor[editor_name2].cursorCoords({line: rlf, ch: 0}, 'page');
-				var rhssh = this.editor[editor_name2].getLineHandle(rlf);
-				rs = { top: tls.top, bottom: tls.top + rhssh.height };
-
-				var tle = this.editor[editor_name2].cursorCoords({line: rlt, ch: 0}, 'page');
-				var rhseh = this.editor[editor_name2].getLineHandle(rlt);
-				re = { top: tle.top, bottom: tle.top + rhseh.height };
-			}
-			else {
-				// If not using line-wrapping, we can calculate the line position
-				ls = {
-					top: lhschc.top + llf * this.em_height,
-					bottom: lhschc.bottom + llf * this.em_height + 2
-				};
-				le = {
-					top: lhschc.top + llt * this.em_height,
-					bottom: lhschc.bottom + llt * this.em_height + 2
-				};
-				rs = {
-					top: rhschc.top + rlf * this.em_height,
-					bottom: rhschc.bottom + rlf * this.em_height + 2
-				};
-				re = {
-					top: rhschc.top + rlt * this.em_height,
-					bottom: rhschc.bottom + rlt * this.em_height + 2
-				};
-			}
-
-			if (change['op'] == 'a') {
-				// adds (right), normally start from the end of the lhs,
-				// except for the case when the start of the rhs is 0
-				if (rlf > 0) {
-					ls.top = ls.bottom;
-					ls.bottom += this.em_height;
-					le = ls;
-				}
-			}
-			else if (change['op'] == 'd') {
-				// deletes (left) normally finish from the end of the rhs,
-				// except for the case when the start of the lhs is 0
-				if (llf > 0) {
-					rs.top = rs.bottom;
-					rs.bottom += this.em_height;
-					re = rs;
-				}
-			}
-			change['lhs-y-start'] = this.draw_top_offset + ls.top;
-			if (change['op'] == 'c' || change['op'] == 'd') {
-				change['lhs-y-end'] = this.draw_top_offset + le.bottom;
-			}
-			else {
-				change['lhs-y-end'] = this.draw_top_offset + le.top;
-			}
-			change['rhs-y-start'] = this.draw_top_offset + rs.top;
-			if (change['op'] == 'c' || change['op'] == 'a') {
-				change['rhs-y-end'] = this.draw_top_offset + re.bottom;
-			}
-			else {
-				change['rhs-y-end'] = this.draw_top_offset + re.top;
-			}
-			this.trace('calc', 'change calculated', i, change);
-		}
-		return changes;
-	},
-	_markup_changes: function (editor_name1, editor_name2, changes) {
-		jQuery('.merge-button').remove(); // clear
-
-		var self = this;
-		var led = this.editor[editor_name1];
-		var red = this.editor[editor_name2];
-
-		var timer = new Mgly.Timer();
-		led.operation(function() {
-			for (var i = 0; i < changes.length; ++i) {
-				var change = changes[i];
-				var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
-				var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
-				var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
-				var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
-
-				var clazz = ['mergely', 'lhs', change['op'], 'cid-' + i];
-				led.addLineClass(llf, 'background', 'start');
-				led.addLineClass(llt, 'background', 'end');
-
-				if (llf == 0 && llt == 0 && rlf == 0) {
-					led.addLineClass(llf, 'background', clazz.join(' '));
-					led.addLineClass(llf, 'background', 'first');
-				}
-				else {
-					// apply change for each line in-between the changed lines
-					for (var j = llf; j <= llt; ++j) {
-						led.addLineClass(j, 'background', clazz.join(' '));
-						led.addLineClass(j, 'background', clazz.join(' '));
-					}
-				}
-
-				if (!red.getOption('readOnly')) {
-					// add widgets to lhs, if rhs is not read only
-					var rhs_button = self.merge_rhs_button.clone();
-					if (rhs_button.button) {
-						//jquery-ui support
-						rhs_button.button({icons: {primary: 'ui-icon-triangle-1-e'}, text: false});
-					}
-					rhs_button.addClass('merge-button');
-					rhs_button.attr('id', 'merge-rhs-' + i);
-					led.setGutterMarker(llf, 'merge', rhs_button.get(0));
-				}
-			}
-		});
-
-		var vp = this._get_viewport(editor_name1, editor_name2);
-
-		this.trace('change', 'markup lhs-editor time', timer.stop());
-		red.operation(function() {
-			for (var i = 0; i < changes.length; ++i) {
-				var change = changes[i];
-				var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
-				var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
-				var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
-				var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
-
-				if (!self._is_change_in_view(vp, change)) {
-					// if the change is outside the viewport, skip
-					continue;
-				}
-
-				var clazz = ['mergely', 'rhs', change['op'], 'cid-' + i];
-				red.addLineClass(rlf, 'background', 'start');
-				red.addLineClass(rlt, 'background', 'end');
-
-				if (rlf == 0 && rlt == 0 && llf == 0) {
-					red.addLineClass(rlf, 'background', clazz.join(' '));
-					red.addLineClass(rlf, 'background', 'first');
-				}
-				else {
-					// apply change for each line in-between the changed lines
-					for (var j = rlf; j <= rlt; ++j) {
-						red.addLineClass(j, 'background', clazz.join(' '));
-						red.addLineClass(j, 'background', clazz.join(' '));
-					}
-				}
-
-				if (!led.getOption('readOnly')) {
-					// add widgets to rhs, if lhs is not read only
-					var lhs_button = self.merge_lhs_button.clone();
-					if (lhs_button.button) {
-						//jquery-ui support
-						lhs_button.button({icons: {primary: 'ui-icon-triangle-1-w'}, text: false});
-					}
-					lhs_button.addClass('merge-button');
-					lhs_button.attr('id', 'merge-lhs-' + i);
-					red.setGutterMarker(rlf, 'merge', lhs_button.get(0));
-				}
-			}
-		});
-		this.trace('change', 'markup rhs-editor time', timer.stop());
-
-		// mark text deleted, LCS changes
-		var marktext = [];
-		for (var i = 0; this.settings.lcs && i < changes.length; ++i) {
-			var change = changes[i];
-			var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
-			var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
-			var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
-			var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
-
-			if (!this._is_change_in_view(vp, change)) {
-				// if the change is outside the viewport, skip
-				continue;
-			}
-			if (change['op'] == 'd') {
-				// apply delete to cross-out (left-hand side only)
-				var from = llf;
-				var to = llt;
-				var to_ln = led.lineInfo(to);
-				if (to_ln) {
-					marktext.push([led, {line:from, ch:0}, {line:to, ch:to_ln.text.length}, {className: 'mergely ch d lhs'}]);
-				}
-			}
-			else if (change['op'] == 'c') {
-				// apply LCS changes to each line
-				for (var j = llf, k = rlf, p = 0;
-					 ((j >= 0) && (j <= llt)) || ((k >= 0) && (k <= rlt));
-					 ++j, ++k) {
-					if (k + p > rlt) {
-						// lhs continues past rhs, mark lhs as deleted
-						var lhs_line = led.getLine( j );
-						marktext.push([led, {line:j, ch:0}, {line:j, ch:lhs_line.length}, {className: 'mergely ch d lhs'}]);
-						continue;
-					}
-					if (j + p > llt) {
-						// rhs continues past lhs, mark rhs as added
-						var rhs_line = red.getLine( k );
-						marktext.push([red, {line:k, ch:0}, {line:k, ch:rhs_line.length}, {className: 'mergely ch a rhs'}]);
-						continue;
-					}
-					var lhs_line = led.getLine( j );
-					var rhs_line = red.getLine( k );
-					var lhs_start = { line: -1, ch: -1 };
-					var lhs_stop = { line: -1, ch: -1 };
-					var rhs_start = { line: -1, ch: -1 };
-					var rhs_stop = { line: -1, ch: -1 };
-
-					var lcs = new Mgly.LCS(lhs_line, rhs_line);
-					lcs.diff(
-						function (from, to) {//added
-							marktext.push([red, {line:k, ch:from}, {line:k, ch:to}, {className: 'mergely ch a rhs'}]);
-						},
-						removed = function (from, to) {//removed
-							marktext.push([led, {line:j, ch:from}, {line:j, ch:to}, {className: 'mergely ch d lhs'}]);
-						}
-					);
-				}
-			}
-		}
-		this.trace('change', 'LCS marktext time', timer.stop());
-
-		// mark changes outside closure
-		led.operation(function() {
-			// apply lhs markup
-			for (var i = 0; i < marktext.length; ++i) {
-				var m = marktext[i];
-				if (m[0].doc.id != led.getDoc().id) continue;
-				self.chfns[self.id + '-lhs'].push(m[0].markText(m[1], m[2], m[3]));
-			}
-		});
-		red.operation(function() {
-			// apply lhs markup
-			for (var i = 0; i < marktext.length; ++i) {
-				var m = marktext[i];
-				if (m[0].doc.id != red.getDoc().id) continue;
-				self.chfns[self.id + '-rhs'].push(m[0].markText(m[1], m[2], m[3]));
-			}
-		});
-		this.trace('change', 'LCS markup time', timer.stop());
-
-		// merge buttons
-		var ed = {lhs:led, rhs:red};
-		jQuery('.merge-button').on('click', function(ev){
-			// side of mouseenter
-			var side = 'rhs';
-			var oside = 'lhs';
-			var parent = jQuery(this).parents('#' + self.id + '-editor-lhs');
-			if (parent.length) {
-				side = 'lhs';
-				oside = 'rhs';
-			}
-			var pos = ed[side].coordsChar({left:ev.pageX, top:ev.pageY});
-
-			// get the change id
-			var cid = null;
-			var info = ed[side].lineInfo(pos.line);
-			jQuery.each(info.bgClass.split(' '), function(i, clazz) {
-				if (clazz.indexOf('cid-') == 0) {
-					cid = parseInt(clazz.split('-')[1], 10);
-					return false;
-				}
-			});
-			var change = self.changes[cid];
-
-			var line = {lhs: ed['lhs'].lineInfo(llt), rhs: ed['rhs'].lineInfo(rlt)};
-
-			var text = ed[side].getRange(
-				CodeMirror.Pos(change[side + '-line-from'], 0),
-				CodeMirror.Pos(change[side + '-line-to'] + 1, 0));
-
-			if (change['op'] == 'c') {
-				ed[oside].replaceRange(text,
-					CodeMirror.Pos(change[oside + '-line-from'], 0),
-					CodeMirror.Pos(change[oside + '-line-to'] + 1, 0));
-			}
-			else if (side == 'rhs') {
-				if (change['op'] == 'a') {
-					ed[oside].replaceRange(text,
-						CodeMirror.Pos(change[oside + '-line-from'] + 1, 0),
-						CodeMirror.Pos(change[oside + '-line-to'] + 1, 0));
-				}
-				else {// 'd'
-					var from = parseInt(change[oside + '-line-from']);
-					var to = parseInt(change[oside + '-line-to']);
-					for (var i = to; i >= from; --i) {
-						ed[oside].removeLine(i);
-					}
-				}
-			}
-			else if (side == 'lhs') {
-				if (change['op'] == 'a') {
-					var from = parseInt(change[oside + '-line-from']);
-					var to = parseInt(change[oside + '-line-to']);
-					for (var i = to; i >= from; --i) {
-						ed[oside].removeLine(i);
-					}
-				}
-				else {// 'd'
-					ed[oside].replaceRange( text,
-						CodeMirror.Pos(change[oside + '-line-from'] + 1, 0));
-				}
-			}
-			//reset
-			ed['lhs'].setValue(ed['lhs'].getValue());
-			ed['rhs'].setValue(ed['rhs'].getValue());
-			return false;
-		});
-		this.trace('change', 'markup buttons time', timer.stop());
-	},
-	_draw_info: function(editor_name1, editor_name2) {
-		var visible_page_height = jQuery(this.editor[editor_name1].getScrollerElement()).height();
-		var gutter_height = jQuery(this.editor[editor_name1].getScrollerElement()).children(':first-child').height();
-		var dcanvas = document.getElementById(editor_name1 + '-' + editor_name2 + '-canvas');
-		if (dcanvas == undefined) throw 'Failed to find: ' + editor_name1 + '-' + editor_name2 + '-canvas';
-		var clhs = jQuery('#' + this.id + '-lhs-margin');
-		var crhs = jQuery('#' + this.id + '-rhs-margin');
-		return {
-			visible_page_height: visible_page_height,
-			gutter_height: gutter_height,
-			visible_page_ratio: (visible_page_height / gutter_height),
-			margin_ratio: (visible_page_height / gutter_height),
-			lhs_scroller: jQuery(this.editor[editor_name1].getScrollerElement()),
-			rhs_scroller: jQuery(this.editor[editor_name2].getScrollerElement()),
-			lhs_lines: this.editor[editor_name1].lineCount(),
-			rhs_lines: this.editor[editor_name2].lineCount(),
-			dcanvas: dcanvas,
-			clhs: clhs,
-			crhs: crhs,
-			lhs_xyoffset: jQuery(clhs).offset(),
-			rhs_xyoffset: jQuery(crhs).offset()
-		};
-	},
-	_draw_diff: function(editor_name1, editor_name2, changes) {
-		var ex = this._draw_info(editor_name1, editor_name2);
-		var mcanvas_lhs = ex.clhs.get(0);
-		var mcanvas_rhs = ex.crhs.get(0);
-		var ctx = ex.dcanvas.getContext('2d');
-		var ctx_lhs = mcanvas_lhs.getContext('2d');
-		var ctx_rhs = mcanvas_rhs.getContext('2d');
-
-		this.trace('draw', 'visible_page_height', ex.visible_page_height);
-		this.trace('draw', 'gutter_height', ex.gutter_height);
-		this.trace('draw', 'visible_page_ratio', ex.visible_page_ratio);
-		this.trace('draw', 'lhs-scroller-top', ex.lhs_scroller.scrollTop());
-		this.trace('draw', 'rhs-scroller-top', ex.rhs_scroller.scrollTop());
-
-		jQuery.each(jQuery.find('#' + this.id + ' canvas'), function () {
-			jQuery(this).get(0).height = ex.visible_page_height;
-		});
-
-		ex.clhs.unbind('click');
-		ex.crhs.unbind('click');
-
-		ctx_lhs.beginPath();
-		ctx_lhs.fillStyle = this.settings.bgcolor;
-		ctx_lhs.strokeStyle = '#888';
-		ctx_lhs.fillRect(0, 0, 6.5, ex.visible_page_height);
-		ctx_lhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
-
-		ctx_rhs.beginPath();
-		ctx_rhs.fillStyle = this.settings.bgcolor;
-		ctx_rhs.strokeStyle = '#888';
-		ctx_rhs.fillRect(0, 0, 6.5, ex.visible_page_height);
-		ctx_rhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
-
-		var vp = this._get_viewport(editor_name1, editor_name2);
-		for (var i = 0; i < changes.length; ++i) {
-			var change = changes[i];
-
-			this.trace('draw', change);
-			// margin indicators
-			var lhs_y_start = ((change['lhs-y-start'] + ex.lhs_scroller.scrollTop()) * ex.visible_page_ratio);
-			var lhs_y_end = ((change['lhs-y-end'] + ex.lhs_scroller.scrollTop()) * ex.visible_page_ratio) + 1;
-			var rhs_y_start = ((change['rhs-y-start'] + ex.rhs_scroller.scrollTop()) * ex.visible_page_ratio);
-			var rhs_y_end = ((change['rhs-y-end'] + ex.rhs_scroller.scrollTop()) * ex.visible_page_ratio) + 1;
-			this.trace('draw', 'marker calculated', lhs_y_start, lhs_y_end, rhs_y_start, rhs_y_end);
-
-			ctx_lhs.beginPath();
-			ctx_lhs.fillStyle = this.settings.fgcolor[change['op']];
-			ctx_lhs.strokeStyle = '#000';
-			ctx_lhs.lineWidth = 1.0;
-			ctx_lhs.fillRect(1.5, lhs_y_start, 4.5, Math.max(lhs_y_end - lhs_y_start, 5));
-			ctx_lhs.strokeRect(1.5, lhs_y_start, 4.5, Math.max(lhs_y_end - lhs_y_start, 5));
-
-			ctx_rhs.beginPath();
-			ctx_rhs.fillStyle = this.settings.fgcolor[change['op']];
-			ctx_rhs.strokeStyle = '#000';
-			ctx_rhs.lineWidth = 1.0;
-			ctx_rhs.fillRect(1.5, rhs_y_start, 4.5, Math.max(rhs_y_end - rhs_y_start, 5));
-			ctx_rhs.strokeRect(1.5, rhs_y_start, 4.5, Math.max(rhs_y_end - rhs_y_start, 5));
-
-			if (!this._is_change_in_view(vp, change)) {
-				continue;
-			}
-
-			lhs_y_start = change['lhs-y-start'];
-			lhs_y_end = change['lhs-y-end'];
-			rhs_y_start = change['rhs-y-start'];
-			rhs_y_end = change['rhs-y-end'];
-
-			var radius = 3;
-
-			// draw left box
-			ctx.beginPath();
-			ctx.strokeStyle = this.settings.fgcolor[change['op']];
-			ctx.lineWidth = 1;
-
-			var rectWidth = this.draw_lhs_width;
-			var rectHeight = lhs_y_end - lhs_y_start - 1;
-			var rectX = this.draw_lhs_min;
-			var rectY = lhs_y_start;
-			// top and top top-right corner
-
-			// draw left box
-			ctx.moveTo(rectX, rectY);
-			if (navigator.appName == 'Microsoft Internet Explorer') {
-				// IE arcs look awful
-				ctx.lineTo(this.draw_lhs_min + this.draw_lhs_width, lhs_y_start);
-				ctx.lineTo(this.draw_lhs_min + this.draw_lhs_width, lhs_y_end + 1);
-				ctx.lineTo(this.draw_lhs_min, lhs_y_end + 1);
-			}
-			else {
-				if (rectHeight <= 0) {
-					ctx.lineTo(rectX + rectWidth, rectY);
-				}
-				else {
-					ctx.arcTo(rectX + rectWidth, rectY, rectX + rectWidth, rectY + radius, radius);
-					ctx.arcTo(rectX + rectWidth, rectY + rectHeight, rectX + rectWidth - radius, rectY + rectHeight, radius);
-				}
-				// bottom line
-				ctx.lineTo(rectX, rectY + rectHeight);
-			}
-			ctx.stroke();
-
-			rectWidth = this.draw_rhs_width;
-			rectHeight = rhs_y_end - rhs_y_start - 1;
-			rectX = this.draw_rhs_max;
-			rectY = rhs_y_start;
-
-			// draw right box
-			ctx.moveTo(rectX, rectY);
-			if (navigator.appName == 'Microsoft Internet Explorer') {
-				ctx.lineTo(this.draw_rhs_max - this.draw_rhs_width, rhs_y_start);
-				ctx.lineTo(this.draw_rhs_max - this.draw_rhs_width, rhs_y_end + 1);
-				ctx.lineTo(this.draw_rhs_max, rhs_y_end + 1);
-			}
-			else {
-				if (rectHeight <= 0) {
-					ctx.lineTo(rectX - rectWidth, rectY);
-				}
-				else {
-					ctx.arcTo(rectX - rectWidth, rectY, rectX - rectWidth, rectY + radius, radius);
-					ctx.arcTo(rectX - rectWidth, rectY + rectHeight, rectX - radius, rectY + rectHeight, radius);
-				}
-				ctx.lineTo(rectX, rectY + rectHeight);
-			}
-			ctx.stroke();
-
-			// connect boxes
-			var cx = this.draw_lhs_min + this.draw_lhs_width;
-			var cy = lhs_y_start + (lhs_y_end + 1 - lhs_y_start) / 2.0;
-			var dx = this.draw_rhs_max - this.draw_rhs_width;
-			var dy = rhs_y_start + (rhs_y_end + 1 - rhs_y_start) / 2.0;
-			ctx.moveTo(cx, cy);
-			if (cy == dy) {
-				ctx.lineTo(dx, dy);
-			}
-			else {
-				// fancy!
-				ctx.bezierCurveTo(
-					cx + 12, cy - 3, // control-1 X,Y
-					dx - 12, dy - 3, // control-2 X,Y
-					dx, dy);
-			}
-			ctx.stroke();
-		}
-
-		// visible window feedback
-		ctx_lhs.fillStyle = this.settings.vpcolor;
-		ctx_rhs.fillStyle = this.settings.vpcolor;
-
-		var lto = ex.clhs.height() * ex.visible_page_ratio;
-		var lfrom = (ex.lhs_scroller.scrollTop() / ex.gutter_height) * ex.clhs.height();
-		var rto = ex.crhs.height() * ex.visible_page_ratio;
-		var rfrom = (ex.rhs_scroller.scrollTop() / ex.gutter_height) * ex.crhs.height();
-		this.trace('draw', 'cls.height', ex.clhs.height());
-		this.trace('draw', 'lhs_scroller.scrollTop()', ex.lhs_scroller.scrollTop());
-		this.trace('draw', 'gutter_height', ex.gutter_height);
-		this.trace('draw', 'visible_page_ratio', ex.visible_page_ratio);
-		this.trace('draw', 'lhs from', lfrom, 'lhs to', lto);
-		this.trace('draw', 'rhs from', rfrom, 'rhs to', rto);
-
-		ctx_lhs.fillRect(1.5, lfrom, 4.5, lto);
-		ctx_rhs.fillRect(1.5, rfrom, 4.5, rto);
-
-		ex.clhs.click(function (ev) {
-			var y = ev.pageY - ex.lhs_xyoffset.top - (lto / 2);
-			var sto = Math.max(0, (y / mcanvas_lhs.height) * ex.lhs_scroller.get(0).scrollHeight);
-			ex.lhs_scroller.scrollTop(sto);
-		});
-		ex.crhs.click(function (ev) {
-			var y = ev.pageY - ex.rhs_xyoffset.top - (rto / 2);
-			var sto = Math.max(0, (y / mcanvas_rhs.height) * ex.rhs_scroller.get(0).scrollHeight);
-			ex.rhs_scroller.scrollTop(sto);
-		});
-	},
-	trace: function(name) {
-		if(this.settings._debug.indexOf(name) >= 0) {
-			arguments[0] = name+':';
-			console.log([].slice.apply(arguments));
-		}
-	}
-});
-
-jQuery.pluginMaker = function(plugin) {
-	// add the plugin function as a jQuery plugin
-	jQuery.fn[plugin.prototype.name] = function(options) {
-		// get the arguments
-		var args = jQuery.makeArray(arguments),
-		after = args.slice(1);
-		var rc = undefined;
-		this.each(function() {
-			// see if we have an instance
-			var instance = jQuery.data(this, plugin.prototype.name);
-			if (instance) {
-				// call a method on the instance
-				if (typeof options == "string") {
-					rc = instance[options].apply(instance, after);
-				} else if (instance.update) {
-					// call update on the instance
-					return instance.update.apply(instance, args);
-				}
-			} else {
-				// create the plugin
-				new plugin(this, options);
-			}
-		});
-		if (rc != undefined) return rc;
-	};
-};
-
-// make the mergely widget
-jQuery.pluginMaker(Mgly.mergely);
--- a/rhodecode/templates/files/diff_2way.html	Mon May 26 17:56:53 2014 -0400
+++ b/rhodecode/templates/files/diff_2way.html	Mon May 26 20:21:00 2014 -0400
@@ -4,11 +4,11 @@
 
 <%def name="js_extra()">
 <script type="text/javascript" src="${h.url('/js/codemirror.js')}"></script>
-<script type="text/javascript" src="${h.url('/js/mergerly.js')}"></script>
+<script type="text/javascript" src="${h.url('/js/mergely.js')}"></script>
 </%def>
 <%def name="css_extra()">
 <link rel="stylesheet" type="text/css" href="${h.url('/css/codemirror.css')}"/>
-<link rel="stylesheet" type="text/css" href="${h.url('/css/mergerly.css')}"/>
+<link rel="stylesheet" type="text/css" href="${h.url('/css/mergely.css')}"/>
 </%def>
 
 <%def name="title()">