A Side-By-Side Diff Viewer Built With Ext JS
One of the great things about Ext JS is how easy it is to create web applications with desktop functionality. In this article I'll show you how to create a familiar application: a visual side-by-side diff tool. The tool provides a diff algorithm implementation in Javascript and a custom code viewing component that can interpret the results of the diff to show insertions, deletions and modifications.
The Diff Algorithm
The most important part of a diff tool is the diff implementation itself. While there are a few Javascript implementations floating around the web these days, for this project I decided to start from scratch using the algorithm described in Eugene Myer's paper. The details of the algorithm itself is beyond the scope of this article. However, the input and output is important to understand. The input is simply two arrays which I'll call A and B. The diff algorithm can take arrays of characters, numbers, strings or other comparable data as input. What the arrays hold is irrelevant as long as their contents can be compared for equality. The output is called an edit script. An edit script is a list of instructions for how to convert array A into array B through the deletion of items from A and the insertion of items into B. As a demonstration, I'll use the example from the paper: "ABCABBA" and "CBABAC". First, I turn these strings into arrays of characters. Then I run the arrays through the diff algorithm to obtain the edit script.var A = ['A', 'B', 'C', 'A', 'B', 'B', 'A'],
B = ['C', 'B', 'A', 'B', 'A', 'C'];
var editScript = diff(A, B);
// editScript = {
// deletions: [0, 1, 5],
// insertions: [1, 5],
// }
Note that in the above reconstruction, I use data from B itself for insertions because the edit script does not contain that information. In other words, the edit script indicates an insertion at index 1 but it does not specify what character should go there. This is fine for the visual diff since all we're interested in is the indexes. For patching, the edit script would also need to include the inserted data.
Making it Visual
Using the above process, it's possible to mark lines as inserted, deleted or modified as they are created in the DOM. Instead of comparing characters in a string, I split the code into arrays of lines. The lines are the input to the diff algorithm. The following code shows how the viewer component renders text using the edit script that is passed in after the diff. Notice that two cursors are maintained to keep track of placement in A and B, and those cursors are used in conjunction with the edit script to set CSS classes or insert empty lines.setCode: function(code, diff) {
// Clear
this.el.update("");
// Create copies of the edit script
var insertions = diff.insertions.slice(0),
deletions = diff.deletions.slice(0),
// Obtain reference to HTML templates
lineTpl = Ext.ux.CodeViewer.lineTpl,
emptyLineTpl = Ext.ux.CodeViewer.emptyLineTpl,
// Create a "pre" tag to hold the code
pre = this.el.createChild({tag: 'pre'}),
// Normalize line-breaks in the code
code = code.replace(/\r\n?/g, '\n'),
// Split code into discrete lines
lines = code.split('\n'),
// Cursors/flags for walking the edit script
sideAIndex = 0,
sideBIndex = 0,
sideAChangeIndex = deletions.shift(),
sideBChangeIndex = insertions.shift(),
prevWasModified = false;
// Loop over each line
for (var i = 0, n = lines.length; i<n; i++) {
// Create the HTML for the line, including highlighting
var el = lineTpl.append(pre, [i+1, this.highlightLine(lines)]);
// By default we want to move both cursors forward
var advanceA = true,
advanceB = true;
// If both cursors indicate a change, consider it to be a modification
if (sideAIndex === sideAChangeIndex && sideBIndex === sideBChangeIndex) {
Ext.fly(el).addClass('ux-codeViewer-modified');
// Get next changes
sideAChangeIndex = deletions.shift();
sideBChangeIndex = insertions.shift();
// Set modified flag so that following lines
// are marked accordingly
prevWasModified = true;
}
else {
// Different logic for side A vs side B
// For instance, an insert means an empty line on side A
// and highlighting on side B
if (this.sideA) {
// If there was a deletion
if (sideAIndex === sideAChangeIndex) {
// Either highlight as deleted or modified depending
// on the previous line
Ext.fly(el).addClass(prevWasModified ? 'ux-codeViewer-modified' : 'ux-codeViewer-deleted');
// Get next change
sideAChangeIndex = deletions.shift();
// Don't advance B cursor
advanceB = false;
}
else {
// If there were insertions, generate empty lines
while (sideBIndex === sideBChangeIndex) {
// Insert empty line
emptyLineTpl.insertBefore(el);
// Get next change
sideBChangeIndex = insertions.shift();
// Keep advancing as long as there was an insertion
sideBIndex++;
}
}
}
// Side B
else {
// If there was an insertation
if (sideBIndex == sideBChangeIndex) {
// Either highlight as inserted or modified depending
// on the previous line
Ext.fly(el).addClass(prevWasModified ? 'ux-codeViewer-modified' : 'ux-codeViewer-inserted');
// Get next change
sideBChangeIndex = insertions.shift();
// Don't advance A cursor
advanceA = false;
}
else {
// If there were deletions, generate empty lines
while (sideAIndex === sideAChangeIndex) {
// Insert empty line
emptyLineTpl.insertBefore(el);
// Get next change
sideAChangeIndex = deletions.shift();
// Keep advancing as long as there was a deletion
sideAIndex++;
}
}
}
// Reset modified flag
prevWasModified = false;
}
// Advance cursors
if (advanceA) { sideAIndex++; }
if (advanceB) { sideBIndex++; }
}
}

There are 25 responses. Add yours.
Nils Dehl
3 years agoNice Example
Jackson
3 years agoThank you for sharing the example
Loiane
3 years agoNice article! Thanks for sharing!
Jay Garcia
3 years agoNow, all you need is some Raphael to draw the arrows and you’re golden
. Great job.
Americo Savinon
3 years agoPretty Cool Example.
Agreed with Jay suggestion
drowsy
3 years agoVery nice!
mschwartz
3 years agoDoing diff for source code is nice.
Ever consider doing diff for HTML code though? It gets really tricky because the differences may occur between the in HTML tags. Think on that!
Cheers
SwamBala
3 years agoJay Garcia + 1
Americo Savinon + 1
Draw the lines, of-course with Raphael.
This kind of implementations helps us to put forward the ExtJS in the enterprise world. I mean the possibilities of using the ExtJS.
Great Jobs guys.
Frank
3 years agoLet me learn!
modern
3 years agogood articles
online review
3 years agofantastic
techno
3 years agoGreat Jobs guys
Animal
3 years agoVery nice work!
nicobarten
3 years agoVery nice indeed!
Gevik
3 years agoWow… very nice work.
pouniok
3 years agoThe demo doesn’t work in Opera
handy.wang
3 years agoYep. You are right this demo donesn’t work in Opera but why you must run this in Opera.
Did any body run this demo on S60(Opera mobile) or Opera mini. So, obviously…...
handy.wang
3 years agoHi James Brantly,
where can I download this demo on this Sencha site or somewhere else?
Thanks.
techno
3 years agogood articles. thanks….........
kitchen cabinets
2 years agoI have been researching for some time for just a sensible read dealing with this kind of niche . Looking around in Search engines I now came across this url. Reading this So i am glad to convey that I get a fine uncanny feeling I ran across whatever I was looking for. I will ensure to don’t forget this site and take a visit consistently.
oem sofware
2 years agoI am totally delighted with incredibly blog greatly that warned me. God bless you “It is not the strongest of the species that survive, nor the most intelligent, but the one most responsive to change.” - Charles Darwin
oem sofware
2 years agoI just can not imagine with strong your blog greatly that helped me. Thank you “Courage is not the absence of fear but rather the judgment that something else is more important than fear.” - Ambrose Redmoon
Uganda
2 years agoI just can not imagine with strong your blog greatly that helped me! Thank you “If you don’t make mistakes, you’re not working on hard enough problems.” - Frank Wilczek
??????
2 years agoI am totally delighted with strong your blog greatly that helped me! God bless you “Love doesn’t make the world go round; love is what makes the ride worthwhile.” - Elizabeth Browning
Lev
2 years agoThanks a lot for a great article.
Unfortunately link to working demo is broken.
Could you fix it?
Thanks,
Lev
Comments are Gravatar enabled. Your email address will not be shown.
Commenting is not available in this channel entry.