Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Core/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ let package = Package(
platforms: [.macOS(.v13)],
products: products,
dependencies: [
.package(url: "https://github.com/azooKey/AzooKeyKanaKanjiConverter", revision: "67ec8e68d17a534b5b9929b920995ef3811e89fe", traits: kanaKanjiConverterTraits),
.package(url: "https://github.com/azooKey/AzooKeyKanaKanjiConverter", revision: "ad714fea8cb2fe113aea86ba5c42563cdaf77cfb", traits: kanaKanjiConverterTraits),
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.0")
],
Expand Down
20 changes: 1 addition & 19 deletions Core/Sources/ConverterServer/ConverterServer+KeyEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ extension ConverterServer {
manager.insertAtCursorPosition("つづき", inputStyle: inputStyle)
effects.append(.requestReplaceSuggestion)
case .acceptPredictionCandidate:
acceptPredictionCandidate(manager: manager, leftSideContext: leftSideContext)
manager.acceptPredictionCandidate()
case .requestReplaceSuggestion:
session.clearReplaceSuggestions()
effects.append(.requestReplaceSuggestion)
Expand Down Expand Up @@ -357,22 +357,4 @@ extension ConverterServer {
session.clearReplaceSuggestions()
return true
}

@MainActor
func acceptPredictionCandidate(manager: SegmentsManager, leftSideContext _: String?) {
let prediction = SegmentsManager.preferredPredictionCandidates(
typoCorrectionCandidates: manager.requestTypoCorrectionPredictionCandidates(),
predictionCandidates: manager.requestPredictionCandidates()
).first
guard let prediction else {
return
}
if prediction.deleteCount > 0 {
manager.deleteBackwardFromCursorPosition(count: prediction.deleteCount)
}
guard !prediction.appendText.isEmpty else {
return
}
manager.insertAtCursorPosition(prediction.appendText, inputStyle: .direct)
}
}
71 changes: 46 additions & 25 deletions Core/Sources/Core/InputUtils/SegmentsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ public final class SegmentsManager {
var targetReading: String
}

private func candidateReading(_ candidate: Candidate) -> String {
candidate.data.map(\.ruby).joined()
}

public func makeCandidatePresentations(_ candidates: [Candidate]) -> [CandidatePresentation] {
let additionalPresentations = self.additionalCandidatePresentationsForSelectionIndex
return candidates.indices.map { index in
Expand Down Expand Up @@ -880,40 +876,35 @@ public final class SegmentsManager {
}

public func requestPredictionCandidates() -> [PredictionCandidate] {
guard Config.DebugPredictiveTyping().value else {
guard let candidate = self.firstPredictionCandidate(),
let prediction = Self.makePredictionCandidate(currentTarget: self.composingText.convertTarget, candidate: candidate) else {
return []
}
return [prediction]
}

private func firstPredictionCandidate() -> Candidate? {
guard Config.DebugPredictiveTyping().value else {
return nil
}

let target = self.composingText.convertTarget
guard !target.isEmpty else {
return []
return nil
}

guard let rawCandidates else {
return []
return nil
}

for candidate in rawCandidates.predictionResults {
let reading = candidateReading(candidate)
guard !reading.isEmpty else {
continue
}
if let predictionCandidate = Self.makePredictionCandidate(
currentTarget: target,
candidateReading: reading,
displayText: candidate.text
) {
return [predictionCandidate]
}
return rawCandidates.predictionResults.first {
Self.makePredictionCandidate(currentTarget: target, candidate: $0) != nil
}

return []
}

static func makePredictionCandidate(
currentTarget: String,
candidateReading: String,
displayText: String
candidate: Candidate
) -> PredictionCandidate? {
var matchTarget = currentTarget
var deleteCount = 0
Expand All @@ -926,7 +917,7 @@ public final class SegmentsManager {
return nil
}

let readingHiragana = candidateReading.toHiragana()
let readingHiragana = candidate.data.map(\.ruby).joined().toHiragana()
let matchTargetHiragana = matchTarget.toHiragana()
guard readingHiragana.hasPrefix(matchTargetHiragana) else {
return nil
Expand All @@ -940,7 +931,37 @@ public final class SegmentsManager {
return nil
}

return .init(displayText: displayText, appendText: appendText, deleteCount: deleteCount)
return .init(displayText: candidate.text, appendText: appendText, deleteCount: deleteCount)
}

@MainActor
public func acceptPredictionCandidate() {
if let prediction = self.requestTypoCorrectionPredictionCandidates().first {
self.acceptTypoCorrectionPredictionCandidate(prediction)
} else if let candidate = self.firstPredictionCandidate() {
self.acceptPredictionCandidate(candidate)
}
}

@MainActor
func acceptPredictionCandidate(_ candidate: Candidate) {
guard self.kanaKanjiConverter.acceptPredictionCandidate(candidate, composingText: &self.composingText) else {
return
}
self.lastInputStyle = .direct
self.lastOperation = .insert
self.shouldShowCandidateWindow = !self.liveConversionEnabled
self.updateRawCandidate()
}

@MainActor
func acceptTypoCorrectionPredictionCandidate(_ prediction: PredictionCandidate) {
if prediction.deleteCount > 0 {
self.deleteBackwardFromCursorPosition(count: prediction.deleteCount)
}
if !prediction.appendText.isEmpty {
self.insertAtCursorPosition(prediction.appendText, inputStyle: .direct)
}
}

private func requestTypoCorrectionCandidates(composingText targetComposingText: ComposingText, inputStyle: InputStyle) -> [String] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
@testable import Core
import Foundation
import KanaKanjiConverterModuleWithDefaultDictionary
import Testing

private func makeCandidate(text: String, reading: String) -> Candidate {
Candidate(
text: text,
value: 0,
composingCount: .inputCount(reading.count),
lastMid: MIDData.一般.mid,
data: [.init(word: text, ruby: reading.toKatakana(), cid: CIDData.一般名詞.cid, mid: MIDData.一般.mid, value: 0)]
)
}

private func makePredictionSegmentsManager() -> SegmentsManager {
SegmentsManager(
kanaKanjiConverter: .withDefaultDictionary(),
applicationDirectoryURL: URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true),
containerURL: nil,
context: .init(useZenzai: false)
)
}

@Test func testMakePredictionCandidateDeletesTrailingASCIIUsedForMatching() async throws {
let source = makeCandidate(text: "おはようございます", reading: "おはようございます")
let candidate = SegmentsManager.makePredictionCandidate(
currentTarget: "おはようございm",
candidateReading: "おはようございます",
displayText: "おはようございます"
candidate: source
)

#expect(candidate?.displayText == "おはようございます")
Expand All @@ -16,11 +37,54 @@ import Testing
@Test func testMakePredictionCandidateKeepsDeleteCountZeroWithoutTrailingASCII() async throws {
let candidate = SegmentsManager.makePredictionCandidate(
currentTarget: "おはようござい",
candidateReading: "おはようございます",
displayText: "おはようございます"
candidate: makeCandidate(text: "おはようございます", reading: "おはようございます")
)

#expect(candidate?.displayText == "おはようございます")
#expect(candidate?.appendText == "ます")
#expect(candidate?.deleteCount == 0)
}

@MainActor
@Test func testAcceptPredictionCandidateCompletesReadingAndContinuesRomanInput() throws {
let manager = makePredictionSegmentsManager()
manager.insertAtCursorPosition("hida", inputStyle: .roman2kana)
manager.acceptPredictionCandidate(makeCandidate(text: "←", reading: "ひだり"))
#expect(manager.convertTarget == "ひだり")

manager.insertAtCursorPosition("nimagaru", inputStyle: .roman2kana)
#expect(manager.convertTarget == "ひだりにまがる")
}

@MainActor
@Test func testAcceptPredictionCandidateReplacesPendingRomanSuffix() throws {
let manager = makePredictionSegmentsManager()
manager.insertAtCursorPosition("arigat", inputStyle: .roman2kana)
manager.acceptPredictionCandidate(makeCandidate(text: "有難う", reading: "ありがとう"))

#expect(manager.convertTarget == "ありがとう")
}

@MainActor
@Test func testAcceptPredictionCandidateRejectsStaleCandidateWithoutEditingInput() throws {
let manager = makePredictionSegmentsManager()
manager.insertAtCursorPosition("こんにちは", inputStyle: .direct)
manager.acceptPredictionCandidate(makeCandidate(text: "今晩は", reading: "こんばんは"))

#expect(manager.convertTarget == "こんにちは")
}

@MainActor
@Test func testAcceptTypoCorrectionPredictionCandidateReplacesReading() throws {
let manager = makePredictionSegmentsManager()
manager.insertAtCursorPosition("こんびんは", inputStyle: .direct)
let prediction = try #require(SegmentsManager.makeBackspaceTypoCorrectionPredictionCandidate(
currentConvertTarget: manager.convertTarget,
targetReading: "こんばんは",
displayText: "今晩は"
))

manager.acceptTypoCorrectionPredictionCandidate(prediction)

#expect(manager.convertTarget == "こんばんは")
}
Loading