From 33b5b69ffec448fcbffc4651308559c31afcb37e Mon Sep 17 00:00:00 2001 From: Christian Tietze Date: Sat, 18 Nov 2023 19:22:54 +0100 Subject: [PATCH] Default attributes (#34) * apply default text view attributes * always use stored properties, even with TK2 * opt into TextKit 2 on iOS, too Circumvents #20 * adjust tests for permanent attributes * fix pasting code from Xcode rendering oddly * store defaultTextViewAttributes to fix applying outdated styles --- Projects/NeonExample-iOS/ViewController.swift | 11 ++++-- Projects/NeonExample/ViewController.swift | 10 ++++-- Sources/Neon/TextViewSystemInterface.swift | 35 +++++++++++-------- .../TextViewSystemInterfaceTests.swift | 13 +++---- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/Projects/NeonExample-iOS/ViewController.swift b/Projects/NeonExample-iOS/ViewController.swift index 01ad91a..6d4dbcc 100644 --- a/Projects/NeonExample-iOS/ViewController.swift +++ b/Projects/NeonExample-iOS/ViewController.swift @@ -19,17 +19,25 @@ final class ViewController: UIViewController { let boldFont = UIFont.monospacedSystemFont(ofSize: 16, weight: .bold) let italicFont = regularFont.fontDescriptor.withSymbolicTraits(.traitItalic).map { UIFont(descriptor: $0, size: 16) } ?? regularFont + // Set the default styles. This is applied by stock `NSTextStorage`s during + // so-called "attribute fixing" when you type, and we emulate that as + // part of the highlighting process in `TextViewSystemInterface`. + textView.font = regularFont + textView.textColor = .darkGray + let provider: TextViewSystemInterface.AttributeProvider = { token in return switch token.name { case let keyword where keyword.hasPrefix("keyword"): [.foregroundColor: UIColor.red, .font: boldFont] case "comment": [.foregroundColor: UIColor.green, .font: italicFont] - default: [.foregroundColor: UIColor.darkText, .font: regularFont] + // Note: Default is not actually applied to unstyled/untokenized text. + default: [.foregroundColor: UIColor.blue, .font: regularFont] } } return try! TextViewHighlighter(textView: textView, language: language, highlightQuery: query, + executionMode: .synchronous, attributeProvider: provider) }() @@ -37,7 +45,6 @@ final class ViewController: UIViewController { super.viewDidLoad() _ = highlighter.textView - _ = textView.layoutManager textView.text = """ // Example Code! diff --git a/Projects/NeonExample/ViewController.swift b/Projects/NeonExample/ViewController.swift index 8638cf9..42f9278 100644 --- a/Projects/NeonExample/ViewController.swift +++ b/Projects/NeonExample/ViewController.swift @@ -10,6 +10,7 @@ final class ViewController: NSViewController { init() { self.textView = NSTextView() + textView.isRichText = false // Discards any attributes when pasting. scrollView.documentView = textView @@ -17,15 +18,18 @@ final class ViewController: NSViewController { let boldFont = NSFont.monospacedSystemFont(ofSize: 16, weight: .bold) let italicFont = NSFont(descriptor: regularFont.fontDescriptor.withSymbolicTraits(.italic), size: 16) ?? regularFont - // Alternatively, set `textView.typingAttributes = [.font: regularFont, ...]` - // if you want to customize other default (fallback) attributes. + // Set the default styles. This is applied by stock `NSTextStorage`s during + // so-called "attribute fixing" when you type, and we emulate that as + // part of the highlighting process in `TextViewSystemInterface`. textView.font = regularFont + textView.textColor = .darkGray let provider: TextViewSystemInterface.AttributeProvider = { token in return switch token.name { case let keyword where keyword.hasPrefix("keyword"): [.foregroundColor: NSColor.red, .font: boldFont] case "comment": [.foregroundColor: NSColor.green, .font: italicFont] - default: [.foregroundColor: NSColor.textColor, .font: regularFont] + // Note: Default is not actually applied to unstyled/untokenized text. + default: [.foregroundColor: NSColor.blue, .font: regularFont] } } diff --git a/Sources/Neon/TextViewSystemInterface.swift b/Sources/Neon/TextViewSystemInterface.swift index 4bfc309..f11496e 100644 --- a/Sources/Neon/TextViewSystemInterface.swift +++ b/Sources/Neon/TextViewSystemInterface.swift @@ -14,9 +14,20 @@ public struct TextViewSystemInterface { public let textView: TextView public let attributeProvider: AttributeProvider + public var defaultTextViewAttributes: [NSAttributedString.Key: Any] = [:] - public init(textView: TextView, attributeProvider: @escaping AttributeProvider) { + public init( + textView: TextView, + defaultTextViewAttributes: [NSAttributedString.Key: Any] = [:], + attributeProvider: @escaping AttributeProvider + ) { self.textView = textView + // Assume that the default styles used before enabling any highlighting + // should be retained, unless client code overrides this. + self.defaultTextViewAttributes = [ + .font: textView.font as Any, + .foregroundColor: textView.textColor as Any, + ].merging(defaultTextViewAttributes) { _, override in override } self.attributeProvider = attributeProvider } @@ -43,34 +54,28 @@ public struct TextViewSystemInterface { } extension TextViewSystemInterface: TextSystemInterface { - private func setAttributes(_ attrs: [NSAttributedString.Key : Any], in range: NSRange) { + private func clamped(range: NSRange) -> NSRange { let endLocation = min(range.max, length) assert(endLocation == range.max, "range is out of bounds, is the text state being updated correctly?") - let clampedRange = NSRange(range.location..