mirror of
https://gitlab.gnome.org/GNOME/gitg
synced 2024-10-12 10:53:29 +00:00
Implemented basic word diff rendering
This commit is contained in:
parent
fd508d8bbd
commit
cf36c0f4fa
|
@ -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];
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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 = ' ';
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
tabrepl = '<span class="tab" style="width: ' + data.settings.tab_width + 'ex">\t</span>';
|
||||
|
||||
var added = 0;
|
||||
var removed = 0;
|
||||
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 process = 1;
|
||||
|
||||
var row = '<tr data-offset="' + l.offset + '" data-length="' + l.length + '" class="';
|
||||
|
||||
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>';
|
||||
// Obtain block of added/removed or removed/added
|
||||
var fj = j;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (o == ' ')
|
||||
{
|
||||
o = ' ';
|
||||
}
|
||||
|
||||
row += '<td class="gutter type">' + o + '</td>';
|
||||
|
||||
var content = html_escape(l.content);
|
||||
content = content.replace(/\t/g, tabrepl);
|
||||
|
||||
var ws = '';
|
||||
|
||||
if (l.trailing_whitespace.length > 0)
|
||||
{
|
||||
ws = html_escape(l.trailing_whitespace);
|
||||
ws = ws.replace(/\t/g, tabrepl);
|
||||
|
||||
ws = '<span class="trailing-whitespace">' + ws + '</span>';
|
||||
}
|
||||
|
||||
row += '<td class="code">' + content + ws + '</td>';
|
||||
|
||||
row += '</tr>';
|
||||
|
||||
file_body += row;
|
||||
|
||||
lnstate.processed++;
|
||||
|
||||
proc = lnstate.processed / lnstate.lines;
|
||||
|
||||
if (proc >= lnstate.nexttick)
|
||||
{
|
||||
self.postMessage({tick: proc});
|
||||
|
||||
while (proc >= lnstate.nexttick)
|
||||
while (fj < h.lines.length && h.lines[fj].type == l.type)
|
||||
{
|
||||
lnstate.nexttick += lnstate.tickfreq;
|
||||
fj++;
|
||||
}
|
||||
|
||||
var lj = fj;
|
||||
|
||||
if (lj < h.lines.length && (h.lines[lj].type == LINE_ADDED || h.lines[lj].type == LINE_REMOVED))
|
||||
{
|
||||
var ctp = h.lines[lj].type;
|
||||
|
||||
while (lj < h.lines.length && h.lines[lj].type == ctp)
|
||||
{
|
||||
lj++;
|
||||
}
|
||||
}
|
||||
|
||||
if (lj - fj > 0)
|
||||
{
|
||||
// word diff of block
|
||||
process = 0;
|
||||
|
||||
var flines = h.lines.slice(j, fj);
|
||||
var llines = h.lines.slice(fj, lj);
|
||||
|
||||
var ladded = (l.type == LINE_ADDED ? flines : llines);
|
||||
var lremoved = (l.type == LINE_REMOVED ? flines : llines);
|
||||
|
||||
var wdiff = lines_to_word_diff_rows(lremoved, ladded, ccontext);
|
||||
|
||||
if (wdiff == null)
|
||||
{
|
||||
process = lj - j;
|
||||
}
|
||||
else
|
||||
{
|
||||
file_body += wdiff;
|
||||
|
||||
for (var k = 0; k < lj - j; k++)
|
||||
{
|
||||
lnstate.tick();
|
||||
}
|
||||
|
||||
j = lj;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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 */
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue