diff --git a/Core/Sources/Core/InputUtils/ArrowSymbolCandidates.swift b/Core/Sources/Core/InputUtils/ArrowSymbolCandidates.swift new file mode 100644 index 00000000..153d47ac --- /dev/null +++ b/Core/Sources/Core/InputUtils/ArrowSymbolCandidates.swift @@ -0,0 +1,19 @@ +enum ArrowSymbolCandidates { + /// 日本語入力で変わる長音符・全角記号を、矢印の入力パターンとして扱う。 + static func variants(for reading: String) -> [String] { + let normalized = String(reading.map { character -> Character in + switch character { + case "ー", "ー", "-", "−": "-" + case ">": ">" + case "<": "<" + default: character + } + }) + return switch normalized { + case "->", "-->": ["→", "⇒", "➜"] + case "<-", "<--": ["←", "⇐", "⇦"] + case "<->", "<-->": ["↔", "⇔"] + default: [] + } + } +} diff --git a/Core/Sources/Core/InputUtils/SegmentsManager.swift b/Core/Sources/Core/InputUtils/SegmentsManager.swift index 3188ca36..2d3e315f 100644 --- a/Core/Sources/Core/InputUtils/SegmentsManager.swift +++ b/Core/Sources/Core/InputUtils/SegmentsManager.swift @@ -564,7 +564,7 @@ public final class SegmentsManager { let leftSideContext = forcedLeftSideContext ?? self.getCleanLeftSideContext(maxCount: ContextLength.conversion) let rightSideContext = forcedRightSideContext ?? self.getCleanRightSideContext(maxCount: ContextLength.conversion) - let result = self.kanaKanjiConverter.requestCandidates( + var result = self.kanaKanjiConverter.requestCandidates( self.composingText, options: options( leftSideContext: leftSideContext, @@ -574,6 +574,17 @@ public final class SegmentsManager { requireEnglishPrediction: Config.DebugPredictiveTyping().value ? .manualMix : .disabled ) ) + let arrowInput = self.composingText.prefixToCursorPosition() + let arrowVariants = ArrowSymbolCandidates.variants(for: arrowInput.convertTarget) + if !arrowVariants.isEmpty { + let existingTexts = Set(result.mainResults.map(\.text)) + let arrowCandidates = arrowVariants.filter { !existingTexts.contains($0) }.map { text in + Candidate( + lastMid: 0, + ) + } + result.mainResults.insert(contentsOf: arrowCandidates, at: min(5, result.mainResults.count)) + } self.rawCandidates = result } diff --git a/Core/Tests/CoreTests/InputUtilsTests/ArrowSymbolCandidatesTests.swift b/Core/Tests/CoreTests/InputUtilsTests/ArrowSymbolCandidatesTests.swift new file mode 100644 index 00000000..e46df758 --- /dev/null +++ b/Core/Tests/CoreTests/InputUtilsTests/ArrowSymbolCandidatesTests.swift @@ -0,0 +1,63 @@ +@testable import Core +import Foundation +import KanaKanjiConverterModuleWithDefaultDictionary +import Testing + +@MainActor +struct ArrowSymbolCandidatesTests { + @Test func patterns() { + for input in ["ー>", "ー>", "->", "->", "−>", "ーー>"] { + #expect(ArrowSymbolCandidates.variants(for: input) == ["→", "⇒", "➜"]) + } + #expect(ArrowSymbolCandidates.variants(for: "<ー") == ["←", "⇐", "⇦"]) + #expect(ArrowSymbolCandidates.variants(for: "<ー>") == ["↔", "⇔"]) + for input in ["", "ー", ">", "a->b", "コーヒー>", "<=", "ー >"] { + #expect(ArrowSymbolCandidates.variants(for: input).isEmpty) + } + } + + @Test(arguments: ["→", "⇒", "➜"]) + func convertAndCommit(expected: String) throws { + try withManager { manager in + manager.insertAtCursorPosition("ー>", inputStyle: .direct) + for rich in [false, true] { + manager.update(requestRichCandidates: rich) + manager.requestSetCandidateWindowState(visible: true) + guard case .selecting(let choices, _) = manager.getCurrentCandidateWindow(inputState: .selecting) else { + Issue.record("Expected arrow candidates") + return + } + #expect(choices.filter { $0.text == expected }.count == 1) + #expect(choices.contains { $0.text == "ー>" }) + if rich { + let candidate = try #require(choices.first { $0.text == expected }) + manager.prefixCandidateCommited(candidate, leftSideContext: "") + #expect(manager.isEmpty) + } + } + } + } + + @Test func editedPrefixPreservesSuffix() throws { + try withManager { manager in + manager.insertAtCursorPosition("ー>あ", inputStyle: .direct) + manager.editSegment(count: -1) + guard case .selecting(let choices, _) = manager.getCurrentCandidateWindow(inputState: .selecting) else { + Issue.record("Expected edited candidates") + return + } + let candidate = try #require(choices.first { $0.text == "⇒" }) + manager.prefixCandidateCommited(candidate, leftSideContext: "") + #expect(manager.convertTarget == "あ") + } + } + + private func withManager(_ body: (SegmentsManager) throws -> Void) throws { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + let manager = SegmentsManager(kanaKanjiConverter: .withDefaultDictionary(), applicationDirectoryURL: directory, + containerURL: nil, context: .init(useZenzai: false)) + try body(manager) + } +} diff --git a/README.md b/README.md index e710c7a7..8dbe8193 100644 --- a/README.md +++ b/README.md @@ -151,3 +151,7 @@ Thanks to authors!! ## Acknowledgement 本プロジェクトは情報処理推進機構(IPA)による[2024年度未踏IT人材発掘・育成事業](https://www.ipa.go.jp/jinzai/mitou/it/2024/koubokekka.html)の支援を受けて開発を行いました。 + +### 記号の組み合わせから矢印へ変換 + +`ー>` / `->` → `→`・`⇒`・`➜`、`<ー` → `←`・`⇐`・`⇦`、`<ー>` → `↔`・`⇔`を候補に追加します。全角・半角の記号に対応し、元の入力も選べます。文節編集中は選択範囲だけを確定します。