mirror of
https://github.com/github/semantic.git
synced 2024-12-27 17:05:33 +03:00
Merge pull request #227 from github/line-numbers
Add line numbers to diffs
This commit is contained in:
commit
4f19e4c700
@ -10,11 +10,11 @@ import Foundation
|
||||
import Madness
|
||||
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
|
||||
|
||||
// Inlined for performance reasons
|
||||
let whitespace = oneOf(" \n\r\t")*
|
||||
let whitespace = many(oneOf(" \n\r\t"))
|
||||
|
||||
// TODO: Parse unicode escape sequence
|
||||
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
|
||||
func members(json: JSONParser) -> MembersParser {
|
||||
let keyAndKeyTerm: Parser<String.CharacterView, (String, CofreeJSON)>.Function = quoted --> { (_, range, key) in
|
||||
(key, Cofree(range, .Leaf(.String(key))))
|
||||
let keyAndKeyTerm: Parser<String.CharacterView, (String, CofreeJSON)>.Function = quoted --> { _, lines, columns, range, key in
|
||||
(key, Cofree((lines, columns, range), .Leaf(.String(key))))
|
||||
}
|
||||
let pairs: Parser<String.CharacterView, (String, CofreeJSON)>.Function = (curry(pair) <^>
|
||||
keyAndKeyTerm
|
||||
<* whitespace
|
||||
<* %":"
|
||||
<* whitespace
|
||||
<*> json) --> { (_, range, values) in
|
||||
(values.0.0, Cofree(range, .Fixed([values.0.1, values.1])))
|
||||
<*> json) --> { _, lines, columns, range, values in
|
||||
(values.0.0, Cofree((lines, columns, range), .Fixed([values.0.1, values.1])))
|
||||
}
|
||||
|
||||
return sepBy(pairs, whitespace <* %"," <* whitespace)
|
||||
@ -46,7 +46,7 @@ func members(json: JSONParser) -> MembersParser {
|
||||
|
||||
public let json: JSONParser = fix { json in
|
||||
let string: JSONParser = quoted --> {
|
||||
Cofree($1, .Leaf(.String($2)))
|
||||
Cofree(($1, $2, $3), .Leaf(.String($4)))
|
||||
} <?> "string"
|
||||
|
||||
let array: JSONParser = %"["
|
||||
@ -55,7 +55,7 @@ public let json: JSONParser = fix { json in
|
||||
<* whitespace
|
||||
<* %"]"
|
||||
--> {
|
||||
Cofree($1, .Indexed($2))
|
||||
Cofree(($1, $2, $3), .Indexed($4))
|
||||
} <?> "array"
|
||||
|
||||
let object: JSONParser = %"{"
|
||||
@ -63,21 +63,21 @@ public let json: JSONParser = fix { json in
|
||||
*> members(json)
|
||||
<* whitespace
|
||||
<* %"}"
|
||||
--> { (_, range, values: [(String, CofreeJSON)]) in
|
||||
Cofree(range, .Keyed(Dictionary(elements: values)))
|
||||
--> { (_, lines, columns, range, values: [(String, CofreeJSON)]) in
|
||||
Cofree((lines, columns, range), .Keyed(Dictionary(elements: values)))
|
||||
} <?> "object"
|
||||
|
||||
let numberParser: JSONParser = (number --> { _, range, value in
|
||||
Cofree(range, .Leaf(JSONLeaf.Number(value)))
|
||||
let numberParser: JSONParser = (number --> { _, lines, columns, range, value in
|
||||
Cofree((lines, columns, range), .Leaf(JSONLeaf.Number(value)))
|
||||
}) <?> "number"
|
||||
|
||||
let null: JSONParser = %"null" --> { (_, range, value) in
|
||||
return Cofree(range, .Leaf(.Null))
|
||||
let null: JSONParser = %"null" --> { _, lines, columns, range, value in
|
||||
return Cofree((lines, columns, range), .Leaf(.Null))
|
||||
} <?> "null"
|
||||
|
||||
let boolean: JSONParser = %"false" <|> %"true" --> { (_, range, value) in
|
||||
let boolean: JSONParser = %"false" <|> %"true" --> { _, lines, columns, range, value in
|
||||
let boolean = value == "true"
|
||||
return Cofree(range, .Leaf(.Boolean(boolean)))
|
||||
return Cofree((lines, columns, range), .Leaf(.Boolean(boolean)))
|
||||
} <?> "boolean"
|
||||
|
||||
// TODO: This should be JSON = dict <|> array and
|
||||
|
@ -16,10 +16,10 @@ final class JSONParserTests: XCTestCase {
|
||||
|
||||
let expected: Cofree<JSONLeaf, Range<Int>> = Cofree(0..<42, .Keyed(["hello": fixedPairs]))
|
||||
let actual = Madness.parse(json, input: dictWithArray).right!
|
||||
let firstIndex = actual.extract
|
||||
let new: Cofree<JSONLeaf, Range<Int>> = actual.map({ range in
|
||||
let startI: Int = firstIndex.startIndex.distanceTo(range.startIndex)
|
||||
let endI: Int = firstIndex.startIndex.distanceTo(range.endIndex)
|
||||
let startRange = actual.extract.2
|
||||
let new: Cofree<JSONLeaf, Range<Int>> = actual.map({ tuple in
|
||||
let startI: Int = startRange.startIndex.distanceTo(tuple.2.startIndex)
|
||||
let endI: Int = startRange.startIndex.distanceTo(tuple.2.endIndex)
|
||||
return Range(start: startI, end: endI)
|
||||
})
|
||||
|
||||
|
2
prototype/External/Madness
vendored
2
prototype/External/Madness
vendored
@ -1 +1 @@
|
||||
Subproject commit 68fcabcdecd9219f3b3de0f423ff5e08aaaffae8
|
||||
Subproject commit 5f4f5f518f2ac881ee1ab4c188f111af3df92163
|
70
prototype/UI/diff.js
vendored
70
prototype/UI/diff.js
vendored
@ -129,7 +129,7 @@ function termToDOM(source, syntax, extract, getRange, recur) {
|
||||
}
|
||||
|
||||
/// Diff -> String -> DOM
|
||||
function diffToDOM(diff, sources) {
|
||||
function diffToDOM(diff, sources, lineNumbers) {
|
||||
|
||||
function getRange(diffOrTerm) {
|
||||
if (diffOrTerm.pure != null) {
|
||||
@ -160,17 +160,17 @@ function diffToDOM(diff, sources) {
|
||||
}
|
||||
|
||||
if (diff.pure != null) {
|
||||
return pureToDOM(sources, diff.pure, getRange, function(diff) {
|
||||
return diffToDOM(diff, sources);
|
||||
return pureToDOM(sources, diff.pure, lineNumbers, getRange, function(diff) {
|
||||
return diffToDOM(diff, sources, lineNumbers);
|
||||
})
|
||||
}
|
||||
|
||||
return rollToDOM(sources, diff.roll, getRange, function(diff) {
|
||||
return diffToDOM(diff, sources);
|
||||
return rollToDOM(sources, diff.roll, lineNumbers, getRange, function(diff) {
|
||||
return diffToDOM(diff, sources, lineNumbers);
|
||||
})
|
||||
}
|
||||
|
||||
function pureToDOM(sources, patch, getRangeFun, diffToDOMFun) {
|
||||
function pureToDOM(sources, patch, lineNumbers, getRangeFun, diffToDOMFun) {
|
||||
var elementA, elementB;
|
||||
if (patch.before != null) {
|
||||
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) {
|
||||
elementA.classList.add("replace");
|
||||
}
|
||||
|
||||
elementA.setAttribute("data-line-number", patch.before.extract.lines[0])
|
||||
}
|
||||
|
||||
if (patch.after != null) {
|
||||
@ -186,22 +188,28 @@ function pureToDOM(sources, patch, getRangeFun, diffToDOMFun) {
|
||||
if (patch.before != null) {
|
||||
elementB.classList.add("replace");
|
||||
}
|
||||
|
||||
elementB.setAttribute("data-line-number", patch.after.extract.lines[0])
|
||||
}
|
||||
|
||||
if (elementA == null) {
|
||||
elementA = elementB.cloneNode(true)
|
||||
elementA.classList.add("invisible")
|
||||
elementA = elementB.cloneNode(true);
|
||||
elementA.classList.add("invisible");
|
||||
|
||||
elementA.setAttribute("data-line-number", '\u00A0')
|
||||
}
|
||||
|
||||
if (elementB == null) {
|
||||
elementB = elementA.cloneNode(true)
|
||||
elementB.classList.add("invisible")
|
||||
elementB = elementA.cloneNode(true);
|
||||
elementB.classList.add("invisible");
|
||||
|
||||
elementB.setAttribute("data-line-number", '\u00A0')
|
||||
}
|
||||
|
||||
return { "before": elementA || "", "after": elementB || "" };
|
||||
}
|
||||
|
||||
function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
|
||||
function rollToDOM(sources, rollOrTerm, lineNumbers, getRangeFun, diffToDOMFun) {
|
||||
var syntax = rollOrTerm.unwrap
|
||||
var categories = {
|
||||
before: rollOrTerm.extract.before.categories,
|
||||
@ -212,6 +220,11 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
|
||||
after: rollOrTerm.extract.after.range
|
||||
}
|
||||
|
||||
var lines = {
|
||||
before: rollOrTerm.extract.before.lines[0],
|
||||
after: rollOrTerm.extract.after.lines[0]
|
||||
}
|
||||
|
||||
var elementA;
|
||||
var elementB;
|
||||
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]);
|
||||
elementB = document.createElement("span");
|
||||
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) {
|
||||
var values = syntax.indexed || syntax.fixed;
|
||||
elementA = document.createElement("ul");
|
||||
elementB = document.createElement("ul");
|
||||
var previousBefore = range.before[0];
|
||||
var previousAfter = range.after[0];
|
||||
|
||||
var lineNumbers = { "before": [], "after": [] };
|
||||
for (i in values) {
|
||||
var child = values[i];
|
||||
if (child.pure == "") continue;
|
||||
@ -243,6 +261,11 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
|
||||
previousBefore = beforeRange[0] + beforeRange[1];
|
||||
}
|
||||
elementA.appendChild(li);
|
||||
|
||||
var lineNumber = beforeAfterChild.before.getAttribute("data-line-number")
|
||||
if (lineNumber != null) {
|
||||
lineNumbers.before.push(lineNumber)
|
||||
}
|
||||
}
|
||||
if (childRange.after != null) {
|
||||
var afterRange = childRange.after;
|
||||
@ -256,6 +279,11 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
|
||||
previousAfter = afterRange[0] + afterRange[1];
|
||||
}
|
||||
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);
|
||||
@ -263,6 +291,9 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
|
||||
|
||||
elementA.appendChild(document.createTextNode(beforeText));
|
||||
elementB.appendChild(document.createTextNode(afterText));
|
||||
|
||||
elementA.setAttribute("data-line-number", lineNumbers.before)
|
||||
elementB.setAttribute("data-line-number", lineNumbers.after)
|
||||
} else if (syntax.keyed != null) {
|
||||
elementA = document.createElement("dl");
|
||||
elementB = document.createElement("dl");
|
||||
@ -330,6 +361,8 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
|
||||
var previousA = range.before[0];
|
||||
var previousB = range.after[0];
|
||||
|
||||
var lineNumbers = { "before": [ lines.before ], "after": [ lines.after ] };
|
||||
|
||||
zip(befores, afters, function (a, b) {
|
||||
var key = a.key
|
||||
var childElA = a.child
|
||||
@ -388,6 +421,11 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
|
||||
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));
|
||||
elementB.appendChild(dtB);
|
||||
var ddB = wrap("dd", childElB);
|
||||
@ -397,6 +435,10 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
|
||||
ddB.classList.add("invisible");
|
||||
}
|
||||
|
||||
var lineNumberB = childElB.getAttribute("data-line-number");
|
||||
if (lineNumberB != null) {
|
||||
lineNumbers.after.push(lineNumberB)
|
||||
}
|
||||
|
||||
if (isFirst || !childElA.classList.contains("invisible")) {
|
||||
previousA = childRangeA[0] + childRangeA[1]
|
||||
@ -440,6 +482,12 @@ function rollToDOM(sources, rollOrTerm, getRangeFun, diffToDOMFun) {
|
||||
elementA.appendChild(document.createTextNode(textA));
|
||||
var textB = sources.after.substr(previousB, range.after[0] + range.after[1] - previousB);
|
||||
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) {
|
||||
|
@ -43,6 +43,13 @@
|
||||
color: initial;
|
||||
}
|
||||
|
||||
ul.line-numbers {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.diff dt {
|
||||
display: none;
|
||||
}
|
||||
@ -110,16 +117,50 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="before" class="diff"></div>
|
||||
<div id="after" class="diff"></div>
|
||||
<div id="before">
|
||||
<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">
|
||||
loadJSON((window.location.search || '?diff.json').substr(1), function (json) {
|
||||
var diff = diffFromJSON(json.diff);
|
||||
var dom = diffToDOM(diff, { "before": json["before"] , "after": json["after"] })
|
||||
document.getElementById("before").appendChild(dom.before);
|
||||
document.getElementById("after").appendChild(dom.after);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<script type="text/javascript">
|
||||
var unique = function(array) {
|
||||
return array.reduce(function(accum, current) {
|
||||
if (accum.indexOf(current) < 0) {
|
||||
accum.push(current);
|
||||
}
|
||||
return accum;
|
||||
}, []);
|
||||
}
|
||||
|
||||
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>
|
||||
|
@ -1,17 +1,25 @@
|
||||
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.lines = lines
|
||||
self.columns = columns
|
||||
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
|
||||
self.range = Int(String(range.startIndex))!..<Int(String(range.endIndex))!
|
||||
self.lines = lines
|
||||
self.columns = columns
|
||||
self.categories = categories
|
||||
}
|
||||
|
||||
let range: Range<Int>
|
||||
|
||||
let lines: Range<Line>
|
||||
|
||||
let columns: Range<Column>
|
||||
|
||||
|
||||
// MARK: Categorizable
|
||||
|
||||
@ -23,14 +31,16 @@ struct Info: Categorizable, CustomJSONConvertible, Equatable {
|
||||
var JSON: Doubt.JSON {
|
||||
return [
|
||||
"range": range.JSON,
|
||||
"lines": lines.JSON,
|
||||
"columns": columns.JSON,
|
||||
"categories": Array(categories).JSON
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -93,16 +93,20 @@ func termWithInput(language: TSLanguage)(_ string: String) throws -> Term {
|
||||
}
|
||||
} (root, "program")
|
||||
.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 {
|
||||
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 {
|
||||
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):
|
||||
return Term(annotation, .Indexed(i.map(toTerm)))
|
||||
case let .Fixed(f):
|
||||
@ -115,14 +119,16 @@ func toTerm(term: CofreeJSON) -> Term {
|
||||
func lines(input: String) -> Term {
|
||||
var lines: [Term] = []
|
||||
var previous = 0
|
||||
var lineNumber = 0
|
||||
input.enumerateSubstringsInRange(input.characters.indices, options: .ByLines) { (line, _, enclosingRange, _) in
|
||||
let range: Range<Int> = previous..<(previous + enclosingRange.count)
|
||||
previous = range.endIndex
|
||||
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 {
|
||||
@ -154,8 +160,8 @@ extension ForwardIndexType {
|
||||
func refineLeafReplacement(aString: String, _ bString: String)(_ patch: Patch<Term>) -> Diff {
|
||||
switch patch {
|
||||
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 b = bString.utf16[bExtract.range].enumerate().map { Term(Info(range: (bExtract.range.startIndex + $0).range, categories: bExtract.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, 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) })))
|
||||
default:
|
||||
return .Pure(patch)
|
||||
|
Loading…
Reference in New Issue
Block a user