From d9c1256619bc310ef9a474c71b88fe67ac7a00ef Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Tue, 4 Jun 2024 10:12:41 -0400 Subject: [PATCH 1/3] thread through hunk header --- webdiff/unified_diff.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/webdiff/unified_diff.py b/webdiff/unified_diff.py index 7f1208c..d7f4578 100644 --- a/webdiff/unified_diff.py +++ b/webdiff/unified_diff.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from itertools import groupby -from typing import List, Tuple, Union +from typing import List, Optional, Tuple, Union from unidiff import PatchSet @@ -15,6 +15,7 @@ class Code: """Line range on left side; zero-based, half-open interval.""" after: Tuple[int, int] """Line range on right side; zero-based, half-open interval.""" + header: Optional[str] def read_codes(p: PatchSet) -> Union[List[Code], None]: @@ -27,16 +28,19 @@ def read_codes(p: PatchSet) -> Union[List[Code], None]: return None for hunk in pf: + header = hunk.section_header if hunk.source_start != last_source + 1: out.append( Code( 'skip', (last_source, hunk.source_start - 1), (last_target, hunk.target_start - 1), + header, ) ) last_source = hunk.source_start last_target = hunk.target_start + header = None for type, chunk in groupby(hunk, lambda line: line.line_type): lines = [*chunk] @@ -48,24 +52,30 @@ def read_codes(p: PatchSet) -> Union[List[Code], None]: 'equal', (first.source_line_no - 1, last.source_line_no), (first.target_line_no - 1, last.target_line_no), + header, ) ) + header = None elif type == '-': out.append( Code( 'delete', (first.source_line_no - 1, last.source_line_no), (last_target, last_target), + header, ) ) + header = None elif type == '+': out.append( Code( 'insert', (last_source, last_source), (first.target_line_no - 1, last.target_line_no), + header, ) ) + header = None last_source = last.source_line_no or last_source last_target = last.target_line_no or last_target @@ -88,6 +98,7 @@ def add_replaces(codes: List[Code]) -> List[Code]: 'replace', (c.before[0], nc.before[1]), (c.after[0], nc.after[1]), + c.header, ) ) i += 2 @@ -126,6 +137,7 @@ def diff_to_codes(diff: str, after_num_lines=None) -> Union[List[Code], None]: 'skip', (a2, a2 + end_skip), (b2, b2 + end_skip), + None, ) ) From 4d70df0ae7e234abb7f38e283214202b477d479a Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Tue, 4 Jun 2024 13:22:53 -0400 Subject: [PATCH 2/3] update codediff.js --- ts/CodeDiff.tsx | 1 + webdiff/static/codediff.js/codediff.css | 45 +++++++-- webdiff/static/codediff.js/codediff.js | 124 ++++++++++++++++-------- 3 files changed, 122 insertions(+), 48 deletions(-) diff --git a/ts/CodeDiff.tsx b/ts/CodeDiff.tsx index af43419..ddb15aa 100644 --- a/ts/CodeDiff.tsx +++ b/ts/CodeDiff.tsx @@ -104,6 +104,7 @@ export function CodeDiff(props: {filePair: FilePair, diffOptions: Partial { alert('Unable to get diff!'); + console.error(e); }); }, [filePair, diffOptions]); diff --git a/webdiff/static/codediff.js/codediff.css b/webdiff/static/codediff.js/codediff.css index 2211e6e..ab5ed97 100644 --- a/webdiff/static/codediff.js/codediff.css +++ b/webdiff/static/codediff.js/codediff.css @@ -17,7 +17,14 @@ td.code { /* for table cells, `width` behaves more like `max-width`. */ width: 61ch; white-space: pre-wrap; - overflow-wrap: anywhere; + word-wrap: break-word; +} + +table.diff td.code { + word-break: break-all; +} +table.diff.word-wrap td.code { + word-break: normal; } table.diff td { @@ -32,12 +39,12 @@ table.diff td { .line-no:first-child { background-image: linear-gradient(to left, #f7f7f7, #f7f7f7 3px, transparent, transparent 6px, #f7f7f7 6px), - linear-gradient(#f7f7f7, #f7f7f7 1.4em, #aaa 1.4em); + linear-gradient(#f7f7f7, #f7f7f7 1.5em, #aaa 1.5em); } .line-no:last-child { background-image: linear-gradient(to right, #f7f7f7, #f7f7f7 3px, transparent, transparent 6px, #f7f7f7 6px), - linear-gradient(#f7f7f7, #f7f7f7 1.4em, #aaa 1.4em); + linear-gradient(#f7f7f7, #f7f7f7 1.5em, #aaa 1.5em); } table.diff .line-no:first-child { @@ -51,6 +58,13 @@ table.diff .line-no:last-child { table.diff td:nth-child(2) { border-right: 1px solid #ddd; } +table.diff tr.skip-row td { + border-left: none; + border-right: none; + border-top: 1px solid #eee; + border-bottom: 1px solid #eee; + padding: 0.5em; +} .line-no, .code { padding: 2px; @@ -59,20 +73,31 @@ table.diff td:nth-child(2) { } .diff .skip { text-align: center; - background: #f7f7f7; + background: white; + color: #999; +} +.arrows-left { + float: left; +} +.arrows-right { + float: right; +} +.expand-up + .expand-down { + margin-left: 0.25em; } -.diff .delete { - background-color: #fee; +.hunk-header { + margin-left: 2em; + color: #777; } -.diff .insert { - background-color: #efe; +span.skip { + cursor: pointer; } -.before.replace { +.diff .delete, .before.replace { background-color: #fee; } -.after.replace { +.diff .insert, .after.replace { background-color: #efe; } diff --git a/webdiff/static/codediff.js/codediff.js b/webdiff/static/codediff.js/codediff.js index 5e4bf02..3c4e7d3 100644 --- a/webdiff/static/codediff.js/codediff.js +++ b/webdiff/static/codediff.js/codediff.js @@ -1,4 +1,5 @@ (() => { + function $parcel$export(e, n, v, s) { Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true}); } @@ -58,8 +59,8 @@ function $b8027a79b3117d71$var$html_substr(html, start, count) { el.removeChild(oldNode); } for(var i = 0; i < elsToRemove.length; i++){ - const el1 = elsToRemove[i]; - if (el1 && el1.parentNode) el1.parentNode.removeChild(el1); + const el = elsToRemove[i]; + if (el && el.parentNode) el.parentNode.removeChild(el); } } return div.innerHTML; @@ -283,7 +284,12 @@ function $7b3893c47869bfd4$export$bc16352bf8ce7a63(opcodes, contextSize, minJump } -function $b86f11f5e9077ed0$export$1f6ff06b43176f68(text) { +/** + * @param text Possibly multiline text containing spans that cross + * line breaks. + * @return An array of individual lines, each of which has + * entirely balanced tags. + */ function $b86f11f5e9077ed0$export$1f6ff06b43176f68(text) { const lines = difflib.stringAsLines(text); const spanRe = /(]*>)|(<\/span>)/; const outLines = []; @@ -321,7 +327,10 @@ function $b86f11f5e9077ed0$export$1f6ff06b43176f68(text) { -function $6d1363c84edc199c$export$189ed0fd75cb2ccc(name) { +/** + * Returns a valid HighlightJS language based on a file name/path. + * If it can't guess a language, returns null. + */ function $6d1363c84edc199c$export$189ed0fd75cb2ccc(name) { var lang = function() { var m = /\.([^.]+)$/.exec(name); if (m) { @@ -380,39 +389,51 @@ function $d0fe6ceded5cfd85$export$9ab1e790f5c0e723(type, beforeLineNum, beforeTe $("").text(beforeLineNum || "").get(0), $makeCodeTd(beforeTextOrHtml).addClass("before").get(0), $makeCodeTd(afterTextOrHtml).addClass("after").get(0), - $("").text(afterLineNum || "").get(0), + $("").text(afterLineNum || "").get(0) ]; if (type == "replace") (0, $1190ad35680a3ea6$export$66fc1d006dbd50e9)(cells[1], cells[2]); return $("").append(cells).get(0); } -function $d0fe6ceded5cfd85$export$8d7d7a7d0377361b(beforeIdx, afterIdx, numRowsSkipped) { - var $tr = $('…Show ' + numRowsSkipped + " more lines" + "" + '…' + ""); +function $d0fe6ceded5cfd85$export$8d7d7a7d0377361b(beforeIdx, afterIdx, numRowsSkipped, header, expandLines) { + const arrows = numRowsSkipped <= expandLines ? `\u{2195}` : `\u{21A5}\u{21A7}`; + const showMore = `Show ${numRowsSkipped} more lines`; + const headerHTML = header ? `${header}` : ""; + const $tr = $(` + ${arrows}${showMore} ${headerHTML}${arrows} + `); $tr.find(".skip").data({ beforeStartIndex: beforeIdx, afterStartIndex: afterIdx, - jumpLength: numRowsSkipped + jumpLength: numRowsSkipped, + header: header }); return $tr.get(0); } +const $a4b41c61879d57cc$var$DEFAULT_OPTIONS = { + contextSize: 3, + minJumpSize: 10, + expandLines: 10 +}; +const $a4b41c61879d57cc$var$DEFAULT_PARAMS = { + minJumpSize: 10, + language: null, + beforeName: "Before", + afterName: "After", + wordWrap: false, + expandLines: 10 +}; class $a4b41c61879d57cc$export$d7ae8a2952d3eaf0 { constructor(beforeText, beforeLines, afterText, afterLines, ops, params){ - const defaultParams = { - minJumpSize: 10, - language: null, - beforeName: "Before", - afterName: "After", - wordWrap: false - }; this.params = { - ...defaultParams, + ...$a4b41c61879d57cc$var$DEFAULT_PARAMS, ...params }; this.beforeLines = beforeLines; this.afterLines = afterLines; this.diffRanges = ops; - const { language: language } = this.params; + const { language: language } = this.params; if (language) { this.beforeLinesHighlighted = $a4b41c61879d57cc$var$highlightText(beforeText ?? "", language); this.afterLinesHighlighted = $a4b41c61879d57cc$var$highlightText(afterText ?? "", language); @@ -426,16 +447,26 @@ class $a4b41c61879d57cc$export$d7ae8a2952d3eaf0 { * Attach event listeners, notably for the "show more" links. */ attachHandlers_(el) { // TODO: gross duplication with buildView_ - var language = this.params.language, beforeLines = language ? this.beforeLinesHighlighted : this.beforeLines, afterLines = language ? this.afterLinesHighlighted : this.afterLines; - $(el).on("click", ".skip a", function(e) { + const language = this.params.language; + const beforeLines = language ? this.beforeLinesHighlighted : this.beforeLines; + const afterLines = language ? this.afterLinesHighlighted : this.afterLines; + const expandLines = this.params.expandLines; + $(el).on("click", ".skip a, span.skip", function(e) { e.preventDefault(); - var skipData = $(this).closest(".skip").data(); - var beforeIdx = skipData.beforeStartIndex; - var afterIdx = skipData.afterStartIndex; - var jump = skipData.jumpLength; - var newTrs = []; - for(var i = 0; i < jump; i++)newTrs.push((0, $d0fe6ceded5cfd85$export$9ab1e790f5c0e723)("equal", beforeIdx + i + 1, beforeLines[beforeIdx + i], afterIdx + i + 1, afterLines[afterIdx + i], language)); - // Replace the "skip" rows with real code. + const $skip = $(this).closest(".skip"); + const skipData = $skip.data(); + let type = $skip.hasClass("expand-down") ? "down" : $skip.hasClass("expand-up") ? "up" : "all"; + const beforeIdx = skipData.beforeStartIndex; + const afterIdx = skipData.afterStartIndex; + const jump = skipData.jumpLength; + if (jump < expandLines) type = "all"; + const newTrs = []; + const a = type === "up" || type === "all" ? 0 : jump - expandLines; + const b = type === "up" ? expandLines : jump; + if (type === "down") newTrs.push((0, $d0fe6ceded5cfd85$export$8d7d7a7d0377361b)(beforeIdx, afterIdx, jump - expandLines, skipData.header, expandLines)); + for(let i = a; i < b; i++)newTrs.push((0, $d0fe6ceded5cfd85$export$9ab1e790f5c0e723)("equal", beforeIdx + i + 1, beforeLines[beforeIdx + i], afterIdx + i + 1, afterLines[afterIdx + i], language)); + if (type === "up") newTrs.push((0, $d0fe6ceded5cfd85$export$8d7d7a7d0377361b)(beforeIdx + expandLines, afterIdx + expandLines, jump - expandLines, skipData.header, expandLines)); + // Replace the old "skip" row with the new code and (maybe) new skip row. var $skipTr = $(this).closest("tr"); $skipTr.replaceWith(newTrs); }); @@ -459,19 +490,26 @@ class $a4b41c61879d57cc$export$d7ae8a2952d3eaf0 { } buildView_() { // TODO: is this distinction necessary? - var language = this.params.language, beforeLines = language ? this.beforeLinesHighlighted : this.beforeLines, afterLines = language ? this.afterLinesHighlighted : this.afterLines; - var $table = $(''); + const language = this.params.language; + const beforeLines = language ? this.beforeLinesHighlighted : this.beforeLines; + const afterLines = language ? this.afterLinesHighlighted : this.afterLines; + const expandLines = this.params.expandLines; + const $table = $('
'); $table.append($("").append($('
').text(this.params.beforeName), $('').text(this.params.afterName))); - for(var i = 0; i < this.diffRanges.length; i++){ - var range = this.diffRanges[i], type = range.type, numBeforeRows = range.before[1] - range.before[0], numAfterRows = range.after[1] - range.after[0], numRows = Math.max(numBeforeRows, numAfterRows); - if (type == "skip") $table.append((0, $d0fe6ceded5cfd85$export$8d7d7a7d0377361b)(range.before[0], range.after[0], numRows)); - else for(var j = 0; j < numRows; j++){ - var beforeIdx = j < numBeforeRows ? range.before[0] + j : null, afterIdx = j < numAfterRows ? range.after[0] + j : null; + for (const range of this.diffRanges){ + const type = range.type; + const numBeforeRows = range.before[1] - range.before[0]; + const numAfterRows = range.after[1] - range.after[0]; + const numRows = Math.max(numBeforeRows, numAfterRows); + if (type == "skip") $table.append((0, $d0fe6ceded5cfd85$export$8d7d7a7d0377361b)(range.before[0], range.after[0], numRows, range.header ?? null, expandLines)); + else for(let j = 0; j < numRows; j++){ + const beforeIdx = j < numBeforeRows ? range.before[0] + j : null; + const afterIdx = j < numAfterRows ? range.after[0] + j : null; $table.append((0, $d0fe6ceded5cfd85$export$9ab1e790f5c0e723)(type, beforeIdx != null ? 1 + beforeIdx : null, beforeIdx != null ? beforeLines[beforeIdx] : undefined, afterIdx != null ? 1 + afterIdx : null, afterIdx != null ? afterLines[afterIdx] : undefined, language)); } } if (this.params.wordWrap) $table.addClass("word-wrap"); - var $container = $('
'); + const $container = $('
'); $container.append($table); // Attach event handlers & apply char diffs. this.attachHandlers_($container); @@ -479,9 +517,8 @@ class $a4b41c61879d57cc$export$d7ae8a2952d3eaf0 { } static buildView(beforeText, afterText, userParams) { const params = { - contextSize: 3, - minJumpSize: 10, - wordWrap: false, + ...$a4b41c61879d57cc$var$DEFAULT_OPTIONS, + ...$a4b41c61879d57cc$var$DEFAULT_PARAMS, ...userParams }; const beforeLines = beforeText ? difflib.stringAsLines(beforeText) : []; @@ -495,7 +532,12 @@ class $a4b41c61879d57cc$export$d7ae8a2952d3eaf0 { static buildViewFromOps(beforeText, afterText, ops, params) { const beforeLines = beforeText ? difflib.stringAsLines(beforeText) : []; const afterLines = afterText ? difflib.stringAsLines(afterText) : []; - var d = new $a4b41c61879d57cc$export$d7ae8a2952d3eaf0(beforeText, beforeLines, afterText, afterLines, ops, params); + const fullParams = { + ...$a4b41c61879d57cc$var$DEFAULT_PARAMS, + ...params + }; + const diffRanges = $a4b41c61879d57cc$var$enforceMinJumpSize(ops, fullParams.minJumpSize); + var d = new $a4b41c61879d57cc$export$d7ae8a2952d3eaf0(beforeText, beforeLines, afterText, afterLines, diffRanges, params); return d.buildView_(); } } @@ -513,6 +555,12 @@ class $a4b41c61879d57cc$export$d7ae8a2952d3eaf0 { // structure. We convert them to single-line only here. return (0, $b86f11f5e9077ed0$export$1f6ff06b43176f68)(html); } +/** This removes small skips like "skip 1 line" that are disallowed by minJumpSize. */ function $a4b41c61879d57cc$var$enforceMinJumpSize(diffs, minJumpSize) { + return diffs.map((d)=>d.type === "skip" && d.before[1] - d.before[0] < minJumpSize ? { + ...d, + type: "equal" + } : d); +} window.codediff = { ...$a4b41c61879d57cc$export$d7ae8a2952d3eaf0, // These are exported for testing From 84347cc27118741b6d79a20064181019e690cd34 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Wed, 5 Jun 2024 16:18:07 -0400 Subject: [PATCH 3/3] fix tests --- tests/unified_diff_test.py | 2 +- webdiff/unified_diff.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unified_diff_test.py b/tests/unified_diff_test.py index 4714781..0ea24b7 100644 --- a/tests/unified_diff_test.py +++ b/tests/unified_diff_test.py @@ -102,7 +102,7 @@ def test_read_codes_delete(): def test_read_codes_skip(): codes = read_codes(PatchSet.from_string(skip_insert_hunk)) assert codes == [ - Code(type='skip', before=(0, 2), after=(0, 2)), + Code(type='skip', before=(0, 2), after=(0, 2), header='pytest==7.1.3'), Code(type='equal', before=(2, 5), after=(2, 5)), Code(type='insert', before=(5, 5), after=(5, 6)), Code(type='equal', before=(5, 6), after=(6, 7)), diff --git a/webdiff/unified_diff.py b/webdiff/unified_diff.py index d7f4578..6fbb134 100644 --- a/webdiff/unified_diff.py +++ b/webdiff/unified_diff.py @@ -15,7 +15,7 @@ class Code: """Line range on left side; zero-based, half-open interval.""" after: Tuple[int, int] """Line range on right side; zero-based, half-open interval.""" - header: Optional[str] + header: Optional[str] = None def read_codes(p: PatchSet) -> Union[List[Code], None]: @@ -28,7 +28,7 @@ def read_codes(p: PatchSet) -> Union[List[Code], None]: return None for hunk in pf: - header = hunk.section_header + header = hunk.section_header or None if hunk.source_start != last_source + 1: out.append( Code(