2015-10-26 23:00:52 +03:00
|
|
|
|
import Cocoa
|
|
|
|
|
import Doubt
|
2015-10-28 01:38:25 +03:00
|
|
|
|
import Prelude
|
2015-10-30 21:56:38 +03:00
|
|
|
|
import Madness
|
2015-10-27 18:04:39 +03:00
|
|
|
|
|
2015-11-03 18:19:51 +03:00
|
|
|
|
func benchmark<T>(label: String? = nil, _ f: () throws -> T) rethrows -> T {
|
2015-10-30 20:59:22 +03:00
|
|
|
|
let start = NSDate.timeIntervalSinceReferenceDate()
|
2015-11-03 18:19:51 +03:00
|
|
|
|
let result = try f()
|
2015-10-30 20:59:22 +03:00
|
|
|
|
let end = NSDate.timeIntervalSinceReferenceDate()
|
|
|
|
|
print((label.map { "\($0): " } ?? "") + "\(end - start)s")
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-29 01:17:01 +03:00
|
|
|
|
extension String: ErrorType {}
|
|
|
|
|
|
2015-10-29 01:12:16 +03:00
|
|
|
|
typealias Term = Cofree<String, Info>
|
2015-11-02 22:18:30 +03:00
|
|
|
|
typealias Diff = Free<Term.Leaf, (Term.Annotation, Term.Annotation), Patch<Term>>
|
2015-10-29 23:20:57 +03:00
|
|
|
|
typealias Parser = String throws -> Term
|
2015-10-28 01:37:34 +03:00
|
|
|
|
|
2015-10-29 23:04:10 +03:00
|
|
|
|
struct Source {
|
2015-10-29 23:19:23 +03:00
|
|
|
|
init(_ argument: String) throws {
|
2015-10-31 00:49:59 +03:00
|
|
|
|
let supportedSchemes = [ "http", "https", "file" ]
|
|
|
|
|
if let URL = NSURL(string: argument) where supportedSchemes.contains(URL.scheme) {
|
|
|
|
|
self.URL = URL
|
|
|
|
|
} else {
|
|
|
|
|
self.URL = NSURL(fileURLWithPath: argument)
|
|
|
|
|
}
|
2015-10-29 23:50:51 +03:00
|
|
|
|
contents = try NSString(contentsOfURL: URL, encoding: NSUTF8StringEncoding) as String
|
2015-10-29 23:06:51 +03:00
|
|
|
|
}
|
|
|
|
|
|
2015-10-29 23:04:10 +03:00
|
|
|
|
let URL: NSURL
|
2015-10-31 00:21:47 +03:00
|
|
|
|
var type: String {
|
2015-10-31 00:25:19 +03:00
|
|
|
|
if let pathExtension = URL.pathExtension where pathExtension != "" { return pathExtension }
|
|
|
|
|
return URL.fragment ?? ""
|
2015-10-31 00:21:47 +03:00
|
|
|
|
}
|
2015-10-29 23:50:51 +03:00
|
|
|
|
let contents: String
|
2015-10-29 23:09:31 +03:00
|
|
|
|
|
2015-10-29 23:10:01 +03:00
|
|
|
|
private static let languagesByType: [String:TSLanguage] = [
|
2015-10-29 23:09:31 +03:00
|
|
|
|
"js": ts_language_javascript(),
|
|
|
|
|
"c": ts_language_c(),
|
|
|
|
|
"h": ts_language_c(),
|
|
|
|
|
]
|
2015-10-29 23:04:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
2015-10-29 17:00:56 +03:00
|
|
|
|
|
|
|
|
|
extension String.UTF16View {
|
|
|
|
|
subscript (range: Range<Int>) -> String.UTF16View {
|
|
|
|
|
return self[Index(_offset: range.startIndex)..<Index(_offset: range.endIndex)]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-29 21:59:18 +03:00
|
|
|
|
|
2015-10-29 22:09:57 +03:00
|
|
|
|
/// Allow predicates to occur in pattern matching.
|
2015-10-29 22:09:52 +03:00
|
|
|
|
func ~= <A> (left: A -> Bool, right: A) -> Bool {
|
|
|
|
|
return left(right)
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-29 17:00:56 +03:00
|
|
|
|
|
2015-10-29 23:23:58 +03:00
|
|
|
|
func termWithInput(language: TSLanguage)(_ string: String) throws -> Term {
|
2015-10-29 23:14:05 +03:00
|
|
|
|
let keyedProductions: Set<String> = [ "object" ]
|
|
|
|
|
let fixedProductions: Set<String> = [ "pair", "rel_op", "math_op", "bool_op", "bitwise_op", "type_op", "math_assignment", "assignment", "subscript_access", "member_access", "new_expression", "function_call", "function", "ternary" ]
|
2015-10-27 18:04:18 +03:00
|
|
|
|
let document = ts_document_make()
|
2015-10-28 01:26:39 +03:00
|
|
|
|
defer { ts_document_free(document) }
|
2015-10-29 23:23:58 +03:00
|
|
|
|
return try string.withCString {
|
2015-10-29 22:20:59 +03:00
|
|
|
|
ts_document_set_language(document, language)
|
2015-10-28 01:29:44 +03:00
|
|
|
|
ts_document_set_input_string(document, $0)
|
|
|
|
|
ts_document_parse(document)
|
|
|
|
|
let root = ts_document_root_node(document)
|
2015-10-28 01:07:00 +03:00
|
|
|
|
|
2015-10-29 23:23:58 +03:00
|
|
|
|
return try Cofree
|
2015-10-28 21:20:46 +03:00
|
|
|
|
.ana { node, category in
|
2015-10-28 22:38:20 +03:00
|
|
|
|
let count = node.namedChildren.count
|
2015-10-29 17:10:13 +03:00
|
|
|
|
guard count > 0 else { return try Syntax.Leaf(node.substring(string)) }
|
2015-10-28 22:05:25 +03:00
|
|
|
|
switch category {
|
2015-10-29 22:12:16 +03:00
|
|
|
|
case fixedProductions.contains:
|
2015-10-28 23:20:39 +03:00
|
|
|
|
return try .Fixed(node.namedChildren.map {
|
2015-10-29 00:28:58 +03:00
|
|
|
|
($0, try $0.category(document))
|
2015-10-28 23:20:39 +03:00
|
|
|
|
})
|
2015-10-29 22:10:37 +03:00
|
|
|
|
case keyedProductions.contains:
|
2015-10-28 23:18:38 +03:00
|
|
|
|
return try .Keyed(Dictionary(elements: node.namedChildren.map {
|
2015-10-29 01:32:34 +03:00
|
|
|
|
switch try $0.category(document) {
|
|
|
|
|
case "pair":
|
2015-10-29 17:03:53 +03:00
|
|
|
|
return try ($0.namedChildren[0].substring(string), ($0, "pair"))
|
2015-10-29 01:32:34 +03:00
|
|
|
|
default:
|
2015-10-29 01:33:04 +03:00
|
|
|
|
// We might have a comment inside an object literal. It should still be assigned a key, however.
|
2015-10-29 17:10:13 +03:00
|
|
|
|
return try (try node.substring(string), ($0, $0.category(document)))
|
2015-10-29 01:32:34 +03:00
|
|
|
|
}
|
2015-10-28 23:18:38 +03:00
|
|
|
|
}))
|
2015-10-28 22:05:25 +03:00
|
|
|
|
default:
|
2015-10-28 22:31:08 +03:00
|
|
|
|
return try .Indexed(node.namedChildren.map {
|
2015-10-29 00:28:58 +03:00
|
|
|
|
($0, try $0.category(document))
|
2015-10-28 22:05:25 +03:00
|
|
|
|
})
|
|
|
|
|
}
|
2015-10-29 00:48:51 +03:00
|
|
|
|
} (root, "program")
|
2015-10-28 21:20:46 +03:00
|
|
|
|
.map { node, category in
|
2015-11-04 23:12:22 +03:00
|
|
|
|
// TODO: Calculate line and column from TSNodes
|
2015-11-08 23:21:14 +03:00
|
|
|
|
Info(range: node.range, lines: 0..<1, columns: 0..<1, categories: [ category ])
|
2015-10-28 01:10:17 +03:00
|
|
|
|
}
|
2015-10-28 01:29:44 +03:00
|
|
|
|
}
|
2015-10-28 01:26:39 +03:00
|
|
|
|
}
|
2015-10-28 01:10:17 +03:00
|
|
|
|
|
2015-10-30 21:56:05 +03:00
|
|
|
|
func toTerm(term: CofreeJSON) -> Term {
|
2015-11-08 23:21:14 +03:00
|
|
|
|
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: [])
|
2015-10-30 21:56:05 +03:00
|
|
|
|
switch term.unwrap {
|
|
|
|
|
case let .Leaf(a):
|
2015-11-08 23:21:14 +03:00
|
|
|
|
return Term(Info(range: range, lines: lines, columns: columns, categories: a.categories), Syntax<Term, String>.Leaf(String(a)))
|
2015-10-30 21:56:05 +03:00
|
|
|
|
case let .Indexed(i):
|
|
|
|
|
return Term(annotation, .Indexed(i.map(toTerm)))
|
|
|
|
|
case let .Fixed(f):
|
|
|
|
|
return Term(annotation, .Fixed(f.map(toTerm)))
|
|
|
|
|
case let .Keyed(k):
|
|
|
|
|
return Term(annotation, .Keyed(Dictionary(elements: k.map { ($0, toTerm($1)) })))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-31 01:13:22 +03:00
|
|
|
|
func lines(input: String) -> Term {
|
|
|
|
|
var lines: [Term] = []
|
|
|
|
|
var previous = 0
|
2015-11-04 23:12:22 +03:00
|
|
|
|
var lineNumber = 0
|
2015-10-31 01:13:22 +03:00
|
|
|
|
input.enumerateSubstringsInRange(input.characters.indices, options: .ByLines) { (line, _, enclosingRange, _) in
|
2015-10-31 01:16:46 +03:00
|
|
|
|
let range: Range<Int> = previous..<(previous + enclosingRange.count)
|
2015-10-31 01:13:22 +03:00
|
|
|
|
previous = range.endIndex
|
|
|
|
|
if let line = line {
|
2015-11-04 23:12:22 +03:00
|
|
|
|
lineNumber += 1
|
2015-11-08 23:21:14 +03:00
|
|
|
|
lines.append(Term(Info(range: range, lines: 0..<lineNumber, columns: 0..<1, categories: []), Syntax.Leaf(line)))
|
2015-10-31 01:13:22 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2015-11-08 23:21:14 +03:00
|
|
|
|
return Term(Info(range: 0..<input.utf16.count, lines: 0..<lineNumber, columns: 0..<1, categories: []), .Indexed(lines))
|
2015-10-31 01:13:22 +03:00
|
|
|
|
}
|
|
|
|
|
|
2015-10-31 01:15:16 +03:00
|
|
|
|
func parserForType(type: String) -> String throws -> Term {
|
2015-10-30 21:56:18 +03:00
|
|
|
|
switch type {
|
2015-10-30 21:56:38 +03:00
|
|
|
|
case "json":
|
|
|
|
|
return { (input: String) throws -> Term in
|
|
|
|
|
switch parse(json, input: input.characters) {
|
|
|
|
|
case let .Right(term):
|
|
|
|
|
return toTerm(term)
|
|
|
|
|
case let .Left(error):
|
|
|
|
|
throw error.description
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-10-30 21:56:18 +03:00
|
|
|
|
default:
|
2015-10-31 01:15:16 +03:00
|
|
|
|
if let parser = Source.languagesByType[type].map(termWithInput) {
|
|
|
|
|
return parser
|
|
|
|
|
}
|
|
|
|
|
return lines
|
2015-10-30 21:56:18 +03:00
|
|
|
|
}
|
2015-10-30 20:58:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
2015-11-06 01:10:41 +03:00
|
|
|
|
extension ForwardIndexType {
|
|
|
|
|
/// The range encompassing a single index.
|
|
|
|
|
var range: Range<Self> {
|
|
|
|
|
return self..<self.successor()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-06 01:26:06 +03:00
|
|
|
|
func refineLeafReplacement(aString: String, _ bString: String)(_ patch: Patch<Term>) -> Diff {
|
2015-11-06 01:18:56 +03:00
|
|
|
|
switch patch {
|
|
|
|
|
case let .Replace(.Unroll(aExtract, .Leaf), .Unroll(bExtract, .Leaf)):
|
2015-11-09 23:21:59 +03:00
|
|
|
|
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))) }
|
2015-11-06 01:38:40 +03:00
|
|
|
|
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) })))
|
2015-11-06 00:23:49 +03:00
|
|
|
|
default:
|
2015-11-06 01:18:56 +03:00
|
|
|
|
return .Pure(patch)
|
2015-11-06 00:23:49 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-03 19:56:01 +03:00
|
|
|
|
let parsed = benchmark("parsing arguments & loading sources") { parse(argumentsParser, input: Process.arguments) }
|
2015-11-02 20:48:21 +03:00
|
|
|
|
let arguments: Argument = try parsed.either(ifLeft: { throw "\($0)" }, ifRight: { $0 })
|
2015-11-02 21:06:23 +03:00
|
|
|
|
let (aSource, bSource) = arguments.sources
|
2015-10-30 01:14:02 +03:00
|
|
|
|
let jsonURL = NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).URLByAppendingPathComponent("diff.json")
|
2015-10-30 01:23:32 +03:00
|
|
|
|
guard let uiPath = NSBundle.mainBundle().infoDictionary?["PathToUISource"] as? String else { throw "need ui path" }
|
2015-10-29 23:19:23 +03:00
|
|
|
|
guard aSource.type == bSource.type else { throw "can’t compare files of different types" }
|
2015-10-31 01:15:16 +03:00
|
|
|
|
let parser = parserForType(aSource.type)
|
2015-10-29 23:19:23 +03:00
|
|
|
|
|
2015-11-03 18:20:15 +03:00
|
|
|
|
let a = try benchmark("parsing source a") { try parser(aSource.contents) }
|
|
|
|
|
let b = try benchmark("parsing source b") { try parser(bSource.contents) }
|
2015-11-12 23:07:00 +03:00
|
|
|
|
let initialDiff = benchmark("diffing") { Interpreter<Term>(equal: Term.equals(annotation: const(true), leaf: ==), comparable: Interpreter<Term>.comparable { $0.extract.categories }, cost: Free.sum(Patch.sum)).run(a, b) }
|
|
|
|
|
let diff = benchmark("diffing within leaves") { initialDiff.flatMap(refineLeafReplacement(aSource.contents, bSource.contents)) }
|
2015-11-02 21:20:42 +03:00
|
|
|
|
switch arguments.output {
|
|
|
|
|
case .Split:
|
|
|
|
|
let JSON: Doubt.JSON = [
|
|
|
|
|
"before": .String(aSource.contents),
|
|
|
|
|
"after": .String(bSource.contents),
|
2015-11-03 21:20:38 +03:00
|
|
|
|
"diff": diff.JSON(pure: { $0.JSON { $0.JSON(annotation: { $0.JSON }, leaf: Doubt.JSON.String) } }, leaf: Doubt.JSON.String, annotation: {
|
2015-11-02 21:20:42 +03:00
|
|
|
|
[
|
2015-11-03 21:08:32 +03:00
|
|
|
|
"before": $0.JSON,
|
|
|
|
|
"after": $1.JSON,
|
2015-11-02 21:20:42 +03:00
|
|
|
|
]
|
|
|
|
|
}),
|
|
|
|
|
]
|
|
|
|
|
let data = JSON.serialize()
|
|
|
|
|
try data.writeToURL(jsonURL, options: .DataWritingAtomic)
|
|
|
|
|
|
|
|
|
|
let components = NSURLComponents()
|
|
|
|
|
components.scheme = "file"
|
|
|
|
|
components.path = uiPath
|
|
|
|
|
components.query = jsonURL.absoluteString
|
|
|
|
|
if let URL = components.URL {
|
|
|
|
|
NSWorkspace.sharedWorkspace().openURL(URL)
|
|
|
|
|
}
|
|
|
|
|
case .Unified:
|
2015-11-03 18:20:02 +03:00
|
|
|
|
print(benchmark("formatting unified diff") { unified(diff, before: aSource.contents, after: bSource.contents) })
|
2015-10-27 18:04:18 +03:00
|
|
|
|
}
|