Skip to content

Commit

Permalink
Attempt to simplify range/position abstractions with the Bounded prot…
Browse files Browse the repository at this point in the history
…ocol
  • Loading branch information
mattmassicotte committed Sep 3, 2024
1 parent 5dc217e commit fb90945
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 52 deletions.
14 changes: 1 addition & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,7 @@ typealias TextDirection = UITextDirection
typealias UserInterfaceLayoutDirection = UIUserInterfaceLayoutDirection
```

There are a variety of range/position models within AppKit, UIKit, and even between TextKit 1 and 2. Some abstraction is, unfortunately, required to model this. The protocol `TextPositionSpace` provides a way of mapping positions into and out of ranges. Here's what the implemenation looks like for UITextRanges

```swift
extension UITextView : TextPositionSpace {
public func decomposeRange(_ range: TextRange) -> (TextPosition, TextPosition) {
(range.start, range.end)
}

public func composeRange(_ components: (TextPosition, TextPosition)) -> TextRange? {
textRange(from: components.0, to: components.1)
}
}
```
There are a variety of range/position models within AppKit, UIKit, and even between TextKit 1 and 2. Some abstraction is, unfortunately, required to model this. This should be all automatically handled by the `TextTokenizer` protocol **if** you are using `NSRange` or `NSTextRange`. The cross-platform `TextRange` type cannot do this without additional work on your part, typically by involving the text view.

## Contributing and Collaboration

Expand Down
32 changes: 32 additions & 0 deletions Sources/Ligature/Bounded.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
public protocol Bounded<Bound> {
associatedtype Bound

var lowerBound: Bound { get }
var upperBound: Bound { get }
}

#if os(macOS)
import AppKit
#elseif canImport(UIKit)
import UIKit
#endif

@available(iOS 15.0, macOS 12.0, tvOS 15.0, macCatalyst 15.0, *)
extension NSTextRange : Bounded {
public var lowerBound: NSTextLocation { location }
public var upperBound: NSTextLocation { endLocation }
}

extension TextRange : Bounded {
public nonisolated var lowerBound: TextPosition {
MainActor.assumeIsolated { start }
}

public nonisolated var upperBound: TextPosition {
MainActor.assumeIsolated { end }
}
}

extension NSRange : Bounded {}

extension Range : Bounded {}
2 changes: 1 addition & 1 deletion Sources/Ligature/Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import AppKit

public typealias UserInterfaceLayoutDirection = NSUserInterfaceLayoutDirection

@MainActor
open class TextPosition: NSObject {

}

final class UTF16TextPosition: TextPosition {
Expand Down
6 changes: 4 additions & 2 deletions Sources/Ligature/SourceTokenizer.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Foundation

public struct SourceTokenizer<Position, FallbackTokenzier: TextTokenizer> where FallbackTokenzier.Position == Position, FallbackTokenzier.TextRange == TextRange {
public struct SourceTokenizer<FallbackTokenzier: TextTokenizer> {
public typealias Position = FallbackTokenzier.Position

private let fallbackTokenzier: FallbackTokenzier

init(fallbackTokenzier: FallbackTokenzier) {
Expand All @@ -13,7 +15,7 @@ extension SourceTokenizer : TextTokenizer {
return fallbackTokenzier.position(from: position, toBoundary: granularity, inDirection: direction)
}

public func rangeEnclosingPosition(_ position: Position, with granularity: TextGranularity, inDirection direction: TextDirection) -> Ligature.TextRange? {
public func rangeEnclosingPosition(_ position: Position, with granularity: TextGranularity, inDirection direction: TextDirection) -> FallbackTokenzier.TextRange? {
return fallbackTokenzier.rangeEnclosingPosition(position, with: granularity, inDirection: direction)
}

Expand Down
29 changes: 0 additions & 29 deletions Sources/Ligature/TextPositionSpace.swift

This file was deleted.

55 changes: 48 additions & 7 deletions Sources/Ligature/TextTokenizer.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import Foundation
#if os(macOS)
import AppKit
#elseif canImport(UIKit)
import UIKit
#endif

@MainActor
public protocol TextTokenizer<Position, TextRange> {
associatedtype Position
associatedtype TextRange
public protocol TextTokenizer<TextRange> {
associatedtype TextRange: Bounded

typealias Position = TextRange.Bound
typealias RangeBuilder = (Position, Position) -> TextRange?

func position(from position: Position, toBoundary granularity: TextGranularity, inDirection direction: TextDirection) -> Position?
func rangeEnclosingPosition(_ position: Position, with granularity: TextGranularity, inDirection direction: TextDirection) -> TextRange?
Expand All @@ -13,9 +19,44 @@ public protocol TextTokenizer<Position, TextRange> {
}

#if canImport(UIKit)
import UIKit
extension UITextInputStringTokenizer : TextTokenizer {}
#endif

extension TextTokenizer {
public func range(
from range: TextRange,
to granularity: TextGranularity,
in direction: TextDirection,
rangeBuilder: RangeBuilder
) -> TextRange? {
let components = (range.lowerBound, range.upperBound)

guard
let start = position(from: components.0, toBoundary: granularity, inDirection: direction),
let end = position(from: components.1, toBoundary: granularity, inDirection: direction)
else {
return nil
}

extension UITextInputStringTokenizer : TextTokenizer {
return rangeBuilder(start, end)
}
}

#endif
extension TextTokenizer where TextRange == NSRange {
public func range(from range: TextRange, to granularity: TextGranularity, in direction: TextDirection) -> TextRange? {
self.range(from: range, to: granularity, in: direction, rangeBuilder: { NSRange($0..<$1) })
}
}

extension TextTokenizer where TextRange == Range<Int> {
public func range(from range: TextRange, to granularity: TextGranularity, in direction: TextDirection) -> TextRange? {
self.range(from: range, to: granularity, in: direction, rangeBuilder: { $0..<$1 })
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, macCatalyst 15.0, *)
extension TextTokenizer where TextRange == NSTextRange {
public func range(from range: TextRange, to granularity: TextGranularity, in direction: TextDirection) -> TextRange? {
self.range(from: range, to: granularity, in: direction, rangeBuilder: { NSTextRange(location: $0, end: $1) })
}
}

0 comments on commit fb90945

Please sign in to comment.