From 6278bdecc05d71bd5f6616c001ca8fa0492284f4 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Thu, 5 Sep 2024 07:38:26 -0400 Subject: [PATCH] Implementing some of position(from:toBoundary:inDirection:) --- Sources/Ligature/Platform.swift | 25 ++++++++++++- .../Ligature/TextInputStringTokenizer.swift | 36 ++++++++++--------- Tests/LigatureTests/PlatformTests.swift | 27 ++++++++++++++ Tests/LigatureTests/TextTokenizerTests.swift | 36 +++++++++++++++++-- 4 files changed, 104 insertions(+), 20 deletions(-) diff --git a/Sources/Ligature/Platform.swift b/Sources/Ligature/Platform.swift index 6fcb112..2bfa257 100644 --- a/Sources/Ligature/Platform.swift +++ b/Sources/Ligature/Platform.swift @@ -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 @@ -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 @@ -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? { diff --git a/Sources/Ligature/TextInputStringTokenizer.swift b/Sources/Ligature/TextInputStringTokenizer.swift index 80a11f9..bc1119f 100644 --- a/Sources/Ligature/TextInputStringTokenizer.swift +++ b/Sources/Ligature/TextInputStringTokenizer.swift @@ -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 @@ -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 } @@ -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 diff --git a/Tests/LigatureTests/PlatformTests.swift b/Tests/LigatureTests/PlatformTests.swift index 3973774..2f14da5 100644 --- a/Tests/LigatureTests/PlatformTests.swift +++ b/Tests/LigatureTests/PlatformTests.swift @@ -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 diff --git a/Tests/LigatureTests/TextTokenizerTests.swift b/Tests/LigatureTests/TextTokenizerTests.swift index 2460feb..8483d52 100644 --- a/Tests/LigatureTests/TextTokenizerTests.swift +++ b/Tests/LigatureTests/TextTokenizerTests.swift @@ -1,5 +1,5 @@ import XCTest -#if canImport(AppKit) && !targetEnvironment(macCatalyst) +#if os(macOS) import AppKit typealias TextView = NSTextView @@ -22,7 +22,7 @@ import Ligature final class TextTokenizerTests: XCTestCase { @MainActor - func testTextInputTokenizerFallback() throws { + func testIsPositionByWord() throws { let input = TextView() input.text = "abc def" @@ -30,7 +30,7 @@ final class TextTokenizerTests: XCTestCase { 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))) @@ -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))) + 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) + + } }