Follow Link at Insertion Point in NSTextView
If you ever wondered how to programmatically trigger a click on a link in a NSTextView
, here’s one way to do so.
This assumes that clickable links are not stored as temporary attributes in the NSLayoutManager
, but permanently as part of the “model” in your NSTextStorage
. You can then ask the storage for the attribute at the cursor/insertion point location:
extension NSTextView {
func followLinkAtInsertionPoint(_ sender: Any?) {
guard let textStorage = textStorage else { return }
let location = selectedRange().location
let attributes = textStorage.attributes(at: location, effectiveRange: nil)
guard let url = attributes[.link] as? URL else { return }
self.clicked(onLink: url, at: location)
}
}
To further refine this and make it work with selected ranges properly, you’d need to find all links in the selected range. Call NSTextStorage.attributes(at:longestEffectiveRange:in:)
repeatedly for this.
For example, if your selection spans let wholeRange = NSRange(location: 100, length: 50)
, you call attributes(at: 100, longestEffectiveRange: &effectiveRange, in: wholeRange)
. The storage produces an effectiveRange
clipped to wholeRange
. It might be smaller, e.g. equal to NSRange(location: 100, length: 10)
, so the range from 11 to 50 is still uncovered. If effectiveRange
is fully contained within wholeRange
like this, you can call the method again with the location
parameter advanced by effectiveRange.length + 1
to 111
. Then you get the next piece. Rinse and repeat until wholeRange
is exhausted.
This is how a rich text document with nested styles, e.g. underlining a portion of an italic text, is linearized. Users think of “nested” styles, but the computer would return a range of “italics only”, followed by “italics + underlines”, followed by “italics only” again. That’s how you would iterate over the attributes, too.