diff --git a/Common/AsyncLoader.swift b/Common/AsyncLoader.swift index 3a83572..90300da 100644 --- a/Common/AsyncLoader.swift +++ b/Common/AsyncLoader.swift @@ -1,10 +1,10 @@ // 非同期に辞書のロードを行なう -class AsyncLoader { - var initialized = false +final class AsyncLoader: Sendable { + nonisolated(unsafe) var initialized = false // 辞書をロードする - func load(_ closure: @escaping () -> ()) { - async { + func load(_ closure: @Sendable @escaping () -> ()) { + globalAsync { closure() self.initialized = true } diff --git a/Common/DictionaryCache.swift b/Common/DictionaryCache.swift index 6eb63cc..3a56a8d 100644 --- a/Common/DictionaryCache.swift +++ b/Common/DictionaryCache.swift @@ -1,10 +1,10 @@ -private var kDicitonary : SKKLocalDictionaryFile? -private var kCache : [URL:(Date, Any)] = [:] +private nonisolated(unsafe) var kDicitonary : SKKLocalDictionaryFile? +private nonisolated(unsafe) var kCache : [URL:(Date, Any)] = [:] private let writeQueue = DispatchQueue(label: "DictionaryCache.write") // 辞書のロードには時間がかかるので、一度ロードした結果をキャッシュする // グローバル変数にいれておけば、次回起動時にも残っている(ことがある) -class DictionaryCache { +final class DictionaryCache: Sendable { // L辞書等のインストール済みの辞書をロードする // FIXME: 現時点では二個以上の辞書ファイルは存在しないと仮定している func loadLocalDicitonary(_ url: URL, closure: (URL) -> SKKLocalDictionaryFile) -> SKKLocalDictionaryFile { diff --git a/Common/DictionarySettings.swift b/Common/DictionarySettings.swift index 014456c..a9e6157 100644 --- a/Common/DictionarySettings.swift +++ b/Common/DictionarySettings.swift @@ -5,7 +5,7 @@ class DictionarySettings { // テスト時は違うBundleからロードする // FIXME: もっといい感じに書きたい fileprivate struct ClassProperty { - static var bundle : Bundle? + nonisolated(unsafe) static var bundle : Bundle? } class var bundle: Bundle? { get { diff --git a/Common/SKKDictionary.swift b/Common/SKKDictionary.swift index 3309556..a446af5 100644 --- a/Common/SKKDictionary.swift +++ b/Common/SKKDictionary.swift @@ -3,36 +3,37 @@ // ・辞書のロードの非同期実行 // ・ロード結果のキャッシュ // などを行なう。 -class SKKDictionary : NSObject { +@MainActor +final class SKKDictionary : NSObject, Sendable { // すべての辞書(先頭から順に検索される) - fileprivate var dictionaries : [ SKKDictionaryFile ] = [] + nonisolated(unsafe) fileprivate var dictionaries : [ SKKDictionaryFile ] = [] // ダイナミック変換用辞書 - fileprivate var dynamicDictionaries : [ SKKUserDictionaryFile ] = [] + nonisolated(unsafe) fileprivate var dynamicDictionaries : [ SKKUserDictionaryFile ] = [] // ユーザ辞書 - fileprivate var userDictionary : SKKUserDictionaryFile? + nonisolated(unsafe) fileprivate var userDictionary : SKKUserDictionaryFile? // 学習辞書 - fileprivate var learnDictionary : SKKUserDictionaryFile? + nonisolated(unsafe) fileprivate var learnDictionary : SKKUserDictionaryFile? // 略語辞書 - fileprivate var partialDictionary : SKKUserDictionaryFile? + nonisolated(unsafe) fileprivate var partialDictionary : SKKUserDictionaryFile? // ロード完了を監視するために Key value observing を使う - @objc dynamic var isWaitingForLoad : Bool = false - class func isWaitingForLoadKVOKey() -> String { return "isWaitingForLoad" } + @objc nonisolated(unsafe) dynamic var isWaitingForLoad : Bool = false + nonisolated class func isWaitingForLoadKVOKey() -> String { return "isWaitingForLoad" } fileprivate let loader = AsyncLoader() fileprivate let cache = DictionaryCache() - class func resetLearnDictionary() { + nonisolated class func resetLearnDictionary() { for url in [DictionarySettings.defaultLearnDictionaryURL(), DictionarySettings.defaultPartialDictionaryURL()] { _ = try? FileManager.default.removeItem(at: url as URL) } } - class func additionalDictionaries() -> [URL] { + nonisolated class func additionalDictionaries() -> [URL] { do { let manager = FileManager.default let url = DictionarySettings.additionalDictionaryURL() @@ -97,7 +98,7 @@ class SKKDictionary : NSObject { // 単語を登録する func register(_ normal : String, okuri: String?, kanji: String) { userDictionary?.register(normal, okuri: okuri, kanji: kanji) - async { + globalAsync { self.cache.update(DictionarySettings.defaultUserDictionaryURL()) { self.userDictionary?.serialize() } @@ -107,7 +108,7 @@ class SKKDictionary : NSObject { // 確定結果を学習する func learn(_ normal : String, okuri: String?, kanji: String) { learnDictionary?.register(normal, okuri: okuri, kanji: kanji) - async { + globalAsync { self.cache.update(DictionarySettings.defaultLearnDictionaryURL()) { self.learnDictionary?.serialize() } @@ -117,7 +118,7 @@ class SKKDictionary : NSObject { // InputModeChangeによる確定を学習する func partial(_ kana: String, okuri: String?, kanji: String) { partialDictionary?.register(kana, okuri: okuri, kanji: kanji) - async { + globalAsync { self.cache.update(DictionarySettings.defaultPartialDictionaryURL()) { self.partialDictionary?.serialize() } diff --git a/Common/Utilities.swift b/Common/Utilities.swift index 2a61009..c367bba 100644 --- a/Common/Utilities.swift +++ b/Common/Utilities.swift @@ -20,7 +20,7 @@ extension UIButton { } // 非同期に処理を実行する -func async(_ closure: @escaping () -> ()) { +func globalAsync(_ closure: @Sendable @escaping () -> ()) { DispatchQueue.global(qos: .default).async(execute: closure) } diff --git a/FlickSKK.xcodeproj/project.pbxproj b/FlickSKK.xcodeproj/project.pbxproj index 172928a..22f3fa9 100644 --- a/FlickSKK.xcodeproj/project.pbxproj +++ b/FlickSKK.xcodeproj/project.pbxproj @@ -1294,7 +1294,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "Common/FlickSKK-Common-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -1353,7 +1353,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "Common/FlickSKK-Common-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -1373,6 +1373,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "$(APP_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1392,6 +1393,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "$(APP_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -1413,6 +1415,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.codefirst.FlickSKKTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FlickSKK.app/FlickSKK"; }; @@ -1434,6 +1437,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = org.codefirst.FlickSKKTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FlickSKK.app/FlickSKK"; }; @@ -1455,6 +1459,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(APP_IDENTIFIER).FlickSKKKeyboard"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1475,6 +1480,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(APP_IDENTIFIER).FlickSKKKeyboard"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -1492,6 +1498,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = org.codefirst.Memo; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1509,6 +1516,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = org.codefirst.Memo; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/FlickSKK/AppDelegate.swift b/FlickSKK/AppDelegate.swift index e97125c..2562d13 100644 --- a/FlickSKK/AppDelegate.swift +++ b/FlickSKK/AppDelegate.swift @@ -8,7 +8,7 @@ import UIKit -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? diff --git a/FlickSKK/DownloadDictionary.swift b/FlickSKK/DownloadDictionary.swift index e6f4bb9..7067f41 100644 --- a/FlickSKK/DownloadDictionary.swift +++ b/FlickSKK/DownloadDictionary.swift @@ -7,7 +7,8 @@ // // もしかしたらダウンロード済みの辞書を統合したほうが高速化ができるかもしれないが、 // とりあえず現バージョンでは対応しない。 -class DownloadDictionary { +@MainActor +final class DownloadDictionary: Sendable { fileprivate let remote : URL fileprivate let local : URL @@ -32,7 +33,7 @@ class DownloadDictionary { self.local = local.appendingPathComponent(url.lastPathComponent) } - func call() { + @MainActor func call() { let downloadFile = Tempfile.temp() let utf8File = Tempfile.temp() @@ -45,7 +46,7 @@ class DownloadDictionary { try self.encodeToUTF8(downloadFile as URL, dest: utf8File as URL) // メインスレッドはプログラスバーの更新を行なうので辞書の検証等は別スレッドで行なう。 - async { + globalAsync { let dictionary = LoadLocalDictionary(url: utf8File) // 妥当性のチェック @@ -55,9 +56,9 @@ class DownloadDictionary { // 結果のサマリを渡す let info = DictionaryInfo(dictionary: dictionary) - self.success?(info) + Task { @MainActor in self.success?(info) } } else { - self.error?(NSLocalizedString("InvalidDictionary", comment:""), nil) + Task { @MainActor in self.error?(NSLocalizedString("InvalidDictionary", comment:""), nil) } } } } catch let e { @@ -70,24 +71,26 @@ class DownloadDictionary { } // URLを特定ファイルに保存する。 - fileprivate func save(_ url : URL, path: URL, completion: @escaping (Result) -> Void) { - var observation: NSKeyValueObservation? + fileprivate func save(_ url : URL, path: URL, completion: @MainActor @Sendable @escaping (Result) -> Void) { + nonisolated(unsafe) var observation: NSKeyValueObservation? let task = URLSession.shared.downloadTask(with: url) { url, response, error in observation?.invalidate() if let error = error { - completion(.failure(error)) + Task { @MainActor in completion(.failure(error)) } } guard let url = url else { fatalError() } do { try FileManager.default.createDirectory(at: path.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) try FileManager.default.moveItem(at: url, to: path) - completion(.success(())) + Task { @MainActor in completion(.success(())) } } catch { - completion(.failure(error)) + Task { @MainActor in completion(.failure(error)) } } } observation = task.progress.observe(\.fractionCompleted) { progress, _ in - self.progress?(NSLocalizedString("Downloading", comment:""), Float(progress.fractionCompleted) / 2.0) + Task { @MainActor in + self.progress?(NSLocalizedString("Downloading", comment:""), Float(progress.fractionCompleted) / 2.0) + } } task.resume() } @@ -112,11 +115,13 @@ class DownloadDictionary { // 辞書の検証をする // 検証の進捗状況は逐次表示する - fileprivate func validate(_ dictionary : LoadLocalDictionary) -> Bool { + private nonisolated func validate(_ dictionary : LoadLocalDictionary) -> Bool { let validate = ValidateDictionary(dictionary: dictionary) validate.progress = { (current, total) in let progress = Float(current) / Float(total) - self.progress?(NSLocalizedString("Validating", comment:""), progress / 2 + 0.5) + Task { @MainActor in + self.progress?(NSLocalizedString("Validating", comment:""), progress / 2 + 0.5) + } } return validate.call() } diff --git a/FlickSKK/Tempfile.swift b/FlickSKK/Tempfile.swift index 2aa6653..04997f4 100644 --- a/FlickSKK/Tempfile.swift +++ b/FlickSKK/Tempfile.swift @@ -1,3 +1,4 @@ +@MainActor class Tempfile { fileprivate static var count = 0 diff --git a/FlickSKK/WebViewController.swift b/FlickSKK/WebViewController.swift index 66e121e..dbb9538 100644 --- a/FlickSKK/WebViewController.swift +++ b/FlickSKK/WebViewController.swift @@ -8,7 +8,7 @@ import UIKit import Ikemen -@preconcurrency import WebKit +import WebKit class WebViewController: UIViewController, WKNavigationDelegate { lazy var configure = WKWebViewConfiguration() ※ { (wc: inout WKWebViewConfiguration) in @@ -37,14 +37,14 @@ class WebViewController: UIViewController, WKNavigationDelegate { } // MARK: WebView Delegate - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { if navigationAction.navigationType == .linkActivated { // Open in Safari UIApplication.shared.open(navigationAction.request.url!, options: [:]) { _ in } - decisionHandler(.cancel) + decisionHandler(.cancel, preferences) return } - decisionHandler(.allow) + decisionHandler(.allow, preferences) } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { diff --git a/FlickSKKKeyboard/ComposeModeFactory.swift b/FlickSKKKeyboard/ComposeModeFactory.swift index ff348a5..ff8edc7 100644 --- a/FlickSKKKeyboard/ComposeModeFactory.swift +++ b/FlickSKKKeyboard/ComposeModeFactory.swift @@ -1,3 +1,4 @@ +@MainActor class ComposeModeFactory { fileprivate let dictionary : DictionaryEngine diff --git a/FlickSKKKeyboard/DictionaryEngine.swift b/FlickSKKKeyboard/DictionaryEngine.swift index 6370a0c..32a560a 100644 --- a/FlickSKKKeyboard/DictionaryEngine.swift +++ b/FlickSKKKeyboard/DictionaryEngine.swift @@ -1,4 +1,5 @@ // SKKの辞書をラップして、フリック入力に適したインターフェースを提供する +@MainActor class DictionaryEngine { fileprivate let dictionary : SKKDictionary init(dictionary : SKKDictionary){ diff --git a/FlickSKKKeyboard/KeyHandler.swift b/FlickSKKKeyboard/KeyHandler.swift index c79214b..555ca73 100644 --- a/FlickSKKKeyboard/KeyHandler.swift +++ b/FlickSKKKeyboard/KeyHandler.swift @@ -1,6 +1,7 @@ // キー入力を受け取り、次の状態を返す。 // その際、状態に応じて、テキストの追加・削除を行なう -class KeyHandler { +@MainActor +final class KeyHandler: Sendable { fileprivate weak var delegate : SKKDelegate? fileprivate let dictionary : DictionaryEngine fileprivate let text : TextEngine diff --git a/FlickSKKKeyboard/KeyboardViewController.swift b/FlickSKKKeyboard/KeyboardViewController.swift index de8a54b..999f801 100644 --- a/FlickSKKKeyboard/KeyboardViewController.swift +++ b/FlickSKKKeyboard/KeyboardViewController.swift @@ -48,6 +48,7 @@ class KeyboardViewController: UIInputViewController, SKKDelegate { kb.imageView.image = UIImage(named: "flickskk-arrow")!.withRenderingMode(.alwaysTemplate) kb.imageView.tintColor = ThemeColor.buttonText } + private var observation: NSKeyValueObservation? // MARK: - @@ -150,7 +151,9 @@ class KeyboardViewController: UIInputViewController, SKKDelegate { } } - dictionary.addObserver(self, forKeyPath: SKKDictionary.isWaitingForLoadKVOKey(), options: NSKeyValueObservingOptions(), context: nil) + observation = dictionary.observe(\.isWaitingForLoad) { [weak self] dict, _ in + Task { @MainActor in self?.observeDictionaryIsWaitingForLoad(dict) } + } updateControlButtons() } @@ -159,20 +162,16 @@ class KeyboardViewController: UIInputViewController, SKKDelegate { } deinit { - dictionary.removeObserver(self, forKeyPath: SKKDictionary.isWaitingForLoadKVOKey()) + observation = nil } - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if let dict = object as? SKKDictionary { - if dict.isWaitingForLoad { - self.disableAllKeys() - loadingProgressView.startAnimating() - } else { - self.enableAllKeys() - loadingProgressView.stopAnimating() - } + private func observeDictionaryIsWaitingForLoad(_ dict: SKKDictionary) { + if dict.isWaitingForLoad { + self.disableAllKeys() + loadingProgressView.startAnimating() } else { - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + self.enableAllKeys() + loadingProgressView.stopAnimating() } } diff --git a/FlickSKKKeyboard/SKKDelegate.swift b/FlickSKKKeyboard/SKKDelegate.swift index 9b7f125..7820b4f 100644 --- a/FlickSKKKeyboard/SKKDelegate.swift +++ b/FlickSKKKeyboard/SKKDelegate.swift @@ -8,6 +8,7 @@ import Foundation +@MainActor protocol SKKDelegate : AnyObject { // 確定文字の表示 func insertText(_ text : String) diff --git a/FlickSKKKeyboard/SKKEngine.swift b/FlickSKKKeyboard/SKKEngine.swift index c7c907b..7ca0f1e 100644 --- a/FlickSKKKeyboard/SKKEngine.swift +++ b/FlickSKKKeyboard/SKKEngine.swift @@ -1,6 +1,7 @@ // SKKのメインエンジン -class SKKEngine { +@MainActor +final class SKKEngine: Sendable { fileprivate let keyHandler : KeyHandler fileprivate weak var delegate : SKKDelegate? diff --git a/FlickSKKKeyboard/SessionView.swift b/FlickSKKKeyboard/SessionView.swift index 4bcdf3d..b694f3c 100644 --- a/FlickSKKKeyboard/SessionView.swift +++ b/FlickSKKKeyboard/SessionView.swift @@ -152,7 +152,7 @@ class SessionView: UIView, UICollectionViewDataSource, UICollectionViewDelegate, } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - struct Static { static let layoutCell = CandidateCollectionViewCell() } + @MainActor struct Static { static let layoutCell = CandidateCollectionViewCell() } let minWidth: CGFloat switch Section(rawValue: indexPath.section) { diff --git a/FlickSKKKeyboard/TextEngine.swift b/FlickSKKKeyboard/TextEngine.swift index 16b7c80..4502a66 100644 --- a/FlickSKKKeyboard/TextEngine.swift +++ b/FlickSKKKeyboard/TextEngine.swift @@ -1,7 +1,8 @@ // トップレベルと、単語登録モードではテキストの挿入先が異なる。 // そこを抽象化する。 -class TextEngine { +@MainActor +final class TextEngine: Sendable { enum Status { // トップレベルのため、iOS側にテキストの追加・削除を伝える case topLevel diff --git a/FlickSKKTests/DictionaryEngineSpec.swift b/FlickSKKTests/DictionaryEngineSpec.swift index 7d08437..6cf0bb4 100644 --- a/FlickSKKTests/DictionaryEngineSpec.swift +++ b/FlickSKKTests/DictionaryEngineSpec.swift @@ -1,7 +1,8 @@ import Quick import Nimble -class DictionaryEngineSpec : QuickSpec { +@MainActor +final class DictionaryEngineSpec : QuickSpec, Sendable { lazy var dictionary : SKKDictionary = { DictionarySettings.bundle = Bundle(for: self.classForCoder) let dict = SKKDictionary() @@ -10,15 +11,17 @@ class DictionaryEngineSpec : QuickSpec { }() override func spec() { - var dictionaryEngine : DictionaryEngine! - - beforeEach { - dictionaryEngine = DictionaryEngine(dictionary: self.dictionary) - } - - describe("#find") { - it("送り仮名を補う") { - expect(dictionaryEngine.find("おく", okuri: "る", dynamic: false)).notTo(beEmpty()) + MainActor.assumeIsolated { + var dictionaryEngine : DictionaryEngine! + + beforeEach { + dictionaryEngine = DictionaryEngine(dictionary: self.dictionary) + } + + describe("#find") { + it("送り仮名を補う") { + expect(dictionaryEngine.find("おく", okuri: "る", dynamic: false)).notTo(beEmpty()) + } } } } diff --git a/FlickSKKTests/KeyHandlerBaseSpec.swift b/FlickSKKTests/KeyHandlerBaseSpec.swift index 2feda39..05ea35c 100644 --- a/FlickSKKTests/KeyHandlerBaseSpec.swift +++ b/FlickSKKTests/KeyHandlerBaseSpec.swift @@ -1,7 +1,8 @@ import Quick import Nimble -class KeyHandlerBaseSpec : QuickSpec { +@MainActor +class KeyHandlerBaseSpec : QuickSpec, Sendable { lazy var dictionary : SKKDictionary = { DictionarySettings.bundle = Bundle(for: self.classForCoder) let dict = SKKDictionary() diff --git a/FlickSKKTests/KeyHandlerDirectInputSpec.swift b/FlickSKKTests/KeyHandlerDirectInputSpec.swift index 9675606..c4746e1 100644 --- a/FlickSKKTests/KeyHandlerDirectInputSpec.swift +++ b/FlickSKKTests/KeyHandlerDirectInputSpec.swift @@ -1,56 +1,59 @@ import Quick import Nimble -class KeyHandlerDirectInputSpec : KeyHandlerBaseSpec { +@MainActor +final class KeyHandlerDirectInputSpec : KeyHandlerBaseSpec, Sendable { override func spec() { - var handler : KeyHandler! - var delegate : MockDelegate! - - beforeEach { - let (h, d) = self.create(self.dictionary) - handler = h - delegate = d - } - - context("directInput") { - it("文字入力(シフトなし)") { - _ = handler.handle(.char(kana: "あ", shift: false), composeMode: .directInput) - expect(delegate.insertedText).to(equal("あ")) - } - it("Space") { - _ = handler.handle(.space, composeMode: .directInput) - expect(delegate.insertedText).to(equal(" ")) - } - it("Enter") { - _ = handler.handle(.enter, composeMode: .directInput) - expect(delegate.insertedText).to(equal("\n")) - } - it("Backspace") { - delegate.insertedText = "foo" - _ = handler.handle(.backspace, composeMode: .directInput) - expect(delegate.insertedText).to(equal("fo")) - } - it("大文字変換") { - delegate.insertedText = "foo" - _ = handler.handle(.toggleUpperLower(beforeText: "o"), composeMode: .directInput) - expect(delegate.insertedText).to(equal("foO")) - - } - it("濁点変換") { - delegate.insertedText = "か" - _ = handler.handle(.toggleDakuten(beforeText: "か"), composeMode: .directInput) - expect(delegate.insertedText).to(equal("が")) - } - it("入力モード") { - _ = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: .directInput) - expect(delegate.inputMode).to(equal(SKKInputMode.katakana)) - } - it("シフトあり文字入力") { - let m = handler.handle(.char(kana: "あ", shift: true), composeMode: .directInput) - expect(delegate.insertedText).to(equal("")) - expect(self.kana(m)).to(equal("あ")) - expect(self.candidates(m)).toNot(beEmpty()) + MainActor.assumeIsolated { + var handler : KeyHandler! + var delegate : MockDelegate! + + beforeEach { + let (h, d) = self.create(self.dictionary) + handler = h + delegate = d + } + + context("directInput") { + it("文字入力(シフトなし)") { + _ = handler.handle(.char(kana: "あ", shift: false), composeMode: .directInput) + expect(delegate.insertedText).to(equal("あ")) + } + it("Space") { + _ = handler.handle(.space, composeMode: .directInput) + expect(delegate.insertedText).to(equal(" ")) + } + it("Enter") { + _ = handler.handle(.enter, composeMode: .directInput) + expect(delegate.insertedText).to(equal("\n")) + } + it("Backspace") { + delegate.insertedText = "foo" + _ = handler.handle(.backspace, composeMode: .directInput) + expect(delegate.insertedText).to(equal("fo")) + } + it("大文字変換") { + delegate.insertedText = "foo" + _ = handler.handle(.toggleUpperLower(beforeText: "o"), composeMode: .directInput) + expect(delegate.insertedText).to(equal("foO")) + + } + it("濁点変換") { + delegate.insertedText = "か" + _ = handler.handle(.toggleDakuten(beforeText: "か"), composeMode: .directInput) + expect(delegate.insertedText).to(equal("が")) + } + it("入力モード") { + _ = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: .directInput) + expect(delegate.inputMode).to(equal(SKKInputMode.katakana)) + } + it("シフトあり文字入力") { + let m = handler.handle(.char(kana: "あ", shift: true), composeMode: .directInput) + expect(delegate.insertedText).to(equal("")) + expect(self.kana(m)).to(equal("あ")) + expect(self.candidates(m)).toNot(beEmpty()) + } } } } diff --git a/FlickSKKTests/KeyHandlerKanaComposeSpec.swift b/FlickSKKTests/KeyHandlerKanaComposeSpec.swift index 6fe48f8..8d81924 100644 --- a/FlickSKKTests/KeyHandlerKanaComposeSpec.swift +++ b/FlickSKKTests/KeyHandlerKanaComposeSpec.swift @@ -1,9 +1,11 @@ import Quick import Nimble -class KeyHandlerKanaComposeSpec : KeyHandlerBaseSpec { +@MainActor +final class KeyHandlerKanaComposeSpec : KeyHandlerBaseSpec, Sendable { override func spec() { + MainActor.assumeIsolated { var handler : KeyHandler! var delegate : MockDelegate! @@ -17,171 +19,172 @@ class KeyHandlerKanaComposeSpec : KeyHandlerBaseSpec { let candidates = exacts(["川", "河"]) - context("kana compose") { - let composeMode = ComposeMode.kanaCompose(kana: "かわ", candidates: candidates) - it("文字入力(シフトなし)") { - let m = handler.handle(.char(kana: "ら", shift: false), composeMode: composeMode) - expect(self.kana(m)).to(equal("かわら")) - } - describe("Space") { - it("partialな候補がある場合") { - self.dictionary.partial("かわなんとか", okuri: .none, kanji: "カワナントカ") - let m = handler.handle(.space, composeMode: composeMode) - _ = self.kanji(m)! - if let c = self.candidates(m)?[0] { - switch c { - case let .partial(kanji: kanji, kana: _): - expect(kanji).to(equal("カワナントカ")) + context("kana compose") { + let composeMode = ComposeMode.kanaCompose(kana: "かわ", candidates: candidates) + it("文字入力(シフトなし)") { + let m = handler.handle(.char(kana: "ら", shift: false), composeMode: composeMode) + expect(self.kana(m)).to(equal("かわら")) + } + describe("Space") { + it("partialな候補がある場合") { + self.dictionary.partial("かわなんとか", okuri: .none, kanji: "カワナントカ") + let m = handler.handle(.space, composeMode: composeMode) + _ = self.kanji(m)! + if let c = self.candidates(m)?[0] { + switch c { + case let .partial(kanji: kanji, kana: _): + expect(kanji).to(equal("カワナントカ")) + default: + fail() + } + + } else { + fail() + } + } + + it("単語がある場合") { + let m = handler.handle(.space, composeMode: composeMode) + let (kana, okuri) = self.kanji(m)! + expect(kana).to(equal("かわ")) + expect(okuri).to(equal("")) + expect(self.candidates(m)).toNot(beEmpty()) + } + it("単語がない場合") { + let m = handler.handle(.space, composeMode: .kanaCompose(kana: "あああ", candidates: [])) + switch m { + case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("あああ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("")) + expect(composeMode[0] == .directInput).to(beTrue()) default: fail() } - - } else { - fail() } } - - it("単語がある場合") { - let m = handler.handle(.space, composeMode: composeMode) - let (kana, okuri) = self.kanji(m)! - expect(kana).to(equal("かわ")) - expect(okuri).to(equal("")) - expect(self.candidates(m)).toNot(beEmpty()) - } - it("単語がない場合") { - let m = handler.handle(.space, composeMode: .kanaCompose(kana: "あああ", candidates: [])) - switch m { - case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): - expect(kana).to(equal("あああ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() + describe("SkipPartialCandidates") { + it("partialな候補がある場合はexactまでスキップ") { + self.dictionary.partial("かわなんとか", okuri: .none, kanji: "カワナントカ") + let m = handler.handle(.skipPartialCandidates, composeMode: composeMode) + switch m { + case let .kanjiCompose(kana: kana, okuri: okuri, candidates: candidates, index: index): + expect(kana).to(equal("かわ")) + expect(okuri).to(beNil()) + expect(candidates).toNot(beEmpty()) + expect(index).to(equal(1)) + default: + fail() + } } - } - } - describe("SkipPartialCandidates") { - it("partialな候補がある場合はexactまでスキップ") { - self.dictionary.partial("かわなんとか", okuri: .none, kanji: "カワナントカ") - let m = handler.handle(.skipPartialCandidates, composeMode: composeMode) - switch m { - case let .kanjiCompose(kana: kana, okuri: okuri, candidates: candidates, index: index): - expect(kana).to(equal("かわ")) - expect(okuri).to(beNil()) - expect(candidates).toNot(beEmpty()) - expect(index).to(equal(1)) - default: - fail() + + it("partialな候補がなくexactの候補がある場合は通常の変換") { + let m = handler.handle(.skipPartialCandidates, composeMode: composeMode) + switch m { + case let .kanjiCompose(kana: kana, okuri: okuri, candidates: candidates, index: index): + expect(kana).to(equal("かわ")) + expect(okuri).to(beNil()) + expect(candidates).toNot(beEmpty()) + expect(index).to(equal(1)) + default: + fail() + } } - } - - it("partialな候補がなくexactの候補がある場合は通常の変換") { - let m = handler.handle(.skipPartialCandidates, composeMode: composeMode) - switch m { - case let .kanjiCompose(kana: kana, okuri: okuri, candidates: candidates, index: index): - expect(kana).to(equal("かわ")) - expect(okuri).to(beNil()) - expect(candidates).toNot(beEmpty()) - expect(index).to(equal(1)) - default: - fail() + + it("partial,exactどちらも候補がない場合は登録") { + let m = handler.handle(.space, composeMode: .kanaCompose(kana: "あああ", candidates: [])) + switch m { + case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("あああ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("")) + expect(composeMode[0] == .directInput).to(beTrue()) + default: + fail() + } } } - - it("partial,exactどちらも候補がない場合は登録") { - let m = handler.handle(.space, composeMode: .kanaCompose(kana: "あああ", candidates: [])) - switch m { - case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): - expect(kana).to(equal("あああ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() + it("Enter") { + let m = handler.handle(.enter, composeMode: composeMode) + expect(m == .directInput).to(beTrue()) + expect(delegate.insertedText).to(equal("かわ")) + } + describe("Backspace") { + it("文字がある場合") { + let m = handler.handle(.backspace, composeMode: composeMode) + expect(self.kana(m)).to(equal("か")) + } + it("文字がなくなる場合") { + let m = handler.handle(.backspace, composeMode: .kanaCompose(kana: "か", candidates: [])) + expect(m == .directInput).to(beTrue()) } } - } - it("Enter") { - let m = handler.handle(.enter, composeMode: composeMode) - expect(m == .directInput).to(beTrue()) - expect(delegate.insertedText).to(equal("かわ")) - } - describe("Backspace") { - it("文字がある場合") { - let m = handler.handle(.backspace, composeMode: composeMode) - expect(self.kana(m)).to(equal("か")) + it("大文字変換") { + let m = handler.handle(.toggleUpperLower(beforeText: ""), composeMode: .kanaCompose(kana: "foo", candidates: [])) + expect(self.kana(m)).to(equal("foO")) } - it("文字がなくなる場合") { - let m = handler.handle(.backspace, composeMode: .kanaCompose(kana: "か", candidates: [])) - expect(m == .directInput).to(beTrue()) + it("濁点変換") { + let m = handler.handle(.toggleDakuten(beforeText: ""), composeMode: .kanaCompose(kana: "か", candidates: [])) + expect(self.kana(m)).to(equal("が")) } - } - it("大文字変換") { - let m = handler.handle(.toggleUpperLower(beforeText: ""), composeMode: .kanaCompose(kana: "foo", candidates: [])) - expect(self.kana(m)).to(equal("foO")) - } - it("濁点変換") { - let m = handler.handle(.toggleDakuten(beforeText: ""), composeMode: .kanaCompose(kana: "か", candidates: [])) - expect(self.kana(m)).to(equal("が")) - } - it("入力モード") { - let m = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: composeMode) - expect(m == .directInput).to(beTrue()) - expect(delegate.insertedText).to(equal("カワ")) - } - it("略語学習") { - let composeMode = ComposeMode.kanaCompose(kana: "はなやまた", candidates: candidates) - _ = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: composeMode) - let xs = self.dictionary.findDynamic("はなや").filter { w in - w.kanji == "ハナヤマタ" + it("入力モード") { + let m = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: composeMode) + expect(m == .directInput).to(beTrue()) + expect(delegate.insertedText).to(equal("カワ")) } - expect(xs.count).to(equal(1)) - } - describe("シフトあり文字入力") { - it("単語がある場合") { - self.dictionary.partial("かわなんとか", okuri: .none, kanji: "カワナントカ") - let m = handler.handle(.char(kana: "い", shift: true), composeMode: composeMode) - let (kana, okuri) = self.kanji(m)! - let kanjis = self.candidates(m)?.map({ c in c.kanji }) - - expect(kana).to(equal("かわ")) - expect(okuri).to(equal("い")) - - expect(kanjis).to(contain("乾い")) - expect(kanjis).toNot(contain("カワナントカ")) + it("略語学習") { + let composeMode = ComposeMode.kanaCompose(kana: "はなやまた", candidates: candidates) + _ = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: composeMode) + let xs = self.dictionary.findDynamic("はなや").filter { w in + w.kanji == "ハナヤマタ" + } + expect(xs.count).to(equal(1)) } - it("単語がない場合") { - let m = handler.handle(.char(kana: "あ", shift: true), composeMode: composeMode) - switch m { - case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + describe("シフトあり文字入力") { + it("単語がある場合") { + self.dictionary.partial("かわなんとか", okuri: .none, kanji: "カワナントカ") + let m = handler.handle(.char(kana: "い", shift: true), composeMode: composeMode) + let (kana, okuri) = self.kanji(m)! + let kanjis = self.candidates(m)?.map({ c in c.kanji }) + expect(kana).to(equal("かわ")) - expect(okuri).to(equal("あ")) - expect(composeText).to(equal("")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() + expect(okuri).to(equal("い")) + + expect(kanjis).to(contain("乾い")) + expect(kanjis).toNot(contain("カワナントカ")) + } + it("単語がない場合") { + let m = handler.handle(.char(kana: "あ", shift: true), composeMode: composeMode) + switch m { + case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("かわ")) + expect(okuri).to(equal("あ")) + expect(composeText).to(equal("")) + expect(composeMode[0] == .directInput).to(beTrue()) + default: + fail() + } } } - } - describe("候補選択") { - it("選択") { - let m = handler.handle(.select(index: 0), composeMode: composeMode) - expect(delegate.insertedText).to(equal("川")) - expect(m == .directInput).to(beTrue()) - // 学習したものが先頭にくる - expect(self.dictionary.find("かわ", okuri: nil)[0]).to(equal("川")) - } - it("単語登録モード") { - let m = handler.handle(.select(index: 2), composeMode: composeMode) - switch m { - case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): - expect(kana).to(equal("かわ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() + describe("候補選択") { + it("選択") { + let m = handler.handle(.select(index: 0), composeMode: composeMode) + expect(delegate.insertedText).to(equal("川")) + expect(m == .directInput).to(beTrue()) + // 学習したものが先頭にくる + expect(self.dictionary.find("かわ", okuri: nil)[0]).to(equal("川")) + } + it("単語登録モード") { + let m = handler.handle(.select(index: 2), composeMode: composeMode) + switch m { + case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("かわ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("")) + expect(composeMode[0] == .directInput).to(beTrue()) + default: + fail() + } } } } diff --git a/FlickSKKTests/KeyHandlerKanjiComposeSpec.swift b/FlickSKKTests/KeyHandlerKanjiComposeSpec.swift index d938f15..51974c7 100644 --- a/FlickSKKTests/KeyHandlerKanjiComposeSpec.swift +++ b/FlickSKKTests/KeyHandlerKanjiComposeSpec.swift @@ -1,180 +1,182 @@ import Quick import Nimble -class KeyHandlerKanjiComposeSpec : KeyHandlerBaseSpec { +@MainActor +final class KeyHandlerKanjiComposeSpec : KeyHandlerBaseSpec, Sendable { override func spec() { - var handler : KeyHandler! - var delegate : MockDelegate! - - beforeEach { - let (h, d) = self.create(self.dictionary) - handler = h - delegate = d - } - - let candidates = exacts(["川", "河"]) - let candidatesWithOkuri = exacts(["居る", "入る"]) - - context("kanji compose") { - let composeMode = ComposeMode.kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 0) - - it("文字入力(シフトなし)") { - let m = handler.handle(.char(kana: "に", shift: false), composeMode: composeMode) - expect(delegate.insertedText).to(equal("川に")) - expect(m == .directInput).to(beTrue()) - // 学習したものが先頭にくる - expect(self.dictionary.find("かわ", okuri: nil)[0]).to(equal("川")) + MainActor.assumeIsolated { + var handler : KeyHandler! + var delegate : MockDelegate! + + beforeEach { + let (h, d) = self.create(self.dictionary) + handler = h + delegate = d } - describe("Space") { - it("単語がある場合") { - let m = handler.handle(.space, composeMode: composeMode) - let (kana, okuri) = self.kanji(m)! - expect(kana).to(equal("かわ")) - expect(okuri).to(equal("")) - expect(self.index(m)).to(equal(1)) - } - it("送り仮名がある場合") { - let m = handler.handle(.space, composeMode: ComposeMode.kanjiCompose(kana: "い", okuri: "る", - candidates: candidatesWithOkuri, index: 0)) - let (kana, okuri) = self.kanji(m)! - expect(kana).to(equal("い")) - expect(okuri).to(equal("る")) - expect(self.index(m)).to(equal(1)) + + let candidates = exacts(["川", "河"]) + let candidatesWithOkuri = exacts(["居る", "入る"]) + + context("kanji compose") { + let composeMode = ComposeMode.kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 0) + + it("文字入力(シフトなし)") { + let m = handler.handle(.char(kana: "に", shift: false), composeMode: composeMode) + expect(delegate.insertedText).to(equal("川に")) + expect(m == .directInput).to(beTrue()) + // 学習したものが先頭にくる + expect(self.dictionary.find("かわ", okuri: nil)[0]).to(equal("川")) } - it("単語がない場合") { - let m = handler.handle(.space, composeMode: .kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 1)) - switch m { - case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + describe("Space") { + it("単語がある場合") { + let m = handler.handle(.space, composeMode: composeMode) + let (kana, okuri) = self.kanji(m)! expect(kana).to(equal("かわ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() + expect(okuri).to(equal("")) + expect(self.index(m)).to(equal(1)) } - } - } - describe("SkipPartialCandidates") { - it("partialな候補がある場合はexactまでスキップ") { - let candidates: [Candidate] = [.partial(kanji: "かわなんとか", kana: "カワナントカ0"), .partial(kanji: "かわなんとか", kana: "カワナントカ1")] + self.exacts(["川", "河"]) - let m = handler.handle(.skipPartialCandidates, composeMode: ComposeMode.kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 0)) - switch m { - case let .kanjiCompose(kana: kana, okuri: okuri, candidates: candidates, index: index): - expect(kana).to(equal("かわ")) - expect(okuri).to(beNil()) - expect(candidates).toNot(beEmpty()) - expect(index).to(equal(2)) - default: - fail() + it("送り仮名がある場合") { + let m = handler.handle(.space, composeMode: ComposeMode.kanjiCompose(kana: "い", okuri: "る", + candidates: candidatesWithOkuri, index: 0)) + let (kana, okuri) = self.kanji(m)! + expect(kana).to(equal("い")) + expect(okuri).to(equal("る")) + expect(self.index(m)).to(equal(1)) + } + it("単語がない場合") { + let m = handler.handle(.space, composeMode: .kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 1)) + switch m { + case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("かわ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("")) + expect(composeMode[0] == .directInput).to(beTrue()) + default: + fail() + } } } - - it("partialな候補がなくexactの候補がある場合は通常の変換") { - let m = handler.handle(.skipPartialCandidates, composeMode: composeMode) - switch m { - case let .kanjiCompose(kana: kana, okuri: okuri, candidates: candidates, index: index): - expect(kana).to(equal("かわ")) - expect(okuri).to(beNil()) - expect(candidates).toNot(beEmpty()) - expect(index).to(equal(1)) - default: - fail() + describe("SkipPartialCandidates") { + it("partialな候補がある場合はexactまでスキップ") { + let candidates: [Candidate] = [.partial(kanji: "かわなんとか", kana: "カワナントカ0"), .partial(kanji: "かわなんとか", kana: "カワナントカ1")] + self.exacts(["川", "河"]) + let m = handler.handle(.skipPartialCandidates, composeMode: ComposeMode.kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 0)) + switch m { + case let .kanjiCompose(kana: kana, okuri: okuri, candidates: candidates, index: index): + expect(kana).to(equal("かわ")) + expect(okuri).to(beNil()) + expect(candidates).toNot(beEmpty()) + expect(index).to(equal(2)) + default: + fail() + } + } + + it("partialな候補がなくexactの候補がある場合は通常の変換") { + let m = handler.handle(.skipPartialCandidates, composeMode: composeMode) + switch m { + case let .kanjiCompose(kana: kana, okuri: okuri, candidates: candidates, index: index): + expect(kana).to(equal("かわ")) + expect(okuri).to(beNil()) + expect(candidates).toNot(beEmpty()) + expect(index).to(equal(1)) + default: + fail() + } + } + + it("partial,exactどちらも候補がない場合は登録") { + let m = handler.handle(.skipPartialCandidates, composeMode: .kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 1)) + switch m { + case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("かわ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("")) + expect(composeMode[0] == .directInput).to(beTrue()) + default: + fail() + } } } - - it("partial,exactどちらも候補がない場合は登録") { - let m = handler.handle(.skipPartialCandidates, composeMode: .kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 1)) - switch m { - case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): - expect(kana).to(equal("かわ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() + it("Enter") { + let m = handler.handle(.enter, composeMode: composeMode) + expect(m == .directInput).to(beTrue()) + expect(delegate.insertedText).to(equal("川")) + // 学習したものが先頭にくる + expect(self.dictionary.find("かわ", okuri: nil)[0]).to(equal("川")) + } + describe("Backspace") { + it("index == 0") { + let m = handler.handle(.backspace, composeMode: composeMode) + expect(self.kana(m)).to(equal("かわ")) + } + it("index > 0") { + let m = handler.handle(.backspace, composeMode: ComposeMode.kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 1)) + expect(self.index(m)).to(equal(0)) } } - } - it("Enter") { - let m = handler.handle(.enter, composeMode: composeMode) - expect(m == .directInput).to(beTrue()) - expect(delegate.insertedText).to(equal("川")) - // 学習したものが先頭にくる - expect(self.dictionary.find("かわ", okuri: nil)[0]).to(equal("川")) - } - describe("Backspace") { - it("index == 0") { - let m = handler.handle(.backspace, composeMode: composeMode) - expect(self.kana(m)).to(equal("かわ")) + it("大文字変換") { + // FIXME: 辞書に適当な単語が登録されていないのでテストしにくい } - it("index > 0") { - let m = handler.handle(.backspace, composeMode: ComposeMode.kanjiCompose(kana: "かわ", okuri: .none, candidates: candidates, index: 1)) + it("濁点変換") { + let m = handler.handle(.toggleDakuten(beforeText: ""), + composeMode: ComposeMode.kanjiCompose(kana: "さわ", + okuri: "き", + candidates: candidates, index: 1)) + let (kana, okuri) = self.kanji(m)! + expect(kana).to(equal("さわ")) + expect(okuri).to(equal("ぎ")) + expect(self.candidates(m)).toNot(beEmpty()) expect(self.index(m)).to(equal(0)) } - } - it("大文字変換") { - // FIXME: 辞書に適当な単語が登録されていないのでテストしにくい - } - it("濁点変換") { - let m = handler.handle(.toggleDakuten(beforeText: ""), - composeMode: ComposeMode.kanjiCompose(kana: "さわ", - okuri: "き", - candidates: candidates, index: 1)) - let (kana, okuri) = self.kanji(m)! - expect(kana).to(equal("さわ")) - expect(okuri).to(equal("ぎ")) - expect(self.candidates(m)).toNot(beEmpty()) - expect(self.index(m)).to(equal(0)) - } - it("入力モード") { - let m = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: composeMode) - expect(m == composeMode).to(beTrue()) - expect(delegate.inputMode == .katakana).to(beTrue()) - } - it("シフトあり文字入力") { - let m = handler.handle(.char(kana: "い", shift: true), composeMode: composeMode) - expect(delegate.insertedText).to(equal("川")) - expect(self.kana(m)).to(equal("い")) - } - describe("候補選択") { - it("選択") { - let m = handler.handle(.select(index: 0), composeMode: composeMode) + it("入力モード") { + let m = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: composeMode) + expect(m == composeMode).to(beTrue()) + expect(delegate.inputMode == .katakana).to(beTrue()) + } + it("シフトあり文字入力") { + let m = handler.handle(.char(kana: "い", shift: true), composeMode: composeMode) expect(delegate.insertedText).to(equal("川")) - expect(m == .directInput).to(beTrue()) - } - it("単語登録モード") { - let m = handler.handle(.select(index: 2), composeMode: composeMode) - switch m { - case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): - expect(kana).to(equal("かわ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() - } + expect(self.kana(m)).to(equal("い")) } - } - describe("学習") { - it("送りなし") { - _ = handler.handle(.select(index: 0), composeMode: composeMode) - // 学習したものが先頭にくる - expect(self.dictionary.find("かわ", okuri: nil)[0]).to(equal("川")) + describe("候補選択") { + it("選択") { + let m = handler.handle(.select(index: 0), composeMode: composeMode) + expect(delegate.insertedText).to(equal("川")) + expect(m == .directInput).to(beTrue()) + } + it("単語登録モード") { + let m = handler.handle(.select(index: 2), composeMode: composeMode) + switch m { + case .wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("かわ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("")) + expect(composeMode[0] == .directInput).to(beTrue()) + default: + fail() + } + } } - it("送りあり") { - let composeMode = ComposeMode.kanjiCompose(kana: "い", okuri: "る", candidates: candidatesWithOkuri, index: 0) - _ = handler.handle(.select(index: 1), composeMode: composeMode) - - // 学習したものが先頭にくる - let xs = self.dictionary.find("い", okuri: "r") - expect(xs).notTo(beEmpty()) - - // 送り仮名は学習しない - expect(xs[0]).to(equal("入")) + describe("学習") { + it("送りなし") { + _ = handler.handle(.select(index: 0), composeMode: composeMode) + // 学習したものが先頭にくる + expect(self.dictionary.find("かわ", okuri: nil)[0]).to(equal("川")) + } + it("送りあり") { + let composeMode = ComposeMode.kanjiCompose(kana: "い", okuri: "る", candidates: candidatesWithOkuri, index: 0) + _ = handler.handle(.select(index: 1), composeMode: composeMode) + + // 学習したものが先頭にくる + let xs = self.dictionary.find("い", okuri: "r") + expect(xs).notTo(beEmpty()) + + // 送り仮名は学習しない + expect(xs[0]).to(equal("入")) + } } } } - } } diff --git a/FlickSKKTests/KeyHandlerWordRegisterWithDirectInputSpec.swift b/FlickSKKTests/KeyHandlerWordRegisterWithDirectInputSpec.swift index 3236bb9..117e0c7 100644 --- a/FlickSKKTests/KeyHandlerWordRegisterWithDirectInputSpec.swift +++ b/FlickSKKTests/KeyHandlerWordRegisterWithDirectInputSpec.swift @@ -1,135 +1,138 @@ import Quick import Nimble -class KeyHandlerWordRegisterWithDirectInputSpec : KeyHandlerBaseSpec { - +@MainActor +final class KeyHandlerWordRegisterWithDirectInputSpec : KeyHandlerBaseSpec, Sendable { + override func spec() { - var handler : KeyHandler! - var delegate : MockDelegate! - - beforeEach { - let (h, d) = self.create(self.dictionary) - handler = h - delegate = d - } + MainActor.assumeIsolated { + var handler : KeyHandler! + var delegate : MockDelegate! + + beforeEach { + let (h, d) = self.create(self.dictionary) + handler = h + delegate = d + } - context("word register with direct mode") { - let composeMode = ComposeMode.wordRegister(kana: "ろうた", okuri: "け", composeText : "", composeMode: [ .directInput ]) + context("word register with direct mode") { + let composeMode = ComposeMode.wordRegister(kana: "ろうた", okuri: "け", composeText : "", composeMode: [ .directInput ]) - it("文字入力(シフトなし)") { - let m = handler.handle(.char(kana: "に", shift: false), composeMode: composeMode) - switch m { - case ComposeMode.wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): - expect(kana).to(equal("ろうた")) - expect(okuri).to(equal("け")) - expect(composeText).to(equal("に")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() - } - } - it("Space") { - let m = handler.handle(.space, composeMode: composeMode) - switch m { - case ComposeMode.wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): - expect(kana).to(equal("ろうた")) - expect(okuri).to(equal("け")) - expect(composeText).to(equal(" ")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() - } - } - describe("Enter") { - it("送りなし") { - let m = handler.handle(.enter, composeMode: - .wordRegister(kana: "まじ", okuri: .none, composeText : "本気", composeMode: [ .directInput ])) - expect(m == .directInput).to(beTrue()) - expect(delegate.insertedText).to(equal("本気")) - expect(self.dictionary.find("まじ", okuri: .none)[0]).to(equal("本気")) - } - it("送りあり") { - let m = handler.handle(.enter, composeMode: - .wordRegister(kana: "ろうた", okuri: "け", composeText : "臘長", composeMode: [ .directInput ])) - expect(m == .directInput).to(beTrue()) - expect(delegate.insertedText).to(equal("臘長け")) - expect(self.dictionary.find("ろうた", okuri: "k")).to(contain("臘長")) - } - } - describe("Backspace") { - it("index == 0") { - let m = handler.handle(.backspace, composeMode: composeMode) - expect(self.kana(m)).to(equal("ろうた")) + it("文字入力(シフトなし)") { + let m = handler.handle(.char(kana: "に", shift: false), composeMode: composeMode) + switch m { + case ComposeMode.wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("ろうた")) + expect(okuri).to(equal("け")) + expect(composeText).to(equal("に")) + expect(composeMode[0] == .directInput).to(beTrue()) + default: + fail() + } } - it("index > 0") { - let m = handler.handle(.backspace, - composeMode: .wordRegister(kana: "まじ", okuri: .none, composeText : "本気", composeMode: [ .directInput ])) + it("Space") { + let m = handler.handle(.space, composeMode: composeMode) switch m { case ComposeMode.wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): - expect(kana).to(equal("まじ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("本")) + expect(kana).to(equal("ろうた")) + expect(okuri).to(equal("け")) + expect(composeText).to(equal(" ")) expect(composeMode[0] == .directInput).to(beTrue()) default: fail() } } - } - it("大文字変換") { - let m = handler.handle(.toggleUpperLower(beforeText: ""), composeMode: - .wordRegister(kana: "まじ", okuri: .none, composeText : "foo", composeMode: [ .directInput ])) - switch m { - case ComposeMode.wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): - expect(kana).to(equal("まじ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("foO")) - expect(composeMode[0] == .directInput).to(beTrue()) - default: - fail() + describe("Enter") { + it("送りなし") { + let m = handler.handle(.enter, composeMode: + .wordRegister(kana: "まじ", okuri: .none, composeText : "本気", composeMode: [ .directInput ])) + expect(m == .directInput).to(beTrue()) + expect(delegate.insertedText).to(equal("本気")) + expect(self.dictionary.find("まじ", okuri: .none)[0]).to(equal("本気")) + } + it("送りあり") { + let m = handler.handle(.enter, composeMode: + .wordRegister(kana: "ろうた", okuri: "け", composeText : "臘長", composeMode: [ .directInput ])) + expect(m == .directInput).to(beTrue()) + expect(delegate.insertedText).to(equal("臘長け")) + expect(self.dictionary.find("ろうた", okuri: "k")).to(contain("臘長")) + } } - } - describe("濁点変換") { - it("入力中") { - let m = handler.handle(.toggleDakuten(beforeText: ""), composeMode: - .wordRegister(kana: "まじ", okuri: .none, composeText : "か", composeMode: [ .directInput ])) + describe("Backspace") { + it("index == 0") { + let m = handler.handle(.backspace, composeMode: composeMode) + expect(self.kana(m)).to(equal("ろうた")) + } + it("index > 0") { + let m = handler.handle(.backspace, + composeMode: .wordRegister(kana: "まじ", okuri: .none, composeText : "本気", composeMode: [ .directInput ])) + switch m { + case ComposeMode.wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("まじ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("本")) + expect(composeMode[0] == .directInput).to(beTrue()) + default: + fail() + } + } + } + it("大文字変換") { + let m = handler.handle(.toggleUpperLower(beforeText: ""), composeMode: + .wordRegister(kana: "まじ", okuri: .none, composeText : "foo", composeMode: [ .directInput ])) switch m { case ComposeMode.wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): expect(kana).to(equal("まじ")) expect(okuri).to(beNil()) - expect(composeText).to(equal("が")) + expect(composeText).to(equal("foO")) expect(composeMode[0] == .directInput).to(beTrue()) default: fail() } } - it("冒頭") { - let m = handler.handle(.toggleDakuten(beforeText: ""), composeMode: - .wordRegister(kana: "よ", okuri: "ふ", composeText : "", composeMode: [ .directInput ])) - if let (kana, okuri) = self.kanji(m) { - expect(kana).to(equal("よ")) - expect(okuri).to(equal("ぶ")) - expect(self.candidates(m)).toNot(beEmpty()) - expect(self.index(m)).to(equal(0)) - } else { - fail() + describe("濁点変換") { + it("入力中") { + let m = handler.handle(.toggleDakuten(beforeText: ""), composeMode: + .wordRegister(kana: "まじ", okuri: .none, composeText : "か", composeMode: [ .directInput ])) + switch m { + case ComposeMode.wordRegister(kana: let kana, okuri: let okuri, composeText : let composeText, composeMode : let composeMode): + expect(kana).to(equal("まじ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("が")) + expect(composeMode[0] == .directInput).to(beTrue()) + default: + fail() + } + } + it("冒頭") { + let m = handler.handle(.toggleDakuten(beforeText: ""), composeMode: + .wordRegister(kana: "よ", okuri: "ふ", composeText : "", composeMode: [ .directInput ])) + if let (kana, okuri) = self.kanji(m) { + expect(kana).to(equal("よ")) + expect(okuri).to(equal("ぶ")) + expect(self.candidates(m)).toNot(beEmpty()) + expect(self.index(m)).to(equal(0)) + } else { + fail() + } } } - } - it("入力モード") { - let m = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: composeMode) - expect(m == composeMode).to(beTrue()) - expect(delegate.inputMode == .katakana).to(beTrue()) - } - it("シフトあり文字入力") { - let m = handler.handle(.char(kana: "い", shift: true), composeMode: composeMode) - switch m { - case ComposeMode.wordRegister(kana: let k, okuri: let okuri, composeText : let composeText, composeMode : let xs): - expect(k).to(equal("ろうた")) - expect(okuri).to(equal("け")) - expect(composeText).to(equal("")) - expect(self.kana(xs[0])).to(equal("い")) - default: - fail() + it("入力モード") { + let m = handler.handle(.inputModeChange(inputMode : .katakana), composeMode: composeMode) + expect(m == composeMode).to(beTrue()) + expect(delegate.inputMode == .katakana).to(beTrue()) + } + it("シフトあり文字入力") { + let m = handler.handle(.char(kana: "い", shift: true), composeMode: composeMode) + switch m { + case ComposeMode.wordRegister(kana: let k, okuri: let okuri, composeText : let composeText, composeMode : let xs): + expect(k).to(equal("ろうた")) + expect(okuri).to(equal("け")) + expect(composeText).to(equal("")) + expect(self.kana(xs[0])).to(equal("い")) + default: + fail() + } } } } diff --git a/FlickSKKTests/KeyHandlerWordRegisterWithKanaComposeSpec.swift b/FlickSKKTests/KeyHandlerWordRegisterWithKanaComposeSpec.swift index 1c87b2d..acef042 100644 --- a/FlickSKKTests/KeyHandlerWordRegisterWithKanaComposeSpec.swift +++ b/FlickSKKTests/KeyHandlerWordRegisterWithKanaComposeSpec.swift @@ -1,9 +1,11 @@ import Quick import Nimble -class KeyHandlerWordRegisterWithKanaComposeSpec : KeyHandlerBaseSpec { +@MainActor +final class KeyHandlerWordRegisterWithKanaComposeSpec : KeyHandlerBaseSpec, Sendable { override func spec() { + MainActor.assumeIsolated { var handler : KeyHandler! var delegate : MockDelegate! @@ -40,46 +42,47 @@ class KeyHandlerWordRegisterWithKanaComposeSpec : KeyHandlerBaseSpec { } } - context("word register with kanji mode") { - let composeMode = ComposeMode.wordRegister(kana: "まじ", - okuri: .none, composeText : "か", composeMode: [ .kanjiCompose(kana: "やま", okuri : .none, candidates: self.exacts(["山"]), index: 0)]) - it("Enter") { - let m = handler.handle(.enter, composeMode : composeMode) - switch m { - case ComposeMode.wordRegister(kana: let k, okuri: let okuri, composeText : let composeText, composeMode : _): - expect(k).to(equal("まじ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("か山")) - // 学習したものが先頭にくる - expect(self.dictionary.find("やま", okuri: nil)[0]).to(equal("山")) - default: - fail() + context("word register with kanji mode") { + let composeMode = ComposeMode.wordRegister(kana: "まじ", + okuri: .none, composeText : "か", composeMode: [ .kanjiCompose(kana: "やま", okuri : .none, candidates: self.exacts(["山"]), index: 0)]) + it("Enter") { + let m = handler.handle(.enter, composeMode : composeMode) + switch m { + case ComposeMode.wordRegister(kana: let k, okuri: let okuri, composeText : let composeText, composeMode : _): + expect(k).to(equal("まじ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("か山")) + // 学習したものが先頭にくる + expect(self.dictionary.find("やま", okuri: nil)[0]).to(equal("山")) + default: + fail() + } } - } - it("シフト付き") { - let m = handler.handle(.char(kana: "あ", shift: true), composeMode : composeMode) - switch m { - case ComposeMode.wordRegister(kana: let k, okuri: let okuri, composeText : let composeText, composeMode : let xs): - expect(k).to(equal("まじ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("か山")) - expect(delegate.insertedText).to(equal("")) - expect(self.kana(xs[0])).to(equal("あ")) - default: - fail() + it("シフト付き") { + let m = handler.handle(.char(kana: "あ", shift: true), composeMode : composeMode) + switch m { + case ComposeMode.wordRegister(kana: let k, okuri: let okuri, composeText : let composeText, composeMode : let xs): + expect(k).to(equal("まじ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("か山")) + expect(delegate.insertedText).to(equal("")) + expect(self.kana(xs[0])).to(equal("あ")) + default: + fail() + } } - } - it("シフトなし") { - let m = handler.handle(.char(kana: "あ", shift: false), composeMode : composeMode) - switch m { - case ComposeMode.wordRegister(kana: let k, okuri: let okuri, composeText : let composeText, composeMode : let xs): - expect(k).to(equal("まじ")) - expect(okuri).to(beNil()) - expect(composeText).to(equal("か山あ")) - expect(delegate.insertedText).to(equal("")) - expect(xs[0] == .directInput).to(beTrue()) - default: - fail() + it("シフトなし") { + let m = handler.handle(.char(kana: "あ", shift: false), composeMode : composeMode) + switch m { + case ComposeMode.wordRegister(kana: let k, okuri: let okuri, composeText : let composeText, composeMode : let xs): + expect(k).to(equal("まじ")) + expect(okuri).to(beNil()) + expect(composeText).to(equal("か山あ")) + expect(delegate.insertedText).to(equal("")) + expect(xs[0] == .directInput).to(beTrue()) + default: + fail() + } } } } diff --git a/FlickSKKTests/MockDelegate.swift b/FlickSKKTests/MockDelegate.swift index ab03d43..e17cd9b 100644 --- a/FlickSKKTests/MockDelegate.swift +++ b/FlickSKKTests/MockDelegate.swift @@ -1,4 +1,5 @@ -class MockDelegate : SKKDelegate { +@MainActor +final class MockDelegate : SKKDelegate, Sendable { var insertedText = "" var inputMode : SKKInputMode = .hirakana diff --git a/FlickSKKTests/SKKDictionarySpec.swift b/FlickSKKTests/SKKDictionarySpec.swift index 7a3e168..ce89034 100644 --- a/FlickSKKTests/SKKDictionarySpec.swift +++ b/FlickSKKTests/SKKDictionarySpec.swift @@ -1,7 +1,8 @@ import Quick import Nimble -class SKKDictionarySpec : QuickSpec { +@MainActor +final class SKKDictionarySpec : QuickSpec, Sendable { lazy var dictionary : SKKDictionary = { DictionarySettings.bundle = Bundle(for: self.classForCoder) let dict = SKKDictionary() @@ -10,42 +11,44 @@ class SKKDictionarySpec : QuickSpec { }() override func spec() { - describe("#findDynamic") { - it("重複して取得しない") { - self.dictionary.register("ほんき", okuri: nil, kanji: "本気") - self.dictionary.learn("ほんき", okuri: nil, kanji: "本気") - let xs = self.dictionary.findDynamic("ほん").filter { w in - w.kanji == "本気" + MainActor.assumeIsolated { + describe("#findDynamic") { + it("重複して取得しない") { + self.dictionary.register("ほんき", okuri: nil, kanji: "本気") + self.dictionary.learn("ほんき", okuri: nil, kanji: "本気") + let xs = self.dictionary.findDynamic("ほん").filter { w in + w.kanji == "本気" + } + expect(xs.count).to(equal(1)) + expect(xs[0].kanji).to(equal("本気")) + expect(xs[0].kana).to(equal("ほんき")) } - expect(xs.count).to(equal(1)) - expect(xs[0].kanji).to(equal("本気")) - expect(xs[0].kana).to(equal("ほんき")) } - } - describe("#find") { - it("重複して取得しない") { - self.dictionary.register("ほんき", okuri: nil, kanji: "本気") - let xs = self.dictionary.find("ほんき", okuri: nil).filter { w in - w == "本気" + describe("#find") { + it("重複して取得しない") { + self.dictionary.register("ほんき", okuri: nil, kanji: "本気") + let xs = self.dictionary.find("ほんき", okuri: nil).filter { w in + w == "本気" + } + expect(xs.count).to(equal(1)) } - expect(xs.count).to(equal(1)) } - } - describe("#partial") { - beforeEach { - self.dictionary.partial("ほんき", okuri: nil, kanji: "ホンキ") - } + describe("#partial") { + beforeEach { + self.dictionary.partial("ほんき", okuri: nil, kanji: "ホンキ") + } - it("ダイナミック変換できる") { - let xs = self.dictionary.findDynamic("ほん") - expect(xs[0].kanji).to(equal("ホンキ")) - } + it("ダイナミック変換できる") { + let xs = self.dictionary.findDynamic("ほん") + expect(xs[0].kanji).to(equal("ホンキ")) + } - it("検索にはでてこない") { - let xs = self.dictionary.find("ほんき", okuri: nil) - expect(xs).toNot(contain("ホンキ")) + it("検索にはでてこない") { + let xs = self.dictionary.find("ほんき", okuri: nil) + expect(xs).toNot(contain("ホンキ")) + } } } } diff --git a/FlickSKKTests/SKKEngineSpec.swift b/FlickSKKTests/SKKEngineSpec.swift index 5a460c9..7b87c2b 100644 --- a/FlickSKKTests/SKKEngineSpec.swift +++ b/FlickSKKTests/SKKEngineSpec.swift @@ -5,11 +5,11 @@ // Created by mzp on 2014/10/08. // Copyright (c) 2014年 BAN Jun. All rights reserved. // - import Quick import Nimble -class SKKEngineSpec : QuickSpec, SKKDelegate { +@MainActor +final class SKKEngineSpec : QuickSpec, SKKDelegate, Sendable { // delegate func insertText(_ text : String) { self.insertedText += text } func deleteBackward() {} @@ -27,203 +27,205 @@ class SKKEngineSpec : QuickSpec, SKKDelegate { var candidates: [Candidate]? = nil override func spec() { - var engine : SKKEngine! - DictionarySettings.bundle = Bundle(for: self.classForCoder) - - beforeEach { - SKKDictionary.resetLearnDictionary() - _ = try? FileManager.default.removeItem(at: DictionarySettings.defaultUserDictionaryURL()) - let dict = SKKDictionary() - dict.waitForLoading() - engine = SKKEngine(delegate: self, dictionary: dict) - self.insertedText = "" - } - - context("hirakana mode") { - describe("ひらかな input") { - it("insert text") { - engine.handle(.char(kana: "あ", shift: false)) - engine.handle(.char(kana: "い", shift: false)) - engine.handle(.char(kana: "う", shift: false)) - expect(self.insertedText).to(equal("あいう")) + MainActor.assumeIsolated { + var engine : SKKEngine! + DictionarySettings.bundle = Bundle(for: self.classForCoder) + + beforeEach { @MainActor in + SKKDictionary.resetLearnDictionary() + _ = try? FileManager.default.removeItem(at: DictionarySettings.defaultUserDictionaryURL()) + let dict = SKKDictionary() + dict.waitForLoading() + engine = SKKEngine(delegate: self, dictionary: dict) + self.insertedText = "" + } + + context("hirakana mode") { + describe("ひらかな input") { + it("insert text") { @MainActor in + engine.handle(.char(kana: "あ", shift: false)) + engine.handle(.char(kana: "い", shift: false)) + engine.handle(.char(kana: "う", shift: false)) + expect(self.insertedText).to(equal("あいう")) + } + it("convert kanji") { @MainActor in + engine.handle(.char(kana: "や", shift: true)) + engine.handle(.char(kana: "ま", shift: false)) + expect(self.currentComposeText).to(equal("▽")) + expect(self.currentMarkedText).to(equal("やま")) + engine.handle(.space) + expect(self.currentComposeText).to(equal("▼")) + expect(self.currentMarkedText).to(equal("山")) + engine.handle(.enter) + expect(self.insertedText).to(equal("山")) + } + it("convert kanji with okuri") { @MainActor in + engine.handle(.char(kana: "あ", shift: true)) + engine.handle(.char(kana: "る", shift: true)) + engine.handle(.enter) + expect(self.insertedText).to(equal("荒る")) + } + context("with dakuten") { + it("convert kanji on compose mode") { @MainActor in + engine.handle(.char(kana: "か", shift: true)) + engine.handle(.char(kana: "ん", shift: false)) + engine.handle(.char(kana: "し", shift: true)) + expect(self.currentComposeText).to(equal("▼")) + expect(self.currentMarkedText).to(equal("関し")) + engine.handle(.toggleDakuten(beforeText: "")) + expect(self.currentComposeText).to(equal("▼")) + expect(self.currentMarkedText).to(equal("感じ")) + engine.handle(.enter) + expect(self.insertedText).to(equal("感じ")) + } + it("convert kanji on register mode") { @MainActor in + engine.handle(.char(kana: "わ", shift: true)) + engine.handle(.char(kana: "れ", shift: false)) + engine.handle(.char(kana: "ら", shift: false)) + engine.handle(.char(kana: "か", shift: true)) + engine.handle(.toggleDakuten(beforeText: "")) + engine.handle(.enter) + expect(self.insertedText).to(equal("我等が")) + } + it("convert kanji without dakuten") { @MainActor in + engine.handle(.char(kana: "わ", shift: true)) + engine.handle(.char(kana: "り", shift: false)) + engine.handle(.char(kana: "き", shift: true)) + engine.handle(.toggleDakuten(beforeText: "")) + engine.handle(.toggleDakuten(beforeText: "")) + engine.handle(.enter) + expect(self.insertedText).to(equal("割き")) + } + } + + it("toggle dakuten") { @MainActor in + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.toggleDakuten(beforeText: "かか")) + expect(self.insertedText).to(equal("かかが")) + } } - it("convert kanji") { - engine.handle(.char(kana: "や", shift: true)) - engine.handle(.char(kana: "ま", shift: false)) - expect(self.currentComposeText).to(equal("▽")) - expect(self.currentMarkedText).to(equal("やま")) - engine.handle(.space) - expect(self.currentComposeText).to(equal("▼")) - expect(self.currentMarkedText).to(equal("山")) - engine.handle(.enter) - expect(self.insertedText).to(equal("山")) + describe("number input") { + it("insert text") { @MainActor in + engine.handle(.char(kana: "1", shift: false)) + engine.handle(.char(kana: "2", shift: false)) + engine.handle(.char(kana: "3", shift: false)) + expect(self.insertedText).to(equal("123")) + } + it("convert kanji") { @MainActor in + engine.handle(.char(kana: "1", shift: true)) + engine.handle(.char(kana: "2", shift: false)) + engine.handle(.enter) + expect(self.insertedText).to(equal("12")) + } + it("ignore okuri for number") { @MainActor in + engine.handle(.char(kana: "1", shift: true)) + engine.handle(.char(kana: "2", shift: true)) + engine.handle(.enter) + expect(self.insertedText).to(equal("2")) + } } - it("convert kanji with okuri") { - engine.handle(.char(kana: "あ", shift: true)) - engine.handle(.char(kana: "る", shift: true)) - engine.handle(.enter) - expect(self.insertedText).to(equal("荒る")) + describe("alphabet input") { + it("insert text") { @MainActor in + engine.handle(.char(kana: "a", shift: false)) + engine.handle(.char(kana: "b", shift: false)) + engine.handle(.char(kana: "c", shift: false)) + expect(self.insertedText).to(equal("abc")) + } + it("convert kanji") { @MainActor in + engine.handle(.char(kana: "a", shift: true)) + engine.handle(.char(kana: "b", shift: false)) + engine.handle(.enter) + expect(self.insertedText).to(equal("ab")) + } + it("ignore okuri for alphabet") { @MainActor in + engine.handle(.char(kana: "a", shift: true)) + engine.handle(.char(kana: "b", shift: true)) + engine.handle(.enter) + expect(self.insertedText).to(equal("b")) + } + it("toggle upper/lower") { @MainActor in + engine.handle(.char(kana: "a", shift: false)) + engine.handle(.char(kana: "b", shift: false)) + engine.handle(.toggleUpperLower(beforeText: "ab")) + expect(self.insertedText).to(equal("abB")) + } } - context("with dakuten") { - it("convert kanji on compose mode") { - engine.handle(.char(kana: "か", shift: true)) - engine.handle(.char(kana: "ん", shift: false)) - engine.handle(.char(kana: "し", shift: true)) - expect(self.currentComposeText).to(equal("▼")) - expect(self.currentMarkedText).to(equal("関し")) + describe("「っ」送り仮名変換") { + it("can convert 「はいった」") { @MainActor in + engine.handle(.char(kana: "は", shift: true)) + engine.handle(.char(kana: "い", shift: false)) + engine.handle(.char(kana: "つ", shift: false)) engine.handle(.toggleDakuten(beforeText: "")) - expect(self.currentComposeText).to(equal("▼")) - expect(self.currentMarkedText).to(equal("感じ")) + engine.handle(.char(kana: "た", shift: true)) engine.handle(.enter) - expect(self.insertedText).to(equal("感じ")) + expect(self.insertedText).to(equal("入った")) } - it("convert kanji on register mode") { - engine.handle(.char(kana: "わ", shift: true)) - engine.handle(.char(kana: "れ", shift: false)) - engine.handle(.char(kana: "ら", shift: false)) - engine.handle(.char(kana: "か", shift: true)) + it("can convert 「はいっ」") { @MainActor in + engine.handle(.char(kana: "は", shift: true)) + engine.handle(.char(kana: "い", shift: false)) + engine.handle(.char(kana: "つ", shift: true)) engine.handle(.toggleDakuten(beforeText: "")) engine.handle(.enter) - expect(self.insertedText).to(equal("我等が")) + expect(self.insertedText).to(equal("入っ")) } - it("convert kanji without dakuten") { - engine.handle(.char(kana: "わ", shift: true)) - engine.handle(.char(kana: "り", shift: false)) - engine.handle(.char(kana: "き", shift: true)) + it("can convert 「ひっぱる」"){ @MainActor in + engine.handle(.char(kana: "ひ", shift: true)) + engine.handle(.char(kana: "つ", shift: false)) + engine.handle(.toggleDakuten(beforeText: "")) + engine.handle(.char(kana: "は", shift: true)) engine.handle(.toggleDakuten(beforeText: "")) engine.handle(.toggleDakuten(beforeText: "")) engine.handle(.enter) - expect(self.insertedText).to(equal("割き")) + expect(self.insertedText).to(equal("引っぱ")) + } + + it("can convert 「ばっする」"){ @MainActor in + engine.handle(.char(kana: "は", shift: true)) + engine.handle(.toggleDakuten(beforeText: "")) + engine.handle(.char(kana: "つ", shift: false)) + engine.handle(.toggleDakuten(beforeText: "")) + engine.handle(.char(kana: "す", shift: true)) + engine.handle(.enter) + expect(self.insertedText).to(equal("罰す")) } } - - it("toggle dakuten") { - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.toggleDakuten(beforeText: "かか")) - expect(self.insertedText).to(equal("かかが")) - } - } - describe("number input") { - it("insert text") { - engine.handle(.char(kana: "1", shift: false)) - engine.handle(.char(kana: "2", shift: false)) - engine.handle(.char(kana: "3", shift: false)) - expect(self.insertedText).to(equal("123")) - } - it("convert kanji") { - engine.handle(.char(kana: "1", shift: true)) - engine.handle(.char(kana: "2", shift: false)) - engine.handle(.enter) - expect(self.insertedText).to(equal("12")) - } - it("ignore okuri for number") { - engine.handle(.char(kana: "1", shift: true)) - engine.handle(.char(kana: "2", shift: true)) - engine.handle(.enter) - expect(self.insertedText).to(equal("2")) - } - } - describe("alphabet input") { - it("insert text") { - engine.handle(.char(kana: "a", shift: false)) - engine.handle(.char(kana: "b", shift: false)) - engine.handle(.char(kana: "c", shift: false)) - expect(self.insertedText).to(equal("abc")) - } - it("convert kanji") { - engine.handle(.char(kana: "a", shift: true)) - engine.handle(.char(kana: "b", shift: false)) - engine.handle(.enter) - expect(self.insertedText).to(equal("ab")) - } - it("ignore okuri for alphabet") { - engine.handle(.char(kana: "a", shift: true)) - engine.handle(.char(kana: "b", shift: true)) - engine.handle(.enter) - expect(self.insertedText).to(equal("b")) - } - it("toggle upper/lower") { - engine.handle(.char(kana: "a", shift: false)) - engine.handle(.char(kana: "b", shift: false)) - engine.handle(.toggleUpperLower(beforeText: "ab")) - expect(self.insertedText).to(equal("abB")) - } - } - describe("「っ」送り仮名変換") { - it("can convert 「はいった」") { - engine.handle(.char(kana: "は", shift: true)) - engine.handle(.char(kana: "い", shift: false)) - engine.handle(.char(kana: "つ", shift: false)) - engine.handle(.toggleDakuten(beforeText: "")) - engine.handle(.char(kana: "た", shift: true)) - engine.handle(.enter) - expect(self.insertedText).to(equal("入った")) - } - it("can convert 「はいっ」") { - engine.handle(.char(kana: "は", shift: true)) - engine.handle(.char(kana: "い", shift: false)) - engine.handle(.char(kana: "つ", shift: true)) - engine.handle(.toggleDakuten(beforeText: "")) - engine.handle(.enter) - expect(self.insertedText).to(equal("入っ")) - } - it("can convert 「ひっぱる」"){ - engine.handle(.char(kana: "ひ", shift: true)) - engine.handle(.char(kana: "つ", shift: false)) - engine.handle(.toggleDakuten(beforeText: "")) - engine.handle(.char(kana: "は", shift: true)) - engine.handle(.toggleDakuten(beforeText: "")) - engine.handle(.toggleDakuten(beforeText: "")) - engine.handle(.enter) - expect(self.insertedText).to(equal("引っぱ")) - } - - it("can convert 「ばっする」"){ - engine.handle(.char(kana: "は", shift: true)) - engine.handle(.toggleDakuten(beforeText: "")) - engine.handle(.char(kana: "つ", shift: false)) - engine.handle(.toggleDakuten(beforeText: "")) - engine.handle(.char(kana: "す", shift: true)) - engine.handle(.enter) - expect(self.insertedText).to(equal("罰す")) - } - } - describe("dictionary") { - it("can register dakuten kana") { - engine.handle(.char(kana: "か", shift: true)) - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.space) - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.toggleDakuten(beforeText: "")) - engine.handle(.enter) - expect(self.insertedText).to(equal("が")) - - self.insertedText = "" - - engine.handle(.char(kana: "か", shift: true)) - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.space) - engine.handle(.enter) - expect(self.insertedText).to(equal("が")) - } - it("can register word with okuri") { - // かかk を辞書に登録する - engine.handle(.char(kana: "か", shift: true)) - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.char(kana: "か", shift: true)) - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.enter) - expect(self.insertedText).to(equal("かか")) - self.insertedText = "" - - engine.handle(.char(kana: "か", shift: true)) - engine.handle(.char(kana: "か", shift: false)) - engine.handle(.char(kana: "か", shift: true)) - engine.handle(.enter) - expect(self.insertedText).to(equal("かか")) + describe("dictionary") { + it("can register dakuten kana") { @MainActor in + engine.handle(.char(kana: "か", shift: true)) + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.space) + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.toggleDakuten(beforeText: "")) + engine.handle(.enter) + expect(self.insertedText).to(equal("が")) + + self.insertedText = "" + + engine.handle(.char(kana: "か", shift: true)) + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.space) + engine.handle(.enter) + expect(self.insertedText).to(equal("が")) + } + it("can register word with okuri") { @MainActor in + // かかk を辞書に登録する + engine.handle(.char(kana: "か", shift: true)) + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.char(kana: "か", shift: true)) + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.enter) + expect(self.insertedText).to(equal("かか")) + self.insertedText = "" + + engine.handle(.char(kana: "か", shift: true)) + engine.handle(.char(kana: "か", shift: false)) + engine.handle(.char(kana: "か", shift: true)) + engine.handle(.enter) + expect(self.insertedText).to(equal("かか")) + } } } } diff --git a/FlickSKKTests/TextEngineSpec.swift b/FlickSKKTests/TextEngineSpec.swift index d47b961..7f5a96c 100644 --- a/FlickSKKTests/TextEngineSpec.swift +++ b/FlickSKKTests/TextEngineSpec.swift @@ -1,7 +1,8 @@ import Quick import Nimble -class TextEngineSpec : QuickSpec { +@MainActor +final class TextEngineSpec : QuickSpec, Sendable { lazy var dictionary : SKKDictionary = { DictionarySettings.bundle = Bundle(for: self.classForCoder) let dict = SKKDictionary() @@ -10,27 +11,29 @@ class TextEngineSpec : QuickSpec { }() override func spec() { - var target : TextEngine! - var delegate : MockDelegate! + MainActor.assumeIsolated { + var target : TextEngine! + var delegate : MockDelegate! - beforeEach { - delegate = MockDelegate() - let dictionaryEngine = DictionaryEngine(dictionary: self.dictionary) - target = TextEngine(delegate: delegate, dictionary: dictionaryEngine) - } - - describe("#insertPartial") { - beforeEach { - _ = target.insertPartial("ハナヤマタ", kana: "はなやまた", status: TextEngine.Status.topLevel) + beforeEach { @MainActor in + delegate = MockDelegate() + let dictionaryEngine = DictionaryEngine(dictionary: self.dictionary) + target = TextEngine(delegate: delegate, dictionary: dictionaryEngine) } - it("挿入される") { - expect(delegate.insertedText).to(equal("ハナヤマタ")) - } + describe("#insertPartial") { + beforeEach { @MainActor in + _ = target.insertPartial("ハナヤマタ", kana: "はなやまた", status: TextEngine.Status.topLevel) + } + + it("挿入される") { @MainActor in + expect(delegate.insertedText).to(equal("ハナヤマタ")) + } - it("補完できる") { - let xs = self.dictionary.findDynamic("はなや").filter { w in w.kanji == "ハナヤマタ" } - expect(xs.count).to(equal(1)) + it("補完できる") { @MainActor in + let xs = self.dictionary.findDynamic("はなや").filter { w in w.kanji == "ハナヤマタ" } + expect(xs.count).to(equal(1)) + } } } } diff --git a/Memo/AppDelegate.swift b/Memo/AppDelegate.swift index 0f8f641..5f00ec1 100644 --- a/Memo/AppDelegate.swift +++ b/Memo/AppDelegate.swift @@ -8,7 +8,7 @@ import UIKit -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? diff --git a/Podfile b/Podfile index 020045e..d715a02 100644 --- a/Podfile +++ b/Podfile @@ -31,6 +31,8 @@ post_install do |installer| if Gem::Version.new('12.0') > Gem::Version.new(c.build_settings['IPHONEOS_DEPLOYMENT_TARGET']) c.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' end + + c.build_settings['SWIFT_VERSION'] = '5.0' if %w[NorthLayout FootlessParser].include?(t.name) end end end diff --git a/Podfile.lock b/Podfile.lock index eb870c0..21182cc 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -34,6 +34,6 @@ SPEC CHECKSUMS: Quick: 6676ffb409bf04abba2ff23902af2407bfc6ac90 "※ikemen": dd846bad2317b0ea51e5c7dd8e565108fa40d528 -PODFILE CHECKSUM: 88486acd6e5763ee4c6b6072f3bca52217dae98a +PODFILE CHECKSUM: 290e9b5a53c615e3c594759fc7d8d4838ece6f2a COCOAPODS: 1.15.2