Skip to content

Commit

Permalink
Implementing some of position(from:toBoundary:inDirection:)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Sep 5, 2024
1 parent 2da2638 commit 6278bde
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 20 deletions.
25 changes: 24 additions & 1 deletion Sources/Ligature/Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ final class UTF16TextPosition: TextPosition {
init(value: Int) {
self.value = value
}

func offsetPosition(by amount: Int, maximum: Int) -> UTF16TextPosition? {
let new = value + amount
if new < 0 || new > maximum {
return nil
}

return UTF16TextPosition(value: new)
}
}

extension UTF16TextPosition {
public override var debugDescription: String {
String(value)
}
}

@MainActor
Expand All @@ -35,6 +50,14 @@ open class TextRange: NSObject {
}
}

extension TextRange {
open override var debugDescription: String {
MainActor.assumeIsolated {
"{\(start.debugDescription), \(end.debugDescription)}"
}
}
}

/// Matches the implementation of `UITextGranularity`.
public enum TextGranularity : Int, Sendable, Hashable, Codable {
case character = 0
Expand Down Expand Up @@ -113,7 +136,7 @@ extension NSTextView {
}

public func textRange(from fromPosition: TextPosition, to toPosition: TextPosition) -> TextRange? {
nil
TextRange(start: fromPosition, end: toPosition)
}

public func position(from position: TextPosition, offset: Int) -> TextPosition? {
Expand Down
36 changes: 20 additions & 16 deletions Sources/Ligature/TextInputStringTokenizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,6 @@ import Foundation
#if os(macOS)
import AppKit

extension NSTextView {
func isForwardDirection(_ direction: TextDirection) -> Bool {
switch direction {
case .storage(.forward):
return true
case .layout(.right):
return userInterfaceLayoutDirection == .leftToRight
case .layout(.left):
return userInterfaceLayoutDirection == .rightToLeft
default:
return false
}
}
}

@MainActor
public final class TextInputStringTokenizer {
private let textInput: NSTextView
Expand All @@ -30,6 +15,25 @@ extension TextInputStringTokenizer : TextTokenizer {
public typealias Position = TextPosition

public func position(from position: Position, toBoundary granularity: TextGranularity, inDirection direction: TextDirection) -> Position? {
guard let position = position as? UTF16TextPosition else { return nil }

let maximum = self.textInput.textStorage?.length ?? 0
let forward = direction.textStorageDirection(with: textInput.userInterfaceLayoutDirection) == .forward

switch (granularity, direction) {
case (.character, .storage(.forward)):
return position.offsetPosition(by: 1, maximum: maximum)
case (.character, .storage(.backward)):
return position.offsetPosition(by: -1, maximum: maximum)

case (.character, .layout(.left)), (.character, .layout(.right)):
let offset = forward ? 1 : -1

return position.offsetPosition(by: offset, maximum: maximum)
default:
break
}

return nil
}

Expand All @@ -41,7 +45,7 @@ extension TextInputStringTokenizer : TextTokenizer {
guard let position = position as? UTF16TextPosition else { return false }

let string = (textInput.attributedString().string as NSString)
let forward = textInput.isForwardDirection(direction)
let forward = direction.textStorageDirection(with: textInput.userInterfaceLayoutDirection) == .forward
let start = forward ? max(position.value - 1, 0) : 0
let end = forward ? string.length : min(position.value + 1, string.length)
let options: NSString.EnumerationOptions
Expand Down
27 changes: 27 additions & 0 deletions Tests/LigatureTests/PlatformTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,30 @@ final class PlatformTests: XCTestCase {
XCTAssertEqual(right.textStorageDirection(with: .rightToLeft), .backward)
}
}

#if os(macOS)
import AppKit

@MainActor
extension PlatformTests {
func testPositionOffset() throws {
let view = NSTextView()

view.text = "abcdef"

let position = try XCTUnwrap(view.position(from: view.beginningOfDocument, offset: 3))

XCTAssertEqual(view.offset(from: view.beginningOfDocument, to: position), 3)
}

func testMakeTextRange() throws {
let view = NSTextView()

view.text = "abcdef"

let position = try XCTUnwrap(view.position(from: view.beginningOfDocument, offset: 3))
_ = try XCTUnwrap(view.textRange(from: view.beginningOfDocument, to: position))
}
}

#endif
36 changes: 33 additions & 3 deletions Tests/LigatureTests/TextTokenizerTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import XCTest
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
#if os(macOS)
import AppKit

typealias TextView = NSTextView
Expand All @@ -22,15 +22,15 @@ import Ligature

final class TextTokenizerTests: XCTestCase {
@MainActor
func testTextInputTokenizerFallback() throws {
func testIsPositionByWord() throws {
let input = TextView()
input.text = "abc def"

let tokenzier = TextInputStringTokenizer(textInput: input)

let start = input.beginningOfDocument
let middle = try XCTUnwrap(input.position(from: start, offset: 1))
let end = try XCTUnwrap(input.position(from: start, offset: 3))
let end = try XCTUnwrap(input.position(from: start, offset: 7))

XCTAssertFalse(tokenzier.isPosition(start, atBoundary: .word, inDirection: .storage(.forward)))
XCTAssertTrue(tokenzier.isPosition(start, atBoundary: .word, inDirection: .storage(.backward)))
Expand All @@ -41,4 +41,34 @@ final class TextTokenizerTests: XCTestCase {
XCTAssertTrue(tokenzier.isPosition(end, atBoundary: .word, inDirection: .storage(.forward)))
XCTAssertFalse(tokenzier.isPosition(end, atBoundary: .word, inDirection: .storage(.backward)))
}

@MainActor
func testPositionByCharacter() throws {
let input = TextView()
input.text = "abc def"

let tokenzier = TextInputStringTokenizer(textInput: input)

let start = input.beginningOfDocument
let end = try XCTUnwrap(input.position(from: start, offset: 7))

XCTAssertNil(tokenzier.position(from: start, toBoundary: .character, inDirection: .storage(.backward)))
XCTAssertNil(tokenzier.position(from: start, toBoundary: .character, inDirection: .layout(.left)))

let pos1 = try XCTUnwrap(tokenzier.position(from: start, toBoundary: .character, inDirection: .storage(.forward)))
XCTAssertEqual(input.offset(from: start, to: pos1), 1)

let pos2 = try XCTUnwrap(tokenzier.position(from: start, toBoundary: .character, inDirection: .layout(.right)))

Check failure on line 61 in Tests/LigatureTests/TextTokenizerTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS,variant=Mac Catalyst)

testPositionByCharacter, XCTUnwrap failed: expected non-nil value of type "UITextPosition"

Check failure on line 61 in Tests/LigatureTests/TextTokenizerTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

testPositionByCharacter, XCTUnwrap failed: expected non-nil value of type "UITextPosition"
XCTAssertEqual(input.offset(from: start, to: pos2), 1)

XCTAssertNil(tokenzier.position(from: end, toBoundary: .character, inDirection: .storage(.forward)))
XCTAssertNil(tokenzier.position(from: end, toBoundary: .character, inDirection: .layout(.right)))

let pos3 = try XCTUnwrap(tokenzier.position(from: end, toBoundary: .character, inDirection: .storage(.backward)))
XCTAssertEqual(input.offset(from: start, to: pos3), 6)

let pos4 = try XCTUnwrap(tokenzier.position(from: end, toBoundary: .character, inDirection: .layout(.left)))
XCTAssertEqual(input.offset(from: start, to: pos4), 6)

}
}

0 comments on commit 6278bde

Please sign in to comment.