merge: bring pinyin habit ranking into English keyboard branch
Keep English QuickType/mmap and Chinese schema hygiene on one branch. Secure fields skip Rime; English still loads its lexicon only while English is active.
This commit is contained in:
@@ -35,6 +35,10 @@ public final class EnglishLearningStore: @unchecked Sendable {
|
||||
(defaults.dictionary(forKey: Self.defaultsKey) as? [String: Int]) ?? [:]
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
defaults.removeObject(forKey: Self.defaultsKey)
|
||||
}
|
||||
|
||||
private func mutate(word: String, delta: Int) {
|
||||
let key = word.lowercased()
|
||||
guard !key.isEmpty else { return }
|
||||
|
||||
@@ -58,7 +58,8 @@ public final class LibrimeEngine: RimeEngineBridging {
|
||||
|
||||
public func teardown() {
|
||||
bridge?.clearComposition()
|
||||
bridge?.stopSession()
|
||||
// Session destroy alone does not flush LevelDB user dictionaries.
|
||||
bridge?.finalizeRuntime()
|
||||
bridge = nil
|
||||
composition = .empty
|
||||
isReady = false
|
||||
|
||||
@@ -72,7 +72,7 @@ public struct RimeResourcePaths: Sendable {
|
||||
public actor RimeResourceInstaller {
|
||||
public static let shared = RimeResourceInstaller()
|
||||
/// Bump when SharedSupport layout / schema / import_tables contract changes.
|
||||
public static let resourceVersion = "2.3.0"
|
||||
public static let resourceVersion = "2.4.0"
|
||||
|
||||
public init() {}
|
||||
|
||||
@@ -231,6 +231,28 @@ public actor RimeResourceInstaller {
|
||||
// deployments and is intentionally not needed here.
|
||||
bridge.finalizeRuntime()
|
||||
}
|
||||
|
||||
/// Deletes librime user dictionaries under `UserData`, keeping `build/`
|
||||
/// so `isReady` stays true. Host-only: the keyboard must not race LevelDB.
|
||||
public func clearUserDictionary() throws {
|
||||
guard Self.canDeployInCurrentProcess else {
|
||||
throw RimeResourceError.hostAppRequired
|
||||
}
|
||||
let paths = try RimeResourcePaths.resolve()
|
||||
try Self.removeUserDictionaries(in: paths.userData)
|
||||
}
|
||||
|
||||
/// Testable file-level wipe. Matches LevelDB folders like `osg_pinyin.userdb`.
|
||||
nonisolated public static func removeUserDictionaries(
|
||||
in userData: URL,
|
||||
fileManager: FileManager = .default
|
||||
) throws {
|
||||
guard fileManager.fileExists(atPath: userData.path) else { return }
|
||||
let names = try fileManager.contentsOfDirectory(atPath: userData.path)
|
||||
for name in names where name.lowercased().contains("userdb") {
|
||||
try fileManager.removeItem(at: userData.appendingPathComponent(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RimeResourceInstaller {
|
||||
|
||||
@@ -43,7 +43,9 @@ public enum RimeSchemaGenerator {
|
||||
let alphabet = inputSchema == .fullPinyin
|
||||
? "zyxwvutsrqponmlkjihgfedcba"
|
||||
: "zyxwvutsrqponmlkjihgfedcba;"
|
||||
let algebra = fuzzyRules(fuzzyPairs) + algebraRules(for: inputSchema)
|
||||
// Dialect single-letter syllables must be erased before fuzzy/abbrev
|
||||
// so `n/l` cannot revive 嗯 as `l`, and `wom` cannot exact-match 我呒.
|
||||
let algebra = dialectEraseRules + fuzzyRules(fuzzyPairs) + algebraRules(for: inputSchema)
|
||||
let algebraYAML = algebra.map { " - '\($0)'" }.joined(separator: "\n")
|
||||
|
||||
return """
|
||||
@@ -112,6 +114,16 @@ public enum RimeSchemaGenerator {
|
||||
"""
|
||||
}
|
||||
|
||||
/// Drop Wu/dialect exact spellings (`呒 m`, `嗯 n/ng`, `噷 hm`) before
|
||||
/// first-letter abbrev, matching rime-pinyin-simp. Keep the dictionary
|
||||
/// rows; 嗯 remains reachable as `en`, 呒 as `mu`.
|
||||
public static let dialectEraseRules: [String] = [
|
||||
"erase/^hm$/",
|
||||
"erase/^m$/",
|
||||
"erase/^n$/",
|
||||
"erase/^ng$/"
|
||||
]
|
||||
|
||||
/// Rules run against full-pinyin dictionary codes before double-pinyin
|
||||
/// transforms, so fuzzy pairs work consistently in all three schemas.
|
||||
public static func fuzzyRules(_ enabled: Set<PinyinFuzzyPair>) -> [String] {
|
||||
@@ -144,7 +156,9 @@ public enum RimeSchemaGenerator {
|
||||
case .fullPinyin:
|
||||
return [
|
||||
"derive/^([jqxy])u$/$1v/",
|
||||
"abbrev/^([a-z]).+$/$1/"
|
||||
"abbrev/^([a-z]).+$/$1/",
|
||||
// Two-letter initials; do not add this to double pinyin.
|
||||
"abbrev/^([zcs]h).+$/$1/"
|
||||
]
|
||||
|
||||
case .microsoftDoublePinyin, .sogouDoublePinyin:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// TypingHabitStore.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Cross-language "forget" for implicit typing habits. Ranking stays
|
||||
// language-specific (EnglishLearningStore vs librime userdb).
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum TypingHabitStore {
|
||||
/// Clears English boosts and Chinese Rime user dictionaries.
|
||||
/// Does not touch PersonalDictionary / osg_personal.
|
||||
public static func clearAll(
|
||||
englishStore: EnglishLearningStore = EnglishLearningStore()
|
||||
) async throws {
|
||||
englishStore.clear()
|
||||
try await RimeResourceInstaller.shared.clearUserDictionary()
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,13 @@ public final class TypingSessionController: ObservableObject {
|
||||
@Published public private(set) var lastErrorNeedsHostDeployment: Bool = false
|
||||
|
||||
/// When true, English suggestions / autocorrect stay off (secure fields).
|
||||
@Published public var suggestionsEnabled: Bool = true
|
||||
/// Chinese composition is also skipped so passwords never enter Rime userdb.
|
||||
@Published public var suggestionsEnabled: Bool = true {
|
||||
didSet {
|
||||
guard oldValue, !suggestionsEnabled else { return }
|
||||
abandonChineseComposition()
|
||||
}
|
||||
}
|
||||
/// `UITextChecker` completions / guesses. Empty in unit tests.
|
||||
public var systemLexicon: EnglishSystemLexiconProviding = EmptyEnglishSystemLexicon()
|
||||
/// Names and text replacements from `requestSupplementaryLexicon`.
|
||||
@@ -317,6 +323,12 @@ public final class TypingSessionController: ObservableObject {
|
||||
return handleEnglishCharacter(ch)
|
||||
}
|
||||
|
||||
if !suggestionsEnabled {
|
||||
clearOneShotShiftIfNeeded()
|
||||
abandonChineseComposition()
|
||||
return .insert(String(ch))
|
||||
}
|
||||
|
||||
// Chinese + Shift: insert Latin directly (iOS-style mix-in), leave Rime
|
||||
// composition untouched. Rime's alphabet is lowercase-only, so uppercase
|
||||
// keycodes would otherwise be rejected with no output.
|
||||
@@ -338,6 +350,10 @@ public final class TypingSessionController: ObservableObject {
|
||||
return handleEnglishSpace()
|
||||
}
|
||||
clearPeriodShortcut()
|
||||
if !suggestionsEnabled {
|
||||
abandonChineseComposition()
|
||||
return .insert(" ")
|
||||
}
|
||||
let text = engine.processSpace() ?? " "
|
||||
composition = engine.composition
|
||||
syncCandidatePanelVisibility()
|
||||
@@ -349,6 +365,10 @@ public final class TypingSessionController: ObservableObject {
|
||||
if language == .english {
|
||||
return commitEnglishWord(suffix: "\n")
|
||||
}
|
||||
if !suggestionsEnabled {
|
||||
abandonChineseComposition()
|
||||
return .insert("\n")
|
||||
}
|
||||
let text = engine.processReturn() ?? "\n"
|
||||
composition = engine.composition
|
||||
syncCandidatePanelVisibility()
|
||||
@@ -360,6 +380,9 @@ public final class TypingSessionController: ObservableObject {
|
||||
if language == .english {
|
||||
return selectEnglishCandidate(at: index)
|
||||
}
|
||||
if !suggestionsEnabled {
|
||||
return .none
|
||||
}
|
||||
guard composition.candidates.indices.contains(index) else { return .none }
|
||||
// Display order may put phrases before first-syllable chars; select by engine index.
|
||||
let engineIndex = composition.candidates[index].engineIndex
|
||||
@@ -371,6 +394,13 @@ public final class TypingSessionController: ObservableObject {
|
||||
return text.isEmpty ? .none : .insert(text)
|
||||
}
|
||||
|
||||
/// Drop in-flight pinyin so secure fields cannot commit into userdb.
|
||||
private func abandonChineseComposition() {
|
||||
engineStorage?.clearComposition()
|
||||
composition = .empty
|
||||
isCandidatePanelExpanded = false
|
||||
}
|
||||
|
||||
// MARK: - English
|
||||
|
||||
private func handleEnglishSpace() -> TypingOutput {
|
||||
|
||||
Reference in New Issue
Block a user