From 46daf7c74923ac98396aba38b81a7691fc5af1e1 Mon Sep 17 00:00:00 2001 From: Yukihiro Shinoda Date: Sat, 5 Sep 2026 15:02:50 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(input):=20=E8=A8=98=E5=8F=B7=E3=81=AE?= =?UTF-8?q?=E7=B5=84=E3=81=BF=E5=90=88=E3=82=8F=E3=81=9B=E3=81=8B=E3=82=89?= =?UTF-8?q?=E7=9F=A2=E5=8D=B0=E5=80=99=E8=A3=9C=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InputUtils/ArrowSymbolCandidates.swift | 19 ++++++ .../Core/InputUtils/SegmentsManager.swift | 17 ++++- .../ArrowSymbolCandidatesTests.swift | 63 +++++++++++++++++++ README.md | 4 ++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 Core/Sources/Core/InputUtils/ArrowSymbolCandidates.swift create mode 100644 Core/Tests/CoreTests/InputUtilsTests/ArrowSymbolCandidatesTests.swift 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..8d0a4db0 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,21 @@ 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( + text: text, value: -18, + composingCount: .inputCount(arrowInput.input.count), + lastMid: MIDData.一般.mid, + data: [.init(word: text, ruby: arrowInput.convertTarget, cid: CIDData.記号.cid, mid: MIDData.一般.mid, value: -18)], + isLearningTarget: false + ) + } + 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)の支援を受けて開発を行いました。 + +### 記号の組み合わせから矢印へ変換 + +`ー>` / `->` → `→`・`⇒`・`➜`、`<ー` → `←`・`⇐`・`⇦`、`<ー>` → `↔`・`⇔`を候補に追加します。全角・半角の記号に対応し、元の入力も選べます。文節編集中は選択範囲だけを確定します。 From 338d7280461603825565e96c5bf1da95adc38ee9 Mon Sep 17 00:00:00 2001 From: Yukihiro Shinoda <52367401+sinoda1114@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:08:23 +0900 Subject: [PATCH 2/2] Fix lastMid value in Candidate initialization Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Core/Sources/Core/InputUtils/SegmentsManager.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Core/Sources/Core/InputUtils/SegmentsManager.swift b/Core/Sources/Core/InputUtils/SegmentsManager.swift index 8d0a4db0..2d3e315f 100644 --- a/Core/Sources/Core/InputUtils/SegmentsManager.swift +++ b/Core/Sources/Core/InputUtils/SegmentsManager.swift @@ -580,11 +580,7 @@ public final class SegmentsManager { let existingTexts = Set(result.mainResults.map(\.text)) let arrowCandidates = arrowVariants.filter { !existingTexts.contains($0) }.map { text in Candidate( - text: text, value: -18, - composingCount: .inputCount(arrowInput.input.count), - lastMid: MIDData.一般.mid, - data: [.init(word: text, ruby: arrowInput.convertTarget, cid: CIDData.記号.cid, mid: MIDData.一般.mid, value: -18)], - isLearningTarget: false + lastMid: 0, ) } result.mainResults.insert(contentsOf: arrowCandidates, at: min(5, result.mainResults.count))