The documentation on NSTextLayoutManager.textSelections says each selection point represents a insertion point. I have been using that to mean the insertion point of text or carat on screen - which translates to a location just past the end of document range. But, doing this triggers a crash when setting textSelections on a non-empty document.
For example, if I have a document with content T, my insertion point is at Column 2 (not zero-indexed). Setting range 2..2 as textSelection causes a crash when TextKit tries to compute the offset between two textlocations (the to field is nil).
Should the textSelection range always map to a valid range in the document (and insertion/caret points be tracked differently)?
My minimal code is
// CustomTextLocation
import AppKit
class CustomTextLocation: NSObject {
var column: Int = 1 // Not zero indexed.
init(column: Int) {
self.column = column
}
override public var description: String {
"\(column)"
}
}
extension CustomTextLocation: NSTextLocation {
func compare(_ location: NSTextLocation) -> ComparisonResult {
guard let location = location as? CustomTextLocation else {
fatalError("Expected Document.Location")
}
if column < location.column {
return .orderedAscending
} else if column == location.column {
return .orderedSame
} else {
return .orderedDescending
}
}
override func isEqual(_ object: Any?) -> Bool {
guard let location = object as? CustomTextLocation else {
return false
}
return self.column == location.column
}
}
// DocumentModel
import AppKit
class DocumentModel: NSTextContentManager {
var layoutManager: NSTextLayoutManager!
var data = "T"
override var documentRange: NSTextRange {
let start = CustomTextLocation(column: 1)
let end = CustomTextLocation(column: data.utf8.count + 1)
return NSTextRange(location: start, end: end)!
}
func attach(textLayoutManager: NSTextLayoutManager) {
self.layoutManager = textLayoutManager
// textLayoutManager.delegate = self
super.addTextLayoutManager(layoutManager)
}
// MARK: NSTextContentManager overrides
override func textElements(for range: NSTextRange) -> [NSTextElement] {
let textElement = NSTextElement(textContentManager: self)
// Notes: Setting elementRange clears first crash
textElement.elementRange = documentRange
return [textElement]
}
// MARK: NSTextElementProvider Overrides
override func enumerateTextElements(from textLocation: NSTextLocation?, options: NSTextContentManager.EnumerationOptions = [], using block: (NSTextElement) -> Bool) -> NSTextLocation? {
guard let from = textLocation as? CustomTextLocation else {
return nil
}
let textElements = textElements(for: NSTextRange(location: from, end: documentRange.endLocation)!)
for textElement in textElements {
let _ = block(textElement)
}
return documentRange.endLocation
}
override func location(_ location: NSTextLocation, offsetBy offset: Int) -> NSTextLocation? {
guard let location = location as? CustomTextLocation else {
fatalError("Expected CustomTextLocation")
}
return CustomTextLocation(column: location.column + offset)
}
override func offset(from: NSTextLocation, to: NSTextLocation) -> Int {
guard let from = from as? CustomTextLocation,
let to = to as? CustomTextLocation else {
fatalError("Expected CustomTextLocation")
}
print("offset from: \(String(describing: from)), to: \(String(describing: to))")
return to.column - from.column
}
}
// ViewController
class ViewController: NSViewController {
var layoutManager: NSTextLayoutManager!
var textContainer: NSTextContainer!
var documentModel: DocumentModel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let textContainer = NSTextContainer(size: NSSize(width: 400, height: 400))
self.textContainer = textContainer
let layoutManager = NSTextLayoutManager()
layoutManager.textContainer = textContainer
self.layoutManager = layoutManager
self.documentModel = DocumentModel()
documentModel.attach(textLayoutManager: layoutManager)
layoutManager.enumerateTextLayoutFragments(from: CustomTextLocation(column: 1), options: [.reverse, .ensuresExtraLineFragment, .ensuresLayout]) { fragment in
print("\(fragment)")
return false
}
let selectionLocation = CustomTextLocation(column: 2)
// Following line crashes
layoutManager.textSelections = [NSTextSelection(selectionLocation, affinity: .downstream)]
}
}
Crash message
TextLocationCrash/DocumentModel.swift:60: Fatal error: Expected CustomTextLocation