Skip to content

Commit

Permalink
Updating example projects, platform compat and readme
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Nov 20, 2023
1 parent 9db8bd9 commit c535839
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 31 deletions.
2 changes: 1 addition & 1 deletion Projects/NeonExample-iOS/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class ViewController: UIViewController {
textView.font = regularFont
textView.textColor = .darkGray

let provider: TextViewSystemInterface.AttributeProvider = { token in
let provider: TokenAttributeProvider = { 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]
Expand Down
11 changes: 7 additions & 4 deletions Projects/NeonExample/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ final class ViewController: NSViewController {
// 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
textView.typingAttributes = [
.foregroundColor: NSColor.darkGray,
.font: regularFont,
]

let provider: TextViewSystemInterface.AttributeProvider = { token in
let provider: TokenAttributeProvider = { 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]
Expand All @@ -41,10 +43,11 @@ final class ViewController: NSViewController {
.appendingPathComponent("Contents/Resources/queries/highlights.scm")
let query = try! language.query(contentsOf: url!)

let interface = TextStorageSystemInterface(textView: textView, attributeProvider: provider)
self.highlighter = try! TextViewHighlighter(textView: textView,
language: language,
highlightQuery: query,
attributeProvider: provider)
interface: interface)

super.init(nibName: nil, bundle: nil)
}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Neon is text-system independent. It makes very few assumptions about how text is

### TextViewSystemInterface

An implementation of the `TextSystemInterface` protocol for `NSTextView`/`UITextView`. This takes care of the interface to the view and `NSLayoutManager`, but defers `Token`-style translation (themes) to an external `AttributeProvider`. This is compatible with both TextKit 1 and 2.
An implementation of the `TextSystemInterface` protocol for `NSTextView`/`UITextView`. This takes care of the interface to the view and `NSLayoutManager`, but defers `Token`-style translation (themes) to an external `AttributeProvider`. This is compatible with both TextKit 1 and 2. There are also more specalized versions if you want a little better control over the process. Check out `LayoutManagerSystemInterface`, `TextLayoutManagerSystemInterface`, and `TextStorageSystemInterface`.

### TextViewHighlighter

Expand Down
43 changes: 40 additions & 3 deletions Sources/Neon/TextViewHighlighter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public final class TextViewHighlighter: NSObject {
client: TreeSitterClient,
highlightQuery: Query,
executionMode: TreeSitterClient.ExecutionMode = .asynchronous(prefetch: true),
attributeProvider: @escaping TokenAttributeProvider
interface: TextSystemInterface
) throws {
self.treeSitterClient = client
self.textView = textView
Expand All @@ -54,7 +54,6 @@ public final class TextViewHighlighter: NSObject {

let tokenProvider = client.tokenProvider(with: highlightQuery, executionMode: executionMode, textProvider: textProvider)

let interface = TextViewSystemInterface(textView: textView, attributeProvider: attributeProvider)
self.highlighter = Highlighter(textInterface: interface, tokenProvider: tokenProvider)

super.init()
Expand All @@ -78,6 +77,43 @@ public final class TextViewHighlighter: NSObject {
#endif

treeSitterClient.invalidationHandler = { [weak self] in self?.handleInvalidation($0) }

}

public convenience init(
textView: TextView,
client: TreeSitterClient,
highlightQuery: Query,
executionMode: TreeSitterClient.ExecutionMode = .asynchronous(prefetch: true),
attributeProvider: @escaping TokenAttributeProvider
) throws {
let interface = TextViewSystemInterface(textView: textView, attributeProvider: attributeProvider)

try self.init(
textView: textView,
client: client,
highlightQuery: highlightQuery,
executionMode: executionMode,
interface: interface
)
}

public convenience init(
textView: TextView,
language: Language,
highlightQuery: Query,
executionMode: TreeSitterClient.ExecutionMode = .asynchronous(prefetch: true),
interface: TextSystemInterface
) throws {
let client = try TreeSitterClient(language: language, transformer: { _ in return .zero })

try self.init(
textView: textView,
client: client,
highlightQuery: highlightQuery,
executionMode: executionMode,
interface: interface
)
}

public convenience init(
Expand All @@ -94,7 +130,8 @@ public final class TextViewHighlighter: NSObject {
client: client,
highlightQuery: highlightQuery,
executionMode: executionMode,
attributeProvider: attributeProvider)
attributeProvider: attributeProvider
)
}

@objc private func visibleContentChanged(_ notification: NSNotification) {
Expand Down
40 changes: 25 additions & 15 deletions Sources/Neon/TextViewSystemInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ extension TextViewSystemInterface: TextSystemInterface {
private var effectiveInterface: TextSystemInterface? {
let provider = { textView.visibleTextRange }

if #available(macOS 12.0, iOS 15.0, tvOS 15.0, *) {
if #available(macOS 12.0, iOS 16.0, tvOS 16.0, *) {
if let textLayoutManager {
return TextLayoutManagerSystemInterface(
textLayoutManager: textLayoutManager,
Expand All @@ -68,19 +68,20 @@ extension TextViewSystemInterface: TextSystemInterface {
}
}

#if os(macOS)
if let layoutManager {
return LayoutManagerSystemInterface(
layoutManager: layoutManager,
attributeProvider: attributeProvider,
visibleRangeProvider: provider
)
}
#endif

if let textStorage {
if textStorage != nil {
return TextStorageSystemInterface(
textStorage: textStorage,
attributeProvider: attributeProvider,
visibleRangeProvider: provider
textView: textView,
attributeProvider: attributeProvider
)
}

Expand All @@ -106,6 +107,7 @@ extension TextViewSystemInterface: TextSystemInterface {

#endif

#if os(macOS)
/// A concrete `TextSystemInterface` that uses `NSLayoutManager` temporary attributes.
public struct LayoutManagerSystemInterface {
public let layoutManager: NSLayoutManager
Expand Down Expand Up @@ -151,9 +153,10 @@ extension LayoutManagerSystemInterface: TextSystemInterface {
visibleRangeProvider()
}
}
#endif

/// A concrete `TextSystemInterface` that uses `NSTextLayoutManager` rendering attributes.
@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
@available(macOS 12.0, iOS 16.0, tvOS 16.0, *)
public struct `TextLayoutManagerSystemInterface` {
public let textLayoutManager: NSTextLayoutManager
public let attributeProvider: TokenAttributeProvider
Expand All @@ -173,7 +176,7 @@ public struct `TextLayoutManagerSystemInterface` {
}
}

@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
@available(macOS 12.0, iOS 16.0, tvOS 16.0, *)
extension TextLayoutManagerSystemInterface: TextSystemInterface {
private var textElementProvider: NSTextElementProvider? {
textLayoutManager.textContentManager
Expand Down Expand Up @@ -215,33 +218,40 @@ extension TextLayoutManagerSystemInterface: TextSystemInterface {

/// A concrete `TextSystemInterface` that modifies `NSTextStorage` text attributes.
public struct TextStorageSystemInterface {
public let textStorage: NSTextStorage
private let textStorage: NSTextStorage?
public let attributeProvider: TokenAttributeProvider
public let defaultAttributesProvider: () -> [NSAttributedString.Key : Any]
public let visibleRangeProvider: () -> NSRange

public init(textStorage: NSTextStorage, attributeProvider: @escaping TokenAttributeProvider, visibleRangeProvider: @escaping () -> NSRange) {
public init(
textStorage: NSTextStorage,
attributeProvider: @escaping TokenAttributeProvider,
visibleRangeProvider: @escaping () -> NSRange,
defaultAttributesProvider: @escaping () -> [NSAttributedString.Key : Any]
) {
self.textStorage = textStorage
self.attributeProvider = attributeProvider
self.visibleRangeProvider = visibleRangeProvider
self.defaultAttributesProvider = defaultAttributesProvider
}

public init?(textView: TextView, attributeProvider: @escaping TokenAttributeProvider) {
guard let storage = textView.textStorage else { return nil }
self.textStorage = storage
public init(textView: TextView, attributeProvider: @escaping TokenAttributeProvider) {
self.textStorage = textView.textStorage
self.visibleRangeProvider = { textView.visibleTextRange }
self.attributeProvider = attributeProvider
self.defaultAttributesProvider = { textView.typingAttributes }
}
}

extension TextStorageSystemInterface: TextSystemInterface {
private func setAttributes(_ attrs: [NSAttributedString.Key : Any], in range: NSRange) {
let clampedRange = range.clamped(to: length)

textStorage.setAttributes(attrs, range: clampedRange)
textStorage?.setAttributes(attrs, range: clampedRange)
}

public func clearStyle(in range: NSRange) {
setAttributes([:], in: range)
setAttributes(defaultAttributesProvider(), in: range)
}

public func applyStyle(to token: Token) {
Expand All @@ -251,7 +261,7 @@ extension TextStorageSystemInterface: TextSystemInterface {
}

public var length: Int {
textStorage.length
textStorage?.length ?? 0
}

public var visibleRange: NSRange {
Expand Down
17 changes: 10 additions & 7 deletions Tests/NeonTests/TextViewSystemInterfaceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,18 @@ final class TextViewSystemInterfaceTests: XCTestCase {
var effectiveRange: NSRange = .zero

#if os(macOS)
let allAttrs = textView.textStorage?.attributes(at: 0, effectiveRange: &effectiveRange) ?? [:]
let attrs = textView.layoutManager?.temporaryAttributes(atCharacterIndex: 0, effectiveRange: &effectiveRange) ?? [:]
#else
let allAttrs = textView.textStorage.attributes(at: 0, effectiveRange: &effectiveRange)
#endif

// we have to remove some attributes, like font, that are normal for the textStorage.
let attrs = allAttrs.filter({ $0.key == .foregroundColor })
#endif

XCTAssertEqual(attrs.count, 1)
XCTAssertEqual(attrs[.foregroundColor] as? PlatformColor, PlatformColor.red)
XCTAssertEqual(effectiveRange, NSRange(0..<6))

}
#endif

Expand All @@ -90,13 +91,15 @@ final class TextViewSystemInterfaceTests: XCTestCase {

system.applyStyle(to: Token(name: "test", range: NSRange(0..<6)))

let textStorage = try XCTUnwrap(system.textStorage)
let documentRange = NSRange(location: 0, length: textStorage.length)
let documentRange = textLayoutManager.documentRange

var attrRangePairs = [([NSAttributedString.Key: Any], NSRange)]()
textStorage.enumerateAttributes(in: documentRange) { attrs, range, _ in
var attrRangePairs = [([NSAttributedString.Key: Any], NSTextRange)]()

textLayoutManager.enumerateRenderingAttributes(from: documentRange.location, reverse: false, using: { _, attrs, range in
attrRangePairs.append((attrs, range))
}

return true
})

XCTAssertEqual(attrRangePairs.count, 1)

Expand Down

0 comments on commit c535839

Please sign in to comment.