feature: support on line model
This commit is contained in:
parent
e7ce8411f3
commit
3246d71358
|
@ -21,6 +21,7 @@
|
||||||
/* End PBXAggregateTarget section */
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
/* Begin PBXBuildFile 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 */; };
|
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 */; };
|
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 */; };
|
OBJ_21 /* Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Diff.swift */; };
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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>"; };
|
OBJ_12 /* DiffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffTests.swift; sourceTree = "<group>"; };
|
||||||
|
@ -124,6 +126,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
OBJ_9 /* Diff.swift */,
|
OBJ_9 /* Diff.swift */,
|
||||||
|
1E4BF61A21708396004C5E1F /* DiffSequence.swift */,
|
||||||
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */,
|
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */,
|
||||||
);
|
);
|
||||||
name = Sdifft;
|
name = Sdifft;
|
||||||
|
@ -235,6 +238,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 0;
|
buildActionMask = 0;
|
||||||
files = (
|
files = (
|
||||||
|
1E4BF61B21708396004C5E1F /* DiffSequence.swift in Sources */,
|
||||||
1EB1AD2720BD5E22004D0450 /* NSAttributedString+Diff.swift in Sources */,
|
1EB1AD2720BD5E22004D0450 /* NSAttributedString+Diff.swift in Sources */,
|
||||||
OBJ_21 /* 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
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
|
|
||||||
extension String {
|
import Foundation
|
||||||
/// Return character in string
|
|
||||||
///
|
|
||||||
/// - Parameter idx: index
|
|
||||||
subscript (idx: Int) -> Character {
|
|
||||||
return self[index(startIndex, offsetBy: idx)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias Matrix = [[Int]]
|
typealias Matrix = [[Int]]
|
||||||
|
|
||||||
// swiftlint:disable identifier_name
|
// swiftlint:disable identifier_name
|
||||||
/// Draw LCS matrix with two strings
|
/// Draw LCS matrix with two `DiffSequence`
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - from: string
|
/// - from: DiffSequence
|
||||||
/// - to: string that be compared
|
/// - to: DiffSequence that be compared
|
||||||
/// - Returns: matrix
|
/// - Returns: matrix
|
||||||
func drawMatrix(from: String, to: String) -> Matrix {
|
func drawMatrix<T: DiffSequence>(from: T, to: T) -> Matrix {
|
||||||
let row = from.count + 1
|
let row = from.count + 1
|
||||||
let column = to.count + 1
|
let column = to.count + 1
|
||||||
var result: [[Int]] = Array(repeating: Array(repeating: 0, count: column), count: row)
|
var result: [[Int]] = Array(repeating: Array(repeating: 0, count: column), count: row)
|
||||||
for i in 1..<row {
|
for i in 1..<row {
|
||||||
for j in 1..<column {
|
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
|
result[i][j] = result[i - 1][j - 1] + 1
|
||||||
} else {
|
} else {
|
||||||
result[i][j] = max(result[i][j - 1], result[i - 1][j])
|
result[i][j] = max(result[i][j - 1], result[i - 1][j])
|
||||||
|
@ -66,17 +59,17 @@ typealias DiffIndex = (from: Int, to: Int)
|
||||||
/// LCS
|
/// LCS
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - from: string
|
/// - from: DiffSequence
|
||||||
/// - to: string that be compared
|
/// - to: DiffSequence that be compared
|
||||||
/// - position: current position
|
/// - position: current position
|
||||||
/// - matrix: matrix
|
/// - matrix: matrix
|
||||||
/// - same: same character's indexes
|
/// - same: same element's indexes
|
||||||
/// - Returns: same character's indexes
|
/// - Returns: same element's indexes
|
||||||
func lcs(from: String, to: String, position: Position, matrix: Matrix, same: [DiffIndex]) -> [DiffIndex] {
|
func lcs<T: DiffSequence>(from: T, to: T, position: Position, matrix: Matrix, same: [DiffIndex]) -> [DiffIndex] {
|
||||||
if position.row == 0 || position.column == 0 {
|
if position.row == 0 || position.column == 0 {
|
||||||
return same
|
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)])
|
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] {
|
} 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)
|
return lcs(from: from, to: to, position: (position.row - 1, position.column), matrix: matrix, same: same)
|
||||||
|
@ -85,44 +78,31 @@ func lcs(from: String, to: String, position: Position, matrix: Matrix, same: [Di
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Modification {
|
public struct Modification<Element: DiffSequence> {
|
||||||
public let add: String?
|
public let add, delete, same: Element?
|
||||||
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])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Array where Element == DiffIndex {
|
extension Array where Element == DiffIndex {
|
||||||
func modifications(from: String, to: String) -> [Modification] {
|
func modifications<T: DiffSequence>(from: T, to: T) -> [Modification<T>] {
|
||||||
var modifications: [Modification] = []
|
var modifications: [Modification<T>] = []
|
||||||
var lastFrom = 0
|
var lastFrom = 0
|
||||||
var lastTo = 0
|
var lastTo = 0
|
||||||
modifications += map {
|
modifications += map {
|
||||||
let modification =
|
let modification =
|
||||||
Modification(
|
Modification<T>(
|
||||||
add: lastTo <= $0.to - 1 ? to[lastTo...$0.to - 1] : nil,
|
add: lastTo <= $0.to - 1 ? to.element(withRange: lastTo...$0.to - 1) : nil,
|
||||||
delete: lastFrom <= $0.from - 1 ? from[lastFrom...$0.from - 1] : nil,
|
delete: lastFrom <= $0.from - 1 ? from.element(withRange: lastFrom...$0.from - 1) : nil,
|
||||||
same: to[$0.to...$0.to]
|
same: to.element(withRange: $0.to...$0.to)
|
||||||
)
|
)
|
||||||
lastFrom = $0.from + 1
|
lastFrom = $0.from + 1
|
||||||
lastTo = $0.to + 1
|
lastTo = $0.to + 1
|
||||||
return modification
|
return modification
|
||||||
}
|
}
|
||||||
if lastFrom <= from.count - 1 || lastTo <= to.count - 1 {
|
if lastFrom <= from.count - 1 || lastTo <= to.count - 1 {
|
||||||
modifications.append(
|
modifications.append(
|
||||||
Modification(
|
Modification<T>(
|
||||||
add: lastTo <= to.count - 1 ? to[lastTo...to.count - 1] : nil,
|
add: lastTo <= to.count - 1 ? to.element(withRange: lastTo...to.count - 1) : nil,
|
||||||
delete: lastFrom <= from.count - 1 ? from[lastFrom...from.count - 1] : nil,
|
delete: lastFrom <= from.count - 1 ? from.element(withRange: lastFrom...from.count - 1) : nil,
|
||||||
same: nil
|
same: nil
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -131,21 +111,20 @@ extension Array where Element == DiffIndex {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Diff {
|
public struct Diff<T: DiffSequence> {
|
||||||
public let modifications: [Modification]
|
public let modifications: [Modification<T>]
|
||||||
let matrix: Matrix
|
let matrix: Matrix
|
||||||
let from: String
|
let from, to: T
|
||||||
let to: String
|
public init(from: T, to: T) {
|
||||||
public init(from: String, to: String) {
|
|
||||||
// because LCS is 'bottom-up'
|
|
||||||
// so them need be reversed to get the normal sequence
|
|
||||||
self.from = from
|
self.from = from
|
||||||
self.to = to
|
self.to = to
|
||||||
let reversedFrom = String(from.reversed())
|
// because LCS is 'bottom-up'
|
||||||
let reversedTo = String(to.reversed())
|
// so them need be reversed to get the normal sequence
|
||||||
|
let reversedFrom = from.reversedElement()
|
||||||
|
let reversedTo = to.reversedElement()
|
||||||
matrix = drawMatrix(from: reversedFrom, to: reversedTo)
|
matrix = drawMatrix(from: reversedFrom, to: reversedTo)
|
||||||
var same = lcs(from: reversedFrom, to: reversedTo, position: (from.count, to.count), matrix: matrix, same: [])
|
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)
|
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
|
import Foundation
|
||||||
|
|
||||||
public struct DiffAttributes {
|
public struct DiffAttributes {
|
||||||
public let add: [NSAttributedString.Key: Any]
|
public let add, delete, same: [NSAttributedString.Key: Any]
|
||||||
public let delete: [NSAttributedString.Key: Any]
|
|
||||||
public let same: [NSAttributedString.Key: Any]
|
|
||||||
// swiftlint:disable line_length
|
// swiftlint:disable line_length
|
||||||
public init(add: [NSAttributedString.Key: Any], delete: [NSAttributedString.Key: Any], same: [NSAttributedString.Key: Any]) {
|
public init(add: [NSAttributedString.Key: Any], delete: [NSAttributedString.Key: Any], same: [NSAttributedString.Key: Any]) {
|
||||||
self.add = add
|
self.add = add
|
||||||
|
@ -46,7 +44,7 @@ extension NSAttributedString {
|
||||||
/// - diff: Diff
|
/// - diff: Diff
|
||||||
/// - attributes: DiffAttributes
|
/// - attributes: DiffAttributes
|
||||||
/// - Returns: NSAttributedString
|
/// - 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()
|
let attributedString = NSMutableAttributedString()
|
||||||
diff.modifications.forEach {
|
diff.modifications.forEach {
|
||||||
if let add = $0.add {
|
if let add = $0.add {
|
||||||
|
@ -61,4 +59,19 @@ extension NSAttributedString {
|
||||||
}
|
}
|
||||||
return attributedString
|
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
|
import XCTest
|
||||||
@testable import Sdifft
|
@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] {
|
var sames: [String] {
|
||||||
return compactMap { $0.same }
|
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 {
|
class DiffTests: XCTestCase {
|
||||||
func testMatrix() {
|
func testMatrix() {
|
||||||
assert(
|
assert(
|
||||||
|
@ -94,6 +115,14 @@ class DiffTests: XCTestCase {
|
||||||
diff3.modifications.adds == [] &&
|
diff3.modifications.adds == [] &&
|
||||||
diff3.modifications.deletes == ["\r\n", "\r\n"]
|
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
|
// swiftlint:disable line_length
|
||||||
|
|
|
@ -51,6 +51,20 @@ class NSAttributedStringDiffTests: XCTestCase {
|
||||||
assert(
|
assert(
|
||||||
attributedString2.debugDescription == "a{red}b{black}ex{green}cdhi{red}j{black}k{red}"
|
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 = [
|
static var allTests = [
|
||||||
|
|
Loading…
Reference in New Issue