mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-09-11 11:25:57 +03:00
Count line number with LineEndingScanner
This commit is contained in:
parent
751677a8a6
commit
8db7752d50
@ -8,6 +8,7 @@ Change Log
|
||||
### Improvements
|
||||
|
||||
- [beta] Update the help contents.
|
||||
- [beta][trivial] Optimize the performance of the Warnings pane.
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user