Using NSTextSelection to represent insertion point

49 Views Asked by At

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
0

There are 0 best solutions below