From f2f9d9307572789bdef0259c0606bf3e8c8c5830 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:30:35 -0600 Subject: [PATCH] Fix CRLF Line Ending Typesetting (#20) --- .../TextLine/Typesetter.swift | 29 +++++++- .../TypesetterTests.swift | 74 +++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 Tests/CodeEditTextViewTests/TypesetterTests.swift diff --git a/Sources/CodeEditTextView/TextLine/Typesetter.swift b/Sources/CodeEditTextView/TextLine/Typesetter.swift index 63961ce6..9628bde2 100644 --- a/Sources/CodeEditTextView/TextLine/Typesetter.swift +++ b/Sources/CodeEditTextView/TextLine/Typesetter.swift @@ -182,8 +182,18 @@ final class Typesetter { startingOffset: Int, constrainingWidth: CGFloat ) -> Int { - let breakIndex = startingOffset + CTTypesetterSuggestClusterBreak(typesetter, startingOffset, constrainingWidth) - if breakIndex >= string.length || (breakIndex - 1 > 0 && ensureCharacterCanBreakLine(at: breakIndex - 1)) { + var breakIndex = startingOffset + CTTypesetterSuggestClusterBreak(typesetter, startingOffset, constrainingWidth) + + let isBreakAtEndOfString = breakIndex >= string.length + + let isNextCharacterCarriageReturn = checkIfLineBreakOnCRLF(breakIndex) + if isNextCharacterCarriageReturn { + breakIndex += 1 + } + + let canLastCharacterBreak = (breakIndex - 1 > 0 && ensureCharacterCanBreakLine(at: breakIndex - 1)) + + if isBreakAtEndOfString || canLastCharacterBreak { // Breaking either at the end of the string, or on a whitespace. return breakIndex } else if breakIndex - 1 > 0 { @@ -208,7 +218,20 @@ final class Typesetter { let set = CharacterSet( charactersIn: string.attributedSubstring(from: NSRange(location: index, length: 1)).string ) - return set.isSubset(of: .whitespaces) || set.isSubset(of: .punctuationCharacters) + return set.isSubset(of: .whitespacesAndNewlines) || set.isSubset(of: .punctuationCharacters) + } + + /// Check if the break index is on a CRLF (`\r\n`) character, indicating a valid break position. + /// - Parameter breakIndex: The index to check in the string. + /// - Returns: True, if the break index lies after the `\n` character in a `\r\n` sequence. + private func checkIfLineBreakOnCRLF(_ breakIndex: Int) -> Bool { + guard breakIndex - 1 > 0 && breakIndex + 1 <= string.length else { + return false + } + let substringRange = NSRange(location: breakIndex - 1, length: 2) + let substring = string.attributedSubstring(from: substringRange).string + + return substring == LineEnding.carriageReturnLineFeed.rawValue } deinit { diff --git a/Tests/CodeEditTextViewTests/TypesetterTests.swift b/Tests/CodeEditTextViewTests/TypesetterTests.swift new file mode 100644 index 00000000..07954a7c --- /dev/null +++ b/Tests/CodeEditTextViewTests/TypesetterTests.swift @@ -0,0 +1,74 @@ +import XCTest +@testable import CodeEditTextView + +// swiftlint:disable all + +class TypesetterTests: XCTestCase { + let limitedLineWidthDisplayData = TextLine.DisplayData(maxWidth: 150, lineHeightMultiplier: 1.0, estimatedLineHeight: 20.0) + let unlimitedLineWidthDisplayData = TextLine.DisplayData(maxWidth: .infinity, lineHeightMultiplier: 1.0, estimatedLineHeight: 20.0) + + func test_LineFeedBreak() { + let typesetter = Typesetter() + typesetter.typeset( + NSAttributedString(string: "testline\n"), + displayData: unlimitedLineWidthDisplayData, + breakStrategy: .word, + markedRanges: nil + ) + + XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.") + + typesetter.typeset( + NSAttributedString(string: "testline\n"), + displayData: unlimitedLineWidthDisplayData, + breakStrategy: .character, + markedRanges: nil + ) + + XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.") + } + + func test_carriageReturnBreak() { + let typesetter = Typesetter() + typesetter.typeset( + NSAttributedString(string: "testline\r"), + displayData: unlimitedLineWidthDisplayData, + breakStrategy: .word, + markedRanges: nil + ) + + XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.") + + typesetter.typeset( + NSAttributedString(string: "testline\r"), + displayData: unlimitedLineWidthDisplayData, + breakStrategy: .character, + markedRanges: nil + ) + + XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.") + } + + func test_carriageReturnLineFeedBreak() { + let typesetter = Typesetter() + typesetter.typeset( + NSAttributedString(string: "testline\r\n"), + displayData: unlimitedLineWidthDisplayData, + breakStrategy: .word, + markedRanges: nil + ) + + XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.") + + typesetter.typeset( + NSAttributedString(string: "testline\r\n"), + displayData: unlimitedLineWidthDisplayData, + breakStrategy: .character, + markedRanges: nil + ) + + XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.") + } +} + +// swiftlint:enable all