Skip to content

Commit c08ae57

Browse files
committed
refactor converter process interfaces
1 parent 02c7ccf commit c08ae57

15 files changed

Lines changed: 1476 additions & 1058 deletions
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
import Core
2+
import Foundation
3+
import KanaKanjiConverterModuleWithDefaultDictionary
4+
5+
extension ConverterServer {
6+
@MainActor
7+
func handleKeyEvent(
8+
sessionID: String,
9+
request: ConverterKeyEventRequest
10+
) throws -> ConverterServerResponse {
11+
let session = try getSession(sessionID)
12+
session.setLeftSideContext(request.leftSideContext)
13+
Config.DebugPredictiveTyping().value = request.enablePredictiveTyping
14+
Config.DebugTypoCorrection().value = request.enableTypoCorrection
15+
16+
if request.enableOptionDirectFullWidthInput,
17+
let text = OptionDirectInputResolver.resolve(
18+
characters: request.optionDirectInputText,
19+
modifierFlags: request.event.modifierFlags,
20+
inputLanguage: request.inputLanguage,
21+
inputState: request.inputState.inputState,
22+
typeBackSlash: request.typeBackSlash
23+
) {
24+
return ConverterServerResponse(
25+
effects: [.insertText(text)],
26+
inputState: request.inputState,
27+
inputLanguage: request.inputLanguage,
28+
snapshot: snapshot(for: session, inputState: request.inputState.inputState)
29+
)
30+
}
31+
32+
let userAction = UserAction.getUserAction(
33+
eventCore: request.event,
34+
inputLanguage: request.inputLanguage,
35+
typeBackSlash: request.typeBackSlash
36+
)
37+
let (clientAction, clientActionCallback) = request.inputState.inputState.event(
38+
eventCore: request.event,
39+
userAction: userAction,
40+
inputLanguage: request.inputLanguage,
41+
liveConversionEnabled: request.liveConversionEnabled,
42+
enableDebugWindow: request.enableDebugWindow,
43+
enableSuggestion: request.enableSuggestion
44+
)
45+
46+
var effects: [ConverterClientEffect] = []
47+
var inputLanguage = request.inputLanguage
48+
let actionHandled = perform(
49+
clientAction,
50+
request: request,
51+
session: session,
52+
inputLanguage: &inputLanguage,
53+
effects: &effects
54+
)
55+
guard actionHandled else {
56+
return ConverterServerResponse(
57+
handled: false,
58+
effects: effects,
59+
inputState: request.inputState,
60+
inputLanguage: inputLanguage,
61+
snapshot: snapshot(for: session, inputState: request.inputState.inputState)
62+
)
63+
}
64+
65+
let nextInputState = apply(
66+
clientActionCallback,
67+
currentInputState: request.inputState.inputState,
68+
compositionIsEmpty: session.manager.isEmpty
69+
)
70+
return ConverterServerResponse(
71+
handled: !effects.contains(.fallthroughToApplication),
72+
effects: effects,
73+
inputState: ConverterInputState(nextInputState),
74+
inputLanguage: inputLanguage,
75+
snapshot: snapshot(for: session, inputState: nextInputState)
76+
)
77+
}
78+
79+
@MainActor
80+
// swiftlint:disable:next cyclomatic_complexity function_body_length
81+
func perform(
82+
_ action: ClientAction,
83+
request: ConverterKeyEventRequest,
84+
session: ConverterSession,
85+
inputLanguage: inout InputLanguage,
86+
effects: inout [ConverterClientEffect]
87+
) -> Bool {
88+
let manager = session.manager
89+
let inputState = request.inputState.inputState
90+
let inputStyle = Self.resolveInputStyle(request.inputLanguage == .english ? .direct : request.inputStyle)
91+
switch action {
92+
case .consume:
93+
return true
94+
case .fallthrough:
95+
effects.append(.fallthroughToApplication)
96+
return true
97+
case .showCandidateWindow:
98+
manager.requestSetCandidateWindowState(visible: true)
99+
case .hideCandidateWindow:
100+
manager.requestSetCandidateWindowState(visible: false)
101+
case .appendToMarkedText(let text):
102+
manager.insertAtCursorPosition(text, inputStyle: inputStyle)
103+
case .appendPieceToMarkedText(let pieces):
104+
manager.insertAtCursorPosition(pieces: pieces, inputStyle: inputStyle)
105+
case .insertWithoutMarkedText(let text):
106+
effects.append(.insertText(text))
107+
case .removeLastMarkedText:
108+
manager.deleteBackwardFromCursorPosition()
109+
manager.requestResettingSelection()
110+
case .commitMarkedText:
111+
let text = manager.commitMarkedText(inputState: inputState)
112+
if !text.isEmpty {
113+
effects.append(.insertText(text))
114+
}
115+
case .editSegment(let count):
116+
manager.editSegment(count: count)
117+
case .enterFirstCandidatePreviewMode:
118+
manager.insertCompositionSeparator(inputStyle: inputStyle, skipUpdate: false)
119+
manager.requestSetCandidateWindowState(visible: false)
120+
case .enterCandidateSelectionMode:
121+
manager.insertCompositionSeparator(inputStyle: inputStyle, skipUpdate: true)
122+
manager.update(requestRichCandidates: true)
123+
case .submitSelectedCandidate:
124+
submitSelectedCandidate(manager: manager, leftSideContext: request.leftSideContext, effects: &effects)
125+
case .selectNextCandidate:
126+
manager.requestSelectingNextCandidate()
127+
case .selectPrevCandidate:
128+
manager.requestSelectingPrevCandidate()
129+
case .selectNumberCandidate(let number):
130+
manager.requestSelectingRow(request.visibleCandidateStartIndex + number - 1)
131+
submitSelectedCandidate(manager: manager, leftSideContext: request.leftSideContext, effects: &effects)
132+
manager.requestResettingSelection()
133+
case .selectInputLanguage(let language):
134+
inputLanguage = language
135+
effects.append(.switchInputLanguage(language))
136+
case .commitMarkedTextAndSelectInputLanguage(let language):
137+
let text = manager.commitMarkedText(inputState: inputState)
138+
if !text.isEmpty {
139+
effects.append(.insertText(text))
140+
}
141+
inputLanguage = language
142+
effects.append(.switchInputLanguage(language))
143+
case .commitMarkedTextAndAppendToMarkedText(let text):
144+
commitMarkedTextAndContinue(
145+
manager: manager,
146+
inputState: inputState,
147+
effects: &effects
148+
)
149+
manager.insertAtCursorPosition(text, inputStyle: inputStyle)
150+
case .commitMarkedTextAndAppendPieceToMarkedText(let pieces):
151+
commitMarkedTextAndContinue(
152+
manager: manager,
153+
inputState: inputState,
154+
effects: &effects
155+
)
156+
manager.insertAtCursorPosition(pieces: pieces, inputStyle: inputStyle)
157+
case .enableDebugWindow:
158+
manager.requestDebugWindowMode(enabled: true)
159+
case .disableDebugWindow:
160+
manager.requestDebugWindowMode(enabled: false)
161+
case .forgetMemory:
162+
manager.forgetMemory()
163+
case .submitKatakanaCandidate:
164+
submitTransformedCandidate(.katakana, manager: manager, inputState: inputState, leftSideContext: request.leftSideContext, effects: &effects)
165+
case .submitHiraganaCandidate:
166+
submitTransformedCandidate(.hiragana, manager: manager, inputState: inputState, leftSideContext: request.leftSideContext, effects: &effects)
167+
case .submitHankakuKatakanaCandidate:
168+
submitTransformedCandidate(.halfWidthKatakana, manager: manager, inputState: inputState, leftSideContext: request.leftSideContext, effects: &effects)
169+
case .submitFullWidthRomanCandidate:
170+
submitTransformedCandidate(.fullWidthRoman, manager: manager, inputState: inputState, leftSideContext: request.leftSideContext, effects: &effects)
171+
case .submitHalfWidthRomanCandidate:
172+
submitTransformedCandidate(.halfWidthRoman, manager: manager, inputState: inputState, leftSideContext: request.leftSideContext, effects: &effects)
173+
case .requestPredictiveSuggestion:
174+
manager.insertAtCursorPosition("つづき", inputStyle: inputStyle)
175+
effects.append(.requestReplaceSuggestion)
176+
case .acceptPredictionCandidate:
177+
acceptPredictionCandidate(manager: manager, leftSideContext: request.leftSideContext)
178+
case .requestReplaceSuggestion:
179+
session.clearReplaceSuggestions()
180+
effects.append(.requestReplaceSuggestion)
181+
case .selectNextReplaceSuggestionCandidate:
182+
session.selectNextReplaceSuggestion()
183+
case .selectPrevReplaceSuggestionCandidate:
184+
session.selectPreviousReplaceSuggestion()
185+
case .submitReplaceSuggestionCandidate:
186+
_ = submitSelectedReplaceSuggestion(session: session, effects: &effects)
187+
case .hideReplaceSuggestionWindow:
188+
session.clearReplaceSuggestions()
189+
effects.append(.hideReplaceSuggestionWindow)
190+
case .showPromptInputWindow:
191+
effects.append(.showPromptInputWindow)
192+
case .transformSelectedText(let selectedText, let prompt):
193+
effects.append(.transformSelectedText(selectedText, prompt))
194+
case .enterUnicodeInputMode, .appendToUnicodeInput, .removeLastUnicodeInput, .cancelUnicodeInput:
195+
return true
196+
case .submitUnicodeInput(let codePoint):
197+
if let scalar = UInt32(codePoint, radix: 16), let unicodeScalar = Unicode.Scalar(scalar) {
198+
effects.append(.insertText(String(Character(unicodeScalar))))
199+
}
200+
case .submitSelectedCandidateAndEnterUnicodeInputMode:
201+
submitSelectedCandidate(manager: manager, leftSideContext: request.leftSideContext, effects: &effects)
202+
if !manager.isEmpty {
203+
effects.append(.insertText(manager.convertTarget))
204+
manager.stopComposition()
205+
}
206+
case .stopComposition:
207+
manager.stopComposition()
208+
}
209+
return true
210+
}
211+
212+
@MainActor
213+
func apply(
214+
_ callback: ClientActionCallback,
215+
currentInputState: InputState,
216+
compositionIsEmpty: Bool
217+
) -> InputState {
218+
switch callback {
219+
case .fallthrough:
220+
return currentInputState
221+
case .transition(let inputState):
222+
return inputState
223+
case .basedOnBackspace(let ifIsEmpty, let ifIsNotEmpty),
224+
.basedOnSubmitCandidate(let ifIsEmpty, let ifIsNotEmpty):
225+
return compositionIsEmpty ? ifIsEmpty : ifIsNotEmpty
226+
}
227+
}
228+
229+
@MainActor
230+
func commitMarkedTextAndContinue(
231+
manager: SegmentsManager,
232+
inputState: InputState,
233+
effects: inout [ConverterClientEffect]
234+
) {
235+
let text = manager.commitMarkedText(inputState: inputState)
236+
if !text.isEmpty {
237+
effects.append(.insertText(text))
238+
}
239+
}
240+
241+
@MainActor
242+
func submitSelectedCandidate(
243+
manager: SegmentsManager,
244+
leftSideContext: String?,
245+
effects: inout [ConverterClientEffect]
246+
) {
247+
guard let candidate = manager.selectedCandidate else {
248+
return
249+
}
250+
manager.prefixCandidateCommited(candidate, leftSideContext: leftSideContext ?? "")
251+
effects.append(.insertText(candidate.text))
252+
}
253+
254+
@MainActor
255+
func submitTransformedCandidate(
256+
_ transform: ConverterCandidateTransform,
257+
manager: SegmentsManager,
258+
inputState: InputState,
259+
leftSideContext: String?,
260+
effects: inout [ConverterClientEffect]
261+
) {
262+
let candidate = Self.transformedCandidate(transform, manager: manager, inputState: inputState)
263+
manager.prefixCandidateCommited(candidate, leftSideContext: leftSideContext ?? "")
264+
effects.append(.insertText(candidate.text))
265+
}
266+
267+
@MainActor
268+
func requestReplaceSuggestion(
269+
session: ConverterSession,
270+
leftSideContext: String?
271+
) async throws {
272+
session.clearReplaceSuggestions()
273+
guard !session.manager.isEmpty else {
274+
return
275+
}
276+
let backend: AIBackend
277+
switch session.config.aiBackendPreference {
278+
case .off:
279+
return
280+
case .foundationModels:
281+
backend = .foundationModels
282+
case .openAI:
283+
backend = .openAI
284+
}
285+
let composingText = session.manager.convertTarget
286+
let prompt = session.config.includeContextInAITransform ? (leftSideContext ?? "") : ""
287+
let request = OpenAIRequest(
288+
prompt: prompt,
289+
target: composingText,
290+
modelName: session.config.openAIModelName.isEmpty ? Config.OpenAiModelName.default : session.config.openAIModelName
291+
)
292+
let predictions = try await AIClient.sendRequest(
293+
request,
294+
backend: backend,
295+
apiKey: session.config.openAIAPIKey.value,
296+
apiEndpoint: session.config.openAIEndpoint.isEmpty ? Config.OpenAiApiEndpoint.default : session.config.openAIEndpoint
297+
)
298+
guard session.manager.convertTarget == composingText else {
299+
return
300+
}
301+
session.replaceSuggestions = predictions.map { text in
302+
Candidate(
303+
text: text,
304+
value: PValue(0),
305+
composingCount: .surfaceCount(composingText.count),
306+
lastMid: 0,
307+
data: [],
308+
actions: [],
309+
inputable: true
310+
)
311+
}
312+
if !session.replaceSuggestions.isEmpty {
313+
session.replaceSuggestionSelectionIndex = 0
314+
}
315+
}
316+
317+
@MainActor
318+
func submitSelectedReplaceSuggestion(
319+
session: ConverterSession,
320+
effects: inout [ConverterClientEffect]
321+
) -> Bool {
322+
guard let candidate = session.selectedReplaceSuggestion else {
323+
return false
324+
}
325+
effects.append(.insertText(candidate.text))
326+
session.manager.stopComposition()
327+
session.clearReplaceSuggestions()
328+
return true
329+
}
330+
331+
@MainActor
332+
func acceptPredictionCandidate(manager: SegmentsManager, leftSideContext _: String?) {
333+
let prediction = SegmentsManager.preferredPredictionCandidates(
334+
typoCorrectionCandidates: manager.requestTypoCorrectionPredictionCandidates(),
335+
predictionCandidates: manager.requestPredictionCandidates()
336+
).first
337+
guard let prediction else {
338+
return
339+
}
340+
if prediction.deleteCount > 0 {
341+
manager.deleteBackwardFromCursorPosition(count: prediction.deleteCount)
342+
}
343+
guard !prediction.appendText.isEmpty else {
344+
return
345+
}
346+
manager.insertAtCursorPosition(prediction.appendText, inputStyle: .direct)
347+
}
348+
}

0 commit comments

Comments
 (0)