diff --git a/Core/Sources/Core/InputUtils/SegmentsManager.swift b/Core/Sources/Core/InputUtils/SegmentsManager.swift index bc19356..a51a0a0 100644 --- a/Core/Sources/Core/InputUtils/SegmentsManager.swift +++ b/Core/Sources/Core/InputUtils/SegmentsManager.swift @@ -65,7 +65,7 @@ public final class SegmentsManager { private var backspaceAdjustedPredictionCandidate: PredictionCandidate? private var backspaceTypoCorrectionLock: BackspaceTypoCorrectionLock? - public struct PredictionCandidate: Sendable { + public struct PredictionCandidate: Sendable, Equatable { public var displayText: String public var appendText: String public var deleteCount: Int = 0 @@ -336,13 +336,13 @@ public final class SegmentsManager { @MainActor public func deleteBackwardFromCursorPosition(count: Int = 1) { - var beforeComposingText = self.composingText.prefixToCursorPosition() + var previousComposingText = self.composingText.prefixToCursorPosition() if !self.composingText.isAtEndIndex { // 右端に持っていく _ = self.composingText.moveCursorFromCursorPosition(count: self.composingText.convertTarget.count - self.composingText.convertTargetCursorPosition) // 一度segmentの編集状態もリセットにする self.didExperienceSegmentEdition = false - beforeComposingText = self.composingText.prefixToCursorPosition() + previousComposingText = self.composingText.prefixToCursorPosition() } self.composingText.deleteBackwardFromCursorPosition(count: count) self.lastOperation = .delete @@ -354,7 +354,7 @@ public final class SegmentsManager { self.backspaceTypoCorrectionLock = nil return } - let currentInput = self.composingText.convertTarget + let currentConvertTarget = self.composingText.convertTarget guard count == 1 else { self.backspaceAdjustedPredictionCandidate = nil self.backspaceTypoCorrectionLock = nil @@ -362,7 +362,7 @@ public final class SegmentsManager { } if let lock = self.backspaceTypoCorrectionLock { self.backspaceAdjustedPredictionCandidate = Self.makeBackspaceTypoCorrectionPredictionCandidate( - currentInput: currentInput, + currentConvertTarget: currentConvertTarget, targetReading: lock.targetReading, displayText: lock.displayText ) @@ -371,10 +371,10 @@ public final class SegmentsManager { } return } - self.backspaceTypoCorrectionLock = self.lmBasedBackspaceTypoCorrectionLock(previousComposingText: beforeComposingText) + self.backspaceTypoCorrectionLock = self.lmBasedBackspaceTypoCorrectionLock(previousComposingText: previousComposingText) if let lock = self.backspaceTypoCorrectionLock { self.backspaceAdjustedPredictionCandidate = Self.makeBackspaceTypoCorrectionPredictionCandidate( - currentInput: currentInput, + currentConvertTarget: currentConvertTarget, targetReading: lock.targetReading, displayText: lock.displayText ) @@ -809,6 +809,34 @@ public final class SegmentsManager { suggestSelectionIndex = nil } + public func requestTypoCorrectionPredictionCandidates() -> [PredictionCandidate] { + guard Config.DebugTypoCorrection().value else { + return [] + } + guard let backspaceAdjustedPredictionCandidate else { + return [] + } + return [backspaceAdjustedPredictionCandidate] + } + + public static func preferredPredictionCandidates( + typoCorrectionCandidates: [PredictionCandidate], + predictionCandidates: [PredictionCandidate] + ) -> [PredictionCandidate] { + if !typoCorrectionCandidates.isEmpty { + return typoCorrectionCandidates + } + return predictionCandidates + } + + public static func shouldPresentTypoCorrectionPredictionCandidate( + candidateDisplayText: String, + previousComposingDisplayText: String + ) -> Bool { + // 削除前の previousComposingText と同じ表示候補は、訂正候補としては提示しない。 + candidateDisplayText != previousComposingDisplayText + } + public func requestPredictionCandidates() -> [PredictionCandidate] { guard Config.DebugPredictiveTyping().value else { return [] @@ -819,10 +847,6 @@ public final class SegmentsManager { return [] } - if let backspaceAdjustedPredictionCandidate { - return [backspaceAdjustedPredictionCandidate] - } - guard let rawCandidates else { return [] } @@ -946,17 +970,27 @@ public final class SegmentsManager { reading: correctedReading, leftSideContext: self.getCleanLeftSideContext(maxCount: 30) ) ?? correctedReading + let previousComposingDisplayText = self.convertedText( + reading: previousComposingText.convertTarget, + leftSideContext: self.getCleanLeftSideContext(maxCount: 30) + ) ?? previousComposingText.convertTarget + guard Self.shouldPresentTypoCorrectionPredictionCandidate( + candidateDisplayText: correctedDisplayText, + previousComposingDisplayText: previousComposingDisplayText + ) else { + return nil + } return .init(displayText: correctedDisplayText, targetReading: correctedReading) } static func makeBackspaceTypoCorrectionPredictionCandidate( - currentInput: String, + currentConvertTarget: String, targetReading: String, displayText: String ) -> PredictionCandidate? { - let operation = Self.makeSuffixEditOperation(from: currentInput, to: targetReading) - ?? Self.makeSuffixEditOperation(from: currentInput.toHiragana(), to: targetReading) + let operation = Self.makeSuffixEditOperation(from: currentConvertTarget, to: targetReading) + ?? Self.makeSuffixEditOperation(from: currentConvertTarget.toHiragana(), to: targetReading) guard let operation else { return nil } diff --git a/Core/Tests/CoreTests/InputUtilsTests/SegmentsManagerPredictionBackspaceCorrectionTests.swift b/Core/Tests/CoreTests/InputUtilsTests/SegmentsManagerPredictionBackspaceCorrectionTests.swift index 6fea7c7..843f07a 100644 --- a/Core/Tests/CoreTests/InputUtilsTests/SegmentsManagerPredictionBackspaceCorrectionTests.swift +++ b/Core/Tests/CoreTests/InputUtilsTests/SegmentsManagerPredictionBackspaceCorrectionTests.swift @@ -3,7 +3,7 @@ import Testing @Test func testMakeBackspaceTypoCorrectionPredictionCandidateRecalculatesEditOperationForCurrentInput() async throws { let candidate = SegmentsManager.makeBackspaceTypoCorrectionPredictionCandidate( - currentInput: "くだし", + currentConvertTarget: "くだし", targetReading: "ください", displayText: "下さい" ) @@ -15,7 +15,7 @@ import Testing @Test func testMakeBackspaceTypoCorrectionPredictionCandidateKeepsDisplayTextAndUpdatesAppendTextOnFurtherDelete() async throws { let candidate = SegmentsManager.makeBackspaceTypoCorrectionPredictionCandidate( - currentInput: "くだ", + currentConvertTarget: "くだ", targetReading: "ください", displayText: "下さい" ) @@ -24,3 +24,56 @@ import Testing #expect(candidate?.appendText == "さい") #expect(candidate?.deleteCount == 0) } + +@Test func testPreferredPredictionCandidatesPreferTypoCorrectionCandidates() async throws { + let typoCorrection = SegmentsManager.PredictionCandidate( + displayText: "下さい", + appendText: "さい", + deleteCount: 1 + ) + let prediction = SegmentsManager.PredictionCandidate( + displayText: "くださいました", + appendText: "ました" + ) + + let candidates = SegmentsManager.preferredPredictionCandidates( + typoCorrectionCandidates: [typoCorrection], + predictionCandidates: [prediction] + ) + + #expect(candidates == [typoCorrection]) +} + +@Test func testPreferredPredictionCandidatesFallbackToPredictionCandidates() async throws { + let prediction = SegmentsManager.PredictionCandidate( + displayText: "くださいました", + appendText: "ました" + ) + + let candidates = SegmentsManager.preferredPredictionCandidates( + typoCorrectionCandidates: [], + predictionCandidates: [prediction] + ) + + #expect(candidates == [prediction]) +} + +@Test func testShouldPresentTypoCorrectionPredictionCandidateReturnsFalseForMatchingPreviousComposingDisplay() async throws { + // 削除前の previousComposingText と同じ表示候補は、訂正候補として出さない。 + let shouldPresent = SegmentsManager.shouldPresentTypoCorrectionPredictionCandidate( + candidateDisplayText: "下さい", + previousComposingDisplayText: "下さい" + ) + + #expect(shouldPresent == false) +} + +@Test func testShouldPresentTypoCorrectionPredictionCandidateReturnsTrueForDifferentPreviousComposingDisplay() async throws { + // 削除前の previousComposingText と異なる表示候補だけを、訂正候補として出す。 + let shouldPresent = SegmentsManager.shouldPresentTypoCorrectionPredictionCandidate( + candidateDisplayText: "下さい", + previousComposingDisplayText: "ください" + ) + + #expect(shouldPresent == true) +} diff --git a/azooKeyMac/InputController/azooKeyMacInputController.swift b/azooKeyMac/InputController/azooKeyMacInputController.swift index 7edb3e1..c8cd83f 100644 --- a/azooKeyMac/InputController/azooKeyMacInputController.swift +++ b/azooKeyMac/InputController/azooKeyMacInputController.swift @@ -600,7 +600,7 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s return } - let predictions = self.segmentsManager.requestPredictionCandidates() + let predictions = self.requestPreferredPredictionCandidates() if predictions.isEmpty { let now = Date().timeIntervalSince1970 let elapsed = now - self.lastPredictionUpdateTime @@ -709,7 +709,7 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s @MainActor private func acceptPredictionCandidate() { - let predictions = self.segmentsManager.requestPredictionCandidates() + let predictions = self.requestPreferredPredictionCandidates() guard let prediction = predictions.first else { return } @@ -726,6 +726,13 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s self.segmentsManager.insertAtCursorPosition(appendText, inputStyle: .direct) } + private func requestPreferredPredictionCandidates() -> [SegmentsManager.PredictionCandidate] { + SegmentsManager.preferredPredictionCandidates( + typoCorrectionCandidates: self.segmentsManager.requestTypoCorrectionPredictionCandidates(), + predictionCandidates: self.segmentsManager.requestPredictionCandidates() + ) + } + var retryCount = 0 let maxRetries = 3