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
16 changes: 16 additions & 0 deletions Core/Sources/Core/Extensions/InputPiece+String.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import KanaKanjiConverterModule

public extension Sequence where Element == InputPiece {
func inputString(preferIntention: Bool = true) -> String {
String(self.compactMap {
switch $0 {
case .character(let c):
c
case .key(intention: let intention, input: let input, modifiers: _):
preferIntention ? (intention ?? input) : input
case .compositionSeparator:
nil
}
})
}
}
21 changes: 21 additions & 0 deletions Core/Sources/Core/InputUtils/CandidatePresentationContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import KanaKanjiConverterModuleWithDefaultDictionary

public struct CandidatePresentationContext: Sendable {
public var annotationText: String?
public var extraValues: [String: String]

public init(annotationText: String? = nil, extraValues: [String: String] = [:]) {
self.annotationText = annotationText
self.extraValues = extraValues
}
}

public struct CandidatePresentation: Sendable {
public var candidate: Candidate
public var displayContext: CandidatePresentationContext

public init(candidate: Candidate, displayContext: CandidatePresentationContext = .init()) {
self.candidate = candidate
self.displayContext = displayContext
}
}
18 changes: 4 additions & 14 deletions Core/Sources/Core/InputUtils/InputState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public enum InputState: Sendable, Hashable {
return (.appendPieceToMarkedText(string), .transition(.composing))
case .english:
// 連結する
return (.insertWithoutMarkedText(inputPiecesToString(string)), .fallthrough)
return (.insertWithoutMarkedText(string.inputString(preferIntention: true)), .fallthrough)
}
case .deadKey(let diacritic):
if inputLanguage == .english {
Expand Down Expand Up @@ -94,7 +94,7 @@ public enum InputState: Sendable, Hashable {
case .attachDiacritic(let diacritic):
switch userAction {
case .input(let string):
let string = self.inputPiecesToString(string)
let string = string.inputString(preferIntention: true)
if let result = DiacriticAttacher.attach(deadKeyChar: diacritic, with: string, shift: event.modifierFlags.contains(.shift)) {
return (.insertWithoutMarkedText(result), .transition(.none))
} else {
Expand Down Expand Up @@ -252,7 +252,7 @@ public enum InputState: Sendable, Hashable {
case .selecting:
switch userAction {
case .input(let string):
let s = self.inputPiecesToString(string)
let s = string.inputString(preferIntention: true)
if s == "d" && enableDebugWindow {
return (.enableDebugWindow, .fallthrough)
} else if s == "D" && enableDebugWindow {
Expand Down Expand Up @@ -369,7 +369,7 @@ public enum InputState: Sendable, Hashable {
case .unicodeInput(let codePoint):
switch userAction {
case .input(let pieces):
let input = inputPiecesToString(pieces).lowercased()
let input = pieces.inputString(preferIntention: true).lowercased()
// 16進数のみ受け付ける
let hexChars = CharacterSet(charactersIn: "0123456789abcdef")
let filteredInput = input.unicodeScalars.filter { hexChars.contains($0) }.map { String($0) }.joined()
Expand Down Expand Up @@ -401,14 +401,4 @@ public enum InputState: Sendable, Hashable {
}
}
}

private func inputPiecesToString(_ inputPieces: [InputPiece]) -> String {
String(inputPieces.compactMap {
switch $0 {
case .character(let c): c
case .key(intention: let cint, input: let cinp, modifiers: _): cint ?? cinp
case .compositionSeparator: nil
}
})
}
}
193 changes: 167 additions & 26 deletions Core/Sources/Core/InputUtils/SegmentsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,30 @@ public final class SegmentsManager {
public init(
kanaKanjiConverter: KanaKanjiConverter,
applicationDirectoryURL: URL,
containerURL: URL?
containerURL: URL?,
context: Context = Context()
) {
self.kanaKanjiConverter = kanaKanjiConverter
self.applicationDirectoryURL = applicationDirectoryURL
self.containerURL = containerURL
self.context = context
}

/// テストなどの設定注入のための型。外部には設定を露出させない。
public struct Context {
public init() {}
init(useZenzai: Bool) {
self.useZenzai = useZenzai
}

var useZenzai: Bool = true
}

public weak var delegate: (any SegmentManagerDelegate)?
private var kanaKanjiConverter: KanaKanjiConverter
private let applicationDirectoryURL: URL
private let containerURL: URL?
private let context: Context

private var composingText: ComposingText = ComposingText()

Expand All @@ -38,6 +51,11 @@ public final class SegmentsManager {
private var lastOperation: Operation = .other
private var shouldShowCandidateWindow = false

private var isShowingAdditionalCandidates = false
private var additionalCandidates: [CandidatePresentation] = []
private var showingAdditionalCandidateCount = 0
private var isFixingAdditionalCandidateTop = false

private var shouldShowDebugCandidateWindow: Bool = false
private var debugCandidates: [Candidate] = []

Expand All @@ -53,6 +71,16 @@ public final class SegmentsManager {
candidate.data.map(\.ruby).joined()
}

public func makeCandidatePresentations(_ candidates: [Candidate]) -> [CandidatePresentation] {
let additionalPresentations = self.additionalCandidatePresentationsForSelectionIndex
return candidates.indices.map { index in
if index < additionalPresentations.count {
return .init(candidate: candidates[index], displayContext: additionalPresentations[index].displayContext)
}
return .init(candidate: candidates[index])
}
}

private lazy var zenzaiPersonalizationMode: ConvertRequestOptions.ZenzaiMode.PersonalizationMode? = self.getZenzaiPersonalizationMode()

private func getZenzaiPersonalizationMode() -> ConvertRequestOptions.ZenzaiMode.PersonalizationMode? {
Expand Down Expand Up @@ -106,7 +134,10 @@ public final class SegmentsManager {
}

private func zenzaiMode(leftSideContext: String?, requestRichCandidates: Bool) -> ConvertRequestOptions.ZenzaiMode {
.on(
if !self.context.useZenzai {
return .off
}
return .on(
weight: Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/ggml-model-Q5_K_M.gguf", isDirectory: false),
inferenceLimit: Config.ZenzaiInferenceLimit().value,
requestRichCandidates: requestRichCandidates,
Expand Down Expand Up @@ -172,6 +203,7 @@ public final class SegmentsManager {
self.composingText.stopComposition()
self.shouldShowCandidateWindow = false
self.selectionIndex = nil
self.resetAdditionalCandidates()
}

@MainActor
Expand All @@ -184,6 +216,7 @@ public final class SegmentsManager {
self.lastOperation = .other
self.shouldShowCandidateWindow = false
self.selectionIndex = nil
self.resetAdditionalCandidates()
}

@MainActor
Expand All @@ -195,6 +228,7 @@ public final class SegmentsManager {
self.kanaKanjiConverter.commitUpdateLearningData()
self.shouldShowCandidateWindow = false
self.selectionIndex = nil
self.resetAdditionalCandidates()
}

/// 変換キーを押したタイミングで入力の区切りを示す
Expand Down Expand Up @@ -284,24 +318,52 @@ public final class SegmentsManager {
}

private var candidates: [Candidate]? {
if let rawCandidates {
if !self.didExperienceSegmentEdition {
if rawCandidates.firstClauseResults.contains(where: { self.composingText.isWholeComposingText(composingCount: $0.composingCount) }) {
// firstClauseCandidateがmainResultsと同じサイズの場合は、何もしない方が良い
return rawCandidates.mainResults
} else {
// 変換範囲がエディットされていない場合
let seenAsFirstClauseResults = rawCandidates.firstClauseResults.mapSet(transform: \.text)
return rawCandidates.firstClauseResults + rawCandidates.mainResults.filter {
!seenAsFirstClauseResults.contains($0.text)
}
}
} else {
guard let rawCandidates = self.rawCandidatesList else {
return self.isShowingAdditionalCandidates
? self.additionalCandidatesForSelectionIndex
: nil
}
return self.isShowingAdditionalCandidates
? self.additionalCandidatesForSelectionIndex + rawCandidates
: rawCandidates
}

private var rawCandidatesList: [Candidate]? {
guard let rawCandidates else {
return nil
}
if !self.didExperienceSegmentEdition {
if rawCandidates.firstClauseResults.contains(where: { self.composingText.isWholeComposingText(composingCount: $0.composingCount) }) {
// firstClauseCandidateがmainResultsと同じサイズの場合は、何もしない方が良い
return rawCandidates.mainResults
} else {
// 変換範囲がエディットされていない場合
let seenAsFirstClauseResults = rawCandidates.firstClauseResults.mapSet(transform: \.text)
return rawCandidates.firstClauseResults + rawCandidates.mainResults.filter {
!seenAsFirstClauseResults.contains($0.text)
}
}
} else {
return nil
return rawCandidates.mainResults
}
}

private var candidateOffsetByAdditionalCandidates: Int {
self.isShowingAdditionalCandidates ? self.showingAdditionalCandidateCount : 0
}

private var additionalCandidatesForSelectionIndex: [Candidate] {
self.additionalCandidatePresentationsForSelectionIndex.map(\.candidate)
}

private var additionalCandidatePresentationsForSelectionIndex: [CandidatePresentation] {
guard self.isShowingAdditionalCandidates else {
return []
}
guard self.candidateOffsetByAdditionalCandidates > 0 else {
return []
}
return Array(self.additionalCandidates.suffix(self.candidateOffsetByAdditionalCandidates))
}

public var convertTarget: String {
Expand Down Expand Up @@ -336,6 +398,7 @@ public final class SegmentsManager {
/// - Note:
/// This function is executed on the `@MainActor` to ensure UI consistency.
@MainActor private func updateRawCandidate(requestRichCandidates: Bool = false, forcedLeftSideContext: String? = nil) {
self.resetAdditionalCandidates()
// 不要
if composingText.isEmpty {
self.rawCandidates = nil
Expand Down Expand Up @@ -430,15 +493,46 @@ public final class SegmentsManager {
self.shouldShowDebugCandidateWindow = enabled
}

@MainActor
public func requestSelectingNextCandidate() {
self.isFixingAdditionalCandidateTop = false
self.selectionIndex = (self.selectionIndex ?? -1) + 1
}

@MainActor
public func requestSelectingPrevCandidate() {
self.selectionIndex = max(0, (self.selectionIndex ?? 1) - 1)
let selectionIndex = self.selectionIndex ?? 0

if self.isFixingAdditionalCandidateTop && self.isShowingAdditionalCandidates {
if self.candidateOffsetByAdditionalCandidates < self.additionalCandidates.count {
self.showingAdditionalCandidateCount += 1
}
self.selectionIndex = 0
return
}

if selectionIndex == 0, !self.isShowingAdditionalCandidates {
self.showAdditionalCandidatesIfNeeded()
let additionalCount = self.candidateOffsetByAdditionalCandidates
if additionalCount > 0 {
self.isFixingAdditionalCandidateTop = true
self.selectionIndex = 0
return
}
}
if selectionIndex == 0, self.isShowingAdditionalCandidates, self.candidateOffsetByAdditionalCandidates < self.additionalCandidates.count {
self.isFixingAdditionalCandidateTop = true
self.showingAdditionalCandidateCount += 1
self.selectionIndex = 0
return
}
self.selectionIndex = max(0, selectionIndex - 1)
}

public func requestSelectingRow(_ index: Int) {
if self.isFixingAdditionalCandidateTop, index != 0 {
self.isFixingAdditionalCandidateTop = false
}
self.selectionIndex = max(0, index)
}

Expand All @@ -452,6 +546,8 @@ public final class SegmentsManager {

public func requestResettingSelection() {
self.selectionIndex = nil
self.isFixingAdditionalCandidateTop = false
self.resetAdditionalCandidates()
}

public var selectedCandidate: Candidate? {
Expand Down Expand Up @@ -539,19 +635,21 @@ public final class SegmentsManager {
}

@MainActor
public func getModifiedRomanCandidate(_ transform: (String) -> String) -> Candidate {
let inputString = String(self.composingText.input.compactMap {
switch $0.piece {
case .compositionSeparator: nil
case .character(let c): c
case .key(intention: _, input: let input, modifiers: _): input
}
})
public func getModifiedRomanCandidate(inputState: InputState = .composing, _ transform: (String) -> String) -> Candidate {
let targetComposingText: ComposingText
switch inputState {
case .selecting:
targetComposingText = self.composingText.prefixToCursorPosition()
case .composing, .previewing, .none, .replaceSuggestion, .attachDiacritic, .unicodeInput:
targetComposingText = self.composingText
}
let inputString = targetComposingText.input.map(\.piece).inputString(preferIntention: false)
let composingCount: ComposingCount = .inputCount(targetComposingText.input.count)
let candidateText = transform(inputString)
let candidate = Candidate(
text: candidateText,
value: 0,
composingCount: .inputCount(composingText.input.count),
composingCount: composingCount,
lastMid: 0,
data: [DicdataElement(
word: candidateText,
Expand All @@ -564,6 +662,49 @@ public final class SegmentsManager {
return candidate
}

@MainActor
private func createAdditionalCandidates() -> [CandidatePresentation] {
let candidates: [(candidate: Candidate, annotationText: String?)] = [
(self.getModifiedRomanCandidate(inputState: .selecting) { $0 }, "英数"),
(self.getModifiedRomanCandidate(inputState: .selecting) { $0.applyingTransform(.fullwidthToHalfwidth, reverse: true) ?? $0 }, "全角英数"),
(self.getModifiedRubyCandidate(inputState: .selecting) { $0.toKatakana().applyingTransform(.fullwidthToHalfwidth, reverse: false) ?? $0 }, "半角カナ"),
(self.getModifiedRubyCandidate(inputState: .selecting) { $0.toKatakana() }, "カタカナ"),
(self.getModifiedRubyCandidate(inputState: .selecting) { $0.toHiragana() }, "ひらがな")
]
return candidates.map {
.init(
candidate: $0.candidate,
displayContext: .init(annotationText: $0.annotationText)
)
}
}

@MainActor
private func showAdditionalCandidatesIfNeeded() {
if self.isShowingAdditionalCandidates {
return
}
guard !self.convertTarget.isEmpty else {
self.resetAdditionalCandidates()
return
}
let candidates = self.createAdditionalCandidates()
guard !candidates.isEmpty else {
self.resetAdditionalCandidates()
return
}
self.additionalCandidates = candidates
self.isShowingAdditionalCandidates = true
self.showingAdditionalCandidateCount = 1
}

private func resetAdditionalCandidates() {
self.isShowingAdditionalCandidates = false
self.additionalCandidates = []
self.showingAdditionalCandidateCount = 0
self.isFixingAdditionalCandidateTop = false
}

@MainActor
public func commitMarkedText(inputState: InputState) -> String {
let markedText = self.getCurrentMarkedText(inputState: inputState)
Expand Down
Loading