From cf36c0f4fa217db38ebb9936906fb5a20d919bbd Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Wed, 9 Jul 2014 21:42:23 +0200 Subject: [PATCH] Implemented basic word diff rendering --- libgitg/resources/diff-view-html-builder.js | 591 +++++++++++++++++--- libgitg/resources/diff-view.css | 10 +- libgitg/resources/diff-view.js | 7 + 3 files changed, 513 insertions(+), 95 deletions(-) diff --git a/libgitg/resources/diff-view-html-builder.js b/libgitg/resources/diff-view-html-builder.js index fb66f683..750820b7 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, '>'); @@ -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 = '' + ws + ''; + } + + return '' + content + ws + ''; +} + +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 += ' \ + ' + cold + ' \ + ' + cnew + ' \ + ' + tp + ' \ + ' + row + ''; + + 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 += '' + content + ''; + } + else + { + row += content; + } + } + } + + if (row.length != 0) + { + rows += ' \ + ' + ccontext.old + ' \ + ' + ccontext.new + ' \ +   \ + ' + row + ''; + } + + return rows; +} + +function line_to_row(l, ccontext) +{ + var o = String.fromCharCode(l.type); + + var row = ' \ + ' + ccontext.old + ' \ + ' + ccontext.new + ''; + + ccontext.old++; + ccontext.new++; + break; + case LINE_ADDED: + row += 'added"> \ + \ + ' + ccontext.new + ''; + + ccontext.new++; + ccontext.added++; + break; + case LINE_REMOVED: + row += 'removed"> \ + ' + ccontext.old + ' \ + '; + + ccontext.old++; + ccontext.removed++; + break; + case LINE_CONTEXT_EOFNL: + case LINE_CONTEXT_ADD_EOFNL: + case LINE_CONTEXT_DEL_EOFNL: + row += 'context"> \ + \ + '; + l.content = l.content.substr(1, l.content.length); + break; + default: + o = ' '; + row += '">'; + break; + } + + if (o == ' ') + { + o = ' '; + } + + row += '' + o + ''; + row += make_content_cell(l.content, l.trailing_whitespace, ccontext); + row += ''; + + return row; +} + function diff_file(file, lnstate, data) { - tabrepl = '\t'; - - var added = 0; - var removed = 0; + var tabrepl = '\t'; 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 = '@@ -' + h.range.old.start + ',' + h.range.old.lines + ' +' + h.range.new.start + ',' + h.range.new.lines + ' @@'; @@ -91,92 +498,79 @@ function diff_file(file, lnstate, data) ' + hunk_header + ' \ '; - 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 = ' \ - ' + cold + ' \ - ' + cnew + ''; + // Obtain block of added/removed or removed/added + var fj = j; - cold++; - cnew++; - break; - case '+': - row += 'added"> \ - \ - ' + cnew + ''; - - cnew++; - added++; - break; - case '-': - row += 'removed"> \ - ' + cold + ' \ - '; - - cold++; - removed++; - break; - case '=': - case '>': - case '<': - row += 'context"> \ - \ - '; - l.content = l.content.substr(1, l.content.length); - break; - default: - o = ' '; - row += '">'; - break; - } - - if (o == ' ') - { - o = ' '; - } - - row += '' + o + ''; - - 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 = '' + ws + ''; - } - - row += '' + content + ws + ''; - - row += ''; - - 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 = '' + (added + removed) + ''; + file_stats = '' + (ccontext.added + ccontext.removed) + ''; } 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 b3d0f8fa..c175a339 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 104d8ea3..755e3f59 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)