Count line number with LineEndingScanner

This commit is contained in:
1024jp 2022-04-28 13:01:26 +09:00
parent 751677a8a6
commit 8db7752d50
7 changed files with 48 additions and 26 deletions

View File

@ -8,6 +8,7 @@ Change Log
### Improvements
- [beta] Update the help contents.
- [beta][trivial] Optimize the performance of the Warnings pane.

View File

@ -31,7 +31,6 @@ struct IncompatibleCharacter: Equatable {
let character: Character
let convertedCharacter: String?
let location: Int
let lineNumber: Int
var range: NSRange { NSRange(location: self.location, length: self.character.utf16.count) }
}
@ -73,8 +72,7 @@ extension String {
return IncompatibleCharacter(character: character,
convertedCharacter: converted,
location: location,
lineNumber: self.lineNumber(at: location))
location: location)
}
}
@ -93,8 +91,7 @@ extension String {
return IncompatibleCharacter(character: characters.0,
convertedCharacter: String(characters.1),
location: location,
lineNumber: self.lineNumber(at: location))
location: location)
}
}

View File

@ -250,7 +250,7 @@ extension IncompatibleCharactersViewController: NSTableViewDataSource {
switch identifier {
case .line:
return incompatibleCharacter.lineNumber
return self.document?.lineEndingScanner.lineNumber(at: incompatibleCharacter.location)
case .character:
return String(incompatibleCharacter.character)
case .converted:

View File

@ -148,7 +148,7 @@ extension InconsistentLineEndingsViewController: NSTableViewDataSource {
switch identifier {
case .line:
// calculate the line number first at this point to postpone the high cost processing as much as possible
return self.document?.string.lineNumber(at: lineEnding.location)
return self.document?.lineEndingScanner.lineNumber(at: lineEnding.location)
case .lineEnding:
return lineEnding.item.name
default:

View File

@ -29,13 +29,13 @@ import Combine
final class LineEndingScanner {
@Published private(set) var inconsistentLineEndings: [ItemRange<LineEnding>] = []
@Published private(set) var inconsistentLineEndings: [ItemRange<LineEnding>]
// MARK: Private Properties
private let textStorage: NSTextStorage
private var lineEndings: [ItemRange<LineEnding>] = []
private var lineEndings: [ItemRange<LineEnding>]
private var documentLineEnding: LineEnding {
@ -57,7 +57,8 @@ final class LineEndingScanner {
self.textStorage = textStorage
self.documentLineEnding = lineEnding
self.inconsistentLineEndings = self.scan()
self.lineEndings = textStorage.string.lineEndingRanges()
self.inconsistentLineEndings = self.lineEndings.filter { $0.item != lineEnding }
self.storageObserver = NotificationCenter.default.publisher(for: NSTextStorage.didProcessEditingNotification, object: textStorage)
.compactMap { $0.object as? NSTextStorage }
@ -87,6 +88,18 @@ final class LineEndingScanner {
}
/// Return the 1-based line number at the given character index.
///
/// - Parameter index: The character index.
/// - Returns: The 1-based line number.
func lineNumber(at index: Int) -> Int {
assert(index <= self.textStorage.string.length)
return self.lineEndings.countPrefix { $0.range.upperBound <= index } + 1
}
/// Whether the character at the given index is a line ending inconsistent with the `documentLineEnding`.
///
/// - Parameter characterIndex: The index of character to test.
@ -124,17 +137,6 @@ final class LineEndingScanner {
self.inconsistentLineEndings.replace(items: inconsistentLineEndings, in: scanRange, changeInLength: delta)
}
/// Scan line endings inconsistent with the document line endings.
///
/// - Parameter range: The range to scan.
/// - Returns: The ranes of inconsistent line endings with its type.
private func scan(in range: NSRange? = nil) -> [ItemRange<LineEnding>] {
self.textStorage.string.lineEndingRanges(in: range)
.filter { $0.item != self.documentLineEnding }
}
}

View File

@ -41,14 +41,12 @@ final class IncompatibleCharacterTests: XCTestCase {
XCTAssertEqual(backslash.character, "\\")
XCTAssertEqual(backslash.convertedCharacter, "")
XCTAssertEqual(backslash.location, 3)
XCTAssertEqual(backslash.lineNumber, 1)
let tilde = incompatibles[1]
XCTAssertEqual(tilde.character, "~")
XCTAssertEqual(tilde.convertedCharacter, "?")
XCTAssertEqual(tilde.location, 11)
XCTAssertEqual(tilde.lineNumber, 3)
}
@ -64,7 +62,6 @@ final class IncompatibleCharacterTests: XCTestCase {
XCTAssertEqual(tilde.character, "~")
XCTAssertEqual(tilde.convertedCharacter, "?")
XCTAssertEqual(tilde.location, 1)
XCTAssertEqual(tilde.lineNumber, 1)
}
@ -78,12 +75,10 @@ final class IncompatibleCharacterTests: XCTestCase {
XCTAssertEqual(incompatibles[0].character, "👨‍👨‍👦")
XCTAssertEqual(incompatibles[0].convertedCharacter, "????????")
XCTAssertEqual(incompatibles[0].location, 7)
XCTAssertEqual(incompatibles[0].lineNumber, 1)
XCTAssertEqual(incompatibles[1].character, "🐕")
XCTAssertEqual(incompatibles[1].convertedCharacter, "??")
XCTAssertEqual(incompatibles[1].location, 21)
XCTAssertEqual(incompatibles[1].lineNumber, 1)
}
}

View File

@ -125,6 +125,33 @@ final class LineEndingScannerTests: XCTestCase {
XCTAssertEqual(scanner.majorLineEnding, .crlf) // most used new line must be detected
}
func testLineNumberCalculation() {
let storage = NSTextStorage(string: "dog \n\n cat \n cow \n")
let scanner = LineEndingScanner(textStorage: storage, lineEnding: .lf)
XCTAssertEqual(scanner.lineNumber(at: 0), 1)
XCTAssertEqual(scanner.lineNumber(at: 1), 1)
XCTAssertEqual(scanner.lineNumber(at: 4), 1)
XCTAssertEqual(scanner.lineNumber(at: 5), 2)
XCTAssertEqual(scanner.lineNumber(at: 6), 3)
XCTAssertEqual(scanner.lineNumber(at: 11), 3)
XCTAssertEqual(scanner.lineNumber(at: 12), 4)
XCTAssertEqual(scanner.lineNumber(at: 17), 4)
XCTAssertEqual(scanner.lineNumber(at: 18), 5)
for _ in 0..<20 {
storage.string = String(" 🐶 \n 🐱 \n 🐮 \n".shuffled())
for index in (0..<storage.length).shuffled() {
XCTAssertEqual(scanner.lineNumber(at: index),
storage.string.lineNumber(at: index),
"At \(index) with string \"\(storage.string)\"")
}
}
}
}