[gitg] Implemented basic word diff rendering
- From: Jesse van den Kieboom <jessevdk src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gitg] Implemented basic word diff rendering
- Date: Wed, 9 Jul 2014 19:47:56 +0000 (UTC)
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, '&').replace(/</g, '<').replace(/>/g, '>');
@@ -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 = ' ';
+ 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"> </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 = ' ';
+ }
- 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 = ' ';
- }
-
- 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]