feature: support on line model
This commit is contained in:
parent
e7ce8411f3
commit
3246d71358
|
@ -21,6 +21,7 @@
|
|||
/* End PBXAggregateTarget section */
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1E4BF61B21708396004C5E1F /* DiffSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4BF61A21708396004C5E1F /* DiffSequence.swift */; };
|
||||
1EB1AD2720BD5E22004D0450 /* NSAttributedString+Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */; };
|
||||
1EB1AD2920BD640B004D0450 /* NSAttributedString+DiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB1AD2820BD640B004D0450 /* NSAttributedString+DiffTests.swift */; };
|
||||
OBJ_21 /* Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Diff.swift */; };
|
||||
|
@ -47,6 +48,7 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1E4BF61A21708396004C5E1F /* DiffSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffSequence.swift; sourceTree = "<group>"; };
|
||||
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Diff.swift"; sourceTree = "<group>"; };
|
||||
1EB1AD2820BD640B004D0450 /* NSAttributedString+DiffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+DiffTests.swift"; sourceTree = "<group>"; };
|
||||
OBJ_12 /* DiffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffTests.swift; sourceTree = "<group>"; };
|
||||
|
@ -124,6 +126,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
OBJ_9 /* Diff.swift */,
|
||||
1E4BF61A21708396004C5E1F /* DiffSequence.swift */,
|
||||
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */,
|
||||
);
|
||||
name = Sdifft;
|
||||
|
@ -235,6 +238,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 0;
|
||||
files = (
|
||||
1E4BF61B21708396004C5E1F /* DiffSequence.swift in Sources */,
|
||||
1EB1AD2720BD5E22004D0450 /* NSAttributedString+Diff.swift in Sources */,
|
||||
OBJ_21 /* Diff.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -25,31 +25,24 @@
|
|||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
extension String {
|
||||
/// Return character in string
|
||||
///
|
||||
/// - Parameter idx: index
|
||||
subscript (idx: Int) -> Character {
|
||||
return self[index(startIndex, offsetBy: idx)]
|
||||
}
|
||||
}
|
||||
import Foundation
|
||||
|
||||
typealias Matrix = [[Int]]
|
||||
|
||||
// swiftlint:disable identifier_name
|
||||
/// Draw LCS matrix with two strings
|
||||
/// Draw LCS matrix with two `DiffSequence`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: string
|
||||
/// - to: string that be compared
|
||||
/// - from: DiffSequence
|
||||
/// - to: DiffSequence that be compared
|
||||
/// - Returns: matrix
|
||||
func drawMatrix(from: String, to: String) -> Matrix {
|
||||
func drawMatrix<T: DiffSequence>(from: T, to: T) -> Matrix {
|
||||
let row = from.count + 1
|
||||
let column = to.count + 1
|
||||
var result: [[Int]] = Array(repeating: Array(repeating: 0, count: column), count: row)
|
||||
for i in 1..<row {
|
||||
for j in 1..<column {
|
||||
if from[i - 1] == to[j - 1] {
|
||||
if from.index(of: i - 1) == to.index(of: j - 1) {
|
||||
result[i][j] = result[i - 1][j - 1] + 1
|
||||
} else {
|
||||
result[i][j] = max(result[i][j - 1], result[i - 1][j])
|
||||
|
@ -66,17 +59,17 @@ typealias DiffIndex = (from: Int, to: Int)
|
|||
/// LCS
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: string
|
||||
/// - to: string that be compared
|
||||
/// - from: DiffSequence
|
||||
/// - to: DiffSequence that be compared
|
||||
/// - position: current position
|
||||
/// - matrix: matrix
|
||||
/// - same: same character's indexes
|
||||
/// - Returns: same character's indexes
|
||||
func lcs(from: String, to: String, position: Position, matrix: Matrix, same: [DiffIndex]) -> [DiffIndex] {
|
||||
/// - same: same element's indexes
|
||||
/// - Returns: same element's indexes
|
||||
func lcs<T: DiffSequence>(from: T, to: T, position: Position, matrix: Matrix, same: [DiffIndex]) -> [DiffIndex] {
|
||||
if position.row == 0 || position.column == 0 {
|
||||
return same
|
||||
}
|
||||
if from[position.row - 1] == to[position.column - 1] {
|
||||
if from.index(of: position.row - 1) == to.index(of: position.column - 1) {
|
||||
return lcs(from: from, to: to, position: (position.row - 1, position.column - 1), matrix: matrix, same: same + [(position.row - 1, position.column - 1)])
|
||||
} else if matrix[position.row - 1][position.column] >= matrix[position.row][position.column - 1] {
|
||||
return lcs(from: from, to: to, position: (position.row - 1, position.column), matrix: matrix, same: same)
|
||||
|
@ -85,34 +78,21 @@ func lcs(from: String, to: String, position: Position, matrix: Matrix, same: [Di
|
|||
}
|
||||
}
|
||||
|
||||
public struct Modification {
|
||||
public let add: String?
|
||||
public let delete: String?
|
||||
public let same: String?
|
||||
}
|
||||
|
||||
extension String {
|
||||
/// Return string with range
|
||||
///
|
||||
/// - Parameter range: range
|
||||
subscript(_ range: CountableClosedRange<Int>) -> String {
|
||||
let start = index(startIndex, offsetBy: range.lowerBound)
|
||||
let end = index(startIndex, offsetBy: range.upperBound)
|
||||
return String(self[start...end])
|
||||
}
|
||||
public struct Modification<Element: DiffSequence> {
|
||||
public let add, delete, same: Element?
|
||||
}
|
||||
|
||||
extension Array where Element == DiffIndex {
|
||||
func modifications(from: String, to: String) -> [Modification] {
|
||||
var modifications: [Modification] = []
|
||||
func modifications<T: DiffSequence>(from: T, to: T) -> [Modification<T>] {
|
||||
var modifications: [Modification<T>] = []
|
||||
var lastFrom = 0
|
||||
var lastTo = 0
|
||||
modifications += map {
|
||||
let modification =
|
||||
Modification(
|
||||
add: lastTo <= $0.to - 1 ? to[lastTo...$0.to - 1] : nil,
|
||||
delete: lastFrom <= $0.from - 1 ? from[lastFrom...$0.from - 1] : nil,
|
||||
same: to[$0.to...$0.to]
|
||||
Modification<T>(
|
||||
add: lastTo <= $0.to - 1 ? to.element(withRange: lastTo...$0.to - 1) : nil,
|
||||
delete: lastFrom <= $0.from - 1 ? from.element(withRange: lastFrom...$0.from - 1) : nil,
|
||||
same: to.element(withRange: $0.to...$0.to)
|
||||
)
|
||||
lastFrom = $0.from + 1
|
||||
lastTo = $0.to + 1
|
||||
|
@ -120,9 +100,9 @@ extension Array where Element == DiffIndex {
|
|||
}
|
||||
if lastFrom <= from.count - 1 || lastTo <= to.count - 1 {
|
||||
modifications.append(
|
||||
Modification(
|
||||
add: lastTo <= to.count - 1 ? to[lastTo...to.count - 1] : nil,
|
||||
delete: lastFrom <= from.count - 1 ? from[lastFrom...from.count - 1] : nil,
|
||||
Modification<T>(
|
||||
add: lastTo <= to.count - 1 ? to.element(withRange: lastTo...to.count - 1) : nil,
|
||||
delete: lastFrom <= from.count - 1 ? from.element(withRange: lastFrom...from.count - 1) : nil,
|
||||
same: nil
|
||||
)
|
||||
)
|
||||
|
@ -131,21 +111,20 @@ extension Array where Element == DiffIndex {
|
|||
}
|
||||
}
|
||||
|
||||
public struct Diff {
|
||||
public let modifications: [Modification]
|
||||
public struct Diff<T: DiffSequence> {
|
||||
public let modifications: [Modification<T>]
|
||||
let matrix: Matrix
|
||||
let from: String
|
||||
let to: String
|
||||
public init(from: String, to: String) {
|
||||
// because LCS is 'bottom-up'
|
||||
// so them need be reversed to get the normal sequence
|
||||
let from, to: T
|
||||
public init(from: T, to: T) {
|
||||
self.from = from
|
||||
self.to = to
|
||||
let reversedFrom = String(from.reversed())
|
||||
let reversedTo = String(to.reversed())
|
||||
// because LCS is 'bottom-up'
|
||||
// so them need be reversed to get the normal sequence
|
||||
let reversedFrom = from.reversedElement()
|
||||
let reversedTo = to.reversedElement()
|
||||
matrix = drawMatrix(from: reversedFrom, to: reversedTo)
|
||||
var same = lcs(from: reversedFrom, to: reversedTo, position: (from.count, to.count), matrix: matrix, same: [])
|
||||
same = same.map({ (from.count - 1 - $0, to.count - 1 - $1) })
|
||||
same = same.map { (from.count - 1 - $0, to.count - 1 - $1) }
|
||||
modifications = same.modifications(from: from, to: to)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// DiffSequence.swift
|
||||
// Sdifft
|
||||
//
|
||||
// Created by WzxJiang on 18/5/23.
|
||||
// Copyright © 2018年 WzxJiang. All rights reserved.
|
||||
//
|
||||
// https://github.com/Wzxhaha/Sdifft
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol DiffSequence: Equatable {
|
||||
func index(of idx: Int) -> Self
|
||||
func element(withRange range: CountableClosedRange<Int>) -> Self
|
||||
func reversedElement() -> Self
|
||||
var count: Int { get }
|
||||
}
|
||||
|
||||
extension String: DiffSequence {
|
||||
public func index(of idx: Int) -> String {
|
||||
return String(self[index(startIndex, offsetBy: idx)])
|
||||
}
|
||||
public func element(withRange range: CountableClosedRange<Int>) -> String {
|
||||
let start = index(startIndex, offsetBy: range.lowerBound)
|
||||
let end = index(startIndex, offsetBy: range.upperBound)
|
||||
return String(self[start...end])
|
||||
}
|
||||
public func reversedElement() -> String {
|
||||
return String(reversed())
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: DiffSequence where Element == String {
|
||||
public func index(of idx: Int) -> [Element] {
|
||||
return [self[idx]]
|
||||
}
|
||||
public func element(withRange range: CountableClosedRange<Int>) -> [Element] {
|
||||
return Array(self[range])
|
||||
}
|
||||
public func reversedElement() -> [Element] {
|
||||
return reversed()
|
||||
}
|
||||
}
|
|
@ -28,9 +28,7 @@
|
|||
import Foundation
|
||||
|
||||
public struct DiffAttributes {
|
||||
public let add: [NSAttributedString.Key: Any]
|
||||
public let delete: [NSAttributedString.Key: Any]
|
||||
public let same: [NSAttributedString.Key: Any]
|
||||
public let add, delete, same: [NSAttributedString.Key: Any]
|
||||
// swiftlint:disable line_length
|
||||
public init(add: [NSAttributedString.Key: Any], delete: [NSAttributedString.Key: Any], same: [NSAttributedString.Key: Any]) {
|
||||
self.add = add
|
||||
|
@ -46,7 +44,7 @@ extension NSAttributedString {
|
|||
/// - diff: Diff
|
||||
/// - attributes: DiffAttributes
|
||||
/// - Returns: NSAttributedString
|
||||
public static func attributedString(with diff: Diff, attributes: DiffAttributes) -> NSAttributedString {
|
||||
public static func attributedString(with diff: Diff<String>, attributes: DiffAttributes) -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString()
|
||||
diff.modifications.forEach {
|
||||
if let add = $0.add {
|
||||
|
@ -61,4 +59,19 @@ extension NSAttributedString {
|
|||
}
|
||||
return attributedString
|
||||
}
|
||||
public static func attributedString(with diff: Diff<[String]>, attributes: DiffAttributes) -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString()
|
||||
diff.modifications.forEach {
|
||||
if let add = $0.add {
|
||||
attributedString.append(NSAttributedString(string: add.joined(), attributes: attributes.add))
|
||||
}
|
||||
if let delete = $0.delete {
|
||||
attributedString.append(NSAttributedString(string: delete.joined(), attributes: attributes.delete))
|
||||
}
|
||||
if let same = $0.same {
|
||||
attributedString.append(NSAttributedString(string: same.joined(), attributes: attributes.same))
|
||||
}
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import XCTest
|
||||
@testable import Sdifft
|
||||
|
||||
extension Array where Element == Modification {
|
||||
extension String {
|
||||
subscript(idx: Int) -> String {
|
||||
return index(of: idx)
|
||||
}
|
||||
subscript(range: CountableClosedRange<Int>) -> String {
|
||||
return element(withRange: range)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Modification<String> {
|
||||
var sames: [String] {
|
||||
return compactMap { $0.same }
|
||||
}
|
||||
|
@ -13,6 +22,18 @@ extension Array where Element == Modification {
|
|||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Modification<[String]> {
|
||||
var sames: [[String]] {
|
||||
return compactMap { $0.same }
|
||||
}
|
||||
var adds: [[String]] {
|
||||
return compactMap { $0.add }
|
||||
}
|
||||
var deletes: [[String]] {
|
||||
return compactMap { $0.delete }
|
||||
}
|
||||
}
|
||||
|
||||
class DiffTests: XCTestCase {
|
||||
func testMatrix() {
|
||||
assert(
|
||||
|
@ -94,6 +115,14 @@ class DiffTests: XCTestCase {
|
|||
diff3.modifications.adds == [] &&
|
||||
diff3.modifications.deletes == ["\r\n", "\r\n"]
|
||||
)
|
||||
let to4 = ["a", "bc", "d", "c"]
|
||||
let from4 = ["d"]
|
||||
let diff4 = Diff(from: from4, to: to4)
|
||||
assert(
|
||||
diff4.modifications.sames == [["d"]] &&
|
||||
diff4.modifications.adds == [["a", "bc"], ["c"]] &&
|
||||
diff4.modifications.deletes == []
|
||||
)
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
|
|
|
@ -51,6 +51,20 @@ class NSAttributedStringDiffTests: XCTestCase {
|
|||
assert(
|
||||
attributedString2.debugDescription == "a{red}b{black}ex{green}cdhi{red}j{black}k{red}"
|
||||
)
|
||||
let to3 = ["bexj", "abc", "c"]
|
||||
let from3 = ["abcdhijk"]
|
||||
let diff3 = Diff(from: from3, to: to3)
|
||||
let attributedString3 = NSAttributedString.attributedString(with: diff3, attributes: diffAttributes)
|
||||
assert(
|
||||
attributedString3.debugDescription == "bexjabcc{green}abcdhijk{red}"
|
||||
)
|
||||
let to4 = ["bexj", "abc", "c", "abc"]
|
||||
let from4 = ["abcdhijk", "abc"]
|
||||
let diff4 = Diff(from: from4, to: to4)
|
||||
let attributedString4 = NSAttributedString.attributedString(with: diff4, attributes: diffAttributes)
|
||||
assert(
|
||||
attributedString4.debugDescription == "bexj{green}abcdhijk{red}abc{black}cabc{green}"
|
||||
)
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
|
|
Loading…
Reference in New Issue