[gitg] Implemented basic word diff rendering



commit cf36c0f4fa217db38ebb9936906fb5a20d919bbd
Author: Jesse van den Kieboom <jessevdk gnome org>
Date:   Wed Jul 9 21:42:23 2014 +0200

    Implemented basic word diff rendering

 libgitg/resources/diff-view-html-builder.js |  571 +++++++++++++++++++++++----
 libgitg/resources/diff-view.css             |   10 +-
 libgitg/resources/diff-view.js              |    7 +
 3 files changed, 503 insertions(+), 85 deletions(-)
---
diff --git a/libgitg/resources/diff-view-html-builder.js b/libgitg/resources/diff-view-html-builder.js
index fb66f68..750820b 100644
--- a/libgitg/resources/diff-view-html-builder.js
+++ b/libgitg/resources/diff-view-html-builder.js
@@ -1,3 +1,8 @@
+function log(e)
+{
+       self.postMessage({'log': e});
+}
+
 function html_escape(s)
 {
        return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -53,15 +58,417 @@ Template.prototype.execute = function(replacements) {
        return ret + this.components[this.components.length - 1];
 }
 
-function diff_file(file, lnstate, data)
+const EDIT_INSERT = 0;
+const EDIT_DELETE = 1;
+const EDIT_SUBSTITUTE = 2;
+const EDIT_KEEP = 3;
+
+function min_dist(ins, del, sub)
+{
+       if (ins <= del)
+       {
+               if (sub < ins)
+               {
+                       return {distance: sub, direction: EDIT_SUBSTITUTE};
+               }
+               else
+               {
+                       return {distance: ins, direction: EDIT_INSERT};
+               }
+       }
+       else if (del <= sub)
+       {
+               return {distance: del, direction: EDIT_DELETE};
+       }
+       else
+       {
+               return {distance: sub, direction: EDIT_SUBSTITUTE};
+       }
+}
+
+function edit_distance(a, b)
+{
+       var nr = a.length + 1;
+       var nc = b.length + 1;
+
+       var d = new Uint16Array(nr * nc);
+       var e = new Int8Array(nr * nc);
+
+       for (var i = 0; i < nr; i++)
+       {
+               d[i] = i;
+               e[i] = EDIT_DELETE;
+       }
+
+       var p = 0;
+
+       for (var j = 0; j < nc; j++)
+       {
+               d[p] = j;
+               e[p] = EDIT_INSERT;
+
+               p += nr;
+       }
+
+       // Start calculating distance at first element (row 1, column 1)
+       p = nr + 1;
+
+       for (var j = 0; j < b.length; j++)
+       {
+               for (var i = 0; i < a.length; i++)
+               {
+                       if (a[i] == b[j])
+                       {
+                               // zero cost substitute
+                               d[p] = d[p - nr - 1];
+                               e[p] = EDIT_KEEP;
+                       }
+                       else
+                       {
+                               var md = min_dist(d[p - nr] + 1,      // insert
+                                                 d[p - 1] + 1,       // delete
+                                                 d[p - nr - 1] + 2); // substitute
+
+                               d[p] = md.distance;
+                               e[p] = md.direction;
+                       }
+
+                       p++;
+               }
+
+               // Advance one to skip first row
+               p++;
+       }
+
+       var ret = [];
+       var pi = [nr, 1, nr + 1, nr + 1];
+
+       p = nr * nc - 1;
+
+       var cost = d[p];
+
+       // Walk backwards to determine shortest path
+       while (p > 0)
+       {
+               if (e[p] == EDIT_SUBSTITUTE)
+               {
+                       ret.push(EDIT_INSERT);
+                       ret.push(EDIT_DELETE);
+               }
+               else
+               {
+                       ret.push(e[p]);
+               }
+
+               p -= pi[e[p]];
+       }
+
+       ret.reverse();
+       return {moves: ret, cost: cost};
+}
+
+const LINE_CONTEXT           = ' '.charCodeAt(0);
+const LINE_ADDED             = '+'.charCodeAt(0);
+const LINE_REMOVED           = '-'.charCodeAt(0);
+const LINE_CONTEXT_EOFNL     = '='.charCodeAt(0);
+const LINE_CONTEXT_ADD_EOFNL = '>'.charCodeAt(0);
+const LINE_CONTEXT_DEL_EOFNL = '<'.charCodeAt(0);
+
+function split_words(lines)
+{
+       var ret = [];
+
+       for (var i = 0; i < lines.length; i++)
+       {
+               if (i != 0)
+               {
+                       ret.push('\n');
+               }
+
+               var c = lines[i].content;
+
+               if (lines[i].trailing_whitespace)
+               {
+                       c += lines[i].trailing_whitespace;
+               }
+
+               // Split on word boundaries, as well as underscores and tabs
+               var words = c.split(/\b|(?=[_\t])/);
+
+               if (words.length > 0 && words[0].length == 0)
+               {
+                       words = words.slice(1, words.length);
+               }
+
+               if (words.length > 0 && words[words.length - 1].length == 0)
+               {
+                       words = words.slice(0, words.length - 1);
+               }
+
+               ret = ret.concat(words);
+       }
+
+       ret.push('\n');
+       return ret;
+}
+
+function make_content(content, ccontext)
+{
+       return html_escape(content).replace(/\t/g, ccontext.tabrepl);
+}
+
+function make_content_cell(content, tws, ccontext)
+{
+       content = make_content(content, ccontext);
+
+       var ws = '';
+
+       if (tws)
+       {
+               ws = make_content(tws, ccontext);
+               ws = '<span class="trailing-whitespace">' + ws + '</span>';
+       }
+
+       return '<td class="code">' + content + ws + '</td>';
+}
+
+function edit_type_to_cls(tp)
+{
+       switch (tp)
+       {
+       case EDIT_DELETE:
+               return "removed";
+       case EDIT_INSERT:
+               return "added";
+       default:
+               return "context";
+       }
+}
+
+function lines_to_word_diff_rows(removed, added, ccontext)
+{
+       // concat line contents and split on word boundaries
+       var remc = split_words(removed);
+       var addc = split_words(added);
+
+       var dist = edit_distance(remc, addc);
+
+       var row = '';
+       var rows = '';
+
+       var didinsert = false;
+       var didremove = false;
+
+       var dellines = 0;
+       var inslines = 0;
+
+       var delptr = 0;
+       var insptr = 0;
+
+       // Construct rows containing the word diff, based on moves
+       for (var i = 0; i < dist.moves.length; i++)
+       {
+               var word = '';
+
+               switch (dist.moves[i])
+               {
+               case EDIT_DELETE:
+                       word = remc[delptr];
+                       delptr++;
+
+                       if (word == '\n')
+                       {
+                               dellines++;
+                               ccontext.removed++;
+                       }
+
+                       didremove = true;
+                       break;
+               case EDIT_INSERT:
+                       word = addc[insptr];
+                       insptr++;
+
+                       if (word == '\n')
+                       {
+                               inslines++;
+                               ccontext.added++;
+                       }
+
+                       didinsert = true;
+                       break;
+               case EDIT_KEEP:
+                       // Keep the same
+                       word = remc[delptr];
+
+                       if (word == '\n')
+                       {
+                               inslines++;
+                               dellines++;
+
+                               ccontext.added++;
+                               ccontext.removed++;
+                       }
+                       else
+                       {
+                               didinsert = true;
+                               didremove = true;
+                       }
+
+                       delptr++;
+                       insptr++;
+
+                       break;
+               default:
+                       break;
+               }
+
+               if (word == '\n')
+               {
+                       var tp = '&nbsp;';
+                       var cold = '';
+                       var cnew = '';
+
+                       if (didinsert && didremove)
+                       {
+                               tp = '±';
+
+                               cold = ccontext.old;
+                               cnew = ccontext.new;
+                       }
+                       else if (didinsert)
+                       {
+                               tp = '+';
+
+                               cnew = ccontext.new;
+                       }
+                       else if (didremove)
+                       {
+                               tp = '-';
+
+                               cold = ccontext.old;
+                       }
+
+                       rows += '<tr class="' + edit_type_to_cls(dist.moves[i]) + '"> \
+                               <td class="gutter old">' + cold + '</td> \
+                               <td class="gutter new">' + cnew + '</td> \
+                               <td class="gutter type">' + tp + '</td> \
+                               <td class="code">' + row + '</td></tr>';
+
+                       row = '';
+
+                       didremove = false;
+                       didinsert = false;
+
+                       if (dist.moves[i] == EDIT_INSERT || dist.moves[i] == EDIT_KEEP)
+                       {
+                               ccontext.new++;
+                       }
+
+                       if (dist.moves[i] == EDIT_DELETE || dist.moves[i] == EDIT_KEEP)
+                       {
+                               ccontext.old++;
+                       }
+               }
+               else
+               {
+                       var content = make_content(word, ccontext);
+                       var cls = edit_type_to_cls(dist.moves[i]);
+
+                       if (cls.length != 0)
+                       {
+                               row += '<span class="' + cls + '">' + content + '</span>';
+                       }
+                       else
+                       {
+                               row += content;
+                       }
+               }
+       }
+
+       if (row.length != 0)
+       {
+               rows += '<tr class="' + edit_type_to_cls(dist.moves[dist.moves.length - 1]) + '"> \
+                       <td class="gutter old">' + ccontext.old + '</td> \
+                       <td class="gutter new">' + ccontext.new + '</td> \
+                       <td class="gutter type">&nbsp;</td> \
+                       <td class="code">' + row + '</td></tr>';
+       }
+
+       return rows;
+}
+
+function line_to_row(l, ccontext)
 {
-       tabrepl = '<span class="tab" style="width: ' + data.settings.tab_width + 'ex">\t</span>';
+       var o = String.fromCharCode(l.type);
+
+       var row = '<tr data-offset="' + l.offset + '" data-length="' + l.length + '" class="';
+
+       switch (l.type)
+       {
+               case LINE_CONTEXT:
+                       row += 'context"> \
+                               <td class="gutter old">' + ccontext.old + '</td> \
+                               <td class="gutter new">' + ccontext.new + '</td>';
+
+                       ccontext.old++;
+                       ccontext.new++;
+               break;
+               case LINE_ADDED:
+                       row += 'added"> \
+                               <td class="gutter old"></td> \
+                               <td class="gutter new">' + ccontext.new + '</td>';
+
+                       ccontext.new++;
+                       ccontext.added++;
+               break;
+               case LINE_REMOVED:
+                       row += 'removed"> \
+                               <td class="gutter old">' + ccontext.old + '</td> \
+                               <td class="gutter new"></td>';
+
+                       ccontext.old++;
+                       ccontext.removed++;
+               break;
+               case LINE_CONTEXT_EOFNL:
+               case LINE_CONTEXT_ADD_EOFNL:
+               case LINE_CONTEXT_DEL_EOFNL:
+                       row += 'context"> \
+                               <td class="gutter old"></td> \
+                               <td class="gutter new"></td>';
+                       l.content = l.content.substr(1, l.content.length);
+               break;
+               default:
+                       o = ' ';
+                       row += '">';
+               break;
+       }
+
+       if (o == ' ')
+       {
+               o = '&nbsp;';
+       }
 
-       var added = 0;
-       var removed = 0;
+       row += '<td class="gutter type">' + o + '</td>';
+       row += make_content_cell(l.content, l.trailing_whitespace, ccontext);
+       row += '</tr>';
+
+       return row;
+}
+
+function diff_file(file, lnstate, data)
+{
+       var tabrepl = '<span class="tab" style="width: ' + data.settings.tab_width + 'ex">\t</span>';
 
        var file_body = '';
 
+       var ccontext = {
+               tabrepl: tabrepl,
+               added: 0,
+               removed: 0,
+               old: 0,
+               new: 0
+       };
+
        for (var i = 0; i < file.hunks.length; ++i)
        {
                var h = file.hunks[i];
@@ -77,8 +484,8 @@ function diff_file(file, lnstate, data)
                        continue;
                }
 
-               var cold = h.range.old.start;
-               var cnew = h.range.new.start;
+               ccontext.old = h.range.old.start;
+               ccontext.new = h.range.new.start;
 
                var hunk_header = '<span class="hunk_stats">@@ -' + h.range.old.start + ',' + 
h.range.old.lines + ' +' + h.range.new.start + ',' + h.range.new.lines + ' @@</span>';
 
@@ -91,92 +498,79 @@ function diff_file(file, lnstate, data)
                        <td class="hunk_header">' + hunk_header + '</td> \
                </tr>';
 
-               for (var j = 0; j < h.lines.length; ++j)
+               var j = 0;
+
+               while (j < h.lines.length)
                {
                        var l = h.lines[j];
-                       var o = String.fromCharCode(l.type);
-
-                       var row = '<tr data-offset="' + l.offset + '" data-length="' + l.length + '" class="';
+                       var process = 1;
 
-                       switch (o)
+                       if (data.settings.changes_inline && (l.type == LINE_ADDED || l.type == LINE_REMOVED))
                        {
-                               case ' ':
-                                       row += 'context"> \
-                                               <td class="gutter old">' + cold + '</td> \
-                                               <td class="gutter new">' + cnew + '</td>';
-
-                                       cold++;
-                                       cnew++;
-                               break;
-                               case '+':
-                                       row += 'added"> \
-                                               <td class="gutter old"></td> \
-                                               <td class="gutter new">' + cnew + '</td>';
-
-                                       cnew++;
-                                       added++;
-                               break;
-                               case '-':
-                                       row += 'removed"> \
-                                               <td class="gutter old">' + cold + '</td> \
-                                               <td class="gutter new"></td>';
-
-                                       cold++;
-                                       removed++;
-                               break;
-                               case '=':
-                               case '>':
-                               case '<':
-                                       row += 'context"> \
-                                               <td class="gutter old"></td> \
-                                               <td class="gutter new"></td>';
-                                       l.content = l.content.substr(1, l.content.length);
-                               break;
-                               default:
-                                       o = ' ';
-                                       row += '">';
-                               break;
-                       }
+                               // Obtain block of added/removed or removed/added
+                               var fj = j;
 
-                       if (o == ' ')
-                       {
-                               o = '&nbsp;';
-                       }
-
-                       row += '<td class="gutter type">' + o + '</td>';
-
-                       var content = html_escape(l.content);
-                       content = content.replace(/\t/g, tabrepl);
+                               while (fj < h.lines.length && h.lines[fj].type == l.type)
+                               {
+                                       fj++;
+                               }
 
-                       var ws = '';
+                               var lj = fj;
 
-                       if (l.trailing_whitespace.length > 0)
-                       {
-                               ws = html_escape(l.trailing_whitespace);
-                               ws = ws.replace(/\t/g, tabrepl);
+                               if (lj < h.lines.length && (h.lines[lj].type == LINE_ADDED || 
h.lines[lj].type == LINE_REMOVED))
+                               {
+                                       var ctp = h.lines[lj].type;
 
-                               ws = '<span class="trailing-whitespace">' + ws + '</span>';
-                       }
+                                       while (lj < h.lines.length && h.lines[lj].type == ctp)
+                                       {
+                                               lj++;
+                                       }
+                               }
 
-                       row += '<td class="code">' + content + ws + '</td>';
+                               if (lj - fj > 0)
+                               {
+                                       // word diff of block
+                                       process = 0;
 
-                       row += '</tr>';
+                                       var flines = h.lines.slice(j, fj);
+                                       var llines = h.lines.slice(fj, lj);
 
-                       file_body += row;
+                                       var ladded = (l.type == LINE_ADDED ? flines : llines);
+                                       var lremoved = (l.type == LINE_REMOVED ? flines : llines);
 
-                       lnstate.processed++;
+                                       var wdiff = lines_to_word_diff_rows(lremoved, ladded, ccontext);
 
-                       proc = lnstate.processed / lnstate.lines;
+                                       if (wdiff == null)
+                                       {
+                                               process = lj - j;
+                                       }
+                                       else
+                                       {
+                                               file_body += wdiff;
 
-                       if (proc >= lnstate.nexttick)
-                       {
-                               self.postMessage({tick: proc});
+                                               for (var k = 0; k < lj - j; k++)
+                                               {
+                                                       lnstate.tick();
+                                               }
 
-                               while (proc >= lnstate.nexttick)
+                                               j = lj;
+                                       }
+                               }
+                               else
                                {
-                                       lnstate.nexttick += lnstate.tickfreq;
+                                       // Safe to process directly added/removed lines here, so
+                                       // we don't recheck for a possible block
+                                       process = fj - j;
                                }
                        }
+
+                       for (var k = j; k < j + process; k++)
+                       {
+                               file_body += line_to_row(h.lines[k], ccontext);
+                               lnstate.tick();
+                       }
+
+                       j += process;
                }
        }
 
@@ -199,11 +593,11 @@ function diff_file(file, lnstate, data)
                        file_path = file.file.old.path;
                }
 
-               var total = added + removed;
-               var addedp = Math.floor(added / total * 100);
+               var total = ccontext.added + ccontext.removed;
+               var addedp = Math.floor(ccontext.added / total * 100);
                var removedp = 100 - addedp;
 
-               file_stats = '<span class="file_stats"><span class="number">' + (added + removed)  + 
'</span><span class="bar"><span class="added" style="width: ' + addedp + '%;"></span><span class="removed" 
style="width: ' + removedp + '%;"></span></span></span>';
+               file_stats = '<span class="file_stats"><span class="number">' + (ccontext.added + 
ccontext.removed)  + '</span><span class="bar"><span class="added" style="width: ' + addedp + 
'%;"></span><span class="removed" style="width: ' + removedp + '%;"></span></span></span>';
        }
        else
        {
@@ -243,6 +637,22 @@ function diff_files(files, lines, maxlines, data)
                template: template,
        };
 
+       lnstate.tick = function() {
+               lnstate.processed++;
+
+               var proc = lnstate.processed / lnstate.lines;
+
+               if (proc >= lnstate.nexttick)
+               {
+                       self.postMessage({tick: proc});
+
+                       while (proc >= lnstate.nexttick)
+                       {
+                               lnstate.nexttick += lnstate.tickfreq;
+                       }
+               }
+       };
+
        // special empty background filler
        var f = diff_file({hunks: [null]}, lnstate, data);
 
@@ -254,11 +664,6 @@ function diff_files(files, lines, maxlines, data)
        return f;
 }
 
-function log(e)
-{
-       self.postMessage({'log': e});
-}
-
 self.onmessage = function(event) {
        var data = event.data;
 
@@ -275,3 +680,5 @@ self.onmessage = function(event) {
        r.open("GET", data.url);
        r.send();
 };
+
+/* vi:ts=4 */
diff --git a/libgitg/resources/diff-view.css b/libgitg/resources/diff-view.css
index b3d0f8f..c175a33 100644
--- a/libgitg/resources/diff-view.css
+++ b/libgitg/resources/diff-view.css
@@ -69,6 +69,7 @@ div#diff div.file table td.gutter {
 
 div#diff div.file table td.code {
   white-space: pre;
+  padding: 0;
 }
 
 span.tab {
@@ -84,12 +85,14 @@ div#diff div.file table.wrapped td.code {
   white-space: pre-wrap;
 }
 
-div#diff div.file table tr.context td:last-child {
+div#diff div.file table tr.context td:last-child,
+div#diff div.file table td:last-child span.context {
   background-color: #fafafa;
 }
 
 
-div#diff div.file table tr.added td:last-child {
+div#diff div.file table tr.added td:last-child,
+div#diff div.file table td:last-child span.added {
   background-color: #ddffdd;
 }
 
@@ -101,7 +104,8 @@ div#diff div.file table tr.removed.selected td:last-child {
   background-color: #b8bed6;
 }
 
-div#diff div.file table tr.removed td:last-child {
+div#diff div.file table tr.removed td:last-child,
+div#diff div.file table td:last-child span.removed {
   background-color: #ffdddd;
 }
 
diff --git a/libgitg/resources/diff-view.js b/libgitg/resources/diff-view.js
index 104d8ea..755e3f5 100644
--- a/libgitg/resources/diff-view.js
+++ b/libgitg/resources/diff-view.js
@@ -212,6 +212,7 @@ function prepare_patchset(filediv)
 
                        if (last != null && last[0] == tp && last[2] + last[3] == offset)
                        {
+                               // Contiguous block, just add the length
                                last[3] += length;
                        }
                        else
@@ -233,6 +234,7 @@ function prepare_patchset(filediv)
                        }
                }
 
+               // Keep track of the total offset difference between old and new
                doffset += added ? length : -length;
        }
 
@@ -425,6 +427,11 @@ function update_diff(id, lsettings)
        });
 
        xhr_get("diff", {format: "commit_only"}, function(r) {
+               if (!r)
+               {
+                       return;
+               }
+
                var j = JSON.parse(r);
 
                if ('commit' in j)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]