Skip to content

Commit

Permalink
Merge pull request #75 from qwertyyb/feature/user-dict
Browse files Browse the repository at this point in the history
feat: 支持自定义词库能力
  • Loading branch information
qwertyyb authored Jul 4, 2022
2 parents aecebf9 + e16df5c commit ede5b06
Show file tree
Hide file tree
Showing 12 changed files with 433 additions and 171 deletions.
8 changes: 8 additions & 0 deletions Fire.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/* Begin PBXBuildFile section */
4500AC622869F8CC006F3FCC /* PunctutionPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4500AC612869F8CC006F3FCC /* PunctutionPane.swift */; };
4500AC64286F2B42006F3FCC /* UserDictPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4500AC63286F2B42006F3FCC /* UserDictPane.swift */; };
4500AC68287036CB006F3FCC /* DictManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4500AC67287036CB006F3FCC /* DictManager.swift */; };
450B7D9A26A2847D00808A4D /* ApplicationPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450B7D9926A2847D00808A4D /* ApplicationPane.swift */; };
451E6048232E227B007B0463 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451E6047232E227B007B0463 /* AppDelegate.swift */; };
451E6056232E24A5007B0463 /* FireInputController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451E6055232E24A5007B0463 /* FireInputController.swift */; };
Expand Down Expand Up @@ -78,6 +80,8 @@

/* Begin PBXFileReference section */
4500AC612869F8CC006F3FCC /* PunctutionPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PunctutionPane.swift; sourceTree = "<group>"; };
4500AC63286F2B42006F3FCC /* UserDictPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDictPane.swift; sourceTree = "<group>"; };
4500AC67287036CB006F3FCC /* DictManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictManager.swift; sourceTree = "<group>"; };
450B7D9926A2847D00808A4D /* ApplicationPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPane.swift; sourceTree = "<group>"; };
451E6044232E227B007B0463 /* Fire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Fire.app; sourceTree = BUILT_PRODUCTS_DIR; };
451E6047232E227B007B0463 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -200,6 +204,7 @@
45577EAF254575480064325B /* types.swift */,
45ACDE1A2841C52500658F46 /* Bridging-Header.h */,
453377E42849E76A0064E4F2 /* StatusBar.swift */,
4500AC67287036CB006F3FCC /* DictManager.swift */,
);
path = Fire;
sourceTree = "<group>";
Expand Down Expand Up @@ -250,6 +255,7 @@
45DB6EC827E609CB00A39925 /* ThemePane.swift */,
45EBB54F283A311C00A56CBA /* StatisticsPane.swift */,
4500AC612869F8CC006F3FCC /* PunctutionPane.swift */,
4500AC63286F2B42006F3FCC /* UserDictPane.swift */,
);
path = Preferences;
sourceTree = "<group>";
Expand Down Expand Up @@ -466,6 +472,7 @@
buildActionMask = 2147483647;
files = (
673C417225468FFA00F462A3 /* ModifierKeyUpChecker.swift in Sources */,
4500AC64286F2B42006F3FCC /* UserDictPane.swift in Sources */,
45577EA1254552110064325B /* ThesaurusPane.swift in Sources */,
451E6056232E24A5007B0463 /* FireInputController.swift in Sources */,
451E605F232E400B007B0463 /* Fire.swift in Sources */,
Expand All @@ -483,6 +490,7 @@
45577EB0254575480064325B /* types.swift in Sources */,
45577EB4254576720064325B /* FirePreferencesController.swift in Sources */,
450B7D9A26A2847D00808A4D /* ApplicationPane.swift in Sources */,
4500AC68287036CB006F3FCC /* DictManager.swift in Sources */,
45DCE62226A31F140009FED1 /* ApplicationSettingCache.swift in Sources */,
459DE990232EB26600A3ACD1 /* CandidatesView.swift in Sources */,
45DB6EC727E5B8FE00A39925 /* ThemeConfig.swift in Sources */,
Expand Down
12 changes: 6 additions & 6 deletions Fire/CandidatesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SwiftUI
import Defaults

func getShownCode(candidate: Candidate, origin: String) -> String {
if candidate.type == "py" || !candidate.code.hasPrefix(origin) {
if candidate.type == CandidateType.py || !candidate.code.hasPrefix(origin) {
return "(\(candidate.code))"
}
if candidate.code.hasPrefix(origin) {
Expand Down Expand Up @@ -188,11 +188,11 @@ struct CandidatesView: View {
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
CandidatesView(candidates: [
Candidate(code: "a", text: "", type: "wb"),
Candidate(code: "ab", text: "", type: "wb"),
Candidate(code: "abc", text: "", type: "wb"),
Candidate(code: "abcg", text: "", type: "wb"),
Candidate(code: "addd", text: "", type: "wb")
Candidate(code: "a", text: "", type: CandidateType.wb),
Candidate(code: "ab", text: "", type: CandidateType.wb),
Candidate(code: "abc", text: "", type: CandidateType.wb),
Candidate(code: "abcg", text: "", type: CandidateType.wb),
Candidate(code: "addd", text: "", type: CandidateType.wb)
], origin: "a")
}
}
2 changes: 1 addition & 1 deletion Fire/CandidatesWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class CandidatesWindow: NSWindow, NSWindowDelegate {
print("candidates: \(candidatesData)")
self.setFrameTopLeftPoint(topLeft)
self.orderFront(nil)
NSApp.setActivationPolicy(.prohibited)
// NSApp.setActivationPolicy(.prohibited)
}

override init(
Expand Down
277 changes: 277 additions & 0 deletions Fire/DictManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
//
// DictManager.swift
// Fire
//
// Created by 虚幻 on 2022/7/2.
// Copyright © 2022 qwertyyb. All rights reserved.
//

import Foundation
import Defaults

class DictManager {
static let shared = DictManager()
static let userDictUpdated = Notification.Name("DictManager.userDictUpdated")

let userDictFilePath = NSSearchPathForDirectoriesInDomains(
.applicationSupportDirectory,
.userDomainMask, true).first! + "/" + Bundle.main.bundleIdentifier! + "/user-dict.txt"

private var database: OpaquePointer?
private var queryStatement: OpaquePointer?

private init() {
Defaults.observe(keys: .codeMode, .candidateCount) { () in
self.prepareStatement()
}
.tieToLifetime(of: self)
}
deinit {
close()
}
func reinit() {
close()
prepareStatement()
}
func close() {
queryStatement = nil
sqlite3_close_v2(database)
sqlite3_shutdown()
database = nil
}

private func getStatementSql() -> String {
let candidateCount = Defaults[.candidateCount]
let codeMode = Defaults[.codeMode]
// 比显示的候选词数量多查一个,以此判断有没有下一页
let sql = """
select
\(codeMode == .wubiPinyin ? "max(wbcode)" : "min(wbcode)"),
text,
type, min(query) as query
from wb_py_dict
where query like :query \(
codeMode == .wubi ? "and type = 'wb'"
: codeMode == .pinyin ? "and type = 'py'" : "")
group by text
order by query, id
limit :offset, \(candidateCount + 1)
"""
return sql
}

private func prepareStatement() {
if database == nil {
sqlite3_open_v2(getDatabaseURL().path, &database, SQLITE_OPEN_READWRITE, nil)
}
if queryStatement != nil {
sqlite3_finalize(queryStatement)
queryStatement = nil
}
if sqlite3_prepare_v2(database, getStatementSql(), -1, &queryStatement, nil) == SQLITE_OK {
print("prepare ok")
} else if let err = sqlite3_errmsg(database) {
print("prepare fail: \(err)")
}
}

private func getMinIdFromDictTable() -> Int {
let sql = "select min(id) from wb_py_dict"
var queryStmt: OpaquePointer?
if sqlite3_prepare_v2(database, sql, -1, &queryStmt, nil) == SQLITE_OK {
if sqlite3_step(queryStmt) == SQLITE_ROW {
let minId = sqlite3_column_int(queryStmt, 0)
sqlite3_finalize(queryStmt)
queryStmt = nil
return Int(minId)
}
}
NSLog("[Fire.getMinIdFromDictTable] errmsg: \(String(cString: sqlite3_errmsg(queryStmt)))")
sqlite3_finalize(queryStmt)
queryStmt = nil
return 0
}

private func replaceTextWithVars(_ text: String) -> String {
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy MM dd HH mm ss"
let arr = formatter.string(from: date).split(separator: " ")
let vars: [String: String] = [
"{yyyy}": String(arr[0]),
"{MM}": String(arr[1]),
"{dd}": String(arr[2]),
"{HH}": String(arr[3]),
"{mm}": String(arr[4]),
"{ss}": String(arr[5])
]
var newText = text
vars.forEach { (key, val) in
newText = newText.replacingOccurrences(of: key, with: val)
}
print("[replaceTextWithVars] \(text), \(newText)")
return newText
}

func getCandidates(query: String = String(), page: Int = 1) -> (candidates: [Candidate], hasNext: Bool) {
if query.count <= 0 {
return ([], false)
}
NSLog("get local candidate, origin: \(query), query: ", query)
var candidates: [Candidate] = []
sqlite3_reset(queryStatement)
sqlite3_clear_bindings(queryStatement)
sqlite3_bind_text(queryStatement,
sqlite3_bind_parameter_index(queryStatement, ":code"),
query, -1,
SQLITE_TRANSIENT
)
sqlite3_bind_text(queryStatement,
sqlite3_bind_parameter_index(queryStatement, ":query"),
"\(query)%", -1,
SQLITE_TRANSIENT
)
sqlite3_bind_int(queryStatement,
sqlite3_bind_parameter_index(queryStatement, ":offset"),
Int32((page - 1) * Defaults[.candidateCount])
)
while sqlite3_step(queryStatement) == SQLITE_ROW {
let code = String.init(cString: sqlite3_column_text(queryStatement, 0))
var text = String.init(cString: sqlite3_column_text(queryStatement, 1))
let type = CandidateType(rawValue: String.init(cString: sqlite3_column_text(queryStatement, 2)))!
if type == .user {
text = replaceTextWithVars(text)
}
let candidate = Candidate(code: code, text: text, type: type)
candidates.append(candidate)
}
let count = Defaults[.candidateCount]
let allCount = candidates.count
candidates = Array(candidates.prefix(count))

if candidates.isEmpty {
candidates.append(Candidate(code: query, text: query, type: CandidateType.placeholder))
}
return (candidates, hasNext: allCount > count)
}

func setCandidateToFirst(query: String, candidate: Candidate) {
let newCandidate = Candidate(code: query, text: candidate.text, type: CandidateType.user)
_ = prependCandidate(candidate: newCandidate)
NotificationQueue.default.enqueue(Notification(name: DictManager.userDictUpdated), postingStyle: .whenIdle)
}

func prependCandidate(candidate: Candidate) -> Bool {
let sql = """
insert into wb_py_dict(id, wbcode, text, type, query)
values (
(select MIN(id) - 1 from wb_py_dict), :code, :text, :type, :code
);
"""
var insertStatement: OpaquePointer?
if sqlite3_prepare_v2(database, sql, -1, &insertStatement, nil) == SQLITE_OK {
sqlite3_bind_text(insertStatement,
sqlite3_bind_parameter_index(insertStatement, ":code"),
candidate.code, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(insertStatement,
sqlite3_bind_parameter_index(insertStatement, ":text"),
candidate.text, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(insertStatement,
sqlite3_bind_parameter_index(insertStatement, ":type"),
CandidateType.user.rawValue, -1, SQLITE_TRANSIENT)
if sqlite3_step(insertStatement) == SQLITE_DONE {
sqlite3_finalize(insertStatement)
insertStatement = nil
return true
}
}
sqlite3_finalize(insertStatement)
insertStatement = nil
print("errmsg: \(String(cString: sqlite3_errmsg(database)!))")
return false
}

func prependCandidates(candidates: [Candidate]) {
if candidates.count <= 0 {
return
}
// 2.1 先获取最小id
let minId = getMinIdFromDictTable()
// 2.2 添加对应id
let values = candidates.enumerated().map { (n, candidate) in
"(\(minId - candidates.count + n), '\(candidate.code)', '\(candidate.text)', '\(candidate.type)', '\(candidate.code)')"
}.joined(separator: ",")
let sql = """
insert into wb_py_dict(id, wbcode, text, type, query)
values \(values)
"""
sqlite3_exec(database, sql, nil, nil, nil)
}

func updateUserDict(_ dictContent: String) {
// 1. 先删除之前的用户词库
sqlite3_exec(database, "delete from wb_py_dict where type = '\(CandidateType.user.rawValue)'", nil, nil, nil)
// 2. 添加用户词库
let lines = dictContent.split(whereSeparator: \.isNewline)
let candidates = lines.map { (line) -> [Candidate] in
let strs = line.split(whereSeparator: \.isWhitespace)
if strs.count <= 1 {
return []
}
let code = String(strs.first!)
let candidateTexts = strs[1...]
return candidateTexts.map { text in
Candidate(code: code, text: String(text), type: CandidateType.user)
}
}.reduce([] as [Candidate]) { partialResult, cur in
partialResult + cur
}
prependCandidates(candidates: candidates)
NotificationQueue.default.enqueue(Notification(name: DictManager.userDictUpdated), postingStyle: .whenIdle)
}

func getUserCandidates() -> [Candidate] {
var stmt: OpaquePointer?
let sql = "select query, text from wb_py_dict where type = '\(CandidateType.user.rawValue)'"
if sqlite3_prepare_v2(database, sql, -1, &stmt, nil) == SQLITE_OK {
var candidates: [Candidate] = []
while sqlite3_step(stmt) == SQLITE_ROW {
let code = String(cString: sqlite3_column_text(stmt, 0))
let text = String(cString: sqlite3_column_text(stmt, 1))
candidates.append(Candidate(code: code, text: text, type: .user))
}
sqlite3_finalize(stmt)
stmt = nil
return candidates
}
sqlite3_finalize(stmt)
stmt = nil
return []
}

func getUserDictContent() -> String {
// 获取用户候选词(包括调整顺序的词)
struct UserDictLine {
let code: String
var texts: [String]
}
let candidates = getUserCandidates()
NSLog("[DictManager.exportUserDictToFile] candidates: \(candidates)")
var list: [UserDictLine] = []
candidates.forEach { candidate in
let index = list.firstIndex { dictItem in
dictItem.code == candidate.code
}
if index == nil {
list.append(UserDictLine(code: candidate.code, texts: [candidate.text]))
} else if !list[index!].texts.contains(candidate.text) {
list[index!].texts.append(candidate.text)
}
}
let content = list.map { dictItem in
([dictItem.code] + dictItem.texts).joined(separator: "\t")
}
.joined(separator: "\n")
return content
}
}
Loading

0 comments on commit ede5b06

Please sign in to comment.