1
1
mirror of https://github.com/github/semantic.git synced 2024-12-28 09:21:35 +03:00

Merge pull request #227 from github/line-numbers

Add line numbers to diffs
This commit is contained in:
Rob Rix 2015-11-10 18:36:37 -05:00
commit 4f19e4c700
7 changed files with 160 additions and 55 deletions

View File

@ -10,11 +10,11 @@ import Foundation
import Madness import Madness
import Prelude import Prelude
public typealias CofreeJSON = Cofree<JSONLeaf, Range<String.CharacterView.Index>> public typealias CofreeJSON = Cofree<JSONLeaf, (Range<Line>, Range<Column>, Range<String.Index>)>
public typealias JSONParser = Parser<String.CharacterView, CofreeJSON>.Function public typealias JSONParser = Parser<String.CharacterView, CofreeJSON>.Function
// Inlined for performance reasons // Inlined for performance reasons
let whitespace = oneOf(" \n\r\t")* let whitespace = many(oneOf(" \n\r\t"))
// TODO: Parse unicode escape sequence // TODO: Parse unicode escape sequence
let escapeChar: StringParser = curry(+) <^> %"\\" <*> ({ String($0) } <^> oneOf("\\\"bfnrt")) let escapeChar: StringParser = curry(+) <^> %"\\" <*> ({ String($0) } <^> oneOf("\\\"bfnrt"))
@ -29,16 +29,16 @@ typealias MembersParser = Parser<String.CharacterView, [(String, CofreeJSON)]>.F
// Parses an array of (String, CofreeJSON) object members // Parses an array of (String, CofreeJSON) object members
func members(json: JSONParser) -> MembersParser { func members(json: JSONParser) -> MembersParser {
let keyAndKeyTerm: Parser<String.CharacterView, (String, CofreeJSON)>.Function = quoted --> { (_, range, key) in let keyAndKeyTerm: Parser<String.CharacterView, (String, CofreeJSON)>.Function = quoted --> { _, lines, columns, range, key in
(key, Cofree(range, .Leaf(.String(key)))) (key, Cofree((lines, columns, range), .Leaf(.String(key))))
} }
let pairs: Parser<String.CharacterView, (String, CofreeJSON)>.Function = (curry(pair) <^> let pairs: Parser<String.CharacterView, (String, CofreeJSON)>.Function = (curry(pair) <^>
keyAndKeyTerm keyAndKeyTerm
<* whitespace <* whitespace
<* %":" <* %":"
<* whitespace <* whitespace
<*> json) --> { (_, range, values) in <*> json) --> { _, lines, columns, range, values in
(values.0.0, Cofree(range, .Fixed([values.0.1, values.1]))) (values.0.0, Cofree((lines, columns, range), .Fixed([values.0.1, values.1])))
} }
return sepBy(pairs, whitespace <* %"," <* whitespace) return sepBy(pairs, whitespace <* %"," <* whitespace)
@ -46,7 +46,7 @@ func members(json: JSONParser) -> MembersParser {
public let json: JSONParser = fix { json in public let json: JSONParser = fix { json in
let string: JSONParser = quoted --> { let string: JSONParser = quoted --> {
Cofree($1, .Leaf(.String($2))) Cofree(($1, $2, $3), .Leaf(.String($4)))
} <?> "string" } <?> "string"
let array: JSONParser = %"[" let array: JSONParser = %"["
@ -55,7 +55,7 @@ public let json: JSONParser = fix { json in
<* whitespace <* whitespace
<* %"]" <* %"]"
--> { --> {
Cofree($1, .Indexed($2)) Cofree(($1, $2, $3), .Indexed($4))
} <?> "array" } <?> "array"
let object: JSONParser = %"{" let object: JSONParser = %"{"
@ -63,21 +63,21 @@ public let json: JSONParser = fix { json in
*> members(json) *> members(json)
<* whitespace <* whitespace
<* %"}" <* %"}"
--> { (_, range, values: [(String, CofreeJSON)]) in --> { (_, lines, columns, range, values: [(String, CofreeJSON)]) in
Cofree(range, .Keyed(Dictionary(elements: values))) Cofree((lines, columns, range), .Keyed(Dictionary(elements: values)))
} <?> "object" } <?> "object"
let numberParser: JSONParser = (number --> { _, range, value in let numberParser: JSONParser = (number --> { _, lines, columns, range, value in
Cofree(range, .Leaf(JSONLeaf.Number(value))) Cofree((lines, columns, range), .Leaf(JSONLeaf.Number(value)))
}) <?> "number" }) <?> "number"
let null: JSONParser = %"null" --> { (_, range, value) in let null: JSONParser = %"null" --> { _, lines, columns, range, value in
return Cofree(range, .Leaf(.Null)) return Cofree((lines, columns, range), .Leaf(.Null))
} <?> "null" } <?> "null"
let boolean: JSONParser = %"false" <|> %"true" --> { (_, range, value) in let boolean: JSONParser = %"false" <|> %"true" --> { _, lines, columns, range, value in
let boolean = value == "true" let boolean = value == "true"
return Cofree(range, .Leaf(.Boolean(boolean))) return Cofree((lines, columns, range), .Leaf(.Boolean(boolean)))
} <?> "boolean" } <?> "boolean"
// TODO: This should be JSON = dict <|> array and // TODO: This should be JSON = dict <|> array and

View File

@ -16,10 +16,10 @@ final class JSONParserTests: XCTestCase {
let expected: Cofree<JSONLeaf, Range<Int>> = Cofree(0..<42, .Keyed(["hello": fixedPairs])) let expected: Cofree<JSONLeaf, Range<Int>> = Cofree(0..<42, .Keyed(["hello": fixedPairs]))
let actual = Madness.parse(json, input: dictWithArray).right! let actual = Madness.parse(json, input: dictWithArray).right!
let firstIndex = actual.extract let startRange = actual.extract.2
let new: Cofree<JSONLeaf, Range<Int>> = actual.map({ range in let new: Cofree<JSONLeaf, Range<Int>> = actual.map({ tuple in
let startI: Int = firstIndex.startIndex.distanceTo(range.startIndex) let startI: Int = startRange.startIndex.distanceTo(tuple.2.startIndex)
let endI: Int = firstIndex.startIndex.distanceTo(range.endIndex) let endI: Int = startRange.startIndex.distanceTo(tuple.2.endIndex)
return Range(start: startI, end: endI) return Range(start: startI, end: endI)
}) })

@ -1 +1 @@
Subproject commit 68fcabcdecd9219f3b3de0f423ff5e08aaaffae8 Subproject commit 5f4f5f518f2ac881ee1ab4c188f111af3df92163

70
prototype/UI/diff.js vendored
View File

@ -129,7 +129,7 @@ function termToDOM(source, syntax, extract, getRange, recur) {
} }
/// Diff -> String -> DOM /// Diff -> String -> DOM
function diffToDOM(diff, sources) { function diffToDOM(diff, sources, lineNumbers) {
function getRange(diffOrTerm) { function getRange(diffOrTerm) {
if (diffOrTerm.pure != null) { if (diffOrTerm.pure != null) {
@ -160,17 +160,17 @@ function diffToDOM(diff, sources) {
} }
if (diff.pure != null) { if (diff.pure != null) {
return pureToDOM(sources, diff.pure, getRange, function(diff) { return pureToDOM(sources, diff.pure, lineNumbers, getRange, function(diff) {
return diffToDOM(diff, sources); return diffToDOM(diff, sources, lineNumbers);
}) })
} }
return rollToDOM(sources, diff.roll, getRange, function(diff) { return rollToDOM(sources, diff.roll, lineNumbers, getRange, function(diff) {
return diffToDOM(diff, sources); return diffToDOM(diff, sources, lineNumbers);
}) })
} }
function pureToDOM(sources, patch, getRangeFun, diffToDOMFun) { function pureToDOM(sources, patch, lineNumbers, getRangeFun, diffToDOMFun) {
var elementA, elementB; var elementA, elementB;
if (patch.before != null) { if (patch.before != null) {
elementA = termToDOM(sources.before, patch.before.unwrap, patch.before.extract, getRangeFun); elementA = termToDOM(sources.before, patch.before.unwrap, patch.before.extract, getRangeFun);
@ -178,6 +178,8 @@ function pureToDOM(sources, patch, getRangeFun, diffToDOMFun) {
if (patch.after != null) { if (patch.after != null) {
elementA.classList.add("replace"); elementA.classList.add("replace");
} }
elementA.setAttribute("data-line-number", patch.before.extract.lines[0])
} }
if (patch.after != null) { if (patch.after != null) {
@ -186,22 +188,28 @@ function pureToDOM(sources, patch, getRangeFun, diffToDOMFun) {
if (patch.before != null) { if (patch.before != null) {
elementB.classList.add("replace"); elementB.classList.add("replace");
} }
elementB.setAttribute("data-line-number", patch.after.extract.lines[0])
} }
if (elementA == null) { if (elementA == null) {
elementA = elementB.cloneNode(true) elementA = elementB.cloneNode(true);
elementA.classList.add("invisible") elementA.classList.add("invisible");
elementA.setAttribute("data-line-number", '\u00A0')
} }
if (elementB == null) { if (elementB == null) {
elementB = elementA.cloneNode(true) elementB = elementA.cloneNode(true);
elementB.classList.add("invisible") elementB.classList.add("invisible");
elementB.setAttribute("data-line-number", '\u00A0')
} }
return { "before": elementA || "", "after": elementB || "" }; return { "before": elementA || "", "after": elementB || "" };
} }
function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) { function rollToDOM(sources, rollOrTerm, lineNumbers, getRangeFun, diffToDOMFun) {
var syntax = rollOrTerm.unwrap var syntax = rollOrTerm.unwrap
var categories = { var categories = {
before: rollOrTerm.extract.before.categories, before: rollOrTerm.extract.before.categories,
@ -212,6 +220,11 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
after: rollOrTerm.extract.after.range after: rollOrTerm.extract.after.range
} }
var lines = {
before: rollOrTerm.extract.before.lines[0],
after: rollOrTerm.extract.after.lines[0]
}
var elementA; var elementA;
var elementB; var elementB;
if (syntax.leaf != null) { if (syntax.leaf != null) {
@ -219,12 +232,17 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
elementA.textContent = sources.before.substr(range.before[0], range.before[1]); elementA.textContent = sources.before.substr(range.before[0], range.before[1]);
elementB = document.createElement("span"); elementB = document.createElement("span");
elementB.textContent = sources.after.substr(range.after[0], range.after[1]); elementB.textContent = sources.after.substr(range.after[0], range.after[1]);
elementA.setAttribute("data-line-number", lines.before)
elementB.setAttribute("data-line-number", lines.after)
} else if (syntax.indexed != null || syntax.fixed != null) { } else if (syntax.indexed != null || syntax.fixed != null) {
var values = syntax.indexed || syntax.fixed; var values = syntax.indexed || syntax.fixed;
elementA = document.createElement("ul"); elementA = document.createElement("ul");
elementB = document.createElement("ul"); elementB = document.createElement("ul");
var previousBefore = range.before[0]; var previousBefore = range.before[0];
var previousAfter = range.after[0]; var previousAfter = range.after[0];
var lineNumbers = { "before": [], "after": [] };
for (i in values) { for (i in values) {
var child = values[i]; var child = values[i];
if (child.pure == "") continue; if (child.pure == "") continue;
@ -243,6 +261,11 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
previousBefore = beforeRange[0] + beforeRange[1]; previousBefore = beforeRange[0] + beforeRange[1];
} }
elementA.appendChild(li); elementA.appendChild(li);
var lineNumber = beforeAfterChild.before.getAttribute("data-line-number")
if (lineNumber != null) {
lineNumbers.before.push(lineNumber)
}
} }
if (childRange.after != null) { if (childRange.after != null) {
var afterRange = childRange.after; var afterRange = childRange.after;
@ -256,6 +279,11 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
previousAfter = afterRange[0] + afterRange[1]; previousAfter = afterRange[0] + afterRange[1];
} }
elementB.appendChild(li); elementB.appendChild(li);
var lineNumber = beforeAfterChild.after.getAttribute("data-line-number");
if (lineNumber != null) {
lineNumbers.after.push(lineNumber)
}
} }
} }
var beforeText = sources.before.substr(previousBefore, range.before[0] + range.before[1] - previousBefore); var beforeText = sources.before.substr(previousBefore, range.before[0] + range.before[1] - previousBefore);
@ -263,6 +291,9 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
elementA.appendChild(document.createTextNode(beforeText)); elementA.appendChild(document.createTextNode(beforeText));
elementB.appendChild(document.createTextNode(afterText)); elementB.appendChild(document.createTextNode(afterText));
elementA.setAttribute("data-line-number", lineNumbers.before)
elementB.setAttribute("data-line-number", lineNumbers.after)
} else if (syntax.keyed != null) { } else if (syntax.keyed != null) {
elementA = document.createElement("dl"); elementA = document.createElement("dl");
elementB = document.createElement("dl"); elementB = document.createElement("dl");
@ -330,6 +361,8 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
var previousA = range.before[0]; var previousA = range.before[0];
var previousB = range.after[0]; var previousB = range.after[0];
var lineNumbers = { "before": [ lines.before ], "after": [ lines.after ] };
zip(befores, afters, function (a, b) { zip(befores, afters, function (a, b) {
var key = a.key var key = a.key
var childElA = a.child var childElA = a.child
@ -388,6 +421,11 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
ddA.classList.add("invisible"); ddA.classList.add("invisible");
} }
var lineNumberA = childElA.getAttribute("data-line-number");
if (lineNumberA != null) {
lineNumbers.before.push(lineNumberA)
}
var dtB = wrap("dt", document.createTextNode(key)); var dtB = wrap("dt", document.createTextNode(key));
elementB.appendChild(dtB); elementB.appendChild(dtB);
var ddB = wrap("dd", childElB); var ddB = wrap("dd", childElB);
@ -397,6 +435,10 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
ddB.classList.add("invisible"); ddB.classList.add("invisible");
} }
var lineNumberB = childElB.getAttribute("data-line-number");
if (lineNumberB != null) {
lineNumbers.after.push(lineNumberB)
}
if (isFirst || !childElA.classList.contains("invisible")) { if (isFirst || !childElA.classList.contains("invisible")) {
previousA = childRangeA[0] + childRangeA[1] previousA = childRangeA[0] + childRangeA[1]
@ -440,6 +482,12 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
elementA.appendChild(document.createTextNode(textA)); elementA.appendChild(document.createTextNode(textA));
var textB = sources.after.substr(previousB, range.after[0] + range.after[1] - previousB); var textB = sources.after.substr(previousB, range.after[0] + range.after[1] - previousB);
elementB.appendChild(document.createTextNode(textB)); elementB.appendChild(document.createTextNode(textB));
lineNumbers.before.push(rollOrTerm.extract.before.lines[1])
lineNumbers.after.push(rollOrTerm.extract.after.lines[1])
elementA.setAttribute("data-line-number", lineNumbers.before)
elementB.setAttribute("data-line-number", lineNumbers.after)
} }
for (index in categories.before) { for (index in categories.before) {

View File

@ -43,6 +43,13 @@
color: initial; color: initial;
} }
ul.line-numbers {
list-style: none;
margin: 0;
padding: 0;
float: left;
}
.diff dt { .diff dt {
display: none; display: none;
} }
@ -110,16 +117,50 @@
</script> </script>
</head> </head>
<body> <body>
<div id="before" class="diff"></div> <div id="before">
<div id="after" class="diff"></div> <ul id="before-lines" class="line-numbers"></ul>
<div id="before-diff" class="diff"></div>
</div>
<div id="after">
<ul id="after-lines" class="line-numbers"></ul>
<div id="after-diff" class="diff"></div>
</div>
<script type="text/javascript"> <script type="text/javascript">
loadJSON((window.location.search || '?diff.json').substr(1), function (json) { var unique = function(array) {
var diff = diffFromJSON(json.diff); return array.reduce(function(accum, current) {
var dom = diffToDOM(diff, { "before": json["before"] , "after": json["after"] }) if (accum.indexOf(current) < 0) {
document.getElementById("before").appendChild(dom.before); accum.push(current);
document.getElementById("after").appendChild(dom.after); }
}); return accum;
</script> }, []);
</body> }
</html>
loadJSON((window.location.search || '?diff.json').substr(1), function (json) {
var diff = diffFromJSON(json.diff);
var beforeLinesEl = document.getElementById("before-lines")
var afterLinesEl = document.getElementById("after-lines")
var dom = diffToDOM(diff,
{ "before": json["before"] , "after": json["after"] },
{ "before": beforeLinesEl, "after": afterLinesEl });
var beforeLines = dom.before.getAttribute("data-line-number").split(",")
unique(beforeLines).forEach(function(lineNumber) {
var node = wrap("li", document.createTextNode(lineNumber));
beforeLinesEl.appendChild(node);
});
var afterLines = dom.after.getAttribute("data-line-number").split(",")
unique(afterLines).forEach(function(lineNumber) {
var node = wrap("li", document.createTextNode(lineNumber));
afterLinesEl.appendChild(node);
});
document.getElementById("before-diff").appendChild(dom.before);
document.getElementById("after-diff").appendChild(dom.after);
});
</script>
</body>
</html>

View File

@ -1,17 +1,25 @@
struct Info: Categorizable, CustomJSONConvertible, Equatable { struct Info: Categorizable, CustomJSONConvertible, Equatable {
init(range: Range<Int>, categories: Set<String>) { init(range: Range<Int>, lines: Range<Line>, columns: Range<Column>, categories: Set<String>) {
self.range = range self.range = range
self.lines = lines
self.columns = columns
self.categories = categories self.categories = categories
} }
init(range: Range<String.CharacterView.Index>, categories: Set<String>) { init(range: Range<String.CharacterView.Index>, lines: Range<Line>, columns: Range<Column>, categories: Set<String>) {
// FIXME: this is terrible. see also https://github.com/github/semantic-diff/issues/136 // FIXME: this is terrible. see also https://github.com/github/semantic-diff/issues/136
self.range = Int(String(range.startIndex))!..<Int(String(range.endIndex))! self.range = Int(String(range.startIndex))!..<Int(String(range.endIndex))!
self.lines = lines
self.columns = columns
self.categories = categories self.categories = categories
} }
let range: Range<Int> let range: Range<Int>
let lines: Range<Line>
let columns: Range<Column>
// MARK: Categorizable // MARK: Categorizable
@ -23,14 +31,16 @@ struct Info: Categorizable, CustomJSONConvertible, Equatable {
var JSON: Doubt.JSON { var JSON: Doubt.JSON {
return [ return [
"range": range.JSON, "range": range.JSON,
"lines": lines.JSON,
"columns": columns.JSON,
"categories": Array(categories).JSON "categories": Array(categories).JSON
] ]
} }
} }
func == (left: Info, right: Info) -> Bool { func == (left: Info, right: Info) -> Bool {
return left.range == right.range && left.categories == right.categories return left.range == right.range && left.categories == right.categories && left.lines == left.lines && left.columns == right.columns
} }
import Madness
import Doubt import Doubt

View File

@ -93,16 +93,20 @@ func termWithInput(language: TSLanguage)(_ string: String) throws -> Term {
} }
} (root, "program") } (root, "program")
.map { node, category in .map { node, category in
Info(range: node.range, categories: [ category ]) // TODO: Calculate line and column from TSNodes
Info(range: node.range, lines: 0..<1, columns: 0..<1, categories: [ category ])
} }
} }
} }
func toTerm(term: CofreeJSON) -> Term { func toTerm(term: CofreeJSON) -> Term {
let annotation = Info(range: term.extract, categories: []) let lines = term.extract.0
let columns = term.extract.1
let range = term.extract.2
let annotation = Info(range: range, lines: lines, columns: columns, categories: [])
switch term.unwrap { switch term.unwrap {
case let .Leaf(a): case let .Leaf(a):
return Term(Info(range: term.extract, categories: a.categories), Syntax<Term, String>.Leaf(String(a))) return Term(Info(range: range, lines: lines, columns: columns, categories: a.categories), Syntax<Term, String>.Leaf(String(a)))
case let .Indexed(i): case let .Indexed(i):
return Term(annotation, .Indexed(i.map(toTerm))) return Term(annotation, .Indexed(i.map(toTerm)))
case let .Fixed(f): case let .Fixed(f):
@ -115,14 +119,16 @@ func toTerm(term: CofreeJSON) -> Term {
func lines(input: String) -> Term { func lines(input: String) -> Term {
var lines: [Term] = [] var lines: [Term] = []
var previous = 0 var previous = 0
var lineNumber = 0
input.enumerateSubstringsInRange(input.characters.indices, options: .ByLines) { (line, _, enclosingRange, _) in input.enumerateSubstringsInRange(input.characters.indices, options: .ByLines) { (line, _, enclosingRange, _) in
let range: Range<Int> = previous..<(previous + enclosingRange.count) let range: Range<Int> = previous..<(previous + enclosingRange.count)
previous = range.endIndex previous = range.endIndex
if let line = line { if let line = line {
lines.append(Term(Info(range: range, categories: []), Syntax.Leaf(line))) lineNumber += 1
lines.append(Term(Info(range: range, lines: 0..<lineNumber, columns: 0..<1, categories: []), Syntax.Leaf(line)))
} }
} }
return Term(Info(range: 0..<input.utf16.count, categories: []), .Indexed(lines)) return Term(Info(range: 0..<input.utf16.count, lines: 0..<lineNumber, columns: 0..<1, categories: []), .Indexed(lines))
} }
func parserForType(type: String) -> String throws -> Term { func parserForType(type: String) -> String throws -> Term {
@ -154,8 +160,8 @@ extension ForwardIndexType {
func refineLeafReplacement(aString: String, _ bString: String)(_ patch: Patch<Term>) -> Diff { func refineLeafReplacement(aString: String, _ bString: String)(_ patch: Patch<Term>) -> Diff {
switch patch { switch patch {
case let .Replace(.Unroll(aExtract, .Leaf), .Unroll(bExtract, .Leaf)): case let .Replace(.Unroll(aExtract, .Leaf), .Unroll(bExtract, .Leaf)):
let a = aString.utf16[aExtract.range].enumerate().map { Term(Info(range: (aExtract.range.startIndex + $0).range, categories: aExtract.categories), .Leaf(String($1))) } let a = aString.utf16[aExtract.range].enumerate().map { Term(Info(range: (aExtract.range.startIndex + $0).range, lines: aExtract.lines, columns: aExtract.columns, categories: aExtract.categories), .Leaf(String($1))) }
let b = bString.utf16[bExtract.range].enumerate().map { Term(Info(range: (bExtract.range.startIndex + $0).range, categories: bExtract.categories), .Leaf(String($1))) } let b = bString.utf16[bExtract.range].enumerate().map { Term(Info(range: (bExtract.range.startIndex + $0).range, lines: bExtract.lines, columns: bExtract.columns, categories: bExtract.categories), .Leaf(String($1))) }
return .Roll((aExtract, bExtract), .Indexed(SES(a, b, cost: Diff.sum(Patch.sum), recur: { Term.equals(annotation: const(true), leaf: ==)($0, $1) ? Term.zip($0, $1).map(Diff.init) : Diff.Replace($0, $1) }))) return .Roll((aExtract, bExtract), .Indexed(SES(a, b, cost: Diff.sum(Patch.sum), recur: { Term.equals(annotation: const(true), leaf: ==)($0, $1) ? Term.zip($0, $1).map(Diff.init) : Diff.Replace($0, $1) })))
default: default:
return .Pure(patch) return .Pure(patch)